@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,64 @@
|
|
|
1
|
+
import type { ExpandedLevel } from "@/app/tree-expansion/tree-expansion.types.ts";
|
|
2
|
+
import type { RevealDeps, RevealTarget } from "./tree-reveal.types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How far up the trail is followed before it is given up on.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ NOT a claim about how deep anybody nests. It is the stop against a parent chain that does not
|
|
8
|
+
* end: `parent_id` is a plain column, and a cycle in it — a bad restore, a move that crossed itself
|
|
9
|
+
* — would otherwise be an endless walk making one request per step, in the sidebar, on every screen
|
|
10
|
+
* of the application.
|
|
11
|
+
*/
|
|
12
|
+
export const MaxTrailDepth = 32;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The folders standing above a selected row, root first.
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ The row ITSELF is never in the answer, and a selected folder is no exception. Selecting
|
|
18
|
+
* something says where it is; opening it is a gesture, and the tree does not get to invent one
|
|
19
|
+
* (#809).
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ A failure answers with the trail found SO FAR rather than with nothing. A parent this reader
|
|
22
|
+
* may not see is the ordinary case rather than an error — a shared document is reachable while the
|
|
23
|
+
* folder above it is not (#429) — and giving up on the whole trail would leave a tree closed that
|
|
24
|
+
* could have been opened down to the last folder the reader does see.
|
|
25
|
+
*/
|
|
26
|
+
export async function folderTrail(
|
|
27
|
+
deps: RevealDeps,
|
|
28
|
+
target: RevealTarget,
|
|
29
|
+
): Promise<ExpandedLevel[]> {
|
|
30
|
+
const trail: ExpandedLevel[] = [];
|
|
31
|
+
const seen = new Set<string>([target.id]);
|
|
32
|
+
let parentId: string | null;
|
|
33
|
+
try {
|
|
34
|
+
parentId =
|
|
35
|
+
target.area === "flows"
|
|
36
|
+
? (await deps.flow(target.id)).parentId
|
|
37
|
+
: (await deps.node(target.id)).parentId;
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while (parentId !== null && trail.length < MaxTrailDepth) {
|
|
43
|
+
if (seen.has(parentId)) break;
|
|
44
|
+
const folderId = parentId;
|
|
45
|
+
seen.add(folderId);
|
|
46
|
+
// ⚠️ Read BEFORE taking it into the trail, and the order is the whole point: the answer to this
|
|
47
|
+
// request is also the answer to "may this reader see the folder at all". Taken in first, an
|
|
48
|
+
// unreadable folder would stand in the tree's open list as a level nobody can draw — harmless
|
|
49
|
+
// today, because a level without a row is never loaded (#521), but it is a claim about
|
|
50
|
+
// something the reader does not have.
|
|
51
|
+
let parent: { parentId: string | null };
|
|
52
|
+
try {
|
|
53
|
+
parent = await deps.node(folderId);
|
|
54
|
+
} catch {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
trail.push({ id: folderId, type: "folder" });
|
|
58
|
+
parentId = parent.parentId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Root first, which is the order the tree opens them in anyway: a level is only loaded once the
|
|
62
|
+
// row above it is drawn.
|
|
63
|
+
return trail.reverse();
|
|
64
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Where a selection lives. The area comes from the path the reader is on, never from the id: a
|
|
2
|
+
// `select` on `/flows` names a flow, the same parameter on `/nodes` names a node, and nothing about
|
|
3
|
+
// the id itself tells the two apart.
|
|
4
|
+
export type RevealArea = "nodes" | "flows";
|
|
5
|
+
|
|
6
|
+
export interface RevealTarget {
|
|
7
|
+
id: string;
|
|
8
|
+
area: RevealArea;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Only the one field the trail walks on. Both readers hand back a whole document; asking for the
|
|
12
|
+
// parent alone keeps this module out of the shape of either.
|
|
13
|
+
export interface RevealDeps {
|
|
14
|
+
node(nodeId: string): Promise<{ parentId: string | null }>;
|
|
15
|
+
flow(flowId: string): Promise<{ parentId: string | null }>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
3
|
+
import {
|
|
4
|
+
Archive as ArchiveIcon,
|
|
5
|
+
ChevronsUpDown,
|
|
6
|
+
List,
|
|
7
|
+
LogOut,
|
|
8
|
+
RefreshCw,
|
|
9
|
+
Settings,
|
|
10
|
+
User,
|
|
11
|
+
Wrench,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import {
|
|
15
|
+
DropdownMenu,
|
|
16
|
+
DropdownMenuContent,
|
|
17
|
+
DropdownMenuItem,
|
|
18
|
+
DropdownMenuLabel,
|
|
19
|
+
DropdownMenuSeparator,
|
|
20
|
+
DropdownMenuTrigger,
|
|
21
|
+
} from "@/components/ui/dropdown-menu";
|
|
22
|
+
import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
|
|
23
|
+
import { loginPath } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
24
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
25
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
26
|
+
import { ReindexDialog } from "../reindex-dialog/reindex-dialog.tsx";
|
|
27
|
+
import { SettingsDialog } from "../settings-dialog/settings-dialog.tsx";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Everything that belongs to the person rather than to the content, behind the person (#24).
|
|
31
|
+
*
|
|
32
|
+
* The three rows this used to be — the tool catalogue, the name, the sign-out — spent three lines of
|
|
33
|
+
* sidebar on things nobody opens twice a day. The name and the address stay on screen, because they
|
|
34
|
+
* are the answer to "who am I signed in as", which is a reading and not an action; the two actions
|
|
35
|
+
* move behind them.
|
|
36
|
+
*
|
|
37
|
+
* The catalogue itself is a live portal query with the signed-in user's own token (ADR-0003), so
|
|
38
|
+
* what it shows depends on who is asking.
|
|
39
|
+
*/
|
|
40
|
+
export function UserFooter() {
|
|
41
|
+
const { data } = useIntelRouterContext();
|
|
42
|
+
const i18n = useI18n();
|
|
43
|
+
const navigate = useNavigate();
|
|
44
|
+
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
45
|
+
// The state lives here, not in the dialog: its trigger is a DropdownMenuItem, and Radix unmounts
|
|
46
|
+
// the menu content when an item is chosen — a dialog nested in there would go with it.
|
|
47
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
48
|
+
const [reindexOpen, setReindexOpen] = useState(false);
|
|
49
|
+
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
50
|
+
const logout = useMutation({
|
|
51
|
+
mutationFn: () => data.logout(),
|
|
52
|
+
onSuccess: () => window.location.assign(loginPath()),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const user = session.data ?? null;
|
|
56
|
+
const primary = user
|
|
57
|
+
? (user.name ?? user.email)
|
|
58
|
+
: session.isPending
|
|
59
|
+
? i18n.t("common.loading")
|
|
60
|
+
: i18n.t("shell.userUnavailable");
|
|
61
|
+
const secondary = user?.name ? user.email : null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<SidebarMenu>
|
|
65
|
+
<SidebarMenuItem>
|
|
66
|
+
<DropdownMenu>
|
|
67
|
+
<DropdownMenuTrigger
|
|
68
|
+
aria-label={i18n.t("shell.userMenu", { name: primary })}
|
|
69
|
+
data-slot="signed-in-user"
|
|
70
|
+
// ⚠️ `/feed` joins `/tools` here (#813): both destinations live behind THIS trigger and
|
|
71
|
+
// nowhere else in the sidebar (#742), so the trigger is the only surface that could ever
|
|
72
|
+
// say "you are there" for either one — the tree carries no row for a place that is not a
|
|
73
|
+
// place things are IN. Extending this to `/archive` and `/settings` too was left alone
|
|
74
|
+
// deliberately: the ticket asked about the feed, and touching the other two answers a
|
|
75
|
+
// question nobody put to this ticket.
|
|
76
|
+
data-active={pathname === "/tools" || pathname === "/feed"}
|
|
77
|
+
className="flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 focus-visible:ring-sidebar-ring data-[active=true]:bg-sidebar-accent data-[state=open]:bg-sidebar-accent"
|
|
78
|
+
>
|
|
79
|
+
<span className="flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
|
|
80
|
+
<User aria-hidden="true" className="size-4" />
|
|
81
|
+
</span>
|
|
82
|
+
{/* Read out in both states; sighted users see it in neither. It is what tells a screen
|
|
83
|
+
reader that the name below is a name and not a heading. */}
|
|
84
|
+
<span className="sr-only">{i18n.t("shell.signedInUser")}</span>
|
|
85
|
+
<span className="grid min-w-0 flex-1 leading-tight">
|
|
86
|
+
<span className="truncate font-medium">{primary}</span>
|
|
87
|
+
{secondary ? (
|
|
88
|
+
<span className="truncate text-xs text-muted-foreground">{secondary}</span>
|
|
89
|
+
) : null}
|
|
90
|
+
</span>
|
|
91
|
+
<ChevronsUpDown aria-hidden="true" className="ml-auto size-4 shrink-0" />
|
|
92
|
+
</DropdownMenuTrigger>
|
|
93
|
+
{/* Upwards and as wide as its trigger: it opens from the bottom edge of the screen, and
|
|
94
|
+
a menu that has to be hunted for beside its button is a menu that was not found. */}
|
|
95
|
+
<DropdownMenuContent
|
|
96
|
+
side="top"
|
|
97
|
+
align="start"
|
|
98
|
+
className="w-(--radix-dropdown-menu-trigger-width) min-w-56"
|
|
99
|
+
>
|
|
100
|
+
{/* #452: the head — who is signed in, above the things they can do. It used to stand
|
|
101
|
+
ONLY in the trigger BELOW the menu, so an open menu pointed at the answer instead of
|
|
102
|
+
giving it. Not a menu item: it is a reading, not an action, so it takes no focus and
|
|
103
|
+
nothing happens when it is pressed. */}
|
|
104
|
+
<DropdownMenuLabel className="font-normal">
|
|
105
|
+
<div className="grid text-left text-sm leading-tight">
|
|
106
|
+
<span className="truncate font-medium">{primary}</span>
|
|
107
|
+
{secondary ? (
|
|
108
|
+
<span className="truncate text-xs text-muted-foreground">{secondary}</span>
|
|
109
|
+
) : null}
|
|
110
|
+
</div>
|
|
111
|
+
</DropdownMenuLabel>
|
|
112
|
+
{/* ⚠️ The ONE separator, and the only one left (#452). There used to be three, each with
|
|
113
|
+
a good reason of its own: the settings change nothing anybody else sees, the reindex
|
|
114
|
+
belongs to the installation rather than the person, signing out is destructive.
|
|
115
|
+
Together they cut five rows into five groups — and a grouping that separates every
|
|
116
|
+
single row groups nothing. Above it stands who you are, below it what you can do;
|
|
117
|
+
that is the whole distinction the line carries, and it is the same one Gate draws. */}
|
|
118
|
+
<DropdownMenuSeparator />
|
|
119
|
+
<DropdownMenuItem onSelect={() => void navigate({ to: "/tools" })}>
|
|
120
|
+
<Wrench aria-hidden="true" />
|
|
121
|
+
{i18n.t("nav.tools")}
|
|
122
|
+
</DropdownMenuItem>
|
|
123
|
+
{/* ⚠️ The feed lives here and NOT in the tree, and the placement is the decision
|
|
124
|
+
(#742): it is not a place things are in, and it is not something anybody needs
|
|
125
|
+
twice a day. No counter and no dot on the icon either — a badge is a request for
|
|
126
|
+
attention, and this surface is deliberately not making one. */}
|
|
127
|
+
<DropdownMenuItem onSelect={() => void navigate({ to: "/feed" })}>
|
|
128
|
+
<List aria-hidden="true" />
|
|
129
|
+
{i18n.t("feed.title")}
|
|
130
|
+
</DropdownMenuItem>
|
|
131
|
+
{/* The archive lives here rather than in the tree: it is not a place things are IN,
|
|
132
|
+
it is where they went when they left the tree (#113). */}
|
|
133
|
+
<DropdownMenuItem onSelect={() => void navigate({ to: "/archive" })}>
|
|
134
|
+
<ArchiveIcon aria-hidden="true" />
|
|
135
|
+
{i18n.t("archive.title")}
|
|
136
|
+
</DropdownMenuItem>
|
|
137
|
+
{/* Language and appearance belong to the person, like everything else behind this menu. */}
|
|
138
|
+
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
139
|
+
<Settings aria-hidden="true" />
|
|
140
|
+
{i18n.t("settings.title")}
|
|
141
|
+
</DropdownMenuItem>
|
|
142
|
+
{/* ⚠️ Drawn only for an administrator, and that is a DRAWING decision — `POST
|
|
143
|
+
/nodes/reindex` asks Gate for `intel/admin` itself and would refuse this row's press
|
|
144
|
+
just the same (#416). Hiding it is not the check; it is not offering everybody a
|
|
145
|
+
door they cannot open.
|
|
146
|
+
⚠️ Its own separator is GONE (#452): it set a single row apart, and that is exactly
|
|
147
|
+
the line this ticket removes. The reason it carried — this one belongs to the
|
|
148
|
+
installation — is true and still not worth a rule of its own on a five-row menu. */}
|
|
149
|
+
{session.data?.isAdmin ? (
|
|
150
|
+
// ⚠️ `nav.reindex` is a DIFFERENT key from the dialog's `reindex.title` on purpose
|
|
151
|
+
// (#814): a menu of one-word entries carries a whole sentence for this one row, and
|
|
152
|
+
// the dialog's heading has to stay the long form it is — the sentence beneath it is
|
|
153
|
+
// about time and money, and the heading names what that sentence is about.
|
|
154
|
+
<DropdownMenuItem onSelect={() => setReindexOpen(true)}>
|
|
155
|
+
<RefreshCw aria-hidden="true" />
|
|
156
|
+
{i18n.t("nav.reindex")}
|
|
157
|
+
</DropdownMenuItem>
|
|
158
|
+
) : null}
|
|
159
|
+
{/* Signing out stays recognisable as destructive — the COLOUR does that, and it needs no
|
|
160
|
+
line of its own to say it twice. */}
|
|
161
|
+
<DropdownMenuItem
|
|
162
|
+
disabled={logout.isPending}
|
|
163
|
+
onSelect={() => logout.mutate()}
|
|
164
|
+
className="text-destructive focus:text-destructive"
|
|
165
|
+
>
|
|
166
|
+
<LogOut aria-hidden="true" />
|
|
167
|
+
{i18n.t("auth.signOut")}
|
|
168
|
+
</DropdownMenuItem>
|
|
169
|
+
</DropdownMenuContent>
|
|
170
|
+
</DropdownMenu>
|
|
171
|
+
{/* ⚠️ Outside the DropdownMenu on purpose — see the state above. */}
|
|
172
|
+
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
173
|
+
{/* ⚠️ Outside the DropdownMenu for the same reason as the settings dialog. */}
|
|
174
|
+
<ReindexDialog open={reindexOpen} onOpenChange={setReindexOpen} />
|
|
175
|
+
</SidebarMenuItem>
|
|
176
|
+
{/* ⚠️ Outside the menu on purpose. Choosing sign-out closes the popup, so a refusal rendered
|
|
177
|
+
inside it would be gone in the same frame it was written — the one message that must
|
|
178
|
+
survive is the one saying you are still signed in. */}
|
|
179
|
+
{logout.isError ? (
|
|
180
|
+
<SidebarMenuItem>
|
|
181
|
+
<p role="alert" className="px-2 py-1 text-sm text-destructive">
|
|
182
|
+
{i18n.t("auth.signOutFailed")}
|
|
183
|
+
</p>
|
|
184
|
+
</SidebarMenuItem>
|
|
185
|
+
) : null}
|
|
186
|
+
</SidebarMenu>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
2
|
+
import { Code2, KanbanSquare, ListChecks, Network, PencilRuler, Table2 } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from "@/components/ui/dropdown-menu";
|
|
9
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
10
|
+
import { type IntelView, viewOf } from "@/router/selection-search.ts";
|
|
11
|
+
|
|
12
|
+
// ⚠️ `runs` is NOT `History`, and that is the point of this line rather than a taste in icons.
|
|
13
|
+
// `History` is what the resource menu draws beside "Version history" (`resource-menu.tsx`), and both
|
|
14
|
+
// are visible on the same flow header — one as this switch, one in the menu right below it. The same
|
|
15
|
+
// clock for two different things was read as one: the switch was taken for the version history and
|
|
16
|
+
// asked about as such (#635). Whatever stands here has to stay out of that menu.
|
|
17
|
+
const icons = {
|
|
18
|
+
editor: PencilRuler,
|
|
19
|
+
graph: Network,
|
|
20
|
+
runs: ListChecks,
|
|
21
|
+
kanban: KanbanSquare,
|
|
22
|
+
table: Table2,
|
|
23
|
+
code: Code2,
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
// The NAME of a view, not an instruction to switch to it. A menu entry says what it leads to and
|
|
27
|
+
// the closed trigger says where you are — both need the same word, and "Show this level as a graph"
|
|
28
|
+
// is a sentence for neither.
|
|
29
|
+
const names = {
|
|
30
|
+
editor: "view.name.editor",
|
|
31
|
+
graph: "view.name.graph",
|
|
32
|
+
runs: "view.name.runs",
|
|
33
|
+
kanban: "view.name.kanban",
|
|
34
|
+
table: "view.name.table",
|
|
35
|
+
code: "view.name.code",
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The switch between the ways the selected thing can be shown: one symbol for the view you are in,
|
|
40
|
+
* and one click to see the rest by symbol and name.
|
|
41
|
+
*
|
|
42
|
+
* **This shape is the convention for every view, including the ones not built yet** (#818, Jack's
|
|
43
|
+
* decision of 2026-08-29). Until then there were two switches for one question — a row of buttons
|
|
44
|
+
* in the address for editor/graph/runs, and a second, address-less pair inside the board — which
|
|
45
|
+
* looked alike and behaved differently. Whoever adds a view adds an entry here; nobody adds a
|
|
46
|
+
* second mechanism.
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ **It replaces a decision that had a reason, and the reason is not gone.** #19 and #24 chose a
|
|
49
|
+
* row of buttons over a menu because a menu costs a click in each direction, and with two views a
|
|
50
|
+
* toggle is genuinely cheaper. That price is named and accepted: one uniform switch on every
|
|
51
|
+
* surface is worth a click where there are only two views. What is NOT reopened is cycling through
|
|
52
|
+
* the views (#35, #635) — every destination is one entry, and every entry keeps one name.
|
|
53
|
+
*
|
|
54
|
+
* **Head, not row.** The switch changes how the *current* thing is shown, and the header is where
|
|
55
|
+
* that thing is already named. In a tree row it would sit inside the row's overflow menu (#24), two
|
|
56
|
+
* clicks away in each direction.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ **Where there is only one view there is no switch**, and that is a rule rather than an
|
|
59
|
+
* accident of the call sites: a menu with a single entry is a control that promises a choice it
|
|
60
|
+
* does not have. The callers already leave it out where there is nothing to switch to (a document
|
|
61
|
+
* has no graph); the guard below answers the same question one level lower, where it cannot be
|
|
62
|
+
* forgotten.
|
|
63
|
+
*/
|
|
64
|
+
export function ViewMenu({ views = ["editor", "graph"] }: { views?: readonly IntelView[] }) {
|
|
65
|
+
const i18n = useI18n();
|
|
66
|
+
const navigate = useNavigate();
|
|
67
|
+
const state = useRouterState({
|
|
68
|
+
select: (current) => ({
|
|
69
|
+
pathname: current.location.pathname,
|
|
70
|
+
search: current.location.search as Record<string, unknown>,
|
|
71
|
+
view: viewOf(current.location.search, views),
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
if (views.length < 2) return null;
|
|
75
|
+
const CurrentIcon = icons[state.view];
|
|
76
|
+
return (
|
|
77
|
+
<DropdownMenu>
|
|
78
|
+
{/* ⚠️ The name sits on the BUTTON, never on the icon inside it: children of a button are
|
|
79
|
+
presentational in ARIA, so a labelled mark in here would be announced by nobody (#366). */}
|
|
80
|
+
<DropdownMenuTrigger
|
|
81
|
+
data-view-menu=""
|
|
82
|
+
aria-label={i18n.t("view.current", { name: i18n.t(names[state.view]) })}
|
|
83
|
+
className="inline-flex size-8 shrink-0 items-center justify-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-accent"
|
|
84
|
+
>
|
|
85
|
+
<CurrentIcon aria-hidden="true" className="size-4" />
|
|
86
|
+
</DropdownMenuTrigger>
|
|
87
|
+
<DropdownMenuContent align="end" className="min-w-40 whitespace-nowrap">
|
|
88
|
+
{/* The others, because the one you are in is what the closed symbol already says. A list
|
|
89
|
+
that repeated it would put the reader's own view among the places to go. */}
|
|
90
|
+
{views
|
|
91
|
+
.filter((view) => view !== state.view)
|
|
92
|
+
.map((view) => {
|
|
93
|
+
const Icon = icons[view];
|
|
94
|
+
return (
|
|
95
|
+
<DropdownMenuItem
|
|
96
|
+
key={view}
|
|
97
|
+
onSelect={() => {
|
|
98
|
+
// The first view is the surface's default and is written as the ABSENCE of the
|
|
99
|
+
// parameter, so a link to it stays the plain link it has always been.
|
|
100
|
+
const { view: _view, ...rest } = state.search;
|
|
101
|
+
void navigate({
|
|
102
|
+
to: state.pathname,
|
|
103
|
+
search: view === views[0] ? rest : { ...rest, view },
|
|
104
|
+
});
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<Icon aria-hidden="true" />
|
|
108
|
+
{i18n.t(names[view])}
|
|
109
|
+
</DropdownMenuItem>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</DropdownMenuContent>
|
|
113
|
+
</DropdownMenu>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { RouterProvider } from "@tanstack/react-router";
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
import type { IntelDataProvider } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
5
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
|
+
import { SignInRefused } from "@/sign-in-refused/sign-in-refused.tsx";
|
|
7
|
+
import type { AppRootDeps, RefusalStore } from "./app-root.types.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The one decision this application makes before it draws anything: **is there a session?**
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Without a session there is NO SHELL — not the sidebar, not the header, not the search (#456).
|
|
13
|
+
* It used to paint first and ask afterwards: the `RouterProvider` started, the shell stood, the
|
|
14
|
+
* queries went out, and only when the first of them came back 401 did the redirect happen. Between
|
|
15
|
+
* "painted" and "redirected" lay a full network round trip in which a stranger saw the application.
|
|
16
|
+
* A slow answer stretched that window arbitrarily, and a route that fires no query at all never
|
|
17
|
+
* closed it.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ What is NOT claimed: that data would become visible. The api answers 401 and the lists stay
|
|
20
|
+
* empty. But that one server answer was then the ONLY defence — the client had none. Any surface
|
|
21
|
+
* that later renders something before the first request (a cache, a `localStorage`, an optimistic
|
|
22
|
+
* value) would break it without meaning to. And an application that looks as if one were inside it
|
|
23
|
+
* and contains nothing is the wrong first impression from a product whose business is access
|
|
24
|
+
* control.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ **This is the file #130 asked for.** The logic sat in `main.tsx`, `main.tsx` had no test, and
|
|
27
|
+
* therefore none of the eleven tests written for #118 ever touched it: *"the entry point is exactly
|
|
28
|
+
* the place where 'this is only wiring' sounds most plausible and costs most."* The decision now
|
|
29
|
+
* lives somewhere a test can call.
|
|
30
|
+
*/
|
|
31
|
+
export function AppRoot({ data, router, refusal }: AppRootDeps) {
|
|
32
|
+
const refused = useSyncExternalStore(refusal.subscribe, refusal.isRefused, refusal.isRefused);
|
|
33
|
+
if (refused) return <SignInRefused />;
|
|
34
|
+
return <SessionGate data={data} router={router} />;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ⚠️ Its own component, and not merged into `AppRoot`: the hook below may only run once a
|
|
39
|
+
* `QueryClientProvider` is above it, and `refused` has to be answerable without one — the refusal
|
|
40
|
+
* screen is what stands there when nothing else works.
|
|
41
|
+
*/
|
|
42
|
+
function SessionGate({ data, router }: { data: IntelDataProvider; router: AppRootDeps["router"] }) {
|
|
43
|
+
const i18n = useI18n();
|
|
44
|
+
// ⚠️ `retry: false`, deliberately against the client's default policy: this answer is
|
|
45
|
+
// authoritative in both directions. A 401 is not a hiccup to be retried — the provider's
|
|
46
|
+
// `onUnauthorized` is already taking the reader to Gate while this promise rejects — and a retry
|
|
47
|
+
// here would additionally be PAUSED while the tab is unfocused (`packages/intel/ui/CLAUDE.md`), which
|
|
48
|
+
// is how a guard turns into a permanent "Loading" for somebody looking elsewhere.
|
|
49
|
+
//
|
|
50
|
+
// The key is the one `UserFooter` reads, so the shell's own session query is already answered
|
|
51
|
+
// when it mounts. One question, asked once.
|
|
52
|
+
const session = useQuery({
|
|
53
|
+
queryKey: ["session"],
|
|
54
|
+
queryFn: () => data.getSession(),
|
|
55
|
+
retry: false,
|
|
56
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ⚠️ Both the waiting and the failing case render NO shell. The failing one is not the same as
|
|
60
|
+
// "not signed in": a 401 has already sent the reader away by the time it lands here, so what is
|
|
61
|
+
// left is a surface that could not be reached at all — and saying that is better than a spinner
|
|
62
|
+
// that never ends. Neither of them draws a sidebar.
|
|
63
|
+
if (session.isPending) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="grid min-h-dvh place-items-center bg-background p-8">
|
|
66
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (session.isError) {
|
|
71
|
+
return (
|
|
72
|
+
<div role="alert" className="grid min-h-dvh place-items-center bg-background p-8">
|
|
73
|
+
<div className="max-w-prose text-center">
|
|
74
|
+
<p className="text-sm text-muted-foreground">{i18n.t("signIn.unreachable")}</p>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => window.location.reload()}
|
|
78
|
+
className="mt-6 rounded-md border px-4 py-2 text-sm font-medium outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
79
|
+
>
|
|
80
|
+
{i18n.t("common.retry")}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return <RouterProvider router={router} />;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* ⚠️ Latching, and that is not an optimisation: once the surface has turned down a freshly issued
|
|
91
|
+
* credential, later 401s say nothing new. Without the latch every request the page still had in
|
|
92
|
+
* flight would notify again, and the screen would flicker once per answer.
|
|
93
|
+
*/
|
|
94
|
+
export function createRefusalStore(): RefusalStore {
|
|
95
|
+
let refused = false;
|
|
96
|
+
const listeners = new Set<() => void>();
|
|
97
|
+
return {
|
|
98
|
+
subscribe(listener) {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => listeners.delete(listener);
|
|
101
|
+
},
|
|
102
|
+
isRefused: () => refused,
|
|
103
|
+
refuse() {
|
|
104
|
+
if (refused) return;
|
|
105
|
+
refused = true;
|
|
106
|
+
for (const listener of listeners) listener();
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IntelDataProvider } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
2
|
+
import type { createIntelRouter } from "@/router/router.tsx";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether the surface has turned down a freshly issued credential (#118) — a store rather than a
|
|
6
|
+
* module-level `let`, and that is the whole point of #456.
|
|
7
|
+
*
|
|
8
|
+
* The refusal arrives from inside a `fetch`, not from React: the data provider calls
|
|
9
|
+
* `onUnauthorized`, and something on screen has to change because of it. That used to be a
|
|
10
|
+
* `let refused = false` in `main.tsx` with a hand-written repaint — a decision in bare module body,
|
|
11
|
+
* which is exactly what nothing could call and therefore what no test ever touched (#130).
|
|
12
|
+
*/
|
|
13
|
+
export interface RefusalStore {
|
|
14
|
+
subscribe(listener: () => void): () => void;
|
|
15
|
+
isRefused(): boolean;
|
|
16
|
+
refuse(): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type IntelRouter = ReturnType<typeof createIntelRouter>;
|
|
20
|
+
|
|
21
|
+
export interface AppRootDeps {
|
|
22
|
+
// ⚠️ The provider is handed in rather than read from the router context: that context only exists
|
|
23
|
+
// INSIDE `RouterProvider`, and the whole point of the gate is to decide before it mounts.
|
|
24
|
+
data: IntelDataProvider;
|
|
25
|
+
router: IntelRouter;
|
|
26
|
+
refusal: RefusalStore;
|
|
27
|
+
}
|