mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(quick-capture): auto-fill metadata from active filter state
This commit is contained in:
parent
54285bdee3
commit
bb79032aa9
3 changed files with 297 additions and 4 deletions
|
|
@ -19,6 +19,8 @@ import {
|
|||
} from "@/utils/date/date-utils";
|
||||
import { SuggestManager } from "@/components/ui/suggest";
|
||||
import { EmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
|
||||
import { extractMetadataFromFilter } from "../../task/filter/filter-metadata-extractor";
|
||||
import type { RootFilterState } from "../../task/filter/ViewTaskFilter";
|
||||
|
||||
/**
|
||||
* Quick capture save strategy types
|
||||
|
|
@ -102,11 +104,39 @@ export abstract class BaseQuickCaptureModal extends Modal {
|
|||
// Initialize suggest manager
|
||||
this.suggestManager = new SuggestManager(app, plugin);
|
||||
|
||||
// Initialize metadata
|
||||
// Initialize metadata with filter-based pre-filling
|
||||
let finalMetadata: TaskMetadata = {};
|
||||
|
||||
// 1. Extract metadata from global filter state (if active)
|
||||
const globalFilterState = this.app.loadLocalStorage(
|
||||
"task-genius-view-filter",
|
||||
) as RootFilterState | null;
|
||||
if (globalFilterState) {
|
||||
const extractedFilterMetadata =
|
||||
extractMetadataFromFilter(globalFilterState);
|
||||
finalMetadata = { ...extractedFilterMetadata };
|
||||
}
|
||||
|
||||
// 2. Merge with incoming metadata (incoming has higher priority)
|
||||
if (metadata) {
|
||||
this.taskMetadata = this.normalizeMetadataDates(metadata);
|
||||
const extractedTags = finalMetadata.tags || [];
|
||||
const incomingTags = metadata.tags || [];
|
||||
|
||||
finalMetadata = { ...finalMetadata, ...metadata };
|
||||
|
||||
// Special handling for tags: merge and deduplicate
|
||||
if (extractedTags.length > 0 || incomingTags.length > 0) {
|
||||
finalMetadata.tags = [
|
||||
...new Set([...extractedTags, ...incomingTags]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Apply final metadata
|
||||
if (Object.keys(finalMetadata).length > 0) {
|
||||
this.taskMetadata = this.normalizeMetadataDates(finalMetadata);
|
||||
// Auto-switch to file mode if location is file
|
||||
if (metadata.location === "file") {
|
||||
if (finalMetadata.location === "file") {
|
||||
this.currentMode = "file";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
257
src/components/features/task/filter/filter-metadata-extractor.ts
Normal file
257
src/components/features/task/filter/filter-metadata-extractor.ts
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
/**
|
||||
* Filter Metadata Extractor
|
||||
*
|
||||
* Extracts task metadata from filter state for auto-filling when creating new tasks.
|
||||
* This enables context-aware task creation based on active filters.
|
||||
*/
|
||||
|
||||
import { moment } from "obsidian";
|
||||
import type { RootFilterState, FilterGroup, Filter } from "./ViewTaskFilter";
|
||||
import type { TaskMetadata } from "../../quick-capture/modals/BaseQuickCaptureModal";
|
||||
|
||||
/**
|
||||
* Extracts task metadata from the current filter state.
|
||||
*
|
||||
* Strategy:
|
||||
* - For `rootCondition === 'all'`: Merge metadata from all filter groups (union)
|
||||
* - For `rootCondition === 'any'`: Only extract metadata that is common across all groups (intersection)
|
||||
* - For `rootCondition === 'none'`: Return empty metadata (not suitable for pre-filling)
|
||||
*
|
||||
* @param filterState - The current root filter state
|
||||
* @returns TaskMetadata object with extracted values
|
||||
*/
|
||||
export function extractMetadataFromFilter(
|
||||
filterState: RootFilterState | null | undefined,
|
||||
): TaskMetadata {
|
||||
const metadata: TaskMetadata = {};
|
||||
|
||||
if (
|
||||
!filterState ||
|
||||
!filterState.filterGroups ||
|
||||
filterState.filterGroups.length === 0
|
||||
) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
// 'none' condition is not suitable for pre-filling
|
||||
if (filterState.rootCondition === "none") {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
if (filterState.rootCondition === "all") {
|
||||
// Strategy: Union/Merge
|
||||
// For single-value fields, later groups override earlier ones (Last Write Wins)
|
||||
// For multi-value fields (tags), merge all occurrences
|
||||
filterState.filterGroups.forEach((group) => {
|
||||
const groupMeta = extractFromGroup(group);
|
||||
mergeMetadata(metadata, groupMeta);
|
||||
});
|
||||
} else if (filterState.rootCondition === "any") {
|
||||
// Strategy: Intersection
|
||||
// Only extract metadata that is common across ALL filter groups
|
||||
const allGroupsMeta = filterState.filterGroups.map(extractFromGroup);
|
||||
|
||||
if (allGroupsMeta.length > 0) {
|
||||
const potential = allGroupsMeta[0];
|
||||
|
||||
// Check single-value fields
|
||||
const singleFields: (keyof TaskMetadata)[] = [
|
||||
"priority",
|
||||
"project",
|
||||
"status",
|
||||
"context",
|
||||
];
|
||||
|
||||
singleFields.forEach((field) => {
|
||||
const val = potential[field];
|
||||
if (val !== undefined) {
|
||||
const allMatch = allGroupsMeta.every(
|
||||
(g) => g[field] === val,
|
||||
);
|
||||
if (allMatch) {
|
||||
(metadata as Record<string, unknown>)[field] = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check date fields (compare timestamps)
|
||||
const dateFields: (keyof TaskMetadata)[] = [
|
||||
"startDate",
|
||||
"dueDate",
|
||||
"scheduledDate",
|
||||
];
|
||||
dateFields.forEach((field) => {
|
||||
const val = potential[field];
|
||||
if (val instanceof Date) {
|
||||
const allMatch = allGroupsMeta.every((g) => {
|
||||
const gVal = g[field];
|
||||
return (
|
||||
gVal instanceof Date &&
|
||||
gVal.getTime() === val.getTime()
|
||||
);
|
||||
});
|
||||
if (allMatch) {
|
||||
(metadata as Record<string, unknown>)[field] = val;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check tags (array intersection)
|
||||
if (potential.tags && potential.tags.length > 0) {
|
||||
const commonTags = potential.tags.filter((tag) => {
|
||||
return allGroupsMeta.every(
|
||||
(g) => g.tags && g.tags.includes(tag),
|
||||
);
|
||||
});
|
||||
if (commonTags.length > 0) {
|
||||
metadata.tags = commonTags;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts metadata from a single filter group.
|
||||
* Only processes groups with `groupCondition === 'all'` for reliable extraction.
|
||||
*/
|
||||
function extractFromGroup(group: FilterGroup): TaskMetadata {
|
||||
const groupMeta: TaskMetadata = {};
|
||||
|
||||
// Only extract from 'all' (AND) groups for reliable pre-filling
|
||||
if (group.groupCondition !== "all") {
|
||||
return groupMeta;
|
||||
}
|
||||
|
||||
group.filters.forEach((filter) => {
|
||||
extractFromFilter(filter, groupMeta);
|
||||
});
|
||||
|
||||
return groupMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts metadata from a single filter condition.
|
||||
* Only extracts from conditions that provide definite values (e.g., 'is', 'contains').
|
||||
*/
|
||||
function extractFromFilter(filter: Filter, metadata: TaskMetadata): void {
|
||||
if (!filter.property || !filter.condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const val = filter.value;
|
||||
// Ignore empty values
|
||||
if (val === undefined || val === "" || val === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (filter.property) {
|
||||
case "priority":
|
||||
if (filter.condition === "is") {
|
||||
const parsed = parseInt(val, 10);
|
||||
if (!isNaN(parsed)) {
|
||||
metadata.priority = parsed;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "project":
|
||||
if (filter.condition === "is") {
|
||||
metadata.project = String(val).trim();
|
||||
}
|
||||
break;
|
||||
|
||||
case "status":
|
||||
if (filter.condition === "is") {
|
||||
metadata.status = String(val).trim();
|
||||
}
|
||||
break;
|
||||
|
||||
case "tags":
|
||||
if (filter.condition === "contains" || filter.condition === "is") {
|
||||
if (!metadata.tags) {
|
||||
metadata.tags = [];
|
||||
}
|
||||
const tagVal = String(val).trim();
|
||||
if (tagVal && !metadata.tags.includes(tagVal)) {
|
||||
metadata.tags.push(tagVal);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "dueDate":
|
||||
if (filter.condition === "is") {
|
||||
const date = parseFilterDate(val);
|
||||
if (date) {
|
||||
metadata.dueDate = date;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "startDate":
|
||||
if (filter.condition === "is") {
|
||||
const date = parseFilterDate(val);
|
||||
if (date) {
|
||||
metadata.startDate = date;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "scheduledDate":
|
||||
if (filter.condition === "is") {
|
||||
const date = parseFilterDate(val);
|
||||
if (date) {
|
||||
metadata.scheduledDate = date;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges source metadata into target metadata.
|
||||
* Single-value fields are overwritten; multi-value fields (tags) are merged.
|
||||
*/
|
||||
function mergeMetadata(target: TaskMetadata, source: TaskMetadata): void {
|
||||
// Single-value fields: overwrite
|
||||
if (source.priority !== undefined) target.priority = source.priority;
|
||||
if (source.project !== undefined) target.project = source.project;
|
||||
if (source.status !== undefined) target.status = source.status;
|
||||
if (source.context !== undefined) target.context = source.context;
|
||||
if (source.startDate !== undefined) target.startDate = source.startDate;
|
||||
if (source.dueDate !== undefined) target.dueDate = source.dueDate;
|
||||
if (source.scheduledDate !== undefined)
|
||||
target.scheduledDate = source.scheduledDate;
|
||||
|
||||
// Multi-value fields: merge and deduplicate
|
||||
if (source.tags) {
|
||||
if (!target.tags) {
|
||||
target.tags = [];
|
||||
}
|
||||
source.tags.forEach((t) => {
|
||||
if (!target.tags!.includes(t)) {
|
||||
target.tags!.push(t);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a date string from filter value.
|
||||
* Supports ISO format and YYYY-MM-DD.
|
||||
*/
|
||||
function parseFilterDate(dateStr: unknown): Date | undefined {
|
||||
if (typeof dateStr !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Strict parsing for ISO and YYYY-MM-DD formats
|
||||
const m = moment(dateStr, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm"], true);
|
||||
if (m.isValid()) {
|
||||
return m.toDate();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
import { TaskFilterComponent } from "./ViewTaskFilter";
|
||||
import { ViewTaskFilterModal } from "./ViewTaskFilterModal";
|
||||
import { ViewTaskFilterPopover } from "./ViewTaskFilterPopover";
|
||||
import { extractMetadataFromFilter } from "./filter-metadata-extractor";
|
||||
|
||||
export { TaskFilterComponent, ViewTaskFilterModal, ViewTaskFilterPopover };
|
||||
export {
|
||||
TaskFilterComponent,
|
||||
ViewTaskFilterModal,
|
||||
ViewTaskFilterPopover,
|
||||
extractMetadataFromFilter,
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue