@anchrd/intel-ui 0.19.0 → 0.21.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 +33 -0
- package/package.json +2 -2
- package/src/app/app-tree/app-tree.tsx +45 -5
- package/src/app/header-search/header-search.tsx +2 -1
- package/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
- package/src/app/user-footer/user-footer.tsx +19 -0
- package/src/archive/archive.tsx +2 -1
- package/src/data/intel-data-provider/intel-data-provider.ts +43 -29
- package/src/data/intel-data-provider/intel-data-provider.types.ts +40 -24
- package/src/data/query-client.ts +18 -0
- package/src/data/request-refusal/request-refusal.ts +51 -0
- package/src/document-link/document-link.tsx +1 -1
- package/src/entry-picker/entry-picker.tsx +1 -1
- package/src/flow-runs/flow-runs.tsx +1 -1
- package/src/flows/flows.tsx +11 -3
- package/src/flows/node-icon/node-icon.ts +1 -1
- package/src/flows/node-palette/node-palette.tsx +1 -1
- package/src/flows/node-palette/node-palette.types.ts +1 -1
- package/src/graph-pane/graph-pane.tsx +1 -1
- package/src/i18n/de.json +23 -2
- package/src/i18n/en.json +23 -2
- package/src/i18n/es.json +23 -2
- package/src/main.tsx +3 -2
- package/src/node-editor/node-editor.tsx +15 -2
- package/src/node-graph/graph-notice.tsx +1 -1
- package/src/node-graph/node-graph.ts +1 -1
- package/src/node-graph/node-graph.tsx +1 -1
- package/src/node-table/node-table.tsx +3 -3
- package/src/nodes/nodes.tsx +21 -1
- package/src/resource-menu/resource-menu.tsx +206 -45
- package/src/save-button/save-button.tsx +11 -1
- package/src/tools/tools.tsx +147 -44
package/README.md
CHANGED
|
@@ -43,6 +43,39 @@ Point `intel.json` at your files:
|
|
|
43
43
|
Missing assets, non-SVG branding and incomplete language catalogs fail the build rather than
|
|
44
44
|
silently producing a half-branded application.
|
|
45
45
|
|
|
46
|
+
## What an empty screen means
|
|
47
|
+
|
|
48
|
+
Most "the UI is broken" reports are one of four situations, and the application distinguishes them
|
|
49
|
+
rather than showing one blank page for all of them. The full explanation of the two permission
|
|
50
|
+
layers is in the
|
|
51
|
+
[`@anchrd/intel-api` README](https://www.npmjs.com/package/@anchrd/intel-api); what follows is what
|
|
52
|
+
each one looks like here.
|
|
53
|
+
|
|
54
|
+
| On screen | What it is |
|
|
55
|
+
|---|---|
|
|
56
|
+
| "You do not have permission to see this." | The Gate capability is missing — `nodes.read` and friends |
|
|
57
|
+
| An empty tree, no error | Nothing has been shared with this person yet |
|
|
58
|
+
| "This is not available to you. It may not exist, or it may no longer be shared with you." | A node that was reachable and is not any more, or never existed — Intel deliberately does not say which |
|
|
59
|
+
| Skeletons that never resolve | Should no longer happen: a refusal ends the loading state on the first answer, and no button offers a retry that cannot change anything |
|
|
60
|
+
|
|
61
|
+
A node somebody shares appears in the recipient's sidebar on its own, at the top level, without
|
|
62
|
+
naming the folders above it. Nobody needs to be sent a link.
|
|
63
|
+
|
|
64
|
+
The sharing dialog grants to an email address or to the whole organization. It never shows a raw
|
|
65
|
+
user id: where Intel cannot name an account it says so. ⚠️ Choosing **execute for the whole
|
|
66
|
+
organization** on a folder makes that folder a library that flows from anywhere may call into, and
|
|
67
|
+
withdrawing it is refused while a caller remains — the dialog says so before the click.
|
|
68
|
+
|
|
69
|
+
The Tools screen tells four situations apart — refused, expired, broken, and started-but-never-
|
|
70
|
+
answered — and offers a second attempt everywhere a second attempt could change the answer.
|
|
71
|
+
|
|
72
|
+
The editor answers "Nothing to save" rather than "Saved" when the write produced no new version.
|
|
73
|
+
The two are different outcomes and only one of them means the text is safe to walk away from.
|
|
74
|
+
|
|
75
|
+
⚠️ The editor opens **empty** on content it cannot parse. Documents written over HTTP or MCP have to
|
|
76
|
+
carry the BlockNote media type to be editable in the browser; see the
|
|
77
|
+
[`@anchrd/intel-contract` README](https://www.npmjs.com/package/@anchrd/intel-contract).
|
|
78
|
+
|
|
46
79
|
## What is inside
|
|
47
80
|
|
|
48
81
|
Vite, React, TanStack Router and Query, with shadcn components. The node editor uses BlockNote
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@anchrd/intel-contract": "^0.
|
|
36
|
+
"@anchrd/intel-contract": "^0.15.0",
|
|
37
37
|
"@blocknote/core": "^0.52.1",
|
|
38
38
|
"@blocknote/react": "^0.52.1",
|
|
39
39
|
"@blocknote/shadcn": "^0.52.1",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NodeKind } from "@anchrd/intel-contract/node";
|
|
1
2
|
import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
|
|
2
3
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
3
4
|
import {
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
} from "@/components/ui/sidebar";
|
|
35
36
|
import { flowEntry } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
36
37
|
import type { TreeEntry } from "@/data/intel-data-provider/intel-data-provider.types.ts";
|
|
38
|
+
import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
|
|
37
39
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
38
40
|
import { type ImportKind, useBundleImport } from "@/import-bundle/import-bundle.tsx";
|
|
39
41
|
import { kindIcons } from "@/kind-icon.ts";
|
|
@@ -41,7 +43,15 @@ import { Modal } from "@/modal/modal.tsx";
|
|
|
41
43
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
42
44
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
/**
|
|
47
|
+
* What the plus can MAKE — derived from `NodeKind`, never written out again (#395).
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ `attachment` is excluded because an attachment is uploaded rather than created, and `flow` is
|
|
50
|
+
* added because a flow shares the tree without being a node (ADR-0004 §1). Both are decisions about
|
|
51
|
+
* this menu; the LIST of node kinds is not, and a copy of it here goes stale in the direction that
|
|
52
|
+
* already bit us — the parked kinds stood in such a union long after nothing could make one.
|
|
53
|
+
*/
|
|
54
|
+
type NewKind = Exclude<NodeKind, "attachment"> | "flow";
|
|
45
55
|
type Creating = { parentId: string | null; kind: NewKind };
|
|
46
56
|
|
|
47
57
|
// Attachments are inlined as base64, so the browser holds the file twice while it uploads.
|
|
@@ -328,6 +338,19 @@ export function AppTree() {
|
|
|
328
338
|
</SidebarMenu>
|
|
329
339
|
);
|
|
330
340
|
}
|
|
341
|
+
// ⚠️ A refusal, not a failure (#430). While this branch did not exist, a reader whose share had
|
|
342
|
+
// been revoked — or who holds no `intel.nodes.read` at all — watched the three skeleton bars
|
|
343
|
+
// above for as long as the retries lasted, and the retries are paused whenever the tab is not
|
|
344
|
+
// in front. Neither of those two cases is worth trying again, so what stands here is a sentence
|
|
345
|
+
// and no button.
|
|
346
|
+
const refusal = refusalOf(level.error);
|
|
347
|
+
if (refusal) {
|
|
348
|
+
return (
|
|
349
|
+
<p role="status" className="px-2 py-1.5 text-sm text-muted-foreground">
|
|
350
|
+
{i18n.t(refusal === "no-permission" ? "common.noPermission" : "common.noAccess")}
|
|
351
|
+
</p>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
331
354
|
if (level.isError) {
|
|
332
355
|
return (
|
|
333
356
|
<div role="alert" className="space-y-2 px-2 py-1.5 text-sm">
|
|
@@ -367,15 +390,16 @@ export function AppTree() {
|
|
|
367
390
|
}
|
|
368
391
|
return (
|
|
369
392
|
<SidebarMenu aria-label={label}>
|
|
370
|
-
{entries.map((entry) => renderRow(entry,
|
|
393
|
+
{entries.map((entry) => renderRow(entry, parent, ancestors))}
|
|
371
394
|
</SidebarMenu>
|
|
372
395
|
);
|
|
373
396
|
}
|
|
374
397
|
|
|
375
|
-
// ⚠️
|
|
398
|
+
// ⚠️ A level under a FLOW is derived: its rows are that flow's calls, read out of its graph
|
|
376
399
|
// (ADR-0004 §3, `levelKey`). Nothing there has a `parent_id` to rewrite, so those rows are neither
|
|
377
400
|
// dragged nor dropped on — a move made there would silently be a move of the shared flow itself.
|
|
378
|
-
function renderRow(entry: TreeEntry, ancestors: ReadonlySet<string
|
|
401
|
+
function renderRow(entry: TreeEntry, parent: Level, ancestors: ReadonlySet<string>) {
|
|
402
|
+
const derived = parent.type === "flow";
|
|
379
403
|
const isFolder = entry.kind === "folder";
|
|
380
404
|
// ⚠️ A flow expands too, and what appears under it is what it calls — read from its graph, not
|
|
381
405
|
// from `parent_id`. A flow reused by three callers therefore shows up under all three, which is
|
|
@@ -391,7 +415,23 @@ export function AppTree() {
|
|
|
391
415
|
const area = entry.type === "flow" ? "/flows" : "/nodes";
|
|
392
416
|
const isActive = location.select === entry.id && location.pathname === area;
|
|
393
417
|
// A row's plus files into that row's place: inside a folder, beside anything else.
|
|
394
|
-
|
|
418
|
+
//
|
|
419
|
+
// ⚠️ "Beside it" is the level this row is shown IN, not the parent its own record names. Since
|
|
420
|
+
// #429 the root level also carries rows whose `parentId` points at a folder this reader may not
|
|
421
|
+
// see — a shared document is filed inside the sharer's folder, and only the share reaches down
|
|
422
|
+
// to it. Filing beside such a row through that id would try to write into a folder the reader
|
|
423
|
+
// cannot even open, and the refusal would arrive as a bare "could not be created".
|
|
424
|
+
//
|
|
425
|
+
// ⚠️ A derived level is the exception, because it has no folder to be filed in: what a flow
|
|
426
|
+
// calls is read from its graph, so the record's own parent is the only answer available there.
|
|
427
|
+
//
|
|
428
|
+
// ⚠️ It is the only one, not a safe one. A called flow may be a LIBRARY flow — `execute` without
|
|
429
|
+
// `read`, which ADR-0004 §2/§3 allows on purpose — and its folder is then one this reader may
|
|
430
|
+
// not open at all. The plus there can still be refused, exactly as the plus on any read-only
|
|
431
|
+
// folder row elsewhere in the tree has always been able to be. That is the older question of
|
|
432
|
+
// what a plus should do where writing is not allowed, and it is not this ticket's: what #429
|
|
433
|
+
// changed is a plus pointing at a folder that is not even the level being shown.
|
|
434
|
+
const target = isFolder ? entry.id : derived ? parentOf(entry) : parent.id;
|
|
395
435
|
// Only a folder takes a drop, and only on a level that owns its rows. Everything else keeps the
|
|
396
436
|
// default cursor while a drag is in progress, which is the answer "not here" without a word.
|
|
397
437
|
const drop =
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Flow
|
|
1
|
+
import type { Flow } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { ToolCapability } from "@anchrd/intel-contract/tool";
|
|
2
3
|
import { useQuery } from "@tanstack/react-query";
|
|
3
4
|
import { useNavigate } from "@tanstack/react-router";
|
|
4
5
|
import { FileText, Search, Workflow, Wrench } from "lucide-react";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from "@/components/ui/dialog";
|
|
11
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
12
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rebuilding the search index, asked for before it happens (#416).
|
|
16
|
+
*
|
|
17
|
+
* ⚠️ Two clicks, not one, and the reason is not that it is dangerous — nothing is lost, the index is
|
|
18
|
+
* derived from D1 and R2 and can always be built again. It is that it is EXPENSIVE and invisible:
|
|
19
|
+
* every node is read and embedded again, which costs money and minutes, and the screen looks exactly
|
|
20
|
+
* the same afterwards. A one-click button next to "Preferences" invites a second press when the
|
|
21
|
+
* first one seems to have done nothing.
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ It reports what was QUEUED, never "done". The answer comes back the moment the work is handed
|
|
24
|
+
* to the queue, and the index catches up behind it — writing "finished" here would be a sentence
|
|
25
|
+
* about something this screen cannot see. The number is the honest part: it says how much was
|
|
26
|
+
* accepted, so a repeat press can be compared against it.
|
|
27
|
+
*
|
|
28
|
+
* The dialog is opened from outside, like the settings one: its trigger is a `DropdownMenuItem`, and
|
|
29
|
+
* Radix unmounts the menu content in the same frame the item is chosen.
|
|
30
|
+
*/
|
|
31
|
+
interface ReindexDialogProps {
|
|
32
|
+
open: boolean;
|
|
33
|
+
onOpenChange(open: boolean): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ReindexDialog({ open, onOpenChange }: ReindexDialogProps) {
|
|
37
|
+
const i18n = useI18n();
|
|
38
|
+
const { data } = useIntelRouterContext();
|
|
39
|
+
const reindex = useMutation({ mutationFn: () => data.reindexNodes() });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Dialog
|
|
43
|
+
open={open}
|
|
44
|
+
onOpenChange={(next) => {
|
|
45
|
+
// Closing forgets the last answer, so re-opening never shows a count from an earlier run as
|
|
46
|
+
// if it were this one's.
|
|
47
|
+
if (!next) reindex.reset();
|
|
48
|
+
onOpenChange(next);
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<DialogContent className="sm:max-w-md">
|
|
52
|
+
<DialogHeader>
|
|
53
|
+
<DialogTitle>{i18n.t("reindex.title")}</DialogTitle>
|
|
54
|
+
<DialogDescription>{i18n.t("reindex.description")}</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
56
|
+
{reindex.isSuccess ? (
|
|
57
|
+
<p role="status" className="text-sm">
|
|
58
|
+
{i18n.t("reindex.queued", { count: reindex.data.queued })}
|
|
59
|
+
</p>
|
|
60
|
+
) : null}
|
|
61
|
+
{reindex.isError ? (
|
|
62
|
+
<p role="alert" className="text-sm text-destructive">
|
|
63
|
+
{i18n.t("reindex.failed")}
|
|
64
|
+
</p>
|
|
65
|
+
) : null}
|
|
66
|
+
<DialogFooter>
|
|
67
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
68
|
+
{i18n.t(reindex.isSuccess ? "reindex.close" : "reindex.cancel")}
|
|
69
|
+
</Button>
|
|
70
|
+
{reindex.isSuccess ? null : (
|
|
71
|
+
<Button disabled={reindex.isPending} onClick={() => reindex.mutate()}>
|
|
72
|
+
{i18n.t(reindex.isPending ? "reindex.running" : "reindex.confirm")}
|
|
73
|
+
</Button>
|
|
74
|
+
)}
|
|
75
|
+
</DialogFooter>
|
|
76
|
+
</DialogContent>
|
|
77
|
+
</Dialog>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Archive as ArchiveIcon,
|
|
5
5
|
ChevronsUpDown,
|
|
6
6
|
LogOut,
|
|
7
|
+
RefreshCw,
|
|
7
8
|
Settings,
|
|
8
9
|
User,
|
|
9
10
|
Wrench,
|
|
@@ -20,6 +21,7 @@ import { SidebarMenu, SidebarMenuItem } from "@/components/ui/sidebar";
|
|
|
20
21
|
import { loginPath } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
21
22
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
22
23
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
24
|
+
import { ReindexDialog } from "../reindex-dialog/reindex-dialog.tsx";
|
|
23
25
|
import { SettingsDialog } from "../settings-dialog/settings-dialog.tsx";
|
|
24
26
|
|
|
25
27
|
/**
|
|
@@ -41,6 +43,7 @@ export function UserFooter() {
|
|
|
41
43
|
// The state lives here, not in the dialog: its trigger is a DropdownMenuItem, and Radix unmounts
|
|
42
44
|
// the menu content when an item is chosen — a dialog nested in there would go with it.
|
|
43
45
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
46
|
+
const [reindexOpen, setReindexOpen] = useState(false);
|
|
44
47
|
const session = useQuery({ queryKey: ["session"], queryFn: () => data.getSession() });
|
|
45
48
|
const logout = useMutation({
|
|
46
49
|
mutationFn: () => data.logout(),
|
|
@@ -103,6 +106,20 @@ export function UserFooter() {
|
|
|
103
106
|
<Settings aria-hidden="true" />
|
|
104
107
|
{i18n.t("settings.title")}
|
|
105
108
|
</DropdownMenuItem>
|
|
109
|
+
{/* ⚠️ Drawn only for an administrator, and that is a DRAWING decision — `POST
|
|
110
|
+
/nodes/reindex` asks Gate for `intel/admin` itself and would refuse this row's press
|
|
111
|
+
just the same (#416). Hiding it is not the check; it is not offering everybody a
|
|
112
|
+
door they cannot open. Its own separator, because the rows above belong to the
|
|
113
|
+
person and this one belongs to the installation. */}
|
|
114
|
+
{session.data?.isAdmin ? (
|
|
115
|
+
<>
|
|
116
|
+
<DropdownMenuSeparator />
|
|
117
|
+
<DropdownMenuItem onSelect={() => setReindexOpen(true)}>
|
|
118
|
+
<RefreshCw aria-hidden="true" />
|
|
119
|
+
{i18n.t("reindex.title")}
|
|
120
|
+
</DropdownMenuItem>
|
|
121
|
+
</>
|
|
122
|
+
) : null}
|
|
106
123
|
<DropdownMenuSeparator />
|
|
107
124
|
<DropdownMenuItem
|
|
108
125
|
disabled={logout.isPending}
|
|
@@ -116,6 +133,8 @@ export function UserFooter() {
|
|
|
116
133
|
</DropdownMenu>
|
|
117
134
|
{/* ⚠️ Outside the DropdownMenu on purpose — see the state above. */}
|
|
118
135
|
<SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
136
|
+
{/* ⚠️ Outside the DropdownMenu for the same reason as the settings dialog. */}
|
|
137
|
+
<ReindexDialog open={reindexOpen} onOpenChange={setReindexOpen} />
|
|
119
138
|
</SidebarMenuItem>
|
|
120
139
|
{/* ⚠️ Outside the menu on purpose. Choosing sign-out closes the popup, so a refusal rendered
|
|
121
140
|
inside it would be gone in the same frame it was written — the one message that must
|
package/src/archive/archive.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Flow
|
|
1
|
+
import type { Flow } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import type { Node } from "@anchrd/intel-contract/node";
|
|
2
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
4
|
import { ArchiveRestore } from "lucide-react";
|
|
4
5
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
@@ -1,24 +1,33 @@
|
|
|
1
|
+
import { ProblemDetails, SessionUser } from "@anchrd/intel-contract";
|
|
2
|
+
import { BundleImportResult } from "@anchrd/intel-contract/bundle";
|
|
1
3
|
import {
|
|
2
|
-
AppendTableRowsInput,
|
|
3
|
-
AppendTableRowsResult,
|
|
4
4
|
ArchiveFlowInput,
|
|
5
|
-
ArchiveNodeInput,
|
|
6
|
-
BundleImportResult,
|
|
7
|
-
CompleteFlowRunStepInput,
|
|
8
5
|
CreateFlowInput,
|
|
9
|
-
CreateNodeInput,
|
|
10
|
-
DefineTableInput,
|
|
11
6
|
Flow,
|
|
12
7
|
FlowDocument,
|
|
13
8
|
FlowList,
|
|
14
9
|
FlowPublishPreview,
|
|
15
10
|
FlowRequirements,
|
|
11
|
+
FlowValidation,
|
|
12
|
+
ListFlowsInput,
|
|
13
|
+
PreviewFlowPublishInput,
|
|
14
|
+
PublishFlowInput,
|
|
15
|
+
RelationGraph,
|
|
16
|
+
RelationGraphInput,
|
|
17
|
+
SaveFlowVersionInput,
|
|
18
|
+
UpdateFlowInput,
|
|
19
|
+
} from "@anchrd/intel-contract/flow";
|
|
20
|
+
import {
|
|
21
|
+
CompleteFlowRunStepInput,
|
|
16
22
|
FlowRunHistory,
|
|
17
23
|
FlowRunList,
|
|
18
24
|
FlowRunStep,
|
|
19
|
-
FlowValidation,
|
|
20
25
|
ListFlowRunsInput,
|
|
21
|
-
|
|
26
|
+
StartFlowRunInput,
|
|
27
|
+
} from "@anchrd/intel-contract/flow-run";
|
|
28
|
+
import {
|
|
29
|
+
ArchiveNodeInput,
|
|
30
|
+
CreateNodeInput,
|
|
22
31
|
ListNodesInput,
|
|
23
32
|
Node,
|
|
24
33
|
NodeDocument,
|
|
@@ -27,30 +36,28 @@ import {
|
|
|
27
36
|
NodeList,
|
|
28
37
|
NodeTable,
|
|
29
38
|
NodeVersionList,
|
|
30
|
-
|
|
31
|
-
ProblemDetails,
|
|
32
|
-
PublishFlowInput,
|
|
33
|
-
RelationGraph,
|
|
34
|
-
RelationGraphInput,
|
|
39
|
+
ReindexResult,
|
|
35
40
|
ResolveNodeLinksInput,
|
|
36
41
|
ResolveNodeLinksResult,
|
|
37
|
-
ResourceGrantList,
|
|
38
|
-
RevokeGrantInput,
|
|
39
|
-
RevokeGrantResult,
|
|
40
42
|
SaveAttachmentInput,
|
|
41
|
-
SaveFlowVersionInput,
|
|
42
43
|
SaveNodeVersionInput,
|
|
43
44
|
SearchInput,
|
|
44
45
|
SearchResult,
|
|
45
|
-
|
|
46
|
+
UpdateNodeInput,
|
|
47
|
+
} from "@anchrd/intel-contract/node";
|
|
48
|
+
import {
|
|
49
|
+
ResourceGrantList,
|
|
50
|
+
RevokeGrantInput,
|
|
51
|
+
RevokeGrantResult,
|
|
46
52
|
ShareInput,
|
|
47
53
|
ShareResult,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} from "@anchrd/intel-contract";
|
|
54
|
+
} from "@anchrd/intel-contract/share";
|
|
55
|
+
import {
|
|
56
|
+
AppendTableRowsInput,
|
|
57
|
+
AppendTableRowsResult,
|
|
58
|
+
DefineTableInput,
|
|
59
|
+
} from "@anchrd/intel-contract/table";
|
|
60
|
+
import { ToolCatalog, ToolServerCatalog } from "@anchrd/intel-contract/tool";
|
|
54
61
|
import type { z } from "zod";
|
|
55
62
|
import { createBrowserSignIn } from "@/data/sign-in/sign-in.ts";
|
|
56
63
|
import type { IntelDataProvider, TreeEntry } from "./intel-data-provider.types.ts";
|
|
@@ -187,6 +194,9 @@ export function createIntelDataProvider(
|
|
|
187
194
|
async getSession() {
|
|
188
195
|
return await request("/session", SessionUser);
|
|
189
196
|
},
|
|
197
|
+
async reindexNodes() {
|
|
198
|
+
return await request("/nodes/reindex", ReindexResult, { method: "POST", body: "{}" });
|
|
199
|
+
},
|
|
190
200
|
listNodes,
|
|
191
201
|
async listTreeChildren(parentId) {
|
|
192
202
|
// Both sides of one folder, asked for in parallel and merged here rather than on the server:
|
|
@@ -439,10 +449,14 @@ export function createIntelDataProvider(
|
|
|
439
449
|
},
|
|
440
450
|
async completeFlowStep(input) {
|
|
441
451
|
const parsed = CompleteFlowRunStepInput.parse(input);
|
|
442
|
-
return await request(
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
452
|
+
return await request(
|
|
453
|
+
`/flow-runs/${encodeURIComponent(parsed.runId)}/steps/complete`,
|
|
454
|
+
FlowRunStep,
|
|
455
|
+
{
|
|
456
|
+
method: "POST",
|
|
457
|
+
body: JSON.stringify(parsed),
|
|
458
|
+
},
|
|
459
|
+
);
|
|
446
460
|
},
|
|
447
461
|
async listTools() {
|
|
448
462
|
return await request("/tools", ToolCatalog);
|
|
@@ -1,24 +1,33 @@
|
|
|
1
|
+
import type { SessionUser } from "@anchrd/intel-contract";
|
|
2
|
+
import type { BundleImportResult } from "@anchrd/intel-contract/bundle";
|
|
1
3
|
import type {
|
|
2
|
-
AppendTableRowsInput,
|
|
3
|
-
AppendTableRowsResult,
|
|
4
4
|
ArchiveFlowInput,
|
|
5
|
-
ArchiveNodeInput,
|
|
6
|
-
BundleImportResult,
|
|
7
|
-
CompleteFlowRunStepInput,
|
|
8
5
|
CreateFlowInput,
|
|
9
|
-
CreateNodeInput,
|
|
10
|
-
DefineTableInput,
|
|
11
6
|
Flow,
|
|
12
7
|
FlowDocument,
|
|
13
8
|
FlowList,
|
|
14
9
|
FlowPublishPreview,
|
|
15
10
|
FlowRequirements,
|
|
11
|
+
FlowValidation,
|
|
12
|
+
ListFlowsInput,
|
|
13
|
+
PreviewFlowPublishInput,
|
|
14
|
+
PublishFlowInput,
|
|
15
|
+
RelationGraph,
|
|
16
|
+
RelationGraphScope,
|
|
17
|
+
SaveFlowVersionInput,
|
|
18
|
+
UpdateFlowInput,
|
|
19
|
+
} from "@anchrd/intel-contract/flow";
|
|
20
|
+
import type {
|
|
21
|
+
CompleteFlowRunStepInput,
|
|
16
22
|
FlowRunHistory,
|
|
17
23
|
FlowRunList,
|
|
18
24
|
FlowRunStep,
|
|
19
|
-
FlowValidation,
|
|
20
25
|
ListFlowRunsInput,
|
|
21
|
-
|
|
26
|
+
StartFlowRunInput,
|
|
27
|
+
} from "@anchrd/intel-contract/flow-run";
|
|
28
|
+
import type {
|
|
29
|
+
ArchiveNodeInput,
|
|
30
|
+
CreateNodeInput,
|
|
22
31
|
ListNodesInput,
|
|
23
32
|
Node,
|
|
24
33
|
NodeDocument,
|
|
@@ -28,29 +37,28 @@ import type {
|
|
|
28
37
|
NodeList,
|
|
29
38
|
NodeTable,
|
|
30
39
|
NodeVersionList,
|
|
31
|
-
|
|
32
|
-
PublishFlowInput,
|
|
33
|
-
RelationGraph,
|
|
34
|
-
RelationGraphScope,
|
|
40
|
+
ReindexResult,
|
|
35
41
|
ResolveNodeLinksInput,
|
|
36
42
|
ResolveNodeLinksResult,
|
|
37
|
-
ResourceGrantList,
|
|
38
|
-
RevokeGrantInput,
|
|
39
|
-
RevokeGrantResult,
|
|
40
43
|
SaveAttachmentInput,
|
|
41
|
-
SaveFlowVersionInput,
|
|
42
44
|
SaveNodeVersionInput,
|
|
43
45
|
SearchInput,
|
|
44
46
|
SearchResult,
|
|
45
|
-
|
|
47
|
+
UpdateNodeInput,
|
|
48
|
+
} from "@anchrd/intel-contract/node";
|
|
49
|
+
import type {
|
|
50
|
+
ResourceGrantList,
|
|
51
|
+
RevokeGrantInput,
|
|
52
|
+
RevokeGrantResult,
|
|
46
53
|
ShareInput,
|
|
47
54
|
ShareResult,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} from "@anchrd/intel-contract";
|
|
55
|
+
} from "@anchrd/intel-contract/share";
|
|
56
|
+
import type {
|
|
57
|
+
AppendTableRowsInput,
|
|
58
|
+
AppendTableRowsResult,
|
|
59
|
+
DefineTableInput,
|
|
60
|
+
} from "@anchrd/intel-contract/table";
|
|
61
|
+
import type { ToolCatalog, ToolServerCatalog } from "@anchrd/intel-contract/tool";
|
|
54
62
|
|
|
55
63
|
// One row of the shared tree. Nodes and Flows share the folder, not their nature (ADR-0004), so
|
|
56
64
|
// this is a union that keeps each side's record whole — never a merged "node" that is a bit of both.
|
|
@@ -70,6 +78,14 @@ export type TreeEntry =
|
|
|
70
78
|
|
|
71
79
|
export interface IntelDataProvider {
|
|
72
80
|
getSession(): Promise<SessionUser>;
|
|
81
|
+
/**
|
|
82
|
+
* Rebuild the whole search index (`intel/admin`).
|
|
83
|
+
*
|
|
84
|
+
* ⚠️ Answers as soon as the work is QUEUED, not when it is done — `queued` counts the nodes that
|
|
85
|
+
* will be read again, and the index catches up behind it. A screen that said "finished" here
|
|
86
|
+
* would be reporting the wrong thing.
|
|
87
|
+
*/
|
|
88
|
+
reindexNodes(): Promise<ReindexResult>;
|
|
73
89
|
listNodes(input?: Partial<ListNodesInput>): Promise<NodeList>;
|
|
74
90
|
// One level of the shared tree: the documents and the flows filed in the same folder, in one
|
|
75
91
|
// sorted list. Per level rather than recursive, so opening a folder is what costs a request.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { retryRequest } from "@/data/request-refusal/request-refusal.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The one query client the application runs on.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ It exists as a function rather than as the literal `new QueryClient()` that stood in `main.tsx`
|
|
8
|
+
* so the policy in it can be exercised by a test (#430). A default nothing renders under is a
|
|
9
|
+
* default nothing checks, and this one decides how long a screen stays on "Loading…".
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ Flat, and without a `.unit.ts` of its own on purpose: the only thing to assert here is that the
|
|
12
|
+
* returned client carries `retryRequest`, and what that predicate DOES is already covered next door
|
|
13
|
+
* in `request-refusal.unit.ts` — twice over, because two screen tests render against this very
|
|
14
|
+
* client. A test that restated the wiring would be the "pure wiring" the repository rules exclude.
|
|
15
|
+
*/
|
|
16
|
+
export function createIntelQueryClient(): QueryClient {
|
|
17
|
+
return new QueryClient({ defaultOptions: { queries: { retry: retryRequest } } });
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { IntelRequestError } from "@/data/intel-data-provider/intel-data-provider.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a failed request already settles, as opposed to what it might answer differently next time.
|
|
5
|
+
*
|
|
6
|
+
* - `not-available` is Intel's `404`. It is deliberately the same answer for "there is no such node"
|
|
7
|
+
* and "you may not reach this node" — `requireVisible` in `packages/api/src/nodes/nodes.ts`, held
|
|
8
|
+
* in place by "answers 404 for a foreign version and for an unreadable node" in `nodes.int.ts`.
|
|
9
|
+
* A screen must therefore not guess between them either: one sentence covers both, and that is why
|
|
10
|
+
* `no-permission` is a separate value rather than prose that could be shown for a `404` by mistake.
|
|
11
|
+
* - `no-permission` is a `403`. The server said the word itself, so repeating it reveals nothing.
|
|
12
|
+
* - `null` is everything else: a timeout, a `502`, a network that came back. Those are worth trying
|
|
13
|
+
* again, and the screen keeps its retry button for them.
|
|
14
|
+
*/
|
|
15
|
+
export type Refusal = "not-available" | "no-permission";
|
|
16
|
+
|
|
17
|
+
/** The HTTP status the data provider recorded, or `null` where the failure carried none. */
|
|
18
|
+
export function statusOf(error: unknown): number | null {
|
|
19
|
+
return error instanceof IntelRequestError ? error.status : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function refusalOf(error: unknown): Refusal | null {
|
|
23
|
+
switch (statusOf(error)) {
|
|
24
|
+
case 403:
|
|
25
|
+
return "no-permission";
|
|
26
|
+
case 404:
|
|
27
|
+
return "not-available";
|
|
28
|
+
default:
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The retry policy every query in Intel runs under.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ A refusal is deterministic, and repeating it is what left the screen on "Loading…" after a
|
|
37
|
+
* share was revoked (#430). React Query's default is three retries with a growing delay, so a `404`
|
|
38
|
+
* kept the reader in front of a moving loading state for seconds — and a retry is **paused while
|
|
39
|
+
* the tab is unfocused** (`packages/ui/CLAUDE.md`, #212), so "seconds" has no upper bound whenever
|
|
40
|
+
* the person looked somewhere else. The answer was there the whole time.
|
|
41
|
+
*
|
|
42
|
+
* ⚠️ `408` and `429` are the two `4xx` that DO change on their own — a timeout and a rate limit both
|
|
43
|
+
* say "not now" rather than "not you" — so they keep the retries. Everything else in the range is
|
|
44
|
+
* about this request as it was made, and making it again cannot alter that.
|
|
45
|
+
*/
|
|
46
|
+
export function retryRequest(failureCount: number, error: unknown): boolean {
|
|
47
|
+
const status = statusOf(error);
|
|
48
|
+
const deterministic =
|
|
49
|
+
status !== null && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
50
|
+
return !deterministic && failureCount < 3;
|
|
51
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DocumentLinkInlineType } from "@anchrd/intel-contract";
|
|
1
|
+
import { DocumentLinkInlineType } from "@anchrd/intel-contract/node";
|
|
2
2
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
3
3
|
import { createReactInlineContentSpec } from "@blocknote/react";
|
|
4
4
|
import { FileText, Link2Off } from "lucide-react";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NodeKind } from "@anchrd/intel-contract";
|
|
1
|
+
import type { NodeKind } from "@anchrd/intel-contract/node";
|
|
2
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { ChevronRight, Folder, Search } from "lucide-react";
|
|
4
4
|
import { useMemo, useState } from "react";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FlowRunSummary } from "@anchrd/intel-contract";
|
|
1
|
+
import type { FlowRunSummary } from "@anchrd/intel-contract/flow-run";
|
|
2
2
|
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { ChevronDown, ChevronRight, CornerDownRight } from "lucide-react";
|
|
4
4
|
import { useState } from "react";
|
package/src/flows/flows.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Flow, FlowGraph, FlowNode
|
|
2
|
-
import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
1
|
+
import type { Flow, FlowGraph, FlowNode } from "@anchrd/intel-contract/flow";
|
|
2
|
+
import { flowNodeLayer } from "@anchrd/intel-contract/flow";
|
|
3
|
+
import type { Node } from "@anchrd/intel-contract/node";
|
|
3
4
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
5
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
5
6
|
import {
|
|
@@ -758,7 +759,14 @@ function FlowTitle({
|
|
|
758
759
|
<TooltipContent>{publishLabel}</TooltipContent>
|
|
759
760
|
</Tooltip>
|
|
760
761
|
</TooltipProvider>
|
|
761
|
-
|
|
762
|
+
{/* A draft that was never saved has no version, and the button says so rather than claiming
|
|
763
|
+
one (#432) — the same distinction the publish button next to it already draws. */}
|
|
764
|
+
<SaveButton
|
|
765
|
+
dirty={dirty && canMutate}
|
|
766
|
+
saving={saving}
|
|
767
|
+
stored={flow.currentVersionId !== null}
|
|
768
|
+
onSave={onSave}
|
|
769
|
+
/>
|
|
762
770
|
</TitleRow>
|
|
763
771
|
);
|
|
764
772
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { flowNodeLayer } from "@anchrd/intel-contract";
|
|
1
|
+
import { flowNodeLayer } from "@anchrd/intel-contract/flow";
|
|
2
2
|
import { Plus, X } from "lucide-react";
|
|
3
3
|
import { Fragment, useId, useRef, useState } from "react";
|
|
4
4
|
import { type NodeIcon, nodeIcon } from "@/flows/node-icon/node-icon.ts";
|