From 909134274fb294a62df1a00a8fa95c3bd43395d3 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Wed, 11 Nov 2026 08:12:12 +1100 Subject: [PATCH 1/3] refactor(settings): edit the task type with the model package's editor The app carried its own editor for a task type's statuses, priorities and custom fields, and a second reader for early binding spellings. Both now live in @tasknotes/model (0.3.0-rc.21), shared with the Obsidian plugin. A settings save that writes the type also leaves it with one vocabulary when a hand edit made its schema and binding disagree. --- package.json | 2 +- pnpm-lock.yaml | 11 +- src/components/task-model-draft.ts | 14 +- src/components/task-model-settings.tsx | 52 ++- src/components/task-vocabulary-editor.tsx | 39 +- src/demo/demo-task-repository.ts | 19 +- src/domain/task-configuration.ts | 135 +----- src/domain/task-type-vocabulary.test.ts | 324 -------------- src/domain/task-type-vocabulary.ts | 407 ------------------ src/storage/next-provider-settings.test.ts | 83 ++-- src/storage/next-repository.ts | 42 +- .../tasknotes-model-0.3.0-rc.21-08f2c357.tgz | Bin 0 -> 1272146 bytes 12 files changed, 155 insertions(+), 973 deletions(-) delete mode 100644 src/domain/task-type-vocabulary.test.ts delete mode 100644 src/domain/task-type-vocabulary.ts create mode 100644 vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz diff --git a/package.json b/package.json index 96bed932..73325016 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@sqlite.org/sqlite-wasm": "3.53.4-build2", "@tanstack/react-virtual": "3.14.11", "@tasknotes/calendar": "0.1.10", - "@tasknotes/model": "0.3.0-rc.20", + "@tasknotes/model": "file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz", "codemirror": "^6.0.2", "dexie": "4.4.4", "firebase": "12.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 650cf1f0..0f7ca443 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,8 +84,8 @@ importers: specifier: 0.1.10 version: 0.1.10(react@19.2.8) '@tasknotes/model': - specifier: 0.3.0-rc.20 - version: 0.3.0-rc.20 + specifier: file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz + version: file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -1264,8 +1264,9 @@ packages: react: optional: true - '@tasknotes/model@0.3.0-rc.20': - resolution: {integrity: sha512-Id8bJjU0HhBJAqxJLkJOhtkWjq7MV1JWBm/q0yeTeQLiDw05jctceheh9xiM1hJ86PiH6Qu6E+MVwuxNfUnPJQ==} + '@tasknotes/model@file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz': + resolution: {integrity: sha512-qZNukGM9Ef8d6BpUBiEaV5R5WRJxzQ3xicjVxiJjH7+3fObiljRuMh3VCOCQuLeixzgIx8kjoZMGftcoK2Zfag==, tarball: file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz} + version: 0.3.0-rc.21 '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} @@ -5476,7 +5477,7 @@ snapshots: optionalDependencies: react: 19.2.8 - '@tasknotes/model@0.3.0-rc.20': + '@tasknotes/model@file:vendor/tasknotes-model-0.3.0-rc.21-08f2c357.tgz': dependencies: rrule: 2.8.1 yaml: 2.9.0 diff --git a/src/components/task-model-draft.ts b/src/components/task-model-draft.ts index 5f54106e..614dcf66 100644 --- a/src/components/task-model-draft.ts +++ b/src/components/task-model-draft.ts @@ -2,10 +2,10 @@ import { useCallback, useSyncExternalStore } from "react"; import type { TaskCollectionConfiguration } from "../domain/task-configuration"; import type { - TaskCustomFieldEdit, - TaskPriorityEdit, - TaskStatusEdit, -} from "../domain/task-type-vocabulary"; + TaskNotesPriorityEdit, + TaskNotesStatusEdit, + TaskNotesUserFieldEdit, +} from "@tasknotes/model/mdbase"; /** * `id` names a row while it is edited. A saved entry's id is its stored @@ -32,10 +32,10 @@ export interface TaskModelSettingsDraft { string, { autoArchive: boolean; autoArchiveDelay: number } >; - statuses: DraftEntry[]; + statuses: DraftEntry[]; /** Lowest first; a priority's weight is its position. */ - priorities: DraftEntry[]; - customFields: DraftEntry[]; + priorities: DraftEntry[]; + customFields: DraftEntry[]; } type DraftPatch = Partial; diff --git a/src/components/task-model-settings.tsx b/src/components/task-model-settings.tsx index 8129481a..871ca74f 100644 --- a/src/components/task-model-settings.tsx +++ b/src/components/task-model-settings.tsx @@ -20,12 +20,13 @@ import { StatusListEditor, } from "./task-vocabulary-editor"; import { - TaskVocabularyError, - TaskVocabularyInUseError, - vocabularyProblem, - type TaskVocabularyPatch, - type TaskVocabularyProblem, -} from "../domain/task-type-vocabulary"; + isTaskNotesVocabularyError, + isTaskNotesVocabularyInUseError, + taskNotesVocabularyProblem, + type TaskNotesVocabularyInUseError, + type TaskNotesVocabularyPatch, + type TaskNotesVocabularyProblem, +} from "@tasknotes/model/mdbase"; import { useTaskModelDraft, type TaskModelSettingsDraft, @@ -157,19 +158,20 @@ export function TaskModelSettingsEditor() { if (!writable || !dirty || saveState === "saving") return; const vocabulary = vocabularyChanges(draft, original); const invalid = - vocabularyProblem(vocabulary, { + taskNotesVocabularyProblem(vocabulary, { status: draft.defaultStatus, priority: draft.defaultPriority, }) ?? - (vocabulary.customFields?.add?.some( + (vocabulary.userFields?.add?.some( ({ displayName }) => !displayName.trim(), ) ? ({ kind: "field-label", key: "" } as const) : null); - if (invalid) { + const invalidMessage = invalid && vocabularyProblemMessage(invalid); + if (invalidMessage) { setSaveState("error"); setError(""); - setProblem(vocabularyProblemMessage(invalid)); + setProblem(invalidMessage); // A missing name is the usual reason; put the keyboard on it. (event.currentTarget as HTMLElement) .querySelector( @@ -183,7 +185,7 @@ export function TaskModelSettingsEditor() { setProblem(""); try { await updateTaskModelSettings({ - ...(Object.keys(vocabulary).length ? { vocabulary } : {}), + ...vocabulary, defaultStatus: draft.defaultStatus, defaultPriority: draft.defaultPriority, recurrence: { @@ -217,10 +219,12 @@ export function TaskModelSettingsEditor() { setSaveState("saved"); } catch (reason) { setSaveState("error"); - if (reason instanceof TaskVocabularyInUseError) - setProblem(vocabularyInUseMessage(reason, original)); - else if (reason instanceof TaskVocabularyError) - setProblem(vocabularyProblemMessage(reason.problem)); + const message = isTaskNotesVocabularyInUseError(reason) + ? vocabularyInUseMessage(reason, original) + : isTaskNotesVocabularyError(reason) + ? vocabularyProblemMessage(reason.problem) + : null; + if (message) setProblem(message); else setError(reason instanceof Error ? reason.message : String(reason)); } } @@ -625,10 +629,10 @@ function SettingsGroup({ function vocabularyChanges( draft: TaskModelSettingsDraft, original: TaskModelSettingsDraft, -): TaskVocabularyPatch { +): TaskNotesVocabularyPatch { const changed = (key: "statuses" | "priorities" | "customFields") => JSON.stringify(draft[key]) !== JSON.stringify(original[key]); - const patch: TaskVocabularyPatch = {}; + const patch: TaskNotesVocabularyPatch = {}; if (changed("statuses")) patch.statuses = draft.statuses.map( ({ value, label, color, isCompleted }) => ({ @@ -649,7 +653,7 @@ function vocabularyChanges( original.customFields.map((field) => [field.id, field]), ); const kept = new Set(draft.customFields.map(({ id }) => id)); - patch.customFields = { + patch.userFields = { add: draft.customFields .filter(({ id }) => !before.has(id)) .map(({ key, displayName, type }) => ({ key, displayName, type })), @@ -668,7 +672,13 @@ function vocabularyChanges( return patch; } -function vocabularyProblemMessage(problem: TaskVocabularyProblem): string { +/** + * What to tell someone about a problem they can fix in this form, or null for + * one this form cannot cause. + */ +function vocabularyProblemMessage( + problem: TaskNotesVocabularyProblem, +): string | null { switch (problem.kind) { case "empty-label": return problem.list === "status" @@ -689,11 +699,13 @@ function vocabularyProblemMessage(problem: TaskVocabularyProblem): string { case "field-name": case "field-taken": return translate`“${problem.key}” can’t be used for a custom field. Try another name.`; + case "completed-and-skipped": + return null; } } function vocabularyInUseMessage( - problem: TaskVocabularyInUseError, + problem: TaskNotesVocabularyInUseError, original: TaskModelSettingsDraft, ): string { const entries = diff --git a/src/components/task-vocabulary-editor.tsx b/src/components/task-vocabulary-editor.tsx index 408264ba..c1359462 100644 --- a/src/components/task-vocabulary-editor.tsx +++ b/src/components/task-vocabulary-editor.tsx @@ -3,12 +3,12 @@ import { ArrowDown, ArrowUp, Plus, Trash2 } from "lucide-react"; import { useRef, type ReactNode } from "react"; import { - customFieldKey, - vocabularyValue, - type TaskCustomFieldEdit, - type TaskPriorityEdit, - type TaskStatusEdit, -} from "../domain/task-type-vocabulary"; + taskNotesUserFieldKey, + taskNotesVocabularyValue, + type TaskNotesPriorityEdit, + type TaskNotesStatusEdit, + type TaskNotesUserFieldEdit, +} from "@tasknotes/model/mdbase"; import { TaskNotesSelect } from "./tasknotes-controls"; import type { DraftEntry } from "./task-model-draft"; @@ -110,7 +110,7 @@ function OptionRows({ return; } // A new entry's stored value follows its name until saved. - const value = vocabularyValue( + const value = taskNotesVocabularyValue( label, entries .filter((candidate) => candidate.id !== entry.id) @@ -214,9 +214,9 @@ export function StatusListEditor({ onChange, onDefault, }: { - statuses: DraftEntry[]; + statuses: DraftEntry[]; defaultStatus: string; - onChange(statuses: DraftEntry[]): void; + onChange(statuses: DraftEntry[]): void; onDefault(value: string): void; }) { const { t } = useLingui(); @@ -256,7 +256,7 @@ export function StatusListEditor({ ...statuses, { id: newId(), - value: vocabularyValue( + value: taskNotesVocabularyValue( "", statuses.map(({ value }) => value), ), @@ -280,9 +280,9 @@ export function PriorityListEditor({ onChange, onDefault, }: { - priorities: DraftEntry[]; + priorities: DraftEntry[]; defaultPriority: string; - onChange(priorities: DraftEntry[]): void; + onChange(priorities: DraftEntry[]): void; onDefault(value: string): void; }) { const { t } = useLingui(); @@ -305,7 +305,7 @@ export function PriorityListEditor({ ...priorities, { id: newId(), - value: vocabularyValue( + value: taskNotesVocabularyValue( "", priorities.map(({ value }) => value), ), @@ -327,10 +327,10 @@ export function CustomFieldListEditor({ reservedKeys, onChange, }: { - fields: DraftEntry[]; + fields: DraftEntry[]; /** Property names a new field may not take. */ reservedKeys: readonly string[]; - onChange(fields: DraftEntry[]): void; + onChange(fields: DraftEntry[]): void; }) { const { t } = useLingui(); const root = useRef(null); @@ -357,7 +357,7 @@ export function CustomFieldListEditor({