@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,107 @@
|
|
|
1
|
+
import type { Node } from "@contract/node.ts";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
4
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
|
+
import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A table as a grid (#40).
|
|
10
|
+
*
|
|
11
|
+
* Reading is the whole of the first cut: there is no cell editor, and rows arrive through
|
|
12
|
+
* `node_table_row_create` — the point of the kind is that a flow or an MCP client fills it
|
|
13
|
+
* without a person retyping anything. Editing in the grid is anchrd/intel#165.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ The rows are the server's answer, parsed by the server's one CSV reader. A second reader here
|
|
16
|
+
* would eventually disagree with it about a quoted comma, and the grid would then show something
|
|
17
|
+
* the export does not contain.
|
|
18
|
+
*/
|
|
19
|
+
export function NodeTablePanel({ node }: { node: Node }) {
|
|
20
|
+
const { data } = useIntelRouterContext();
|
|
21
|
+
const i18n = useI18n();
|
|
22
|
+
const table = useQuery({
|
|
23
|
+
queryKey: ["node-table", node.id],
|
|
24
|
+
queryFn: () => data.getNodeTable(node.id),
|
|
25
|
+
});
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
28
|
+
{/* ⚠️ This used to be a second bar of its own, directly under the title line (#54): two
|
|
29
|
+
headers stacked, and the table starting a whole row lower for it. A table is one node kind
|
|
30
|
+
among four, not a screen with its own chrome — so the count goes beside the title as a
|
|
31
|
+
quiet word. It is rendered from here rather than from the screen because this is where the
|
|
32
|
+
query lives; the screen would otherwise have to load a table it does not show.
|
|
33
|
+
|
|
34
|
+
⚠️ The CSV export used to stand here too, as a button WITH TEXT beside two icon buttons.
|
|
35
|
+
#454 moved it into the title line's three-dot menu, and #532 dropped it there: the bundle
|
|
36
|
+
export beside it already writes the table into the zip as `<title>.csv` (`bundle.ts`), so
|
|
37
|
+
the two entries handed out the same bytes. Taking a table home is `Export`, here and
|
|
38
|
+
nowhere else. */}
|
|
39
|
+
<ActionSlot name="title-meta">
|
|
40
|
+
{table.data
|
|
41
|
+
? i18n.t("node.table.summary", {
|
|
42
|
+
rows: table.data.rows.length,
|
|
43
|
+
columns: table.data.columns.length,
|
|
44
|
+
})
|
|
45
|
+
: ""}
|
|
46
|
+
</ActionSlot>
|
|
47
|
+
{table.isPending ? (
|
|
48
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
49
|
+
) : table.isError ? (
|
|
50
|
+
<div role="alert" className="space-y-3 p-6 text-sm">
|
|
51
|
+
<p className="text-destructive">{i18n.t("node.loadFailed")}</p>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => void table.refetch()}
|
|
55
|
+
className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
56
|
+
>
|
|
57
|
+
{i18n.t("common.retry")}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
) : table.data.columns.length === 0 ? (
|
|
61
|
+
<p className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
|
|
62
|
+
{i18n.t("node.table.undefined")}
|
|
63
|
+
</p>
|
|
64
|
+
) : (
|
|
65
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-auto">
|
|
66
|
+
<table className="w-full border-collapse text-sm">
|
|
67
|
+
<caption className="sr-only">{node.title}</caption>
|
|
68
|
+
<thead className="sticky top-0 bg-card">
|
|
69
|
+
<tr>
|
|
70
|
+
{table.data.columns.map((column) => (
|
|
71
|
+
// Column names are distinct on every path that writes a table — `DefineTableInput`,
|
|
72
|
+
// `RedefineTableInput` and, since #322, the bundle import, which used to walk past
|
|
73
|
+
// both — so the name is the key and nothing has to fall back to a position.
|
|
74
|
+
<th
|
|
75
|
+
key={column}
|
|
76
|
+
scope="col"
|
|
77
|
+
className="border-b border-r px-3 py-2 text-left font-medium whitespace-nowrap"
|
|
78
|
+
>
|
|
79
|
+
{column}
|
|
80
|
+
</th>
|
|
81
|
+
))}
|
|
82
|
+
</tr>
|
|
83
|
+
</thead>
|
|
84
|
+
<tbody>
|
|
85
|
+
{table.data.rows.map((row, rowIndex) => (
|
|
86
|
+
// ⚠️ Rows carry no identity of their own. A table is an append-only list of text
|
|
87
|
+
// and its position is the only thing that names a row — there is no id to key on,
|
|
88
|
+
// and inventing one would be inventing data.
|
|
89
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: an appended row has no other key
|
|
90
|
+
<tr key={rowIndex} className="even:bg-muted/40">
|
|
91
|
+
{table.data.columns.map((column, cellIndex) => (
|
|
92
|
+
<td key={column} className="border-b border-r px-3 py-2 align-top">
|
|
93
|
+
{row[cellIndex] ?? ""}
|
|
94
|
+
</td>
|
|
95
|
+
))}
|
|
96
|
+
</tr>
|
|
97
|
+
))}
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
{table.data.rows.length === 0 ? (
|
|
101
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("node.table.empty")}</p>
|
|
102
|
+
) : null}
|
|
103
|
+
</TitleRowScrollArea>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { Node } from "@contract/node.ts";
|
|
2
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
4
|
+
import { lazy, Suspense } from "react";
|
|
5
|
+
import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
|
|
6
|
+
import { ViewMenu } from "@/app/view-menu/view-menu.tsx";
|
|
7
|
+
import { BoardPanel } from "@/board/board-panel/board-panel.tsx";
|
|
8
|
+
import { BoardTitleRow } from "@/board/board-title-row/board-title-row.tsx";
|
|
9
|
+
import { RefusalNotice } from "@/data/request-refusal/refusal-notice.tsx";
|
|
10
|
+
import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
|
|
11
|
+
import { FolderContents } from "@/folder-contents/folder-contents.tsx";
|
|
12
|
+
import { GraphPane } from "@/graph-pane/graph-pane.tsx";
|
|
13
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
14
|
+
import { NodeTablePanel } from "@/node-table/node-table.tsx";
|
|
15
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
16
|
+
import { graphViewFrom, selectedFrom, viewOf } from "@/router/selection-search.ts";
|
|
17
|
+
import { TitleRow, TitleRowFrame } from "@/title-row/title-row.tsx";
|
|
18
|
+
|
|
19
|
+
const NodeEditor = lazy(async () => ({
|
|
20
|
+
default: (await import("@/node-editor/node-editor.tsx")).NodeEditor,
|
|
21
|
+
}));
|
|
22
|
+
const NodeSource = lazy(async () => ({
|
|
23
|
+
default: (await import("@/node-source/node-source.tsx")).NodeSource,
|
|
24
|
+
}));
|
|
25
|
+
const AttachmentViewer = lazy(async () => ({
|
|
26
|
+
default: (await import("@/attachment-viewer/attachment-viewer.tsx")).AttachmentViewer,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A document is markdown, and since #820 it can be read and written as what it is. The editor
|
|
31
|
+
* stands first: it is the view a plain link means.
|
|
32
|
+
*/
|
|
33
|
+
const DocumentViews = ["editor", "code"] as const;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which kinds are a document, and therefore have a source at all.
|
|
37
|
+
*
|
|
38
|
+
* ⚠️ The same list the panel below falls through to: a folder draws its contents, an attachment its
|
|
39
|
+
* preview, a table and a board their own panel, and what is left is a text. A card is one too — it
|
|
40
|
+
* is not reached from this screen in the ordinary way, but a link straight to it lands here, and a
|
|
41
|
+
* switch that appeared for it without a view behind it would be worse than no switch.
|
|
42
|
+
*/
|
|
43
|
+
function writesMarkdown(kind: Node["kind"]): boolean {
|
|
44
|
+
return kind === "document" || kind === "task";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Nodes() {
|
|
48
|
+
const { data } = useIntelRouterContext();
|
|
49
|
+
const i18n = useI18n();
|
|
50
|
+
const queryClient = useQueryClient();
|
|
51
|
+
const navigate = useNavigate();
|
|
52
|
+
// The tree in the sidebar and the header search both name what to open through `?select=`, so this
|
|
53
|
+
// screen has no selection of its own to keep in step with them.
|
|
54
|
+
const selection = useRouterState({
|
|
55
|
+
select: (state) => ({
|
|
56
|
+
id: selectedFrom(state.location.search),
|
|
57
|
+
graph: graphViewFrom(state.location.search),
|
|
58
|
+
// Which of a document's two views is showing (#820). The editor is the first of them and
|
|
59
|
+
// therefore the absence of the parameter, so every link written before it keeps its meaning.
|
|
60
|
+
view: viewOf(state.location.search, DocumentViews),
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
const selectedId = selection.id;
|
|
64
|
+
const document = useQuery({
|
|
65
|
+
queryKey: ["node", selectedId],
|
|
66
|
+
queryFn: () => data.getNode(selectedId ?? ""),
|
|
67
|
+
enabled: Boolean(selectedId),
|
|
68
|
+
});
|
|
69
|
+
const selected = document.data?.node ?? null;
|
|
70
|
+
// ⚠️ Off the ERROR, not off the absence of data: a query that has not answered is not a query that
|
|
71
|
+
// answered "no" (`packages/intel/ui/CLAUDE.md`). While the request is in flight there is no refusal to
|
|
72
|
+
// report, and the loading line below stays.
|
|
73
|
+
const refusal = refusalOf(document.error);
|
|
74
|
+
/**
|
|
75
|
+
* The top level of the tree — the same query the sidebar already holds, by the same key and the
|
|
76
|
+
* same function, so this is a second reader of one cache entry rather than a second request.
|
|
77
|
+
*
|
|
78
|
+
* ⚠️ The screen needs a source of its own because the one above cannot answer here: the refusal
|
|
79
|
+
* branch hangs on `["node", selectedId]`, and without a selection that query is not enabled at
|
|
80
|
+
* all. The empty state was therefore reached BEFORE anything had been asked — and it invited the
|
|
81
|
+
* reader to select something, which is exactly what somebody without `intel.nodes.read` cannot
|
|
82
|
+
* do. The sidebar beside it has said so since #430; two surfaces, two answers (#445).
|
|
83
|
+
*/
|
|
84
|
+
const rootLevel = useQuery({
|
|
85
|
+
queryKey: treeLevelKey(null),
|
|
86
|
+
queryFn: () => data.listTreeChildren(null),
|
|
87
|
+
enabled: selectedId === null,
|
|
88
|
+
});
|
|
89
|
+
// ⚠️ Off the ERROR, and here that is half the ticket rather than a detail. A sentence about a
|
|
90
|
+
// missing permission, shown while the answer is still travelling, claims a refusal nobody has
|
|
91
|
+
// spoken yet — the mistake from #350, and `packages/intel/ui/CLAUDE.md` has the rule: a missing answer
|
|
92
|
+
// is a reason to say less, never to refuse more. So the negative sentence hangs off the refusal;
|
|
93
|
+
// until one arrives, what stands is the invitation, which claims nothing about permission.
|
|
94
|
+
const rootRefusal = refusalOf(rootLevel.error);
|
|
95
|
+
// A folder shows its contents as a graph, and so does the root of the tree (#19). A document has
|
|
96
|
+
// nothing to draw — which is why the switch is not offered on one rather than offered and empty.
|
|
97
|
+
const graphable = selectedId === null || selected?.kind === "folder";
|
|
98
|
+
const relations = useQuery({
|
|
99
|
+
queryKey: ["relation-graph", "folder", selectedId],
|
|
100
|
+
queryFn: () => data.getRelationGraph({ of: "folder", folderId: selectedId }),
|
|
101
|
+
enabled: graphable && selection.graph,
|
|
102
|
+
});
|
|
103
|
+
return (
|
|
104
|
+
<TitleRowFrame className="flex h-full min-h-0 flex-col">
|
|
105
|
+
<section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
|
|
106
|
+
{/* ⚠️ Only the root draws its graph on its own. A folder's graph hangs *under* the folder's
|
|
107
|
+
title line rather than in place of it (#58): the line is the one place its menu lives,
|
|
108
|
+
and a folder that could only be renamed while the graph happened to be switched off
|
|
109
|
+
would be a folder one cannot rename. */}
|
|
110
|
+
{/* ⚠️ #454 took the view switch out of the GLOBAL header — there it applied to the whole
|
|
111
|
+
application although it is a view onto the selected item. For an item it now stands in
|
|
112
|
+
that item's title line.
|
|
113
|
+
|
|
114
|
+
⚠️ The root has no title line, and that is exactly why it gets a row of its own here.
|
|
115
|
+
Without it the graph of the WHOLE tree would have been unreachable after the move — the
|
|
116
|
+
switch was its only way in and out. Moving a control must not abolish a view; that would
|
|
117
|
+
be a decision the ticket did not contain. */}
|
|
118
|
+
{selectedId === null ? (
|
|
119
|
+
<div className="flex shrink-0 items-center justify-end gap-2 border-b px-6 py-2">
|
|
120
|
+
<ViewMenu />
|
|
121
|
+
</div>
|
|
122
|
+
) : null}
|
|
123
|
+
{selectedId === null && selection.graph ? (
|
|
124
|
+
<GraphPane
|
|
125
|
+
query={relations}
|
|
126
|
+
select={(node) =>
|
|
127
|
+
void navigate({
|
|
128
|
+
to: node.kind === "flow" ? "/flows" : "/nodes",
|
|
129
|
+
search: { select: node.id },
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
133
|
+
) : !selectedId ? (
|
|
134
|
+
rootRefusal ? (
|
|
135
|
+
<RefusalNotice refusal={rootRefusal} />
|
|
136
|
+
) : (
|
|
137
|
+
<div className="grid flex-1 place-items-center p-8 text-center text-sm text-muted-foreground">
|
|
138
|
+
{i18n.t("node.select")}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
) : document.isPending ? (
|
|
142
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
143
|
+
) : refusal ? (
|
|
144
|
+
<RefusalNotice refusal={refusal} />
|
|
145
|
+
) : document.isError || !selected ? (
|
|
146
|
+
<div role="alert" className="grid flex-1 place-items-center p-8 text-center text-sm">
|
|
147
|
+
<div className="space-y-3">
|
|
148
|
+
<p className="text-destructive">{i18n.t("node.loadFailed")}</p>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
onClick={() => void document.refetch()}
|
|
152
|
+
className="rounded-md border px-3 py-2 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
153
|
+
>
|
|
154
|
+
{i18n.t("common.retry")}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<>
|
|
160
|
+
{/* The document's own line: its name, and beside it only what acts on this one
|
|
161
|
+
document. What the four loose icons used to do is now one menu (#24) — and since #58
|
|
162
|
+
this line is the only place that menu appears at all, for every kind including a
|
|
163
|
+
folder, whose whole screen is this line.
|
|
164
|
+
⚠️ #454: the view switch now stands HERE. It used to stand in the global header,
|
|
165
|
+
beside the search — where what applies everywhere stands. But a view is a view onto
|
|
166
|
+
THIS item, and each switch is drawn only where its views exist: the graph where
|
|
167
|
+
there is a graph, the source where there is markdown. A kind with one view gets
|
|
168
|
+
none, or the menu would promise a choice it does not have (#818). */}
|
|
169
|
+
{/* ⚠️ A board brings its OWN title row, because an open card changes what the line
|
|
170
|
+
identifies: not the board any more, but where the card sits on it. It is the same
|
|
171
|
+
`TitleRow` underneath, only with a path in place of the name (#692). */}
|
|
172
|
+
{selected.kind === "board" ? (
|
|
173
|
+
<BoardTitleRow node={selected} />
|
|
174
|
+
) : (
|
|
175
|
+
<TitleRow title={selected.title} target={{ type: "node", node: selected }}>
|
|
176
|
+
{graphable ? <ViewMenu /> : null}
|
|
177
|
+
{/* ⚠️ A document has no graph and a folder has no source, so the two switches are
|
|
178
|
+
never both drawn — which is also why neither has to know about the other. */}
|
|
179
|
+
{writesMarkdown(selected.kind) ? <ViewMenu views={DocumentViews} /> : null}
|
|
180
|
+
</TitleRow>
|
|
181
|
+
)}
|
|
182
|
+
{graphable && selection.graph ? (
|
|
183
|
+
<GraphPane
|
|
184
|
+
query={relations}
|
|
185
|
+
select={(node) =>
|
|
186
|
+
void navigate({
|
|
187
|
+
to: node.kind === "flow" ? "/flows" : "/nodes",
|
|
188
|
+
search: { select: node.id },
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
/>
|
|
192
|
+
) : selected.kind === "folder" ? (
|
|
193
|
+
<FolderContents folderId={selected.id} />
|
|
194
|
+
) : selected.kind === "attachment" ? (
|
|
195
|
+
<Suspense
|
|
196
|
+
fallback={
|
|
197
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
198
|
+
}
|
|
199
|
+
>
|
|
200
|
+
<AttachmentViewer document={document.data} />
|
|
201
|
+
</Suspense>
|
|
202
|
+
) : selected.kind === "table" ? (
|
|
203
|
+
<NodeTablePanel node={selected} />
|
|
204
|
+
) : selected.kind === "board" ? (
|
|
205
|
+
<BoardPanel node={selected} />
|
|
206
|
+
) : document.data ? (
|
|
207
|
+
<Suspense
|
|
208
|
+
fallback={
|
|
209
|
+
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
210
|
+
}
|
|
211
|
+
>
|
|
212
|
+
{selection.view === "code" ? (
|
|
213
|
+
<NodeSource
|
|
214
|
+
key={document.data.node.id}
|
|
215
|
+
data={data}
|
|
216
|
+
document={document.data}
|
|
217
|
+
i18n={i18n}
|
|
218
|
+
onSaved={(saved) => {
|
|
219
|
+
queryClient.setQueryData(["node", selected.id], saved);
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<NodeEditor
|
|
224
|
+
key={document.data.node.id}
|
|
225
|
+
data={data}
|
|
226
|
+
document={document.data}
|
|
227
|
+
i18n={i18n}
|
|
228
|
+
onSaved={(saved) => {
|
|
229
|
+
queryClient.setQueryData(["node", selected.id], saved);
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
)}
|
|
233
|
+
</Suspense>
|
|
234
|
+
) : null}
|
|
235
|
+
</>
|
|
236
|
+
)}
|
|
237
|
+
</section>
|
|
238
|
+
</TitleRowFrame>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Flow } from "@contract/flow.ts";
|
|
2
|
+
import type { Node } from "@contract/node.ts";
|
|
3
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { allTreeLevelsKey } from "@/app/tree-move/tree-move.tsx";
|
|
5
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
|
+
|
|
7
|
+
/** The two things that carry a title, and the two doors that change one. */
|
|
8
|
+
export type RenameTarget = { type: "node"; node: Node } | { type: "flow"; flow: Flow };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Everything a change to one resource can make stale (#823).
|
|
12
|
+
*
|
|
13
|
+
* The row sits in one level of the tree, the open screen reads the record, and the flat collections
|
|
14
|
+
* behind the search, the link picker and the relation view all carry the title as well — a rename
|
|
15
|
+
* that only reached the tree would leave yesterday's name standing on three other screens.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Every level rather than the one this row sits in: archiving the last row of a folder takes
|
|
18
|
+
* that folder's arrow away, and the arrow is drawn from the folder's own row one level higher
|
|
19
|
+
* (`allTreeLevelsKey`, #519). Renaming needs no more than the one level — but two invalidations
|
|
20
|
+
* that differ per entry are how one of them goes missing.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ It lives here rather than in either caller because there are now TWO ways to rename: the
|
|
23
|
+
* menu's dialog and the tree's own row (#823). A copy of this list is a second place for one rule
|
|
24
|
+
* to drift, and the direction it drifts in is silent — a name that stays right on the screen the
|
|
25
|
+
* renamer is looking at and wrong on the three they are not.
|
|
26
|
+
*/
|
|
27
|
+
export function useResourceRefresh(target: RenameTarget): () => Promise<void> {
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
const id = target.type === "flow" ? target.flow.id : target.node.id;
|
|
30
|
+
return async () => {
|
|
31
|
+
await Promise.all([
|
|
32
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
33
|
+
queryClient.invalidateQueries({
|
|
34
|
+
queryKey: target.type === "flow" ? ["flow", id] : ["node", id],
|
|
35
|
+
}),
|
|
36
|
+
queryClient.invalidateQueries({
|
|
37
|
+
queryKey: [target.type === "flow" ? "flows" : "node-graph"],
|
|
38
|
+
}),
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: ["relation-graph"] }),
|
|
40
|
+
]);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One new title, through the door the thing behind it has (#823).
|
|
46
|
+
*
|
|
47
|
+
* ⚠️ `baseUpdatedAt` is not optional decoration. Renaming takes the same conflict route as every
|
|
48
|
+
* other change, or two people renaming at once end with one of them silently overwritten — and the
|
|
49
|
+
* one who loses never learns that they did.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ A flow and a document are two doors, not one with a flag: `updateFlow` and `updateNode` write
|
|
52
|
+
* different tables and answer to different capabilities (ADR-0004 §1).
|
|
53
|
+
*/
|
|
54
|
+
export function useResourceRename(target: RenameTarget, onRenamed?: () => void) {
|
|
55
|
+
const { data } = useIntelRouterContext();
|
|
56
|
+
const refresh = useResourceRefresh(target);
|
|
57
|
+
return useMutation({
|
|
58
|
+
mutationFn: async (next: string) => {
|
|
59
|
+
if (target.type === "flow") {
|
|
60
|
+
await data.updateFlow({
|
|
61
|
+
flowId: target.flow.id,
|
|
62
|
+
baseUpdatedAt: target.flow.updatedAt,
|
|
63
|
+
title: next,
|
|
64
|
+
idempotencyKey: crypto.randomUUID(),
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await data.updateNode({
|
|
69
|
+
nodeId: target.node.id,
|
|
70
|
+
baseUpdatedAt: target.node.updatedAt,
|
|
71
|
+
title: next,
|
|
72
|
+
idempotencyKey: crypto.randomUUID(),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
onSuccess: async () => {
|
|
76
|
+
onRenamed?.();
|
|
77
|
+
await refresh();
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { createContext, type ReactNode, useContext, useMemo } from "react";
|
|
3
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The names behind a set of user ids a screen is currently showing (#700).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ **One request for the whole screen, not one per person.** A hook that resolved a single id
|
|
9
|
+
* would be called once per row and once per circle, and a board with forty cards would open forty
|
|
10
|
+
* requests for four distinct people. The ids are collected where they are already all in one
|
|
11
|
+
* place — the board view, the access summary — and asked for together.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **Not a board thing**, though a board was the first to need it: `directory_resolve` names ids
|
|
14
|
+
* the caller already holds, and any screen that draws people can wrap itself in this (#803). It
|
|
15
|
+
* lives outside `board/` for that reason — the name that suggested otherwise cost a duplicated
|
|
16
|
+
* door once already (#751).
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ **Absent is not an error.** An id that cannot be named is missing from the answer, and the
|
|
19
|
+
* map simply has no entry for it. Callers fall back to the same wording they used before names
|
|
20
|
+
* existed at all; nobody ever draws the raw id (#258).
|
|
21
|
+
*/
|
|
22
|
+
const ResolvedNames = createContext<ReadonlyMap<string, string> | null>(null);
|
|
23
|
+
|
|
24
|
+
export function ResolvedNamesProvider({
|
|
25
|
+
ids,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
ids: readonly (string | null)[];
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}) {
|
|
31
|
+
const { data } = useIntelRouterContext();
|
|
32
|
+
// Sorted and de-duplicated so that the same screen produces the same key twice running: an
|
|
33
|
+
// unstable key would refetch on every render that reorders its rows, which a drag does
|
|
34
|
+
// constantly.
|
|
35
|
+
const wanted = useMemo(
|
|
36
|
+
() => [...new Set(ids.filter((id): id is string => id !== null))].sort(),
|
|
37
|
+
[ids],
|
|
38
|
+
);
|
|
39
|
+
const query = useQuery({
|
|
40
|
+
queryKey: ["resolved-names", wanted],
|
|
41
|
+
// ⚠️ Not `enabled: wanted.length > 0` alone — the queryFn must never be reached with an empty
|
|
42
|
+
// list, because the contract refuses it (`min(1)`) and the refusal would surface as a broken
|
|
43
|
+
// board rather than as the empty answer it means.
|
|
44
|
+
enabled: wanted.length > 0,
|
|
45
|
+
queryFn: async () => await data.resolveNames(wanted),
|
|
46
|
+
staleTime: 5 * 60 * 1000,
|
|
47
|
+
});
|
|
48
|
+
const names = useMemo(() => {
|
|
49
|
+
const map = new Map<string, string>();
|
|
50
|
+
for (const person of query.data?.items ?? []) map.set(person.id, person.name);
|
|
51
|
+
return map;
|
|
52
|
+
}, [query.data]);
|
|
53
|
+
return <ResolvedNames.Provider value={names}>{children}</ResolvedNames.Provider>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The name behind one id, or `null` where this browser cannot say.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ Outside a provider this answers `null` for everybody, which is exactly the behaviour every
|
|
60
|
+
* caller already handles. A screen that forgets the provider shows what it showed before this
|
|
61
|
+
* existed; it does not break, and it does not leak an id.
|
|
62
|
+
*/
|
|
63
|
+
export function useResolvedName(id: string | null): string | null {
|
|
64
|
+
const names = useContext(ResolvedNames);
|
|
65
|
+
if (id === null) return null;
|
|
66
|
+
return names?.get(id) ?? null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which sentence a refused change to a resource is told with.
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ A conflict is the one that must not be swallowed: every write in the product names the state it
|
|
5
|
+
* was made against — `baseUpdatedAt` for a record, `baseVersionId` for a table — and turns a parallel
|
|
6
|
+
* edit into a refusal. A refusal nobody words is indistinguishable from a change that went through.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ Flat and on its own since #531, where the table's column dialog became the second reader. It
|
|
9
|
+
* used to live in `resource-menu.tsx`; importing it from there would have made the menu and the
|
|
10
|
+
* dialog import each other, and a cycle is a thing that works until the bundler splits differently.
|
|
11
|
+
*/
|
|
12
|
+
export function resourceErrorKey(error: unknown): string {
|
|
13
|
+
const code =
|
|
14
|
+
typeof error === "object" && error !== null && "code" in error
|
|
15
|
+
? String((error as { code: unknown }).code)
|
|
16
|
+
: null;
|
|
17
|
+
switch (code) {
|
|
18
|
+
// ⚠️ `version_conflict` is the table's word for the same event (`nodes.ts`, `tableStateFor`):
|
|
19
|
+
// the positions or the mapping were read from a version that is no longer the newest. It is the
|
|
20
|
+
// conflict sentence and not the general one — "reload it and try again" is exactly the way out,
|
|
21
|
+
// and letting it fall through to `resource.failed` would tell somebody to reload in the same
|
|
22
|
+
// breath as claiming nobody knows what happened.
|
|
23
|
+
case "version_conflict":
|
|
24
|
+
case "update_conflict":
|
|
25
|
+
case "flow_update_conflict":
|
|
26
|
+
return "resource.conflict";
|
|
27
|
+
case "node_forbidden":
|
|
28
|
+
case "flow_edit_forbidden":
|
|
29
|
+
return "resource.forbidden";
|
|
30
|
+
default:
|
|
31
|
+
return "resource.failed";
|
|
32
|
+
}
|
|
33
|
+
}
|