@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,545 @@
|
|
|
1
|
+
import type { Flow } from "@contract/flow.ts";
|
|
2
|
+
import type { Node } from "@contract/node.ts";
|
|
3
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Link } from "@tanstack/react-router";
|
|
5
|
+
import { ChevronRight } from "lucide-react";
|
|
6
|
+
import { type ReactNode, useId, useLayoutEffect, useRef, useState } from "react";
|
|
7
|
+
import { allTreeLevelsKey, treeLevelKey } from "@/app/tree-move/tree-move.tsx";
|
|
8
|
+
import { attachmentPreviewKind, formatFileSize } from "@/attachment-detail/attachment-detail.ts";
|
|
9
|
+
import { boardKey } from "@/board/board-data/board-data.ts";
|
|
10
|
+
import { folderContentsOf, folderIndexOf } from "@/folder-index/folder-index.ts";
|
|
11
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
12
|
+
import { cn } from "@/lib/utils";
|
|
13
|
+
import { NodeEditor } from "@/node-editor/node-editor.tsx";
|
|
14
|
+
import { resourceErrorKey } from "@/resource-error.ts";
|
|
15
|
+
import type { ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
16
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
17
|
+
import { useDateTime } from "@/time/time-context.tsx";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The details of the open thing: its description, and the facts that belong to its kind (#819).
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ **One field for every kind, and that is the whole point of the ticket.** A file had one since
|
|
23
|
+
* #470 and nothing else did, so the description of everything else stood as grey running text under
|
|
24
|
+
* the title. In a tree where everything is a node, a details field that knows one kind is the
|
|
25
|
+
* exception and one for all of them is the rule.
|
|
26
|
+
*
|
|
27
|
+
* ⚠️ **The panel is MOUNTED only while it is open**, never merely hidden. Every kind answers its
|
|
28
|
+
* facts from a query, and a hidden panel would ask for them on every screen the reader passes
|
|
29
|
+
* through: opening a folder would read its contents twice, once for the tree and once for a field
|
|
30
|
+
* nobody looked at. Mounting is also what keeps the queries free of an `enabled` flag per kind.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ **The fold is local state and never a stored field.** It is how the thing is being looked at,
|
|
33
|
+
* not what it says (the same line the head block draws in `frontmatter.tsx`, #657).
|
|
34
|
+
*/
|
|
35
|
+
export function NodeDetails({ target }: { target: ResourceTarget }) {
|
|
36
|
+
const i18n = useI18n();
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const panelId = useId();
|
|
39
|
+
return (
|
|
40
|
+
<div className="mt-1 min-w-0">
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
aria-expanded={open}
|
|
44
|
+
aria-controls={panelId}
|
|
45
|
+
onClick={() => setOpen((current) => !current)}
|
|
46
|
+
className="-ml-1 inline-flex items-center gap-1 rounded-md px-1 py-0.5 text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
47
|
+
>
|
|
48
|
+
<ChevronRight
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
className={cn("size-3.5 transition-transform", open && "rotate-90")}
|
|
51
|
+
/>
|
|
52
|
+
{i18n.t("details.title")}
|
|
53
|
+
</button>
|
|
54
|
+
{open ? (
|
|
55
|
+
<div id={panelId} className="mt-2 max-w-2xl space-y-3 rounded-md bg-muted/40 px-3 py-3">
|
|
56
|
+
<DescriptionField target={target} />
|
|
57
|
+
<Facts target={target} />
|
|
58
|
+
{target.type === "node" && target.node.kind === "folder" ? (
|
|
59
|
+
<FolderText folder={target.node} />
|
|
60
|
+
) : null}
|
|
61
|
+
</div>
|
|
62
|
+
) : null}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The description, readable and changeable in the same place.
|
|
69
|
+
*
|
|
70
|
+
* ⚠️ **`baseUpdatedAt` is not decoration**: the description takes the same conflict route as a
|
|
71
|
+
* rename, or two people writing at once end with one of them silently overwritten
|
|
72
|
+
* (`resource-menu.tsx`). The refusal is worded through `resourceErrorKey`, so a conflict says
|
|
73
|
+
* "somebody else changed it" rather than the general failure sentence.
|
|
74
|
+
*/
|
|
75
|
+
function DescriptionField({ target }: { target: ResourceTarget }) {
|
|
76
|
+
const i18n = useI18n();
|
|
77
|
+
const { data } = useIntelRouterContext();
|
|
78
|
+
const queryClient = useQueryClient();
|
|
79
|
+
const description = target.type === "flow" ? target.flow.description : target.node.description;
|
|
80
|
+
const [editing, setEditing] = useState(false);
|
|
81
|
+
const [draft, setDraft] = useState(description ?? "");
|
|
82
|
+
const fieldId = useId();
|
|
83
|
+
|
|
84
|
+
const save = useMutation({
|
|
85
|
+
mutationFn: async (next: string) => {
|
|
86
|
+
// An empty field means "there is none", which the contract spells `null` rather than "".
|
|
87
|
+
const value = next.trim().length === 0 ? null : next.trim();
|
|
88
|
+
if (target.type === "flow") {
|
|
89
|
+
await data.updateFlow({
|
|
90
|
+
flowId: target.flow.id,
|
|
91
|
+
baseUpdatedAt: target.flow.updatedAt,
|
|
92
|
+
description: value,
|
|
93
|
+
idempotencyKey: crypto.randomUUID(),
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await data.updateNode({
|
|
98
|
+
nodeId: target.node.id,
|
|
99
|
+
baseUpdatedAt: target.node.updatedAt,
|
|
100
|
+
description: value,
|
|
101
|
+
idempotencyKey: crypto.randomUUID(),
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
onSuccess: async () => {
|
|
105
|
+
setEditing(false);
|
|
106
|
+
const id = target.type === "flow" ? target.flow.id : target.node.id;
|
|
107
|
+
await Promise.all([
|
|
108
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
109
|
+
queryClient.invalidateQueries({
|
|
110
|
+
queryKey: target.type === "flow" ? ["flow", id] : ["node", id],
|
|
111
|
+
}),
|
|
112
|
+
]);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (editing) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="space-y-2">
|
|
119
|
+
<label htmlFor={fieldId} className="block text-sm font-medium">
|
|
120
|
+
{i18n.t("details.description")}
|
|
121
|
+
</label>
|
|
122
|
+
<textarea
|
|
123
|
+
id={fieldId}
|
|
124
|
+
rows={4}
|
|
125
|
+
maxLength={2_000}
|
|
126
|
+
value={draft}
|
|
127
|
+
onChange={(event) => setDraft(event.target.value)}
|
|
128
|
+
className="w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
129
|
+
/>
|
|
130
|
+
{save.isError ? (
|
|
131
|
+
<p role="alert" className="text-sm text-destructive">
|
|
132
|
+
{i18n.t(resourceErrorKey(save.error))}
|
|
133
|
+
</p>
|
|
134
|
+
) : null}
|
|
135
|
+
<div className="flex gap-2">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
disabled={save.isPending}
|
|
139
|
+
onClick={() => save.mutate(draft)}
|
|
140
|
+
className="rounded-md border px-3 py-1 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
141
|
+
>
|
|
142
|
+
{i18n.t("common.save")}
|
|
143
|
+
</button>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
onClick={() => {
|
|
147
|
+
setDraft(description ?? "");
|
|
148
|
+
save.reset();
|
|
149
|
+
setEditing(false);
|
|
150
|
+
}}
|
|
151
|
+
className="rounded-md px-3 py-1 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
152
|
+
>
|
|
153
|
+
{i18n.t("common.cancel")}
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="space-y-1">
|
|
162
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
163
|
+
<span className="text-sm font-medium">{i18n.t("details.description")}</span>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onClick={() => {
|
|
167
|
+
setDraft(description ?? "");
|
|
168
|
+
setEditing(true);
|
|
169
|
+
}}
|
|
170
|
+
className="shrink-0 rounded-md px-1 text-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
171
|
+
>
|
|
172
|
+
{i18n.t("details.descriptionEdit")}
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
{description ? (
|
|
176
|
+
<ClampedText text={description} />
|
|
177
|
+
) : (
|
|
178
|
+
<p className="text-sm text-muted-foreground">{i18n.t("details.descriptionEmpty")}</p>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A few lines by default, then "more".
|
|
186
|
+
*
|
|
187
|
+
* ⚠️ The button appears only when the text is actually cut off, and that is measured rather than
|
|
188
|
+
* guessed from the length: a clamp depends on the width the row happens to have, so a count of
|
|
189
|
+
* characters would offer "more" over a text that is fully visible and withhold it from one that is
|
|
190
|
+
* not. The observer covers the same case one resize later.
|
|
191
|
+
*/
|
|
192
|
+
function ClampedText({ text }: { text: string }) {
|
|
193
|
+
const i18n = useI18n();
|
|
194
|
+
const textRef = useRef<HTMLParagraphElement>(null);
|
|
195
|
+
const textId = useId();
|
|
196
|
+
const [clipped, setClipped] = useState(false);
|
|
197
|
+
const [expanded, setExpanded] = useState(false);
|
|
198
|
+
|
|
199
|
+
useLayoutEffect(() => {
|
|
200
|
+
const element = textRef.current;
|
|
201
|
+
if (!element) return;
|
|
202
|
+
const update = () => {
|
|
203
|
+
// Measured against the CLAMPED box, so the check has to run while the clamp is on.
|
|
204
|
+
if (!expanded) setClipped(element.scrollHeight > element.clientHeight + 1);
|
|
205
|
+
};
|
|
206
|
+
update();
|
|
207
|
+
const observer = new ResizeObserver(update);
|
|
208
|
+
observer.observe(element);
|
|
209
|
+
return () => observer.disconnect();
|
|
210
|
+
}, [expanded]);
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div className="min-w-0 text-sm">
|
|
214
|
+
<p
|
|
215
|
+
ref={textRef}
|
|
216
|
+
id={textId}
|
|
217
|
+
data-description=""
|
|
218
|
+
className={cn("whitespace-pre-wrap break-words", !expanded && "line-clamp-3")}
|
|
219
|
+
>
|
|
220
|
+
{text}
|
|
221
|
+
</p>
|
|
222
|
+
{clipped ? (
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
// ⚠️ The button says WHICH text it unfolds, not merely that something is expanded. The
|
|
226
|
+
// field can hold several paragraphs of its own, and `aria-expanded` alone leaves a reader
|
|
227
|
+
// to guess which one moved.
|
|
228
|
+
aria-controls={textId}
|
|
229
|
+
aria-expanded={expanded}
|
|
230
|
+
onClick={() => setExpanded((current) => !current)}
|
|
231
|
+
className="mt-1 rounded-md font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
232
|
+
>
|
|
233
|
+
{i18n.t(expanded ? "title.descriptionLess" : "title.descriptionMore")}
|
|
234
|
+
</button>
|
|
235
|
+
) : null}
|
|
236
|
+
</div>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function Rows({ rows, pending }: { rows: [string, ReactNode][]; pending?: boolean }) {
|
|
241
|
+
const i18n = useI18n();
|
|
242
|
+
if (pending) return <p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>;
|
|
243
|
+
if (rows.length === 0) return null;
|
|
244
|
+
return (
|
|
245
|
+
<dl className="grid grid-cols-[max-content_minmax(0,1fr)] gap-x-4 gap-y-1 text-sm">
|
|
246
|
+
{rows.map(([label, value]) => (
|
|
247
|
+
<div key={label} className="contents">
|
|
248
|
+
<dt className="text-muted-foreground">{label}</dt>
|
|
249
|
+
<dd className="break-words">{value}</dd>
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</dl>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The facts that belong to the KIND, and only those.
|
|
258
|
+
*
|
|
259
|
+
* ⚠️ **A size in megabytes does not appear on a document** (#819). A document has a byte count like
|
|
260
|
+
* everything else in R2, and printing it would answer a question nobody asked with a number nobody
|
|
261
|
+
* can act on: what a reader wants to know about a text is how often it was written and when last.
|
|
262
|
+
*/
|
|
263
|
+
function Facts({ target }: { target: ResourceTarget }) {
|
|
264
|
+
if (target.type === "flow") return <FlowFacts flow={target.flow} />;
|
|
265
|
+
const node = target.node;
|
|
266
|
+
switch (node.kind) {
|
|
267
|
+
case "folder":
|
|
268
|
+
return <FolderFacts node={node} />;
|
|
269
|
+
case "attachment":
|
|
270
|
+
return <FileFacts node={node} />;
|
|
271
|
+
case "table":
|
|
272
|
+
return <TableFacts node={node} />;
|
|
273
|
+
case "board":
|
|
274
|
+
return <BoardFacts node={node} />;
|
|
275
|
+
default:
|
|
276
|
+
// A document and a board card are the same thing here: a text with a history.
|
|
277
|
+
return <WrittenFacts node={node} />;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* The folder's own text: the document called `index` that lives in it (#824).
|
|
283
|
+
*
|
|
284
|
+
* ⚠️ **The row is not hidden, it is moved.** The tree drops it from the folder's contents and it
|
|
285
|
+
* appears here instead, in the one place that is about this folder rather than beside the things
|
|
286
|
+
* the folder holds. Nothing about the document changes: it keeps its versions, its full text, its
|
|
287
|
+
* vectors and its own screen — and over MCP it was never hidden at all.
|
|
288
|
+
*
|
|
289
|
+
* ⚠️ **The same query the tree and the facts above already hold**, so a folder whose details are
|
|
290
|
+
* open costs no second read of its level.
|
|
291
|
+
*
|
|
292
|
+
* ⚠️ The document is fetched only once one is there. A folder without an `index` says nothing at
|
|
293
|
+
* all here rather than offering to make one: the convention is served where it exists, and a folder
|
|
294
|
+
* that does not follow it is not being told off.
|
|
295
|
+
*/
|
|
296
|
+
function FolderText({ folder }: { folder: Node }) {
|
|
297
|
+
const i18n = useI18n();
|
|
298
|
+
const { data } = useIntelRouterContext();
|
|
299
|
+
const queryClient = useQueryClient();
|
|
300
|
+
const level = useQuery({
|
|
301
|
+
queryKey: treeLevelKey(folder.id),
|
|
302
|
+
queryFn: () => data.listTreeChildren(folder.id),
|
|
303
|
+
});
|
|
304
|
+
const index = folderIndexOf(folder.id, level.data ?? []);
|
|
305
|
+
const document = useQuery({
|
|
306
|
+
queryKey: ["node", index?.id],
|
|
307
|
+
queryFn: async () => await data.getNode(index?.id ?? ""),
|
|
308
|
+
enabled: index !== null,
|
|
309
|
+
});
|
|
310
|
+
if (index === null) return null;
|
|
311
|
+
return (
|
|
312
|
+
<section className="border-t pt-3">
|
|
313
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
314
|
+
<h3 className="text-sm font-medium">{i18n.t("details.folderText")}</h3>
|
|
315
|
+
{/* ⚠️ The way back, and the reason it is not optional: a row the tree does not draw can
|
|
316
|
+
only be moved, shared or archived from its own screen, and this is the only link left
|
|
317
|
+
to it (#824). */}
|
|
318
|
+
<Link
|
|
319
|
+
to="/nodes"
|
|
320
|
+
search={{ select: index.id }}
|
|
321
|
+
className="rounded-md px-1 py-0.5 text-sm text-muted-foreground underline outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
322
|
+
>
|
|
323
|
+
{i18n.t("details.folderTextOpen")}
|
|
324
|
+
</Link>
|
|
325
|
+
</div>
|
|
326
|
+
{document.isPending ? (
|
|
327
|
+
<p className="text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
328
|
+
) : document.isError || !document.data ? (
|
|
329
|
+
// ⚠️ Its own sentence, not the folder's. The list came back — it is where this document was
|
|
330
|
+
// found — and only the document behind it did not: its access may have changed on its own,
|
|
331
|
+
// or it was archived a moment ago. "This folder could not be read" would send the reader
|
|
332
|
+
// after something that was read perfectly well (found in review of #843).
|
|
333
|
+
<p role="alert" className="text-sm text-destructive">
|
|
334
|
+
{i18n.t("details.folderTextFailed")}
|
|
335
|
+
</p>
|
|
336
|
+
) : (
|
|
337
|
+
/* Readable and writable in the same place, through the editor the document has anywhere
|
|
338
|
+
else — a second, smaller editor here would be a second set of rules about one text. */
|
|
339
|
+
<NodeEditor
|
|
340
|
+
data={data}
|
|
341
|
+
document={document.data}
|
|
342
|
+
i18n={i18n}
|
|
343
|
+
onSaved={(saved) => {
|
|
344
|
+
queryClient.setQueryData(["node", index.id], saved);
|
|
345
|
+
}}
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
</section>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function FolderFacts({ node }: { node: Node }) {
|
|
353
|
+
const i18n = useI18n();
|
|
354
|
+
const { data } = useIntelRouterContext();
|
|
355
|
+
// ⚠️ The key the sidebar already holds for this level, so an open folder costs no second request.
|
|
356
|
+
const level = useQuery({
|
|
357
|
+
queryKey: treeLevelKey(node.id),
|
|
358
|
+
queryFn: () => data.listTreeChildren(node.id),
|
|
359
|
+
});
|
|
360
|
+
// ⚠️ What the folder SHOWS, which since #824 is one row fewer than what it holds: its own text
|
|
361
|
+
// stands below this line rather than among its contents, and a count that included it would
|
|
362
|
+
// disagree with every list on the screen.
|
|
363
|
+
//
|
|
364
|
+
// ⚠️ This is a count for a reader and nothing else. What decides whether a folder may be deleted
|
|
365
|
+
// is asked on the server, over every table that points at the container — knowledge AND flows
|
|
366
|
+
// (`.claude/rules/destructive.md`, anchrd/intel#457) — and nothing here is part of that answer.
|
|
367
|
+
return (
|
|
368
|
+
<Rows
|
|
369
|
+
pending={level.isPending}
|
|
370
|
+
rows={[
|
|
371
|
+
[i18n.t("details.contents"), String(folderContentsOf(node.id, level.data ?? []).length)],
|
|
372
|
+
]}
|
|
373
|
+
/>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function WrittenFacts({ node }: { node: Node }) {
|
|
378
|
+
const i18n = useI18n();
|
|
379
|
+
const dateTime = useDateTime();
|
|
380
|
+
const { data } = useIntelRouterContext();
|
|
381
|
+
const versions = useQuery({
|
|
382
|
+
queryKey: ["node-versions", node.id],
|
|
383
|
+
queryFn: () => data.listNodeVersions(node.id),
|
|
384
|
+
});
|
|
385
|
+
const items = versions.data?.items ?? [];
|
|
386
|
+
// The list arrives newest first (`ORDER BY sequence DESC`), so the head of it is the last write.
|
|
387
|
+
const newest = items[0];
|
|
388
|
+
return (
|
|
389
|
+
<Rows
|
|
390
|
+
pending={versions.isPending}
|
|
391
|
+
rows={
|
|
392
|
+
newest === undefined
|
|
393
|
+
? [[i18n.t("details.versions"), "0"]]
|
|
394
|
+
: [
|
|
395
|
+
[i18n.t("details.versions"), String(items.length)],
|
|
396
|
+
[i18n.t("details.lastWritten"), dateTime.at(newest.createdAt)],
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function TableFacts({ node }: { node: Node }) {
|
|
404
|
+
const i18n = useI18n();
|
|
405
|
+
const { data } = useIntelRouterContext();
|
|
406
|
+
// The key the table panel already uses.
|
|
407
|
+
const table = useQuery({
|
|
408
|
+
queryKey: ["node-table", node.id],
|
|
409
|
+
queryFn: () => data.getNodeTable(node.id),
|
|
410
|
+
});
|
|
411
|
+
return (
|
|
412
|
+
<Rows
|
|
413
|
+
pending={table.isPending}
|
|
414
|
+
rows={[
|
|
415
|
+
[i18n.t("details.columns"), String(table.data?.columns.length ?? 0)],
|
|
416
|
+
[i18n.t("details.rows"), String(table.data?.rows.length ?? 0)],
|
|
417
|
+
]}
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function BoardFacts({ node }: { node: Node }) {
|
|
423
|
+
const i18n = useI18n();
|
|
424
|
+
const { data } = useIntelRouterContext();
|
|
425
|
+
// ⚠️ The UNFILTERED entry, which is the one the board panel writes its answers into
|
|
426
|
+
// (`board-data.ts`). Asking under a filter would count the cards somebody is looking at rather
|
|
427
|
+
// than the cards there are.
|
|
428
|
+
const board = useQuery({
|
|
429
|
+
queryKey: [...boardKey(node.id), {}],
|
|
430
|
+
queryFn: () => data.getBoard({ boardId: node.id, includeArchived: false }),
|
|
431
|
+
});
|
|
432
|
+
return (
|
|
433
|
+
<Rows
|
|
434
|
+
pending={board.isPending}
|
|
435
|
+
rows={[
|
|
436
|
+
[i18n.t("details.columns"), String(board.data?.columns.length ?? 0)],
|
|
437
|
+
[i18n.t("details.cards"), String(board.data?.tasks.length ?? 0)],
|
|
438
|
+
]}
|
|
439
|
+
/>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function FlowFacts({ flow }: { flow: Flow }) {
|
|
444
|
+
const i18n = useI18n();
|
|
445
|
+
const dateTime = useDateTime();
|
|
446
|
+
const { data } = useIntelRouterContext();
|
|
447
|
+
const versions = useQuery({
|
|
448
|
+
queryKey: ["flow-versions", flow.id],
|
|
449
|
+
queryFn: () => data.listFlowVersions(flow.id),
|
|
450
|
+
});
|
|
451
|
+
const items = versions.data?.items ?? [];
|
|
452
|
+
const newest = items[0];
|
|
453
|
+
return (
|
|
454
|
+
<Rows
|
|
455
|
+
pending={versions.isPending}
|
|
456
|
+
rows={
|
|
457
|
+
newest === undefined
|
|
458
|
+
? [[i18n.t("details.versions"), "0"]]
|
|
459
|
+
: [
|
|
460
|
+
[i18n.t("details.versions"), String(items.length)],
|
|
461
|
+
[i18n.t("details.lastWritten"), dateTime.at(newest.createdAt)],
|
|
462
|
+
]
|
|
463
|
+
}
|
|
464
|
+
/>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* A file, with what #470 already showed beside its preview: size, format, and what the format
|
|
470
|
+
* itself can say.
|
|
471
|
+
*
|
|
472
|
+
* ⚠️ **This IS the file panel, moved rather than copied.** The viewer's own `Info` button is gone
|
|
473
|
+
* with #819: two details fields on one kind would be the state the ticket exists to end, and the
|
|
474
|
+
* one that lost would be the one every other kind now has.
|
|
475
|
+
*/
|
|
476
|
+
function FileFacts({ node }: { node: Node }) {
|
|
477
|
+
const i18n = useI18n();
|
|
478
|
+
const dateTime = useDateTime();
|
|
479
|
+
const { data } = useIntelRouterContext();
|
|
480
|
+
const versions = useQuery({
|
|
481
|
+
queryKey: ["node-versions", node.id],
|
|
482
|
+
queryFn: () => data.listNodeVersions(node.id),
|
|
483
|
+
});
|
|
484
|
+
const version = versions.data?.items[0];
|
|
485
|
+
const kind = version ? attachmentPreviewKind(node.title, version.mediaType) : "unsupported";
|
|
486
|
+
// ⚠️ The same key the viewer loads the file under, so an open preview pays for it once. It is a
|
|
487
|
+
// download, which is why it hangs on the panel being open rather than on the screen being shown.
|
|
488
|
+
const file = useQuery({
|
|
489
|
+
queryKey: ["attachment", node.id, version?.id],
|
|
490
|
+
queryFn: () => data.getNodeAttachment(node.id),
|
|
491
|
+
enabled: version !== undefined,
|
|
492
|
+
retry: false,
|
|
493
|
+
});
|
|
494
|
+
const formatDetail = useQuery({
|
|
495
|
+
queryKey: ["attachment-format-detail", node.id, version?.id],
|
|
496
|
+
// ⚠️ Reached through `import()` rather than an import at the top, and that is the second half
|
|
497
|
+
// of the split in `attachment-detail.ts`: this function unpacks the file with `fflate`, and a
|
|
498
|
+
// static import would put that in the main chunk for the sake of one kind. The module is the
|
|
499
|
+
// viewer's own, which is already mounted whenever there is a file to ask about, so the chunk
|
|
500
|
+
// is paid for either way.
|
|
501
|
+
queryFn: async () => {
|
|
502
|
+
if (!file.data || !version) return null;
|
|
503
|
+
const { attachmentFormatDetail } = await import("@/attachment-viewer/attachment-viewer.tsx");
|
|
504
|
+
return await attachmentFormatDetail(
|
|
505
|
+
new File([file.data], node.title, { type: version.mediaType }),
|
|
506
|
+
kind,
|
|
507
|
+
);
|
|
508
|
+
},
|
|
509
|
+
// Neither `audio` nor `video` ever yields a result here (#815, same as `unsupported`) — gated
|
|
510
|
+
// out rather than left to resolve to `null` so a large media file is not read into memory for
|
|
511
|
+
// a question this function never answers for it.
|
|
512
|
+
enabled:
|
|
513
|
+
file.data !== undefined && kind !== "unsupported" && kind !== "audio" && kind !== "video",
|
|
514
|
+
retry: false,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
if (version === undefined) {
|
|
518
|
+
return (
|
|
519
|
+
<Rows pending={versions.isPending} rows={[[i18n.t("attachment.details.size"), "0 B"]]} />
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
const rows: [string, ReactNode][] = [
|
|
523
|
+
[i18n.t("attachment.details.size"), formatFileSize(version.size)],
|
|
524
|
+
[
|
|
525
|
+
i18n.t("attachment.details.format"),
|
|
526
|
+
node.title.split(".").at(-1)?.toUpperCase() ?? i18n.t("attachment.kind.unsupported"),
|
|
527
|
+
],
|
|
528
|
+
[i18n.t("attachment.details.type"), i18n.t(`attachment.kind.${kind}`)],
|
|
529
|
+
[i18n.t("attachment.details.mimeType"), version.mediaType],
|
|
530
|
+
[i18n.t("attachment.details.uploaded"), dateTime.at(version.createdAt)],
|
|
531
|
+
[i18n.t("attachment.details.uploadedBy"), version.createdBy],
|
|
532
|
+
[i18n.t("attachment.details.version"), String(version.sequence)],
|
|
533
|
+
];
|
|
534
|
+
if (formatDetail.data) {
|
|
535
|
+
rows.push([i18n.t(`attachment.details.${formatDetail.data.label}`), formatDetail.data.value]);
|
|
536
|
+
}
|
|
537
|
+
rows.push([i18n.t("attachment.details.storage"), i18n.t("attachment.details.private")]);
|
|
538
|
+
rows.push([
|
|
539
|
+
i18n.t("attachment.details.checksum"),
|
|
540
|
+
<span key="checksum" className="font-mono text-xs">
|
|
541
|
+
{`${version.contentHash.slice(0, 8)}…${version.contentHash.slice(-4)}`}
|
|
542
|
+
</span>,
|
|
543
|
+
]);
|
|
544
|
+
return <Rows pending={versions.isPending} rows={rows} />;
|
|
545
|
+
}
|