@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,610 @@
|
|
|
1
|
+
import type { BoardColumn, BoardTaskFilter } from "@contract/board.ts";
|
|
2
|
+
import {
|
|
3
|
+
type ColumnVisibilityState,
|
|
4
|
+
columnGroupingFeature,
|
|
5
|
+
columnVisibilityFeature,
|
|
6
|
+
createColumnHelper,
|
|
7
|
+
createExpandedRowModel,
|
|
8
|
+
createGroupedRowModel,
|
|
9
|
+
createSortedRowModel,
|
|
10
|
+
type ExpandedState,
|
|
11
|
+
type GroupingState,
|
|
12
|
+
type Row as LibRow,
|
|
13
|
+
rowExpandingFeature,
|
|
14
|
+
rowSortingFeature,
|
|
15
|
+
type SortingState,
|
|
16
|
+
sortFn_alphanumeric,
|
|
17
|
+
sortFn_basic,
|
|
18
|
+
tableFeatures,
|
|
19
|
+
useTable,
|
|
20
|
+
} from "@tanstack/react-table";
|
|
21
|
+
import { ArrowUpDown, ChevronDown, ChevronRight, Columns3, Filter, LayoutList } from "lucide-react";
|
|
22
|
+
import { useMemo, useState } from "react";
|
|
23
|
+
import { useAssigneeLabel } from "@/board/board-assignee/board-assignee.ts";
|
|
24
|
+
import { AssigneeChip } from "@/board/board-assignee/board-assignee.tsx";
|
|
25
|
+
import { BoardChip } from "@/board/board-chip/board-chip.tsx";
|
|
26
|
+
import { isFiltering } from "@/board/board-data/board-data.ts";
|
|
27
|
+
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
28
|
+
import { StatusChip } from "@/board/board-status/board-status.tsx";
|
|
29
|
+
import {
|
|
30
|
+
DropdownMenu,
|
|
31
|
+
DropdownMenuCheckboxItem,
|
|
32
|
+
DropdownMenuContent,
|
|
33
|
+
DropdownMenuRadioGroup,
|
|
34
|
+
DropdownMenuRadioItem,
|
|
35
|
+
DropdownMenuTrigger,
|
|
36
|
+
} from "@/components/ui/dropdown-menu";
|
|
37
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
38
|
+
import { type Row, rowsOf } from "./board-table.ts";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ⚠️ Registered one by one rather than through `stockFeatures`. Every feature this table does not
|
|
42
|
+
* use — pagination, selection, pinning, resizing, faceting — would otherwise sit in the bundle of a
|
|
43
|
+
* screen that never calls it, and the list below is also the honest answer to "what can this table
|
|
44
|
+
* do".
|
|
45
|
+
*/
|
|
46
|
+
const features = tableFeatures({
|
|
47
|
+
columnGroupingFeature,
|
|
48
|
+
columnVisibilityFeature,
|
|
49
|
+
rowExpandingFeature,
|
|
50
|
+
rowSortingFeature,
|
|
51
|
+
// ⚠️ A row-model slot has to stand AFTER the feature it needs: sorting, then grouping, then
|
|
52
|
+
// expanding — the order the rows actually pass through.
|
|
53
|
+
sortedRowModel: createSortedRowModel(),
|
|
54
|
+
groupedRowModel: createGroupedRowModel(),
|
|
55
|
+
expandedRowModel: createExpandedRowModel(),
|
|
56
|
+
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** One row of THIS table — the library's row type, bound to these features and this data. */
|
|
60
|
+
type TableRow = LibRow<typeof features, Row>;
|
|
61
|
+
|
|
62
|
+
const helper = createColumnHelper<typeof features, Row>();
|
|
63
|
+
|
|
64
|
+
// The one place a grouping is offered. Not free-form: a board has fixed fields, and grouping by
|
|
65
|
+
// anything else would be grouping by something nobody can create.
|
|
66
|
+
/**
|
|
67
|
+
* ⚠️ **Empty becomes `undefined`, never `null`.** `sortUndefined: "last"` fires on `undefined`
|
|
68
|
+
* only; with `null` the row falls through to the comparator, where `null < "2026-08-19"` and
|
|
69
|
+
* `null > "2026-08-19"` are BOTH false. The comparator then claims an equality that is not there,
|
|
70
|
+
* and the order depends on where the empty rows happened to sit. A card without a due date is not
|
|
71
|
+
* the earliest one — it is one whose date nobody knows.
|
|
72
|
+
*/
|
|
73
|
+
const orNothing = (value: string | null): string | undefined => value ?? undefined;
|
|
74
|
+
|
|
75
|
+
export function BoardTable({
|
|
76
|
+
board,
|
|
77
|
+
filter,
|
|
78
|
+
onFilter,
|
|
79
|
+
hidden,
|
|
80
|
+
onHidden,
|
|
81
|
+
onOpen,
|
|
82
|
+
}: {
|
|
83
|
+
board: BoardHandle;
|
|
84
|
+
filter: Partial<BoardTaskFilter>;
|
|
85
|
+
onFilter(next: Partial<BoardTaskFilter>): void;
|
|
86
|
+
/**
|
|
87
|
+
* ⚠️ Held ABOVE the view switch, like the filter since #671. This component is unmounted the
|
|
88
|
+
* moment the kanban is shown OR a card is opened, so state kept here would be gone on the way
|
|
89
|
+
* back, and the choice would silently undo itself. Sorting and grouping do stay here: they are
|
|
90
|
+
* about the rows on screen and have no meaning once the rows are gone.
|
|
91
|
+
*/
|
|
92
|
+
hidden: ColumnVisibilityState;
|
|
93
|
+
onHidden(next: ColumnVisibilityState): void;
|
|
94
|
+
/** ⚠️ The SAME surface a kanban card opens — one screen for a task, not two that agree today. */
|
|
95
|
+
onOpen(taskId: string): void;
|
|
96
|
+
}) {
|
|
97
|
+
const i18n = useI18n();
|
|
98
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
99
|
+
const [grouping, setGrouping] = useState<GroupingState>([]);
|
|
100
|
+
const [expanded, setExpanded] = useState<ExpandedState>(true);
|
|
101
|
+
|
|
102
|
+
const data = useMemo(() => rowsOf(board.columns), [board.columns]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* ⚠️ `helper.columns([...])` rather than a bare array. The accessors carry different value types
|
|
106
|
+
* (`string`, `string | undefined`), and a plain array makes TypeScript compare their column
|
|
107
|
+
* definitions structurally — where the `header` and `footer` templates are contravariant and the
|
|
108
|
+
* error reads "two different types with this name exist". The helper is the API's own answer to
|
|
109
|
+
* that; it is not a cast, and it keeps every accessor's own type.
|
|
110
|
+
*/
|
|
111
|
+
const columns = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
helper.columns([
|
|
114
|
+
helper.accessor((row) => row.task.title, {
|
|
115
|
+
id: "title",
|
|
116
|
+
header: i18n.t("board.column.title"),
|
|
117
|
+
// ⚠️ Not hideable. It IS the row: an entry in the chooser that changes nothing when
|
|
118
|
+
// pressed is a control that lies about what it does.
|
|
119
|
+
enableHiding: false,
|
|
120
|
+
sortFn: "alphanumeric",
|
|
121
|
+
// Grouping by the title would make one group per row, which is the table again with an
|
|
122
|
+
// extra line above every entry.
|
|
123
|
+
enableGrouping: false,
|
|
124
|
+
}),
|
|
125
|
+
helper.accessor((row) => row.columnTitle, {
|
|
126
|
+
id: "status",
|
|
127
|
+
header: i18n.t("board.column.status"),
|
|
128
|
+
sortFn: "alphanumeric",
|
|
129
|
+
}),
|
|
130
|
+
helper.accessor((row) => orNothing(row.task.assigneeId), {
|
|
131
|
+
id: "assignee",
|
|
132
|
+
header: i18n.t("board.column.assignee"),
|
|
133
|
+
sortFn: "alphanumeric",
|
|
134
|
+
sortUndefined: "last",
|
|
135
|
+
// ⚠️ Grouped by the RAW id, drawn as a name. Grouping by what is drawn would put everybody
|
|
136
|
+
// this browser cannot name into one group called "a user account".
|
|
137
|
+
}),
|
|
138
|
+
helper.accessor((row) => orNothing(row.task.startDate), {
|
|
139
|
+
id: "start",
|
|
140
|
+
header: i18n.t("board.column.start"),
|
|
141
|
+
// An ISO date sorts correctly as a string, so `basic` is the whole of the comparison.
|
|
142
|
+
sortFn: "basic",
|
|
143
|
+
sortUndefined: "last",
|
|
144
|
+
enableGrouping: false,
|
|
145
|
+
}),
|
|
146
|
+
helper.accessor((row) => orNothing(row.task.dueDate), {
|
|
147
|
+
id: "due",
|
|
148
|
+
header: i18n.t("board.column.due"),
|
|
149
|
+
// An ISO date sorts correctly as a string, so `basic` is the whole of the comparison.
|
|
150
|
+
sortFn: "basic",
|
|
151
|
+
sortUndefined: "last",
|
|
152
|
+
enableGrouping: false,
|
|
153
|
+
}),
|
|
154
|
+
helper.accessor((row) => orNothing(row.blocking), {
|
|
155
|
+
id: "dependsOn",
|
|
156
|
+
header: i18n.t("board.column.dependsOn"),
|
|
157
|
+
sortFn: "alphanumeric",
|
|
158
|
+
sortUndefined: "last",
|
|
159
|
+
// Grouping by what a card waits for is a real question: everything blocked by the same
|
|
160
|
+
// card, in one place.
|
|
161
|
+
}),
|
|
162
|
+
helper.accessor((row) => row.task.labels.join(", "), {
|
|
163
|
+
id: "labels",
|
|
164
|
+
header: i18n.t("board.column.labels"),
|
|
165
|
+
enableSorting: false,
|
|
166
|
+
// ⚠️ NOT groupable. A card carries several labels, and grouping a joined string groups by
|
|
167
|
+
// the accident of their order: `a, b` becomes a group of its own rather than appearing
|
|
168
|
+
// under `a` and under `b`. That is not "grouped by label", and it looks close enough to
|
|
169
|
+
// it that nobody checks.
|
|
170
|
+
enableGrouping: false,
|
|
171
|
+
}),
|
|
172
|
+
]),
|
|
173
|
+
[i18n],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const table = useTable({
|
|
177
|
+
features,
|
|
178
|
+
columns,
|
|
179
|
+
data,
|
|
180
|
+
// The hierarchy the ticket asks for, in one option. Everything else about depth — the indent,
|
|
181
|
+
// the chevron, which rows exist at all — follows from it.
|
|
182
|
+
getSubRows: (row: Row) => row.subRows,
|
|
183
|
+
getRowId: (row: Row) => row.task.id,
|
|
184
|
+
state: { sorting, columnVisibility: hidden, grouping, expanded },
|
|
185
|
+
onSortingChange: setSorting,
|
|
186
|
+
onColumnVisibilityChange: (next) => onHidden(typeof next === "function" ? next(hidden) : next),
|
|
187
|
+
onGroupingChange: setGrouping,
|
|
188
|
+
onExpandedChange: setExpanded,
|
|
189
|
+
/**
|
|
190
|
+
* ⚠️ **Off, and it has to be off.** TanStack resets the expanded state whenever the row
|
|
191
|
+
* structure changes, and on a board that happens constantly: every card edit, every drag and
|
|
192
|
+
* every archive flip hands `data` a new array. With the reset on, opening an epic and then
|
|
193
|
+
* renaming anything on the board closes it again — and switching grouping on collapsed every
|
|
194
|
+
* group the moment it appeared.
|
|
195
|
+
*/
|
|
196
|
+
autoResetExpanded: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (board.isPending) {
|
|
200
|
+
return <p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
|
|
201
|
+
}
|
|
202
|
+
if (board.isError) {
|
|
203
|
+
return (
|
|
204
|
+
<p role="alert" className="p-6 text-sm text-destructive">
|
|
205
|
+
{i18n.t("board.failed")}
|
|
206
|
+
</p>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const grouped = grouping[0] ?? "none";
|
|
211
|
+
const groupable = table.getAllLeafColumns().filter((column) => column.getCanGroup());
|
|
212
|
+
const sortable = table.getAllLeafColumns().filter((column) => column.getCanSort());
|
|
213
|
+
const configured: BoardColumn[] = board.columns
|
|
214
|
+
.filter((entry) => entry.unknown !== true)
|
|
215
|
+
.map((entry) => entry.column);
|
|
216
|
+
const rows = table.getRowModel().rows;
|
|
217
|
+
// Which chips a row may draw. The column chooser therefore still decides what is on screen, it
|
|
218
|
+
// just decides it per chip instead of per column.
|
|
219
|
+
const visible = new Set(table.getVisibleLeafColumns().map((column) => column.id));
|
|
220
|
+
// ⚠️ Measured on the ANSWER, not on the rows on screen. The filter travels to the server, so an
|
|
221
|
+
// empty answer under a filter means "nothing matches" while an empty answer without one means
|
|
222
|
+
// "this board has no cards" — two different sentences for the same zero.
|
|
223
|
+
const empty = data.length === 0;
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div className="flex min-h-0 flex-1 flex-col gap-3 p-4" aria-busy={board.isWriting}>
|
|
227
|
+
{/* ⚠️ Icons only — what each one offers is in its popup. But the ACTIVE state has to stay
|
|
228
|
+
readable without opening anything: the icon fills, and the quiet line beside it names what
|
|
229
|
+
is set. An icon trio that says nothing about its own state forces a click to find out. */}
|
|
230
|
+
<div className="flex flex-wrap items-center gap-1 text-sm">
|
|
231
|
+
<DropdownMenu>
|
|
232
|
+
<DropdownMenuTrigger
|
|
233
|
+
aria-label={i18n.t("board.table.columns")}
|
|
234
|
+
className="inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-expanded:bg-muted"
|
|
235
|
+
>
|
|
236
|
+
<Columns3 aria-hidden="true" className="size-4" />
|
|
237
|
+
</DropdownMenuTrigger>
|
|
238
|
+
<DropdownMenuContent align="start">
|
|
239
|
+
{table.getAllLeafColumns().map((column) => (
|
|
240
|
+
<DropdownMenuCheckboxItem
|
|
241
|
+
key={column.id}
|
|
242
|
+
checked={column.getIsVisible()}
|
|
243
|
+
onCheckedChange={(next) => column.toggleVisibility(next)}
|
|
244
|
+
>
|
|
245
|
+
{String(column.columnDef.header)}
|
|
246
|
+
</DropdownMenuCheckboxItem>
|
|
247
|
+
))}
|
|
248
|
+
</DropdownMenuContent>
|
|
249
|
+
</DropdownMenu>
|
|
250
|
+
|
|
251
|
+
<DropdownMenu>
|
|
252
|
+
<DropdownMenuTrigger
|
|
253
|
+
aria-label={i18n.t("board.groupBy")}
|
|
254
|
+
className={`inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-expanded:bg-muted ${grouped === "none" ? "" : "bg-accent text-accent-foreground"}`}
|
|
255
|
+
>
|
|
256
|
+
<LayoutList aria-hidden="true" className="size-4" />
|
|
257
|
+
</DropdownMenuTrigger>
|
|
258
|
+
<DropdownMenuContent align="start">
|
|
259
|
+
<DropdownMenuRadioGroup
|
|
260
|
+
value={grouped}
|
|
261
|
+
onValueChange={(next) => setGrouping(next === "none" ? [] : [next])}
|
|
262
|
+
>
|
|
263
|
+
<DropdownMenuRadioItem value="none">
|
|
264
|
+
{i18n.t("board.group.none")}
|
|
265
|
+
</DropdownMenuRadioItem>
|
|
266
|
+
{/* ⚠️ Read off the COLUMNS, not a list written here. A hand-kept list is a second
|
|
267
|
+
truth about which columns can group: add a column and the menu stays silent about
|
|
268
|
+
it, and nothing fails — `enableGrouping` on the column itself is the one answer. */}
|
|
269
|
+
{groupable.map((column) => (
|
|
270
|
+
<DropdownMenuRadioItem key={column.id} value={column.id}>
|
|
271
|
+
{String(column.columnDef.header)}
|
|
272
|
+
</DropdownMenuRadioItem>
|
|
273
|
+
))}
|
|
274
|
+
</DropdownMenuRadioGroup>
|
|
275
|
+
</DropdownMenuContent>
|
|
276
|
+
</DropdownMenu>
|
|
277
|
+
|
|
278
|
+
<DropdownMenu>
|
|
279
|
+
<DropdownMenuTrigger
|
|
280
|
+
aria-label={i18n.t("board.filter.status")}
|
|
281
|
+
className={`inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-expanded:bg-muted ${filter.status === undefined ? "" : "bg-accent text-accent-foreground"}`}
|
|
282
|
+
>
|
|
283
|
+
<Filter aria-hidden="true" className="size-4" />
|
|
284
|
+
</DropdownMenuTrigger>
|
|
285
|
+
{/* ⚠️ This one TRAVELS. `useBoard` carries it in the query key and sends it, so the server
|
|
286
|
+
answers with the cards that match — a view that fetched everything and hid rows in the
|
|
287
|
+
browser would look identical on screen and be a different thing over the wire. */}
|
|
288
|
+
<DropdownMenuContent align="start">
|
|
289
|
+
<DropdownMenuRadioGroup
|
|
290
|
+
value={filter.status ?? ""}
|
|
291
|
+
onValueChange={(next) =>
|
|
292
|
+
onFilter(
|
|
293
|
+
next === "" ? { ...filter, status: undefined } : { ...filter, status: next },
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
>
|
|
297
|
+
<DropdownMenuRadioItem value="">{i18n.t("board.filter.all")}</DropdownMenuRadioItem>
|
|
298
|
+
{configured.map((column) => (
|
|
299
|
+
<DropdownMenuRadioItem key={column.id} value={column.id}>
|
|
300
|
+
{column.title}
|
|
301
|
+
</DropdownMenuRadioItem>
|
|
302
|
+
))}
|
|
303
|
+
</DropdownMenuRadioGroup>
|
|
304
|
+
</DropdownMenuContent>
|
|
305
|
+
</DropdownMenu>
|
|
306
|
+
|
|
307
|
+
<DropdownMenu>
|
|
308
|
+
{/* ⚠️ **Sorting needs its own place now.** It used to live on the column headings, and
|
|
309
|
+
those are gone: one heading is left, and hanging four sort orders off it would be a
|
|
310
|
+
control nobody finds. The toolbar is where the other three view settings already are. */}
|
|
311
|
+
<DropdownMenuTrigger
|
|
312
|
+
aria-label={i18n.t("board.sortBy")}
|
|
313
|
+
className={`inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-expanded:bg-muted ${sorting.length === 0 ? "" : "bg-accent text-accent-foreground"}`}
|
|
314
|
+
>
|
|
315
|
+
<ArrowUpDown aria-hidden="true" className="size-4" />
|
|
316
|
+
</DropdownMenuTrigger>
|
|
317
|
+
<DropdownMenuContent align="start">
|
|
318
|
+
<DropdownMenuRadioGroup
|
|
319
|
+
value={
|
|
320
|
+
sorting[0] === undefined
|
|
321
|
+
? ""
|
|
322
|
+
: `${sorting[0].id}:${sorting[0].desc ? "desc" : "asc"}`
|
|
323
|
+
}
|
|
324
|
+
onValueChange={(next) =>
|
|
325
|
+
setSorting(
|
|
326
|
+
next === ""
|
|
327
|
+
? []
|
|
328
|
+
: [{ id: next.split(":")[0] ?? "", desc: next.endsWith(":desc") }],
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
>
|
|
332
|
+
<DropdownMenuRadioItem value="">{i18n.t("board.sort.none")}</DropdownMenuRadioItem>
|
|
333
|
+
{/* ⚠️ Read off the COLUMNS like the grouping menu, not from a list written here: a
|
|
334
|
+
column that stops being sortable disappears from the menu on its own. */}
|
|
335
|
+
{sortable.flatMap((column) => [
|
|
336
|
+
<DropdownMenuRadioItem key={`${column.id}:asc`} value={`${column.id}:asc`}>
|
|
337
|
+
{i18n.t("board.sort.asc", { column: String(column.columnDef.header) })}
|
|
338
|
+
</DropdownMenuRadioItem>,
|
|
339
|
+
<DropdownMenuRadioItem key={`${column.id}:desc`} value={`${column.id}:desc`}>
|
|
340
|
+
{i18n.t("board.sort.desc", { column: String(column.columnDef.header) })}
|
|
341
|
+
</DropdownMenuRadioItem>,
|
|
342
|
+
])}
|
|
343
|
+
</DropdownMenuRadioGroup>
|
|
344
|
+
</DropdownMenuContent>
|
|
345
|
+
</DropdownMenu>
|
|
346
|
+
|
|
347
|
+
<span className="ml-2 text-xs text-muted-foreground">
|
|
348
|
+
{[
|
|
349
|
+
sorting[0] === undefined
|
|
350
|
+
? null
|
|
351
|
+
: i18n.t(sorting[0].desc ? "board.sort.desc" : "board.sort.asc", {
|
|
352
|
+
column: String(
|
|
353
|
+
sortable.find((column) => column.id === sorting[0]?.id)?.columnDef.header ??
|
|
354
|
+
sorting[0].id,
|
|
355
|
+
),
|
|
356
|
+
}),
|
|
357
|
+
grouped === "none"
|
|
358
|
+
? null
|
|
359
|
+
: i18n.t("board.table.groupedBy", {
|
|
360
|
+
column: String(
|
|
361
|
+
groupable.find((column) => column.id === grouped)?.columnDef.header ?? grouped,
|
|
362
|
+
),
|
|
363
|
+
}),
|
|
364
|
+
filter.status === undefined
|
|
365
|
+
? null
|
|
366
|
+
: i18n.t("board.table.filteredBy", {
|
|
367
|
+
column: configured.find((c) => c.id === filter.status)?.title ?? filter.status,
|
|
368
|
+
}),
|
|
369
|
+
]
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join(" · ")}
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
376
|
+
<table className="w-full border-collapse text-left text-sm">
|
|
377
|
+
{/* ⚠️ ONE heading. The other five said what their column was called and nothing about the
|
|
378
|
+
row; a card without a due date filled a cell with emptiness, and a table half made of
|
|
379
|
+
emptiness is one people stop reading. What a value IS, its chip says. */}
|
|
380
|
+
<thead>
|
|
381
|
+
<tr className="border-b">
|
|
382
|
+
{/* ⚠️ `aria-sort` survives the loss of the other headings. It is the only place the
|
|
383
|
+
TABLE itself says it is ordered; the quiet line in the toolbar says it to whoever
|
|
384
|
+
reads the toolbar, and a screen reader announcing a table does not. It reads
|
|
385
|
+
`none` while another column orders the rows, because that is what is true of this
|
|
386
|
+
one. */}
|
|
387
|
+
<th
|
|
388
|
+
scope="col"
|
|
389
|
+
aria-sort={
|
|
390
|
+
sorting[0]?.id !== "title" ? "none" : sorting[0].desc ? "descending" : "ascending"
|
|
391
|
+
}
|
|
392
|
+
className="px-2 py-3 font-medium"
|
|
393
|
+
>
|
|
394
|
+
{i18n.t("board.column.title")}
|
|
395
|
+
</th>
|
|
396
|
+
</tr>
|
|
397
|
+
</thead>
|
|
398
|
+
{/* ⚠️ One `tbody` PER GROUP, and the heading scopes to it. That is what actually
|
|
399
|
+
associates a heading with the rows under it: `scope="row"` binds to the heading's own
|
|
400
|
+
row and says nothing about the ones below, so a single `tbody` with headings sprinkled
|
|
401
|
+
through it reads to a screen reader as one long list with odd rows in it. */}
|
|
402
|
+
{groupsOf(rows).map((group) => (
|
|
403
|
+
<tbody key={group.key}>
|
|
404
|
+
{group.heading === undefined ? null : <GroupRow row={group.heading} span={1} />}
|
|
405
|
+
{group.rows.map((row) => (
|
|
406
|
+
<tr
|
|
407
|
+
key={row.id}
|
|
408
|
+
// ⚠️ A very light grey, and on the whole row. It says which row the pointer is on,
|
|
409
|
+
// which is the only thing a table this wide cannot say by itself.
|
|
410
|
+
className="border-b last:border-0 hover:bg-muted/40"
|
|
411
|
+
>
|
|
412
|
+
<td className="px-2 py-3">
|
|
413
|
+
<RowContent
|
|
414
|
+
row={row}
|
|
415
|
+
visible={visible}
|
|
416
|
+
onOpen={onOpen}
|
|
417
|
+
board={board}
|
|
418
|
+
columns={configured}
|
|
419
|
+
/>
|
|
420
|
+
</td>
|
|
421
|
+
</tr>
|
|
422
|
+
))}
|
|
423
|
+
</tbody>
|
|
424
|
+
))}
|
|
425
|
+
</table>
|
|
426
|
+
{empty ? (
|
|
427
|
+
<p className="p-6 text-sm text-muted-foreground">
|
|
428
|
+
{i18n.t(isFiltering(filter) ? "board.filterEmpty" : "board.tableEmpty")}
|
|
429
|
+
</p>
|
|
430
|
+
) : null}
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The rows split into groups, each with the heading that belongs to it.
|
|
438
|
+
*
|
|
439
|
+
* With no grouping there is exactly one group and no heading, so the view keeps ONE code path
|
|
440
|
+
* rather than a branch that only the grouped case exercises.
|
|
441
|
+
*/
|
|
442
|
+
function groupsOf(rows: TableRow[]): { key: string; heading?: TableRow; rows: TableRow[] }[] {
|
|
443
|
+
const groups: { key: string; heading?: TableRow; rows: TableRow[] }[] = [];
|
|
444
|
+
for (const row of rows) {
|
|
445
|
+
if (row.getIsGrouped()) groups.push({ key: row.id, heading: row, rows: [] });
|
|
446
|
+
else if (groups.length === 0) groups.push({ key: "all", rows: [row] });
|
|
447
|
+
else groups[groups.length - 1]?.rows.push(row);
|
|
448
|
+
}
|
|
449
|
+
return groups;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The heading of one group.
|
|
454
|
+
*
|
|
455
|
+
* ⚠️ A component rather than a branch in the loop, because the label needs a hook: grouping by who
|
|
456
|
+
* a card belongs to groups by the raw Gate id, and that id must not reach the screen — nor an
|
|
457
|
+
* `aria-label`, which is screen for everybody using one (`user-name.ts`, #258).
|
|
458
|
+
*/
|
|
459
|
+
function GroupRow({ row, span }: { row: TableRow; span: number }) {
|
|
460
|
+
const i18n = useI18n();
|
|
461
|
+
const column = row.groupingColumnId;
|
|
462
|
+
const value = column === undefined ? undefined : row.getGroupingValue(column);
|
|
463
|
+
const assignee = column === "assignee";
|
|
464
|
+
const resolved = useAssigneeLabel(assignee ? ((value as string | null) ?? null) : null);
|
|
465
|
+
const label = assignee
|
|
466
|
+
? resolved
|
|
467
|
+
: value === undefined || value === null || value === ""
|
|
468
|
+
? i18n.t("board.group.empty")
|
|
469
|
+
: String(value);
|
|
470
|
+
// ⚠️ Every row of the group, not `row.subRows`. A grouped row's `subRows` holds the TOP level
|
|
471
|
+
// only, so an epic with three subtasks under it counts as one — a heading that says `2` above
|
|
472
|
+
// four visible rows.
|
|
473
|
+
const count = row.getLeafRows().length;
|
|
474
|
+
|
|
475
|
+
return (
|
|
476
|
+
<tr className="border-b bg-muted/40">
|
|
477
|
+
<th scope="rowgroup" colSpan={span} className="px-2 py-2 text-left font-medium">
|
|
478
|
+
<button
|
|
479
|
+
type="button"
|
|
480
|
+
onClick={row.getToggleExpandedHandler()}
|
|
481
|
+
aria-label={i18n.t(row.getIsExpanded() ? "board.table.collapse" : "board.table.expand", {
|
|
482
|
+
row: label,
|
|
483
|
+
})}
|
|
484
|
+
className="inline-flex items-center gap-1 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
485
|
+
>
|
|
486
|
+
{row.getIsExpanded() ? (
|
|
487
|
+
<ChevronDown aria-hidden="true" className="size-4" />
|
|
488
|
+
) : (
|
|
489
|
+
<ChevronRight aria-hidden="true" className="size-4" />
|
|
490
|
+
)}
|
|
491
|
+
{label}
|
|
492
|
+
<span className="text-xs text-muted-foreground tabular-nums">{count}</span>
|
|
493
|
+
</button>
|
|
494
|
+
</th>
|
|
495
|
+
</tr>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* One row: the title, and beside it only the values that are set.
|
|
501
|
+
*
|
|
502
|
+
* ⚠️ **Always the same order** — column, dates, labels, what it waits for, then who it is for. The
|
|
503
|
+
* rows carry different numbers of chips, so without a fixed order the eye has no line to follow and
|
|
504
|
+
* every row has to be read from the start.
|
|
505
|
+
*
|
|
506
|
+
* ⚠️ **The circle is last, always.** It is the only round thing in the row; between the chips it
|
|
507
|
+
* breaks the line, and a row nobody is on simply ends earlier.
|
|
508
|
+
*
|
|
509
|
+
* ⚠️ **A chip is drawn only when its column is visible AND its value is set.** The column chooser
|
|
510
|
+
* therefore still works, it just hides a chip rather than an empty cell.
|
|
511
|
+
*/
|
|
512
|
+
function RowContent({
|
|
513
|
+
row,
|
|
514
|
+
visible,
|
|
515
|
+
onOpen,
|
|
516
|
+
board,
|
|
517
|
+
columns,
|
|
518
|
+
}: {
|
|
519
|
+
row: TableRow;
|
|
520
|
+
visible: Set<string>;
|
|
521
|
+
onOpen(taskId: string): void;
|
|
522
|
+
board: BoardHandle;
|
|
523
|
+
columns: BoardColumn[];
|
|
524
|
+
}) {
|
|
525
|
+
const i18n = useI18n();
|
|
526
|
+
const { task, columnTitle, blocking } = row.original;
|
|
527
|
+
|
|
528
|
+
const write = (input: Partial<Omit<Parameters<BoardHandle["updateTask"]>[0], "taskId">>) => {
|
|
529
|
+
// ⚠️ Refused while a write is in flight, like every other surface of this board (#651).
|
|
530
|
+
if (board.isWriting) return;
|
|
531
|
+
void board.updateTask({ taskId: task.id, ...input, idempotencyKey: crypto.randomUUID() });
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<span className="flex items-center gap-2" style={{ paddingLeft: `${row.depth * 20}px` }}>
|
|
536
|
+
{row.getCanExpand() ? (
|
|
537
|
+
<button
|
|
538
|
+
type="button"
|
|
539
|
+
aria-label={i18n.t(row.getIsExpanded() ? "board.table.collapse" : "board.table.expand", {
|
|
540
|
+
row: task.title,
|
|
541
|
+
})}
|
|
542
|
+
onClick={row.getToggleExpandedHandler()}
|
|
543
|
+
className="rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
544
|
+
>
|
|
545
|
+
{row.getIsExpanded() ? (
|
|
546
|
+
<ChevronDown aria-hidden="true" className="size-4 text-muted-foreground" />
|
|
547
|
+
) : (
|
|
548
|
+
<ChevronRight aria-hidden="true" className="size-4 text-muted-foreground" />
|
|
549
|
+
)}
|
|
550
|
+
</button>
|
|
551
|
+
) : (
|
|
552
|
+
<span className="size-4" />
|
|
553
|
+
)}
|
|
554
|
+
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
onClick={() => onOpen(task.id)}
|
|
558
|
+
className="min-w-0 flex-1 truncate rounded-sm text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
559
|
+
>
|
|
560
|
+
{task.title}
|
|
561
|
+
</button>
|
|
562
|
+
|
|
563
|
+
{visible.has("status") ? (
|
|
564
|
+
<StatusChip status={task.status} title={columnTitle} columns={columns} write={write} />
|
|
565
|
+
) : null}
|
|
566
|
+
|
|
567
|
+
{visible.has("start") && task.startDate !== null ? (
|
|
568
|
+
<BoardChip
|
|
569
|
+
label={i18n.t("board.fromDate", { date: task.startDate.slice(0, 10) })}
|
|
570
|
+
action={i18n.t("board.clearStart")}
|
|
571
|
+
onClick={() => write({ startDate: null })}
|
|
572
|
+
/>
|
|
573
|
+
) : null}
|
|
574
|
+
{visible.has("due") && task.dueDate !== null ? (
|
|
575
|
+
<BoardChip
|
|
576
|
+
label={i18n.t("board.untilDate", { date: task.dueDate.slice(0, 10) })}
|
|
577
|
+
action={i18n.t("board.clearDue")}
|
|
578
|
+
onClick={() => write({ dueDate: null })}
|
|
579
|
+
/>
|
|
580
|
+
) : null}
|
|
581
|
+
|
|
582
|
+
{visible.has("labels")
|
|
583
|
+
? task.labels.map((label) => (
|
|
584
|
+
<BoardChip
|
|
585
|
+
key={label}
|
|
586
|
+
label={label}
|
|
587
|
+
action={i18n.t("board.removeLabel")}
|
|
588
|
+
onClick={() => write({ labels: task.labels.filter((keep) => keep !== label) })}
|
|
589
|
+
/>
|
|
590
|
+
))
|
|
591
|
+
: null}
|
|
592
|
+
|
|
593
|
+
{visible.has("dependsOn") && blocking !== null ? (
|
|
594
|
+
<BoardChip
|
|
595
|
+
label={blocking}
|
|
596
|
+
action={i18n.t("board.clearDependsOn")}
|
|
597
|
+
onClick={() => write({ dependsOn: null })}
|
|
598
|
+
/>
|
|
599
|
+
) : null}
|
|
600
|
+
|
|
601
|
+
{visible.has("assignee") && task.assigneeId !== null ? (
|
|
602
|
+
<AssigneeChip
|
|
603
|
+
id={task.assigneeId}
|
|
604
|
+
boardId={board.boardId}
|
|
605
|
+
onPick={(assigneeId) => write({ assigneeId })}
|
|
606
|
+
/>
|
|
607
|
+
) : null}
|
|
608
|
+
</span>
|
|
609
|
+
);
|
|
610
|
+
}
|