diff --git a/services/jtype-core/src/lib.rs b/services/jtype-core/src/lib.rs index 8e63021..6302137 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, /// Card slugs this card is blocked by (frontmatter `blocked_by`). pub blocked_by: Vec, /// Card slugs this card blocks (frontmatter `blocks`). @@ -1505,6 +1508,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(), blocked_by: fm.get("blocked_by").map(|v| parse_card_links(v)).unwrap_or_default(), blocks: fm.get("blocks").map(|v| parse_card_links(v)).unwrap_or_default(), relates: fm.get("relates").map(|v| parse_card_links(v)).unwrap_or_default(), diff --git a/services/jtype-web/frontend/src/pages/Kanban.tsx b/services/jtype-web/frontend/src/pages/Kanban.tsx index d46e25d..c84630c 100644 --- a/services/jtype-web/frontend/src/pages/Kanban.tsx +++ b/services/jtype-web/frontend/src/pages/Kanban.tsx @@ -19,7 +19,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}` @@ -126,6 +126,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, @@ -140,9 +143,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( () => @@ -152,6 +156,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, swimlaneBy: view.swimlaneBy, colorColumns: view.colorColumns, doneColumn: view.doneColumn, @@ -247,10 +252,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 eb43797..93997e8 100644 --- a/services/jtype-web/frontend/src/pages/WebBoardView.tsx +++ b/services/jtype-web/frontend/src/pages/WebBoardView.tsx @@ -9,6 +9,7 @@ import { countTasks, parseLinks, parseTagList, + pickCustomFields, serializeLinks, slugify, type BoardViewCard, @@ -25,6 +26,7 @@ type BoardConfigJSON = { doneColumn?: string colorColumns?: boolean viewType?: 'board' | 'table' + fields?: { key: string; label: string; type?: 'text' | 'number' | 'date' }[] swimlaneBy?: 'status' | 'priority' | 'assignee' } @@ -95,6 +97,7 @@ export function WebBoardView({ taskDone: tasks.done, taskTotal: tasks.total, excerpt: bodyExcerpt(fm.body), + custom: pickCustomFields(fm.data, cfg.fields), blockedBy: fm.data.blocked_by ? parseLinks(fm.data.blocked_by) : [], blocks: fm.data.blocks ? parseLinks(fm.data.blocks) : [], relates: fm.data.relates ? parseLinks(fm.data.relates) : [], @@ -181,6 +184,7 @@ export function WebBoardView({ doneColumn: config.doneColumn, colorColumns: config.colorColumns, viewType: config.viewType, + fields: config.fields, swimlaneBy: config.swimlaneBy as BoardViewConfig['swimlaneBy'], groupBy: (config.groupBy as BoardViewConfig['groupBy']) || 'status', } @@ -222,6 +226,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 ?? '' if (patch.blockedBy !== undefined) next.blocked_by = serializeLinks(patch.blockedBy) if (patch.blocks !== undefined) next.blocks = serializeLinks(patch.blocks) if (patch.relates !== undefined) next.relates = serializeLinks(patch.relates) diff --git a/shared/components/board/BoardPeek.tsx b/shared/components/board/BoardPeek.tsx index b48c897..73495a2 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, ChatBubbleLeftIcon, ClockIcon } 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, BoardComment, BoardActivityEvent } from "../../lib/board"; +import type { BoardTag, BoardFieldDef, BoardComment, BoardActivityEvent } 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, dependencyCards, loadNotes, loadComments, @@ -34,6 +36,10 @@ 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; /** Sibling cards (excluding this one) offered as dependency targets. */ dependencyCards?: { slug: string; title: string }[]; loadNotes?: (id: string) => Promise; @@ -47,6 +53,7 @@ export function BoardPeek({ 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"); @@ -292,6 +299,41 @@ 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)} + /> +
+ + )} {dependencyCards && ( <> diff --git a/shared/components/board/BoardSurface.tsx b/shared/components/board/BoardSurface.tsx index fa51154..8127ddc 100644 --- a/shared/components/board/BoardSurface.tsx +++ b/shared/components/board/BoardSurface.tsx @@ -39,6 +39,8 @@ import { cardSlug, effectiveColumns, groupValueOf, + RESERVED_CARD_KEYS, + slugify, sortCards as sortCardsFn, todayStr, visibleCards as visibleCardsFn, @@ -881,6 +883,19 @@ export function BoardSurface({ statusOptions={config.columns.map((c) => ({ value: c.key, label: c.name }))} assigneeOptions={assigneeOptions} tagOptions={tagOptions} + fields={config.fields} + onAddField={(label) => { + // 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; + while (existing.has(`${key}-${n}`)) n += 1; + key = `${key}-${n}`; + } + void actions.setConfig({ fields: [...(config.fields ?? []), { key, label }] }); + }} dependencyCards={cards.filter((c) => c.id !== selected.id).map((c) => ({ slug: cardSlug(c), title: c.title }))} loadNotes={loadNotes} loadComments={loadComments} diff --git a/shared/i18n/locales/en/messages.mjs b/shared/i18n/locales/en/messages.mjs index 61065a5..33ca39f 100644 --- a/shared/i18n/locales/en/messages.mjs +++ b/shared/i18n/locales/en/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"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\"],\"7s3WlU\":[\"Blocks\"],\"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\"],\"C6-ZRl\":[\"Someone\"],\"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…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"Bold\"],\"KvW1VO\":[\"Draw.io diagram\"],\"LQn6-8\":[\"Accept local\"],\"MHrjPM\":[\"Title\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"Tags\"],\"OepdfE\":[\"Group: Status\"],\"Q2mGA7\":[\"Clear filter\"],\"QD8opX\":[\"Board\"],\"QlsPZy\":[\"Write Mermaid syntax to see the diagram.\"],\"S5Qbb1\":[\"comma, separated\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"Blank card\"],\"VNa_N2\":[\"This file type can not be previewed yet.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"Sort: Priority\"],\"X03-eC\":[\"Please enter a value.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"Diagram error\"],\"Zot9XS\":[\"No cards\"],\"_5CsXX\":[\"Done column\"],\"_EsjyQ\":[\"Use this\"],\"a6uhHr\":[\"Bold (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"Add details...\"],\"agOeRN\":[\"Could not render this API specification.\"],\"b4hVKD\":[\"Color columns\"],\"cfaWH-\":[\"Add labels\"],\"cnGeoo\":[\"Delete\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP limit \",[\"0\"]],\"dEgA5A\":[\"Cancel\"],\"euc6Ns\":[\"Duplicate\"],\"fYcKtB\":[\"Sort: Due\"],\"gLDJuJ\":[\"Untitled card\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF document\"],\"i4_LY_\":[\"Write\"],\"iTylMl\":[\"Templates\"],\"iYVqZq\":[\"Column name\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"Color\"],\"kZlRKE\":[\"Mermaid source\"],\"kryGs-\":[\"Card\"],\"lCF0wC\":[\"Refresh\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"Save merged result\"],\"nabda1\":[\"Delete card\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"Filter\"],\"o8va6N\":[\"Restored\"],\"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\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"Italic (Ctrl+I)\"],\"wtw-au\":[\"Set as done column\"],\"wwu18a\":[\"Icon\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"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("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"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\"],\"7s3WlU\":[\"Blocks\"],\"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\"],\"C6-ZRl\":[\"Someone\"],\"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…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"Bold\"],\"KvW1VO\":[\"Draw.io diagram\"],\"LQn6-8\":[\"Accept local\"],\"MHrjPM\":[\"Title\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"Tags\"],\"OepdfE\":[\"Group: Status\"],\"Q2mGA7\":[\"Clear filter\"],\"QD8opX\":[\"Board\"],\"QlsPZy\":[\"Write Mermaid syntax to see the diagram.\"],\"S5Qbb1\":[\"comma, separated\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"Blank card\"],\"VNa_N2\":[\"This file type can not be previewed yet.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"Sort: Priority\"],\"X03-eC\":[\"Please enter a value.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"Diagram error\"],\"Zot9XS\":[\"No cards\"],\"_5CsXX\":[\"Done column\"],\"_EsjyQ\":[\"Use this\"],\"a6uhHr\":[\"Bold (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"Add details...\"],\"agOeRN\":[\"Could not render this API specification.\"],\"b4hVKD\":[\"Color columns\"],\"cfaWH-\":[\"Add labels\"],\"cnGeoo\":[\"Delete\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP limit \",[\"0\"]],\"dEgA5A\":[\"Cancel\"],\"euc6Ns\":[\"Duplicate\"],\"fYcKtB\":[\"Sort: Due\"],\"gLDJuJ\":[\"Untitled card\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF document\"],\"i4_LY_\":[\"Write\"],\"iTylMl\":[\"Templates\"],\"iYVqZq\":[\"Column name\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"Color\"],\"kZlRKE\":[\"Mermaid source\"],\"kryGs-\":[\"Card\"],\"lCF0wC\":[\"Refresh\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"Save merged result\"],\"nabda1\":[\"Delete card\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"Filter\"],\"o8va6N\":[\"Restored\"],\"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\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"Italic (Ctrl+I)\"],\"wtw-au\":[\"Set as done column\"],\"wwu18a\":[\"Icon\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"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 3fb7745..1c18c4f 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 dfbac1b..b694198 100644 --- a/shared/i18n/locales/ja/messages.mjs +++ b/shared/i18n/locales/ja/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"ノート\"],\"1YABGm\":[\"リンク (Ctrl+K)\"],\"1hKEom\":[\"優先度\"],\"2wxgft\":[\"名前を変更\"],\"3qkggm\":[\"全画面表示\"],\"4gdyen\":[\"ローカル(自分の)\"],\"4hJhzz\":[\"表\"],\"54sFiP\":[\"flowchart TD\\n A[開始] --> B[終了]\"],\"5Q_DQ6\":[\"インラインコード\"],\"7VpPHA\":[\"確認\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 図\"],\"8hSn0h\":[\"結果(編集可能)\"],\"8lE269\":[\"並べ替え:手動\"],\"9gxam6\":[\"この Draw.io 図をレンダリングできませんでした。\"],\"AC9Gkf\":[\"列を展開\"],\"AS5WO9\":[\"この PDF をレンダリングできませんでした。\"],\"AVreQ5\":[\"ドラッグしてサイズ変更\"],\"AgvHni\":[\"列を追加\"],\"AxAubu\":[\"グループ:担当者\"],\"BfMZ7w\":[\"クラウドを採用\"],\"BnmEvM\":[\"テンプレートとして保存\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"コード\"],\"EbMPZJ\":[\"未割り当て\"],\"G4qrLy\":[\"完了列を解除\"],\"GKu3m4\":[\"ラベルなし\"],\"Gpfctt\":[\"期限\"],\"H_SQFv\":[\"色なし\"],\"I6SWEy\":[\"分割\"],\"ICip_B\":[\"クラウド(リモート)\"],\"Ik60OC\":[\"エディターで開く\"],\"Iw6WJa\":[\"WIP 制限を設定\"],\"JTYvAw\":[\"カードを検索\"],\"K_F6pa\":[\"保存中…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"太字\"],\"KvW1VO\":[\"Draw.io 図\"],\"LQn6-8\":[\"ローカルを採用\"],\"MHrjPM\":[\"タイトル\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"タグ\"],\"OepdfE\":[\"グループ:ステータス\"],\"Q2mGA7\":[\"フィルターをクリア\"],\"QD8opX\":[\"ボード\"],\"QlsPZy\":[\"Mermaid 構文を書くと図が表示されます。\"],\"S5Qbb1\":[\"カンマ区切り\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"空のカード\"],\"VNa_N2\":[\"このファイル形式はまだプレビューできません。\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"並べ替え:優先度\"],\"X03-eC\":[\"値を入力してください。\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"図のエラー\"],\"Zot9XS\":[\"カードなし\"],\"_5CsXX\":[\"完了列\"],\"_EsjyQ\":[\"これを使用\"],\"a6uhHr\":[\"太字 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"詳細を追加...\"],\"agOeRN\":[\"この API 仕様をレンダリングできませんでした。\"],\"b4hVKD\":[\"色付き列\"],\"cfaWH-\":[\"ラベルを追加\"],\"cnGeoo\":[\"削除\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 制限 \",[\"0\"]],\"dEgA5A\":[\"キャンセル\"],\"euc6Ns\":[\"複製\"],\"fYcKtB\":[\"並べ替え:期限\"],\"gLDJuJ\":[\"無題のカード\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF ドキュメント\"],\"i4_LY_\":[\"記述\"],\"iTylMl\":[\"テンプレート\"],\"iYVqZq\":[\"列名\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"カラー\"],\"kZlRKE\":[\"Mermaid ソース\"],\"kryGs-\":[\"カード\"],\"lCF0wC\":[\"更新\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"マージ結果を保存\"],\"nabda1\":[\"カードを削除\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"フィルター\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"担当者\"],\"p9yTeb\":[\"並べ替え:タイトル\"],\"pKztsX\":[\"フルエディターで開く\"],\"pnrmSP\":[\"新規カード\"],\"pwN6Ae\":[\"列を折りたたむ\"],\"pzutoc\":[\"イタリック\"],\"rdUucN\":[\"プレビュー\"],\"sCzmvQ\":[\"枚のカード\"],\"sQpDn6\":[\"全画面表示を終了\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 件の競合\",[\"1\"],\"を解決中\"],\"u2IprG\":[\"カードのタイトル(Enter で追加、Esc でキャンセル)\"],\"uAQUqI\":[\"ステータス\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"イタリック (Ctrl+I)\"],\"wtw-au\":[\"完了列に設定\"],\"wwu18a\":[\"アイコン\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"リンクをコピー\"],\"y9cj46\":[\"グループ:優先度\"],\"ybGQtY\":[\"← リストに戻る\"],\"yz7wBu\":[\"閉じる\"],\"yzF66j\":[\"リンク\"],\"zOc0vf\":[\"アイコンなし\"],\"zga9sT\":[\"OK\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"ノート\"],\"1YABGm\":[\"リンク (Ctrl+K)\"],\"1hKEom\":[\"優先度\"],\"2wxgft\":[\"名前を変更\"],\"3qkggm\":[\"全画面表示\"],\"4gdyen\":[\"ローカル(自分の)\"],\"4hJhzz\":[\"表\"],\"54sFiP\":[\"flowchart TD\\n A[開始] --> B[終了]\"],\"5Q_DQ6\":[\"インラインコード\"],\"7VpPHA\":[\"確認\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 図\"],\"8hSn0h\":[\"結果(編集可能)\"],\"8lE269\":[\"並べ替え:手動\"],\"9gxam6\":[\"この Draw.io 図をレンダリングできませんでした。\"],\"AC9Gkf\":[\"列を展開\"],\"AS5WO9\":[\"この PDF をレンダリングできませんでした。\"],\"AVreQ5\":[\"ドラッグしてサイズ変更\"],\"AgvHni\":[\"列を追加\"],\"AxAubu\":[\"グループ:担当者\"],\"BfMZ7w\":[\"クラウドを採用\"],\"BnmEvM\":[\"テンプレートとして保存\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"コード\"],\"EbMPZJ\":[\"未割り当て\"],\"G4qrLy\":[\"完了列を解除\"],\"GKu3m4\":[\"ラベルなし\"],\"Gpfctt\":[\"期限\"],\"H_SQFv\":[\"色なし\"],\"I6SWEy\":[\"分割\"],\"ICip_B\":[\"クラウド(リモート)\"],\"Ik60OC\":[\"エディターで開く\"],\"Iw6WJa\":[\"WIP 制限を設定\"],\"JTYvAw\":[\"カードを検索\"],\"K_F6pa\":[\"保存中…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"太字\"],\"KvW1VO\":[\"Draw.io 図\"],\"LQn6-8\":[\"ローカルを採用\"],\"MHrjPM\":[\"タイトル\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"タグ\"],\"OepdfE\":[\"グループ:ステータス\"],\"Q2mGA7\":[\"フィルターをクリア\"],\"QD8opX\":[\"ボード\"],\"QlsPZy\":[\"Mermaid 構文を書くと図が表示されます。\"],\"S5Qbb1\":[\"カンマ区切り\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"空のカード\"],\"VNa_N2\":[\"このファイル形式はまだプレビューできません。\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"並べ替え:優先度\"],\"X03-eC\":[\"値を入力してください。\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"図のエラー\"],\"Zot9XS\":[\"カードなし\"],\"_5CsXX\":[\"完了列\"],\"_EsjyQ\":[\"これを使用\"],\"a6uhHr\":[\"太字 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"詳細を追加...\"],\"agOeRN\":[\"この API 仕様をレンダリングできませんでした。\"],\"b4hVKD\":[\"色付き列\"],\"cfaWH-\":[\"ラベルを追加\"],\"cnGeoo\":[\"削除\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 制限 \",[\"0\"]],\"dEgA5A\":[\"キャンセル\"],\"euc6Ns\":[\"複製\"],\"fYcKtB\":[\"並べ替え:期限\"],\"gLDJuJ\":[\"無題のカード\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF ドキュメント\"],\"i4_LY_\":[\"記述\"],\"iTylMl\":[\"テンプレート\"],\"iYVqZq\":[\"列名\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"カラー\"],\"kZlRKE\":[\"Mermaid ソース\"],\"kryGs-\":[\"カード\"],\"lCF0wC\":[\"更新\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"マージ結果を保存\"],\"nabda1\":[\"カードを削除\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"フィルター\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"担当者\"],\"p9yTeb\":[\"並べ替え:タイトル\"],\"pKztsX\":[\"フルエディターで開く\"],\"pnrmSP\":[\"新規カード\"],\"pwN6Ae\":[\"列を折りたたむ\"],\"pzutoc\":[\"イタリック\"],\"rdUucN\":[\"プレビュー\"],\"sCzmvQ\":[\"枚のカード\"],\"sQpDn6\":[\"全画面表示を終了\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 件の競合\",[\"1\"],\"を解決中\"],\"u2IprG\":[\"カードのタイトル(Enter で追加、Esc でキャンセル)\"],\"uAQUqI\":[\"ステータス\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"イタリック (Ctrl+I)\"],\"wtw-au\":[\"完了列に設定\"],\"wwu18a\":[\"アイコン\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"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 3b288ea..a1f8e10 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 87a7662..059685b 100644 --- a/shared/i18n/locales/ko/messages.mjs +++ b/shared/i18n/locales/ko/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"y1eoq1\":[\"링크 복사\"],\"y9cj46\":[\"그룹: 우선순위\"],\"ybGQtY\":[\"← 목록으로\"],\"yz7wBu\":[\"닫기\"],\"yzF66j\":[\"링크\"],\"zOc0vf\":[\"아이콘 없음\"],\"zga9sT\":[\"확인\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"Blocked by\"],\"-b7T3G\":[\"Updated\"],\"1DBGsz\":[\"노트\"],\"1YABGm\":[\"링크 (Ctrl+K)\"],\"1hKEom\":[\"우선순위\"],\"2wxgft\":[\"이름 변경\"],\"3qkggm\":[\"전체 화면\"],\"4gdyen\":[\"로컈 (내 것)\"],\"4hJhzz\":[\"테이블\"],\"54sFiP\":[\"flowchart TD\\n A[시작] --> B[끝]\"],\"5Q_DQ6\":[\"인라인 코드\"],\"7VpPHA\":[\"확인\"],\"7s3WlU\":[\"Blocks\"],\"8PifYj\":[\"Mermaid 다이어그램\"],\"8hSn0h\":[\"결과 (편집 가능)\"],\"8lE269\":[\"정렬: 수동\"],\"9gxam6\":[\"이 Draw.io 다이어그램을 렌더링할 수 없습니다.\"],\"AC9Gkf\":[\"열 펼치기\"],\"AS5WO9\":[\"이 PDF를 렌더링할 수 없습니다.\"],\"AVreQ5\":[\"드래그하여 크기 조정\"],\"AgvHni\":[\"열 추가\"],\"AxAubu\":[\"그룹: 담당자\"],\"BfMZ7w\":[\"클라우드 수낙\"],\"BnmEvM\":[\"템플릿으로 저장\"],\"C6-ZRl\":[\"Someone\"],\"EWPtMO\":[\"코드\"],\"EbMPZJ\":[\"미할당\"],\"G4qrLy\":[\"완료 열 해제\"],\"GKu3m4\":[\"라벨 없음\"],\"Gpfctt\":[\"마감\"],\"H_SQFv\":[\"색상 없음\"],\"I6SWEy\":[\"스플릿\"],\"ICip_B\":[\"클라우드 (원격)\"],\"Ik60OC\":[\"에디터에서 열기\"],\"Iw6WJa\":[\"WIP 한도 설정\"],\"JTYvAw\":[\"카드 검색\"],\"K_F6pa\":[\"저장 중…\"],\"KjXDqG\":[\"Swimlane: None\"],\"KmydK6\":[\"굵게\"],\"KvW1VO\":[\"Draw.io 다이어그램\"],\"LQn6-8\":[\"로컈 수낙\"],\"MHrjPM\":[\"제목\"],\"Mm72la\":[\"No comments yet\"],\"NBdIgR\":[\"Comment\"],\"OYHzN1\":[\"태그\"],\"OepdfE\":[\"그룹: 상태\"],\"Q2mGA7\":[\"필터 지우기\"],\"QD8opX\":[\"보드\"],\"QlsPZy\":[\"Mermaid 구문을 작성하면 다이어그램이 표시됩니다.\"],\"S5Qbb1\":[\"쉼표로 구분\"],\"TdfEV7\":[\"Archived\"],\"UQOvxZ\":[\"빈 카드\"],\"VNa_N2\":[\"이 파일 형식은 아직 미리볼 수 없습니다.\"],\"VbyRUy\":[\"Comments\"],\"WSP6v1\":[\"정렬: 우선순위\"],\"X03-eC\":[\"값을 입력해 주세요.\"],\"XJOV1Y\":[\"Activity\"],\"Ya7bZl\":[\"다이어그램 오류\"],\"Zot9XS\":[\"카드 없음\"],\"_5CsXX\":[\"완료 열\"],\"_EsjyQ\":[\"이것 사용\"],\"a6uhHr\":[\"굵게 (Ctrl+B)\"],\"aDvLhk\":[\"Add a comment…\"],\"abUZlY\":[\"세부정보 추가...\"],\"agOeRN\":[\"이 API 명세를 렌더링할 수 없습니다.\"],\"b4hVKD\":[\"색상 열\"],\"cfaWH-\":[\"라벨 추가\"],\"cnGeoo\":[\"삭제\"],\"d-F6q9\":[\"Created\"],\"d5z6xQ\":[\"WIP 한도 \",[\"0\"]],\"dEgA5A\":[\"취소\"],\"euc6Ns\":[\"복제\"],\"fYcKtB\":[\"정렬: 마감\"],\"gLDJuJ\":[\"제목 없는 카드\"],\"hh4sEG\":[\"Relates\"],\"hnK1gR\":[\"PDF 문서\"],\"i4_LY_\":[\"작성\"],\"iTylMl\":[\"템플릿\"],\"iYVqZq\":[\"열 이름\"],\"jUbC3Z\":[\"Swimlane: Priority\"],\"jZlrte\":[\"색상\"],\"kZlRKE\":[\"Mermaid 소스\"],\"kryGs-\":[\"카드\"],\"lCF0wC\":[\"새로고침\"],\"lHxVTh\":[\"Swimlane: Assignee\"],\"ltF1xa\":[\"병합 결과 저장\"],\"nabda1\":[\"카드 삭제\"],\"njJFtc\":[\"Delete comment\"],\"o7J4JM\":[\"필터\"],\"o8va6N\":[\"Restored\"],\"ojKCLU\":[\"담당자\"],\"p9yTeb\":[\"정렬: 제목\"],\"pKztsX\":[\"전체 에디터에서 열기\"],\"pnrmSP\":[\"새 카드\"],\"pwN6Ae\":[\"열 접기\"],\"pzutoc\":[\"기울임꼴\"],\"rdUucN\":[\"미리보기\"],\"sCzmvQ\":[\"개 카드\"],\"sQpDn6\":[\"전체 화면 종료\"],\"tK2x9T\":[\"⚠ 해결할 충돌 \",[\"0\"],\"건\",[\"1\"]],\"u2IprG\":[\"카드 제목 (Enter로 추가, Esc로 취소)\"],\"uAQUqI\":[\"상태\"],\"ucJg3u\":[\"Swimlane: Status\"],\"wf6Djn\":[\"기울임꼴 (Ctrl+I)\"],\"wtw-au\":[\"완료 열로 설정\"],\"wwu18a\":[\"아이콘\"],\"x52RAh\":[\"Blocked by \",[\"blockedCount\"],\" unfinished card(s)\"],\"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 6c852e0..70b0b22 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 1039f41..7405069 100644 --- a/shared/i18n/locales/zh/messages.mjs +++ b/shared/i18n/locales/zh/messages.mjs @@ -1 +1 @@ -/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"被阻塞于\"],\"-b7T3G\":[\"更新\"],\"1DBGsz\":[\"备注\"],\"1YABGm\":[\"链接 (Ctrl+K)\"],\"1hKEom\":[\"优先级\"],\"2wxgft\":[\"重命名\"],\"3qkggm\":[\"全屏\"],\"4gdyen\":[\"本地(我的)\"],\"4hJhzz\":[\"表格\"],\"54sFiP\":[\"flowchart TD\\n A[开始] --> B[结束]\"],\"5Q_DQ6\":[\"行内代码\"],\"7VpPHA\":[\"确认\"],\"7s3WlU\":[\"阻塞\"],\"8PifYj\":[\"Mermaid 图表\"],\"8hSn0h\":[\"结果(可编辑)\"],\"8lE269\":[\"排序:手动\"],\"9gxam6\":[\"无法渲染此 Draw.io 图表。\"],\"AC9Gkf\":[\"展开列\"],\"AS5WO9\":[\"无法渲染此 PDF。\"],\"AVreQ5\":[\"拖动调整宽度\"],\"AgvHni\":[\"添加列\"],\"AxAubu\":[\"分组:负责人\"],\"BfMZ7w\":[\"接受云端\"],\"BnmEvM\":[\"存为模板\"],\"C6-ZRl\":[\"某人\"],\"EWPtMO\":[\"代码\"],\"EbMPZJ\":[\"未分配\"],\"G4qrLy\":[\"取消完成列\"],\"GKu3m4\":[\"暂无标签\"],\"Gpfctt\":[\"截止日期\"],\"H_SQFv\":[\"无颜色\"],\"I6SWEy\":[\"分栏\"],\"ICip_B\":[\"云端(远程)\"],\"Ik60OC\":[\"在编辑器中打开\"],\"Iw6WJa\":[\"设置 WIP 限制\"],\"JTYvAw\":[\"搜索卡片\"],\"K_F6pa\":[\"保存中…\"],\"KjXDqG\":[\"泳道:无\"],\"KmydK6\":[\"粗体\"],\"KvW1VO\":[\"Draw.io 图表\"],\"LQn6-8\":[\"接受本地\"],\"MHrjPM\":[\"标题\"],\"Mm72la\":[\"暂无评论\"],\"NBdIgR\":[\"评论\"],\"OYHzN1\":[\"标签\"],\"OepdfE\":[\"分组:状态\"],\"Q2mGA7\":[\"清除筛选\"],\"QD8opX\":[\"看板\"],\"QlsPZy\":[\"输入 Mermaid 语法以查看图表。\"],\"S5Qbb1\":[\"用逗号分隔\"],\"TdfEV7\":[\"归档\"],\"UQOvxZ\":[\"空白卡片\"],\"VNa_N2\":[\"暂不支持预览此文件类型。\"],\"VbyRUy\":[\"评论\"],\"WSP6v1\":[\"排序:优先级\"],\"X03-eC\":[\"请输入内容。\"],\"XJOV1Y\":[\"活动\"],\"Ya7bZl\":[\"图表错误\"],\"Zot9XS\":[\"暂无卡片\"],\"_5CsXX\":[\"完成列\"],\"_EsjyQ\":[\"使用此版本\"],\"a6uhHr\":[\"粗体 (Ctrl+B)\"],\"aDvLhk\":[\"添加评论…\"],\"abUZlY\":[\"添加详情...\"],\"agOeRN\":[\"无法渲染此 API 规范。\"],\"b4hVKD\":[\"彩色列\"],\"cfaWH-\":[\"添加标签\"],\"cnGeoo\":[\"删除\"],\"d-F6q9\":[\"创建\"],\"d5z6xQ\":[\"WIP 限制 \",[\"0\"]],\"dEgA5A\":[\"取消\"],\"euc6Ns\":[\"复制卡片\"],\"fYcKtB\":[\"排序:截止\"],\"gLDJuJ\":[\"未命名卡片\"],\"hh4sEG\":[\"相关\"],\"hnK1gR\":[\"PDF 文档\"],\"i4_LY_\":[\"写作\"],\"iTylMl\":[\"模板\"],\"iYVqZq\":[\"列名称\"],\"jUbC3Z\":[\"泳道:优先级\"],\"jZlrte\":[\"颜色\"],\"kZlRKE\":[\"Mermaid 源码\"],\"kryGs-\":[\"卡片\"],\"lCF0wC\":[\"刷新\"],\"lHxVTh\":[\"泳道:负责人\"],\"ltF1xa\":[\"保存合并结果\"],\"nabda1\":[\"删除卡片\"],\"njJFtc\":[\"删除评论\"],\"o7J4JM\":[\"筛选\"],\"o8va6N\":[\"恢复\"],\"ojKCLU\":[\"负责人\"],\"p9yTeb\":[\"排序:标题\"],\"pKztsX\":[\"在完整编辑器中打开\"],\"pnrmSP\":[\"新建卡片\"],\"pwN6Ae\":[\"折叠列\"],\"pzutoc\":[\"斜体\"],\"rdUucN\":[\"预览\"],\"sCzmvQ\":[\"张卡片\"],\"sQpDn6\":[\"退出全屏\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 个冲突\",[\"1\"],\"待解决\"],\"u2IprG\":[\"卡片标题(回车添加,Esc 取消)\"],\"uAQUqI\":[\"状态\"],\"ucJg3u\":[\"泳道:状态\"],\"wf6Djn\":[\"斜体 (Ctrl+I)\"],\"wtw-au\":[\"设为完成列\"],\"wwu18a\":[\"图标\"],\"x52RAh\":[\"被 \",[\"blockedCount\"],\" 张未完成卡片阻塞\"],\"y1eoq1\":[\"复制链接\"],\"y9cj46\":[\"分组:优先级\"],\"ybGQtY\":[\"← 返回列表\"],\"yz7wBu\":[\"关闭\"],\"yzF66j\":[\"链接\"],\"zOc0vf\":[\"无图标\"],\"zga9sT\":[\"确定\"]}"); \ No newline at end of file +/*eslint-disable*/export const messages=JSON.parse("{\"--lIxB\":[\"被阻塞于\"],\"-b7T3G\":[\"更新\"],\"1DBGsz\":[\"备注\"],\"1YABGm\":[\"链接 (Ctrl+K)\"],\"1hKEom\":[\"优先级\"],\"2wxgft\":[\"重命名\"],\"3qkggm\":[\"全屏\"],\"4gdyen\":[\"本地(我的)\"],\"4hJhzz\":[\"表格\"],\"54sFiP\":[\"flowchart TD\\n A[开始] --> B[结束]\"],\"5Q_DQ6\":[\"行内代码\"],\"7VpPHA\":[\"确认\"],\"7s3WlU\":[\"阻塞\"],\"8PifYj\":[\"Mermaid 图表\"],\"8hSn0h\":[\"结果(可编辑)\"],\"8lE269\":[\"排序:手动\"],\"9gxam6\":[\"无法渲染此 Draw.io 图表。\"],\"AC9Gkf\":[\"展开列\"],\"AS5WO9\":[\"无法渲染此 PDF。\"],\"AVreQ5\":[\"拖动调整宽度\"],\"AgvHni\":[\"添加列\"],\"AxAubu\":[\"分组:负责人\"],\"BfMZ7w\":[\"接受云端\"],\"BnmEvM\":[\"存为模板\"],\"C6-ZRl\":[\"某人\"],\"EWPtMO\":[\"代码\"],\"EbMPZJ\":[\"未分配\"],\"G4qrLy\":[\"取消完成列\"],\"GKu3m4\":[\"暂无标签\"],\"Gpfctt\":[\"截止日期\"],\"H_SQFv\":[\"无颜色\"],\"I6SWEy\":[\"分栏\"],\"ICip_B\":[\"云端(远程)\"],\"Ik60OC\":[\"在编辑器中打开\"],\"Iw6WJa\":[\"设置 WIP 限制\"],\"JTYvAw\":[\"搜索卡片\"],\"K_F6pa\":[\"保存中…\"],\"KjXDqG\":[\"泳道:无\"],\"KmydK6\":[\"粗体\"],\"KvW1VO\":[\"Draw.io 图表\"],\"LQn6-8\":[\"接受本地\"],\"MHrjPM\":[\"标题\"],\"Mm72la\":[\"暂无评论\"],\"NBdIgR\":[\"评论\"],\"OYHzN1\":[\"标签\"],\"OepdfE\":[\"分组:状态\"],\"Q2mGA7\":[\"清除筛选\"],\"QD8opX\":[\"看板\"],\"QlsPZy\":[\"输入 Mermaid 语法以查看图表。\"],\"S5Qbb1\":[\"用逗号分隔\"],\"TdfEV7\":[\"归档\"],\"UQOvxZ\":[\"空白卡片\"],\"VNa_N2\":[\"暂不支持预览此文件类型。\"],\"VbyRUy\":[\"评论\"],\"WSP6v1\":[\"排序:优先级\"],\"X03-eC\":[\"请输入内容。\"],\"XJOV1Y\":[\"活动\"],\"Ya7bZl\":[\"图表错误\"],\"Zot9XS\":[\"暂无卡片\"],\"_5CsXX\":[\"完成列\"],\"_EsjyQ\":[\"使用此版本\"],\"a6uhHr\":[\"粗体 (Ctrl+B)\"],\"aDvLhk\":[\"添加评论…\"],\"abUZlY\":[\"添加详情...\"],\"agOeRN\":[\"无法渲染此 API 规范。\"],\"b4hVKD\":[\"彩色列\"],\"cfaWH-\":[\"添加标签\"],\"cnGeoo\":[\"删除\"],\"d-F6q9\":[\"创建\"],\"d5z6xQ\":[\"WIP 限制 \",[\"0\"]],\"dEgA5A\":[\"取消\"],\"euc6Ns\":[\"复制卡片\"],\"fYcKtB\":[\"排序:截止\"],\"gLDJuJ\":[\"未命名卡片\"],\"hh4sEG\":[\"相关\"],\"hnK1gR\":[\"PDF 文档\"],\"i4_LY_\":[\"写作\"],\"iTylMl\":[\"模板\"],\"iYVqZq\":[\"列名称\"],\"jUbC3Z\":[\"泳道:优先级\"],\"jZlrte\":[\"颜色\"],\"kZlRKE\":[\"Mermaid 源码\"],\"kryGs-\":[\"卡片\"],\"lCF0wC\":[\"刷新\"],\"lHxVTh\":[\"泳道:负责人\"],\"ltF1xa\":[\"保存合并结果\"],\"nabda1\":[\"删除卡片\"],\"njJFtc\":[\"删除评论\"],\"o7J4JM\":[\"筛选\"],\"o8va6N\":[\"恢复\"],\"ojKCLU\":[\"负责人\"],\"p9yTeb\":[\"排序:标题\"],\"pKztsX\":[\"在完整编辑器中打开\"],\"pnrmSP\":[\"新建卡片\"],\"pwN6Ae\":[\"折叠列\"],\"pzutoc\":[\"斜体\"],\"rdUucN\":[\"预览\"],\"sCzmvQ\":[\"张卡片\"],\"sQpDn6\":[\"退出全屏\"],\"tK2x9T\":[\"⚠ \",[\"0\"],\" 个冲突\",[\"1\"],\"待解决\"],\"u2IprG\":[\"卡片标题(回车添加,Esc 取消)\"],\"uAQUqI\":[\"状态\"],\"ucJg3u\":[\"泳道:状态\"],\"wf6Djn\":[\"斜体 (Ctrl+I)\"],\"wtw-au\":[\"设为完成列\"],\"wwu18a\":[\"图标\"],\"x52RAh\":[\"被 \",[\"blockedCount\"],\" 张未完成卡片阻塞\"],\"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 626e528..4214a8f 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 110e034..2c223cf 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[]; /** * Second grouping dimension rendered as horizontal swimlanes (rows) in the * board view. Must differ from `groupBy`; unset = no swimlanes. @@ -51,6 +57,8 @@ 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; /** Card slugs this card is blocked by (frontmatter `blocked_by`). */ blockedBy?: string[]; /** Card slugs this card blocks (frontmatter `blocks`). */ @@ -59,6 +67,20 @@ export type BoardViewCard = { relates?: string[]; }; +/** 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 }; /** A card comment (DB board). */ @@ -80,6 +102,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; diff --git a/src/components/BoardView.tsx b/src/components/BoardView.tsx index 87b87dc..cceea36 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, serializeLinks, slugify, type BoardViewCard, @@ -87,11 +88,12 @@ export function BoardView({ boardPath, boardRelativePath }: { boardPath: string; taskDone: c.taskDone, taskTotal: c.taskTotal, excerpt: c.excerpt ?? null, + custom: pickCustomFields(c.properties, config?.fields), blockedBy: c.blockedBy ?? [], blocks: c.blocks ?? [], relates: c.relates ?? [], })), - [rawCards], + [rawCards, config?.fields], ); const viewConfig: BoardViewConfig = useMemo( @@ -103,6 +105,7 @@ export function BoardView({ boardPath, boardRelativePath }: { boardPath: string; doneColumn: config.doneColumn, colorColumns: config.colorColumns, viewType: config.viewType, + fields: config.fields, swimlaneBy: config.swimlaneBy as BoardViewConfig["swimlaneBy"], groupBy: (config.groupBy as BoardViewConfig["groupBy"]) || "status", } @@ -173,6 +176,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 ?? ""; if (patch.blockedBy !== undefined) next.blocked_by = serializeLinks(patch.blockedBy); if (patch.blocks !== undefined) next.blocks = serializeLinks(patch.blocks); if (patch.relates !== undefined) next.relates = serializeLinks(patch.relates); diff --git a/src/lib/types.ts b/src/lib/types.ts index 921f855..7fcbccf 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" }[]; /** Optional second grouping dimension rendered as swimlane rows in the board view. */ swimlaneBy?: "status" | "priority" | "assignee"; }; @@ -40,6 +42,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; blockedBy?: string[]; blocks?: string[]; relates?: string[]; 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({}); +});