@anchrd/intel-ui 0.28.0 → 0.31.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/package.json +2 -2
- package/src/app/app-tree/app-tree.tsx +26 -5
- package/src/app/tree-move/tree-move.tsx +31 -5
- package/src/archive/archive.tsx +3 -2
- package/src/flows/flows.tsx +300 -99
- package/src/folder-contents/folder-contents.tsx +2 -2
- package/src/i18n/de.json +17 -7
- package/src/i18n/en.json +16 -6
- package/src/i18n/es.json +16 -6
- package/src/import-bundle/import-bundle.tsx +5 -2
- package/src/resource-menu/resource-menu.tsx +7 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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.19.0",
|
|
37
37
|
"@blocknote/core": "^0.52.1",
|
|
38
38
|
"@blocknote/react": "^0.52.1",
|
|
39
39
|
"@blocknote/shadcn": "^0.52.1",
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "lucide-react";
|
|
13
13
|
import { useRef, useState } from "react";
|
|
14
14
|
import {
|
|
15
|
+
allTreeLevelsKey,
|
|
15
16
|
type MoveDestination,
|
|
16
17
|
moveErrorKey,
|
|
17
18
|
moveVerdict,
|
|
@@ -176,13 +177,18 @@ export function AppTree() {
|
|
|
176
177
|
// ⚠️ The level a row appears in is not the only thing that goes stale. The header search reads
|
|
177
178
|
// the whole flow list, and the graph is what the link picker and the flow editor's tree links
|
|
178
179
|
// offer — a row created here has to reach those too, or another screen keeps showing yesterday.
|
|
180
|
+
//
|
|
181
|
+
// ⚠️ And the level written into is not enough either: the folder's own ROW says whether it can be
|
|
182
|
+
// opened at all, and that row stands one level higher (`allTreeLevelsKey`, #519). Without it the
|
|
183
|
+
// `toggle` above opens a folder whose arrow never appeared, so the first thing filed into an empty
|
|
184
|
+
// folder was invisible until the page was reloaded.
|
|
179
185
|
async function reveal(
|
|
180
186
|
parentId: string | null,
|
|
181
187
|
collection: "flows" | "node-graph",
|
|
182
188
|
): Promise<void> {
|
|
183
189
|
if (parentId !== null) toggle({ id: parentId, type: "folder" }, true);
|
|
184
190
|
await Promise.all([
|
|
185
|
-
queryClient.invalidateQueries({ queryKey:
|
|
191
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
186
192
|
queryClient.invalidateQueries({ queryKey: [collection] }),
|
|
187
193
|
// The graph view of that same level draws exactly this list, so it goes stale for the same
|
|
188
194
|
// reason the level does (#19).
|
|
@@ -233,8 +239,19 @@ export function AppTree() {
|
|
|
233
239
|
},
|
|
234
240
|
onSuccess: async (created) => {
|
|
235
241
|
setCreating(null);
|
|
236
|
-
|
|
237
|
-
|
|
242
|
+
// ⚠️ The screen first, the refreshes after. Since #519 `reveal` waits on EVERY mounted level,
|
|
243
|
+
// and a React Query retry is paused while the tab is not in front (#212) — awaiting that
|
|
244
|
+
// before navigating would leave somebody who just made a document sitting on the old screen
|
|
245
|
+
// for as long as one level takes to answer.
|
|
246
|
+
//
|
|
247
|
+
// ⚠️ `finally`, because both screens are lazy chunks: a failed chunk load throws out of
|
|
248
|
+
// `navigate`, and without this the tree, the collections and the graph would silently keep
|
|
249
|
+
// yesterday's rows for the rest of the session — the very state this ticket is about.
|
|
250
|
+
try {
|
|
251
|
+
await navigate({ to: created.area, search: { select: created.id } });
|
|
252
|
+
} finally {
|
|
253
|
+
await reveal(created.parentId, created.collection);
|
|
254
|
+
}
|
|
238
255
|
},
|
|
239
256
|
});
|
|
240
257
|
|
|
@@ -275,8 +292,12 @@ export function AppTree() {
|
|
|
275
292
|
return { id: node.id, parentId };
|
|
276
293
|
},
|
|
277
294
|
onSuccess: async (created) => {
|
|
278
|
-
|
|
279
|
-
|
|
295
|
+
// The order — and the `finally` — are the ones above, for the same two reasons.
|
|
296
|
+
try {
|
|
297
|
+
await navigate({ to: "/nodes", search: { select: created.id } });
|
|
298
|
+
} finally {
|
|
299
|
+
await reveal(created.parentId, "node-graph");
|
|
300
|
+
}
|
|
280
301
|
},
|
|
281
302
|
});
|
|
282
303
|
|
|
@@ -31,6 +31,33 @@ export function treeLevelKey(parentId: string | null): readonly unknown[] {
|
|
|
31
31
|
return ["tree", parentId];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Every level of the tree at once — what a write that changed what is IN a folder makes stale.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ Not the level that was written into: that is the obvious half, and it was the whole of #519.
|
|
38
|
+
* A folder ROW carries `openable` — since #59 the arrow only stands where there is something to
|
|
39
|
+
* open — and that is a statement about the folder's CONTENTS living one level ABOVE it. Refresh
|
|
40
|
+
* only the level below and the row keeps yesterday's answer: the first thing filed into an EMPTY
|
|
41
|
+
* folder has no arrow to appear under, `reveal` opens a level `renderRow` then never renders, and
|
|
42
|
+
* reloading the page was the only thing that read the row again. The other direction is the same
|
|
43
|
+
* bug from the other side — the last child moved or archived out leaves an arrow promising a level
|
|
44
|
+
* that is empty.
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ A prefix rather than "the level, plus the level its folder row sits in", because that second
|
|
47
|
+
* level is not always the one the record names: since #429 a shared row also stands at the root
|
|
48
|
+
* (`levelsToClear`).
|
|
49
|
+
*
|
|
50
|
+
* ⚠️ What it costs is one request per MOUNTED level, and that is not the same as "per level on
|
|
51
|
+
* screen": `expanded` in `app-tree.tsx` is only ever added to, so a folder that was opened and has
|
|
52
|
+
* since left the tree — archived, moved away — keeps its query mounted for the rest of the session
|
|
53
|
+
* and is re-read by every write from then on. Nothing is wrong on screen; the requests are simply
|
|
54
|
+
* paid for. #521 prunes that list, and until it does, this is the honest bound.
|
|
55
|
+
*
|
|
56
|
+
* The same prefix is what `archive.tsx` has always used for the same reason: a row can come back
|
|
57
|
+
* anywhere, and the screen writing it does not know where.
|
|
58
|
+
*/
|
|
59
|
+
export const allTreeLevelsKey: readonly unknown[] = ["tree"];
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Which cached levels a move has to take the row out of.
|
|
36
63
|
*
|
|
@@ -368,12 +395,11 @@ export function useTreeMove({
|
|
|
368
395
|
for (const [key, value] of context?.snapshot ?? []) queryClient.setQueryData(key, value);
|
|
369
396
|
},
|
|
370
397
|
onSuccess: (_result, { destination }) => onMoved?.(destination),
|
|
371
|
-
onSettled: async (_result, _error, { entry
|
|
398
|
+
onSettled: async (_result, _error, { entry }) => {
|
|
372
399
|
await Promise.all([
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
),
|
|
376
|
-
queryClient.invalidateQueries({ queryKey: treeLevelKey(destination.id) }),
|
|
400
|
+
// Both ends of the move and the rows they hang under: a folder that just received its first
|
|
401
|
+
// row needs its arrow, and one that lost its last has to give it back (#519).
|
|
402
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
377
403
|
queryClient.invalidateQueries({
|
|
378
404
|
queryKey: [entry.type === "flow" ? "flows" : "node-graph"],
|
|
379
405
|
}),
|
package/src/archive/archive.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type { Node } from "@anchrd/intel-contract/node";
|
|
|
3
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { ArchiveRestore, LoaderCircle, Trash2 } from "lucide-react";
|
|
5
5
|
import { useState } from "react";
|
|
6
|
+
import { allTreeLevelsKey } from "@/app/tree-move/tree-move.tsx";
|
|
6
7
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
7
8
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
8
9
|
import { kindIcons } from "@/kind-icon.ts";
|
|
@@ -101,7 +102,7 @@ export function Archive() {
|
|
|
101
102
|
queryClient.invalidateQueries({ queryKey: ["archive"] }),
|
|
102
103
|
// The tree gets it back, so every level has to be asked again — the entry may sit
|
|
103
104
|
// anywhere, and the archive does not know where.
|
|
104
|
-
queryClient.invalidateQueries({ queryKey:
|
|
105
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
105
106
|
queryClient.invalidateQueries({ queryKey: ["node-graph"] }),
|
|
106
107
|
queryClient.invalidateQueries({ queryKey: ["flows"] }),
|
|
107
108
|
]);
|
|
@@ -126,7 +127,7 @@ export function Archive() {
|
|
|
126
127
|
queryClient.invalidateQueries({ queryKey: ["archive"] }),
|
|
127
128
|
// The tree and the graph did not show the node, but its DISAPPEARANCE can be visible
|
|
128
129
|
// elsewhere: a reference to it no longer resolves afterwards.
|
|
129
|
-
queryClient.invalidateQueries({ queryKey:
|
|
130
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
130
131
|
queryClient.invalidateQueries({ queryKey: ["node-graph"] }),
|
|
131
132
|
queryClient.invalidateQueries({ queryKey: ["flows"] }),
|
|
132
133
|
]);
|
package/src/flows/flows.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Flow, FlowGraph, FlowNode } from "@anchrd/intel-contract/flow";
|
|
2
2
|
import { flowNodeLayer } from "@anchrd/intel-contract/flow";
|
|
3
3
|
import type { Node } from "@anchrd/intel-contract/node";
|
|
4
|
+
import { serverOf } from "@anchrd/intel-contract/tool";
|
|
4
5
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
6
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
6
7
|
import {
|
|
@@ -285,6 +286,7 @@ function newNode(
|
|
|
285
286
|
index: number,
|
|
286
287
|
firstNodeId?: string,
|
|
287
288
|
firstFlowId?: string,
|
|
289
|
+
firstServer?: string,
|
|
288
290
|
): FlowNode {
|
|
289
291
|
const common = {
|
|
290
292
|
id: `${kind}-${crypto.randomUUID()}`,
|
|
@@ -315,10 +317,13 @@ function newNode(
|
|
|
315
317
|
return {
|
|
316
318
|
...common,
|
|
317
319
|
kind,
|
|
320
|
+
// ⚠️ A server, not an empty string. `ToolServerHandle` has a minimum length, so a draft
|
|
321
|
+
// with `""` cannot be saved at all — the author would build a step and lose it at the next
|
|
322
|
+
// save with a raw field-path error. The tree link one line up takes the same way out.
|
|
318
323
|
configuration: {
|
|
319
|
-
|
|
324
|
+
server: firstServer ?? "",
|
|
325
|
+
allow: null,
|
|
320
326
|
fingerprint: null,
|
|
321
|
-
arguments: {},
|
|
322
327
|
},
|
|
323
328
|
};
|
|
324
329
|
case "condition":
|
|
@@ -361,9 +366,27 @@ function FlowsEditor() {
|
|
|
361
366
|
const theme = useResolvedTheme();
|
|
362
367
|
const queryClient = useQueryClient();
|
|
363
368
|
const navigate = useNavigate();
|
|
369
|
+
// The servers a tool step can name (#489). The flat function list the picker used to show is
|
|
370
|
+
// what that ticket set out to remove; which function runs is decided while the flow runs.
|
|
371
|
+
// ⚠️ The flat function list is NOT what a step is chosen from any more (#489) — it is only what
|
|
372
|
+
// an author narrows a chosen server WITH. It is asked for regardless of whether a tool step is
|
|
373
|
+
// selected, because a query that starts when a panel opens shows a spinner inside the panel.
|
|
364
374
|
const tools = useQuery({
|
|
365
375
|
queryKey: ["tools"],
|
|
366
376
|
queryFn: () => data.listTools(),
|
|
377
|
+
// ⚠️ No retry. This query only ENRICHES the panel — the server is already chosen without it —
|
|
378
|
+
// and a retry is paused while the tab is unfocused, so a deterministic failure would leave the
|
|
379
|
+
// function list empty for as long as the reader looks away, with nothing saying why (#212).
|
|
380
|
+
retry: false,
|
|
381
|
+
});
|
|
382
|
+
const toolServers = useQuery({
|
|
383
|
+
queryKey: ["tool-servers"],
|
|
384
|
+
queryFn: () => data.listToolServers(),
|
|
385
|
+
// ⚠️ No retry here either, and for a sharper reason than the list above: the panel shows
|
|
386
|
+
// NOTHING while this is pending. A deterministic 502 with an unfocused tab pauses the retry,
|
|
387
|
+
// and the author then sits in front of an empty server field with no sentence saying why —
|
|
388
|
+
// which is #212 exactly. `error` is a state this panel can speak; `pending` is not.
|
|
389
|
+
retry: false,
|
|
367
390
|
});
|
|
368
391
|
// A tree link points at a node, so the editor needs candidates. The graph is the authorized
|
|
369
392
|
// flat list of them; walking the folder tree for a picker would be the N+1 all over again.
|
|
@@ -414,6 +437,13 @@ function FlowsEditor() {
|
|
|
414
437
|
// guarantee that the loaded document is both the selected flow and settled.
|
|
415
438
|
const documentReady = selectedFlowId !== null && document.data?.flow.id === selectedFlowId;
|
|
416
439
|
const canMutate = documentReady && !document.isFetching;
|
|
440
|
+
// ⚠️ A tool step without a server cannot be saved at all — `ToolServerHandle` has a minimum
|
|
441
|
+
// length. Letting the request go out anyway answers with a generic failure that names no step, so
|
|
442
|
+
// the author learns "something is wrong" instead of "this one needs a server". The refusal
|
|
443
|
+
// belongs here, before the write (#517).
|
|
444
|
+
const stepsMissingServer = nodes.filter(
|
|
445
|
+
(node) => node.data.node.kind === "tool" && node.data.node.configuration.server === "",
|
|
446
|
+
);
|
|
417
447
|
useEffect(() => {
|
|
418
448
|
if (!document.data || document.data.flow.id !== selectedFlowId) return;
|
|
419
449
|
const next = canvas(document.data.version?.graph ?? defaultGraph());
|
|
@@ -462,6 +492,7 @@ function FlowsEditor() {
|
|
|
462
492
|
dirty={dirty}
|
|
463
493
|
saving={save.isPending}
|
|
464
494
|
canMutate={canMutate}
|
|
495
|
+
incomplete={stepsMissingServer.map((node) => ({ label: node.data.node.label }))}
|
|
465
496
|
onSave={() => save.mutate()}
|
|
466
497
|
onPublish={() => setPublishing(true)}
|
|
467
498
|
/>
|
|
@@ -519,6 +550,7 @@ function FlowsEditor() {
|
|
|
519
550
|
nodes.length,
|
|
520
551
|
nodeGraph.data?.nodes[0]?.id,
|
|
521
552
|
callable.data?.items.find((entry) => entry.id !== selectedFlowId)?.id,
|
|
553
|
+
toolServers.data?.items[0]?.handle,
|
|
522
554
|
);
|
|
523
555
|
setNodes((current) => [
|
|
524
556
|
...current,
|
|
@@ -639,7 +671,19 @@ function FlowsEditor() {
|
|
|
639
671
|
<NodeInspector
|
|
640
672
|
node={selectedNode}
|
|
641
673
|
update={updateNode}
|
|
642
|
-
|
|
674
|
+
toolServers={toolServers.data?.items ?? []}
|
|
675
|
+
toolServersState={
|
|
676
|
+
toolServers.isPending
|
|
677
|
+
? "pending"
|
|
678
|
+
: toolServers.isError
|
|
679
|
+
? "error"
|
|
680
|
+
: toolServers.data?.portalConnected === false
|
|
681
|
+
? "disconnected"
|
|
682
|
+
: "ready"
|
|
683
|
+
}
|
|
684
|
+
toolNames={(tools.data?.items ?? []).map((entry) => entry.name)}
|
|
685
|
+
toolNamesFailed={tools.isError}
|
|
686
|
+
toolNamesPending={tools.isPending}
|
|
643
687
|
flows={(callable.data?.items ?? []).filter(
|
|
644
688
|
(entry) => entry.id !== selectedFlowId,
|
|
645
689
|
)}
|
|
@@ -700,6 +744,7 @@ function FlowTitle({
|
|
|
700
744
|
dirty,
|
|
701
745
|
saving,
|
|
702
746
|
canMutate,
|
|
747
|
+
incomplete,
|
|
703
748
|
onSave,
|
|
704
749
|
onPublish,
|
|
705
750
|
}: {
|
|
@@ -707,6 +752,9 @@ function FlowTitle({
|
|
|
707
752
|
dirty: boolean;
|
|
708
753
|
saving: boolean;
|
|
709
754
|
canMutate: boolean;
|
|
755
|
+
// Tool steps that name no server yet. They cannot be saved, so the refusal is shown here rather
|
|
756
|
+
// than fetched from the server as a message that names no step.
|
|
757
|
+
incomplete: Array<{ label: string }>;
|
|
710
758
|
onSave(): void;
|
|
711
759
|
onPublish(): void;
|
|
712
760
|
}) {
|
|
@@ -733,7 +781,16 @@ function FlowTitle({
|
|
|
733
781
|
applies EVERYWHERE stands. But it says how THIS flow is shown, and so belongs in the line
|
|
734
782
|
that names this flow. A flow is the only level with runs, and therefore the only one with
|
|
735
783
|
three views (#35). */}
|
|
736
|
-
|
|
784
|
+
{incomplete.length > 0 && (
|
|
785
|
+
<p className="mr-2 text-xs text-destructive">
|
|
786
|
+
{i18n.t("flows.toolStepNeedsServer", { label: incomplete[0]?.label ?? "" })}
|
|
787
|
+
</p>
|
|
788
|
+
)}
|
|
789
|
+
<SaveButton
|
|
790
|
+
dirty={dirty && canMutate && incomplete.length === 0}
|
|
791
|
+
saving={saving}
|
|
792
|
+
onSave={onSave}
|
|
793
|
+
/>
|
|
737
794
|
<TooltipProvider delayDuration={300}>
|
|
738
795
|
<Tooltip>
|
|
739
796
|
<TooltipTrigger asChild>
|
|
@@ -797,10 +854,34 @@ function PublishPreview({
|
|
|
797
854
|
) : (
|
|
798
855
|
<>
|
|
799
856
|
<p className="text-sm text-muted-foreground">
|
|
800
|
-
{preview.data.calls.length === 0
|
|
857
|
+
{preview.data.calls.length === 0 && preview.data.tools.length === 0
|
|
801
858
|
? i18n.t("flows.publishPreviewEmpty")
|
|
802
859
|
: i18n.t("flows.publishPreviewIntro")}
|
|
803
860
|
</p>
|
|
861
|
+
{/* ⚠️ The tool surface belongs in the preview because publishing FREEZES it: after this
|
|
862
|
+
confirmation the step is pinned to these functions and inherits nothing the provider
|
|
863
|
+
adds later. An author who is not shown it is asked to freeze something unseen (#517). */}
|
|
864
|
+
{preview.data.tools.length > 0 && (
|
|
865
|
+
<ul className="mt-4 space-y-2">
|
|
866
|
+
{preview.data.tools.map((tool) => (
|
|
867
|
+
<li key={tool.nodeId} className="rounded-md border p-3 text-sm">
|
|
868
|
+
<span className="block font-medium">{tool.nodeLabel}</span>
|
|
869
|
+
<span className="block text-xs text-muted-foreground">{tool.server}</span>
|
|
870
|
+
<span
|
|
871
|
+
className={`mt-1 block text-xs ${tool.available ? "text-muted-foreground" : "text-destructive"}`}
|
|
872
|
+
>
|
|
873
|
+
{!tool.available
|
|
874
|
+
? i18n.t("flows.publishPreviewToolUnavailable")
|
|
875
|
+
: tool.allow === null
|
|
876
|
+
? i18n.t("flows.toolAllowAll")
|
|
877
|
+
: i18n.t("flows.publishPreviewToolAllow", {
|
|
878
|
+
functions: tool.allow.join(", "),
|
|
879
|
+
})}
|
|
880
|
+
</span>
|
|
881
|
+
</li>
|
|
882
|
+
))}
|
|
883
|
+
</ul>
|
|
884
|
+
)}
|
|
804
885
|
<ul className="mt-4 space-y-2">
|
|
805
886
|
{preview.data.calls.map((call) => {
|
|
806
887
|
// ⚠️ One condition for the sentence and its colour. They were two, so a call that
|
|
@@ -852,12 +933,30 @@ function PublishPreview({
|
|
|
852
933
|
function NodeInspector({
|
|
853
934
|
node,
|
|
854
935
|
update,
|
|
855
|
-
|
|
936
|
+
toolServers,
|
|
937
|
+
toolServersState,
|
|
938
|
+
toolNames,
|
|
939
|
+
toolNamesFailed,
|
|
940
|
+
toolNamesPending,
|
|
856
941
|
flows,
|
|
857
942
|
}: {
|
|
858
943
|
node: CanvasNode | null;
|
|
859
944
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
860
|
-
|
|
945
|
+
toolServers: Array<{ handle: string; name: string; toolCount: number }>;
|
|
946
|
+
// ⚠️ FOUR answers, not two. A list that is still loading is not one that came back empty; a
|
|
947
|
+
// failure is neither; and a portal this user never connected is not a portal that reaches
|
|
948
|
+
// nothing. Drawing them all as "no servers" is the mistake #350 records for the tools screen —
|
|
949
|
+
// and the fourth one is the worst of them: it answers a SIGN-IN problem with a sentence about
|
|
950
|
+
// reach, in a place that offers no way to sign in.
|
|
951
|
+
toolServersState: "pending" | "error" | "disconnected" | "ready";
|
|
952
|
+
toolNames: string[];
|
|
953
|
+
// Told apart from "this server offers nothing": a function list that could not be loaded must not
|
|
954
|
+
// read as a narrowing that lost its functions.
|
|
955
|
+
toolNamesFailed: boolean;
|
|
956
|
+
// ⚠️ A query that has not answered is not one that answered "nothing". Without this a step opened
|
|
957
|
+
// while the catalog is still loading shows its allowed functions as missing — the panel accusing
|
|
958
|
+
// the author of a narrowing that broke, moments before the list arrives.
|
|
959
|
+
toolNamesPending: boolean;
|
|
861
960
|
flows: Flow[];
|
|
862
961
|
}) {
|
|
863
962
|
const i18n = useI18n();
|
|
@@ -946,54 +1045,198 @@ function NodeInspector({
|
|
|
946
1045
|
</div>
|
|
947
1046
|
)}
|
|
948
1047
|
{contract.kind === "tool" && (
|
|
949
|
-
<
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1048
|
+
<ToolStepFields
|
|
1049
|
+
configuration={contract.configuration}
|
|
1050
|
+
servers={toolServers}
|
|
1051
|
+
state={toolServersState}
|
|
1052
|
+
toolNames={toolNames}
|
|
1053
|
+
toolNamesFailed={toolNamesFailed}
|
|
1054
|
+
toolNamesPending={toolNamesPending}
|
|
1055
|
+
update={update}
|
|
1056
|
+
/>
|
|
1057
|
+
)}
|
|
1058
|
+
</div>
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* What a tool step may reach: one server, and optionally only some of its functions.
|
|
1064
|
+
*
|
|
1065
|
+
* ⚠️ The server comes first and the functions are a NARROWING, not the other way round (#489). The
|
|
1066
|
+
* old field offered every function of every server in one flat list — 85 entries here, portal
|
|
1067
|
+
* management tools among them — and asked the author to pick the one call they wanted before they
|
|
1068
|
+
* knew what the step would say. Which function runs is a run-time answer; which server may be
|
|
1069
|
+
* reached is an authoring decision, and that is the one this asks for.
|
|
1070
|
+
*/
|
|
1071
|
+
export function ToolStepFields({
|
|
1072
|
+
configuration,
|
|
1073
|
+
servers,
|
|
1074
|
+
state,
|
|
1075
|
+
toolNames,
|
|
1076
|
+
toolNamesFailed,
|
|
1077
|
+
toolNamesPending,
|
|
1078
|
+
update,
|
|
1079
|
+
}: {
|
|
1080
|
+
configuration: { server: string; allow: string[] | null; fingerprint: string | null };
|
|
1081
|
+
servers: Array<{ handle: string; name: string; toolCount: number }>;
|
|
1082
|
+
state: "pending" | "error" | "disconnected" | "ready";
|
|
1083
|
+
toolNames: string[];
|
|
1084
|
+
toolNamesFailed: boolean;
|
|
1085
|
+
toolNamesPending: boolean;
|
|
1086
|
+
update(fn: (node: FlowNode) => FlowNode): void;
|
|
1087
|
+
}) {
|
|
1088
|
+
const i18n = useI18n();
|
|
1089
|
+
// ⚠️ The group's identity, not its data. Two steps on one screen with the same server would
|
|
1090
|
+
// otherwise share one radio group and unset each other — and the component is exported now, so
|
|
1091
|
+
// that invariant no longer lives in the file that guarantees it.
|
|
1092
|
+
const groupId = useId();
|
|
1093
|
+
const hintId = `${groupId}-hint`;
|
|
1094
|
+
// The one sentence this field has to say, or none. Derived once so the control and the paragraph
|
|
1095
|
+
// cannot disagree about whether there is something to point at.
|
|
1096
|
+
const hint =
|
|
1097
|
+
state === "error"
|
|
1098
|
+
? ({ key: "flows.toolServersFailed", tone: "bad" } as const)
|
|
1099
|
+
: state === "disconnected"
|
|
1100
|
+
? ({ key: "flows.toolServersDisconnected", tone: "bad" } as const)
|
|
1101
|
+
: state === "ready" && servers.length === 0
|
|
1102
|
+
? ({ key: "flows.toolServersEmpty", tone: "plain" } as const)
|
|
1103
|
+
: null;
|
|
1104
|
+
const ownFunctions = toolNames.filter(
|
|
1105
|
+
(name) => configuration.server !== "" && serverOf(name, [configuration.server]) !== null,
|
|
1106
|
+
);
|
|
1107
|
+
// ⚠️ An allowed function the catalog does not carry is DRAWN, not dropped. Rendering only what
|
|
1108
|
+
// the catalog knows would let the panel and the saved graph disagree in silence: the author reads
|
|
1109
|
+
// "narrowed to nothing" while the step still names something. The API treats the same condition
|
|
1110
|
+
// as serious enough to refuse a publish, so the editor may not swallow it.
|
|
1111
|
+
// Nothing is "missing" until the catalog has actually answered.
|
|
1112
|
+
const missing = toolNamesPending
|
|
1113
|
+
? []
|
|
1114
|
+
: (configuration.allow ?? []).filter((name) => !ownFunctions.includes(name));
|
|
1115
|
+
const functions = [...ownFunctions, ...missing];
|
|
1116
|
+
|
|
1117
|
+
function setTool(
|
|
1118
|
+
change: (current: { server: string; allow: string[] | null; fingerprint: string | null }) => {
|
|
1119
|
+
server: string;
|
|
1120
|
+
allow: string[] | null;
|
|
1121
|
+
fingerprint: string | null;
|
|
1122
|
+
},
|
|
1123
|
+
) {
|
|
1124
|
+
update((value) =>
|
|
1125
|
+
value.kind === "tool" ? { ...value, configuration: change(value.configuration) } : value,
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return (
|
|
1130
|
+
<div className="mt-4 space-y-3">
|
|
1131
|
+
<label className="block text-sm font-medium">
|
|
1132
|
+
{i18n.t("flows.tool")}
|
|
1133
|
+
<select
|
|
1134
|
+
value={configuration.server}
|
|
1135
|
+
// ⚠️ A disabled control drops out of the tab order, so the reason has to be ANNOUNCED
|
|
1136
|
+
// rather than merely printed beside it. It points at the hint only when the hint is
|
|
1137
|
+
// actually rendered — `pending` prints nothing on purpose, and a reference to an id that
|
|
1138
|
+
// does not exist is worse than none: a screen reader reads it as a broken relation.
|
|
1139
|
+
aria-describedby={hint === null ? undefined : hintId}
|
|
1140
|
+
disabled={state !== "ready" || servers.length === 0}
|
|
1141
|
+
onChange={(event) => {
|
|
1142
|
+
// ⚠️ Read out of the event NOW, not inside the updater. The updater runs later, and by
|
|
1143
|
+
// then this controlled select has been set back to the state's value — the change would
|
|
1144
|
+
// apply to itself and nothing would move.
|
|
1145
|
+
const server = event.target.value;
|
|
1146
|
+
// Changing the server drops the narrowing AND the frozen surface. Keeping `allow` would
|
|
1147
|
+
// leave functions of the old server behind — the contract refuses that outright — and
|
|
1148
|
+
// keeping the fingerprint would claim a surface nobody has confirmed.
|
|
1149
|
+
setTool(() => ({ server, allow: null, fingerprint: null }));
|
|
1150
|
+
}}
|
|
1151
|
+
className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
1152
|
+
>
|
|
1153
|
+
<option value="">{i18n.t("flows.selectTool")}</option>
|
|
1154
|
+
{servers.map((entry) => (
|
|
1155
|
+
<option key={entry.handle} value={entry.handle}>
|
|
1156
|
+
{entry.name}
|
|
1157
|
+
</option>
|
|
1158
|
+
))}
|
|
1159
|
+
</select>
|
|
1160
|
+
</label>
|
|
1161
|
+
{/* The three states of the list, told apart. Silence while loading; a reason when it failed;
|
|
1162
|
+
and "you reach none" only once that is actually known. */}
|
|
1163
|
+
{hint !== null && (
|
|
1164
|
+
<p
|
|
1165
|
+
id={hintId}
|
|
1166
|
+
className={`text-xs ${hint.tone === "bad" ? "text-destructive" : "text-muted-foreground"}`}
|
|
1167
|
+
>
|
|
1168
|
+
{i18n.t(hint.key)}
|
|
1169
|
+
</p>
|
|
1170
|
+
)}
|
|
1171
|
+
{configuration.server !== "" && (
|
|
1172
|
+
<fieldset className="space-y-2">
|
|
1173
|
+
<legend className="text-sm font-medium">{i18n.t("flows.toolFunctions")}</legend>
|
|
1174
|
+
<label className="flex items-center gap-2 text-sm">
|
|
1175
|
+
<input
|
|
1176
|
+
type="radio"
|
|
1177
|
+
name={groupId}
|
|
1178
|
+
checked={configuration.allow === null}
|
|
1179
|
+
onChange={() =>
|
|
1180
|
+
setTool((current) => ({ ...current, allow: null, fingerprint: null }))
|
|
1181
|
+
}
|
|
1182
|
+
className="size-4"
|
|
1183
|
+
/>
|
|
1184
|
+
{i18n.t("flows.toolAllowAll")}
|
|
979
1185
|
</label>
|
|
980
|
-
<
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1186
|
+
<label className="flex items-center gap-2 text-sm">
|
|
1187
|
+
<input
|
|
1188
|
+
type="radio"
|
|
1189
|
+
name={groupId}
|
|
1190
|
+
checked={configuration.allow !== null}
|
|
1191
|
+
onChange={() => setTool((current) => ({ ...current, allow: [], fingerprint: null }))}
|
|
1192
|
+
className="size-4"
|
|
1193
|
+
/>
|
|
1194
|
+
{i18n.t("flows.toolAllowSome")}
|
|
1195
|
+
</label>
|
|
1196
|
+
{configuration.allow !== null && (
|
|
1197
|
+
<ul className="max-h-48 space-y-1 overflow-y-auto pl-6">
|
|
1198
|
+
{functions.map((name) => (
|
|
1199
|
+
<li key={name}>
|
|
1200
|
+
<label className="flex items-center gap-2 text-sm">
|
|
1201
|
+
<input
|
|
1202
|
+
type="checkbox"
|
|
1203
|
+
checked={configuration.allow?.includes(name) ?? false}
|
|
1204
|
+
onChange={(event) => {
|
|
1205
|
+
// Same reason as the select above: the checked flag is read now, the
|
|
1206
|
+
// updater runs later.
|
|
1207
|
+
const add = event.target.checked;
|
|
1208
|
+
setTool((current) => ({
|
|
1209
|
+
...current,
|
|
1210
|
+
allow: add
|
|
1211
|
+
? [...(current.allow ?? []), name]
|
|
1212
|
+
: (current.allow ?? []).filter((entry) => entry !== name),
|
|
1213
|
+
fingerprint: null,
|
|
1214
|
+
}));
|
|
1215
|
+
}}
|
|
1216
|
+
className="size-4"
|
|
1217
|
+
/>
|
|
1218
|
+
<span className="min-w-0 truncate">{name}</span>
|
|
1219
|
+
</label>
|
|
1220
|
+
</li>
|
|
1221
|
+
))}
|
|
1222
|
+
</ul>
|
|
1223
|
+
)}
|
|
1224
|
+
{/* Two sentences that only appear when they are true, and neither is the generic hint:
|
|
1225
|
+
a narrowing to nothing cannot be published, and a name the catalog does not carry is
|
|
1226
|
+
something the author has to see rather than lose. */}
|
|
1227
|
+
{configuration.allow?.length === 0 && (
|
|
1228
|
+
<p className="text-xs text-destructive">{i18n.t("flows.toolAllowNone")}</p>
|
|
1229
|
+
)}
|
|
1230
|
+
{toolNamesFailed && (
|
|
1231
|
+
<p className="text-xs text-destructive">{i18n.t("flows.toolFunctionsFailed")}</p>
|
|
1232
|
+
)}
|
|
1233
|
+
{!toolNamesFailed && missing.length > 0 && (
|
|
1234
|
+
<p className="text-xs text-destructive">
|
|
1235
|
+
{i18n.t("flows.toolAllowUnknown", { count: missing.length })}
|
|
1236
|
+
</p>
|
|
1237
|
+
)}
|
|
1238
|
+
<p className="text-xs text-muted-foreground">{i18n.t("flows.toolAllowHint")}</p>
|
|
1239
|
+
</fieldset>
|
|
997
1240
|
)}
|
|
998
1241
|
</div>
|
|
999
1242
|
);
|
|
@@ -1015,7 +1258,7 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1015
1258
|
needs.data !== undefined &&
|
|
1016
1259
|
needs.data.nodes.length === 0 &&
|
|
1017
1260
|
needs.data.hiddenNodes === 0 &&
|
|
1018
|
-
needs.data.
|
|
1261
|
+
needs.data.servers.length === 0;
|
|
1019
1262
|
return (
|
|
1020
1263
|
<section aria-label={i18n.t("flows.needs")} className="mt-auto border-t p-5">
|
|
1021
1264
|
<h2 className="font-semibold">{i18n.t("flows.needs")}</h2>
|
|
@@ -1052,13 +1295,13 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1052
1295
|
</ul>
|
|
1053
1296
|
</>
|
|
1054
1297
|
)}
|
|
1055
|
-
{needs.data.
|
|
1298
|
+
{needs.data.servers.length > 0 && (
|
|
1056
1299
|
<>
|
|
1057
1300
|
<h3 className="mt-4 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
1058
1301
|
{i18n.t("flows.needsTools")}
|
|
1059
1302
|
</h3>
|
|
1060
1303
|
<ul className="mt-2 space-y-1 text-sm">
|
|
1061
|
-
{needs.data.
|
|
1304
|
+
{needs.data.servers.map((name) => (
|
|
1062
1305
|
<li key={name} className="flex items-center gap-2">
|
|
1063
1306
|
<Wrench aria-hidden="true" className="size-3.5 shrink-0" />
|
|
1064
1307
|
<span className="min-w-0 truncate">{name}</span>
|
|
@@ -1067,7 +1310,6 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1067
1310
|
</ul>
|
|
1068
1311
|
</>
|
|
1069
1312
|
)}
|
|
1070
|
-
<p className="mt-4 text-xs text-muted-foreground">{i18n.t("flows.needsHint")}</p>
|
|
1071
1313
|
</>
|
|
1072
1314
|
)}
|
|
1073
1315
|
</section>
|
|
@@ -1118,47 +1360,6 @@ function SubflowVersionField({
|
|
|
1118
1360
|
);
|
|
1119
1361
|
}
|
|
1120
1362
|
|
|
1121
|
-
function ToolArgumentsEditor({
|
|
1122
|
-
label,
|
|
1123
|
-
invalidLabel,
|
|
1124
|
-
initialValue,
|
|
1125
|
-
onValid,
|
|
1126
|
-
}: {
|
|
1127
|
-
label: string;
|
|
1128
|
-
invalidLabel: string;
|
|
1129
|
-
initialValue: Record<string, unknown>;
|
|
1130
|
-
onValid(value: Record<string, unknown>): void;
|
|
1131
|
-
}) {
|
|
1132
|
-
const [text, setText] = useState(() => JSON.stringify(initialValue, null, 2));
|
|
1133
|
-
const [invalid, setInvalid] = useState(false);
|
|
1134
|
-
return (
|
|
1135
|
-
<label className="mt-4 block text-sm font-medium">
|
|
1136
|
-
{label}
|
|
1137
|
-
<textarea
|
|
1138
|
-
rows={6}
|
|
1139
|
-
value={text}
|
|
1140
|
-
onChange={(event) => {
|
|
1141
|
-
const next = event.target.value;
|
|
1142
|
-
setText(next);
|
|
1143
|
-
try {
|
|
1144
|
-
const parsed: unknown = JSON.parse(next);
|
|
1145
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1146
|
-
setInvalid(true);
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
setInvalid(false);
|
|
1150
|
-
onValid(parsed as Record<string, unknown>);
|
|
1151
|
-
} catch {
|
|
1152
|
-
setInvalid(true);
|
|
1153
|
-
}
|
|
1154
|
-
}}
|
|
1155
|
-
className="mt-2 w-full resize-y rounded-md border bg-background px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
1156
|
-
/>
|
|
1157
|
-
{invalid && <span className="mt-1 block text-xs text-destructive">{invalidLabel}</span>}
|
|
1158
|
-
</label>
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1363
|
function Field({
|
|
1163
1364
|
label,
|
|
1164
1365
|
value,
|
|
@@ -34,8 +34,8 @@ function targetOf(entry: TreeEntry): ResourceTarget {
|
|
|
34
34
|
* fetch separately show it differently the moment one of them is stale, and that is the kind of
|
|
35
35
|
* difference a customer finds before anybody here does. The other half of the bargain is free —
|
|
36
36
|
* every invalidation the tree already does after a create, a move or an archive lands here too, and
|
|
37
|
-
* that now includes the ones this screen's own menu makes:
|
|
38
|
-
* `
|
|
37
|
+
* that now includes the ones this screen's own menu makes: since #519 they all refresh every tree
|
|
38
|
+
* level (`allTreeLevelsKey`), and this level is one of them.
|
|
39
39
|
*
|
|
40
40
|
* ⚠️ There is no "Kind" column since #101. The icon says what a row is, in the same picture the
|
|
41
41
|
* sidebar draws two centimetres to its left, and `sr-only` says it in words for everyone who does
|
package/src/i18n/de.json
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"archive.restoreFailed": "Es wurde nicht wiederhergestellt. Vielleicht hat jemand anderes es geändert — lade neu und versuche es erneut.",
|
|
98
98
|
"archive.purge": "{title} endgültig löschen",
|
|
99
99
|
"archive.purge.title": "Endgültig löschen?",
|
|
100
|
-
"archive.purge.body": "„{title}
|
|
100
|
+
"archive.purge.body": "„{title}“ und alles, was dazugehört — jede Version, der Inhalt und die Einträge im Suchindex — ist danach weg. Das lässt sich nicht rückgängig machen.",
|
|
101
101
|
"archive.purge.links.none": "Keine anderen Dokumente verweisen darauf.",
|
|
102
102
|
"archive.purge.links.one": "Ein anderes Dokument verweist darauf; dieser Verweis wird brechen.",
|
|
103
103
|
"archive.purge.links.many": "{count} andere Dokumente verweisen darauf; diese Verweise werden brechen.",
|
|
@@ -215,8 +215,6 @@
|
|
|
215
215
|
"tools.toolCount": "{count} Werkzeuge",
|
|
216
216
|
"tools.toolCountOne": "1 Werkzeug",
|
|
217
217
|
"tools.liveNote": "Diese Liste ist eine Live-Abfrage mit deinem eigenen Portal-Zugriff. Intel speichert weder die Werkzeuge noch, wer sie nutzen darf; beides entscheidet das Portal.",
|
|
218
|
-
"tools.arguments": "Argumente",
|
|
219
|
-
"tools.invalidJson": "Die Argumente müssen ein JSON-Objekt in geschweiften Klammern sein; eine Liste oder ein einzelner Wert funktioniert nicht.",
|
|
220
218
|
"flows.select": "Wähle einen Flow oder lege einen an, um den visuellen Editor zu öffnen.",
|
|
221
219
|
"flows.inspect": "Wähle einen Schritt, um seine Konfiguration zu bearbeiten.",
|
|
222
220
|
"flows.publish": "Veröffentlichen",
|
|
@@ -277,11 +275,10 @@
|
|
|
277
275
|
"flows.publishPreviewUnavailable": "Der aufgerufene Flow hat noch nichts veröffentlicht, das lässt sich nicht einfrieren.",
|
|
278
276
|
"flows.publishConfirm": "Veröffentlichen",
|
|
279
277
|
"flows.publishFailed": "Das Veröffentlichen ist fehlgeschlagen. Lade die neueste Fassung und versuche es erneut.",
|
|
280
|
-
"flows.tool": "MCP-
|
|
281
|
-
"flows.selectTool": "
|
|
278
|
+
"flows.tool": "MCP-Server",
|
|
279
|
+
"flows.selectTool": "Server auswählen",
|
|
282
280
|
"flows.operationFailed": "Der Flow-Vorgang ist fehlgeschlagen. Lade die neueste Fassung und versuche es erneut.",
|
|
283
281
|
"flows.needs": "Was dieser Flow braucht",
|
|
284
|
-
"flows.needsHint": "Aus dem Graphen gelesen. Ob eine bestimmte Person davon etwas erreicht, entscheidet sich, wenn sie ihn ausführt.",
|
|
285
282
|
"flows.needsNodes": "Dokumente",
|
|
286
283
|
"flows.needsTools": "Werkzeuge",
|
|
287
284
|
"flows.needsHidden": "{count} weitere, die du nicht sehen kannst",
|
|
@@ -354,5 +351,18 @@
|
|
|
354
351
|
"common.noAccess": "Das ist für dich nicht verfügbar. Es existiert nicht, oder es ist nicht mehr für dich freigegeben.",
|
|
355
352
|
"common.noPermission": "Dir fehlt die Berechtigung, das zu sehen.",
|
|
356
353
|
"title.descriptionMore": "Mehr",
|
|
357
|
-
"title.descriptionLess": "Weniger"
|
|
354
|
+
"title.descriptionLess": "Weniger",
|
|
355
|
+
"flows.toolServersEmpty": "Über das Portal erreichst du noch keinen MCP-Server.",
|
|
356
|
+
"flows.toolServersFailed": "Die Serverliste ließ sich nicht laden, daher lässt sich gerade keiner auswählen.",
|
|
357
|
+
"flows.toolAllowAll": "Der Agent darf jede Funktion nutzen",
|
|
358
|
+
"flows.toolAllowSome": "Den Agenten auf bestimmte Funktionen hinweisen",
|
|
359
|
+
"flows.toolAllowHint": "Intel sagt, wofür ein Schritt da ist; der Agent wählt den Aufruf und erreicht das Portal mit seinen eigenen Rechten.",
|
|
360
|
+
"flows.toolFunctions": "Funktionen",
|
|
361
|
+
"flows.toolServersDisconnected": "Du bist nicht mit dem Portal verbunden, daher lässt sich kein Server wählen. Verbinde es zuerst in den Werkzeug-Einstellungen.",
|
|
362
|
+
"flows.toolAllowNone": "Es ist noch keine Funktion ausgewählt, daher lässt sich dieser Schritt nicht veröffentlichen.",
|
|
363
|
+
"flows.toolAllowUnknown": "{count} ausgewählte Funktion(en) bietet dieser Server gerade nicht an.",
|
|
364
|
+
"flows.toolStepNeedsServer": "Dem Schritt „{label}“ fehlt noch ein Server.",
|
|
365
|
+
"flows.toolFunctionsFailed": "Die Funktionsliste ließ sich nicht laden, daher ist die Auswahl möglicherweise unvollständig.",
|
|
366
|
+
"flows.publishPreviewToolUnavailable": "Diesen Server erreichst du nicht, daher wird das Veröffentlichen abgelehnt.",
|
|
367
|
+
"flows.publishPreviewToolAllow": "Eingefroren auf: {functions}"
|
|
358
368
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -215,8 +215,6 @@
|
|
|
215
215
|
"tools.toolCount": "{count} tools",
|
|
216
216
|
"tools.toolCountOne": "1 tool",
|
|
217
217
|
"tools.liveNote": "This list is a live query with your own portal access. Intel stores neither the tools nor who may use them; the portal decides both.",
|
|
218
|
-
"tools.arguments": "Arguments",
|
|
219
|
-
"tools.invalidJson": "The arguments must be a JSON object in curly braces; a list or a single value will not work.",
|
|
220
218
|
"flows.select": "Select a flow or create one to open the visual editor.",
|
|
221
219
|
"flows.inspect": "Select a node to edit its configuration.",
|
|
222
220
|
"flows.publish": "Publish",
|
|
@@ -277,11 +275,10 @@
|
|
|
277
275
|
"flows.publishPreviewUnavailable": "The called flow has published nothing yet, so this cannot be frozen.",
|
|
278
276
|
"flows.publishConfirm": "Publish",
|
|
279
277
|
"flows.publishFailed": "Publishing failed. Reload the latest version and try again.",
|
|
280
|
-
"flows.tool": "MCP
|
|
281
|
-
"flows.selectTool": "Select a
|
|
278
|
+
"flows.tool": "MCP server",
|
|
279
|
+
"flows.selectTool": "Select a server",
|
|
282
280
|
"flows.operationFailed": "The flow operation failed. Reload the latest version and try again.",
|
|
283
281
|
"flows.needs": "What this flow needs",
|
|
284
|
-
"flows.needsHint": "Read out of the graph. Whether a given person may reach any of it is decided when they run it.",
|
|
285
282
|
"flows.needsNodes": "Documents",
|
|
286
283
|
"flows.needsTools": "Tools",
|
|
287
284
|
"flows.needsHidden": "{count} more you cannot see",
|
|
@@ -354,5 +351,18 @@
|
|
|
354
351
|
"common.noAccess": "This is not available to you. It may not exist, or it may no longer be shared with you.",
|
|
355
352
|
"common.noPermission": "You do not have permission to see this.",
|
|
356
353
|
"title.descriptionMore": "More",
|
|
357
|
-
"title.descriptionLess": "Less"
|
|
354
|
+
"title.descriptionLess": "Less",
|
|
355
|
+
"flows.toolServersEmpty": "You reach no MCP server through the portal yet.",
|
|
356
|
+
"flows.toolServersFailed": "The list of servers could not be loaded, so none can be chosen right now.",
|
|
357
|
+
"flows.toolAllowAll": "Let the agent use any function",
|
|
358
|
+
"flows.toolAllowSome": "Point the agent at specific functions",
|
|
359
|
+
"flows.toolAllowHint": "Intel says what a step is for; the agent chooses the call and reaches the portal with its own permissions.",
|
|
360
|
+
"flows.toolFunctions": "Functions",
|
|
361
|
+
"flows.toolServersDisconnected": "You are not connected to the portal, so no server can be chosen. Connect it in the tool settings first.",
|
|
362
|
+
"flows.toolAllowNone": "No function is selected yet, so this step cannot be published.",
|
|
363
|
+
"flows.toolAllowUnknown": "{count} selected function(s) are not offered by this server right now.",
|
|
364
|
+
"flows.toolStepNeedsServer": "The step “{label}” still needs a server.",
|
|
365
|
+
"flows.toolFunctionsFailed": "The list of functions could not be loaded, so the selection may be incomplete.",
|
|
366
|
+
"flows.publishPreviewToolUnavailable": "You do not reach this server, so publishing will be refused.",
|
|
367
|
+
"flows.publishPreviewToolAllow": "Frozen to: {functions}"
|
|
358
368
|
}
|
package/src/i18n/es.json
CHANGED
|
@@ -215,8 +215,6 @@
|
|
|
215
215
|
"tools.toolCount": "{count} herramientas",
|
|
216
216
|
"tools.toolCountOne": "1 herramienta",
|
|
217
217
|
"tools.liveNote": "Esta lista es una consulta en vivo con tu propio acceso al portal. Intel no guarda ni las herramientas ni quién puede usarlas; ambas cosas las decide el portal.",
|
|
218
|
-
"tools.arguments": "Argumentos",
|
|
219
|
-
"tools.invalidJson": "Los argumentos tienen que ser un objeto JSON entre llaves; una lista o un valor suelto no sirven.",
|
|
220
218
|
"flows.select": "Elige un flujo o crea uno para abrir el editor visual.",
|
|
221
219
|
"flows.inspect": "Elige un paso para editar su configuración.",
|
|
222
220
|
"flows.publish": "Publicar",
|
|
@@ -277,11 +275,10 @@
|
|
|
277
275
|
"flows.publishPreviewUnavailable": "El flujo llamado todavía no ha publicado nada, así que esto no se puede congelar.",
|
|
278
276
|
"flows.publishConfirm": "Publicar",
|
|
279
277
|
"flows.publishFailed": "La publicación ha fallado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
280
|
-
"flows.tool": "
|
|
281
|
-
"flows.selectTool": "
|
|
278
|
+
"flows.tool": "Servidor MCP",
|
|
279
|
+
"flows.selectTool": "Elegir un servidor",
|
|
282
280
|
"flows.operationFailed": "La operación del flujo ha fallado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
283
281
|
"flows.needs": "Qué necesita este flujo",
|
|
284
|
-
"flows.needsHint": "Leído del grafo. Si una persona concreta llega a algo de esto se decide cuando lo ejecuta.",
|
|
285
282
|
"flows.needsNodes": "Documentos",
|
|
286
283
|
"flows.needsTools": "Herramientas",
|
|
287
284
|
"flows.needsHidden": "{count} más que no puedes ver",
|
|
@@ -354,5 +351,18 @@
|
|
|
354
351
|
"common.noAccess": "Esto no está disponible para ti. Puede que no exista o que ya no esté compartido contigo.",
|
|
355
352
|
"common.noPermission": "No tienes permiso para ver esto.",
|
|
356
353
|
"title.descriptionMore": "Más",
|
|
357
|
-
"title.descriptionLess": "Menos"
|
|
354
|
+
"title.descriptionLess": "Menos",
|
|
355
|
+
"flows.toolServersEmpty": "Todavía no alcanzas ningún servidor MCP a través del portal.",
|
|
356
|
+
"flows.toolServersFailed": "No se pudo cargar la lista de servidores, así que ahora no se puede elegir ninguno.",
|
|
357
|
+
"flows.toolAllowAll": "El agente puede usar cualquier función",
|
|
358
|
+
"flows.toolAllowSome": "Indicar al agente funciones concretas",
|
|
359
|
+
"flows.toolAllowHint": "Intel dice para qué sirve un paso; el agente elige la llamada y accede al portal con sus propios permisos.",
|
|
360
|
+
"flows.toolFunctions": "Funciones",
|
|
361
|
+
"flows.toolServersDisconnected": "No estás conectado al portal, así que no se puede elegir ningún servidor. Conéctalo primero en la configuración de herramientas.",
|
|
362
|
+
"flows.toolAllowNone": "Todavía no hay ninguna función seleccionada, así que este paso no se puede publicar.",
|
|
363
|
+
"flows.toolAllowUnknown": "Este servidor no ofrece ahora mismo {count} función(es) seleccionada(s).",
|
|
364
|
+
"flows.toolStepNeedsServer": "Al paso «{label}» todavía le falta un servidor.",
|
|
365
|
+
"flows.toolFunctionsFailed": "No se pudo cargar la lista de funciones, así que la selección puede estar incompleta.",
|
|
366
|
+
"flows.publishPreviewToolUnavailable": "No alcanzas este servidor, así que la publicación será rechazada.",
|
|
367
|
+
"flows.publishPreviewToolAllow": "Congelado en: {functions}"
|
|
358
368
|
}
|
|
@@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
2
2
|
import { zipSync } from "fflate";
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
-
import {
|
|
5
|
+
import { allTreeLevelsKey } from "@/app/tree-move/tree-move.tsx";
|
|
6
6
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
7
7
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
8
8
|
|
|
@@ -93,7 +93,10 @@ export function useBundleImport(options: {
|
|
|
93
93
|
onSuccess: async (imported) => {
|
|
94
94
|
await options.onImported?.(imported.parentId);
|
|
95
95
|
await Promise.all([
|
|
96
|
-
|
|
96
|
+
// ⚠️ Every level, not the one imported into: the target folder's own row carries whether it
|
|
97
|
+
// can be opened, and it stands one level higher (#519). An import into an empty folder was
|
|
98
|
+
// otherwise unreachable in the tree until the page was reloaded.
|
|
99
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
97
100
|
queryClient.invalidateQueries({ queryKey: ["node-graph"] }),
|
|
98
101
|
// The graph view of that same level draws exactly this list (#19).
|
|
99
102
|
queryClient.invalidateQueries({ queryKey: ["relation-graph"] }),
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import type * as React from "react";
|
|
21
21
|
import { useState } from "react";
|
|
22
22
|
import { AccessSummary } from "@/access-summary/access-summary.tsx";
|
|
23
|
-
import { moveErrorKey, useTreeMove } from "@/app/tree-move/tree-move.tsx";
|
|
23
|
+
import { allTreeLevelsKey, moveErrorKey, useTreeMove } from "@/app/tree-move/tree-move.tsx";
|
|
24
24
|
import {
|
|
25
25
|
DropdownMenu,
|
|
26
26
|
DropdownMenuContent,
|
|
@@ -80,10 +80,6 @@ function titleOf(target: ResourceTarget): string {
|
|
|
80
80
|
return target.type === "node" ? target.node.title : target.flow.title;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function parentOfTarget(target: ResourceTarget): string | null {
|
|
84
|
-
return target.type === "node" ? target.node.parentId : target.flow.parentId;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
83
|
/**
|
|
88
84
|
* Which sentence explains a verb.
|
|
89
85
|
*
|
|
@@ -175,9 +171,14 @@ export function ResourceMenu({
|
|
|
175
171
|
// reads the record, and the flat collections behind the search, the link picker and the relation
|
|
176
172
|
// view all carry the title as well — a rename that only reached the tree would leave yesterday's
|
|
177
173
|
// name standing on three other screens.
|
|
174
|
+
//
|
|
175
|
+
// ⚠️ Every level rather than the one this row sits in: archiving the last row of a folder takes
|
|
176
|
+
// that folder's arrow away, and the arrow is drawn from the folder's own row one level higher
|
|
177
|
+
// (`allTreeLevelsKey`, #519). Renaming needs no more than the one level — but two invalidations
|
|
178
|
+
// that differ per entry are how one of them goes missing.
|
|
178
179
|
async function refresh(): Promise<void> {
|
|
179
180
|
await Promise.all([
|
|
180
|
-
queryClient.invalidateQueries({ queryKey:
|
|
181
|
+
queryClient.invalidateQueries({ queryKey: allTreeLevelsKey }),
|
|
181
182
|
queryClient.invalidateQueries({
|
|
182
183
|
queryKey: target.type === "flow" ? ["flow", id] : ["node", id],
|
|
183
184
|
}),
|