@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,34 @@
|
|
|
1
|
+
import type { FlowNode } from "@contract/flow.ts";
|
|
2
|
+
import {
|
|
3
|
+
CircleDot,
|
|
4
|
+
CirclePlay,
|
|
5
|
+
FileText,
|
|
6
|
+
Folder,
|
|
7
|
+
GitBranch,
|
|
8
|
+
Paperclip,
|
|
9
|
+
Sparkles,
|
|
10
|
+
Table,
|
|
11
|
+
Workflow,
|
|
12
|
+
Wrench,
|
|
13
|
+
} from "lucide-react";
|
|
14
|
+
import type { ComponentType, SVGProps } from "react";
|
|
15
|
+
|
|
16
|
+
// The canvas card and the palette entry have to show the same symbol for the same kind — a step that
|
|
17
|
+
// looks one way in the bar and another on the graph is two things to learn instead of one. The map
|
|
18
|
+
// lives apart from both so neither has to import the other.
|
|
19
|
+
export type NodeIcon = ComponentType<SVGProps<SVGSVGElement>>;
|
|
20
|
+
|
|
21
|
+
export const nodeIcon: Record<FlowNode["kind"], NodeIcon> = {
|
|
22
|
+
trigger: CirclePlay,
|
|
23
|
+
instruction: Sparkles,
|
|
24
|
+
condition: GitBranch,
|
|
25
|
+
subflow: Workflow,
|
|
26
|
+
// The four links wear the symbol their kind wears in the tree, so a document is the same shape
|
|
27
|
+
// wherever it is seen — the sidebar, the picker and the canvas do not each teach their own.
|
|
28
|
+
folder: Folder,
|
|
29
|
+
document: FileText,
|
|
30
|
+
upload: Paperclip,
|
|
31
|
+
table: Table,
|
|
32
|
+
tool: Wrench,
|
|
33
|
+
output: CircleDot,
|
|
34
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { flowNodeLayer } from "@contract/flow.ts";
|
|
2
|
+
import { Plus, X } from "lucide-react";
|
|
3
|
+
import { Fragment, useId, useRef, useState } from "react";
|
|
4
|
+
import { type NodeIcon, nodeIcon } from "@/flows/node-icon/node-icon.ts";
|
|
5
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
|
+
import { cn } from "@/lib/utils.ts";
|
|
7
|
+
import type { NodePaletteProps, PaletteKind } from "./node-palette.types.ts";
|
|
8
|
+
|
|
9
|
+
const storageKey = "intel.flow-palette";
|
|
10
|
+
|
|
11
|
+
// ⚠️ Reading `localStorage` throws outright in a few privacy modes, so the guard is a try, not a
|
|
12
|
+
// typeof check — the same reason `sidebar-preferences` guards it that way. Losing the preference is
|
|
13
|
+
// acceptable; losing the editor is not.
|
|
14
|
+
function readOpen(): boolean {
|
|
15
|
+
try {
|
|
16
|
+
return typeof window !== "undefined" && window.localStorage.getItem(storageKey) === "open";
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeOpen(open: boolean): void {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof window !== "undefined")
|
|
25
|
+
window.localStorage.setItem(storageKey, open ? "open" : "closed");
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Closed is the default, and the preference is written through: the bar is chrome, exactly like the
|
|
30
|
+
// sidebar whose open state already survives a reload. Someone who builds flows all day would
|
|
31
|
+
// otherwise re-open it on every load, and the ticket's own reason for collapsing it — the canvas
|
|
32
|
+
// belongs to the graph — is just as true for someone who never opens it.
|
|
33
|
+
export function usePaletteOpen(): [boolean, (open: boolean) => void] {
|
|
34
|
+
const [open, setOpen] = useState(readOpen);
|
|
35
|
+
return [
|
|
36
|
+
open,
|
|
37
|
+
(next: boolean) => {
|
|
38
|
+
setOpen(next);
|
|
39
|
+
writeOpen(next);
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function NodePalette<K extends PaletteKind>({
|
|
45
|
+
kinds,
|
|
46
|
+
open,
|
|
47
|
+
setOpen,
|
|
48
|
+
add,
|
|
49
|
+
disabledReason,
|
|
50
|
+
}: NodePaletteProps<K>) {
|
|
51
|
+
const i18n = useI18n();
|
|
52
|
+
const listId = useId();
|
|
53
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
54
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
55
|
+
// A toolbar carries one tab stop, so the group is entered with a single Tab and left with the
|
|
56
|
+
// next one instead of costing seven.
|
|
57
|
+
const [active, setActive] = useState(0);
|
|
58
|
+
|
|
59
|
+
// The focus goes back where it came from, so closing does not drop the caret at the top of the
|
|
60
|
+
// document and make the next Tab start over from the page.
|
|
61
|
+
function close() {
|
|
62
|
+
setOpen(false);
|
|
63
|
+
triggerRef.current?.focus();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function entries(): HTMLButtonElement[] {
|
|
67
|
+
return [
|
|
68
|
+
...(listRef.current?.querySelectorAll<HTMLButtonElement>("button[data-palette-item]") ?? []),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A disabled entry is stepped over rather than focused: its reason is already on the entry, and
|
|
73
|
+
// stopping there would only be a dead end for the keyboard.
|
|
74
|
+
function focusEntry(from: number, delta: number) {
|
|
75
|
+
const items = entries();
|
|
76
|
+
if (items.length === 0) return;
|
|
77
|
+
for (let step = 1; step <= items.length; step += 1) {
|
|
78
|
+
const index = (((from + delta * step) % items.length) + items.length) % items.length;
|
|
79
|
+
const item = items[index];
|
|
80
|
+
if (item && !item.disabled) {
|
|
81
|
+
setActive(index);
|
|
82
|
+
item.focus();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function onListKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
|
|
89
|
+
if (event.key === "Escape") {
|
|
90
|
+
event.stopPropagation();
|
|
91
|
+
close();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const forward = event.key === "ArrowDown" || event.key === "ArrowRight";
|
|
95
|
+
const backward = event.key === "ArrowUp" || event.key === "ArrowLeft";
|
|
96
|
+
if (!forward && !backward) return;
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
const items = entries();
|
|
99
|
+
const current = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
100
|
+
focusEntry(current < 0 ? 0 : current, forward ? 1 : -1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onTriggerKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) {
|
|
104
|
+
if (!open) return;
|
|
105
|
+
if (event.key === "Escape") {
|
|
106
|
+
event.stopPropagation();
|
|
107
|
+
close();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (event.key === "ArrowDown" || event.key === "ArrowRight") {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
focusEntry(-1, 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Open, the trigger is the first segment of the bar itself rather than a second box above it: one
|
|
117
|
+
// closes a bar where it is. Two details carry that and neither is decoration:
|
|
118
|
+
//
|
|
119
|
+
// ⚠️ The negative margin is the frame's own padding plus its border. Opening hands the frame from
|
|
120
|
+
// the trigger to the row around it, and a frame drawn inside would push the trigger down and right
|
|
121
|
+
// by exactly that sum. The margin lets the row grow outwards instead, so the 36px one clicks stays
|
|
122
|
+
// on the same pixel open and collapsed. Change `p-1` and this has to change with it.
|
|
123
|
+
//
|
|
124
|
+
// ⚠️ The toolbar stays an element of its own inside the row: it is what `aria-controls` names and
|
|
125
|
+
// what the arrow keys walk, and the trigger must not become an eighth entry in that ring.
|
|
126
|
+
//
|
|
127
|
+
// The `w-max` that used to stand here went with the structure it guarded. While the bar was a
|
|
128
|
+
// block below the trigger, the shrink-wrapped box took the collapsed 36px as its maximum and
|
|
129
|
+
// folded the entries into a column; one flex row is sized from its content in either state, which
|
|
130
|
+
// was measured at 1280px and at 420px with the class and without it (#55).
|
|
131
|
+
return (
|
|
132
|
+
<div
|
|
133
|
+
className={cn(
|
|
134
|
+
"absolute left-4 top-4 z-10 max-w-[calc(100%-2rem)]",
|
|
135
|
+
open &&
|
|
136
|
+
"-m-[calc(0.25rem+1px)] flex flex-wrap items-center gap-1 rounded-lg border bg-card/95 p-1 shadow-sm backdrop-blur",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<button
|
|
140
|
+
ref={triggerRef}
|
|
141
|
+
type="button"
|
|
142
|
+
aria-expanded={open}
|
|
143
|
+
aria-controls={listId}
|
|
144
|
+
onClick={() => (open ? close() : setOpen(true))}
|
|
145
|
+
onKeyDown={onTriggerKeyDown}
|
|
146
|
+
className={cn(
|
|
147
|
+
"inline-flex size-9 shrink-0 items-center justify-center rounded-lg text-card-foreground outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
|
|
148
|
+
!open && "border bg-card/95 shadow-sm backdrop-blur",
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{open ? (
|
|
152
|
+
<X aria-hidden="true" className="size-4" />
|
|
153
|
+
) : (
|
|
154
|
+
<Plus aria-hidden="true" className="size-4" />
|
|
155
|
+
)}
|
|
156
|
+
<span className="sr-only">{i18n.t(open ? "flows.closePalette" : "flows.addNode")}</span>
|
|
157
|
+
</button>
|
|
158
|
+
{/* Over the canvas rather than pushing it: the graph is a spatial workspace, and resizing the
|
|
159
|
+
viewport would shift every node under the pointer each time the bar opens. It is dismissed
|
|
160
|
+
instead — the trigger, Escape, or a click on the canvas. */}
|
|
161
|
+
{open && (
|
|
162
|
+
<div
|
|
163
|
+
ref={listRef}
|
|
164
|
+
id={listId}
|
|
165
|
+
role="toolbar"
|
|
166
|
+
aria-orientation="horizontal"
|
|
167
|
+
aria-label={i18n.t("flows.nodePalette")}
|
|
168
|
+
onKeyDown={onListKeyDown}
|
|
169
|
+
// `min-w-0` so a narrow bar makes the entries wrap among themselves instead of forcing the
|
|
170
|
+
// whole row wider than the canvas allows.
|
|
171
|
+
className="flex min-w-0 max-w-full flex-wrap gap-1"
|
|
172
|
+
>
|
|
173
|
+
{kinds.map((kind, index) => {
|
|
174
|
+
const Icon: NodeIcon = nodeIcon[kind];
|
|
175
|
+
const reason = disabledReason?.(kind) ?? null;
|
|
176
|
+
// ⚠️ A separator before the first entry of each layer after the first (D25). It is
|
|
177
|
+
// decoration, not structure: the toolbar's keyboard walks `[data-palette-item]`, so a
|
|
178
|
+
// divider in between must not be one — otherwise the arrows would stop on a line.
|
|
179
|
+
const opensLayer =
|
|
180
|
+
index > 0 && flowNodeLayer[kind] !== flowNodeLayer[kinds[index - 1] ?? kind];
|
|
181
|
+
return (
|
|
182
|
+
<Fragment key={kind}>
|
|
183
|
+
{opensLayer ? (
|
|
184
|
+
<span
|
|
185
|
+
aria-hidden="true"
|
|
186
|
+
className="mx-1 my-1.5 w-px self-stretch bg-border"
|
|
187
|
+
title={i18n.t(`flows.layer.${flowNodeLayer[kind]}`)}
|
|
188
|
+
/>
|
|
189
|
+
) : null}
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
data-palette-item=""
|
|
193
|
+
disabled={reason !== null}
|
|
194
|
+
title={reason ?? undefined}
|
|
195
|
+
tabIndex={index === active ? 0 : -1}
|
|
196
|
+
onClick={() => {
|
|
197
|
+
setActive(index);
|
|
198
|
+
add(kind);
|
|
199
|
+
}}
|
|
200
|
+
// `h-9` is the trigger's height: sharing one row only reads as one row if the entries
|
|
201
|
+
// start on the trigger's top edge instead of floating in the middle of it.
|
|
202
|
+
className="inline-flex h-9 items-center gap-1.5 rounded-md px-2 text-xs outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent"
|
|
203
|
+
>
|
|
204
|
+
<Icon aria-hidden="true" className="size-3.5" />
|
|
205
|
+
{i18n.t(`flows.node.${kind}`)}
|
|
206
|
+
</button>
|
|
207
|
+
</Fragment>
|
|
208
|
+
);
|
|
209
|
+
})}
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FlowNode } from "@contract/flow.ts";
|
|
2
|
+
|
|
3
|
+
export type PaletteKind = FlowNode["kind"];
|
|
4
|
+
|
|
5
|
+
// Generic over the kinds it is given: the caller decides which subset the bar offers, and `add` is
|
|
6
|
+
// handed back exactly that subset rather than "any node kind" it would have to re-narrow.
|
|
7
|
+
export interface NodePaletteProps<K extends PaletteKind = PaletteKind> {
|
|
8
|
+
kinds: readonly K[];
|
|
9
|
+
open: boolean;
|
|
10
|
+
setOpen(open: boolean): void;
|
|
11
|
+
add(kind: K): void;
|
|
12
|
+
// Why an entry is off, in words, or `null` when it is not. One function rather than a flag and a
|
|
13
|
+
// string: an entry that is greyed out without a reason is just a broken button.
|
|
14
|
+
disabledReason?(kind: K): string | null;
|
|
15
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { useQueries, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
3
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
4
|
+
import { effectiveAccessQuery, ResourceAccessSummary } from "@/access-summary/access-summary.tsx";
|
|
5
|
+
import { treeLevelKey } from "@/app/tree-move/tree-move.tsx";
|
|
6
|
+
import { Skeleton } from "@/components/ui/skeleton.tsx";
|
|
7
|
+
import {
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableHeader,
|
|
13
|
+
TableRow,
|
|
14
|
+
} from "@/components/ui/table.tsx";
|
|
15
|
+
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
16
|
+
import { folderContentsOf } from "@/folder-index/folder-index.ts";
|
|
17
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
18
|
+
import { iconFor } from "@/kind-icon.ts";
|
|
19
|
+
import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
20
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
21
|
+
import { RelativeTime } from "@/time/relative-time.tsx";
|
|
22
|
+
import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
23
|
+
|
|
24
|
+
// The same record the menu is handed everywhere else, read off a level row. A re-labelling, never a
|
|
25
|
+
// second source of truth — a rename works on the whole record, not on a summary of it.
|
|
26
|
+
function targetOf(entry: TreeEntry): ResourceTarget {
|
|
27
|
+
return entry.type === "flow"
|
|
28
|
+
? { type: "flow", flow: entry.flow }
|
|
29
|
+
: { type: "node", node: entry.node };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whose access a level row shows (#817).
|
|
34
|
+
*
|
|
35
|
+
* The function exists because two places need the same answer — the cell that draws the summary,
|
|
36
|
+
* and the table that waits for every one of them before it draws itself. Two expressions would
|
|
37
|
+
* share nothing but their author's intention, and one of them would eventually watch a key the
|
|
38
|
+
* other never asks for.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ For a FLOW row it answers with the folder the flow lies in, and that is carried over from
|
|
41
|
+
* before this ticket rather than decided in it. It is not the whole truth any more: since D51
|
|
42
|
+
* (`anchrd/intel#530`, amendment to ADR-0004) a flow carries grants of its own, and
|
|
43
|
+
* `listEffectiveFlowAccess` is the call that would name them. Changing what this column shows is a
|
|
44
|
+
* different question from making the table load in one piece, so it has a ticket of its own (#844)
|
|
45
|
+
* rather than a quiet fix in the middle of this one.
|
|
46
|
+
*/
|
|
47
|
+
function accessResourceId(entry: TreeEntry, folderId: string): string {
|
|
48
|
+
return entry.type === "flow" ? (entry.flow.parentId ?? folderId) : entry.id;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The placeholder rows, and how many there are (#817).
|
|
53
|
+
*
|
|
54
|
+
* The count IS this array's length — a separate constant beside it is joined to the widths by
|
|
55
|
+
* nothing but agreement, and the row that outlives the agreement has no width. The bars shrink from
|
|
56
|
+
* row to row so the block does not read as one grey rectangle. Same three widths, same `h-4` bar and
|
|
57
|
+
* same one-skeleton-per-column shape as the Gate half (`anchrd/gate#440`), because the two tables
|
|
58
|
+
* are supposed to look alike.
|
|
59
|
+
*/
|
|
60
|
+
const skeletonRowWidths = ["w-full", "w-5/6", "w-2/3"];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* How long the table holds itself back for the access cells (#817).
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ A ceiling, not a timeout: nothing is cancelled when it passes. The table simply stops waiting
|
|
66
|
+
* and draws itself with whatever the access cells have — which for an undecided row is an empty
|
|
67
|
+
* cell that fills itself the moment its answer lands. Without the ceiling one slow row would keep a
|
|
68
|
+
* whole folder off the screen, and the reader would be worse off than with the sentence this
|
|
69
|
+
* replaced.
|
|
70
|
+
*/
|
|
71
|
+
export const accessWaitMs = 1500;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether the table may draw itself yet (#817).
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ DECIDED, not answered. The access queries run with `retry: false`, so a refusal is a final
|
|
77
|
+
* state — waiting for `success` would leave a whole folder off the screen forever because one row
|
|
78
|
+
* may not be inspected. `status !== "pending"` is both outcomes, and that word is the trap this
|
|
79
|
+
* ticket is about.
|
|
80
|
+
*
|
|
81
|
+
* ⚠️ It is a function of its own because a rendered table cannot tell the two apart. A build that
|
|
82
|
+
* waited for `success` looks identical on screen — the ceiling below rescues it a second and a half
|
|
83
|
+
* later and every cell ends up in the same place. Only the CLOCK differs, and a clock is what a
|
|
84
|
+
* test under load may not measure. Here the difference is a value, and a value can be asserted.
|
|
85
|
+
*/
|
|
86
|
+
export function tableIsReady(
|
|
87
|
+
accessStatuses: Array<"pending" | "error" | "success">,
|
|
88
|
+
ceilingPassed: boolean,
|
|
89
|
+
): boolean {
|
|
90
|
+
return ceilingPassed || accessStatuses.every((status) => status !== "pending");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A folder's own screen: what lies directly in it, as a table.
|
|
95
|
+
*
|
|
96
|
+
* ⚠️ It reads the SAME query the sidebar reads for the same folder — `treeLevelKey`, filled by
|
|
97
|
+
* `listTreeChildren`. Not a second call and not a second cache entry: two readers of one folder that
|
|
98
|
+
* fetch separately show it differently the moment one of them is stale, and that is the kind of
|
|
99
|
+
* difference a customer finds before anybody here does. The other half of the bargain is free —
|
|
100
|
+
* every invalidation the tree already does after a create, a move or an archive lands here too, and
|
|
101
|
+
* that now includes the ones this screen's own menu makes: since #519 they all refresh every tree
|
|
102
|
+
* level (`allTreeLevelsKey`), and this level is one of them.
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ There is no "Kind" column since #101. The icon says what a row is, in the same picture the
|
|
105
|
+
* sidebar draws two centimetres to its left, and `sr-only` says it in words for everyone who does
|
|
106
|
+
* not read pictures. A column repeating the icon in prose costs the width the title needs.
|
|
107
|
+
*
|
|
108
|
+
* ⚠️ The table appears as a whole (#817). Every row used to ask for its own effective access and
|
|
109
|
+
* draw nothing until the answer arrived, so six rows were six answers trickling in one after
|
|
110
|
+
* another. The waiting is done here instead, over the very same query keys, so nothing is asked
|
|
111
|
+
* twice — `useQueries` and the cells below share one cache entry per resource.
|
|
112
|
+
*/
|
|
113
|
+
export function FolderContents({ folderId }: { folderId: string }) {
|
|
114
|
+
const { data } = useIntelRouterContext();
|
|
115
|
+
const i18n = useI18n();
|
|
116
|
+
const navigate = useNavigate();
|
|
117
|
+
const level = useQuery({
|
|
118
|
+
queryKey: treeLevelKey(folderId),
|
|
119
|
+
queryFn: () => data.listTreeChildren(folderId),
|
|
120
|
+
});
|
|
121
|
+
// The folder's own text is not one of its contents (#824) — the same reading the tree does, from
|
|
122
|
+
// the same function, because two answers to "what is in this folder" is how they come to differ.
|
|
123
|
+
// ⚠️ It has to happen HERE, above the access queries: the folder text is not a row, so it must
|
|
124
|
+
// not be one of the answers the table waits for either.
|
|
125
|
+
const entries = folderContentsOf(folderId, level.data ?? []);
|
|
126
|
+
// ⚠️ The same options object the cells use, from the same factory. The point is the shared cache
|
|
127
|
+
// entry: this starts the requests while the placeholders are on screen, and the cell that renders
|
|
128
|
+
// afterwards reads the answer rather than asking again.
|
|
129
|
+
const access = useQueries({
|
|
130
|
+
queries: entries.map((entry) =>
|
|
131
|
+
effectiveAccessQuery(data, {
|
|
132
|
+
resourceId: accessResourceId(entry, folderId),
|
|
133
|
+
kind: "node",
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
// ⚠️ Both pieces of state below remember WHICH folder they are about rather than a bare `true`,
|
|
138
|
+
// so opening the next folder invalidates them by itself. A boolean would need a reset beside it,
|
|
139
|
+
// and a reset that runs one commit late shows the new folder through the old folder's decision.
|
|
140
|
+
const entryIds = entries.map((entry) => entry.id).join(",");
|
|
141
|
+
|
|
142
|
+
// The clock starts when the rows are known, not when the screen mounts: otherwise a slow folder
|
|
143
|
+
// listing would spend the access budget before a single access query had been sent.
|
|
144
|
+
const [ceilingFor, setCeilingFor] = useState("");
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (entryIds === "") return;
|
|
147
|
+
const timer = setTimeout(() => setCeilingFor(entryIds), accessWaitMs);
|
|
148
|
+
return () => clearTimeout(timer);
|
|
149
|
+
}, [entryIds]);
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* ⚠️ Once the table is up it stays up, and that is not a nicety (#817).
|
|
153
|
+
*
|
|
154
|
+
* Waiting for every row is what the FIRST appearance is about. Afterwards it would be the very
|
|
155
|
+
* thing this ticket is against: the access queries go pending again whenever the effective-grants
|
|
156
|
+
* prefix is invalidated, which a single share does to every summary on screen, and an added row
|
|
157
|
+
* puts one pending query beside four settled ones. Either way the whole folder would blink back
|
|
158
|
+
* to placeholders — a table that disappears as a whole, which is worse than the trickle it
|
|
159
|
+
* replaced, because the rows that were readable a moment ago are gone too.
|
|
160
|
+
*
|
|
161
|
+
* ⚠️ It is keyed on the FOLDER, not on the row set. Keyed on the rows it dissolves the moment
|
|
162
|
+
* somebody creates a document in the open folder, which is exactly the case it exists for; the
|
|
163
|
+
* new row then arrives with an empty access cell that fills itself, and nothing else moves.
|
|
164
|
+
*/
|
|
165
|
+
const [revealedFor, setRevealedFor] = useState("");
|
|
166
|
+
const ready =
|
|
167
|
+
revealedFor === folderId ||
|
|
168
|
+
tableIsReady(
|
|
169
|
+
access.map((query) => query.status),
|
|
170
|
+
ceilingFor === entryIds,
|
|
171
|
+
);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (ready && entryIds !== "") setRevealedFor(folderId);
|
|
174
|
+
}, [ready, entryIds, folderId]);
|
|
175
|
+
|
|
176
|
+
if (level.isError) {
|
|
177
|
+
return (
|
|
178
|
+
<p role="alert" className="p-6 text-sm text-destructive">
|
|
179
|
+
{i18n.t("node.folderFailed")}
|
|
180
|
+
</p>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
// ⚠️ An empty folder keeps a sentence, unlike an empty row in the tree (#59). A folder IS this
|
|
184
|
+
// screen: a blank rectangle leaves someone who just created it with nowhere to go, which is the
|
|
185
|
+
// same reason `tree.empty` survives at the root.
|
|
186
|
+
if (!level.isPending && entries.length === 0) {
|
|
187
|
+
return (
|
|
188
|
+
<div className="grid flex-1 place-items-center p-8 text-sm text-muted-foreground">
|
|
189
|
+
{i18n.t("node.folderEmpty")}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (level.isPending || !ready) {
|
|
195
|
+
return (
|
|
196
|
+
<LevelTable i18n={i18n}>
|
|
197
|
+
{/* The word the sentence used to carry, kept for the readers a pulsing rectangle says
|
|
198
|
+
nothing to. The placeholder rows themselves are hidden from them: they are a picture of
|
|
199
|
+
a table, and an accessibility tree that lists twelve empty cells is worse than silence. */}
|
|
200
|
+
<TableRow className="hover:bg-transparent">
|
|
201
|
+
<TableCell colSpan={4} className="h-0 p-0">
|
|
202
|
+
<span role="status" className="sr-only">
|
|
203
|
+
{i18n.t("common.loading")}
|
|
204
|
+
</span>
|
|
205
|
+
</TableCell>
|
|
206
|
+
</TableRow>
|
|
207
|
+
{skeletonRowWidths.map((width) => (
|
|
208
|
+
<TableRow key={width} aria-hidden="true" className="hover:bg-transparent">
|
|
209
|
+
<TableCell className="px-4 py-2.5">
|
|
210
|
+
<Skeleton className={`h-4 ${width}`} />
|
|
211
|
+
</TableCell>
|
|
212
|
+
<TableCell className="px-4">
|
|
213
|
+
<Skeleton className={`h-4 ${width}`} />
|
|
214
|
+
</TableCell>
|
|
215
|
+
<TableCell className="px-4">
|
|
216
|
+
<Skeleton className={`h-4 ${width}`} />
|
|
217
|
+
</TableCell>
|
|
218
|
+
<TableCell className="w-14 px-3">
|
|
219
|
+
<Skeleton className="h-4 w-4" />
|
|
220
|
+
</TableCell>
|
|
221
|
+
</TableRow>
|
|
222
|
+
))}
|
|
223
|
+
</LevelTable>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<LevelTable i18n={i18n}>
|
|
229
|
+
{entries.map((entry) => {
|
|
230
|
+
const changed = entry.type === "flow" ? entry.flow.updatedAt : entry.node.updatedAt;
|
|
231
|
+
const Icon = iconFor(entry);
|
|
232
|
+
return (
|
|
233
|
+
<TableRow key={entry.id} className="group/row hover:bg-muted">
|
|
234
|
+
{/* ⚠️ The button carries the whole cell rather than the cell carrying an onClick.
|
|
235
|
+
A row that only reacts to a mouse is a row nobody can reach with a keyboard, and
|
|
236
|
+
a `<tr onClick>` has no name to read out either. */}
|
|
237
|
+
<TableCell className="p-0">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() =>
|
|
241
|
+
void navigate({
|
|
242
|
+
to: entry.type === "flow" ? "/flows" : "/nodes",
|
|
243
|
+
search: { select: entry.id },
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
className="flex w-full min-w-0 items-center gap-3 px-4 py-2.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
|
|
247
|
+
>
|
|
248
|
+
{/* The icon gets a tile of its own so five kinds under one another read as a
|
|
249
|
+
column, instead of five loose glyphs at five different widths. */}
|
|
250
|
+
<span
|
|
251
|
+
aria-hidden="true"
|
|
252
|
+
className="grid size-8 shrink-0 place-items-center rounded-md bg-muted/50 text-muted-foreground transition-colors group-hover/row:bg-accent group-hover/row:text-accent-foreground"
|
|
253
|
+
>
|
|
254
|
+
<Icon className="size-4" />
|
|
255
|
+
</span>
|
|
256
|
+
<span className="min-w-0 truncate font-medium group-hover/row:underline">
|
|
257
|
+
{entry.title}
|
|
258
|
+
</span>
|
|
259
|
+
{/* The icon carries the type on screen; this carries it everywhere else, and
|
|
260
|
+
it is why a flow needs no suffix in its name to be told from a document. */}
|
|
261
|
+
<span className="sr-only">{i18n.t(`node.kind.${entry.kind}`)}</span>
|
|
262
|
+
</button>
|
|
263
|
+
</TableCell>
|
|
264
|
+
<TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
|
|
265
|
+
<RelativeTime value={changed} />
|
|
266
|
+
</TableCell>
|
|
267
|
+
<TableCell className="px-4">
|
|
268
|
+
<ResourceAccessSummary
|
|
269
|
+
resourceId={accessResourceId(entry, folderId)}
|
|
270
|
+
ownerId={entry.type === "flow" ? entry.flow.ownerId : entry.node.ownerId}
|
|
271
|
+
/>
|
|
272
|
+
</TableCell>
|
|
273
|
+
{/* ⚠️ The same menu as the title line of the open thing, not a shorter one built
|
|
274
|
+
for here. #58 collected every action in one place so that place is always the
|
|
275
|
+
same; a folder listing offering three of the six would be a second place. */}
|
|
276
|
+
<TableCell className="px-3 text-right">
|
|
277
|
+
<ResourceMenu target={targetOf(entry)} variant="row" />
|
|
278
|
+
</TableCell>
|
|
279
|
+
</TableRow>
|
|
280
|
+
);
|
|
281
|
+
})}
|
|
282
|
+
</LevelTable>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The chrome both states are drawn in (#817).
|
|
288
|
+
*
|
|
289
|
+
* ⚠️ One shell rather than two, and that is what keeps the columns from jumping: the header row
|
|
290
|
+
* governs the widths and it is the SAME header in both states, over a body whose cell count matches.
|
|
291
|
+
* Two tables that merely look alike would be two places to change a column in.
|
|
292
|
+
*/
|
|
293
|
+
function LevelTable({ i18n, children }: { i18n: ReturnType<typeof useI18n>; children: ReactNode }) {
|
|
294
|
+
return (
|
|
295
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-y-auto p-6">
|
|
296
|
+
<div className="overflow-hidden rounded-lg border bg-card">
|
|
297
|
+
<Table>
|
|
298
|
+
<TableHeader className="[&_tr]:bg-muted/40">
|
|
299
|
+
<TableRow className="hover:bg-transparent">
|
|
300
|
+
<TableHead className="h-9 px-4 text-xs tracking-wide uppercase">
|
|
301
|
+
{i18n.t("common.title")}
|
|
302
|
+
</TableHead>
|
|
303
|
+
<TableHead className="h-9 px-4 text-right text-xs tracking-wide uppercase">
|
|
304
|
+
{i18n.t("node.changed")}
|
|
305
|
+
</TableHead>
|
|
306
|
+
<TableHead className="h-9 px-4 text-xs tracking-wide uppercase">
|
|
307
|
+
{i18n.t("node.access")}
|
|
308
|
+
</TableHead>
|
|
309
|
+
{/* A `<th>` with nothing in it leaves the column unnamed for anyone reading the table
|
|
310
|
+
by its headers. The word is there; only the eye is spared it. */}
|
|
311
|
+
<TableHead className="h-9 w-14">
|
|
312
|
+
<span className="sr-only">{i18n.t("node.rowActions")}</span>
|
|
313
|
+
</TableHead>
|
|
314
|
+
</TableRow>
|
|
315
|
+
</TableHeader>
|
|
316
|
+
<TableBody>{children}</TableBody>
|
|
317
|
+
</Table>
|
|
318
|
+
</div>
|
|
319
|
+
</TitleRowScrollArea>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isFolderIndexTitle } from "@contract/node.ts";
|
|
2
|
+
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Which row of a folder is the folder's own text, and which rows are its contents (#824).
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ **The split happens here and not in the data provider.** The provider answers what the server
|
|
8
|
+
* said, and the server hides nothing — over MCP an `index` is a document like any other, which is
|
|
9
|
+
* the point of doing this in the view. A provider that filtered would make the browser's cache a
|
|
10
|
+
* second, quieter truth about the tree, and the folder's own details panel — which needs the very
|
|
11
|
+
* row the tree drops — would have to ask again for what it already had.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **Only a document**, never a folder, a table or a board of that name. What is being served is
|
|
14
|
+
* a convention about TEXT, and a folder called `index` disappearing into its parent's details is
|
|
15
|
+
* the same word meaning two things.
|
|
16
|
+
*/
|
|
17
|
+
export function folderIndexOf(
|
|
18
|
+
parentId: string | null,
|
|
19
|
+
entries: readonly TreeEntry[],
|
|
20
|
+
): TreeEntry | null {
|
|
21
|
+
/**
|
|
22
|
+
* ⚠️ **The top level is not a folder, and the convention does not reach it** (found in review of
|
|
23
|
+
* #843). A folder shows its own text in its details; the root has no node, no details panel and
|
|
24
|
+
* no menu — so a document called `index` up there would lose its only row and gain nothing in
|
|
25
|
+
* exchange, reachable afterwards through the search or a link somebody already had. The rule
|
|
26
|
+
* lives here rather than in the two callers, because a rule that has to be remembered twice is
|
|
27
|
+
* one that will be remembered once.
|
|
28
|
+
*/
|
|
29
|
+
if (parentId === null) return null;
|
|
30
|
+
const candidates = entries.filter(
|
|
31
|
+
(entry) =>
|
|
32
|
+
entry.type === "node" && entry.kind === "document" && isFolderIndexTitle(entry.title),
|
|
33
|
+
);
|
|
34
|
+
/**
|
|
35
|
+
* ⚠️ **Two documents of that name can exist, and exactly ONE of them becomes the folder's text:
|
|
36
|
+
* the oldest.** The ids are ULIDs, so the smallest is the one written first — the one whose
|
|
37
|
+
* author meant the convention, before anybody added a second.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ And the others STAY IN THE TREE. That is the answer to "then one of them would be
|
|
40
|
+
* invisible": a row that no list draws and no menu reaches is a document nobody can move,
|
|
41
|
+
* archive or even find, and nothing in the folder would say it exists. Two rows called `index`,
|
|
42
|
+
* one of them the folder's text and one of them an ordinary document, is a state somebody can
|
|
43
|
+
* see and put right.
|
|
44
|
+
*/
|
|
45
|
+
return candidates.sort((left, right) => left.id.localeCompare(right.id))[0] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Everything a folder shows as its contents: its rows, minus the one that is its own text. */
|
|
49
|
+
export function folderContentsOf(
|
|
50
|
+
parentId: string | null,
|
|
51
|
+
entries: readonly TreeEntry[],
|
|
52
|
+
): TreeEntry[] {
|
|
53
|
+
const index = folderIndexOf(parentId, entries);
|
|
54
|
+
return index === null ? [...entries] : entries.filter((entry) => entry.id !== index.id);
|
|
55
|
+
}
|