@anchrd/intel-ui 0.25.0 → 0.29.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 +90 -40
- package/src/app/tree-move/tree-move.tsx +119 -22
- package/src/app/view-toggle/view-toggle.tsx +1 -1
- package/src/archive/archive.tsx +7 -6
- package/src/data/intel-data-provider/intel-data-provider.ts +4 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +2 -0
- package/src/flow-runs/flow-runs.tsx +5 -5
- package/src/flows/flows.tsx +50 -102
- package/src/folder-contents/folder-contents.tsx +8 -8
- package/src/i18n/de.json +9 -5
- package/src/i18n/en.json +9 -5
- package/src/i18n/es.json +10 -6
- package/src/import-bundle/import-bundle.tsx +5 -2
- package/src/main.tsx +6 -3
- package/src/node-editor/node-editor.tsx +4 -11
- package/src/node-table/node-table.tsx +3 -2
- package/src/nodes/nodes.tsx +5 -78
- package/src/resource-menu/resource-menu.tsx +92 -37
- package/src/save-button/save-button.tsx +12 -31
- package/src/time/relative-time.tsx +41 -0
- package/src/time/time-context.tsx +7 -1
- package/src/time/time.ts +25 -0
- package/src/title-row/title-row.tsx +124 -4
package/src/flows/flows.tsx
CHANGED
|
@@ -38,7 +38,7 @@ import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
|
38
38
|
import { selectedFrom, viewFrom } from "@/router/selection-search.ts";
|
|
39
39
|
import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
40
40
|
import { useResolvedTheme } from "@/theme/theme-context.tsx";
|
|
41
|
-
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
41
|
+
import { TitleRow, TitleRowFrame, TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
42
42
|
|
|
43
43
|
type CanvasNode = ReactFlowNode<{ node: FlowNode }, "intel">;
|
|
44
44
|
type CanvasEdge = Edge;
|
|
@@ -285,6 +285,7 @@ function newNode(
|
|
|
285
285
|
index: number,
|
|
286
286
|
firstNodeId?: string,
|
|
287
287
|
firstFlowId?: string,
|
|
288
|
+
firstServer?: string,
|
|
288
289
|
): FlowNode {
|
|
289
290
|
const common = {
|
|
290
291
|
id: `${kind}-${crypto.randomUUID()}`,
|
|
@@ -315,10 +316,13 @@ function newNode(
|
|
|
315
316
|
return {
|
|
316
317
|
...common,
|
|
317
318
|
kind,
|
|
319
|
+
// ⚠️ A server, not an empty string. `ToolServerHandle` has a minimum length, so a draft
|
|
320
|
+
// with `""` cannot be saved at all — the author would build a step and lose it at the next
|
|
321
|
+
// save with a raw field-path error. The tree link one line up takes the same way out.
|
|
318
322
|
configuration: {
|
|
319
|
-
|
|
323
|
+
server: firstServer ?? "",
|
|
324
|
+
allow: null,
|
|
320
325
|
fingerprint: null,
|
|
321
|
-
arguments: {},
|
|
322
326
|
},
|
|
323
327
|
};
|
|
324
328
|
case "condition":
|
|
@@ -361,9 +365,11 @@ function FlowsEditor() {
|
|
|
361
365
|
const theme = useResolvedTheme();
|
|
362
366
|
const queryClient = useQueryClient();
|
|
363
367
|
const navigate = useNavigate();
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
// The servers a tool step can name (#489). The flat function list the picker used to show is
|
|
369
|
+
// what that ticket set out to remove; which function runs is decided while the flow runs.
|
|
370
|
+
const toolServers = useQuery({
|
|
371
|
+
queryKey: ["tool-servers"],
|
|
372
|
+
queryFn: () => data.listToolServers(),
|
|
367
373
|
});
|
|
368
374
|
// A tree link points at a node, so the editor needs candidates. The graph is the authorized
|
|
369
375
|
// flat list of them; walking the folder tree for a picker would be the N+1 all over again.
|
|
@@ -455,7 +461,7 @@ function FlowsEditor() {
|
|
|
455
461
|
}
|
|
456
462
|
|
|
457
463
|
return (
|
|
458
|
-
<
|
|
464
|
+
<TitleRowFrame className="flex h-full min-h-0 flex-col">
|
|
459
465
|
{selectedFlowId && document.data ? (
|
|
460
466
|
<FlowTitle
|
|
461
467
|
flow={document.data.flow}
|
|
@@ -519,6 +525,7 @@ function FlowsEditor() {
|
|
|
519
525
|
nodes.length,
|
|
520
526
|
nodeGraph.data?.nodes[0]?.id,
|
|
521
527
|
callable.data?.items.find((entry) => entry.id !== selectedFlowId)?.id,
|
|
528
|
+
toolServers.data?.items[0]?.handle,
|
|
522
529
|
);
|
|
523
530
|
setNodes((current) => [
|
|
524
531
|
...current,
|
|
@@ -634,14 +641,18 @@ function FlowsEditor() {
|
|
|
634
641
|
<Controls />
|
|
635
642
|
</ReactFlow>
|
|
636
643
|
</section>
|
|
637
|
-
<aside className="flex w-80 shrink-0 flex-col
|
|
638
|
-
<
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
644
|
+
<aside className="flex w-80 shrink-0 flex-col border-l bg-card">
|
|
645
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-y-auto">
|
|
646
|
+
<NodeInspector
|
|
647
|
+
node={selectedNode}
|
|
648
|
+
update={updateNode}
|
|
649
|
+
toolServers={toolServers.data?.items ?? []}
|
|
650
|
+
flows={(callable.data?.items ?? []).filter(
|
|
651
|
+
(entry) => entry.id !== selectedFlowId,
|
|
652
|
+
)}
|
|
653
|
+
/>
|
|
654
|
+
<FlowNeeds flowId={selectedFlowId} />
|
|
655
|
+
</TitleRowScrollArea>
|
|
645
656
|
</aside>
|
|
646
657
|
</>
|
|
647
658
|
)}
|
|
@@ -673,7 +684,7 @@ function FlowsEditor() {
|
|
|
673
684
|
}}
|
|
674
685
|
/>
|
|
675
686
|
) : null}
|
|
676
|
-
</
|
|
687
|
+
</TitleRowFrame>
|
|
677
688
|
);
|
|
678
689
|
}
|
|
679
690
|
|
|
@@ -729,7 +740,7 @@ function FlowTitle({
|
|
|
729
740
|
applies EVERYWHERE stands. But it says how THIS flow is shown, and so belongs in the line
|
|
730
741
|
that names this flow. A flow is the only level with runs, and therefore the only one with
|
|
731
742
|
three views (#35). */}
|
|
732
|
-
<
|
|
743
|
+
<SaveButton dirty={dirty && canMutate} saving={saving} onSave={onSave} />
|
|
733
744
|
<TooltipProvider delayDuration={300}>
|
|
734
745
|
<Tooltip>
|
|
735
746
|
<TooltipTrigger asChild>
|
|
@@ -742,8 +753,8 @@ function FlowTitle({
|
|
|
742
753
|
onClick={onPublish}
|
|
743
754
|
disabled={!publishable}
|
|
744
755
|
aria-label={publishLabel}
|
|
745
|
-
className={`inline-flex size-8 items-center justify-center rounded-md
|
|
746
|
-
publishable && unpublished ? "
|
|
756
|
+
className={`inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50 ${
|
|
757
|
+
publishable && unpublished ? "bg-primary/10 text-primary" : ""
|
|
747
758
|
}`}
|
|
748
759
|
>
|
|
749
760
|
<Send aria-hidden="true" className="size-4" />
|
|
@@ -753,14 +764,7 @@ function FlowTitle({
|
|
|
753
764
|
<TooltipContent>{publishLabel}</TooltipContent>
|
|
754
765
|
</Tooltip>
|
|
755
766
|
</TooltipProvider>
|
|
756
|
-
{
|
|
757
|
-
one (#432) — the same distinction the publish button next to it already draws. */}
|
|
758
|
-
<SaveButton
|
|
759
|
-
dirty={dirty && canMutate}
|
|
760
|
-
saving={saving}
|
|
761
|
-
stored={flow.currentVersionId !== null}
|
|
762
|
-
onSave={onSave}
|
|
763
|
-
/>
|
|
767
|
+
<ViewToggle views={["editor", "graph", "runs"]} />
|
|
764
768
|
</TitleRow>
|
|
765
769
|
);
|
|
766
770
|
}
|
|
@@ -855,12 +859,12 @@ function PublishPreview({
|
|
|
855
859
|
function NodeInspector({
|
|
856
860
|
node,
|
|
857
861
|
update,
|
|
858
|
-
|
|
862
|
+
toolServers,
|
|
859
863
|
flows,
|
|
860
864
|
}: {
|
|
861
865
|
node: CanvasNode | null;
|
|
862
866
|
update(fn: (node: FlowNode) => FlowNode): void;
|
|
863
|
-
|
|
867
|
+
toolServers: Array<{ handle: string; name: string; toolCount: number }>;
|
|
864
868
|
flows: Flow[];
|
|
865
869
|
}) {
|
|
866
870
|
const i18n = useI18n();
|
|
@@ -952,50 +956,36 @@ function NodeInspector({
|
|
|
952
956
|
<div className="mt-4">
|
|
953
957
|
<label className="block text-sm font-medium">
|
|
954
958
|
{i18n.t("flows.tool")}
|
|
959
|
+
{/* ⚠️ The step names a SERVER since #489. This picker is the smallest thing that keeps
|
|
960
|
+
the field usable on the new model; the real one — server names from the portal plus
|
|
961
|
+
an optional narrowing to single functions — is #489's second package and is
|
|
962
|
+
deliberately not smuggled in here. */}
|
|
955
963
|
<select
|
|
956
|
-
value={contract.configuration.
|
|
964
|
+
value={contract.configuration.server}
|
|
957
965
|
onChange={(event) => {
|
|
958
|
-
const
|
|
966
|
+
const server = event.target.value;
|
|
959
967
|
update((value) =>
|
|
960
968
|
value.kind === "tool"
|
|
961
969
|
? {
|
|
962
970
|
...value,
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
tools.find((entry) => entry.name === toolName)?.fingerprint ?? null,
|
|
968
|
-
},
|
|
971
|
+
// The fingerprint belongs to a surface that publishing freezes, and it is
|
|
972
|
+
// the server's to compute — a value guessed in the browser would be a
|
|
973
|
+
// second answer to a question the API already owns.
|
|
974
|
+
configuration: { ...value.configuration, server, fingerprint: null },
|
|
969
975
|
}
|
|
970
976
|
: value,
|
|
971
977
|
);
|
|
972
978
|
}}
|
|
973
979
|
className="mt-2 w-full rounded-md border bg-background px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
974
980
|
>
|
|
975
|
-
<option value="
|
|
976
|
-
{
|
|
977
|
-
<option key={entry.
|
|
978
|
-
{entry.
|
|
981
|
+
<option value="">{i18n.t("flows.selectTool")}</option>
|
|
982
|
+
{toolServers.map((entry) => (
|
|
983
|
+
<option key={entry.handle} value={entry.handle}>
|
|
984
|
+
{entry.name}
|
|
979
985
|
</option>
|
|
980
986
|
))}
|
|
981
987
|
</select>
|
|
982
988
|
</label>
|
|
983
|
-
<ToolArgumentsEditor
|
|
984
|
-
key={contract.id}
|
|
985
|
-
label={i18n.t("tools.arguments")}
|
|
986
|
-
invalidLabel={i18n.t("tools.invalidJson")}
|
|
987
|
-
initialValue={contract.configuration.arguments}
|
|
988
|
-
onValid={(argumentsValue) => {
|
|
989
|
-
update((value) =>
|
|
990
|
-
value.kind === "tool"
|
|
991
|
-
? {
|
|
992
|
-
...value,
|
|
993
|
-
configuration: { ...value.configuration, arguments: argumentsValue },
|
|
994
|
-
}
|
|
995
|
-
: value,
|
|
996
|
-
);
|
|
997
|
-
}}
|
|
998
|
-
/>
|
|
999
989
|
</div>
|
|
1000
990
|
)}
|
|
1001
991
|
</div>
|
|
@@ -1018,7 +1008,7 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1018
1008
|
needs.data !== undefined &&
|
|
1019
1009
|
needs.data.nodes.length === 0 &&
|
|
1020
1010
|
needs.data.hiddenNodes === 0 &&
|
|
1021
|
-
needs.data.
|
|
1011
|
+
needs.data.servers.length === 0;
|
|
1022
1012
|
return (
|
|
1023
1013
|
<section aria-label={i18n.t("flows.needs")} className="mt-auto border-t p-5">
|
|
1024
1014
|
<h2 className="font-semibold">{i18n.t("flows.needs")}</h2>
|
|
@@ -1055,13 +1045,13 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1055
1045
|
</ul>
|
|
1056
1046
|
</>
|
|
1057
1047
|
)}
|
|
1058
|
-
{needs.data.
|
|
1048
|
+
{needs.data.servers.length > 0 && (
|
|
1059
1049
|
<>
|
|
1060
1050
|
<h3 className="mt-4 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
1061
1051
|
{i18n.t("flows.needsTools")}
|
|
1062
1052
|
</h3>
|
|
1063
1053
|
<ul className="mt-2 space-y-1 text-sm">
|
|
1064
|
-
{needs.data.
|
|
1054
|
+
{needs.data.servers.map((name) => (
|
|
1065
1055
|
<li key={name} className="flex items-center gap-2">
|
|
1066
1056
|
<Wrench aria-hidden="true" className="size-3.5 shrink-0" />
|
|
1067
1057
|
<span className="min-w-0 truncate">{name}</span>
|
|
@@ -1070,7 +1060,6 @@ function FlowNeeds({ flowId }: { flowId: string }) {
|
|
|
1070
1060
|
</ul>
|
|
1071
1061
|
</>
|
|
1072
1062
|
)}
|
|
1073
|
-
<p className="mt-4 text-xs text-muted-foreground">{i18n.t("flows.needsHint")}</p>
|
|
1074
1063
|
</>
|
|
1075
1064
|
)}
|
|
1076
1065
|
</section>
|
|
@@ -1121,47 +1110,6 @@ function SubflowVersionField({
|
|
|
1121
1110
|
);
|
|
1122
1111
|
}
|
|
1123
1112
|
|
|
1124
|
-
function ToolArgumentsEditor({
|
|
1125
|
-
label,
|
|
1126
|
-
invalidLabel,
|
|
1127
|
-
initialValue,
|
|
1128
|
-
onValid,
|
|
1129
|
-
}: {
|
|
1130
|
-
label: string;
|
|
1131
|
-
invalidLabel: string;
|
|
1132
|
-
initialValue: Record<string, unknown>;
|
|
1133
|
-
onValid(value: Record<string, unknown>): void;
|
|
1134
|
-
}) {
|
|
1135
|
-
const [text, setText] = useState(() => JSON.stringify(initialValue, null, 2));
|
|
1136
|
-
const [invalid, setInvalid] = useState(false);
|
|
1137
|
-
return (
|
|
1138
|
-
<label className="mt-4 block text-sm font-medium">
|
|
1139
|
-
{label}
|
|
1140
|
-
<textarea
|
|
1141
|
-
rows={6}
|
|
1142
|
-
value={text}
|
|
1143
|
-
onChange={(event) => {
|
|
1144
|
-
const next = event.target.value;
|
|
1145
|
-
setText(next);
|
|
1146
|
-
try {
|
|
1147
|
-
const parsed: unknown = JSON.parse(next);
|
|
1148
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1149
|
-
setInvalid(true);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
setInvalid(false);
|
|
1153
|
-
onValid(parsed as Record<string, unknown>);
|
|
1154
|
-
} catch {
|
|
1155
|
-
setInvalid(true);
|
|
1156
|
-
}
|
|
1157
|
-
}}
|
|
1158
|
-
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"
|
|
1159
|
-
/>
|
|
1160
|
-
{invalid && <span className="mt-1 block text-xs text-destructive">{invalidLabel}</span>}
|
|
1161
|
-
</label>
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1113
|
function Field({
|
|
1166
1114
|
label,
|
|
1167
1115
|
value,
|
|
@@ -15,7 +15,8 @@ import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
|
15
15
|
import { kindIcons } from "@/kind-icon.ts";
|
|
16
16
|
import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
17
17
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
18
|
-
import {
|
|
18
|
+
import { RelativeTime } from "@/time/relative-time.tsx";
|
|
19
|
+
import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
19
20
|
|
|
20
21
|
// The same record the menu is handed everywhere else, read off a level row. A re-labelling, never a
|
|
21
22
|
// second source of truth — a rename works on the whole record, not on a summary of it.
|
|
@@ -33,8 +34,8 @@ function targetOf(entry: TreeEntry): ResourceTarget {
|
|
|
33
34
|
* fetch separately show it differently the moment one of them is stale, and that is the kind of
|
|
34
35
|
* difference a customer finds before anybody here does. The other half of the bargain is free —
|
|
35
36
|
* every invalidation the tree already does after a create, a move or an archive lands here too, and
|
|
36
|
-
* that now includes the ones this screen's own menu makes:
|
|
37
|
-
* `
|
|
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.
|
|
38
39
|
*
|
|
39
40
|
* ⚠️ There is no "Kind" column since #101. The icon says what a row is, in the same picture the
|
|
40
41
|
* sidebar draws two centimetres to its left, and `sr-only` says it in words for everyone who does
|
|
@@ -43,7 +44,6 @@ function targetOf(entry: TreeEntry): ResourceTarget {
|
|
|
43
44
|
export function FolderContents({ folderId }: { folderId: string }) {
|
|
44
45
|
const { data } = useIntelRouterContext();
|
|
45
46
|
const i18n = useI18n();
|
|
46
|
-
const dateTime = useDateTime();
|
|
47
47
|
const navigate = useNavigate();
|
|
48
48
|
const level = useQuery({
|
|
49
49
|
queryKey: treeLevelKey(folderId),
|
|
@@ -74,7 +74,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
return (
|
|
77
|
-
<
|
|
77
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-y-auto p-6">
|
|
78
78
|
<div className="overflow-hidden rounded-lg border bg-card">
|
|
79
79
|
<Table>
|
|
80
80
|
<TableHeader className="[&_tr]:bg-muted/40">
|
|
@@ -119,7 +119,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
119
119
|
column, instead of five loose glyphs at five different widths. */}
|
|
120
120
|
<span
|
|
121
121
|
aria-hidden="true"
|
|
122
|
-
className="grid size-8 shrink-0 place-items-center rounded-md
|
|
122
|
+
className="grid size-8 shrink-0 place-items-center rounded-md bg-muted/50 text-muted-foreground transition-colors group-hover/row:bg-accent group-hover/row:text-accent-foreground"
|
|
123
123
|
>
|
|
124
124
|
<Icon className="size-4" />
|
|
125
125
|
</span>
|
|
@@ -132,7 +132,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
132
132
|
</button>
|
|
133
133
|
</TableCell>
|
|
134
134
|
<TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
|
|
135
|
-
{
|
|
135
|
+
<RelativeTime value={changed} />
|
|
136
136
|
</TableCell>
|
|
137
137
|
<TableCell className="px-4">
|
|
138
138
|
<ResourceAccessSummary
|
|
@@ -154,6 +154,6 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
154
154
|
</TableBody>
|
|
155
155
|
</Table>
|
|
156
156
|
</div>
|
|
157
|
-
</
|
|
157
|
+
</TitleRowScrollArea>
|
|
158
158
|
);
|
|
159
159
|
}
|
package/src/i18n/de.json
CHANGED
|
@@ -71,10 +71,15 @@
|
|
|
71
71
|
"tree.move.failed.other": "Es konnte nicht verschoben werden. Prüfe deinen Zugriff und versuche es erneut.",
|
|
72
72
|
"resource.menu": "Weitere Aktionen für {title}",
|
|
73
73
|
"resource.rename": "Umbenennen",
|
|
74
|
+
"resource.move": "Verschieben",
|
|
74
75
|
"resource.renameTitle": "{title} umbenennen",
|
|
75
76
|
"resource.archive": "Archivieren",
|
|
76
77
|
"resource.export": "Export",
|
|
77
78
|
"resource.import": "Import",
|
|
79
|
+
"resource.downloadCsv": "CSV herunterladen",
|
|
80
|
+
"resource.validate": "Prüfen",
|
|
81
|
+
"resource.links": "Verweise",
|
|
82
|
+
"resource.share": "Freigeben",
|
|
78
83
|
"tree.new.importZip": "ZIP-Bündel importieren",
|
|
79
84
|
"tree.new.importFolder": "Ordner importieren",
|
|
80
85
|
"tree.importZipInto": "ZIP-Bündel in {where} importieren",
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
"archive.purge.confirm": "Endgültig löschen",
|
|
104
109
|
"archive.purge.cancel": "Abbrechen",
|
|
105
110
|
"archive.purgeFailed": "Es konnte nicht vollständig gelöscht werden. Lade neu und versuche es erneut.",
|
|
106
|
-
"archive.
|
|
111
|
+
"archive.archived": "Archiviert",
|
|
107
112
|
"resource.conflict": "Nicht geändert: Jemand anderes hat diesen Eintrag zuerst geändert. Lade ihn neu und versuche es erneut.",
|
|
108
113
|
"resource.forbidden": "Nicht geändert: Du darfst diesen Eintrag nicht ändern.",
|
|
109
114
|
"resource.failed": "Die Änderung wurde nicht gespeichert. Lade die neueste Fassung und versuche es erneut.",
|
|
@@ -210,8 +215,6 @@
|
|
|
210
215
|
"tools.toolCount": "{count} Werkzeuge",
|
|
211
216
|
"tools.toolCountOne": "1 Werkzeug",
|
|
212
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.",
|
|
213
|
-
"tools.arguments": "Argumente",
|
|
214
|
-
"tools.invalidJson": "Die Argumente müssen ein JSON-Objekt in geschweiften Klammern sein; eine Liste oder ein einzelner Wert funktioniert nicht.",
|
|
215
218
|
"flows.select": "Wähle einen Flow oder lege einen an, um den visuellen Editor zu öffnen.",
|
|
216
219
|
"flows.inspect": "Wähle einen Schritt, um seine Konfiguration zu bearbeiten.",
|
|
217
220
|
"flows.publish": "Veröffentlichen",
|
|
@@ -276,7 +279,6 @@
|
|
|
276
279
|
"flows.selectTool": "Wähle ein gefundenes Werkzeug",
|
|
277
280
|
"flows.operationFailed": "Der Flow-Vorgang ist fehlgeschlagen. Lade die neueste Fassung und versuche es erneut.",
|
|
278
281
|
"flows.needs": "Was dieser Flow braucht",
|
|
279
|
-
"flows.needsHint": "Aus dem Graphen gelesen. Ob eine bestimmte Person davon etwas erreicht, entscheidet sich, wenn sie ihn ausführt.",
|
|
280
282
|
"flows.needsNodes": "Dokumente",
|
|
281
283
|
"flows.needsTools": "Werkzeuge",
|
|
282
284
|
"flows.needsHidden": "{count} weitere, die du nicht sehen kannst",
|
|
@@ -347,5 +349,7 @@
|
|
|
347
349
|
"common.close": "Schließen",
|
|
348
350
|
"common.unavailable": "Intel ist derzeit nicht verfügbar.",
|
|
349
351
|
"common.noAccess": "Das ist für dich nicht verfügbar. Es existiert nicht, oder es ist nicht mehr für dich freigegeben.",
|
|
350
|
-
"common.noPermission": "Dir fehlt die Berechtigung, das zu sehen."
|
|
352
|
+
"common.noPermission": "Dir fehlt die Berechtigung, das zu sehen.",
|
|
353
|
+
"title.descriptionMore": "Mehr",
|
|
354
|
+
"title.descriptionLess": "Weniger"
|
|
351
355
|
}
|
package/src/i18n/en.json
CHANGED
|
@@ -71,10 +71,15 @@
|
|
|
71
71
|
"tree.move.failed.other": "It could not be moved. Check your access and try again.",
|
|
72
72
|
"resource.menu": "More actions for {title}",
|
|
73
73
|
"resource.rename": "Rename",
|
|
74
|
+
"resource.move": "Move",
|
|
74
75
|
"resource.renameTitle": "Rename {title}",
|
|
75
76
|
"resource.archive": "Archive",
|
|
76
77
|
"resource.export": "Export",
|
|
77
78
|
"resource.import": "Import",
|
|
79
|
+
"resource.downloadCsv": "Download CSV",
|
|
80
|
+
"resource.validate": "Check",
|
|
81
|
+
"resource.links": "Links",
|
|
82
|
+
"resource.share": "Share",
|
|
78
83
|
"tree.new.importZip": "Import zip bundle",
|
|
79
84
|
"tree.new.importFolder": "Import folder",
|
|
80
85
|
"tree.importZipInto": "Import a zip bundle into {where}",
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
"archive.purge.confirm": "Delete for good",
|
|
104
109
|
"archive.purge.cancel": "Cancel",
|
|
105
110
|
"archive.purgeFailed": "It could not be deleted completely. Reload and try again.",
|
|
106
|
-
"archive.
|
|
111
|
+
"archive.archived": "Archived",
|
|
107
112
|
"resource.conflict": "It was not changed: somebody else changed this entry first. Reload it and try again.",
|
|
108
113
|
"resource.forbidden": "It was not changed: you may not change this entry.",
|
|
109
114
|
"resource.failed": "The change was not saved. Reload the latest version and try again.",
|
|
@@ -210,8 +215,6 @@
|
|
|
210
215
|
"tools.toolCount": "{count} tools",
|
|
211
216
|
"tools.toolCountOne": "1 tool",
|
|
212
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.",
|
|
213
|
-
"tools.arguments": "Arguments",
|
|
214
|
-
"tools.invalidJson": "The arguments must be a JSON object in curly braces; a list or a single value will not work.",
|
|
215
218
|
"flows.select": "Select a flow or create one to open the visual editor.",
|
|
216
219
|
"flows.inspect": "Select a node to edit its configuration.",
|
|
217
220
|
"flows.publish": "Publish",
|
|
@@ -276,7 +279,6 @@
|
|
|
276
279
|
"flows.selectTool": "Select a discovered tool",
|
|
277
280
|
"flows.operationFailed": "The flow operation failed. Reload the latest version and try again.",
|
|
278
281
|
"flows.needs": "What this flow needs",
|
|
279
|
-
"flows.needsHint": "Read out of the graph. Whether a given person may reach any of it is decided when they run it.",
|
|
280
282
|
"flows.needsNodes": "Documents",
|
|
281
283
|
"flows.needsTools": "Tools",
|
|
282
284
|
"flows.needsHidden": "{count} more you cannot see",
|
|
@@ -347,5 +349,7 @@
|
|
|
347
349
|
"common.close": "Close",
|
|
348
350
|
"common.unavailable": "Intel is currently unavailable.",
|
|
349
351
|
"common.noAccess": "This is not available to you. It may not exist, or it may no longer be shared with you.",
|
|
350
|
-
"common.noPermission": "You do not have permission to see this."
|
|
352
|
+
"common.noPermission": "You do not have permission to see this.",
|
|
353
|
+
"title.descriptionMore": "More",
|
|
354
|
+
"title.descriptionLess": "Less"
|
|
351
355
|
}
|
package/src/i18n/es.json
CHANGED
|
@@ -70,11 +70,16 @@
|
|
|
70
70
|
"tree.move.failed.gone": "No se ha movido: esa carpeta ya no existe.",
|
|
71
71
|
"tree.move.failed.other": "No se ha podido mover. Comprueba tu acceso e inténtalo de nuevo.",
|
|
72
72
|
"resource.menu": "Más acciones para {title}",
|
|
73
|
-
"resource.rename": "
|
|
73
|
+
"resource.rename": "Renombrar",
|
|
74
|
+
"resource.move": "Mover",
|
|
74
75
|
"resource.renameTitle": "Cambiar el nombre de {title}",
|
|
75
76
|
"resource.archive": "Archivar",
|
|
76
77
|
"resource.export": "Exportar",
|
|
77
78
|
"resource.import": "Importar",
|
|
79
|
+
"resource.downloadCsv": "Descargar CSV",
|
|
80
|
+
"resource.validate": "Comprobar",
|
|
81
|
+
"resource.links": "Enlaces",
|
|
82
|
+
"resource.share": "Compartir",
|
|
78
83
|
"tree.new.importZip": "Importar un paquete zip",
|
|
79
84
|
"tree.new.importFolder": "Importar una carpeta",
|
|
80
85
|
"tree.importZipInto": "Importar un paquete zip en {where}",
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
"archive.purge.confirm": "Eliminar definitivamente",
|
|
104
109
|
"archive.purge.cancel": "Cancelar",
|
|
105
110
|
"archive.purgeFailed": "No se pudo eliminar por completo. Recarga e inténtalo de nuevo.",
|
|
106
|
-
"archive.
|
|
111
|
+
"archive.archived": "Archivado",
|
|
107
112
|
"resource.conflict": "No se ha cambiado: otra persona ha cambiado esta entrada antes. Recárgala e inténtalo de nuevo.",
|
|
108
113
|
"resource.forbidden": "No se ha cambiado: no puedes cambiar esta entrada.",
|
|
109
114
|
"resource.failed": "El cambio no se ha guardado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
@@ -210,8 +215,6 @@
|
|
|
210
215
|
"tools.toolCount": "{count} herramientas",
|
|
211
216
|
"tools.toolCountOne": "1 herramienta",
|
|
212
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.",
|
|
213
|
-
"tools.arguments": "Argumentos",
|
|
214
|
-
"tools.invalidJson": "Los argumentos tienen que ser un objeto JSON entre llaves; una lista o un valor suelto no sirven.",
|
|
215
218
|
"flows.select": "Elige un flujo o crea uno para abrir el editor visual.",
|
|
216
219
|
"flows.inspect": "Elige un paso para editar su configuración.",
|
|
217
220
|
"flows.publish": "Publicar",
|
|
@@ -276,7 +279,6 @@
|
|
|
276
279
|
"flows.selectTool": "Elige una herramienta descubierta",
|
|
277
280
|
"flows.operationFailed": "La operación del flujo ha fallado. Carga la versión más reciente e inténtalo de nuevo.",
|
|
278
281
|
"flows.needs": "Qué necesita este flujo",
|
|
279
|
-
"flows.needsHint": "Leído del grafo. Si una persona concreta llega a algo de esto se decide cuando lo ejecuta.",
|
|
280
282
|
"flows.needsNodes": "Documentos",
|
|
281
283
|
"flows.needsTools": "Herramientas",
|
|
282
284
|
"flows.needsHidden": "{count} más que no puedes ver",
|
|
@@ -347,5 +349,7 @@
|
|
|
347
349
|
"common.close": "Cerrar",
|
|
348
350
|
"common.unavailable": "Intel no está disponible en este momento.",
|
|
349
351
|
"common.noAccess": "Esto no está disponible para ti. Puede que no exista o que ya no esté compartido contigo.",
|
|
350
|
-
"common.noPermission": "No tienes permiso para ver esto."
|
|
352
|
+
"common.noPermission": "No tienes permiso para ver esto.",
|
|
353
|
+
"title.descriptionMore": "Más",
|
|
354
|
+
"title.descriptionLess": "Menos"
|
|
351
355
|
}
|
|
@@ -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"] }),
|
package/src/main.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { createLanguages } from "@/i18n/i18n-languages/i18n-languages.ts";
|
|
|
13
13
|
import { createIntelRouter } from "@/router/router.tsx";
|
|
14
14
|
import { applyTheme, readThemeChoice, SystemThemeQuery, themeFor } from "@/theme/theme.ts";
|
|
15
15
|
import { ThemeProvider } from "@/theme/theme-context.tsx";
|
|
16
|
+
import { RelativeClockProvider } from "@/time/relative-time.tsx";
|
|
16
17
|
import { TimeZoneProvider } from "@/time/time-context.tsx";
|
|
17
18
|
import "./styles.css";
|
|
18
19
|
import "./theme/custom.css";
|
|
@@ -55,9 +56,11 @@ mounted.render(
|
|
|
55
56
|
{/* ⚠️ INSIDE `I18nProvider`: the formatter needs both — the language says HOW a time is
|
|
56
57
|
written, the zone WHICH time it is (#467). */}
|
|
57
58
|
<TimeZoneProvider store={window.localStorage}>
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
59
|
+
<RelativeClockProvider>
|
|
60
|
+
<QueryClientProvider client={queryClient}>
|
|
61
|
+
<AppRoot data={data} router={router} refusal={refusal} />
|
|
62
|
+
</QueryClientProvider>
|
|
63
|
+
</RelativeClockProvider>
|
|
61
64
|
</TimeZoneProvider>
|
|
62
65
|
</ThemeProvider>
|
|
63
66
|
</I18nProvider>
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
24
24
|
import { Modal } from "@/modal/modal.tsx";
|
|
25
25
|
import { SaveButton, UnsavedChangesGuard } from "@/save-button/save-button.tsx";
|
|
26
|
+
import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
26
27
|
|
|
27
28
|
function storedBlocks(content: string | null): IntelEditorPartialBlock[] | undefined {
|
|
28
29
|
if (!content) return undefined;
|
|
@@ -167,15 +168,7 @@ export function NodeEditor({
|
|
|
167
168
|
because this is where `dirty` lives — moving the state up to the screen would mean the
|
|
168
169
|
screen tracking every keystroke in an editor it does not own. */}
|
|
169
170
|
<ActionSlot name="title-actions">
|
|
170
|
-
{
|
|
171
|
-
sets on success: it turns true when the answer that carried a version has reached the
|
|
172
|
-
query cache, and never a moment earlier (#432). */}
|
|
173
|
-
<SaveButton
|
|
174
|
-
dirty={dirty}
|
|
175
|
-
saving={save.isPending}
|
|
176
|
-
stored={document.version !== null}
|
|
177
|
-
onSave={() => save.mutate()}
|
|
178
|
-
/>
|
|
171
|
+
<SaveButton dirty={dirty} saving={save.isPending} onSave={() => save.mutate()} />
|
|
179
172
|
</ActionSlot>
|
|
180
173
|
<UnsavedChangesGuard dirty={dirty} />
|
|
181
174
|
{save.isError && (
|
|
@@ -183,7 +176,7 @@ export function NodeEditor({
|
|
|
183
176
|
{i18n.t("node.saveError")}
|
|
184
177
|
</p>
|
|
185
178
|
)}
|
|
186
|
-
<
|
|
179
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-y-auto py-6">
|
|
187
180
|
<DocumentLinkProvider
|
|
188
181
|
value={{
|
|
189
182
|
titles: new Map((resolved.data?.items ?? []).map((item) => [item.nodeId, item.title])),
|
|
@@ -219,7 +212,7 @@ export function NodeEditor({
|
|
|
219
212
|
/>
|
|
220
213
|
</BlockNoteView>
|
|
221
214
|
</DocumentLinkProvider>
|
|
222
|
-
</
|
|
215
|
+
</TitleRowScrollArea>
|
|
223
216
|
{picking ? (
|
|
224
217
|
<DocumentPicker
|
|
225
218
|
data={data}
|
|
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
3
3
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
4
4
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
5
5
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
6
|
+
import { TitleRowScrollArea } from "@/title-row/title-row.tsx";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* A table as a grid (#40).
|
|
@@ -59,7 +60,7 @@ export function NodeTablePanel({ node }: { node: Node }) {
|
|
|
59
60
|
{i18n.t("node.table.undefined")}
|
|
60
61
|
</p>
|
|
61
62
|
) : (
|
|
62
|
-
<
|
|
63
|
+
<TitleRowScrollArea className="min-h-0 flex-1 overflow-auto">
|
|
63
64
|
<table className="w-full border-collapse text-sm">
|
|
64
65
|
<caption className="sr-only">{node.title}</caption>
|
|
65
66
|
<thead className="sticky top-0 bg-card">
|
|
@@ -97,7 +98,7 @@ export function NodeTablePanel({ node }: { node: Node }) {
|
|
|
97
98
|
{table.data.rows.length === 0 ? (
|
|
98
99
|
<p className="p-6 text-sm text-muted-foreground">{i18n.t("node.table.empty")}</p>
|
|
99
100
|
) : null}
|
|
100
|
-
</
|
|
101
|
+
</TitleRowScrollArea>
|
|
101
102
|
)}
|
|
102
103
|
</div>
|
|
103
104
|
);
|