diff --git a/src/lib/dataApi.ts b/src/lib/dataApi.ts index 5bc8c5d0..d1f7ed5d 100644 --- a/src/lib/dataApi.ts +++ b/src/lib/dataApi.ts @@ -78,6 +78,43 @@ export class DataApi { ); } + async addOption( + paths: string[], + field: DataField, + value: Optional + ): Promise { + Promise.all( + paths + .map((path) => this.fileSystem.getFile(path)) + .filter(notEmpty) + .map((file) => + this.updateFile(file, (data) => doAddOption(data, field, value))() + ) + ); + } + + async renameOption(paths: string[], from: string, to: string): Promise { + Promise.all( + paths + .map((path) => this.fileSystem.getFile(path)) + .filter(notEmpty) + .map((file) => + this.updateFile(file, (data) => doRenameOption(data, from, to))() + ) + ); + } + + async deleteOption(paths: string[], name: string): Promise { + Promise.all( + paths + .map((path) => this.fileSystem.getFile(path)) + .filter(notEmpty) + .map((file) => + this.updateFile(file, (data) => doDeleteOption(data, name))() + ) + ); + } + async createNote(record: DataRecord, templatePath: string): Promise { let content = ""; @@ -226,6 +263,57 @@ export function doRenameField( ); } +export function doRenameOption( + data: string, + from: string, + to: string +): E.Either { + return F.pipe( + data, + decodeFrontMatter, + E.map((frontmatter) => ({ + ...frontmatter, + [to]: frontmatter[from], + [from]: undefined, + })), + E.chain((frontmatter) => + encodeFrontMatter(data, frontmatter, getDefaultStringType()) + ) + ); +} + +export function doAddOption( + data: string, + field: DataField, + value: Optional +): E.Either { + return F.pipe( + data, + decodeFrontMatter, + E.map((frontmatter) => ({ + ...frontmatter, + [field.name]: value, + })), + E.chain((frontmatter) => + encodeFrontMatter(data, frontmatter, getDefaultStringType()) + ) + ); +} + +export function doDeleteOption(data: string, field: string) { + return F.pipe( + data, + decodeFrontMatter, + E.map((frontmatter) => ({ + ...frontmatter, + [field]: undefined, + })), + E.chain((frontmatter) => + encodeFrontMatter(data, frontmatter, getDefaultStringType()) + ) + ); +} + export function createProject(): ProjectDefinition { return Object.assign({}, DEFAULT_PROJECT, { id: uuidv4(), diff --git a/src/lib/stores/dataframe.ts b/src/lib/stores/dataframe.ts index 4f1394fe..aebd9797 100644 --- a/src/lib/stores/dataframe.ts +++ b/src/lib/stores/dataframe.ts @@ -86,6 +86,41 @@ function createDataFrame() { }) ); }, + addOption(newField: DataField, position?: number) { + update((state) => + produce(state, (draft) => { + position + ? draft.fields.splice(position, 0, newField) + : draft.fields.push(newField); + }) + ); + }, + updateOption(updated: DataField, oldName?: string) { + update((state) => + produce(state, (draft) => { + draft.fields = draft.fields + .map((field) => (field.name === oldName ? updated : field)) + .filter((field) => field.name !== oldName); + + draft.records = draft.records.map((record) => + produce(record, (draft) => { + if (oldName) { + // @ts-ignore + draft.values[updated.name] = draft.values[oldName]; + delete draft.values[oldName]; + } + }) + ); + }) + ); + }, + deleteOption(fieldName: string) { + update((state) => + produce(state, (draft) => { + draft.records.values; + }) + ); + }, merge(updated: DataFrame) { update((existing) => produce(existing, (draft) => { diff --git a/src/lib/viewApi.ts b/src/lib/viewApi.ts index 55a38792..29b64145 100644 --- a/src/lib/viewApi.ts +++ b/src/lib/viewApi.ts @@ -66,4 +66,34 @@ export class ViewApi { field ); } + + addOption(field: DataField, value: Optional, position?: number) { + dataFrame.addOption(field, position); + + this.dataApi.addOption( + get(dataFrame).records.map((record) => record.id), + field, + value + ); + } + + updateOption(field: DataField, oldName?: string) { + dataFrame.updateOption(field, oldName); + + if (oldName) { + this.dataApi.renameOption( + get(dataFrame).records.map((record) => record.id), + oldName, + field.name + ); + } + } + + deleteOption(field: string) { + dataFrame.deleteOption(field); + this.dataApi.deleteOption( + get(dataFrame).records.map((record) => record.id), + field + ); + } } diff --git a/src/ui/app/toolbar/ViewItem.svelte b/src/ui/app/toolbar/ViewItem.svelte index f952c370..63d514a5 100644 --- a/src/ui/app/toolbar/ViewItem.svelte +++ b/src/ui/app/toolbar/ViewItem.svelte @@ -100,6 +100,8 @@ } }} on:blur={() => { + editing = false; + if (!error) { fallback = label; diff --git a/src/ui/views/Board/BoardView.svelte b/src/ui/views/Board/BoardView.svelte index a664ed15..83c6581b 100644 --- a/src/ui/views/Board/BoardView.svelte +++ b/src/ui/views/Board/BoardView.svelte @@ -10,6 +10,7 @@ import { notUndefined } from "src/lib/helpers"; import { i18n } from "src/lib/stores/i18n"; import { app } from "src/lib/stores/obsidian"; + import { settings } from "src/lib/stores/settings"; import type { ViewApi } from "src/lib/viewApi"; import type { ProjectDefinition } from "src/settings/settings"; import { CreateNoteModal } from "src/ui/modals/createNoteModal"; @@ -207,6 +208,61 @@ }).open(); }; + // catch rename error of yaml parser! + const handleColumnRename = ( + field: DataField | undefined, + names: string[], // all column names, detected & defined + records: DataRecord[], // notes that belong to the column + oldname: string, // old column id + newname: string // proposing column id + ) => { + if (!field) return; + if (oldname === newname) return; + + records.forEach((record) => { + api.updateRecord( + { + ...record, + values: { ...record.values, [field.name]: newname }, + }, + fields + ); + }); + + const projectFields = Object.fromEntries( + Object.entries(project.fieldConfig ?? {}).filter(([key, _]) => + fields.find((f) => f.name === key) + ) + ); + + if (field.typeConfig && field.typeConfig.options) { + const options = field.typeConfig.options.map((option) => + option === oldname ? newname : option + ); + + settings.updateProject({ + ...project, + fieldConfig: { + ...projectFields, + [field.name]: { + ...field.typeConfig, + options: options, + }, + }, + }); + } + + const columns = names.map((name) => (name === oldname ? newname : name)); + saveConfig({ + ...config, + columns: Object.fromEntries( + columns.map((name, i) => { + return [name, { weight: i }]; + }) + ), + }); + }; + const handleSortColumns = (field: DataField | undefined): OnSortColumns => (names) => { @@ -229,6 +285,53 @@ }); }; + function handleColumnAdd( + field: DataField | undefined, + columns: string[], // all column names, detected & predefined + name: string + ) { + if (!field) return; + + const projectFields = Object.fromEntries( + Object.entries(project.fieldConfig ?? {}).filter(([key, _]) => + fields.find((f) => f.name === key) + ) + ); + + if (field.typeConfig && field.typeConfig.options) { + settings.updateProject({ + ...project, + fieldConfig: { + ...projectFields, + [field.name]: { + ...field.typeConfig, + options: [...field.typeConfig.options, name], + }, + }, + }); + } else { + settings.updateProject({ + ...project, + fieldConfig: { + ...projectFields, + [field.name]: { + ...field.typeConfig, + options: [name], + }, + }, + }); + } + + saveConfig({ + ...config, + columns: Object.fromEntries( + [...columns, name].map((column, i) => { + return [column, { weight: i }]; + }) + ), + }); + } + function saveConfig(cfg: BoardConfig) { config = cfg; onConfigChange(cfg); @@ -262,6 +365,10 @@ onRecordAdd={handleRecordAdd(groupByField)} onRecordUpdate={handleRecordUpdate(groupByField)} onSortColumns={handleSortColumns(groupByField)} + onColumnAdd={(columns, name) => + handleColumnAdd(groupByField, columns, name)} + onColumnRename={(columns, records, oldname, newname) => + handleColumnRename(groupByField, columns, records, oldname, newname)} {readonly} richText={groupByField?.typeConfig?.richText ?? false} /> diff --git a/src/ui/views/Board/components/Board/Board.svelte b/src/ui/views/Board/components/Board/Board.svelte index b1ea6276..de76e5e4 100644 --- a/src/ui/views/Board/components/Board/Board.svelte +++ b/src/ui/views/Board/components/Board/Board.svelte @@ -3,6 +3,7 @@ import { dndzone } from "svelte-dnd-action"; import BoardColumn from "./BoardColumn.svelte"; + import NewColumn from "./NewColumn.svelte"; import type { Column, OnRecordAdd, @@ -20,6 +21,13 @@ export let onRecordAdd: OnRecordAdd; export let columnWidth: number; export let onSortColumns: OnSortColumns; + export let onColumnAdd: (columns: string[], name: string) => void; + export let onColumnRename: ( + columns: string[], + records: DataRecord[], + oldname: string, + newname: string + ) => void; export let includeFields: DataField[]; const flipDurationMs = 200; @@ -70,3 +78,70 @@ /> {/each} +
+
+ {#each columns as column (column.id)} + onRecordAdd(column.id)} + onColumnRename={(name) => + onColumnRename( + columns.map((col) => col.id), + column.records, + column.id, + name + )} + onDrop={(records) => { + records.forEach((record) => { + onRecordUpdate(column.id, record); + }); + }} + {includeFields} + /> + {/each} +
+ {#if !readonly} + + { + onColumnAdd( + columns.map((col) => col.id), + name + ); + }} + onValidate={() => true} + /> + + {/if} +
+ + diff --git a/src/ui/views/Board/components/Board/BoardColumn.svelte b/src/ui/views/Board/components/Board/BoardColumn.svelte index 3fd3e484..71e9e824 100644 --- a/src/ui/views/Board/components/Board/BoardColumn.svelte +++ b/src/ui/views/Board/components/Board/BoardColumn.svelte @@ -15,10 +15,18 @@ export let onDrop: OnRecordDrop; export let onRecordClick: OnRecordClick; export let onRecordAdd: () => void; + + export let onColumnRename: (name: string) => void;
- + onColumnRename(name)} + onValidate={() => true} + /> {#if !readonly} diff --git a/src/ui/views/Board/components/Board/ColumnHeader.svelte b/src/ui/views/Board/components/Board/ColumnHeader.svelte index 53b2fa8a..285c6908 100644 --- a/src/ui/views/Board/components/Board/ColumnHeader.svelte +++ b/src/ui/views/Board/components/Board/ColumnHeader.svelte @@ -2,10 +2,29 @@ import { MarkdownRenderer } from "obsidian"; import { app, view } from "src/lib/stores/obsidian"; import { getContext } from "svelte"; + import { TextInput, useClickOutside } from "obsidian-svelte"; export let value: string; + export let readonly: boolean = false; export let richText: boolean = false; + export let onValidate: (value: string) => boolean; + export let onRename: (value: string) => void; + + let editing: boolean = false; + let inputRef: HTMLInputElement; + $: if (editing && inputRef) { + inputRef.focus(); + inputRef.select(); + } + + let fallback: string = value; + function rollback() { + value = fallback; + } + + $: error = !onValidate(value); + const sourcePath = getContext("sourcePath") ?? ""; function useMarkdown(node: HTMLElement, value: string) { @@ -41,13 +60,54 @@ } -{#if richText} -
-{:else} -
- {value} -
-{/if} +
{ + if (!readonly) editing = true; + }} + use:useClickOutside={() => (editing = false)} +> + {#if editing} + { + if (event.key === "Enter") { + editing = false; + + if (fallback == value) { + return; + } + + if (!error) { + fallback = value; + onRename(value); + } else { + rollback(); + } + } + if (event.key === "Escape") { + editing = false; + rollback(); + } + }} + on:blur={() => { + editing = false; + if (!error) { + fallback = value; + onRename(value); + } else { + rollback(); + } + }} + /> + {:else if richText} + + {:else} + {value} + {/if} +
diff --git a/src/ui/views/Board/components/Board/NewColumn.svelte b/src/ui/views/Board/components/Board/NewColumn.svelte new file mode 100644 index 00000000..88b0fc87 --- /dev/null +++ b/src/ui/views/Board/components/Board/NewColumn.svelte @@ -0,0 +1,94 @@ + + +{#if editing} +
+ { + if (event.key === "Enter") { + editing = false; + + if (fallback == value) { + return; + } + + if (!error) { + fallback = value; + onColumnAdd(value); + } else { + rollback(); + } + } + if (event.key === "Escape") { + editing = false; + rollback(); + } + }} + on:blur={() => { + editing = false; + if (!error) { + fallback = value; + onColumnAdd(value); + } else { + rollback(); + } + }} + /> +
+ + + +
+{:else} + +{/if} + +