@anchrd/intel-ui 0.21.0 → 0.23.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 +47 -8
- package/src/app/app.tsx +7 -38
- package/src/app/settings-dialog/settings-dialog.tsx +38 -5
- package/src/app/user-footer/user-footer.tsx +31 -13
- package/src/app-root/app-root.tsx +109 -0
- package/src/app-root/app-root.types.ts +27 -0
- package/src/archive/archive.tsx +76 -2
- package/src/data/intel-data-provider/intel-data-provider.ts +16 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +8 -0
- package/src/flow-runs/flow-runs.tsx +3 -1
- package/src/flows/flows.tsx +5 -11
- package/src/folder-contents/folder-contents.tsx +3 -1
- package/src/i18n/de.json +15 -4
- package/src/i18n/en.json +15 -4
- package/src/i18n/es.json +15 -4
- package/src/main.tsx +26 -32
- package/src/node-table/node-table.tsx +10 -50
- package/src/nodes/nodes.tsx +21 -18
- package/src/resource-menu/resource-menu.tsx +62 -1
- package/src/time/time-context.tsx +72 -0
- package/src/time/time.ts +140 -0
- package/src/components/ui/breadcrumb.tsx +0 -102
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
ListFlowsInput,
|
|
13
13
|
PreviewFlowPublishInput,
|
|
14
14
|
PublishFlowInput,
|
|
15
|
+
PurgeFlowInput,
|
|
16
|
+
PurgeFlowResult,
|
|
15
17
|
RelationGraph,
|
|
16
18
|
RelationGraphScope,
|
|
17
19
|
SaveFlowVersionInput,
|
|
@@ -37,6 +39,8 @@ import type {
|
|
|
37
39
|
NodeList,
|
|
38
40
|
NodeTable,
|
|
39
41
|
NodeVersionList,
|
|
42
|
+
PurgeNodeInput,
|
|
43
|
+
PurgeNodeResult,
|
|
40
44
|
ReindexResult,
|
|
41
45
|
ResolveNodeLinksInput,
|
|
42
46
|
ResolveNodeLinksResult,
|
|
@@ -125,6 +129,10 @@ export interface IntelDataProvider {
|
|
|
125
129
|
listNodeVersions(nodeId: string): Promise<NodeVersionList>;
|
|
126
130
|
updateNode(input: UpdateNodeInput): Promise<Node>;
|
|
127
131
|
archiveNode(input: ArchiveNodeInput): Promise<Node>;
|
|
132
|
+
// ⚠️ The one way across this seam after which nothing is really left (#457). The title comes back
|
|
133
|
+
// because nothing can look it up afterwards.
|
|
134
|
+
purgeNode(input: PurgeNodeInput): Promise<PurgeNodeResult>;
|
|
135
|
+
purgeFlow(input: PurgeFlowInput): Promise<PurgeFlowResult>;
|
|
128
136
|
searchNodes(input: SearchInput): Promise<SearchResult>;
|
|
129
137
|
listGrants(resourceId: string): Promise<ResourceGrantList>;
|
|
130
138
|
// The grant, and what the grant does not cover: the documents the flows in this folder read that
|
|
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
import type { I18n } from "@/i18n/i18n.types.ts";
|
|
6
6
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
7
7
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
8
|
+
import { useDateTime } from "@/time/time-context.tsx";
|
|
8
9
|
|
|
9
10
|
// One page is what a person reads before deciding, not what a database can return. The server caps
|
|
10
11
|
// it at fifty; twenty is what fits on a screen without scrolling past the answer.
|
|
@@ -115,6 +116,7 @@ export function FlowRuns({ flowId }: { flowId: string }) {
|
|
|
115
116
|
|
|
116
117
|
function RunRow({ run, open, toggle }: { run: FlowRunSummary; open: boolean; toggle(): void }) {
|
|
117
118
|
const i18n = useI18n();
|
|
119
|
+
const dateTime = useDateTime();
|
|
118
120
|
const failed = run.status === "failed";
|
|
119
121
|
const Chevron = open ? ChevronDown : ChevronRight;
|
|
120
122
|
return (
|
|
@@ -133,7 +135,7 @@ function RunRow({ run, open, toggle }: { run: FlowRunSummary; open: boolean; tog
|
|
|
133
135
|
>
|
|
134
136
|
{i18n.t(`runs.status.${run.status}`)}
|
|
135
137
|
</span>
|
|
136
|
-
<span>{
|
|
138
|
+
<span>{dateTime.at(run.startedAt)}</span>
|
|
137
139
|
<span className="text-muted-foreground">
|
|
138
140
|
{run.durationMs === null
|
|
139
141
|
? i18n.t("runs.stillRunning")
|
package/src/flows/flows.tsx
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
} from "@xyflow/react";
|
|
25
25
|
import { FileSearch, Info, Send, Wrench } from "lucide-react";
|
|
26
26
|
import { useEffect, useId, useMemo, useState } from "react";
|
|
27
|
-
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
28
27
|
import { TreeEntryMediaType } from "@/app/app-tree/app-tree.tsx";
|
|
29
28
|
import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
|
|
30
29
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
@@ -457,16 +456,6 @@ function FlowsEditor() {
|
|
|
457
456
|
|
|
458
457
|
return (
|
|
459
458
|
<div className="flex h-full min-h-0 flex-col">
|
|
460
|
-
{/* Only the view switch belongs to the area: it says how the thing the breadcrumb names is
|
|
461
|
-
being shown, and it is mounted only where there is a flow to draw — a switch with nothing
|
|
462
|
-
to point at is what #19 rules out for the header. A flow is the one level that has runs,
|
|
463
|
-
so it is the one that offers the third view (#35). Everything that acts on the flow itself
|
|
464
|
-
left this bar for the flow's own title line below (#24). */}
|
|
465
|
-
{selectedFlowId ? (
|
|
466
|
-
<ActionSlot>
|
|
467
|
-
<ViewToggle views={["editor", "graph", "runs"]} />
|
|
468
|
-
</ActionSlot>
|
|
469
|
-
) : null}
|
|
470
459
|
{selectedFlowId && document.data ? (
|
|
471
460
|
<FlowTitle
|
|
472
461
|
flow={document.data.flow}
|
|
@@ -736,6 +725,11 @@ function FlowTitle({
|
|
|
736
725
|
// hit without looking is worth more than the primary button being outermost. `TitleRow` is what
|
|
737
726
|
// enforces that — nothing passed in here can get past the menu.
|
|
738
727
|
<TitleRow title={flow.title} description={flow.description} target={{ type: "flow", flow }}>
|
|
728
|
+
{/* ⚠️ #454: the toggle used to stand in the global header, beside the search — where what
|
|
729
|
+
applies EVERYWHERE stands. But it says how THIS flow is shown, and so belongs in the line
|
|
730
|
+
that names this flow. A flow is the only level with runs, and therefore the only one with
|
|
731
|
+
three views (#35). */}
|
|
732
|
+
<ViewToggle views={["editor", "graph", "runs"]} />
|
|
739
733
|
<TooltipProvider delayDuration={300}>
|
|
740
734
|
<Tooltip>
|
|
741
735
|
<TooltipTrigger asChild>
|
|
@@ -14,6 +14,7 @@ import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
|
14
14
|
import { kindIcons } from "@/kind-icon.ts";
|
|
15
15
|
import { ResourceMenu, type ResourceTarget } from "@/resource-menu/resource-menu.tsx";
|
|
16
16
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
17
|
+
import { useDateTime } from "@/time/time-context.tsx";
|
|
17
18
|
|
|
18
19
|
// The same record the menu is handed everywhere else, read off a level row. A re-labelling, never a
|
|
19
20
|
// second source of truth — a rename works on the whole record, not on a summary of it.
|
|
@@ -41,6 +42,7 @@ function targetOf(entry: TreeEntry): ResourceTarget {
|
|
|
41
42
|
export function FolderContents({ folderId }: { folderId: string }) {
|
|
42
43
|
const { data } = useIntelRouterContext();
|
|
43
44
|
const i18n = useI18n();
|
|
45
|
+
const dateTime = useDateTime();
|
|
44
46
|
const navigate = useNavigate();
|
|
45
47
|
const level = useQuery({
|
|
46
48
|
queryKey: treeLevelKey(folderId),
|
|
@@ -126,7 +128,7 @@ export function FolderContents({ folderId }: { folderId: string }) {
|
|
|
126
128
|
</button>
|
|
127
129
|
</TableCell>
|
|
128
130
|
<TableCell className="px-4 text-right text-sm text-muted-foreground tabular-nums">
|
|
129
|
-
{
|
|
131
|
+
{dateTime.on(changed)}
|
|
130
132
|
</TableCell>
|
|
131
133
|
{/* ⚠️ The same menu as the title line of the open thing, not a shorter one built
|
|
132
134
|
for here. #58 collected every action in one place so that place is always the
|
package/src/i18n/de.json
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
"$locale": "de-DE",
|
|
3
3
|
"app.name": "Intel",
|
|
4
4
|
"nav.primary": "Hauptnavigation",
|
|
5
|
-
"nav.nodes": "Wissen",
|
|
6
|
-
"nav.flows": "Flows",
|
|
7
5
|
"nav.tools": "Werkzeuge",
|
|
8
6
|
"shell.toggleSidebar": "Seitenleiste ein- oder ausklappen",
|
|
9
7
|
"shell.resizeSidebar": "Breite der Seitenleiste",
|
|
@@ -33,7 +31,8 @@
|
|
|
33
31
|
"tree.expandCalls": "Die Flows zeigen, die {title} aufruft",
|
|
34
32
|
"tree.collapseCalls": "Die Flows verbergen, die {title} aufruft",
|
|
35
33
|
"tree.add": "Zu {title} hinzufügen",
|
|
36
|
-
"tree.addRoot": "
|
|
34
|
+
"tree.addRoot": "Element",
|
|
35
|
+
"tree.addRoot.long": "Auf oberster Ebene anlegen",
|
|
37
36
|
"tree.new.folder": "Neuer Ordner",
|
|
38
37
|
"tree.new.document": "Neues Dokument",
|
|
39
38
|
"tree.new.table": "Neue Tabelle",
|
|
@@ -50,6 +49,7 @@
|
|
|
50
49
|
"tree.kind.table": "Tabelle",
|
|
51
50
|
"tree.kind.flow": "Flow",
|
|
52
51
|
"tree.move.action": "Verschieben nach …",
|
|
52
|
+
"tree.move.hint": "oder ziehen",
|
|
53
53
|
"tree.move.title": "{title} verschieben",
|
|
54
54
|
"tree.move.root": "Oberste Ebene",
|
|
55
55
|
"tree.move.dropRoot": "Hier ablegen, um auf die oberste Ebene zu verschieben",
|
|
@@ -88,6 +88,12 @@
|
|
|
88
88
|
"archive.empty": "Es ist nichts archiviert.",
|
|
89
89
|
"archive.failed": "Das Archiv konnte nicht gelesen werden. Prüfe deinen Zugriff und versuche es erneut.",
|
|
90
90
|
"archive.restoreFailed": "Es wurde nicht wiederhergestellt. Vielleicht hat jemand anderes es geändert — lade neu und versuche es erneut.",
|
|
91
|
+
"archive.purge": "{title} endgültig löschen",
|
|
92
|
+
"archive.purge.title": "Endgültig löschen?",
|
|
93
|
+
"archive.purge.body": "„{title}\u201c 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.",
|
|
94
|
+
"archive.purge.confirm": "Endgültig löschen",
|
|
95
|
+
"archive.purge.cancel": "Abbrechen",
|
|
96
|
+
"archive.purgeFailed": "Es konnte nicht gelöscht werden. Ein Ordner muss zuerst leer sein, und was ein publizierter Flow noch benutzt, kann gar nicht weg.",
|
|
91
97
|
"archive.archivedAt": "Archiviert {when}",
|
|
92
98
|
"resource.conflict": "Nicht geändert: Jemand anderes hat diesen Eintrag zuerst geändert. Lade ihn neu und versuche es erneut.",
|
|
93
99
|
"resource.forbidden": "Nicht geändert: Du darfst diesen Eintrag nicht ändern.",
|
|
@@ -97,6 +103,7 @@
|
|
|
97
103
|
"node.attachmentHelp": "Die kanonische Datei liegt privat in Intel. Ihre KI-lesbare Projektion wird getrennt indexiert.",
|
|
98
104
|
"node.table.summary": "{rows} Zeilen, {columns} Spalten",
|
|
99
105
|
"node.table.download": "CSV herunterladen",
|
|
106
|
+
"node.table.downloadFailed": "Die CSV konnte nicht erzeugt werden. Möglicherweise ist die Tabelle noch leer.",
|
|
100
107
|
"node.table.empty": "Noch keine Zeilen. Zeilen werden von Flows angehängt.",
|
|
101
108
|
"node.table.undefined": "Diese Tabelle hat noch keine Spalten.",
|
|
102
109
|
"node.select": "Wähle ein Dokument oder einen Ordner, um damit zu arbeiten.",
|
|
@@ -166,7 +173,7 @@
|
|
|
166
173
|
"node.saveError": "Dieses Dokument konnte nicht gespeichert werden. Lade neu und versuche es erneut.",
|
|
167
174
|
"tools.signingIn": "Du wirst am Firmenportal angemeldet …",
|
|
168
175
|
"tools.noAccess": "Kein Zugriff auf das Firmenportal",
|
|
169
|
-
"tools.noAccessHelp": "Dein
|
|
176
|
+
"tools.noAccessHelp": "Dein Konto darf das MCP-Portal der Firma nicht erreichen, deshalb gibt es hier nichts zu zeigen. Diese Erlaubnis wird nicht in Intel vergeben — bitte die Stelle darum, die in deiner Organisation die Konten verwaltet.",
|
|
170
177
|
"tools.empty": "Keine Werkzeuge für dich verfügbar",
|
|
171
178
|
"tools.emptyHelp": "Das Portal hat geantwortet, und es bietet deinem Konto keine Werkzeuge an. Welche Server es gibt und wer sie erreicht, entscheidet eine Administration im Portal.",
|
|
172
179
|
"tools.unreachable": "Portal nicht erreichbar",
|
|
@@ -290,6 +297,9 @@
|
|
|
290
297
|
"settings.description": "Sprache, Erscheinungsbild und Zeitzone. Alle drei gelten sofort und werden auf diesem Gerät gemerkt.",
|
|
291
298
|
"settings.language": "Sprache",
|
|
292
299
|
"settings.appearance": "Erscheinungsbild",
|
|
300
|
+
"settings.timezone": "Zeitzone",
|
|
301
|
+
"settings.timezone.device": "Dieses Gerät ({zone})",
|
|
302
|
+
"settings.timezone.hint": "Zeiten werden nur in dieser Zone GELESEN. Gespeichert und übertragen wird weiterhin UTC.",
|
|
293
303
|
"settings.appearance.system": "Dem System folgen",
|
|
294
304
|
"settings.appearance.light": "Hell",
|
|
295
305
|
"settings.appearance.dark": "Dunkel",
|
|
@@ -303,6 +313,7 @@
|
|
|
303
313
|
"reindex.failed": "Der Neuaufbau wurde nicht gestartet. Prüfe deine Berechtigung und versuche es noch einmal.",
|
|
304
314
|
"auth.signOut": "Abmelden",
|
|
305
315
|
"auth.signOutFailed": "Das Abmelden ist fehlgeschlagen. Prüfe deine Verbindung und versuche es erneut.",
|
|
316
|
+
"signIn.unreachable": "Intel war nicht erreichbar. Prüfe deine Verbindung und versuche es erneut.",
|
|
306
317
|
"signIn.refusedTitle": "Angemeldet, aber nicht angenommen",
|
|
307
318
|
"signIn.refusedBody": "Gate hat dich angemeldet, und diese Installation hat den Ausweis nicht angenommen. Du wurdest nicht zurück zum Login geschickt, denn dort beginnt die Schleife.",
|
|
308
319
|
"signIn.refusedLikely": "Meistens ist die Sitzung zwischen den beiden Schritten abgelaufen, oder der Ausweis wurde für eine andere Installation ausgestellt. Ein Neuladen ist einen Versuch wert; danach muss sich anschauen, wer diese Installation betreibt.",
|
package/src/i18n/en.json
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
"$locale": "en-US",
|
|
3
3
|
"app.name": "Intel",
|
|
4
4
|
"nav.primary": "Primary navigation",
|
|
5
|
-
"nav.nodes": "Intelligence",
|
|
6
|
-
"nav.flows": "Flows",
|
|
7
5
|
"nav.tools": "Tools",
|
|
8
6
|
"shell.toggleSidebar": "Collapse or expand the sidebar",
|
|
9
7
|
"shell.resizeSidebar": "Sidebar width",
|
|
@@ -33,7 +31,8 @@
|
|
|
33
31
|
"tree.expandCalls": "Show the flows {title} calls",
|
|
34
32
|
"tree.collapseCalls": "Hide the flows {title} calls",
|
|
35
33
|
"tree.add": "Add to {title}",
|
|
36
|
-
"tree.addRoot": "
|
|
34
|
+
"tree.addRoot": "Element",
|
|
35
|
+
"tree.addRoot.long": "Add at the top level",
|
|
37
36
|
"tree.new.folder": "New folder",
|
|
38
37
|
"tree.new.document": "New document",
|
|
39
38
|
"tree.new.table": "New table",
|
|
@@ -50,6 +49,7 @@
|
|
|
50
49
|
"tree.kind.table": "Table",
|
|
51
50
|
"tree.kind.flow": "Flow",
|
|
52
51
|
"tree.move.action": "Move to…",
|
|
52
|
+
"tree.move.hint": "or drag",
|
|
53
53
|
"tree.move.title": "Move {title}",
|
|
54
54
|
"tree.move.root": "Top level",
|
|
55
55
|
"tree.move.dropRoot": "Drop here to move to the top level",
|
|
@@ -88,6 +88,12 @@
|
|
|
88
88
|
"archive.empty": "Nothing is archived.",
|
|
89
89
|
"archive.failed": "The archive could not be read. Check your access and try again.",
|
|
90
90
|
"archive.restoreFailed": "It was not restored. Somebody else may have changed it — reload and try again.",
|
|
91
|
+
"archive.purge": "Delete {title} for good",
|
|
92
|
+
"archive.purge.title": "Delete this for good?",
|
|
93
|
+
"archive.purge.body": "“{title}” and everything belonging to it — every version, its content and its entries in the search index — will be gone. This cannot be undone.",
|
|
94
|
+
"archive.purge.confirm": "Delete for good",
|
|
95
|
+
"archive.purge.cancel": "Cancel",
|
|
96
|
+
"archive.purgeFailed": "It could not be deleted. A folder has to be empty first, and a node a published flow still uses cannot go at all.",
|
|
91
97
|
"archive.archivedAt": "Archived {when}",
|
|
92
98
|
"resource.conflict": "It was not changed: somebody else changed this entry first. Reload it and try again.",
|
|
93
99
|
"resource.forbidden": "It was not changed: you may not change this entry.",
|
|
@@ -97,6 +103,7 @@
|
|
|
97
103
|
"node.attachmentHelp": "The canonical file is stored privately in Intel. Its AI-readable projection is indexed separately.",
|
|
98
104
|
"node.table.summary": "{rows} rows, {columns} columns",
|
|
99
105
|
"node.table.download": "Download CSV",
|
|
106
|
+
"node.table.downloadFailed": "The CSV could not be created. The table may still be empty.",
|
|
100
107
|
"node.table.empty": "No rows yet. Rows are appended by flows.",
|
|
101
108
|
"node.table.undefined": "This table has no columns yet.",
|
|
102
109
|
"node.select": "Select a document or folder to work with it.",
|
|
@@ -166,7 +173,7 @@
|
|
|
166
173
|
"node.saveError": "This document could not be saved. Reload and try again.",
|
|
167
174
|
"tools.signingIn": "Signing you in to the company portal…",
|
|
168
175
|
"tools.noAccess": "No access to the company portal",
|
|
169
|
-
"tools.noAccessHelp": "Your
|
|
176
|
+
"tools.noAccessHelp": "Your account is not permitted to reach the company MCP portal, so there is nothing to show here. That permission is not given in Intel — ask whoever administers the accounts of your organisation to grant it.",
|
|
170
177
|
"tools.empty": "No tools available to you",
|
|
171
178
|
"tools.emptyHelp": "The portal answered, and it offers your account no tools. An administrator decides in the portal which servers exist and who may reach them.",
|
|
172
179
|
"tools.unreachable": "Portal not reachable",
|
|
@@ -290,6 +297,9 @@
|
|
|
290
297
|
"settings.description": "Language, appearance and timezone. All three take effect immediately and are remembered on this device.",
|
|
291
298
|
"settings.language": "Language",
|
|
292
299
|
"settings.appearance": "Appearance",
|
|
300
|
+
"settings.timezone": "Time zone",
|
|
301
|
+
"settings.timezone.device": "This device ({zone})",
|
|
302
|
+
"settings.timezone.hint": "Times are only READ in this zone. What is stored and sent stays UTC.",
|
|
293
303
|
"settings.appearance.system": "Follow the system",
|
|
294
304
|
"settings.appearance.light": "Light",
|
|
295
305
|
"settings.appearance.dark": "Dark",
|
|
@@ -303,6 +313,7 @@
|
|
|
303
313
|
"reindex.failed": "The rebuild was not started. Check your access and try again.",
|
|
304
314
|
"auth.signOut": "Sign out",
|
|
305
315
|
"auth.signOutFailed": "Signing out failed. Check your connection and try again.",
|
|
316
|
+
"signIn.unreachable": "Intel could not be reached. Check your connection and try again.",
|
|
306
317
|
"signIn.refusedTitle": "Signed in, but not accepted",
|
|
307
318
|
"signIn.refusedBody": "Gate signed you in, and this installation did not accept the credential. You have not been sent back to the login, because that is where the loop starts.",
|
|
308
319
|
"signIn.refusedLikely": "Most often the session expired between the two steps, or the credential was issued for a different installation. Reloading is worth one try; after that, whoever runs this installation has to look.",
|
package/src/i18n/es.json
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
"$locale": "es-ES",
|
|
3
3
|
"app.name": "Intel",
|
|
4
4
|
"nav.primary": "Navegación principal",
|
|
5
|
-
"nav.nodes": "Conocimiento",
|
|
6
|
-
"nav.flows": "Flujos",
|
|
7
5
|
"nav.tools": "Herramientas",
|
|
8
6
|
"shell.toggleSidebar": "Plegar o desplegar la barra lateral",
|
|
9
7
|
"shell.resizeSidebar": "Ancho de la barra lateral",
|
|
@@ -33,7 +31,8 @@
|
|
|
33
31
|
"tree.expandCalls": "Mostrar los flujos a los que llama {title}",
|
|
34
32
|
"tree.collapseCalls": "Ocultar los flujos a los que llama {title}",
|
|
35
33
|
"tree.add": "Añadir a {title}",
|
|
36
|
-
"tree.addRoot": "
|
|
34
|
+
"tree.addRoot": "Elemento",
|
|
35
|
+
"tree.addRoot.long": "Añadir en el nivel superior",
|
|
37
36
|
"tree.new.folder": "Carpeta nueva",
|
|
38
37
|
"tree.new.document": "Documento nuevo",
|
|
39
38
|
"tree.new.table": "Tabla nueva",
|
|
@@ -50,6 +49,7 @@
|
|
|
50
49
|
"tree.kind.table": "Tabla",
|
|
51
50
|
"tree.kind.flow": "Flujo",
|
|
52
51
|
"tree.move.action": "Mover a…",
|
|
52
|
+
"tree.move.hint": "o arrastra",
|
|
53
53
|
"tree.move.title": "Mover {title}",
|
|
54
54
|
"tree.move.root": "Nivel superior",
|
|
55
55
|
"tree.move.dropRoot": "Suelta aquí para mover al nivel superior",
|
|
@@ -88,6 +88,12 @@
|
|
|
88
88
|
"archive.empty": "No hay nada archivado.",
|
|
89
89
|
"archive.failed": "El archivo no se ha podido leer. Comprueba tu acceso e inténtalo de nuevo.",
|
|
90
90
|
"archive.restoreFailed": "No se ha restaurado. Puede que otra persona lo haya cambiado — recarga e inténtalo de nuevo.",
|
|
91
|
+
"archive.purge": "Eliminar «{title}» definitivamente",
|
|
92
|
+
"archive.purge.title": "¿Eliminar definitivamente?",
|
|
93
|
+
"archive.purge.body": "«{title}» y todo lo que le pertenece — cada versión, su contenido y sus entradas en el índice de búsqueda — desaparecerá. Esto no se puede deshacer.",
|
|
94
|
+
"archive.purge.confirm": "Eliminar definitivamente",
|
|
95
|
+
"archive.purge.cancel": "Cancelar",
|
|
96
|
+
"archive.purgeFailed": "No se pudo eliminar. Una carpeta debe estar vacía primero, y lo que un flujo publicado todavía usa no puede eliminarse.",
|
|
91
97
|
"archive.archivedAt": "Archivado {when}",
|
|
92
98
|
"resource.conflict": "No se ha cambiado: otra persona ha cambiado esta entrada antes. Recárgala e inténtalo de nuevo.",
|
|
93
99
|
"resource.forbidden": "No se ha cambiado: no puedes cambiar esta entrada.",
|
|
@@ -97,6 +103,7 @@
|
|
|
97
103
|
"node.attachmentHelp": "El archivo canónico se guarda en privado en Intel. Su proyección legible por la IA se indexa por separado.",
|
|
98
104
|
"node.table.summary": "{rows} filas, {columns} columnas",
|
|
99
105
|
"node.table.download": "Descargar CSV",
|
|
106
|
+
"node.table.downloadFailed": "No se pudo crear el CSV. Puede que la tabla todavía esté vacía.",
|
|
100
107
|
"node.table.empty": "Todavía no hay filas. Las filas las añaden los flujos.",
|
|
101
108
|
"node.table.undefined": "Esta tabla todavía no tiene columnas.",
|
|
102
109
|
"node.select": "Elige un documento o una carpeta para trabajar con ello.",
|
|
@@ -166,7 +173,7 @@
|
|
|
166
173
|
"node.saveError": "Este documento no se ha podido guardar. Recarga e inténtalo de nuevo.",
|
|
167
174
|
"tools.signingIn": "Iniciando tu sesión en el portal de la empresa…",
|
|
168
175
|
"tools.noAccess": "Sin acceso al portal de la empresa",
|
|
169
|
-
"tools.noAccessHelp": "Tu cuenta
|
|
176
|
+
"tools.noAccessHelp": "Tu cuenta no tiene permiso para llegar al portal MCP de la empresa, así que aquí no hay nada que mostrar. Ese permiso no se concede en Intel — pídeselo a quien administre las cuentas de tu organización.",
|
|
170
177
|
"tools.empty": "No tienes herramientas disponibles",
|
|
171
178
|
"tools.emptyHelp": "El portal ha respondido, y no ofrece ninguna herramienta a tu cuenta. Qué servidores existen y quién llega a ellos lo decide la administración en el portal.",
|
|
172
179
|
"tools.unreachable": "Portal no accesible",
|
|
@@ -290,6 +297,9 @@
|
|
|
290
297
|
"settings.description": "Idioma, apariencia y zona horaria. Las tres se aplican de inmediato y se recuerdan en este dispositivo.",
|
|
291
298
|
"settings.language": "Idioma",
|
|
292
299
|
"settings.appearance": "Apariencia",
|
|
300
|
+
"settings.timezone": "Zona horaria",
|
|
301
|
+
"settings.timezone.device": "Este dispositivo ({zone})",
|
|
302
|
+
"settings.timezone.hint": "Las horas solo se LEEN en esta zona. Lo que se guarda y se envía sigue siendo UTC.",
|
|
293
303
|
"settings.appearance.system": "Seguir al sistema",
|
|
294
304
|
"settings.appearance.light": "Claro",
|
|
295
305
|
"settings.appearance.dark": "Oscuro",
|
|
@@ -303,6 +313,7 @@
|
|
|
303
313
|
"reindex.failed": "No se inició la reconstrucción. Comprueba tu acceso e inténtalo de nuevo.",
|
|
304
314
|
"auth.signOut": "Cerrar sesión",
|
|
305
315
|
"auth.signOutFailed": "El cierre de sesión ha fallado. Comprueba tu conexión e inténtalo de nuevo.",
|
|
316
|
+
"signIn.unreachable": "No se pudo contactar con Intel. Revisa tu conexión e inténtalo de nuevo.",
|
|
306
317
|
"signIn.refusedTitle": "Sesión iniciada, pero no aceptada",
|
|
307
318
|
"signIn.refusedBody": "Gate ha iniciado tu sesión, y esta instalación no ha aceptado la credencial. No se te ha devuelto al inicio de sesión, porque ahí es donde empieza el bucle.",
|
|
308
319
|
"signIn.refusedLikely": "Lo más habitual es que la sesión caducara entre los dos pasos, o que la credencial se emitiera para otra instalación. Recargar merece un intento; después de eso, tiene que mirarlo quien gestiona esta instalación.",
|
package/src/main.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { QueryClientProvider } from "@tanstack/react-query";
|
|
2
|
-
import { RouterProvider } from "@tanstack/react-router";
|
|
3
2
|
import { StrictMode } from "react";
|
|
4
3
|
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { AppRoot, createRefusalStore } from "@/app-root/app-root.tsx";
|
|
5
5
|
import {
|
|
6
6
|
createIntelDataProvider,
|
|
7
7
|
loginPath,
|
|
@@ -11,9 +11,9 @@ import { createBrowserSignIn, createSignInGuard } from "@/data/sign-in/sign-in.t
|
|
|
11
11
|
import { I18nProvider } from "@/i18n/i18n-context.tsx";
|
|
12
12
|
import { createLanguages } from "@/i18n/i18n-languages/i18n-languages.ts";
|
|
13
13
|
import { createIntelRouter } from "@/router/router.tsx";
|
|
14
|
-
import { SignInRefused } from "@/sign-in-refused/sign-in-refused.tsx";
|
|
15
14
|
import { applyTheme, readThemeChoice, SystemThemeQuery, themeFor } from "@/theme/theme.ts";
|
|
16
15
|
import { ThemeProvider } from "@/theme/theme-context.tsx";
|
|
16
|
+
import { TimeZoneProvider } from "@/time/time-context.tsx";
|
|
17
17
|
import "./styles.css";
|
|
18
18
|
import "./theme/custom.css";
|
|
19
19
|
|
|
@@ -35,37 +35,31 @@ const signIn = createBrowserSignIn(loginPath);
|
|
|
35
35
|
const mounted = createRoot(root);
|
|
36
36
|
const queryClient = createIntelQueryClient();
|
|
37
37
|
|
|
38
|
-
// The
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// ⚠️ The refusal arrives from inside a fetch rather than from React, and it used to be answered by
|
|
39
|
+
// a module-level `let` plus a hand-written repaint. Both are gone (#456): the flag is a store React
|
|
40
|
+
// can subscribe to, and everything that decides what is on screen lives in `AppRoot` — a component
|
|
41
|
+
// a test can render. `main.tsx` is wiring again, and only wiring.
|
|
42
|
+
const refusal = createRefusalStore();
|
|
42
43
|
const data = createIntelDataProvider({
|
|
43
|
-
onUnauthorized: createSignInGuard(signIn, () =>
|
|
44
|
-
refused = true;
|
|
45
|
-
paint();
|
|
46
|
-
}),
|
|
44
|
+
onUnauthorized: createSignInGuard(signIn, () => refusal.refuse()),
|
|
47
45
|
});
|
|
48
46
|
const router = createIntelRouter({ data });
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
paint();
|
|
48
|
+
mounted.render(
|
|
49
|
+
<StrictMode>
|
|
50
|
+
{/* The refusal screen sits INSIDE the providers too: it is text like any other, and it is the
|
|
51
|
+
one screen a reader sees when nothing else works — English-only there would be the worst
|
|
52
|
+
place for it. */}
|
|
53
|
+
<I18nProvider languages={languages}>
|
|
54
|
+
<ThemeProvider store={window.localStorage}>
|
|
55
|
+
{/* ⚠️ INSIDE `I18nProvider`: the formatter needs both — the language says HOW a time is
|
|
56
|
+
written, the zone WHICH time it is (#467). */}
|
|
57
|
+
<TimeZoneProvider store={window.localStorage}>
|
|
58
|
+
<QueryClientProvider client={queryClient}>
|
|
59
|
+
<AppRoot data={data} router={router} refusal={refusal} />
|
|
60
|
+
</QueryClientProvider>
|
|
61
|
+
</TimeZoneProvider>
|
|
62
|
+
</ThemeProvider>
|
|
63
|
+
</I18nProvider>
|
|
64
|
+
</StrictMode>,
|
|
65
|
+
);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Node } from "@anchrd/intel-contract/node";
|
|
2
|
-
import {
|
|
3
|
-
import { Download } from "lucide-react";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
4
3
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
5
4
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
6
5
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
@@ -23,35 +22,17 @@ export function NodeTablePanel({ node }: { node: Node }) {
|
|
|
23
22
|
queryKey: ["node-table", node.id],
|
|
24
23
|
queryFn: () => data.getNodeTable(node.id),
|
|
25
24
|
});
|
|
26
|
-
// CSV is the export. The canonical body is already CSV, so downloading is handing over the bytes
|
|
27
|
-
// that are stored rather than rendering a second format that could drift from them.
|
|
28
|
-
const download = useMutation({
|
|
29
|
-
mutationFn: async () => {
|
|
30
|
-
const document_ = await data.getNode(node.id);
|
|
31
|
-
return new Blob([document_.content ?? ""], { type: "text/csv;charset=utf-8" });
|
|
32
|
-
},
|
|
33
|
-
onSuccess: (blob) => {
|
|
34
|
-
const url = URL.createObjectURL(blob);
|
|
35
|
-
const anchor = document.createElement("a");
|
|
36
|
-
anchor.href = url;
|
|
37
|
-
anchor.download = node.title.toLowerCase().endsWith(".csv")
|
|
38
|
-
? node.title
|
|
39
|
-
: `${node.title}.csv`;
|
|
40
|
-
document.body.append(anchor);
|
|
41
|
-
anchor.click();
|
|
42
|
-
anchor.remove();
|
|
43
|
-
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
|
|
47
25
|
return (
|
|
48
26
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
49
|
-
{/* ⚠️
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
{/* ⚠️ This used to be a second bar of its own, directly under the title line (#54): two
|
|
28
|
+
headers stacked, and the table starting a whole row lower for it. A table is one node kind
|
|
29
|
+
among four, not a screen with its own chrome — so the count goes beside the title as a
|
|
30
|
+
quiet word. It is rendered from here rather than from the screen because this is where the
|
|
31
|
+
query lives; the screen would otherwise have to load a table it does not show.
|
|
32
|
+
|
|
33
|
+
⚠️ The CSV export used to stand here too, as a button WITH TEXT beside two icon buttons.
|
|
34
|
+
Since #454 it is an entry in the title line's three-dot menu (`resource-menu.tsx`) — where
|
|
35
|
+
the bundle export sits as well, because both do the same: hand out the whole thing. */}
|
|
55
36
|
<ActionSlot name="title-meta">
|
|
56
37
|
{table.data
|
|
57
38
|
? i18n.t("node.table.summary", {
|
|
@@ -60,27 +41,6 @@ export function NodeTablePanel({ node }: { node: Node }) {
|
|
|
60
41
|
})
|
|
61
42
|
: ""}
|
|
62
43
|
</ActionSlot>
|
|
63
|
-
<ActionSlot name="title-actions">
|
|
64
|
-
<button
|
|
65
|
-
type="button"
|
|
66
|
-
onClick={() => download.mutate()}
|
|
67
|
-
// Unchanged: nothing to export before the header is known, and a button that answers with
|
|
68
|
-
// an empty file is worse than one that says it is not ready.
|
|
69
|
-
disabled={download.isPending || !table.data?.columns.length}
|
|
70
|
-
className="inline-flex h-8 items-center gap-2 rounded-md border px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50"
|
|
71
|
-
>
|
|
72
|
-
<Download aria-hidden="true" className="size-4" />
|
|
73
|
-
{i18n.t("node.table.download")}
|
|
74
|
-
</button>
|
|
75
|
-
</ActionSlot>
|
|
76
|
-
{/* ⚠️ The refusal stays down here, in the body, where a sentence has room to be read. The
|
|
77
|
-
title line has none — a failed export that only greyed a button in a header would be a
|
|
78
|
-
failure nobody is told about. */}
|
|
79
|
-
{download.isError ? (
|
|
80
|
-
<p role="alert" className="mx-6 mt-4 text-sm text-destructive">
|
|
81
|
-
{i18n.t("node.downloadFailed")}
|
|
82
|
-
</p>
|
|
83
|
-
) : null}
|
|
84
44
|
{table.isPending ? (
|
|
85
45
|
<p className="p-6 text-sm text-muted-foreground">{i18n.t("common.loading")}</p>
|
|
86
46
|
) : table.isError ? (
|
package/src/nodes/nodes.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
|
3
3
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
|
4
4
|
import { Download, History, Paperclip } from "lucide-react";
|
|
5
5
|
import { lazy, Suspense, useState } from "react";
|
|
6
|
-
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
7
6
|
import { ViewToggle } from "@/app/view-toggle/view-toggle.tsx";
|
|
8
7
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
9
8
|
import { refusalOf } from "@/data/request-refusal/request-refusal.ts";
|
|
@@ -13,6 +12,7 @@ import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
|
13
12
|
import { NodeTablePanel } from "@/node-table/node-table.tsx";
|
|
14
13
|
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
15
14
|
import { graphViewFrom, selectedFrom } from "@/router/selection-search.ts";
|
|
15
|
+
import { useDateTime } from "@/time/time-context.tsx";
|
|
16
16
|
import { TitleRow } from "@/title-row/title-row.tsx";
|
|
17
17
|
|
|
18
18
|
const NodeEditor = lazy(async () => ({
|
|
@@ -54,21 +54,24 @@ export function Nodes() {
|
|
|
54
54
|
});
|
|
55
55
|
return (
|
|
56
56
|
<div className="flex h-full min-h-0 flex-col">
|
|
57
|
-
{/* The screen owns the one action that belongs to the tree as a whole. Creating things is
|
|
58
|
-
the tree's, in the sidebar, at the place a new thing is meant to go.
|
|
59
|
-
|
|
60
|
-
⚠️ Mounted only where there is a graph to switch to. A switch on a document would point at
|
|
61
|
-
nothing, and the header is where that gets noticed first (#19). */}
|
|
62
|
-
{graphable ? (
|
|
63
|
-
<ActionSlot>
|
|
64
|
-
<ViewToggle />
|
|
65
|
-
</ActionSlot>
|
|
66
|
-
) : null}
|
|
67
57
|
<section className="relative flex min-h-0 min-w-0 flex-1 flex-col bg-card">
|
|
68
58
|
{/* ⚠️ Only the root draws its graph on its own. A folder's graph hangs *under* the folder's
|
|
69
59
|
title line rather than in place of it (#58): the line is the one place its menu lives,
|
|
70
60
|
and a folder that could only be renamed while the graph happened to be switched off
|
|
71
61
|
would be a folder one cannot rename. */}
|
|
62
|
+
{/* ⚠️ #454 took the toggle out of the GLOBAL header — there it applied to the whole
|
|
63
|
+
application although it is a view onto the selected item. For an item it now stands in
|
|
64
|
+
that item's title line.
|
|
65
|
+
|
|
66
|
+
⚠️ The root has no title line, and that is exactly why it gets a row of its own here.
|
|
67
|
+
Without it the graph of the WHOLE tree would have been unreachable after the move — the
|
|
68
|
+
toggle was its only way in and out. Moving a control must not abolish a view; that would
|
|
69
|
+
be a decision the ticket did not contain. */}
|
|
70
|
+
{selectedId === null ? (
|
|
71
|
+
<div className="flex shrink-0 items-center justify-end gap-2 border-b px-6 py-2">
|
|
72
|
+
<ViewToggle />
|
|
73
|
+
</div>
|
|
74
|
+
) : null}
|
|
72
75
|
{selectedId === null && selection.graph ? (
|
|
73
76
|
<GraphPane
|
|
74
77
|
query={relations}
|
|
@@ -122,14 +125,16 @@ export function Nodes() {
|
|
|
122
125
|
⚠️ The order of the right-hand group is `TitleRow`'s (#53), not this screen's: the
|
|
123
126
|
version button is handed over as a child and lands before the menu whatever else
|
|
124
127
|
shows up later.
|
|
125
|
-
⚠️
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
⚠️ #454: the toggle now stands HERE. It used to stand in the global header, beside
|
|
129
|
+
the search — where what applies everywhere stands. But a graph view does not apply
|
|
130
|
+
everywhere, it is a view onto THIS item. It is only drawn where there is a graph: on
|
|
131
|
+
a document it pointed at nothing. */}
|
|
128
132
|
<TitleRow
|
|
129
133
|
title={selected.title}
|
|
130
134
|
description={selected.description}
|
|
131
135
|
target={{ type: "node", node: selected }}
|
|
132
136
|
>
|
|
137
|
+
{graphable && <ViewToggle />}
|
|
133
138
|
{selected.kind !== "folder" && (
|
|
134
139
|
<TooltipProvider delayDuration={300}>
|
|
135
140
|
<Tooltip>
|
|
@@ -234,6 +239,7 @@ function AttachmentPanel({ node }: { node: Node }) {
|
|
|
234
239
|
function VersionHistory({ nodeId, close }: { nodeId: string; close(): void }) {
|
|
235
240
|
const { data } = useIntelRouterContext();
|
|
236
241
|
const i18n = useI18n();
|
|
242
|
+
const dateTime = useDateTime();
|
|
237
243
|
const versions = useQuery({
|
|
238
244
|
queryKey: ["node-versions", nodeId],
|
|
239
245
|
queryFn: () => data.listNodeVersions(nodeId),
|
|
@@ -269,10 +275,7 @@ function VersionHistory({ nodeId, close }: { nodeId: string; close(): void }) {
|
|
|
269
275
|
{i18n.t("node.version", { sequence: version.sequence })}
|
|
270
276
|
</span>
|
|
271
277
|
<time className="mt-1 block text-xs text-muted-foreground" dateTime={version.createdAt}>
|
|
272
|
-
{
|
|
273
|
-
dateStyle: "medium",
|
|
274
|
-
timeStyle: "short",
|
|
275
|
-
}).format(new Date(version.createdAt))}
|
|
278
|
+
{dateTime.at(version.createdAt)}
|
|
276
279
|
</time>
|
|
277
280
|
</li>
|
|
278
281
|
))}
|