@anchrd/intel-ui 0.33.0 → 0.35.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 +100 -11
- package/src/app/tree-move/tree-move.tsx +5 -5
- package/src/archive/archive.tsx +83 -7
- package/src/data/intel-data-provider/intel-data-provider.ts +12 -1
- package/src/data/request-refusal/refusal-notice.tsx +36 -0
- package/src/entry-picker/entry-picker.tsx +27 -4
- package/src/flow-runs/flow-runs.tsx +9 -1
- package/src/flows/flows.tsx +264 -109
- package/src/graph-pane/graph-pane.tsx +9 -0
- package/src/i18n/de.json +34 -17
- package/src/i18n/en.json +34 -17
- package/src/i18n/es.json +34 -17
- package/src/node-editor/node-editor.tsx +24 -1
- package/src/nodes/nodes.tsx +31 -17
- package/src/resource-menu/resource-menu.tsx +84 -24
- package/src/tools/tools.tsx +5 -4
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
ShieldCheck,
|
|
19
19
|
} from "lucide-react";
|
|
20
20
|
import type * as React from "react";
|
|
21
|
-
import { useState } from "react";
|
|
21
|
+
import { useId, useState } from "react";
|
|
22
22
|
import { AccessSummary } from "@/access-summary/access-summary.tsx";
|
|
23
23
|
import { allTreeLevelsKey, moveErrorKey, useTreeMove } from "@/app/tree-move/tree-move.tsx";
|
|
24
24
|
import {
|
|
@@ -154,6 +154,11 @@ export function ResourceMenu({
|
|
|
154
154
|
// nowhere else: a person had strictly less reach on their own table than a model did.
|
|
155
155
|
const isTable = node !== null && node.kind === "table";
|
|
156
156
|
const bundleImport = useBundleImport({ where: title });
|
|
157
|
+
// ⚠️ Per instance, not a constant: the folder table renders one menu PER ROW, so a fixed id would
|
|
158
|
+
// put the same `id` in the document twenty times and every entry's name would resolve to the
|
|
159
|
+
// first one's.
|
|
160
|
+
const exportLabelId = useId();
|
|
161
|
+
const exportHintId = useId();
|
|
157
162
|
|
|
158
163
|
// Everything a change here can make stale. The row sits in one level of the tree, the open screen
|
|
159
164
|
// reads the record, and the flat collections behind the search, the link picker and the relation
|
|
@@ -316,10 +321,40 @@ export function ResourceMenu({
|
|
|
316
321
|
⚠️ The one case where they differed is the table that has no header yet: `downloadCsv`
|
|
317
322
|
refused it out loud, `tableCsv` joins no segments and writes a file of zero bytes
|
|
318
323
|
(#534). That is the export's gap on every kind it can hit, not something this entry
|
|
319
|
-
was covering — which is why it is fixed there and not by keeping a second entry.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
was covering — which is why it is fixed there and not by keeping a second entry.
|
|
325
|
+
|
|
326
|
+
⚠️ The second line is not decoration and it belongs HERE rather than after the click
|
|
327
|
+
(#433, ADR-0006). A bundle carries one version per node; whoever moves an installation
|
|
328
|
+
this way loses every earlier one, and until now the only place that said so was
|
|
329
|
+
`excluded` in a `manifest.json` inside the zip — read, if ever, long after the source
|
|
330
|
+
installation is gone. A loss that cannot be undone has to be readable BEFORE the
|
|
331
|
+
gesture, and a menu item is where this gesture is chosen.
|
|
332
|
+
|
|
333
|
+
⚠️ Its third clause is the flow half (#585), and it is a DIFFERENT loss from the first
|
|
334
|
+
two rather than an example of them: a published flow travels as its published graph,
|
|
335
|
+
so edits drafted since do not travel at all. Those edits are not an earlier version —
|
|
336
|
+
they are the newest state there is, and somebody reading only "earlier versions stay
|
|
337
|
+
behind" would expect the opposite of what happens. `Excluded` in `bundle.ts` names it
|
|
338
|
+
with a word of its own for the same reason.
|
|
339
|
+
|
|
340
|
+
⚠️ It is the entry's DESCRIPTION, not part of its name: `aria-labelledby` keeps the
|
|
341
|
+
name at the one word every other entry here uses, and `aria-describedby` is what a
|
|
342
|
+
screen reader reads after it. Folding a whole sentence into the name would make this
|
|
343
|
+
the only entry in the menu somebody has to listen through to know what it does — and
|
|
344
|
+
the sentence is a condition of the action, which is what a description is for. */}
|
|
345
|
+
<DropdownMenuItem
|
|
346
|
+
className="items-start"
|
|
347
|
+
aria-labelledby={exportLabelId}
|
|
348
|
+
aria-describedby={exportHintId}
|
|
349
|
+
onSelect={() => exportBundle.mutate()}
|
|
350
|
+
>
|
|
351
|
+
<FolderDown aria-hidden="true" className="mt-0.5" />
|
|
352
|
+
<span className="flex flex-col gap-0.5">
|
|
353
|
+
<span id={exportLabelId}>{i18n.t("resource.export")}</span>
|
|
354
|
+
<span id={exportHintId} className="text-xs text-muted-foreground">
|
|
355
|
+
{i18n.t("resource.exportExcludes")}
|
|
356
|
+
</span>
|
|
357
|
+
</span>
|
|
323
358
|
</DropdownMenuItem>
|
|
324
359
|
{/* The other half of the same round trip, next to it rather than in the tree's plus
|
|
325
360
|
(#346): one word in the menu, both sources under it. */}
|
|
@@ -411,19 +446,30 @@ export function ResourceMenu({
|
|
|
411
446
|
(`flows.problem.<code>` is one string) and repeating it under itself says nothing
|
|
412
447
|
the reader did not just read. What differs is the detail, and every detail is
|
|
413
448
|
still here — which is the promise the comment above makes: somebody with two
|
|
414
|
-
missing tools should not have to ask twice.
|
|
449
|
+
missing tools should not have to ask twice.
|
|
450
|
+
|
|
451
|
+
⚠️ What identifies a detail is its POSITION in `problems`, not its text (#461).
|
|
452
|
+
Two details of one code are routinely the same sentence: `collectRunProblems`
|
|
453
|
+
files one problem per sub-flow NODE and words it with that node's label
|
|
454
|
+
(`packages/api/src/flows/flows.ts`), and calling the same flow twice is the normal
|
|
455
|
+
case. Under `key={detail}` those two collided on one key, and React does not
|
|
456
|
+
answer that with a console warning alone — it drops one of the siblings on the
|
|
457
|
+
next render, so the reader sees ONE entry where there are two causes. */}
|
|
415
458
|
{[
|
|
416
459
|
...validation.problems
|
|
417
|
-
.reduce((byCode, problem) => {
|
|
418
|
-
byCode.set(problem.code, [
|
|
460
|
+
.reduce((byCode, problem, at) => {
|
|
461
|
+
byCode.set(problem.code, [
|
|
462
|
+
...(byCode.get(problem.code) ?? []),
|
|
463
|
+
{ at, detail: problem.detail },
|
|
464
|
+
]);
|
|
419
465
|
return byCode;
|
|
420
|
-
}, new Map<string, string[]>())
|
|
466
|
+
}, new Map<string, { at: number; detail: string }[]>())
|
|
421
467
|
.entries(),
|
|
422
468
|
].map(([code, details]) => (
|
|
423
469
|
<li key={code} className="rounded-md border p-3 text-sm">
|
|
424
470
|
<span className="block font-medium">{i18n.t(`flows.problem.${code}`)}</span>
|
|
425
|
-
{details.map((detail) => (
|
|
426
|
-
<span key={
|
|
471
|
+
{details.map(({ at, detail }) => (
|
|
472
|
+
<span key={at} className="mt-1 block text-xs text-muted-foreground">
|
|
427
473
|
{detail}
|
|
428
474
|
</span>
|
|
429
475
|
))}
|
|
@@ -776,27 +822,41 @@ function SharePanel({ target, close }: { target: ResourceTarget; close(): void }
|
|
|
776
822
|
*
|
|
777
823
|
* ⚠️ Taking back `organization` + `execute` on a folder is refused with `409
|
|
778
824
|
* folder_execute_in_use` while flows from outside still call into it (`nodes.ts`,
|
|
779
|
-
* `revokeGrant`)
|
|
780
|
-
*
|
|
781
|
-
*
|
|
825
|
+
* `revokeGrant`), and the CALLERS are what makes that refusal actionable. Rendering it as the
|
|
826
|
+
* generic "access was not changed" threw exactly that away and left the reader with a dialog that
|
|
827
|
+
* refuses and will not say why, for the one grant that is hardest to undo.
|
|
782
828
|
*
|
|
783
|
-
* ⚠️
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
* evidence under it.
|
|
829
|
+
* ⚠️ Since #448 they arrive as data — the flows this actor may see by name, the rest as a count —
|
|
830
|
+
* so the sentence is written here, in the reader's language, exactly like the `shareUnreadable`
|
|
831
|
+
* warning below it. The server still decides WHICH of them may be named; that is an authorization
|
|
832
|
+
* answer and nothing on this side recomputes it.
|
|
788
833
|
*/
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
? revoke.error.message
|
|
792
|
-
: null;
|
|
834
|
+
const revokeError = revoke.error instanceof IntelRequestError ? revoke.error : null;
|
|
835
|
+
const revokeInUse = revokeError?.code === "folder_execute_in_use" ? revokeError : null;
|
|
793
836
|
|
|
794
837
|
return (
|
|
795
838
|
<Modal title={i18n.t("node.share")} close={close}>
|
|
796
839
|
{revokeInUse ? (
|
|
797
840
|
<div role="alert" className="mb-4 space-y-1 text-sm text-destructive">
|
|
798
841
|
<p>{i18n.t("node.revokeInUse")}</p>
|
|
799
|
-
|
|
842
|
+
{/* An API that predates #448 sends no `callers`, and its sentence is then the only place
|
|
843
|
+
the names exist at all. Showing it beats showing nothing — losing the titles is the
|
|
844
|
+
failure this ticket is about. */}
|
|
845
|
+
{revokeInUse.callers === null ? (
|
|
846
|
+
<p className="text-xs">{revokeInUse.message}</p>
|
|
847
|
+
) : (
|
|
848
|
+
<p className="text-xs">
|
|
849
|
+
{revokeInUse.callers.titles.length > 0
|
|
850
|
+
? i18n.t("node.revokeInUseCallers", {
|
|
851
|
+
titles: revokeInUse.callers.titles.join(", "),
|
|
852
|
+
})
|
|
853
|
+
: i18n.t("node.revokeInUseCallersHidden", { count: revokeInUse.callers.hidden })}
|
|
854
|
+
{revokeInUse.callers.titles.length > 0 && revokeInUse.callers.hidden > 0
|
|
855
|
+
? ` ${i18n.t("node.revokeInUseCallersMore", { count: revokeInUse.callers.hidden })}`
|
|
856
|
+
: ""}
|
|
857
|
+
{` ${i18n.t("node.revokeInUseHint")}`}
|
|
858
|
+
</p>
|
|
859
|
+
)}
|
|
800
860
|
</div>
|
|
801
861
|
) : (
|
|
802
862
|
(share.isError || revoke.isError || grants.isError) && (
|
package/src/tools/tools.tsx
CHANGED
|
@@ -415,10 +415,11 @@ function ServerGroup({
|
|
|
415
415
|
// with it — otherwise Enter on a hit lands on a screen where nothing happened.
|
|
416
416
|
const holdsRequested = group.items.some((item) => item.name === requested);
|
|
417
417
|
const destructive = group.items.filter((item) => item.annotations.destructiveHint).length;
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
418
|
+
// ⚠️ One key, not a `…One` beside a `…Many` (#605): the catalog carries a wording that fits every
|
|
419
|
+
// number, so nothing here branches on the count. The branch that used to stand here is what the
|
|
420
|
+
// rule in `packages/ui/CLAUDE.md` forbids — it puts the inflection into the component and into
|
|
421
|
+
// every future language.
|
|
422
|
+
const count = i18n.t("tools.toolCount", { count: group.items.length });
|
|
422
423
|
// The badges beside the name read as one run-on word when a screen reader concatenates them
|
|
423
424
|
// ("wiki1 tool"), so the control says its own name instead of being read off its contents.
|
|
424
425
|
const spoken = [`${i18n.t("tools.origin")}: ${group.label}`, count];
|