@anchrd/intel 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +496 -0
- package/bin/intel.mjs +134 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
- package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
- package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
- package/dist/adapters/cloudflare/cloudflare.js +416 -0
- package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
- package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
- package/dist/adapters/content/content.d.ts +3 -0
- package/dist/adapters/content/content.js +128 -0
- package/dist/adapters/content/content.types.d.ts +38 -0
- package/dist/adapters/content/content.types.js +1 -0
- package/dist/adapters/db/db-audit.d.ts +62 -0
- package/dist/adapters/db/db-audit.js +150 -0
- package/dist/adapters/db/db-boards.d.ts +6 -0
- package/dist/adapters/db/db-boards.js +324 -0
- package/dist/adapters/db/db-feed.d.ts +30 -0
- package/dist/adapters/db/db-feed.js +276 -0
- package/dist/adapters/db/db-flows.d.ts +42 -0
- package/dist/adapters/db/db-flows.js +1311 -0
- package/dist/adapters/db/db-grants.d.ts +133 -0
- package/dist/adapters/db/db-grants.js +269 -0
- package/dist/adapters/db/db-indexing.d.ts +3 -0
- package/dist/adapters/db/db-indexing.js +200 -0
- package/dist/adapters/db/db-oauth.d.ts +3 -0
- package/dist/adapters/db/db-oauth.js +26 -0
- package/dist/adapters/db/db-prompts.d.ts +46 -0
- package/dist/adapters/db/db-prompts.js +144 -0
- package/dist/adapters/db/db.d.ts +6 -0
- package/dist/adapters/db/db.js +1687 -0
- package/dist/adapters/db/db.types.d.ts +16 -0
- package/dist/adapters/db/db.types.js +1 -0
- package/dist/adapters/document-converter/document-converter.d.ts +2 -0
- package/dist/adapters/document-converter/document-converter.js +26 -0
- package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
- package/dist/adapters/document-converter/document-converter.types.js +1 -0
- package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
- package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
- package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
- package/dist/adapters/index-queue/index-queue.d.ts +10 -0
- package/dist/adapters/index-queue/index-queue.js +12 -0
- package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
- package/dist/adapters/index-queue/index-queue.types.js +1 -0
- package/dist/adapters/json-schema/json-schema.d.ts +2 -0
- package/dist/adapters/json-schema/json-schema.js +77 -0
- package/dist/adapters/openid/openid.d.ts +5 -0
- package/dist/adapters/openid/openid.js +301 -0
- package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
- package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
- package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
- package/dist/adapters/remote-tools/remote-tools.js +129 -0
- package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
- package/dist/adapters/semantic-index/semantic-index.js +139 -0
- package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
- package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
- package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
- package/dist/adapters/session-cookie/session-cookie.js +87 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
- package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
- package/dist/audit/audit.d.ts +2 -0
- package/dist/audit/audit.js +87 -0
- package/dist/audit/audit.types.d.ts +23 -0
- package/dist/audit/audit.types.js +1 -0
- package/dist/auth/auth.d.ts +2 -0
- package/dist/auth/auth.js +284 -0
- package/dist/auth/auth.types.d.ts +108 -0
- package/dist/auth/auth.types.js +1 -0
- package/dist/boards/boards.d.ts +44 -0
- package/dist/boards/boards.js +447 -0
- package/dist/boards/boards.types.d.ts +117 -0
- package/dist/boards/boards.types.js +1 -0
- package/dist/build/build.d.ts +2 -0
- package/dist/build/build.js +192 -0
- package/dist/build/build.types.d.ts +17 -0
- package/dist/build/build.types.js +1 -0
- package/dist/bundle/bundle.d.ts +4 -0
- package/dist/bundle/bundle.js +1194 -0
- package/dist/bundle/bundle.types.d.ts +33 -0
- package/dist/bundle/bundle.types.js +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.js +262 -0
- package/dist/cli/cli.types.d.ts +14 -0
- package/dist/cli/cli.types.js +1 -0
- package/dist/contract/audit.d.ts +50 -0
- package/dist/contract/audit.js +72 -0
- package/dist/contract/board.d.ts +150 -0
- package/dist/contract/board.js +262 -0
- package/dist/contract/bundle.d.ts +147 -0
- package/dist/contract/bundle.js +139 -0
- package/dist/contract/contract.d.ts +112 -0
- package/dist/contract/contract.js +143 -0
- package/dist/contract/feed.d.ts +126 -0
- package/dist/contract/feed.js +149 -0
- package/dist/contract/flow-run.d.ts +346 -0
- package/dist/contract/flow-run.js +194 -0
- package/dist/contract/flow.d.ts +1057 -0
- package/dist/contract/flow.js +552 -0
- package/dist/contract/node.d.ts +551 -0
- package/dist/contract/node.js +456 -0
- package/dist/contract/share.d.ts +216 -0
- package/dist/contract/share.js +134 -0
- package/dist/contract/table.d.ts +171 -0
- package/dist/contract/table.js +117 -0
- package/dist/contract/tool.d.ts +122 -0
- package/dist/contract/tool.js +172 -0
- package/dist/feed/feed.d.ts +2 -0
- package/dist/feed/feed.js +77 -0
- package/dist/feed/feed.types.d.ts +23 -0
- package/dist/feed/feed.types.js +1 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +2216 -0
- package/dist/flows/flows.types.d.ts +339 -0
- package/dist/flows/flows.types.js +1 -0
- package/dist/http/http.d.ts +9 -0
- package/dist/http/http.js +874 -0
- package/dist/http/http.types.d.ts +22 -0
- package/dist/http/http.types.js +1 -0
- package/dist/indexing/indexing.d.ts +6 -0
- package/dist/indexing/indexing.js +239 -0
- package/dist/indexing/indexing.types.d.ts +11 -0
- package/dist/indexing/indexing.types.js +1 -0
- package/dist/intel/intel.d.ts +3 -0
- package/dist/intel/intel.js +194 -0
- package/dist/intel/intel.types.d.ts +24 -0
- package/dist/intel/intel.types.js +1 -0
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +1292 -0
- package/dist/mcp/mcp.types.d.ts +25 -0
- package/dist/mcp/mcp.types.js +1 -0
- package/dist/nodes/document-links/document-links.d.ts +20 -0
- package/dist/nodes/document-links/document-links.js +57 -0
- package/dist/nodes/nodes.d.ts +2 -0
- package/dist/nodes/nodes.js +1667 -0
- package/dist/nodes/nodes.types.d.ts +511 -0
- package/dist/nodes/nodes.types.js +1 -0
- package/dist/prepare/prepare.d.ts +4 -0
- package/dist/prepare/prepare.js +16 -0
- package/dist/prepare/prepare.types.d.ts +9 -0
- package/dist/prepare/prepare.types.js +1 -0
- package/dist/prompts/prompts.d.ts +2 -0
- package/dist/prompts/prompts.js +65 -0
- package/dist/prompts/prompts.types.d.ts +71 -0
- package/dist/prompts/prompts.types.js +1 -0
- package/dist/shared/callers/callers.d.ts +28 -0
- package/dist/shared/callers/callers.js +30 -0
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/document-text/document-text.d.ts +21 -0
- package/dist/shared/document-text/document-text.js +31 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
- package/dist/shared/gate-authorization/gate-authorization.js +26 -0
- package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
- package/dist/shared/grant-expiry/grant-expiry.js +26 -0
- package/dist/shared/intel-error/intel-error.d.ts +22 -0
- package/dist/shared/intel-error/intel-error.js +27 -0
- package/dist/shared/plain-title/plain-title.d.ts +16 -0
- package/dist/shared/plain-title/plain-title.js +45 -0
- package/dist/shared/problem-details/problem-details.d.ts +30 -0
- package/dist/shared/problem-details/problem-details.js +18 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
- package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
- package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
- package/dist/shared/safe-return-path/safe-return-path.js +11 -0
- package/dist/shared/sha256/sha256.d.ts +2 -0
- package/dist/shared/sha256/sha256.js +14 -0
- package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
- package/dist/tools/tool-servers/tool-servers.js +177 -0
- package/dist/tools/tools.d.ts +2 -0
- package/dist/tools/tools.js +207 -0
- package/dist/tools/tools.types.d.ts +56 -0
- package/dist/tools/tools.types.js +1 -0
- package/examples/branding/favicon.svg +5 -0
- package/examples/branding/logo.svg +5 -0
- package/examples/branding/theme.css +16 -0
- package/examples/dev.vars.example +7 -0
- package/examples/intel.json +9 -0
- package/migrations/0000_intel_foundation.sql +203 -0
- package/migrations/0001_portal_native_tools.sql +21 -0
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/migrations/0007_no_node_waits.sql +69 -0
- package/migrations/0008_three_layers.sql +130 -0
- package/migrations/0009_no_context_policy.sql +48 -0
- package/migrations/0010_no_run_waits_a_year.sql +27 -0
- package/migrations/0011_one_name_for_the_tree.sql +53 -0
- package/migrations/0012_table_snapshots.sql +29 -0
- package/migrations/0017_a_vector_per_card.sql +38 -0
- package/migrations/0018_no_context_policy_at_last.sql +97 -0
- package/migrations/0019_one_name_for_the_grants.sql +52 -0
- package/migrations/0020_cascade_purge_replay.sql +11 -0
- package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
- package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
- package/migrations/0023_a_board_and_its_tasks.sql +116 -0
- package/migrations/0024_the_archive_is_a_column.sql +33 -0
- package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
- package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
- package/migrations/0027_the_runs_of_every_flow.sql +27 -0
- package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
- package/package.json +118 -0
- package/src/contract/CLAUDE.md +152 -0
- package/src/contract/README.md +102 -0
- package/src/contract/audit.ts +85 -0
- package/src/contract/board.ts +306 -0
- package/src/contract/bundle.ts +157 -0
- package/src/contract/contract.ts +161 -0
- package/src/contract/feed.ts +165 -0
- package/src/contract/flow-run.ts +241 -0
- package/src/contract/flow.ts +675 -0
- package/src/contract/node.ts +583 -0
- package/src/contract/share.ts +196 -0
- package/src/contract/table.ts +178 -0
- package/src/contract/tool.ts +202 -0
- package/ui/README.md +110 -0
- package/ui/components.json +21 -0
- package/ui/index.html +13 -0
- package/ui/package.json +11 -0
- package/ui/scripts/lint-tokens.mjs +187 -0
- package/ui/src/access-summary/access-summary.tsx +385 -0
- package/ui/src/app/action-slot/action-slot.tsx +27 -0
- package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
- package/ui/src/app/app-tree/app-tree.tsx +1175 -0
- package/ui/src/app/app.tsx +87 -0
- package/ui/src/app/header-search/header-search.tsx +294 -0
- package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
- package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
- package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
- package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
- package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
- package/ui/src/app/tree-move/tree-move.tsx +427 -0
- package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
- package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
- package/ui/src/app/user-footer/user-footer.tsx +188 -0
- package/ui/src/app/view-menu/view-menu.tsx +115 -0
- package/ui/src/app-root/app-root.tsx +109 -0
- package/ui/src/app-root/app-root.types.ts +27 -0
- package/ui/src/archive/archive.tsx +417 -0
- package/ui/src/attachment-detail/attachment-detail.ts +52 -0
- package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
- package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
- package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
- package/ui/src/board/board-assignee/board-assignee.ts +48 -0
- package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
- package/ui/src/board/board-chip/board-chip.tsx +46 -0
- package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
- package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
- package/ui/src/board/board-data/board-data.ts +195 -0
- package/ui/src/board/board-data/board-data.types.ts +38 -0
- package/ui/src/board/board-dates/board-dates.ts +40 -0
- package/ui/src/board/board-dates/board-dates.tsx +105 -0
- package/ui/src/board/board-kanban/board-kanban.ts +162 -0
- package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
- package/ui/src/board/board-open-task/board-open-task.ts +32 -0
- package/ui/src/board/board-panel/board-panel.tsx +259 -0
- package/ui/src/board/board-settings/board-settings.tsx +226 -0
- package/ui/src/board/board-status/board-status.tsx +60 -0
- package/ui/src/board/board-stripes/board-stripes.ts +128 -0
- package/ui/src/board/board-table/board-table.ts +53 -0
- package/ui/src/board/board-table/board-table.tsx +610 -0
- package/ui/src/board/board-task/board-task.ts +108 -0
- package/ui/src/board/board-task/board-task.tsx +335 -0
- package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
- package/ui/src/branding/branding.tsx +17 -0
- package/ui/src/branding/custom-logo.ts +1 -0
- package/ui/src/branding/favicon.default.svg +5 -0
- package/ui/src/branding/favicon.svg +5 -0
- package/ui/src/components/ui/avatar.tsx +39 -0
- package/ui/src/components/ui/button.tsx +64 -0
- package/ui/src/components/ui/collapsible.tsx +20 -0
- package/ui/src/components/ui/command.tsx +160 -0
- package/ui/src/components/ui/dialog.tsx +143 -0
- package/ui/src/components/ui/dropdown-menu.tsx +187 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/popover.tsx +41 -0
- package/ui/src/components/ui/select.tsx +163 -0
- package/ui/src/components/ui/separator.tsx +26 -0
- package/ui/src/components/ui/sheet.tsx +136 -0
- package/ui/src/components/ui/sidebar.tsx +693 -0
- package/ui/src/components/ui/skeleton.tsx +13 -0
- package/ui/src/components/ui/switch.tsx +25 -0
- package/ui/src/components/ui/table.tsx +82 -0
- package/ui/src/components/ui/tabs.tsx +52 -0
- package/ui/src/components/ui/tooltip.tsx +51 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
- package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
- package/ui/src/data/query-client.ts +18 -0
- package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/ui/src/data/request-refusal/request-refusal.ts +51 -0
- package/ui/src/data/sign-in/sign-in.ts +97 -0
- package/ui/src/data/sign-in/sign-in.types.ts +15 -0
- package/ui/src/document-link/document-link.tsx +114 -0
- package/ui/src/document-markdown/document-markdown.ts +195 -0
- package/ui/src/editor-schema/editor-schema.ts +23 -0
- package/ui/src/entry-picker/entry-picker.tsx +238 -0
- package/ui/src/feed/feed.tsx +344 -0
- package/ui/src/file-preview/file-preview-view.tsx +496 -0
- package/ui/src/file-preview/file-preview.tsx +28 -0
- package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
- package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
- package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
- package/ui/src/file-preview/word-file-preview.tsx +5 -0
- package/ui/src/flow-runs/flow-runs.tsx +238 -0
- package/ui/src/flows/flows.tsx +1646 -0
- package/ui/src/flows/node-icon/node-icon.ts +34 -0
- package/ui/src/flows/node-palette/node-palette.tsx +214 -0
- package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
- package/ui/src/folder-contents/folder-contents.tsx +321 -0
- package/ui/src/folder-index/folder-index.ts +55 -0
- package/ui/src/frontmatter/frontmatter.tsx +193 -0
- package/ui/src/graph-pane/graph-pane.tsx +53 -0
- package/ui/src/hooks/use-mobile.ts +19 -0
- package/ui/src/i18n/custom.json +4 -0
- package/ui/src/i18n/de.json +609 -0
- package/ui/src/i18n/en.json +609 -0
- package/ui/src/i18n/es.json +609 -0
- package/ui/src/i18n/i18n-context.tsx +65 -0
- package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
- package/ui/src/i18n/i18n.ts +32 -0
- package/ui/src/i18n/i18n.types.ts +49 -0
- package/ui/src/kind-icon.ts +97 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +68 -0
- package/ui/src/modal/modal.tsx +53 -0
- package/ui/src/node-details/node-details.tsx +545 -0
- package/ui/src/node-editor/node-editor.tsx +304 -0
- package/ui/src/node-graph/graph-notice.tsx +21 -0
- package/ui/src/node-graph/node-graph.ts +214 -0
- package/ui/src/node-graph/node-graph.tsx +295 -0
- package/ui/src/node-import/node-import.tsx +418 -0
- package/ui/src/node-save-error.ts +46 -0
- package/ui/src/node-source/node-source.tsx +128 -0
- package/ui/src/node-table/node-table.tsx +107 -0
- package/ui/src/nodes/nodes.tsx +240 -0
- package/ui/src/rename-resource/rename-resource.ts +80 -0
- package/ui/src/resolved-names/resolved-names.tsx +67 -0
- package/ui/src/resource-error.ts +33 -0
- package/ui/src/resource-menu/resource-menu.tsx +1156 -0
- package/ui/src/router/router-context.ts +6 -0
- package/ui/src/router/router.tsx +67 -0
- package/ui/src/router/router.types.ts +8 -0
- package/ui/src/router/selection-search.ts +88 -0
- package/ui/src/save-button/save-button.tsx +94 -0
- package/ui/src/section-hint/section-hint.tsx +40 -0
- package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
- package/ui/src/styles.css +189 -0
- package/ui/src/table-columns/table-columns.tsx +379 -0
- package/ui/src/theme/custom.css +1 -0
- package/ui/src/theme/theme-context.tsx +89 -0
- package/ui/src/theme/theme.ts +70 -0
- package/ui/src/time/relative-time.tsx +41 -0
- package/ui/src/time/time-context.tsx +78 -0
- package/ui/src/time/time.ts +165 -0
- package/ui/src/title-row/title-row.tsx +185 -0
- package/ui/src/tools/tools.tsx +546 -0
- package/ui/src/user-name/user-name.ts +61 -0
- package/ui/src/verbatim/verbatim.tsx +68 -0
- package/ui/tsconfig.json +17 -0
- package/ui/vite.config.ts +129 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useNavigate, useSearch } from "@tanstack/react-router";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
import { openTaskFrom } from "@/router/selection-search.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Which card is open, and the one way to change that.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ **One truth for two places.** The title row and the panel both open cards, and they are
|
|
9
|
+
* rendered in different subtrees: the row above the content, the panel inside it. Written twice,
|
|
10
|
+
* one of them would eventually push where the other replaces, and the back button would behave
|
|
11
|
+
* differently depending on which control was used.
|
|
12
|
+
*/
|
|
13
|
+
export function useOpenTask(): { openId: string | null; open(taskId: string | null): void } {
|
|
14
|
+
const search = useSearch({ strict: false });
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
|
|
17
|
+
const open = useCallback(
|
|
18
|
+
(taskId: string | null) =>
|
|
19
|
+
void navigate({
|
|
20
|
+
to: ".",
|
|
21
|
+
// ⚠️ **Closing REPLACES, opening pushes.** Opening a card is a step somebody took and the
|
|
22
|
+
// back button must undo it; closing is the undo itself. Pushed as well, the history holds
|
|
23
|
+
// board → card → board, and one press of Back re-opens the card that was just closed.
|
|
24
|
+
replace: taskId === null,
|
|
25
|
+
search: (previous: Record<string, unknown>) =>
|
|
26
|
+
taskId === null ? { ...previous, task: undefined } : { ...previous, task: taskId },
|
|
27
|
+
}),
|
|
28
|
+
[navigate],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return { openId: openTaskFrom(search), open };
|
|
32
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { BoardTaskFilter } from "@contract/board.ts";
|
|
2
|
+
import type { Node } from "@contract/node.ts";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useRouterState } from "@tanstack/react-router";
|
|
5
|
+
import type { ColumnVisibilityState } from "@tanstack/react-table";
|
|
6
|
+
import { Settings } from "lucide-react";
|
|
7
|
+
import { Suspense, useState } from "react";
|
|
8
|
+
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
9
|
+
import { ViewMenu } from "@/app/view-menu/view-menu.tsx";
|
|
10
|
+
import { useBoard } from "@/board/board-data/board-data.ts";
|
|
11
|
+
import { BoardKanban } from "@/board/board-kanban/board-kanban.tsx";
|
|
12
|
+
import { useOpenTask } from "@/board/board-open-task/board-open-task.ts";
|
|
13
|
+
import { BoardSettings } from "@/board/board-settings/board-settings.tsx";
|
|
14
|
+
import { BoardTable } from "@/board/board-table/board-table.tsx";
|
|
15
|
+
import { BoardTaskDocument } from "@/board/board-task/board-task.tsx";
|
|
16
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
17
|
+
import { NodeEditor } from "@/node-editor/node-editor.tsx";
|
|
18
|
+
import { ResolvedNamesProvider } from "@/resolved-names/resolved-names.tsx";
|
|
19
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
20
|
+
import { type IntelView, viewOf } from "@/router/selection-search.ts";
|
|
21
|
+
|
|
22
|
+
// ⚠️ The kanban stands first because the first view is the DEFAULT, and the default is what the
|
|
23
|
+
// absence of the `view` parameter means (`selection-search.ts`). A board opened from a plain link
|
|
24
|
+
// is a kanban, exactly as it was before its state moved into the address (#818).
|
|
25
|
+
const BoardViews: readonly IntelView[] = ["kanban", "table"];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A board on the node screen, beside the folder, the table and the editor (D66, #376).
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ The whole board is ONE answer. `board_get` returns the columns and every card in them, and the
|
|
31
|
+
* views below read that one object — no view fetches per card, per column, or on scroll.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ **The controls live in the title line, not in a row of their own** (#363). `title-actions` puts
|
|
34
|
+
* them before the three-dot menu, which is where everything that acts on THIS thing already sits —
|
|
35
|
+
* a third bar above the board would be a permanent row for something nobody uses twice a minute.
|
|
36
|
+
*/
|
|
37
|
+
export function BoardPanel({ node }: { node: Node }) {
|
|
38
|
+
const i18n = useI18n();
|
|
39
|
+
/**
|
|
40
|
+
* ⚠️ **In the address, not in this component** (#818). It used to be `useState` here, and that was
|
|
41
|
+
* the second of the two switches the ticket abolishes: it looked like the editor/graph switch and
|
|
42
|
+
* behaved differently — a table somebody was reading could not be linked to, and a reload put
|
|
43
|
+
* them back on the kanban. The two now answer to one mechanism, and this is the half that had to
|
|
44
|
+
* move for that sentence to be true.
|
|
45
|
+
*/
|
|
46
|
+
const view = useRouterState({
|
|
47
|
+
select: (current) => viewOf(current.location.search, BoardViews),
|
|
48
|
+
});
|
|
49
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
50
|
+
// Which columns the table shows outlives the trip through the kanban — see `BoardTable`.
|
|
51
|
+
const [hidden, setHidden] = useState<ColumnVisibilityState>({});
|
|
52
|
+
/**
|
|
53
|
+
* ⚠️ **The filter lives HERE and not in the table, since #671.** Opening a card unmounts the
|
|
54
|
+
* table, so a filter kept down there was silently thrown away by the one control that is the only
|
|
55
|
+
* way back out. It does NOT reach the kanban: that view reads this component's own unfiltered
|
|
56
|
+
* board answer, a different query — which is what kept the filter out of it before, and still is.
|
|
57
|
+
*/
|
|
58
|
+
const [filter, setFilter] = useState<Partial<BoardTaskFilter>>({});
|
|
59
|
+
const board = useBoard(node.id);
|
|
60
|
+
const { openId, open } = useOpenTask();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* ⚠️ Collected HERE and nowhere lower (#700). This component holds the one unfiltered board
|
|
64
|
+
* answer that both views and the open card read, so the ids are already all in one place. A hook
|
|
65
|
+
* per card would open one request per row for a handful of distinct people, and a hook per view
|
|
66
|
+
* would ask twice for the same names when somebody switches between them.
|
|
67
|
+
*/
|
|
68
|
+
const assigneeIds = (board.columns ?? []).flatMap((column) =>
|
|
69
|
+
column.cards.map((card) => card.assigneeId),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<ResolvedNamesProvider ids={assigneeIds}>
|
|
74
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
75
|
+
<ActionSlot name="title-actions">
|
|
76
|
+
{/* ⚠️ **Gone while a card is open.** The switch chooses between two views OF THE BOARD, and
|
|
77
|
+
an open card is neither — left standing it would name a view nobody is looking at and
|
|
78
|
+
navigate away from the card when used, which is a control that lies twice. */}
|
|
79
|
+
{openId === null ? <ViewMenu views={BoardViews} /> : null}
|
|
80
|
+
{/* ⚠️ Gone while a card is open, like the view switch above it. It configures the BOARD,
|
|
81
|
+
and the line it sits in belongs to the card then — a control that acts on something the
|
|
82
|
+
reader is not looking at is worse than a missing one, because it looks like it acts on
|
|
83
|
+
what they see. */}
|
|
84
|
+
{openId === null ? (
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
aria-label={i18n.t("board.settings.open")}
|
|
88
|
+
onClick={() => setSettingsOpen(true)}
|
|
89
|
+
className="inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
90
|
+
>
|
|
91
|
+
<Settings aria-hidden="true" className="size-4" />
|
|
92
|
+
</button>
|
|
93
|
+
) : null}
|
|
94
|
+
</ActionSlot>
|
|
95
|
+
{/* ⚠️ Mounted only while open, so its draft is seeded once per opening. Handed an `open` flag
|
|
96
|
+
instead, a draft re-seeded from the board would lose what somebody typed the moment a
|
|
97
|
+
write from elsewhere answers. */}
|
|
98
|
+
{settingsOpen ? (
|
|
99
|
+
<BoardSettings board={board} onClose={() => setSettingsOpen(false)} />
|
|
100
|
+
) : null}
|
|
101
|
+
{openId === null ? (
|
|
102
|
+
view === "kanban" ? (
|
|
103
|
+
<KanbanView board={board} onOpen={open} />
|
|
104
|
+
) : (
|
|
105
|
+
<TableView
|
|
106
|
+
nodeId={node.id}
|
|
107
|
+
hidden={hidden}
|
|
108
|
+
onHidden={setHidden}
|
|
109
|
+
onOpen={open}
|
|
110
|
+
filter={filter}
|
|
111
|
+
onFilter={setFilter}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
) : (
|
|
115
|
+
// ⚠️ Drawn from the panel's own UNFILTERED board answer, which is also what the kanban
|
|
116
|
+
// reads — the open task is not a second read of the card. ⚠️ It is NOT the table's answer:
|
|
117
|
+
// the table keeps its own filtered query, so a card filtered out of the table still opens
|
|
118
|
+
// from a link. What this cannot find is a card that is gone, or one on another board.
|
|
119
|
+
<OpenTask board={board} taskId={openId} onOpen={open} />
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</ResolvedNamesProvider>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function KanbanView({
|
|
127
|
+
board,
|
|
128
|
+
onOpen,
|
|
129
|
+
}: {
|
|
130
|
+
board: ReturnType<typeof useBoard>;
|
|
131
|
+
onOpen(taskId: string): void;
|
|
132
|
+
}) {
|
|
133
|
+
return <BoardKanban board={board} onOpen={onOpen} />;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One card, opened.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ **The body is the node's own document and is the ONE thing this screen reads separately.** The
|
|
140
|
+
* head and the link list come out of `board_get`; the markdown text does not live in that answer at
|
|
141
|
+
* all — a card's text is a node version, and `board_get` returns cards, not documents.
|
|
142
|
+
*/
|
|
143
|
+
function OpenTask({
|
|
144
|
+
board,
|
|
145
|
+
taskId,
|
|
146
|
+
onOpen,
|
|
147
|
+
}: {
|
|
148
|
+
board: ReturnType<typeof useBoard>;
|
|
149
|
+
taskId: string;
|
|
150
|
+
onOpen(taskId: string | null): void;
|
|
151
|
+
}) {
|
|
152
|
+
const i18n = useI18n();
|
|
153
|
+
const task = board.columns.flatMap((entry) => entry.cards).find((card) => card.id === taskId);
|
|
154
|
+
|
|
155
|
+
if (board.isPending) {
|
|
156
|
+
return <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
|
|
157
|
+
}
|
|
158
|
+
// ⚠️ Back to the board rather than an empty screen. The board answer here is unfiltered, so a
|
|
159
|
+
// card missing from it is a card that is gone, archived out of view, or on a different board —
|
|
160
|
+
// a stale link and a deleted card look exactly the same from here, and both want the same way out.
|
|
161
|
+
if (task === undefined) {
|
|
162
|
+
return (
|
|
163
|
+
<div className="flex flex-col items-start gap-3 p-6">
|
|
164
|
+
<p className="text-sm text-muted-foreground">{i18n.t("board.taskGone")}</p>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
onClick={() => onOpen(null)}
|
|
168
|
+
className="rounded-md border px-3 py-1 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
169
|
+
>
|
|
170
|
+
{board.title}
|
|
171
|
+
</button>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<BoardTaskDocument
|
|
178
|
+
task={task}
|
|
179
|
+
board={board}
|
|
180
|
+
onOpen={onOpen}
|
|
181
|
+
body={<TaskBody nodeId={task.id} />}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* ⚠️ **The filter is handed DOWN since #671, and that is safe for a different reason than it looks.**
|
|
188
|
+
* It used to live here so that unmounting the table took it along — until opening a card, which
|
|
189
|
+
* also unmounts the table, threw the filter away through the one control that is the way back out.
|
|
190
|
+
* It sits in the panel now, and the kanban still shows everything: that view reads the panel's own
|
|
191
|
+
* unfiltered query, a different key, so no filtered answer ever reaches it. Measured, not argued —
|
|
192
|
+
* after a switch there is no new request at all, and `leaves the kanban unfiltered after a filter
|
|
193
|
+
* was set in the table` renders the whole panel, so it exercises exactly this arrangement.
|
|
194
|
+
*/
|
|
195
|
+
function TableView({
|
|
196
|
+
nodeId,
|
|
197
|
+
hidden,
|
|
198
|
+
onHidden,
|
|
199
|
+
onOpen,
|
|
200
|
+
filter,
|
|
201
|
+
onFilter,
|
|
202
|
+
}: {
|
|
203
|
+
nodeId: string;
|
|
204
|
+
hidden: ColumnVisibilityState;
|
|
205
|
+
onHidden(next: ColumnVisibilityState): void;
|
|
206
|
+
onOpen(taskId: string): void;
|
|
207
|
+
filter: Partial<BoardTaskFilter>;
|
|
208
|
+
onFilter(next: Partial<BoardTaskFilter>): void;
|
|
209
|
+
}) {
|
|
210
|
+
const board = useBoard(nodeId, filter);
|
|
211
|
+
// ⚠️ Sorting and grouping live INSIDE the table since #670 — they are the table's own state and
|
|
212
|
+
// never travel. The column choice and the filter are handed DOWN from the panel because both have
|
|
213
|
+
// to outlive this component; the kanban stays unfiltered because it reads a different query.
|
|
214
|
+
return (
|
|
215
|
+
<BoardTable
|
|
216
|
+
board={board}
|
|
217
|
+
filter={filter}
|
|
218
|
+
onFilter={onFilter}
|
|
219
|
+
hidden={hidden}
|
|
220
|
+
onHidden={onHidden}
|
|
221
|
+
onOpen={onOpen}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The card's markdown text — the node's own document, in the editor every other document uses.
|
|
228
|
+
*
|
|
229
|
+
* ⚠️ **A second read, and the only one on this screen.** `board_get` answers with cards, not with
|
|
230
|
+
* documents: a card's text is a node version and is not in that answer at all. Building a second
|
|
231
|
+
* editor here instead would be the more expensive mistake — one text surface that saves, links and
|
|
232
|
+
* renders differently from the one next door.
|
|
233
|
+
*/
|
|
234
|
+
function TaskBody({ nodeId }: { nodeId: string }) {
|
|
235
|
+
const i18n = useI18n();
|
|
236
|
+
const { data } = useIntelRouterContext();
|
|
237
|
+
const queryClient = useQueryClient();
|
|
238
|
+
const document = useQuery({
|
|
239
|
+
queryKey: ["node", nodeId],
|
|
240
|
+
queryFn: () => data.getNode(nodeId),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (document.data === undefined) {
|
|
244
|
+
return <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
|
|
245
|
+
}
|
|
246
|
+
return (
|
|
247
|
+
<Suspense
|
|
248
|
+
fallback={<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>}
|
|
249
|
+
>
|
|
250
|
+
<NodeEditor
|
|
251
|
+
key={nodeId}
|
|
252
|
+
data={data}
|
|
253
|
+
document={document.data}
|
|
254
|
+
i18n={i18n}
|
|
255
|
+
onSaved={(saved) => queryClient.setQueryData(["node", nodeId], saved)}
|
|
256
|
+
/>
|
|
257
|
+
</Suspense>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { ARCHIVE_COLUMN_ID, type BoardColumn, DEFAULT_COLUMN_COLOR } from "@contract/board.ts";
|
|
2
|
+
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A board's basic settings: its columns, and whether the archive is drawn.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **The whole list is written at once and never patched.** Adding, renaming and reordering are
|
|
19
|
+
* one call, because the ORDER is the list's order — there is nothing to patch a position with. So
|
|
20
|
+
* the dialog holds a draft and sends it on save; a control that wrote per keystroke would write a
|
|
21
|
+
* version per letter.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ **The archive column is not in this list and cannot be.** It is derived from `archived_at`
|
|
24
|
+
* (D68) and the server refuses a configured column that claims its id. What the switch below
|
|
25
|
+
* changes is only whether it is drawn.
|
|
26
|
+
*/
|
|
27
|
+
export function BoardSettings({ board, onClose }: { board: BoardHandle; onClose(): void }) {
|
|
28
|
+
const i18n = useI18n();
|
|
29
|
+
// ⚠️ Seeded once per OPENING, which is why the caller mounts this only while it is open rather
|
|
30
|
+
// than handing it an `open` flag: a draft re-seeded from a prop would lose what somebody typed
|
|
31
|
+
// the moment the board answers a write from somewhere else.
|
|
32
|
+
const [columns, setColumns] = useState<BoardColumn[]>(() =>
|
|
33
|
+
board.columns
|
|
34
|
+
.filter((entry) => entry.unknown !== true && entry.column.id !== ARCHIVE_COLUMN_ID)
|
|
35
|
+
.map((entry) => entry.column),
|
|
36
|
+
);
|
|
37
|
+
const [archiveVisible, setArchiveVisible] = useState(board.archiveVisible);
|
|
38
|
+
const [failed, setFailed] = useState(false);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How many cards each column carries, taken once from the board.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **A column with cards is not removable here** — `destructive.md`: *verweigern schlägt
|
|
44
|
+
* kaskadieren*. Removed, its cards do not vanish from the database, but their `status` names a
|
|
45
|
+
* column nobody configured any more: they fall into the stray column, and nobody was asked where
|
|
46
|
+
* they should go. That is the same cascade one level delayed.
|
|
47
|
+
*/
|
|
48
|
+
const cardsIn = new Map(board.columns.map((entry) => [entry.column.id, entry.cards.length]));
|
|
49
|
+
|
|
50
|
+
const move = (index: number, by: -1 | 1) => {
|
|
51
|
+
const next = [...columns];
|
|
52
|
+
const target = index + by;
|
|
53
|
+
const a = next[index];
|
|
54
|
+
const b = next[target];
|
|
55
|
+
if (a === undefined || b === undefined) return;
|
|
56
|
+
next[index] = b;
|
|
57
|
+
next[target] = a;
|
|
58
|
+
setColumns(next);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const save = () => {
|
|
62
|
+
setFailed(false);
|
|
63
|
+
void board
|
|
64
|
+
.setColumns({
|
|
65
|
+
columns: columns.map((column) => ({ ...column, title: column.title.trim() })),
|
|
66
|
+
archiveVisible,
|
|
67
|
+
idempotencyKey: crypto.randomUUID(),
|
|
68
|
+
})
|
|
69
|
+
.then(onClose)
|
|
70
|
+
.catch(() => setFailed(true));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ⚠️ An empty title would be refused by the contract, and a list with none at all by the server.
|
|
74
|
+
// Saying so before the call is the difference between a form and a guessing game.
|
|
75
|
+
const titles = columns.map((column) => column.title.trim());
|
|
76
|
+
/**
|
|
77
|
+
* ⚠️ **Two columns with the same NAME are refused too**, although the server only refuses
|
|
78
|
+
* duplicate ids. An id nobody sees may repeat without confusing anybody; a name is what a person
|
|
79
|
+
* reads, and two columns called the same thing are two columns nobody can tell apart — on the
|
|
80
|
+
* board, in the filter, and in this very dialog.
|
|
81
|
+
*/
|
|
82
|
+
const duplicate = titles.find((title, index) => titles.indexOf(title) !== index);
|
|
83
|
+
const incomplete =
|
|
84
|
+
columns.length === 0 || titles.some((title) => title.length === 0) || duplicate !== undefined;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Dialog open onOpenChange={(next) => (next ? undefined : onClose())}>
|
|
88
|
+
<DialogContent>
|
|
89
|
+
<DialogHeader>
|
|
90
|
+
<DialogTitle>{i18n.t("board.settings.title")}</DialogTitle>
|
|
91
|
+
<DialogDescription>{i18n.t("board.settings.description")}</DialogDescription>
|
|
92
|
+
</DialogHeader>
|
|
93
|
+
|
|
94
|
+
<ul className="space-y-2">
|
|
95
|
+
{columns.map((column, index) => (
|
|
96
|
+
<li key={column.id} className="flex items-center gap-2">
|
|
97
|
+
{/* ⚠️ A free colour, not a palette (Jack's decision 2026-08-22) — see the contract
|
|
98
|
+
for why that is affordable here: the value only ever fills a dot.
|
|
99
|
+
⚠️ The native `input type="color"`, not a built one. It is the platform's own
|
|
100
|
+
picker, it works with a keyboard and a screen reader without anybody writing that
|
|
101
|
+
part, and "kaufen statt bauen" is the rule this repository states for exactly this
|
|
102
|
+
case. */}
|
|
103
|
+
<input
|
|
104
|
+
type="color"
|
|
105
|
+
value={column.color ?? DEFAULT_COLUMN_COLOR}
|
|
106
|
+
aria-label={i18n.t("board.settings.columnColor", { column: column.title })}
|
|
107
|
+
onChange={(event) => {
|
|
108
|
+
const next = [...columns];
|
|
109
|
+
next[index] = { ...column, color: event.target.value };
|
|
110
|
+
setColumns(next);
|
|
111
|
+
}}
|
|
112
|
+
className="size-7 shrink-0 cursor-pointer rounded-md border bg-background outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
113
|
+
/>
|
|
114
|
+
<input
|
|
115
|
+
value={column.title}
|
|
116
|
+
aria-label={i18n.t("board.settings.columnName", { column: column.title })}
|
|
117
|
+
onChange={(event) => {
|
|
118
|
+
const next = [...columns];
|
|
119
|
+
next[index] = { ...column, title: event.target.value };
|
|
120
|
+
setColumns(next);
|
|
121
|
+
}}
|
|
122
|
+
className="min-w-0 flex-1 rounded-md border bg-background px-2 py-1 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
123
|
+
/>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
aria-label={i18n.t("board.settings.moveUp", { column: column.title })}
|
|
127
|
+
disabled={index === 0}
|
|
128
|
+
onClick={() => move(index, -1)}
|
|
129
|
+
className="rounded-md p-1 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-30"
|
|
130
|
+
>
|
|
131
|
+
<ChevronUp aria-hidden="true" className="size-4" />
|
|
132
|
+
</button>
|
|
133
|
+
<button
|
|
134
|
+
type="button"
|
|
135
|
+
aria-label={i18n.t("board.settings.moveDown", { column: column.title })}
|
|
136
|
+
disabled={index === columns.length - 1}
|
|
137
|
+
onClick={() => move(index, 1)}
|
|
138
|
+
className="rounded-md p-1 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-30"
|
|
139
|
+
>
|
|
140
|
+
<ChevronDown aria-hidden="true" className="size-4" />
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
aria-label={
|
|
145
|
+
(cardsIn.get(column.id) ?? 0) > 0
|
|
146
|
+
? // ⚠️ No count in this sentence. `packages/intel/ui/CLAUDE.md`: a sentence that
|
|
147
|
+
// carries a number has to read correctly at ONE, and "1 card(s)" is a written
|
|
148
|
+
// admission that it does not. The number is on the board anyway; here what
|
|
149
|
+
// matters is that the column is not empty.
|
|
150
|
+
i18n.t("board.settings.removeBlocked", { column: column.title })
|
|
151
|
+
: i18n.t("board.settings.remove", { column: column.title })
|
|
152
|
+
}
|
|
153
|
+
disabled={(cardsIn.get(column.id) ?? 0) > 0}
|
|
154
|
+
onClick={() => setColumns(columns.filter((entry) => entry.id !== column.id))}
|
|
155
|
+
className="rounded-md p-1 outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-30"
|
|
156
|
+
>
|
|
157
|
+
<X aria-hidden="true" className="size-4" />
|
|
158
|
+
</button>
|
|
159
|
+
</li>
|
|
160
|
+
))}
|
|
161
|
+
</ul>
|
|
162
|
+
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
onClick={() =>
|
|
166
|
+
setColumns([
|
|
167
|
+
...columns,
|
|
168
|
+
{
|
|
169
|
+
id: crypto.randomUUID(),
|
|
170
|
+
title: i18n.t("board.settings.newColumn"),
|
|
171
|
+
terminal: false,
|
|
172
|
+
},
|
|
173
|
+
])
|
|
174
|
+
}
|
|
175
|
+
className="self-start rounded-md px-2 py-1 text-sm text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
176
|
+
>
|
|
177
|
+
{i18n.t("board.settings.addColumn")}
|
|
178
|
+
</button>
|
|
179
|
+
|
|
180
|
+
<label className="flex items-start gap-3 border-t pt-3 text-sm">
|
|
181
|
+
<input
|
|
182
|
+
type="checkbox"
|
|
183
|
+
checked={archiveVisible}
|
|
184
|
+
onChange={(event) => setArchiveVisible(event.target.checked)}
|
|
185
|
+
className="mt-1"
|
|
186
|
+
/>
|
|
187
|
+
<span>
|
|
188
|
+
{i18n.t("board.settings.showArchive")}
|
|
189
|
+
<span className="block text-xs text-muted-foreground">
|
|
190
|
+
{i18n.t("board.settings.showArchiveHint")}
|
|
191
|
+
</span>
|
|
192
|
+
</span>
|
|
193
|
+
</label>
|
|
194
|
+
|
|
195
|
+
{duplicate === undefined ? null : (
|
|
196
|
+
<p role="alert" className="text-sm text-destructive">
|
|
197
|
+
{i18n.t("board.settings.duplicateName", { column: duplicate })}
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
200
|
+
{failed ? (
|
|
201
|
+
<p role="alert" className="text-sm text-destructive">
|
|
202
|
+
{i18n.t("board.settings.failed")}
|
|
203
|
+
</p>
|
|
204
|
+
) : null}
|
|
205
|
+
|
|
206
|
+
<DialogFooter>
|
|
207
|
+
<button
|
|
208
|
+
type="button"
|
|
209
|
+
onClick={onClose}
|
|
210
|
+
className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
211
|
+
>
|
|
212
|
+
{i18n.t("common.cancel")}
|
|
213
|
+
</button>
|
|
214
|
+
<button
|
|
215
|
+
type="button"
|
|
216
|
+
disabled={incomplete || board.isWriting}
|
|
217
|
+
onClick={save}
|
|
218
|
+
className="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
219
|
+
>
|
|
220
|
+
{i18n.t("common.save")}
|
|
221
|
+
</button>
|
|
222
|
+
</DialogFooter>
|
|
223
|
+
</DialogContent>
|
|
224
|
+
</Dialog>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { BoardColumn } from "@contract/board.ts";
|
|
2
|
+
import { ARCHIVE_COLUMN_ID } from "@contract/board.ts";
|
|
3
|
+
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuRadioGroup,
|
|
8
|
+
DropdownMenuRadioItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from "@/components/ui/dropdown-menu.tsx";
|
|
11
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The column a card sits in, and the way to move it.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ **One truth for two surfaces.** The opened card and the table both offer this, and the archive
|
|
17
|
+
* is an entry in it: written twice, one of them would eventually keep an entry the other dropped,
|
|
18
|
+
* and a reader would find a card archivable from one screen and not from the other. Exactly the
|
|
19
|
+
* reason `AssigneeChip` was pulled out beside it.
|
|
20
|
+
*/
|
|
21
|
+
export function StatusChip({
|
|
22
|
+
status,
|
|
23
|
+
title,
|
|
24
|
+
columns,
|
|
25
|
+
write,
|
|
26
|
+
}: {
|
|
27
|
+
status: string;
|
|
28
|
+
title: string;
|
|
29
|
+
columns: BoardColumn[];
|
|
30
|
+
write(input: Partial<Omit<Parameters<BoardHandle["updateTask"]>[0], "taskId">>): void;
|
|
31
|
+
}) {
|
|
32
|
+
const i18n = useI18n();
|
|
33
|
+
return (
|
|
34
|
+
<DropdownMenu>
|
|
35
|
+
<DropdownMenuTrigger
|
|
36
|
+
// ⚠️ The VALUE first, then what pressing it does. A label that says only "move to a column"
|
|
37
|
+
// replaces the one thing the pill is there to show, and that fact then exists on screen and
|
|
38
|
+
// nowhere else.
|
|
39
|
+
aria-label={`${title}, ${i18n.t("board.moveTo")}`}
|
|
40
|
+
className="shrink-0 rounded-full border px-2.5 py-0.5 text-xs outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
41
|
+
>
|
|
42
|
+
{title}
|
|
43
|
+
</DropdownMenuTrigger>
|
|
44
|
+
<DropdownMenuContent align="start">
|
|
45
|
+
{/* ⚠️ Where a card SITS is a move, not a field edit: the server computes a position and
|
|
46
|
+
checks the cycles for it. Everything else on these screens is a plain change. */}
|
|
47
|
+
<DropdownMenuRadioGroup value={status} onValueChange={(next) => write({ status: next })}>
|
|
48
|
+
{columns.map((entry) => (
|
|
49
|
+
<DropdownMenuRadioItem key={entry.id} value={entry.id}>
|
|
50
|
+
{entry.title}
|
|
51
|
+
</DropdownMenuRadioItem>
|
|
52
|
+
))}
|
|
53
|
+
<DropdownMenuRadioItem value={ARCHIVE_COLUMN_ID}>
|
|
54
|
+
{i18n.t("board.column.archive")}
|
|
55
|
+
</DropdownMenuRadioItem>
|
|
56
|
+
</DropdownMenuRadioGroup>
|
|
57
|
+
</DropdownMenuContent>
|
|
58
|
+
</DropdownMenu>
|
|
59
|
+
);
|
|
60
|
+
}
|