trekoon 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/trekoon/SKILL.md +20 -577
- package/.agents/skills/trekoon/reference/execution-with-team.md +21 -9
- package/.agents/skills/trekoon/reference/execution.md +246 -7
- package/.agents/skills/trekoon/reference/planning.md +138 -1
- package/.agents/skills/trekoon/reference/status-machine.md +21 -0
- package/.agents/skills/trekoon/reference/sync.md +129 -0
- package/README.md +8 -7
- package/docs/ai-agents.md +17 -2
- package/docs/commands.md +147 -3
- package/docs/machine-contracts.md +123 -0
- package/docs/quickstart.md +52 -0
- package/package.json +1 -1
- package/src/board/assets/app.js +49 -16
- package/src/board/assets/components/Component.js +22 -8
- package/src/board/assets/components/Workspace.js +9 -3
- package/src/board/assets/components/helpers.js +5 -1
- package/src/board/assets/runtime/delegation.js +8 -0
- package/src/board/assets/runtime/focus-trap.js +48 -0
- package/src/board/assets/state/actions.js +47 -4
- package/src/board/assets/state/api.js +284 -11
- package/src/board/assets/state/store.js +87 -11
- package/src/board/assets/state/url.js +10 -0
- package/src/board/assets/state/utils.js +2 -1
- package/src/board/event-bus.ts +72 -0
- package/src/board/routes.ts +412 -33
- package/src/board/server.ts +77 -8
- package/src/board/wal-watcher.ts +302 -0
- package/src/commands/board.ts +52 -17
- package/src/commands/epic.ts +7 -9
- package/src/commands/error-utils.ts +54 -1
- package/src/commands/help.ts +69 -4
- package/src/commands/migrate.ts +153 -24
- package/src/commands/quickstart.ts +7 -0
- package/src/commands/subtask.ts +71 -10
- package/src/commands/suggest.ts +6 -13
- package/src/commands/task.ts +137 -88
- package/src/domain/batch-validation.ts +329 -0
- package/src/domain/cascade-planner.ts +412 -0
- package/src/domain/dependency-rules.ts +15 -0
- package/src/domain/mutation-service.ts +828 -192
- package/src/domain/search.ts +113 -0
- package/src/domain/tracker-domain.ts +150 -680
- package/src/domain/types.ts +53 -2
- package/src/index.ts +37 -0
- package/src/runtime/cli-shell.ts +44 -0
- package/src/runtime/daemon.ts +639 -0
- package/src/storage/backup.ts +166 -0
- package/src/storage/database.ts +261 -4
- package/src/storage/migrations.ts +422 -20
- package/src/storage/path.ts +8 -0
- package/src/storage/schema.ts +5 -1
- package/src/sync/event-writes.ts +38 -11
- package/src/sync/git-context.ts +226 -8
- package/src/sync/service.ts +650 -147
|
@@ -80,11 +80,18 @@ function getNamespacedFormIdentity(form) {
|
|
|
80
80
|
return "anonymous-form";
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function getManagedControls(root) {
|
|
83
|
+
function getManagedControls(root, cache) {
|
|
84
|
+
if (cache) {
|
|
85
|
+
const cached = cache.get(root);
|
|
86
|
+
if (cached) return cached;
|
|
87
|
+
const controls = Array.from(root.querySelectorAll("input, textarea, select"));
|
|
88
|
+
cache.set(root, controls);
|
|
89
|
+
return controls;
|
|
90
|
+
}
|
|
84
91
|
return Array.from(root.querySelectorAll("input, textarea, select"));
|
|
85
92
|
}
|
|
86
93
|
|
|
87
|
-
function getControlIdentity(el, form) {
|
|
94
|
+
function getControlIdentity(el, form, cache) {
|
|
88
95
|
const controlKey = el.getAttribute("data-control-id");
|
|
89
96
|
if (controlKey) {
|
|
90
97
|
return `control:${controlKey}`;
|
|
@@ -98,12 +105,12 @@ function getControlIdentity(el, form) {
|
|
|
98
105
|
if (name) {
|
|
99
106
|
const tagName = el.tagName.toLowerCase();
|
|
100
107
|
const type = tagName === "input" ? (el.getAttribute("type") ?? "text") : tagName;
|
|
101
|
-
const peers = getManagedControls(form).filter((candidate) => candidate.getAttribute("name") === name);
|
|
108
|
+
const peers = getManagedControls(form, cache).filter((candidate) => candidate.getAttribute("name") === name);
|
|
102
109
|
const index = peers.indexOf(el);
|
|
103
110
|
return `name:${name}:${type}:${index}`;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
const index = getManagedControls(form).indexOf(el);
|
|
113
|
+
const index = getManagedControls(form, cache).indexOf(el);
|
|
107
114
|
return `index:${index}:${el.tagName.toLowerCase()}`;
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -143,7 +150,11 @@ function restoreSelection(el, selectionStart, selectionEnd) {
|
|
|
143
150
|
*/
|
|
144
151
|
export function preserveFormState(container, writeFn, options = {}) {
|
|
145
152
|
const resetFormIds = new Set(options.resetFormIds ?? []);
|
|
146
|
-
|
|
153
|
+
|
|
154
|
+
// Per-form cache for getManagedControls: avoids the O(n^2) re-query that
|
|
155
|
+
// occurs when many controls share the same form root.
|
|
156
|
+
const captureCache = new Map();
|
|
157
|
+
const inputs = getManagedControls(container, captureCache);
|
|
147
158
|
|
|
148
159
|
const activeElement = document.activeElement;
|
|
149
160
|
let focusedIdentity = null;
|
|
@@ -151,7 +162,7 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
151
162
|
const savedStates = inputs.map((el) => {
|
|
152
163
|
const form = getFormRoot(el);
|
|
153
164
|
const formId = getNamespacedFormIdentity(form);
|
|
154
|
-
const controlId = form ? getControlIdentity(el, form) : null;
|
|
165
|
+
const controlId = form ? getControlIdentity(el, form, captureCache) : null;
|
|
155
166
|
const identity = controlId ? { formId, controlId } : null;
|
|
156
167
|
|
|
157
168
|
if (activeElement === el) {
|
|
@@ -177,13 +188,16 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
177
188
|
]),
|
|
178
189
|
);
|
|
179
190
|
|
|
191
|
+
// Fresh cache for the restore pass (DOM was replaced by writeFn).
|
|
192
|
+
const restoreCache = new Map();
|
|
193
|
+
|
|
180
194
|
for (const state of savedStates) {
|
|
181
195
|
const { formId, controlId } = state.identity;
|
|
182
196
|
if (resetFormIds.has(formId)) {
|
|
183
197
|
continue;
|
|
184
198
|
}
|
|
185
199
|
const form = formsByIdentity.get(formId) ?? container;
|
|
186
|
-
const restored = getManagedControls(form).find((control) => getControlIdentity(control, form) === controlId);
|
|
200
|
+
const restored = getManagedControls(form, restoreCache).find((control) => getControlIdentity(control, form, restoreCache) === controlId);
|
|
187
201
|
if (restored && restored.value !== state.value) {
|
|
188
202
|
restored.value = state.value;
|
|
189
203
|
}
|
|
@@ -199,7 +213,7 @@ export function preserveFormState(container, writeFn, options = {}) {
|
|
|
199
213
|
return;
|
|
200
214
|
}
|
|
201
215
|
const form = formsByIdentity.get(formId) ?? container;
|
|
202
|
-
const restored = getManagedControls(form).find((control) => getControlIdentity(control, form) === controlId);
|
|
216
|
+
const restored = getManagedControls(form, restoreCache).find((control) => getControlIdentity(control, form, restoreCache) === controlId);
|
|
203
217
|
if (restored) {
|
|
204
218
|
restored.focus({ preventScroll: true });
|
|
205
219
|
const focusedState = savedStates.find((state) => state.identity?.formId === formId && state.identity?.controlId === controlId);
|
|
@@ -166,7 +166,7 @@ function renderWorkspaceHeader(props) {
|
|
|
166
166
|
// ---------------------------------------------------------------------------
|
|
167
167
|
|
|
168
168
|
function renderKanbanColumns(props) {
|
|
169
|
-
const { visibleTasks, selectedTaskId, isMutating, taskStatusFilter } = props;
|
|
169
|
+
const { visibleTasks, selectedTaskId, isMutating, taskStatusFilter, dragFeedback } = props;
|
|
170
170
|
const filter = taskStatusFilter || { ...DEFAULT_STATUS_FILTER };
|
|
171
171
|
|
|
172
172
|
const columnsMarkup = STATUS_ORDER
|
|
@@ -178,6 +178,12 @@ function renderKanbanColumns(props) {
|
|
|
178
178
|
? renderEmptyState(`No ${columnTitle.toLowerCase()} work`, "Adjust search or switch epics to inspect more tasks.")
|
|
179
179
|
: columnTasks.map((task) => renderTaskCard({ task, selected: selectedTaskId === task.id, isMutating })).join("");
|
|
180
180
|
|
|
181
|
+
// Re-apply drag-feedback class from store so a rerender during drag does
|
|
182
|
+
// not silently wipe the visual feedback that was set by handleDragover.
|
|
183
|
+
const feedbackClass = dragFeedback?.targetStatus === status
|
|
184
|
+
? (dragFeedback.kind === "valid" ? " board-drop-valid" : " board-drop-invalid")
|
|
185
|
+
: "";
|
|
186
|
+
|
|
181
187
|
return `
|
|
182
188
|
<section class="board-column board-column--dense ${secondaryPanelClasses("flex min-h-[20rem] min-w-0 flex-col p-3")}" aria-labelledby="column-${status}">
|
|
183
189
|
<header class="board-column__header flex items-start justify-between gap-3 border-b border-[var(--board-border)] pb-3">
|
|
@@ -190,7 +196,7 @@ function renderKanbanColumns(props) {
|
|
|
190
196
|
</div>
|
|
191
197
|
${columnTasks.length > 0 ? `<span class="board-column__count text-xs font-medium text-[var(--board-text-soft)]">${columnTasks.length === 1 ? "1 task" : `${columnTasks.length} tasks`}</span>` : ""}
|
|
192
198
|
</header>
|
|
193
|
-
<div class="board-column__tasks mt-3 grid min-h-0 flex-1 content-start gap-2.5 overflow-auto pr-1 overscroll-contain" id="column-${status}" data-drop-status="${escapeHtml(status)}">${content}</div>
|
|
199
|
+
<div class="board-column__tasks mt-3 grid min-h-0 flex-1 content-start gap-2.5 overflow-auto pr-1 overscroll-contain${feedbackClass}" id="column-${status}" data-drop-status="${escapeHtml(status)}">${content}</div>
|
|
194
200
|
</section>
|
|
195
201
|
`;
|
|
196
202
|
}).join("");
|
|
@@ -292,7 +298,7 @@ function render(props) {
|
|
|
292
298
|
});
|
|
293
299
|
|
|
294
300
|
const contentMarkup = store.view === "kanban"
|
|
295
|
-
? renderKanbanColumns({ visibleTasks, selectedTaskId, isMutating: store.isMutating, taskStatusFilter: store.taskStatusFilter })
|
|
301
|
+
? renderKanbanColumns({ visibleTasks, selectedTaskId, isMutating: store.isMutating, taskStatusFilter: store.taskStatusFilter, dragFeedback: store.dragFeedback ?? null })
|
|
296
302
|
: renderListView({ visibleTasks, selectedTaskId });
|
|
297
303
|
|
|
298
304
|
return `
|
|
@@ -252,7 +252,11 @@ export function isCompactViewport() {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
export function shouldUseTaskModal(boardState, store) {
|
|
255
|
-
return Boolean(boardState?.selectedTask);
|
|
255
|
+
return Boolean(boardState?.taskModalOpen && boardState?.selectedTask);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function shouldUseSubtaskModal(boardState) {
|
|
259
|
+
return Boolean(boardState?.subtaskModalOpen && boardState?.selectedSubtask);
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
export function lookupNode(snapshot, id) {
|
|
@@ -310,6 +310,8 @@ export function createDelegation(rootElement, actions) {
|
|
|
310
310
|
for (const el of rootElement.querySelectorAll(".board-drop-valid, .board-drop-invalid")) {
|
|
311
311
|
el.classList.remove("board-drop-valid", "board-drop-invalid");
|
|
312
312
|
}
|
|
313
|
+
// Clear feedback in store so rerenders do not re-apply stale classes.
|
|
314
|
+
actions.setDragFeedback?.(null);
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
function handleDragstart(event) {
|
|
@@ -336,11 +338,15 @@ export function createDelegation(rootElement, actions) {
|
|
|
336
338
|
const targetStatus = column.dataset.dropStatus;
|
|
337
339
|
if (draggedTaskStatus && isValidTransition(draggedTaskStatus, targetStatus)) {
|
|
338
340
|
event.preventDefault();
|
|
341
|
+
// Fast path: apply directly to DOM for immediate visual feedback.
|
|
339
342
|
column.classList.add("board-drop-valid");
|
|
340
343
|
column.classList.remove("board-drop-invalid");
|
|
344
|
+
// Also persist in store so a rerender during drag restores the class.
|
|
345
|
+
actions.setDragFeedback?.({ targetStatus, kind: "valid" });
|
|
341
346
|
} else if (draggedTaskStatus && targetStatus !== draggedTaskStatus) {
|
|
342
347
|
column.classList.add("board-drop-invalid");
|
|
343
348
|
column.classList.remove("board-drop-valid");
|
|
349
|
+
actions.setDragFeedback?.({ targetStatus, kind: "invalid" });
|
|
344
350
|
}
|
|
345
351
|
}
|
|
346
352
|
|
|
@@ -348,6 +354,8 @@ export function createDelegation(rootElement, actions) {
|
|
|
348
354
|
const column = event.target.closest("[data-drop-status]");
|
|
349
355
|
if (column && !column.contains(event.relatedTarget)) {
|
|
350
356
|
column.classList.remove("board-drop-valid", "board-drop-invalid");
|
|
357
|
+
// Clear store feedback when leaving the column.
|
|
358
|
+
actions.setDragFeedback?.(null);
|
|
351
359
|
}
|
|
352
360
|
}
|
|
353
361
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy overlay focus-trap controller.
|
|
3
|
+
*
|
|
4
|
+
* Attaches the document-level keydown/focusin listeners only while an overlay
|
|
5
|
+
* is actually open, and detaches them on close. Without this, plain Tab outside
|
|
6
|
+
* any overlay can be intercepted by a stale handler in the microtask window
|
|
7
|
+
* between overlay close and rerender.
|
|
8
|
+
*
|
|
9
|
+
* @param {{
|
|
10
|
+
* doc?: Document,
|
|
11
|
+
* onTabKey: (event: KeyboardEvent) => void,
|
|
12
|
+
* onFocusIn: (event: FocusEvent) => void,
|
|
13
|
+
* }} options
|
|
14
|
+
*/
|
|
15
|
+
export function createOverlayFocusTrap(options) {
|
|
16
|
+
const doc = options.doc ?? (typeof document !== "undefined" ? document : null);
|
|
17
|
+
if (!doc) {
|
|
18
|
+
return {
|
|
19
|
+
attach() {},
|
|
20
|
+
detach() {},
|
|
21
|
+
isAttached() { return false; },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const onKeyDown = options.onTabKey;
|
|
26
|
+
const onFocusIn = options.onFocusIn;
|
|
27
|
+
let attached = false;
|
|
28
|
+
|
|
29
|
+
function attach() {
|
|
30
|
+
if (attached) return;
|
|
31
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
32
|
+
doc.addEventListener("focusin", onFocusIn, true);
|
|
33
|
+
attached = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detach() {
|
|
37
|
+
if (!attached) return;
|
|
38
|
+
doc.removeEventListener("keydown", onKeyDown, true);
|
|
39
|
+
doc.removeEventListener("focusin", onFocusIn, true);
|
|
40
|
+
attached = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
attach,
|
|
45
|
+
detach,
|
|
46
|
+
isAttached() { return attached; },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -142,6 +142,13 @@ export function createBoardActions(options) {
|
|
|
142
142
|
searchFocusKeys,
|
|
143
143
|
} = options;
|
|
144
144
|
const { store, persist, getBoardState, getTaskById, syncState } = model;
|
|
145
|
+
// Direct mutations of `store.*` (filter toggles, notice changes, drag
|
|
146
|
+
// feedback) bypass setState/syncState, so `getBoardState()`'s memo would
|
|
147
|
+
// otherwise return a stale reference until the next syncState/notify.
|
|
148
|
+
// Route these through invalidateBoardStateMemo() before rerender.
|
|
149
|
+
const invalidateMemo = typeof model.invalidateBoardStateMemo === "function"
|
|
150
|
+
? () => model.invalidateBoardStateMemo()
|
|
151
|
+
: () => {};
|
|
145
152
|
|
|
146
153
|
const transition = (patch = {}, options = {}) => {
|
|
147
154
|
const { persistState = true, rerenderBoard = true } = options;
|
|
@@ -263,6 +270,7 @@ export function createBoardActions(options) {
|
|
|
263
270
|
selectedEpicId: epicId || null,
|
|
264
271
|
selectedTaskId: null,
|
|
265
272
|
selectedSubtaskId: null,
|
|
273
|
+
taskModalOpen: false,
|
|
266
274
|
});
|
|
267
275
|
},
|
|
268
276
|
selectEpic(epicId) {
|
|
@@ -271,6 +279,7 @@ export function createBoardActions(options) {
|
|
|
271
279
|
selectedEpicId: epicId || null,
|
|
272
280
|
selectedTaskId: null,
|
|
273
281
|
selectedSubtaskId: null,
|
|
282
|
+
taskModalOpen: false,
|
|
274
283
|
});
|
|
275
284
|
},
|
|
276
285
|
showEpics() {
|
|
@@ -278,6 +287,7 @@ export function createBoardActions(options) {
|
|
|
278
287
|
screen: "epics",
|
|
279
288
|
selectedTaskId: null,
|
|
280
289
|
selectedSubtaskId: null,
|
|
290
|
+
taskModalOpen: false,
|
|
281
291
|
});
|
|
282
292
|
},
|
|
283
293
|
showBoard() {
|
|
@@ -295,6 +305,7 @@ export function createBoardActions(options) {
|
|
|
295
305
|
selectedEpicId: fallbackEpicId,
|
|
296
306
|
selectedTaskId: null,
|
|
297
307
|
selectedSubtaskId: null,
|
|
308
|
+
taskModalOpen: false,
|
|
298
309
|
});
|
|
299
310
|
},
|
|
300
311
|
setView(view) {
|
|
@@ -340,16 +351,25 @@ export function createBoardActions(options) {
|
|
|
340
351
|
screen: "tasks",
|
|
341
352
|
selectedEpicId: task.epicId,
|
|
342
353
|
selectedTaskId: taskId,
|
|
354
|
+
taskModalOpen: true,
|
|
343
355
|
});
|
|
344
356
|
},
|
|
345
357
|
closeTask() {
|
|
346
|
-
transition({
|
|
358
|
+
transition({
|
|
359
|
+
selectedTaskId: null,
|
|
360
|
+
selectedSubtaskId: null,
|
|
361
|
+
taskModalOpen: false,
|
|
362
|
+
subtaskModalOpen: false,
|
|
363
|
+
});
|
|
347
364
|
},
|
|
348
365
|
openSubtask(subtaskId) {
|
|
349
|
-
transition(
|
|
366
|
+
transition(
|
|
367
|
+
{ selectedSubtaskId: subtaskId || null, subtaskModalOpen: Boolean(subtaskId) },
|
|
368
|
+
{ persistState: false },
|
|
369
|
+
);
|
|
350
370
|
},
|
|
351
371
|
closeSubtask() {
|
|
352
|
-
transition({ selectedSubtaskId: null }, { persistState: false });
|
|
372
|
+
transition({ selectedSubtaskId: null, subtaskModalOpen: false }, { persistState: false });
|
|
353
373
|
},
|
|
354
374
|
submitTaskForm(taskId, formData) {
|
|
355
375
|
const updates = {
|
|
@@ -408,12 +428,31 @@ export function createBoardActions(options) {
|
|
|
408
428
|
const task = getTaskById(taskId);
|
|
409
429
|
return task?.status ?? null;
|
|
410
430
|
},
|
|
431
|
+
/**
|
|
432
|
+
* Record the current drag-over feedback in store state so it survives a
|
|
433
|
+
* rerender triggered by an unrelated event (e.g. a server SSE push).
|
|
434
|
+
* Pass null to clear feedback (dragend / drop / dragleave).
|
|
435
|
+
*
|
|
436
|
+
* @param {{ targetStatus: string, kind: 'valid'|'invalid' }|null} feedback
|
|
437
|
+
*/
|
|
438
|
+
setDragFeedback(feedback) {
|
|
439
|
+
// Reference equality on freshly-allocated `{ targetStatus, kind }` always
|
|
440
|
+
// fails, so dragover would rerender on every move. Compare by primitive
|
|
441
|
+
// key so repeats over the same column become a no-op.
|
|
442
|
+
const prev = store.dragFeedback;
|
|
443
|
+
const prevKey = prev ? `${prev.targetStatus}|${prev.kind}` : null;
|
|
444
|
+
const nextKey = feedback ? `${feedback.targetStatus}|${feedback.kind}` : null;
|
|
445
|
+
if (prevKey === nextKey) return;
|
|
446
|
+
store.dragFeedback = feedback;
|
|
447
|
+
rerender();
|
|
448
|
+
},
|
|
411
449
|
dropTaskStatus(taskId, nextStatus) {
|
|
412
450
|
const task = getTaskById(taskId);
|
|
413
451
|
if (!task || !nextStatus || task.status === nextStatus) {
|
|
414
452
|
return;
|
|
415
453
|
}
|
|
416
|
-
|
|
454
|
+
// Drag/drop is a status change only; do not mutate selection or modal state,
|
|
455
|
+
// otherwise dropping a card while another task modal is open hijacks it.
|
|
417
456
|
api.patchTask(taskId, { status: nextStatus }, (snapshot) => updateTaskInSnapshot(snapshot, taskId, { status: nextStatus }, normalizeSnapshot));
|
|
418
457
|
},
|
|
419
458
|
changeEpicStatus(epicId, newStatus) {
|
|
@@ -433,22 +472,26 @@ export function createBoardActions(options) {
|
|
|
433
472
|
toggleEpicStatusFilter(status) {
|
|
434
473
|
const current = store.epicStatusFilter || { ...DEFAULT_STATUS_FILTER };
|
|
435
474
|
store.epicStatusFilter = { ...current, [status]: !current[status] };
|
|
475
|
+
invalidateMemo();
|
|
436
476
|
persist();
|
|
437
477
|
rerender();
|
|
438
478
|
},
|
|
439
479
|
toggleTaskStatusFilter(status) {
|
|
440
480
|
const current = store.taskStatusFilter || { ...DEFAULT_STATUS_FILTER };
|
|
441
481
|
store.taskStatusFilter = { ...current, [status]: !current[status] };
|
|
482
|
+
invalidateMemo();
|
|
442
483
|
persist();
|
|
443
484
|
rerender();
|
|
444
485
|
},
|
|
445
486
|
resetEpicFilter() {
|
|
446
487
|
store.epicStatusFilter = { ...DEFAULT_STATUS_FILTER };
|
|
488
|
+
invalidateMemo();
|
|
447
489
|
persist();
|
|
448
490
|
rerender();
|
|
449
491
|
},
|
|
450
492
|
resetTaskFilter() {
|
|
451
493
|
store.taskStatusFilter = { ...DEFAULT_STATUS_FILTER };
|
|
494
|
+
invalidateMemo();
|
|
452
495
|
persist();
|
|
453
496
|
rerender();
|
|
454
497
|
},
|