Skip to content

Commit bb79032

Browse files
committed
feat(quick-capture): auto-fill metadata from active filter state
1 parent 54285bd commit bb79032

File tree

3 files changed

+297
-4
lines changed

3 files changed

+297
-4
lines changed

src/components/features/quick-capture/modals/BaseQuickCaptureModal.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
} from "@/utils/date/date-utils";
2020
import { SuggestManager } from "@/components/ui/suggest";
2121
import { EmbeddableMarkdownEditor } from "@/editor-extensions/core/markdown-editor";
22+
import { extractMetadataFromFilter } from "../../task/filter/filter-metadata-extractor";
23+
import type { RootFilterState } from "../../task/filter/ViewTaskFilter";
2224

2325
/**
2426
* Quick capture save strategy types
@@ -102,11 +104,39 @@ export abstract class BaseQuickCaptureModal extends Modal {
102104
// Initialize suggest manager
103105
this.suggestManager = new SuggestManager(app, plugin);
104106

105-
// Initialize metadata
107+
// Initialize metadata with filter-based pre-filling
108+
let finalMetadata: TaskMetadata = {};
109+
110+
// 1. Extract metadata from global filter state (if active)
111+
const globalFilterState = this.app.loadLocalStorage(
112+
"task-genius-view-filter",
113+
) as RootFilterState | null;
114+
if (globalFilterState) {
115+
const extractedFilterMetadata =
116+
extractMetadataFromFilter(globalFilterState);
117+
finalMetadata = { ...extractedFilterMetadata };
118+
}
119+
120+
// 2. Merge with incoming metadata (incoming has higher priority)
106121
if (metadata) {
107-
this.taskMetadata = this.normalizeMetadataDates(metadata);
122+
const extractedTags = finalMetadata.tags || [];
123+
const incomingTags = metadata.tags || [];
124+
125+
finalMetadata = { ...finalMetadata, ...metadata };
126+
127+
// Special handling for tags: merge and deduplicate
128+
if (extractedTags.length > 0 || incomingTags.length > 0) {
129+
finalMetadata.tags = [
130+
...new Set([...extractedTags, ...incomingTags]),
131+
];
132+
}
133+
}
134+
135+
// 3. Apply final metadata
136+
if (Object.keys(finalMetadata).length > 0) {
137+
this.taskMetadata = this.normalizeMetadataDates(finalMetadata);
108138
// Auto-switch to file mode if location is file
109-
if (metadata.location === "file") {
139+
if (finalMetadata.location === "file") {
110140
this.currentMode = "file";
111141
}
112142
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/**
2+
* Filter Metadata Extractor
3+
*
4+
* Extracts task metadata from filter state for auto-filling when creating new tasks.
5+
* This enables context-aware task creation based on active filters.
6+
*/
7+
8+
import { moment } from "obsidian";
9+
import type { RootFilterState, FilterGroup, Filter } from "./ViewTaskFilter";
10+
import type { TaskMetadata } from "../../quick-capture/modals/BaseQuickCaptureModal";
11+
12+
/**
13+
* Extracts task metadata from the current filter state.
14+
*
15+
* Strategy:
16+
* - For `rootCondition === 'all'`: Merge metadata from all filter groups (union)
17+
* - For `rootCondition === 'any'`: Only extract metadata that is common across all groups (intersection)
18+
* - For `rootCondition === 'none'`: Return empty metadata (not suitable for pre-filling)
19+
*
20+
* @param filterState - The current root filter state
21+
* @returns TaskMetadata object with extracted values
22+
*/
23+
export function extractMetadataFromFilter(
24+
filterState: RootFilterState | null | undefined,
25+
): TaskMetadata {
26+
const metadata: TaskMetadata = {};
27+
28+
if (
29+
!filterState ||
30+
!filterState.filterGroups ||
31+
filterState.filterGroups.length === 0
32+
) {
33+
return metadata;
34+
}
35+
36+
// 'none' condition is not suitable for pre-filling
37+
if (filterState.rootCondition === "none") {
38+
return metadata;
39+
}
40+
41+
if (filterState.rootCondition === "all") {
42+
// Strategy: Union/Merge
43+
// For single-value fields, later groups override earlier ones (Last Write Wins)
44+
// For multi-value fields (tags), merge all occurrences
45+
filterState.filterGroups.forEach((group) => {
46+
const groupMeta = extractFromGroup(group);
47+
mergeMetadata(metadata, groupMeta);
48+
});
49+
} else if (filterState.rootCondition === "any") {
50+
// Strategy: Intersection
51+
// Only extract metadata that is common across ALL filter groups
52+
const allGroupsMeta = filterState.filterGroups.map(extractFromGroup);
53+
54+
if (allGroupsMeta.length > 0) {
55+
const potential = allGroupsMeta[0];
56+
57+
// Check single-value fields
58+
const singleFields: (keyof TaskMetadata)[] = [
59+
"priority",
60+
"project",
61+
"status",
62+
"context",
63+
];
64+
65+
singleFields.forEach((field) => {
66+
const val = potential[field];
67+
if (val !== undefined) {
68+
const allMatch = allGroupsMeta.every(
69+
(g) => g[field] === val,
70+
);
71+
if (allMatch) {
72+
(metadata as Record<string, unknown>)[field] = val;
73+
}
74+
}
75+
});
76+
77+
// Check date fields (compare timestamps)
78+
const dateFields: (keyof TaskMetadata)[] = [
79+
"startDate",
80+
"dueDate",
81+
"scheduledDate",
82+
];
83+
dateFields.forEach((field) => {
84+
const val = potential[field];
85+
if (val instanceof Date) {
86+
const allMatch = allGroupsMeta.every((g) => {
87+
const gVal = g[field];
88+
return (
89+
gVal instanceof Date &&
90+
gVal.getTime() === val.getTime()
91+
);
92+
});
93+
if (allMatch) {
94+
(metadata as Record<string, unknown>)[field] = val;
95+
}
96+
}
97+
});
98+
99+
// Check tags (array intersection)
100+
if (potential.tags && potential.tags.length > 0) {
101+
const commonTags = potential.tags.filter((tag) => {
102+
return allGroupsMeta.every(
103+
(g) => g.tags && g.tags.includes(tag),
104+
);
105+
});
106+
if (commonTags.length > 0) {
107+
metadata.tags = commonTags;
108+
}
109+
}
110+
}
111+
}
112+
113+
return metadata;
114+
}
115+
116+
/**
117+
* Extracts metadata from a single filter group.
118+
* Only processes groups with `groupCondition === 'all'` for reliable extraction.
119+
*/
120+
function extractFromGroup(group: FilterGroup): TaskMetadata {
121+
const groupMeta: TaskMetadata = {};
122+
123+
// Only extract from 'all' (AND) groups for reliable pre-filling
124+
if (group.groupCondition !== "all") {
125+
return groupMeta;
126+
}
127+
128+
group.filters.forEach((filter) => {
129+
extractFromFilter(filter, groupMeta);
130+
});
131+
132+
return groupMeta;
133+
}
134+
135+
/**
136+
* Extracts metadata from a single filter condition.
137+
* Only extracts from conditions that provide definite values (e.g., 'is', 'contains').
138+
*/
139+
function extractFromFilter(filter: Filter, metadata: TaskMetadata): void {
140+
if (!filter.property || !filter.condition) {
141+
return;
142+
}
143+
144+
const val = filter.value;
145+
// Ignore empty values
146+
if (val === undefined || val === "" || val === null) {
147+
return;
148+
}
149+
150+
switch (filter.property) {
151+
case "priority":
152+
if (filter.condition === "is") {
153+
const parsed = parseInt(val, 10);
154+
if (!isNaN(parsed)) {
155+
metadata.priority = parsed;
156+
}
157+
}
158+
break;
159+
160+
case "project":
161+
if (filter.condition === "is") {
162+
metadata.project = String(val).trim();
163+
}
164+
break;
165+
166+
case "status":
167+
if (filter.condition === "is") {
168+
metadata.status = String(val).trim();
169+
}
170+
break;
171+
172+
case "tags":
173+
if (filter.condition === "contains" || filter.condition === "is") {
174+
if (!metadata.tags) {
175+
metadata.tags = [];
176+
}
177+
const tagVal = String(val).trim();
178+
if (tagVal && !metadata.tags.includes(tagVal)) {
179+
metadata.tags.push(tagVal);
180+
}
181+
}
182+
break;
183+
184+
case "dueDate":
185+
if (filter.condition === "is") {
186+
const date = parseFilterDate(val);
187+
if (date) {
188+
metadata.dueDate = date;
189+
}
190+
}
191+
break;
192+
193+
case "startDate":
194+
if (filter.condition === "is") {
195+
const date = parseFilterDate(val);
196+
if (date) {
197+
metadata.startDate = date;
198+
}
199+
}
200+
break;
201+
202+
case "scheduledDate":
203+
if (filter.condition === "is") {
204+
const date = parseFilterDate(val);
205+
if (date) {
206+
metadata.scheduledDate = date;
207+
}
208+
}
209+
break;
210+
}
211+
}
212+
213+
/**
214+
* Merges source metadata into target metadata.
215+
* Single-value fields are overwritten; multi-value fields (tags) are merged.
216+
*/
217+
function mergeMetadata(target: TaskMetadata, source: TaskMetadata): void {
218+
// Single-value fields: overwrite
219+
if (source.priority !== undefined) target.priority = source.priority;
220+
if (source.project !== undefined) target.project = source.project;
221+
if (source.status !== undefined) target.status = source.status;
222+
if (source.context !== undefined) target.context = source.context;
223+
if (source.startDate !== undefined) target.startDate = source.startDate;
224+
if (source.dueDate !== undefined) target.dueDate = source.dueDate;
225+
if (source.scheduledDate !== undefined)
226+
target.scheduledDate = source.scheduledDate;
227+
228+
// Multi-value fields: merge and deduplicate
229+
if (source.tags) {
230+
if (!target.tags) {
231+
target.tags = [];
232+
}
233+
source.tags.forEach((t) => {
234+
if (!target.tags!.includes(t)) {
235+
target.tags!.push(t);
236+
}
237+
});
238+
}
239+
}
240+
241+
/**
242+
* Parses a date string from filter value.
243+
* Supports ISO format and YYYY-MM-DD.
244+
*/
245+
function parseFilterDate(dateStr: unknown): Date | undefined {
246+
if (typeof dateStr !== "string") {
247+
return undefined;
248+
}
249+
250+
// Strict parsing for ISO and YYYY-MM-DD formats
251+
const m = moment(dateStr, ["YYYY-MM-DD", "YYYY-MM-DDTHH:mm"], true);
252+
if (m.isValid()) {
253+
return m.toDate();
254+
}
255+
256+
return undefined;
257+
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { TaskFilterComponent } from "./ViewTaskFilter";
22
import { ViewTaskFilterModal } from "./ViewTaskFilterModal";
33
import { ViewTaskFilterPopover } from "./ViewTaskFilterPopover";
4+
import { extractMetadataFromFilter } from "./filter-metadata-extractor";
45

5-
export { TaskFilterComponent, ViewTaskFilterModal, ViewTaskFilterPopover };
6+
export {
7+
TaskFilterComponent,
8+
ViewTaskFilterModal,
9+
ViewTaskFilterPopover,
10+
extractMetadataFromFilter,
11+
};

0 commit comments

Comments
 (0)