@anchrd/intel-ui 0.25.0 → 0.28.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 +1 -1
- package/src/app/app-tree/app-tree.tsx +64 -35
- package/src/app/tree-move/tree-move.tsx +92 -21
- package/src/app/view-toggle/view-toggle.tsx +1 -1
- package/src/archive/archive.tsx +4 -4
- 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 +19 -22
- package/src/folder-contents/folder-contents.tsx +6 -6
- package/src/i18n/de.json +9 -2
- package/src/i18n/en.json +9 -2
- package/src/i18n/es.json +10 -3
- 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 +85 -31
- 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
|
@@ -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.
|
|
@@ -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.",
|
|
@@ -347,5 +352,7 @@
|
|
|
347
352
|
"common.close": "Schließen",
|
|
348
353
|
"common.unavailable": "Intel ist derzeit nicht verfügbar.",
|
|
349
354
|
"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."
|
|
355
|
+
"common.noPermission": "Dir fehlt die Berechtigung, das zu sehen.",
|
|
356
|
+
"title.descriptionMore": "Mehr",
|
|
357
|
+
"title.descriptionLess": "Weniger"
|
|
351
358
|
}
|
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.",
|
|
@@ -347,5 +352,7 @@
|
|
|
347
352
|
"common.close": "Close",
|
|
348
353
|
"common.unavailable": "Intel is currently unavailable.",
|
|
349
354
|
"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."
|
|
355
|
+
"common.noPermission": "You do not have permission to see this.",
|
|
356
|
+
"title.descriptionMore": "More",
|
|
357
|
+
"title.descriptionLess": "Less"
|
|
351
358
|
}
|
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.",
|
|
@@ -347,5 +352,7 @@
|
|
|
347
352
|
"common.close": "Cerrar",
|
|
348
353
|
"common.unavailable": "Intel no está disponible en este momento.",
|
|
349
354
|
"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."
|
|
355
|
+
"common.noPermission": "No tienes permiso para ver esto.",
|
|
356
|
+
"title.descriptionMore": "Más",
|
|
357
|
+
"title.descriptionLess": "Menos"
|
|
351
358
|
}
|
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
|
);
|
package/src/nodes/nodes.tsx
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { Node } from "@anchrd/intel-contract/node";
|
|
2
2
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
4
|
-
import { Download,
|
|
5
|
-
import { lazy, Suspense
|
|
4
|
+
import { Download, Paperclip } from "lucide-react";
|
|
5
|
+
import { lazy, Suspense } from "react";
|
|
6
6
|
import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
|
|
7
|
-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
8
7
|
import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
|
|
9
8
|
import { FolderContents } from "@/folder-contents/folder-contents.tsx";
|
|
10
9
|
import { GraphPane } from "@/graph-pane/graph-pane.tsx";
|
|
@@ -12,8 +11,7 @@ import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
|
12
11
|
import { NodeTablePanel } from "@/node-table/node-table.tsx";
|
|
13
12
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
14
13
|
import { graphViewFrom, selectedFrom } from "@/router/selection-search.ts";
|
|
15
|
-
import {
|
|
16
|
-
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
14
|
+
import { TitleRow, TitleRowFrame } from "@/title-row/title-row.tsx";
|
|
17
15
|
|
|
18
16
|
const NodeEditor = lazy(async () => ({
|
|
19
17
|
default: (await import("@/node-editor/node-editor.tsx")).NodeEditor,
|
|
@@ -33,7 +31,6 @@ export function Nodes() {
|
|
|
33
31
|
}),
|
|
34
32
|
});
|
|
35
33
|
const selectedId = selection.id;
|
|
36
|
-
const [versionsOpen, setVersionsOpen] = useState(false);
|
|
37
34
|
const document = useQuery({
|
|
38
35
|
queryKey: ["node", selectedId],
|
|
39
36
|
queryFn: () => data.getNode(selectedId ?? ""),
|
|
@@ -53,7 +50,7 @@ export function Nodes() {
|
|
|
53
50
|
enabled: graphable && selection.graph,
|
|
54
51
|
});
|
|
55
52
|
return (
|
|
56
|
-
<
|
|
53
|
+
<TitleRowFrame className="flex h-full min-h-0 flex-col">
|
|
57
54
|
<section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
|
|
58
55
|
{/* ⚠️ Only the root draws its graph on its own. A folder's graph hangs *under* the folder's
|
|
59
56
|
title line rather than in place of it (#58): the line is the one place its menu lives,
|
|
@@ -122,9 +119,6 @@ export function Nodes() {
|
|
|
122
119
|
document. What the four loose icons used to do is now one menu (#24) — and since #58
|
|
123
120
|
this line is the only place that menu appears at all, for every kind including a
|
|
124
121
|
folder, whose whole screen is this line.
|
|
125
|
-
⚠️ The order of the right-hand group is `TitleRow`'s (#53), not this screen's: the
|
|
126
|
-
version button is handed over as a child and lands before the menu whatever else
|
|
127
|
-
shows up later.
|
|
128
122
|
⚠️ #454: the toggle now stands HERE. It used to stand in the global header, beside
|
|
129
123
|
the search — where what applies everywhere stands. But a graph view does not apply
|
|
130
124
|
everywhere, it is a view onto THIS item. It is only drawn where there is a graph: on
|
|
@@ -135,22 +129,6 @@ export function Nodes() {
|
|
|
135
129
|
target={{ type: "node", node: selected }}
|
|
136
130
|
>
|
|
137
131
|
{graphable && <ViewToggle />}
|
|
138
|
-
{selected.kind !== "folder" && (
|
|
139
|
-
<TooltipProvider delayDuration={300}>
|
|
140
|
-
<Tooltip>
|
|
141
|
-
<TooltipTrigger
|
|
142
|
-
type="button"
|
|
143
|
-
onClick={() => setVersionsOpen((value) => !value)}
|
|
144
|
-
aria-label={i18n.t("node.versions")}
|
|
145
|
-
aria-expanded={versionsOpen}
|
|
146
|
-
className="inline-flex size-8 items-center justify-center rounded-md border bg-background outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
147
|
-
>
|
|
148
|
-
<History aria-hidden="true" className="size-4" />
|
|
149
|
-
</TooltipTrigger>
|
|
150
|
-
<TooltipContent>{i18n.t("node.versions")}</TooltipContent>
|
|
151
|
-
</Tooltip>
|
|
152
|
-
</TooltipProvider>
|
|
153
|
-
)}
|
|
154
132
|
</TitleRow>
|
|
155
133
|
{graphable && selection.graph ? (
|
|
156
134
|
<GraphPane
|
|
@@ -185,13 +163,10 @@ export function Nodes() {
|
|
|
185
163
|
/>
|
|
186
164
|
</Suspense>
|
|
187
165
|
) : null}
|
|
188
|
-
{versionsOpen && selected.kind !== "folder" && (
|
|
189
|
-
<VersionHistory nodeId={selected.id} close={() => setVersionsOpen(false)} />
|
|
190
|
-
)}
|
|
191
166
|
</>
|
|
192
167
|
)}
|
|
193
168
|
</section>
|
|
194
|
-
</
|
|
169
|
+
</TitleRowFrame>
|
|
195
170
|
);
|
|
196
171
|
}
|
|
197
172
|
|
|
@@ -235,51 +210,3 @@ function AttachmentPanel({ node }: { node: Node }) {
|
|
|
235
210
|
</div>
|
|
236
211
|
);
|
|
237
212
|
}
|
|
238
|
-
|
|
239
|
-
function VersionHistory({ nodeId, close }: { nodeId: string; close(): void }) {
|
|
240
|
-
const { data } = useIntelRouterContext();
|
|
241
|
-
const i18n = useI18n();
|
|
242
|
-
const dateTime = useDateTime();
|
|
243
|
-
const versions = useQuery({
|
|
244
|
-
queryKey: ["node-versions", nodeId],
|
|
245
|
-
queryFn: () => data.listNodeVersions(nodeId),
|
|
246
|
-
});
|
|
247
|
-
return (
|
|
248
|
-
<aside
|
|
249
|
-
aria-label={i18n.t("node.versions")}
|
|
250
|
-
className="absolute bottom-0 right-0 top-0 z-10 w-80 overflow-y-auto border-l bg-card p-5 shadow-xl"
|
|
251
|
-
>
|
|
252
|
-
<div className="flex items-start justify-between gap-4">
|
|
253
|
-
<h3 className="font-semibold">{i18n.t("node.versions")}</h3>
|
|
254
|
-
<button
|
|
255
|
-
type="button"
|
|
256
|
-
onClick={close}
|
|
257
|
-
aria-label={i18n.t("common.close")}
|
|
258
|
-
className="rounded-md px-2 py-1 text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
259
|
-
>
|
|
260
|
-
×
|
|
261
|
-
</button>
|
|
262
|
-
</div>
|
|
263
|
-
{versions.isPending ? (
|
|
264
|
-
<p className="mt-4 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
265
|
-
) : null}
|
|
266
|
-
{versions.isError ? (
|
|
267
|
-
<p role="alert" className="mt-4 text-sm text-destructive">
|
|
268
|
-
{i18n.t("node.operationFailed")}
|
|
269
|
-
</p>
|
|
270
|
-
) : null}
|
|
271
|
-
<ol className="mt-4 space-y-3">
|
|
272
|
-
{versions.data?.items.map((version) => (
|
|
273
|
-
<li key={version.id} className="rounded-md border p-3 text-sm">
|
|
274
|
-
<span className="font-medium">
|
|
275
|
-
{i18n.t("node.version", { sequence: version.sequence })}
|
|
276
|
-
</span>
|
|
277
|
-
<time className="mt-1 block text-xs text-muted-foreground" dateTime={version.createdAt}>
|
|
278
|
-
{dateTime.at(version.createdAt)}
|
|
279
|
-
</time>
|
|
280
|
-
</li>
|
|
281
|
-
))}
|
|
282
|
-
</ol>
|
|
283
|
-
</aside>
|
|
284
|
-
);
|
|
285
|
-
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
FileArchive,
|
|
12
12
|
FolderDown,
|
|
13
13
|
FolderUp,
|
|
14
|
+
History,
|
|
14
15
|
Link2,
|
|
15
16
|
Pencil,
|
|
16
17
|
Share2,
|
|
@@ -24,7 +25,6 @@ import {
|
|
|
24
25
|
DropdownMenu,
|
|
25
26
|
DropdownMenuContent,
|
|
26
27
|
DropdownMenuItem,
|
|
27
|
-
DropdownMenuSeparator,
|
|
28
28
|
DropdownMenuSub,
|
|
29
29
|
DropdownMenuSubContent,
|
|
30
30
|
DropdownMenuSubTrigger,
|
|
@@ -37,6 +37,7 @@ import { useBundleImport } from "@/import-bundle/import-bundle.tsx";
|
|
|
37
37
|
import { Modal } from "@/modal/modal.tsx";
|
|
38
38
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
39
39
|
import { selectedFrom } from "@/router/selection-search.ts";
|
|
40
|
+
import { RelativeTime } from "@/time/relative-time.tsx";
|
|
40
41
|
import { useDateTime } from "@/time/time-context.tsx";
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -142,6 +143,7 @@ export function ResourceMenu({
|
|
|
142
143
|
const [renaming, setRenaming] = useState(false);
|
|
143
144
|
const [sharing, setSharing] = useState(false);
|
|
144
145
|
const [linksOpen, setLinksOpen] = useState(false);
|
|
146
|
+
const [versionsOpen, setVersionsOpen] = useState(false);
|
|
145
147
|
const selected = useRouterState({ select: (state) => selectedFrom(state.location.search) });
|
|
146
148
|
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
|
147
149
|
// ⚠️ Dragging is a pointer gesture and nothing else: no keyboard, no screen reader, no touch worth
|
|
@@ -314,35 +316,26 @@ export function ResourceMenu({
|
|
|
314
316
|
className={
|
|
315
317
|
variant === "row"
|
|
316
318
|
? "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition hover:bg-accent hover:text-accent-foreground focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:opacity-100 group-focus-within/row:opacity-100 group-hover/row:opacity-100 md:opacity-0"
|
|
317
|
-
: "inline-flex size-8 shrink-0 items-center justify-center rounded-md
|
|
319
|
+
: "inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-muted outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
318
320
|
}
|
|
319
321
|
>
|
|
320
322
|
<Ellipsis aria-hidden="true" className="size-4" />
|
|
321
323
|
</DropdownMenuTrigger>
|
|
322
|
-
<DropdownMenuContent align="start" className="w-
|
|
324
|
+
<DropdownMenuContent align="start" className="min-w-40 whitespace-nowrap">
|
|
323
325
|
<DropdownMenuItem onSelect={() => setRenaming(true)}>
|
|
324
326
|
<Pencil aria-hidden="true" />
|
|
325
327
|
{i18n.t("resource.rename")}
|
|
326
328
|
</DropdownMenuItem>
|
|
327
|
-
{/*
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
readable without the pointer ever touching the row.
|
|
336
|
-
|
|
337
|
-
⚠️ The dialog behind it stays the main path, not the fallback: touch has no
|
|
338
|
-
`dragstart`, and nothing can be dragged by keyboard at all. Dragging is the shortcut
|
|
339
|
-
for the mouse — the hint says exactly that, in the place that is reachable both ways. */}
|
|
340
|
-
<DropdownMenuItem onSelect={() => move.start(entryOf(target), null)}>
|
|
329
|
+
{/* The dialog stays the complete keyboard and touch path. Dragging is only a mouse
|
|
330
|
+
shortcut; it does not need a second label beside this action. */}
|
|
331
|
+
{/* ⚠️ `undefined`, and that is the honest answer rather than a missing one: this menu hangs
|
|
332
|
+
on a resource's own title line, so there is no rendered tree level to name. Passing the
|
|
333
|
+
record's parent instead would claim to know a level — and for a shared row it would
|
|
334
|
+
name the sharer's folder, which is exactly the wrong one (#446). `useTreeMove` reads
|
|
335
|
+
the absence and cleans up both levels such a row can be standing in. */}
|
|
336
|
+
<DropdownMenuItem onSelect={() => move.start(entryOf(target), undefined, null)}>
|
|
341
337
|
<CornerLeftUp aria-hidden="true" />
|
|
342
|
-
{i18n.t("
|
|
343
|
-
<span aria-hidden="true" className="ml-auto text-xs text-muted-foreground">
|
|
344
|
-
{i18n.t("tree.move.hint")}
|
|
345
|
-
</span>
|
|
338
|
+
{i18n.t("resource.move")}
|
|
346
339
|
</DropdownMenuItem>
|
|
347
340
|
{/* Every kind exports: a folder takes its subtree along, everything else is a bundle of
|
|
348
341
|
one (#136). The entry sits with rename and move because it acts on the whole thing,
|
|
@@ -356,7 +349,7 @@ export function ResourceMenu({
|
|
|
356
349
|
{isTable ? (
|
|
357
350
|
<DropdownMenuItem onSelect={() => downloadCsv.mutate()}>
|
|
358
351
|
<Download aria-hidden="true" />
|
|
359
|
-
{i18n.t("
|
|
352
|
+
{i18n.t("resource.downloadCsv")}
|
|
360
353
|
</DropdownMenuItem>
|
|
361
354
|
) : null}
|
|
362
355
|
{/* The other half of the same round trip, next to it rather than in the tree's plus
|
|
@@ -384,14 +377,13 @@ export function ResourceMenu({
|
|
|
384
377
|
{node === null ? (
|
|
385
378
|
<DropdownMenuItem onSelect={() => validate.mutate()}>
|
|
386
379
|
<ShieldCheck aria-hidden="true" />
|
|
387
|
-
{i18n.t("
|
|
380
|
+
{i18n.t("resource.validate")}
|
|
388
381
|
</DropdownMenuItem>
|
|
389
382
|
) : null}
|
|
390
|
-
{retrievable || linkable || node ? <DropdownMenuSeparator /> : null}
|
|
391
383
|
{linkable ? (
|
|
392
384
|
<DropdownMenuItem onSelect={() => setLinksOpen(true)}>
|
|
393
385
|
<Link2 aria-hidden="true" />
|
|
394
|
-
{i18n.t("
|
|
386
|
+
{i18n.t("resource.links")}
|
|
395
387
|
</DropdownMenuItem>
|
|
396
388
|
) : null}
|
|
397
389
|
{/* A flow has no share of its own: it is reached through the folder it is filed in, and a
|
|
@@ -400,17 +392,19 @@ export function ResourceMenu({
|
|
|
400
392
|
{node ? (
|
|
401
393
|
<DropdownMenuItem onSelect={() => setSharing(true)}>
|
|
402
394
|
<Share2 aria-hidden="true" />
|
|
403
|
-
{i18n.t("
|
|
395
|
+
{i18n.t("resource.share")}
|
|
404
396
|
</DropdownMenuItem>
|
|
405
397
|
) : null}
|
|
398
|
+
{(node === null || node.kind !== "folder") && (
|
|
399
|
+
<DropdownMenuItem onSelect={() => setVersionsOpen(true)}>
|
|
400
|
+
<History aria-hidden="true" />
|
|
401
|
+
{i18n.t("node.versions")}
|
|
402
|
+
</DropdownMenuItem>
|
|
403
|
+
)}
|
|
406
404
|
{/* A flow is archived like a document (#109). It keeps its versions and its runs; what it
|
|
407
405
|
loses is reach — it leaves the tree, cannot be started, and stops resolving as another
|
|
408
406
|
flow's callee. */}
|
|
409
|
-
<
|
|
410
|
-
<DropdownMenuItem
|
|
411
|
-
onSelect={() => archive.mutate()}
|
|
412
|
-
className="text-destructive focus:text-destructive"
|
|
413
|
-
>
|
|
407
|
+
<DropdownMenuItem onSelect={() => archive.mutate()}>
|
|
414
408
|
<Archive aria-hidden="true" />
|
|
415
409
|
{i18n.t("resource.archive")}
|
|
416
410
|
</DropdownMenuItem>
|
|
@@ -497,10 +491,70 @@ export function ResourceMenu({
|
|
|
497
491
|
) : null}
|
|
498
492
|
{sharing && node ? <SharePanel node={node} close={() => setSharing(false)} /> : null}
|
|
499
493
|
{linksOpen && node ? <NodeLinksPanel node={node} close={() => setLinksOpen(false)} /> : null}
|
|
494
|
+
{versionsOpen ? (
|
|
495
|
+
<VersionHistory target={target} close={() => setVersionsOpen(false)} />
|
|
496
|
+
) : null}
|
|
500
497
|
</>
|
|
501
498
|
);
|
|
502
499
|
}
|
|
503
500
|
|
|
501
|
+
function VersionHistory({ target, close }: { target: ResourceTarget; close(): void }) {
|
|
502
|
+
const { data } = useIntelRouterContext();
|
|
503
|
+
const i18n = useI18n();
|
|
504
|
+
const id = idOf(target);
|
|
505
|
+
const versions = useQuery({
|
|
506
|
+
queryKey: [target.type === "flow" ? "flow-versions" : "node-versions", id],
|
|
507
|
+
queryFn: async () => {
|
|
508
|
+
const result =
|
|
509
|
+
target.type === "flow" ? await data.listFlowVersions(id) : await data.listNodeVersions(id);
|
|
510
|
+
return result.items.map((version) => ({
|
|
511
|
+
id: version.id,
|
|
512
|
+
sequence: version.sequence,
|
|
513
|
+
createdAt: version.createdAt,
|
|
514
|
+
}));
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
return (
|
|
518
|
+
<aside
|
|
519
|
+
aria-label={i18n.t("node.versions")}
|
|
520
|
+
className="absolute bottom-0 right-0 top-0 z-10 w-80 overflow-y-auto border-l bg-card p-5 shadow-xl"
|
|
521
|
+
>
|
|
522
|
+
<div className="flex items-start justify-between gap-4">
|
|
523
|
+
<h3 className="font-semibold">{i18n.t("node.versions")}</h3>
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
onClick={close}
|
|
527
|
+
aria-label={i18n.t("common.close")}
|
|
528
|
+
className="rounded-md px-2 py-1 text-muted-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
|
|
529
|
+
>
|
|
530
|
+
×
|
|
531
|
+
</button>
|
|
532
|
+
</div>
|
|
533
|
+
{versions.isPending ? (
|
|
534
|
+
<p className="mt-4 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
535
|
+
) : null}
|
|
536
|
+
{versions.isError ? (
|
|
537
|
+
<p role="alert" className="mt-4 text-sm text-destructive">
|
|
538
|
+
{i18n.t(target.type === "flow" ? "flows.operationFailed" : "node.operationFailed")}
|
|
539
|
+
</p>
|
|
540
|
+
) : null}
|
|
541
|
+
<ol className="mt-4 space-y-3">
|
|
542
|
+
{versions.data?.map((version) => (
|
|
543
|
+
<li key={version.id} className="rounded-md border p-3 text-sm">
|
|
544
|
+
<span className="font-medium">
|
|
545
|
+
{i18n.t("node.version", { sequence: version.sequence })}
|
|
546
|
+
</span>
|
|
547
|
+
<RelativeTime
|
|
548
|
+
value={version.createdAt}
|
|
549
|
+
className="mt-1 block text-xs text-muted-foreground"
|
|
550
|
+
/>
|
|
551
|
+
</li>
|
|
552
|
+
))}
|
|
553
|
+
</ol>
|
|
554
|
+
</aside>
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
504
558
|
// One refusal, over the screen rather than in the menu that is already gone by the time it arrives.
|
|
505
559
|
function MenuFailure({ children }: { children: React.ReactNode }) {
|
|
506
560
|
return (
|