From 86d0b827ba90b955089c013f6630c07ffefe60c6 Mon Sep 17 00:00:00 2001 From: jack Date: Mon, 22 Jun 2026 22:04:38 +0800 Subject: [PATCH 1/2] feat(kanban): user-defined custom fields on cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let a board declare custom fields (key/label/type) that render as editable inputs on each card. File-board values live in card frontmatter (so they ride document sync to desktop + web); DB-board values live in properties_extra (cloud-only). - shared/lib/board.ts: BoardFieldDef + config.fields + card.custom + pickCustomFields() helper. - jtype-core: scan_board_cards now returns the full frontmatter map (properties) so the desktop can surface declared custom fields without re-reading files. - BoardPeek: render a typed input (text/number/date) per declared field, plus an inline "+ Add field" composer. - BoardSurface: pass fields + wire onAddField → setConfig (slug-keyed, deduped). - Desktop + web file-board adapters: read declared keys from frontmatter into card.custom and write them back on updateCard. DB board: read/write properties_extra (merged with the existing icon handling). - Thread `fields` through all three platform config types. - tests/unit/boardCustomFields.spec.ts + i18n (zh). Verified: cargo check + 34 jtype-core tests, root+web tsc, TS unit tests, and a throwaway harness confirmed typed field inputs populated from card data and the add-field composer appending a new field live. Follow-up: select-type fields with options, delete/rename field defs, showing custom fields on the card face. Co-Authored-By: Claude Opus 4.8 --- services/jtype-core/src/lib.rs | 4 ++ .../jtype-web/frontend/src/pages/Kanban.tsx | 18 +++++-- .../frontend/src/pages/WebBoardView.tsx | 5 ++ shared/components/board/BoardPeek.tsx | 47 ++++++++++++++++++- shared/components/board/BoardSurface.tsx | 12 +++++ shared/i18n/locales/en/messages.mjs | 2 +- shared/i18n/locales/en/messages.po | 4 ++ shared/i18n/locales/ja/messages.mjs | 2 +- shared/i18n/locales/ja/messages.po | 4 ++ shared/i18n/locales/ko/messages.mjs | 2 +- shared/i18n/locales/ko/messages.po | 4 ++ shared/i18n/locales/zh/messages.mjs | 2 +- shared/i18n/locales/zh/messages.po | 4 ++ shared/lib/board.ts | 22 +++++++++ src/components/BoardView.tsx | 6 ++- src/lib/types.ts | 4 ++ tests/unit/boardCustomFields.spec.ts | 22 +++++++++ 17 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 tests/unit/boardCustomFields.spec.ts diff --git a/services/jtype-core/src/lib.rs b/services/jtype-core/src/lib.rs index ef9ef31..19c22c3 100644 --- a/services/jtype-core/src/lib.rs +++ b/services/jtype-core/src/lib.rs @@ -1349,6 +1349,9 @@ pub struct BoardCardInfo { pub task_total: i64, pub icon: Option, pub excerpt: Option, + /// Full frontmatter key/values, so the board can surface user-defined custom + /// fields declared in the `.board` config without re-reading the file. + pub properties: HashMap, } /// The body content after the frontmatter block (for previews/excerpts). @@ -1482,6 +1485,7 @@ fn scan_board_cards_inner( task_total, icon: fm.get("icon").cloned().filter(|v| !v.is_empty()), excerpt: body_excerpt(&content), + properties: fm.clone(), }); } } diff --git a/services/jtype-web/frontend/src/pages/Kanban.tsx b/services/jtype-web/frontend/src/pages/Kanban.tsx index 225c182..34575e9 100644 --- a/services/jtype-web/frontend/src/pages/Kanban.tsx +++ b/services/jtype-web/frontend/src/pages/Kanban.tsx @@ -16,7 +16,7 @@ import { } from '../api' import { useConfirm, usePrompt } from '@shared/components/PromptDialogContext' import { BoardSurface, type BoardActions } from '@shared/components/board' -import { countTasks, bodyExcerpt, type BoardViewCard, type BoardViewConfig } from '@shared/lib/board' +import { countTasks, bodyExcerpt, pickCustomFields, type BoardViewCard, type BoardViewConfig } from '@shared/lib/board' import { useWorkspaceSocket } from '../hooks/useWorkspaceSocket' const VIEW_KEY = (boardId: string) => `kanban-view:${boardId}` @@ -122,6 +122,9 @@ export function Kanban() { .filter(c => !c.archivedAt) .map(c => { const tasks = countTasks(c.description ?? '') + const propsObj = c.propertiesExtra && typeof c.propertiesExtra === 'object' ? (c.propertiesExtra as Record) : {} + const propsStr: Record = {} + for (const [k, v] of Object.entries(propsObj)) if (typeof v === 'string') propsStr[k] = v return { id: c.id, columnKey: c.columnId, @@ -136,9 +139,10 @@ export function Kanban() { taskDone: tasks.done, taskTotal: tasks.total, excerpt: bodyExcerpt(c.description ?? ''), + custom: pickCustomFields(propsStr, view.fields), } }) - }, [board, memberName]) + }, [board, memberName, view.fields]) const viewConfig: BoardViewConfig = useMemo( () => @@ -148,6 +152,7 @@ export function Kanban() { columns: board.columns.slice().sort((a, b) => a.position - b.position).map(c => ({ key: c.id, name: c.name, color: c.color, limit: c.wipLimit })), groupBy: (view.groupBy as BoardViewConfig['groupBy']) ?? 'status', viewType: view.viewType ?? 'board', + fields: view.fields, colorColumns: view.colorColumns, doneColumn: view.doneColumn, } @@ -242,10 +247,13 @@ export function Kanban() { if (patch.due !== undefined) body.dueAt = patch.due ? `${patch.due} 00:00:00` : null if (patch.tags !== undefined) body.labelIds = patch.tags.map(t => t.id).filter(Boolean) as string[] if (patch.notes !== undefined) body.description = patch.notes - if (patch.icon !== undefined) { + if (patch.icon !== undefined || patch.custom !== undefined) { const cur = raw.propertiesExtra && typeof raw.propertiesExtra === 'object' ? { ...(raw.propertiesExtra as Record) } : {} - if (patch.icon) cur.icon = patch.icon - else delete cur.icon + if (patch.icon !== undefined) { + if (patch.icon) cur.icon = patch.icon + else delete cur.icon + } + if (patch.custom !== undefined) for (const [k, v] of Object.entries(patch.custom)) { if (v) cur[k] = v; else delete cur[k] } body.propertiesExtra = cur } try { diff --git a/services/jtype-web/frontend/src/pages/WebBoardView.tsx b/services/jtype-web/frontend/src/pages/WebBoardView.tsx index 3cc3c9c..462a11b 100644 --- a/services/jtype-web/frontend/src/pages/WebBoardView.tsx +++ b/services/jtype-web/frontend/src/pages/WebBoardView.tsx @@ -8,6 +8,7 @@ import { bodyExcerpt, countTasks, parseTagList, + pickCustomFields, slugify, type BoardViewCard, type BoardViewConfig, @@ -23,6 +24,7 @@ type BoardConfigJSON = { doneColumn?: string colorColumns?: boolean viewType?: 'board' | 'table' + fields?: { key: string; label: string; type?: 'text' | 'number' | 'date' }[] } function rand() { @@ -92,6 +94,7 @@ export function WebBoardView({ taskDone: tasks.done, taskTotal: tasks.total, excerpt: bodyExcerpt(fm.body), + custom: pickCustomFields(fm.data, cfg.fields), }) } setMetaByPath(nextMeta) @@ -175,6 +178,7 @@ export function WebBoardView({ doneColumn: config.doneColumn, colorColumns: config.colorColumns, viewType: config.viewType, + fields: config.fields, groupBy: (config.groupBy as BoardViewConfig['groupBy']) || 'status', } : { title: boardDir, columns: [] }, @@ -215,6 +219,7 @@ export function WebBoardView({ if (patch.due !== undefined) next.due = patch.due ?? '' if (patch.icon !== undefined) next.icon = patch.icon ?? '' if (patch.tags !== undefined) next.tags = patch.tags.map((t) => t.label).join(', ') + if (patch.custom !== undefined) for (const [k, v] of Object.entries(patch.custom)) next[k] = v ?? '' const newBody = patch.notes !== undefined ? patch.notes : body try { await saveCard(id, next, newBody) diff --git a/shared/components/board/BoardPeek.tsx b/shared/components/board/BoardPeek.tsx index 83bd58e..24c5338 100644 --- a/shared/components/board/BoardPeek.tsx +++ b/shared/components/board/BoardPeek.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { Fragment, useCallback, useEffect, useRef, useState } from "react"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { XMarkIcon, TrashIcon, ArrowsPointingOutIcon, EyeIcon, PencilSquareIcon } from "@heroicons/react/24/outline"; @@ -6,7 +6,7 @@ import { renderToContainer } from "../../lib/markdown"; import { PRIORITIES, type BoardViewCard } from "../../lib/board"; import { fieldCls, EmojiField, ListboxSelect, TagMultiSelect } from "./controls"; import type { BoardOption } from "./types"; -import type { BoardTag } from "../../lib/board"; +import type { BoardTag, BoardFieldDef } from "../../lib/board"; /** * Side peek for editing a card without leaving the board. Platform-agnostic: it @@ -18,6 +18,8 @@ export function BoardPeek({ statusOptions, assigneeOptions, tagOptions, + fields, + onAddField, loadNotes, onChange, onClose, @@ -28,12 +30,17 @@ export function BoardPeek({ statusOptions: BoardOption[]; assigneeOptions?: BoardOption[]; tagOptions?: BoardTag[]; + /** Board-level custom field definitions to render as editable inputs. */ + fields?: BoardFieldDef[]; + /** Add a new custom field to the board (collected inline). */ + onAddField?: (label: string) => void; loadNotes?: (id: string) => Promise; onChange: (patch: Partial) => void; onClose: () => void; onDelete: () => void; onOpenFull?: () => void; }) { + const [newField, setNewField] = useState(""); const [draft, setDraft] = useState(card); const [notes, setNotes] = useState(card.notes ?? ""); const [mode, setMode] = useState<"write" | "preview">("write"); @@ -204,6 +211,42 @@ export function BoardPeek({ } /> )} + + {fields?.map((f) => ( + + + {f.label} + + setField({ custom: { ...(draft.custom ?? {}), [f.key]: e.target.value } }, f.type === "date" || f.type === "number")} + /> + + ))} + {onAddField && ( + <> + +
{ + e.preventDefault(); + const label = newField.trim(); + if (label) { + onAddField(label); + setNewField(""); + } + }} + > + setNewField(e.target.value)} + /> +
+ + )}
diff --git a/shared/components/board/BoardSurface.tsx b/shared/components/board/BoardSurface.tsx index 787f3f1..c1e37fc 100644 --- a/shared/components/board/BoardSurface.tsx +++ b/shared/components/board/BoardSurface.tsx @@ -35,6 +35,7 @@ import { PRIORITY_STYLE, effectiveColumns, groupValueOf, + slugify, sortCards as sortCardsFn, todayStr, visibleCards as visibleCardsFn, @@ -818,6 +819,17 @@ export function BoardSurface({ statusOptions={config.columns.map((c) => ({ value: c.key, label: c.name }))} assigneeOptions={assigneeOptions} tagOptions={tagOptions} + fields={config.fields} + onAddField={(label) => { + const existing = new Set((config.fields ?? []).map((f) => f.key)); + let key = slugify(label); + if (existing.has(key)) { + let n = 2; + while (existing.has(`${key}-${n}`)) n += 1; + key = `${key}-${n}`; + } + void actions.setConfig({ fields: [...(config.fields ?? []), { key, label }] }); + }} loadNotes={loadNotes} onChange={(patch) => void actions.updateCard(selected.id, patch)} onClose={() => setSelectedId(null)} diff --git a/shared/i18n/locales/en/messages.mjs b/shared/i18n/locales/en/messages.mjs index 1242b40..b19451d 100644 --- a/shared/i18n/locales/en/messages.mjs +++ b/shared/i18n/locales/en/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"Notes\"],\"1YABGm\":[\"Link (Ctrl+K)\"],\"1hKEom\":[\"Priority\"],\"2wxgft\":[\"Rename\"],\"3qkggm\":[\"Fullscreen\"],\"4gdyen\":[\"Local (yours)\"],\"4hJhzz\":[\"Table\"],\"54sFiP\":[\"flowchart TD\\n A[Start] --> B[End]\"],\"5Q_DQ6\":[\"Inline Code\"],\"7VpPHA\":[\"Confirm\"],\"8PifYj\":[\"Mermaid diagram\"],\"8hSn0h\":[\"Result (editable)\"],\"8lE269\":[\"Sort: Manual\"],\"9gxam6\":[\"Could not render this Draw.io diagram.\"],\"AC9Gkf\":[\"Expand column\"],\"AS5WO9\":[\"Could not render this PDF.\"],\"AVreQ5\":[\"Drag to resize\"],\"AgvHni\":[\"Add column\"],\"AxAubu\":[\"Group: Assignee\"],\"BfMZ7w\":[\"Accept cloud\"],\"BnmEvM\":[\"Save as template\"],\"EWPtMO\":[\"Code\"],\"EbMPZJ\":[\"Unassigned\"],\"G4qrLy\":[\"Unset done column\"],\"GKu3m4\":[\"No labels\"],\"Gpfctt\":[\"Due\"],\"H_SQFv\":[\"No color\"],\"I6SWEy\":[\"Split\"],\"ICip_B\":[\"Cloud (remote)\"],\"Ik60OC\":[\"Open in editor\"],\"Iw6WJa\":[\"Set WIP limit\"],\"JTYvAw\":[\"Search cards\"],\"K_F6pa\":[\"Saving…\"],\"KmydK6\":[\"Bold\"],\"KvW1VO\":[\"Draw.io diagram\"],\"LQn6-8\":[\"Accept local\"],\"MHrjPM\":[\"Title\"],\"OYHzN1\":[\"Tags\"],\"OepdfE\":[\"Group: Status\"],\"Q2mGA7\":[\"Clear filter\"],\"QD8opX\":[\"Board\"],\"QlsPZy\":[\"Write Mermaid syntax to see the diagram.\"],\"S5Qbb1\":[\"comma, separated\"],\"UQOvxZ\":[\"Blank card\"],\"VNa_N2\":[\"This file type can not be previewed yet.\"],\"WSP6v1\":[\"Sort: Priority\"],\"X03-eC\":[\"Please enter a value.\"],\"Ya7bZl\":[\"Diagram error\"],\"Zot9XS\":[\"No cards\"],\"_5CsXX\":[\"Done column\"],\"_EsjyQ\":[\"Use this\"],\"a6uhHr\":[\"Bold (Ctrl+B)\"],\"abUZlY\":[\"Add details...\"],\"agOeRN\":[\"Could not render this API specification.\"],\"b4hVKD\":[\"Color columns\"],\"cfaWH-\":[\"Add labels\"],\"cnGeoo\":[\"Delete\"],\"d5z6xQ\":[\"WIP limit \",[\"0\"]],\"dEgA5A\":[\"Cancel\"],\"euc6Ns\":[\"Duplicate\"],\"fYcKtB\":[\"Sort: Due\"],\"gLDJuJ\":[\"Untitled card\"],\"hnK1gR\":[\"PDF document\"],\"i4_LY_\":[\"Write\"],\"iTylMl\":[\"Templates\"],\"iYVqZq\":[\"Column name\"],\"jZlrte\":[\"Color\"],\"kZlRKE\":[\"Mermaid source\"],\"kryGs-\":[\"Card\"],\"lCF0wC\":[\"Refresh\"],\"ltF1xa\":[\"Save merged result\"],\"nabda1\":[\"Delete card\"],\"o7J4JM\":[\"Filter\"],\"ojKCLU\":[\"Assignee\"],\"p9yTeb\":[\"Sort: Title\"],\"pKztsX\":[\"Open in full editor\"],\"pnrmSP\":[\"New card\"],\"pwN6Ae\":[\"Collapse column\"],\"pzutoc\":[\"Italic\"],\"rdUucN\":[\"Preview\"],\"sCzmvQ\":[\"cards\"],\"sQpDn6\":[\"Exit fullscreen\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" Conflict\",[\"1\"],\" to Resolve\"],\"u2IprG\":[\"Card title (Enter to add, Esc to cancel)\"],\"uAQUqI\":[\"Status\"],\"wf6Djn\":[\"Italic (Ctrl+I)\"],\"wtw-au\":[\"Set as done column\"],\"wwu18a\":[\"Icon\"],\"y1eoq1\":[\"Copy link\"],\"y9cj46\":[\"Group: Priority\"],\"ybGQtY\":[\"← Back to list\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"zOc0vf\":[\"No icon\"],\"zga9sT\":[\"OK\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"Notes\"],\"1YABGm\":[\"Link (Ctrl+K)\"],\"1hKEom\":[\"Priority\"],\"2wxgft\":[\"Rename\"],\"3qkggm\":[\"Fullscreen\"],\"4gdyen\":[\"Local (yours)\"],\"4hJhzz\":[\"Table\"],\"54sFiP\":[\"flowchart TD\\n A[Start] --> B[End]\"],\"5Q_DQ6\":[\"Inline Code\"],\"7VpPHA\":[\"Confirm\"],\"8PifYj\":[\"Mermaid diagram\"],\"8hSn0h\":[\"Result (editable)\"],\"8lE269\":[\"Sort: Manual\"],\"9gxam6\":[\"Could not render this Draw.io diagram.\"],\"AC9Gkf\":[\"Expand column\"],\"AS5WO9\":[\"Could not render this PDF.\"],\"AVreQ5\":[\"Drag to resize\"],\"AgvHni\":[\"Add column\"],\"AxAubu\":[\"Group: Assignee\"],\"BfMZ7w\":[\"Accept cloud\"],\"BnmEvM\":[\"Save as template\"],\"EWPtMO\":[\"Code\"],\"EbMPZJ\":[\"Unassigned\"],\"G4qrLy\":[\"Unset done column\"],\"GKu3m4\":[\"No labels\"],\"Gpfctt\":[\"Due\"],\"H_SQFv\":[\"No color\"],\"I6SWEy\":[\"Split\"],\"ICip_B\":[\"Cloud (remote)\"],\"Ik60OC\":[\"Open in editor\"],\"Iw6WJa\":[\"Set WIP limit\"],\"JTYvAw\":[\"Search cards\"],\"K_F6pa\":[\"Saving…\"],\"KmydK6\":[\"Bold\"],\"KvW1VO\":[\"Draw.io diagram\"],\"LQn6-8\":[\"Accept local\"],\"MHrjPM\":[\"Title\"],\"OYHzN1\":[\"Tags\"],\"OepdfE\":[\"Group: Status\"],\"Q2mGA7\":[\"Clear filter\"],\"QD8opX\":[\"Board\"],\"QlsPZy\":[\"Write Mermaid syntax to see the diagram.\"],\"S5Qbb1\":[\"comma, separated\"],\"UQOvxZ\":[\"Blank card\"],\"VNa_N2\":[\"This file type can not be previewed yet.\"],\"WSP6v1\":[\"Sort: Priority\"],\"X03-eC\":[\"Please enter a value.\"],\"Ya7bZl\":[\"Diagram error\"],\"Zot9XS\":[\"No cards\"],\"_5CsXX\":[\"Done column\"],\"_EsjyQ\":[\"Use this\"],\"a6uhHr\":[\"Bold (Ctrl+B)\"],\"abUZlY\":[\"Add details...\"],\"agOeRN\":[\"Could not render this API specification.\"],\"b4hVKD\":[\"Color columns\"],\"cfaWH-\":[\"Add labels\"],\"cnGeoo\":[\"Delete\"],\"d5z6xQ\":[\"WIP limit \",[\"0\"]],\"dEgA5A\":[\"Cancel\"],\"euc6Ns\":[\"Duplicate\"],\"fYcKtB\":[\"Sort: Due\"],\"gLDJuJ\":[\"Untitled card\"],\"hnK1gR\":[\"PDF document\"],\"i4_LY_\":[\"Write\"],\"iTylMl\":[\"Templates\"],\"iYVqZq\":[\"Column name\"],\"jZlrte\":[\"Color\"],\"kZlRKE\":[\"Mermaid source\"],\"kryGs-\":[\"Card\"],\"lCF0wC\":[\"Refresh\"],\"ltF1xa\":[\"Save merged result\"],\"nabda1\":[\"Delete card\"],\"o7J4JM\":[\"Filter\"],\"ojKCLU\":[\"Assignee\"],\"p9yTeb\":[\"Sort: Title\"],\"pKztsX\":[\"Open in full editor\"],\"pnrmSP\":[\"New card\"],\"pwN6Ae\":[\"Collapse column\"],\"pzutoc\":[\"Italic\"],\"rdUucN\":[\"Preview\"],\"sCzmvQ\":[\"cards\"],\"sQpDn6\":[\"Exit fullscreen\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" Conflict\",[\"1\"],\" to Resolve\"],\"u2IprG\":[\"Card title (Enter to add, Esc to cancel)\"],\"uAQUqI\":[\"Status\"],\"wf6Djn\":[\"Italic (Ctrl+I)\"],\"wtw-au\":[\"Set as done column\"],\"wwu18a\":[\"Icon\"],\"y1eoq1\":[\"Copy link\"],\"y9cj46\":[\"Group: Priority\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← Back to list\"],\"yz7wBu\":[\"Close\"],\"yzF66j\":[\"Link\"],\"zOc0vf\":[\"No icon\"],\"zga9sT\":[\"OK\"]}"); \ No newline at end of file diff --git a/shared/i18n/locales/en/messages.po b/shared/i18n/locales/en/messages.po index 075d16d..1922806 100644 --- a/shared/i18n/locales/en/messages.po +++ b/shared/i18n/locales/en/messages.po @@ -22,6 +22,10 @@ msgstr "" msgid "← Back to list" msgstr "← Back to list" +#: shared/components/board/BoardPeek.tsx +msgid "+ Add field" +msgstr "+ Add field" + #. placeholder {0}: conflicts.length #. placeholder {1}: conflicts.length > 1 ? "s" : "" #: shared/components/ConflictResolver.tsx diff --git a/shared/i18n/locales/ja/messages.mjs b/shared/i18n/locales/ja/messages.mjs index 6262cda..fccc534 100644 --- a/shared/i18n/locales/ja/messages.mjs +++ b/shared/i18n/locales/ja/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"ノート\"],\"1YABGm\":[\"リンク (Ctrl+K)\"],\"1hKEom\":[\"優先度\"],\"2wxgft\":[\"名前を変更\"],\"3qkggm\":[\"全画面表示\"],\"4gdyen\":[\"ローカル(自分の)\"],\"4hJhzz\":[\"表\"],\"54sFiP\":[\"flowchart TD\\n A[開始] --> B[終了]\"],\"5Q_DQ6\":[\"インラインコード\"],\"7VpPHA\":[\"確認\"],\"8PifYj\":[\"Mermaid 図\"],\"8hSn0h\":[\"結果(編集可能)\"],\"8lE269\":[\"並べ替え:手動\"],\"9gxam6\":[\"この Draw.io 図をレンダリングできませんでした。\"],\"AC9Gkf\":[\"列を展開\"],\"AS5WO9\":[\"この PDF をレンダリングできませんでした。\"],\"AVreQ5\":[\"ドラッグしてサイズ変更\"],\"AgvHni\":[\"列を追加\"],\"AxAubu\":[\"グループ:担当者\"],\"BfMZ7w\":[\"クラウドを採用\"],\"BnmEvM\":[\"テンプレートとして保存\"],\"EWPtMO\":[\"コード\"],\"EbMPZJ\":[\"未割り当て\"],\"G4qrLy\":[\"完了列を解除\"],\"GKu3m4\":[\"ラベルなし\"],\"Gpfctt\":[\"期限\"],\"H_SQFv\":[\"色なし\"],\"I6SWEy\":[\"分割\"],\"ICip_B\":[\"クラウド(リモート)\"],\"Ik60OC\":[\"エディターで開く\"],\"Iw6WJa\":[\"WIP 制限を設定\"],\"JTYvAw\":[\"カードを検索\"],\"K_F6pa\":[\"保存中…\"],\"KmydK6\":[\"太字\"],\"KvW1VO\":[\"Draw.io 図\"],\"LQn6-8\":[\"ローカルを採用\"],\"MHrjPM\":[\"タイトル\"],\"OYHzN1\":[\"タグ\"],\"OepdfE\":[\"グループ:ステータス\"],\"Q2mGA7\":[\"フィルターをクリア\"],\"QD8opX\":[\"ボード\"],\"QlsPZy\":[\"Mermaid 構文を書くと図が表示されます。\"],\"S5Qbb1\":[\"カンマ区切り\"],\"UQOvxZ\":[\"空のカード\"],\"VNa_N2\":[\"このファイル形式はまだプレビューできません。\"],\"WSP6v1\":[\"並べ替え:優先度\"],\"X03-eC\":[\"値を入力してください。\"],\"Ya7bZl\":[\"図のエラー\"],\"Zot9XS\":[\"カードなし\"],\"_5CsXX\":[\"完了列\"],\"_EsjyQ\":[\"これを使用\"],\"a6uhHr\":[\"太字 (Ctrl+B)\"],\"abUZlY\":[\"詳細を追加...\"],\"agOeRN\":[\"この API 仕様をレンダリングできませんでした。\"],\"b4hVKD\":[\"色付き列\"],\"cfaWH-\":[\"ラベルを追加\"],\"cnGeoo\":[\"削除\"],\"d5z6xQ\":[\"WIP 制限 \",[\"0\"]],\"dEgA5A\":[\"キャンセル\"],\"euc6Ns\":[\"複製\"],\"fYcKtB\":[\"並べ替え:期限\"],\"gLDJuJ\":[\"無題のカード\"],\"hnK1gR\":[\"PDF ドキュメント\"],\"i4_LY_\":[\"記述\"],\"iTylMl\":[\"テンプレート\"],\"iYVqZq\":[\"列名\"],\"jZlrte\":[\"カラー\"],\"kZlRKE\":[\"Mermaid ソース\"],\"kryGs-\":[\"カード\"],\"lCF0wC\":[\"更新\"],\"ltF1xa\":[\"マージ結果を保存\"],\"nabda1\":[\"カードを削除\"],\"o7J4JM\":[\"フィルター\"],\"ojKCLU\":[\"担当者\"],\"p9yTeb\":[\"並べ替え:タイトル\"],\"pKztsX\":[\"フルエディターで開く\"],\"pnrmSP\":[\"新規カード\"],\"pwN6Ae\":[\"列を折りたたむ\"],\"pzutoc\":[\"イタリック\"],\"rdUucN\":[\"プレビュー\"],\"sCzmvQ\":[\"枚のカード\"],\"sQpDn6\":[\"全画面表示を終了\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 件の競合\",[\"1\"],\"を解決中\"],\"u2IprG\":[\"カードのタイトル(Enter で追加、Esc でキャンセル)\"],\"uAQUqI\":[\"ステータス\"],\"wf6Djn\":[\"イタリック (Ctrl+I)\"],\"wtw-au\":[\"完了列に設定\"],\"wwu18a\":[\"アイコン\"],\"y1eoq1\":[\"リンクをコピー\"],\"y9cj46\":[\"グループ:優先度\"],\"ybGQtY\":[\"← リストに戻る\"],\"yz7wBu\":[\"閉じる\"],\"yzF66j\":[\"リンク\"],\"zOc0vf\":[\"アイコンなし\"],\"zga9sT\":[\"OK\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"ノート\"],\"1YABGm\":[\"リンク (Ctrl+K)\"],\"1hKEom\":[\"優先度\"],\"2wxgft\":[\"名前を変更\"],\"3qkggm\":[\"全画面表示\"],\"4gdyen\":[\"ローカル(自分の)\"],\"4hJhzz\":[\"表\"],\"54sFiP\":[\"flowchart TD\\n A[開始] --> B[終了]\"],\"5Q_DQ6\":[\"インラインコード\"],\"7VpPHA\":[\"確認\"],\"8PifYj\":[\"Mermaid 図\"],\"8hSn0h\":[\"結果(編集可能)\"],\"8lE269\":[\"並べ替え:手動\"],\"9gxam6\":[\"この Draw.io 図をレンダリングできませんでした。\"],\"AC9Gkf\":[\"列を展開\"],\"AS5WO9\":[\"この PDF をレンダリングできませんでした。\"],\"AVreQ5\":[\"ドラッグしてサイズ変更\"],\"AgvHni\":[\"列を追加\"],\"AxAubu\":[\"グループ:担当者\"],\"BfMZ7w\":[\"クラウドを採用\"],\"BnmEvM\":[\"テンプレートとして保存\"],\"EWPtMO\":[\"コード\"],\"EbMPZJ\":[\"未割り当て\"],\"G4qrLy\":[\"完了列を解除\"],\"GKu3m4\":[\"ラベルなし\"],\"Gpfctt\":[\"期限\"],\"H_SQFv\":[\"色なし\"],\"I6SWEy\":[\"分割\"],\"ICip_B\":[\"クラウド(リモート)\"],\"Ik60OC\":[\"エディターで開く\"],\"Iw6WJa\":[\"WIP 制限を設定\"],\"JTYvAw\":[\"カードを検索\"],\"K_F6pa\":[\"保存中…\"],\"KmydK6\":[\"太字\"],\"KvW1VO\":[\"Draw.io 図\"],\"LQn6-8\":[\"ローカルを採用\"],\"MHrjPM\":[\"タイトル\"],\"OYHzN1\":[\"タグ\"],\"OepdfE\":[\"グループ:ステータス\"],\"Q2mGA7\":[\"フィルターをクリア\"],\"QD8opX\":[\"ボード\"],\"QlsPZy\":[\"Mermaid 構文を書くと図が表示されます。\"],\"S5Qbb1\":[\"カンマ区切り\"],\"UQOvxZ\":[\"空のカード\"],\"VNa_N2\":[\"このファイル形式はまだプレビューできません。\"],\"WSP6v1\":[\"並べ替え:優先度\"],\"X03-eC\":[\"値を入力してください。\"],\"Ya7bZl\":[\"図のエラー\"],\"Zot9XS\":[\"カードなし\"],\"_5CsXX\":[\"完了列\"],\"_EsjyQ\":[\"これを使用\"],\"a6uhHr\":[\"太字 (Ctrl+B)\"],\"abUZlY\":[\"詳細を追加...\"],\"agOeRN\":[\"この API 仕様をレンダリングできませんでした。\"],\"b4hVKD\":[\"色付き列\"],\"cfaWH-\":[\"ラベルを追加\"],\"cnGeoo\":[\"削除\"],\"d5z6xQ\":[\"WIP 制限 \",[\"0\"]],\"dEgA5A\":[\"キャンセル\"],\"euc6Ns\":[\"複製\"],\"fYcKtB\":[\"並べ替え:期限\"],\"gLDJuJ\":[\"無題のカード\"],\"hnK1gR\":[\"PDF ドキュメント\"],\"i4_LY_\":[\"記述\"],\"iTylMl\":[\"テンプレート\"],\"iYVqZq\":[\"列名\"],\"jZlrte\":[\"カラー\"],\"kZlRKE\":[\"Mermaid ソース\"],\"kryGs-\":[\"カード\"],\"lCF0wC\":[\"更新\"],\"ltF1xa\":[\"マージ結果を保存\"],\"nabda1\":[\"カードを削除\"],\"o7J4JM\":[\"フィルター\"],\"ojKCLU\":[\"担当者\"],\"p9yTeb\":[\"並べ替え:タイトル\"],\"pKztsX\":[\"フルエディターで開く\"],\"pnrmSP\":[\"新規カード\"],\"pwN6Ae\":[\"列を折りたたむ\"],\"pzutoc\":[\"イタリック\"],\"rdUucN\":[\"プレビュー\"],\"sCzmvQ\":[\"枚のカード\"],\"sQpDn6\":[\"全画面表示を終了\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 件の競合\",[\"1\"],\"を解決中\"],\"u2IprG\":[\"カードのタイトル(Enter で追加、Esc でキャンセル)\"],\"uAQUqI\":[\"ステータス\"],\"wf6Djn\":[\"イタリック (Ctrl+I)\"],\"wtw-au\":[\"完了列に設定\"],\"wwu18a\":[\"アイコン\"],\"y1eoq1\":[\"リンクをコピー\"],\"y9cj46\":[\"グループ:優先度\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← リストに戻る\"],\"yz7wBu\":[\"閉じる\"],\"yzF66j\":[\"リンク\"],\"zOc0vf\":[\"アイコンなし\"],\"zga9sT\":[\"OK\"]}"); \ No newline at end of file diff --git a/shared/i18n/locales/ja/messages.po b/shared/i18n/locales/ja/messages.po index 6e5e086..2942be1 100644 --- a/shared/i18n/locales/ja/messages.po +++ b/shared/i18n/locales/ja/messages.po @@ -22,6 +22,10 @@ msgstr "" msgid "← Back to list" msgstr "← リストに戻る" +#: shared/components/board/BoardPeek.tsx +msgid "+ Add field" +msgstr "" + #. placeholder {0}: conflicts.length #. placeholder {1}: conflicts.length > 1 ? "s" : "" #: shared/components/ConflictResolver.tsx diff --git a/shared/i18n/locales/ko/messages.mjs b/shared/i18n/locales/ko/messages.mjs index a03ec1a..bd7f29e 100644 --- a/shared/i18n/locales/ko/messages.mjs +++ b/shared/i18n/locales/ko/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"o7J4JM\":[\"필터\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"o7J4JM\":[\"필터\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"yEbJGs\":[\"+ Add field\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); \ No newline at end of file diff --git a/shared/i18n/locales/ko/messages.po b/shared/i18n/locales/ko/messages.po index 0179773..36374ef 100644 --- a/shared/i18n/locales/ko/messages.po +++ b/shared/i18n/locales/ko/messages.po @@ -22,6 +22,10 @@ msgstr "" msgid "← Back to list" msgstr "← 목록으로" +#: shared/components/board/BoardPeek.tsx +msgid "+ Add field" +msgstr "" + #. placeholder {0}: conflicts.length #. placeholder {1}: conflicts.length > 1 ? "s" : "" #: shared/components/ConflictResolver.tsx diff --git a/shared/i18n/locales/zh/messages.mjs b/shared/i18n/locales/zh/messages.mjs index f5caa7d..9057eb6 100644 --- a/shared/i18n/locales/zh/messages.mjs +++ b/shared/i18n/locales/zh/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"备注\"],\"1YABGm\":[\"链接 (Ctrl+K)\"],\"1hKEom\":[\"优先级\"],\"2wxgft\":[\"重命名\"],\"3qkggm\":[\"全屏\"],\"4gdyen\":[\"本地(我的)\"],\"4hJhzz\":[\"表格\"],\"54sFiP\":[\"flowchart TD\\n A[开始] --> B[结束]\"],\"5Q_DQ6\":[\"行内代码\"],\"7VpPHA\":[\"确认\"],\"8PifYj\":[\"Mermaid 图表\"],\"8hSn0h\":[\"结果(可编辑)\"],\"8lE269\":[\"排序:手动\"],\"9gxam6\":[\"无法渲染此 Draw.io 图表。\"],\"AC9Gkf\":[\"展开列\"],\"AS5WO9\":[\"无法渲染此 PDF。\"],\"AVreQ5\":[\"拖动调整宽度\"],\"AgvHni\":[\"添加列\"],\"AxAubu\":[\"分组:负责人\"],\"BfMZ7w\":[\"接受云端\"],\"BnmEvM\":[\"存为模板\"],\"EWPtMO\":[\"代码\"],\"EbMPZJ\":[\"未分配\"],\"G4qrLy\":[\"取消完成列\"],\"GKu3m4\":[\"暂无标签\"],\"Gpfctt\":[\"截止日期\"],\"H_SQFv\":[\"无颜色\"],\"I6SWEy\":[\"分栏\"],\"ICip_B\":[\"云端(远程)\"],\"Ik60OC\":[\"在编辑器中打开\"],\"Iw6WJa\":[\"设置 WIP 限制\"],\"JTYvAw\":[\"搜索卡片\"],\"K_F6pa\":[\"保存中…\"],\"KmydK6\":[\"粗体\"],\"KvW1VO\":[\"Draw.io 图表\"],\"LQn6-8\":[\"接受本地\"],\"MHrjPM\":[\"标题\"],\"OYHzN1\":[\"标签\"],\"OepdfE\":[\"分组:状态\"],\"Q2mGA7\":[\"清除筛选\"],\"QD8opX\":[\"看板\"],\"QlsPZy\":[\"输入 Mermaid 语法以查看图表。\"],\"S5Qbb1\":[\"用逗号分隔\"],\"UQOvxZ\":[\"空白卡片\"],\"VNa_N2\":[\"暂不支持预览此文件类型。\"],\"WSP6v1\":[\"排序:优先级\"],\"X03-eC\":[\"请输入内容。\"],\"Ya7bZl\":[\"图表错误\"],\"Zot9XS\":[\"暂无卡片\"],\"_5CsXX\":[\"完成列\"],\"_EsjyQ\":[\"使用此版本\"],\"a6uhHr\":[\"粗体 (Ctrl+B)\"],\"abUZlY\":[\"添加详情...\"],\"agOeRN\":[\"无法渲染此 API 规范。\"],\"b4hVKD\":[\"彩色列\"],\"cfaWH-\":[\"添加标签\"],\"cnGeoo\":[\"删除\"],\"d5z6xQ\":[\"WIP 限制 \",[\"0\"]],\"dEgA5A\":[\"取消\"],\"euc6Ns\":[\"复制卡片\"],\"fYcKtB\":[\"排序:截止\"],\"gLDJuJ\":[\"未命名卡片\"],\"hnK1gR\":[\"PDF 文档\"],\"i4_LY_\":[\"写作\"],\"iTylMl\":[\"模板\"],\"iYVqZq\":[\"列名称\"],\"jZlrte\":[\"颜色\"],\"kZlRKE\":[\"Mermaid 源码\"],\"kryGs-\":[\"卡片\"],\"lCF0wC\":[\"刷新\"],\"ltF1xa\":[\"保存合并结果\"],\"nabda1\":[\"删除卡片\"],\"o7J4JM\":[\"筛选\"],\"ojKCLU\":[\"负责人\"],\"p9yTeb\":[\"排序:标题\"],\"pKztsX\":[\"在完整编辑器中打开\"],\"pnrmSP\":[\"新建卡片\"],\"pwN6Ae\":[\"折叠列\"],\"pzutoc\":[\"斜体\"],\"rdUucN\":[\"预览\"],\"sCzmvQ\":[\"张卡片\"],\"sQpDn6\":[\"退出全屏\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 个冲突\",[\"1\"],\"待解决\"],\"u2IprG\":[\"卡片标题(回车添加,Esc 取消)\"],\"uAQUqI\":[\"状态\"],\"wf6Djn\":[\"斜体 (Ctrl+I)\"],\"wtw-au\":[\"设为完成列\"],\"wwu18a\":[\"图标\"],\"y1eoq1\":[\"复制链接\"],\"y9cj46\":[\"分组:优先级\"],\"ybGQtY\":[\"← 返回列表\"],\"yz7wBu\":[\"关闭\"],\"yzF66j\":[\"链接\"],\"zOc0vf\":[\"无图标\"],\"zga9sT\":[\"确定\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"1DBGsz\":[\"备注\"],\"1YABGm\":[\"链接 (Ctrl+K)\"],\"1hKEom\":[\"优先级\"],\"2wxgft\":[\"重命名\"],\"3qkggm\":[\"全屏\"],\"4gdyen\":[\"本地(我的)\"],\"4hJhzz\":[\"表格\"],\"54sFiP\":[\"flowchart TD\\n A[开始] --> B[结束]\"],\"5Q_DQ6\":[\"行内代码\"],\"7VpPHA\":[\"确认\"],\"8PifYj\":[\"Mermaid 图表\"],\"8hSn0h\":[\"结果(可编辑)\"],\"8lE269\":[\"排序:手动\"],\"9gxam6\":[\"无法渲染此 Draw.io 图表。\"],\"AC9Gkf\":[\"展开列\"],\"AS5WO9\":[\"无法渲染此 PDF。\"],\"AVreQ5\":[\"拖动调整宽度\"],\"AgvHni\":[\"添加列\"],\"AxAubu\":[\"分组:负责人\"],\"BfMZ7w\":[\"接受云端\"],\"BnmEvM\":[\"存为模板\"],\"EWPtMO\":[\"代码\"],\"EbMPZJ\":[\"未分配\"],\"G4qrLy\":[\"取消完成列\"],\"GKu3m4\":[\"暂无标签\"],\"Gpfctt\":[\"截止日期\"],\"H_SQFv\":[\"无颜色\"],\"I6SWEy\":[\"分栏\"],\"ICip_B\":[\"云端(远程)\"],\"Ik60OC\":[\"在编辑器中打开\"],\"Iw6WJa\":[\"设置 WIP 限制\"],\"JTYvAw\":[\"搜索卡片\"],\"K_F6pa\":[\"保存中…\"],\"KmydK6\":[\"粗体\"],\"KvW1VO\":[\"Draw.io 图表\"],\"LQn6-8\":[\"接受本地\"],\"MHrjPM\":[\"标题\"],\"OYHzN1\":[\"标签\"],\"OepdfE\":[\"分组:状态\"],\"Q2mGA7\":[\"清除筛选\"],\"QD8opX\":[\"看板\"],\"QlsPZy\":[\"输入 Mermaid 语法以查看图表。\"],\"S5Qbb1\":[\"用逗号分隔\"],\"UQOvxZ\":[\"空白卡片\"],\"VNa_N2\":[\"暂不支持预览此文件类型。\"],\"WSP6v1\":[\"排序:优先级\"],\"X03-eC\":[\"请输入内容。\"],\"Ya7bZl\":[\"图表错误\"],\"Zot9XS\":[\"暂无卡片\"],\"_5CsXX\":[\"完成列\"],\"_EsjyQ\":[\"使用此版本\"],\"a6uhHr\":[\"粗体 (Ctrl+B)\"],\"abUZlY\":[\"添加详情...\"],\"agOeRN\":[\"无法渲染此 API 规范。\"],\"b4hVKD\":[\"彩色列\"],\"cfaWH-\":[\"添加标签\"],\"cnGeoo\":[\"删除\"],\"d5z6xQ\":[\"WIP 限制 \",[\"0\"]],\"dEgA5A\":[\"取消\"],\"euc6Ns\":[\"复制卡片\"],\"fYcKtB\":[\"排序:截止\"],\"gLDJuJ\":[\"未命名卡片\"],\"hnK1gR\":[\"PDF 文档\"],\"i4_LY_\":[\"写作\"],\"iTylMl\":[\"模板\"],\"iYVqZq\":[\"列名称\"],\"jZlrte\":[\"颜色\"],\"kZlRKE\":[\"Mermaid 源码\"],\"kryGs-\":[\"卡片\"],\"lCF0wC\":[\"刷新\"],\"ltF1xa\":[\"保存合并结果\"],\"nabda1\":[\"删除卡片\"],\"o7J4JM\":[\"筛选\"],\"ojKCLU\":[\"负责人\"],\"p9yTeb\":[\"排序:标题\"],\"pKztsX\":[\"在完整编辑器中打开\"],\"pnrmSP\":[\"新建卡片\"],\"pwN6Ae\":[\"折叠列\"],\"pzutoc\":[\"斜体\"],\"rdUucN\":[\"预览\"],\"sCzmvQ\":[\"张卡片\"],\"sQpDn6\":[\"退出全屏\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 个冲突\",[\"1\"],\"待解决\"],\"u2IprG\":[\"卡片标题(回车添加,Esc 取消)\"],\"uAQUqI\":[\"状态\"],\"wf6Djn\":[\"斜体 (Ctrl+I)\"],\"wtw-au\":[\"设为完成列\"],\"wwu18a\":[\"图标\"],\"y1eoq1\":[\"复制链接\"],\"y9cj46\":[\"分组:优先级\"],\"yEbJGs\":[\"+ 添加字段\"],\"ybGQtY\":[\"← 返回列表\"],\"yz7wBu\":[\"关闭\"],\"yzF66j\":[\"链接\"],\"zOc0vf\":[\"无图标\"],\"zga9sT\":[\"确定\"]}"); \ No newline at end of file diff --git a/shared/i18n/locales/zh/messages.po b/shared/i18n/locales/zh/messages.po index c1429d0..494c0f8 100644 --- a/shared/i18n/locales/zh/messages.po +++ b/shared/i18n/locales/zh/messages.po @@ -22,6 +22,10 @@ msgstr "" msgid "← Back to list" msgstr "← 返回列表" +#: shared/components/board/BoardPeek.tsx +msgid "+ Add field" +msgstr "+ 添加字段" + #. placeholder {0}: conflicts.length #. placeholder {1}: conflicts.length > 1 ? "s" : "" #: shared/components/ConflictResolver.tsx diff --git a/shared/lib/board.ts b/shared/lib/board.ts index c330787..f434551 100644 --- a/shared/lib/board.ts +++ b/shared/lib/board.ts @@ -15,6 +15,10 @@ export type BoardGroupKey = "status" | "priority" | "assignee"; export type BoardSortKey = "manual" | "due" | "priority" | "title"; export type BoardViewType = "board" | "table"; +export type BoardFieldType = "text" | "number" | "date"; +/** A user-defined custom field on a board's cards (stored in frontmatter / properties). */ +export type BoardFieldDef = { key: string; label: string; type?: BoardFieldType }; + export type BoardViewConfig = { title: string; columns: BoardViewColumn[]; @@ -24,6 +28,8 @@ export type BoardViewConfig = { colorColumns?: boolean; viewType?: BoardViewType; groupBy?: BoardGroupKey; + /** User-defined custom fields shown/edited on cards (board-level schema). */ + fields?: BoardFieldDef[]; }; export type BoardTag = { id?: string; label: string; color?: string | null }; @@ -46,8 +52,24 @@ export type BoardViewCard = { taskDone?: number; taskTotal?: number; excerpt?: string | null; + /** Values for the board's user-defined custom fields, keyed by field key. */ + custom?: Record; }; +/** Read the declared custom-field values out of a flat property/frontmatter map. */ +export function pickCustomFields( + props: Record | null | undefined, + fields: BoardFieldDef[] | undefined, +): Record { + const out: Record = {}; + if (!props || !fields) return out; + for (const f of fields) { + const v = props[f.key]; + if (v !== undefined && v !== "") out[f.key] = v; + } + return out; +} + export type CardFilter = { prop: "priority" | "assignee" | "tag"; value: string }; export const PRIORITIES = ["none", "low", "medium", "high", "urgent"]; diff --git a/src/components/BoardView.tsx b/src/components/BoardView.tsx index 82fb8d2..2f3a90d 100644 --- a/src/components/BoardView.tsx +++ b/src/components/BoardView.tsx @@ -8,6 +8,7 @@ import { BoardSurface } from "@shared/components/board"; import type { BoardActions } from "@shared/components/board"; import { DEFAULT_DONE_COLUMN, + pickCustomFields, slugify, type BoardViewCard, type BoardViewConfig, @@ -86,8 +87,9 @@ export function BoardView({ boardPath, boardRelativePath }: { boardPath: string; taskDone: c.taskDone, taskTotal: c.taskTotal, excerpt: c.excerpt ?? null, + custom: pickCustomFields(c.properties, config?.fields), })), - [rawCards], + [rawCards, config?.fields], ); const viewConfig: BoardViewConfig = useMemo( @@ -99,6 +101,7 @@ export function BoardView({ boardPath, boardRelativePath }: { boardPath: string; doneColumn: config.doneColumn, colorColumns: config.colorColumns, viewType: config.viewType, + fields: config.fields, groupBy: (config.groupBy as BoardViewConfig["groupBy"]) || "status", } : { title: boardName, columns: [] }, @@ -168,6 +171,7 @@ export function BoardView({ boardPath, boardRelativePath }: { boardPath: string; if (patch.due !== undefined) next.due = patch.due ?? ""; if (patch.icon !== undefined) next.icon = patch.icon ?? ""; if (patch.tags !== undefined) next.tags = patch.tags.map((tg) => tg.label).join(", "); + if (patch.custom !== undefined) for (const [k, v] of Object.entries(patch.custom)) next[k] = v ?? ""; const newBody = patch.notes !== undefined ? patch.notes : body; await tauri.writeFile(id, writeFrontmatter(newBody, next)); await load(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 77862af..278ca6d 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -21,6 +21,8 @@ export type BoardConfig = { colorColumns?: boolean; /** Which renderer this board shows: kanban columns or a flat table. Defaults to "board". */ viewType?: "board" | "table"; + /** User-defined custom fields shown/edited on this board's cards. */ + fields?: { key: string; label: string; type?: "text" | "number" | "date" }[]; }; /** A card = a real `.md` note that belongs to a board (frontmatter `board == id`). */ @@ -38,6 +40,8 @@ export type BoardCard = { taskTotal?: number; icon?: string | null; excerpt?: string | null; + /** Full frontmatter map (for custom fields declared in the board config). */ + properties?: Record; }; /** A reusable card template (`.md` in `/.templates/`). */ diff --git a/tests/unit/boardCustomFields.spec.ts b/tests/unit/boardCustomFields.spec.ts new file mode 100644 index 0000000..2e44778 --- /dev/null +++ b/tests/unit/boardCustomFields.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { pickCustomFields, type BoardFieldDef } from "../../shared/lib/board"; + +// Pure-logic acceptance for C5 custom fields: the adapters read declared field +// values out of a flat property/frontmatter map via pickCustomFields. + +const fields: BoardFieldDef[] = [ + { key: "story_points", label: "Story Points", type: "number" }, + { key: "owner_team", label: "Owner Team" }, +]; + +test("pickCustomFields extracts only declared, non-empty keys", () => { + const props = { story_points: "5", owner_team: "Platform", board: "proj", status: "todo" }; + expect(pickCustomFields(props, fields)).toEqual({ story_points: "5", owner_team: "Platform" }); +}); + +test("pickCustomFields skips missing/empty values and undeclared keys", () => { + expect(pickCustomFields({ story_points: "3", owner_team: "" }, fields)).toEqual({ story_points: "3" }); + expect(pickCustomFields({ unrelated: "x" }, fields)).toEqual({}); + expect(pickCustomFields(null, fields)).toEqual({}); + expect(pickCustomFields({ story_points: "1" }, undefined)).toEqual({}); +}); From 074b21b3454a3b925645d60ec43223a89888b94f Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 23 Jun 2026 11:58:24 +0800 Subject: [PATCH 2/2] fix(kanban): custom-field keys can't collide with core card keys (review #39) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onAddField slugified the label and only de-duped against existing field keys, so a field named "Status"/"Due"/"Board"/… produced key status/due/board and writing its value would clobber the card's core frontmatter. Seed the dedup set with RESERVED_CARD_KEYS (title/board/status/position/priority/assignee/due/tags/icon/ blocked_by/blocks/relates/attachments) so a colliding label is disambiguated (e.g. status -> status-2). Co-Authored-By: Claude Opus 4.8 --- shared/components/board/BoardSurface.tsx | 5 ++++- shared/lib/board.ts | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/shared/components/board/BoardSurface.tsx b/shared/components/board/BoardSurface.tsx index c1e37fc..34ab580 100644 --- a/shared/components/board/BoardSurface.tsx +++ b/shared/components/board/BoardSurface.tsx @@ -35,6 +35,7 @@ import { PRIORITY_STYLE, effectiveColumns, groupValueOf, + RESERVED_CARD_KEYS, slugify, sortCards as sortCardsFn, todayStr, @@ -821,7 +822,9 @@ export function BoardSurface({ tagOptions={tagOptions} fields={config.fields} onAddField={(label) => { - const existing = new Set((config.fields ?? []).map((f) => f.key)); + // Seed with the reserved card keys so a custom field can never + // collide with (and clobber) a core frontmatter attribute. + const existing = new Set([...RESERVED_CARD_KEYS, ...(config.fields ?? []).map((f) => f.key)]); let key = slugify(label); if (existing.has(key)) { let n = 2; diff --git a/shared/lib/board.ts b/shared/lib/board.ts index f434551..a6f3f24 100644 --- a/shared/lib/board.ts +++ b/shared/lib/board.ts @@ -85,6 +85,27 @@ export const PRIORITY_STYLE: Record = { export const COLUMN_COLORS = ["#ef4444", "#f59e0b", "#eab308", "#22c55e", "#0ea5e9", "#6366f1", "#a855f7", "#ec4899", "#78716c"]; export const DEFAULT_DONE_COLUMN = "done"; +/** + * Frontmatter / property keys the board itself owns. A user-defined custom-field + * key must never equal one of these, or writing the field value would clobber a + * core card attribute (e.g. a field called "Status" → key `status`). + */ +export const RESERVED_CARD_KEYS = [ + "title", + "board", + "status", + "position", + "priority", + "assignee", + "due", + "tags", + "icon", + "blocked_by", + "blocks", + "relates", + "attachments", +]; + /** Count Markdown task checkboxes (`- [ ]` / `- [x]`) in a body → (done, total). */ export function countTasks(md: string): { done: number; total: number } { let done = 0;