@tldiagram/core-ui 1.91.0 → 1.93.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/dist/api/client.d.ts +13 -1
- package/dist/components/ElementNode.d.ts +9 -0
- package/dist/config/runtime-vscode.d.ts +1 -0
- package/dist/config/runtime.d.ts +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +10063 -9512
- package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +2 -1
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +20 -21
- package/dist/shims/empty-node-module.d.ts +2 -0
- package/dist/store/useStore.d.ts +78 -0
- package/dist/store/useStore.test.d.ts +1 -0
- package/package.json +7 -4
- package/src/App.tsx +0 -4
- package/src/api/client.ts +39 -1
- package/src/components/ElementNode.tsx +11 -58
- package/src/components/ElementPanel.tsx +2 -2
- package/src/components/LayoutSection.tsx +68 -93
- package/src/components/ViewGridNode.tsx +1 -4
- package/src/components/ZUI/renderer.ts +166 -66
- package/src/components/ZUI/useZUIInteraction.ts +235 -81
- package/src/config/runtime-vscode.ts +6 -0
- package/src/config/runtime.ts +4 -0
- package/src/index.ts +0 -1
- package/src/main.tsx +26 -14
- package/src/pages/ViewEditor/context.tsx +12 -4
- package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +172 -121
- package/src/pages/ViewEditor/hooks/useViewData.ts +455 -253
- package/src/pages/ViewEditor/index.tsx +45 -32
- package/src/shims/empty-node-module.ts +1 -0
- package/src/store/useStore.test.ts +272 -0
- package/src/store/useStore.ts +285 -0
- package/dist/demo/DemoPage.d.ts +0 -9
- package/dist/demo/seed.d.ts +0 -9
- package/dist/demo/store.d.ts +0 -137
- package/src/demo/DemoPage.tsx +0 -184
- package/src/demo/seed.ts +0 -67
- package/src/demo/store.ts +0 -536
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
import type { Edge as RFEdge, Node as RFNode } from 'reactflow'
|
|
3
|
+
import type {
|
|
4
|
+
Connector,
|
|
5
|
+
IncomingViewConnector,
|
|
6
|
+
LibraryElement,
|
|
7
|
+
PlacedElement,
|
|
8
|
+
ViewConnector,
|
|
9
|
+
ViewTreeNode,
|
|
10
|
+
} from '../types'
|
|
11
|
+
|
|
12
|
+
export type StoreSetter<T> = T | ((previous: T) => T)
|
|
13
|
+
|
|
14
|
+
export type ViewEditorUiState = {
|
|
15
|
+
viewId: number | null
|
|
16
|
+
canEdit: boolean
|
|
17
|
+
isOwner: boolean
|
|
18
|
+
isFreePlan: boolean
|
|
19
|
+
snapToGrid: boolean
|
|
20
|
+
selectedElement: LibraryElement | null
|
|
21
|
+
selectedConnector: Connector | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ViewContentLinks = {
|
|
25
|
+
linksMap: Record<number, ViewConnector[]>
|
|
26
|
+
parentLinksMap: Record<number, ViewConnector[]>
|
|
27
|
+
incomingLinks: IncomingViewConnector[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ViewContentPayload = ViewContentLinks & {
|
|
31
|
+
view: ViewTreeNode | null
|
|
32
|
+
viewElements: PlacedElement[]
|
|
33
|
+
connectors: Connector[]
|
|
34
|
+
treeData: ViewTreeNode[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CanvasStoreState = ViewEditorUiState & {
|
|
38
|
+
view: ViewTreeNode | null | undefined
|
|
39
|
+
viewElements: PlacedElement[]
|
|
40
|
+
connectors: Connector[]
|
|
41
|
+
nodes: RFNode[]
|
|
42
|
+
edges: RFEdge[]
|
|
43
|
+
linksMap: Record<number, ViewConnector[]>
|
|
44
|
+
parentLinksMap: Record<number, ViewConnector[]>
|
|
45
|
+
incomingLinks: IncomingViewConnector[]
|
|
46
|
+
treeData: ViewTreeNode[]
|
|
47
|
+
allElements: LibraryElement[]
|
|
48
|
+
libraryRefresh: number
|
|
49
|
+
|
|
50
|
+
setViewEditorUi: (patch: Partial<ViewEditorUiState>) => void
|
|
51
|
+
setSnapToGrid: (snapToGrid: boolean) => void
|
|
52
|
+
setSelectedElement: (selectedElement: LibraryElement | null) => void
|
|
53
|
+
setSelectedConnector: (selectedConnector: Connector | null) => void
|
|
54
|
+
setView: (view: ViewTreeNode | null | undefined) => void
|
|
55
|
+
setViewElements: (next: StoreSetter<PlacedElement[]>) => void
|
|
56
|
+
setConnectors: (next: StoreSetter<Connector[]>) => void
|
|
57
|
+
setNodes: (next: StoreSetter<RFNode[]>) => void
|
|
58
|
+
setEdges: (next: StoreSetter<RFEdge[]>) => void
|
|
59
|
+
setLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void
|
|
60
|
+
setParentLinksMap: (next: StoreSetter<Record<number, ViewConnector[]>>) => void
|
|
61
|
+
setIncomingLinks: (next: StoreSetter<IncomingViewConnector[]>) => void
|
|
62
|
+
setTreeData: (next: StoreSetter<ViewTreeNode[]>) => void
|
|
63
|
+
setAllElements: (next: StoreSetter<LibraryElement[]>) => void
|
|
64
|
+
setLibraryRefresh: (next: StoreSetter<number>) => void
|
|
65
|
+
resetCanvas: () => void
|
|
66
|
+
hydrateViewContent: (payload: ViewContentPayload) => void
|
|
67
|
+
updateElementPosition: (elementId: number, x: number, y: number) => void
|
|
68
|
+
removeElementPlacement: (elementId: number) => void
|
|
69
|
+
removeElementEverywhere: (elementId: number) => void
|
|
70
|
+
mergeSavedElement: (saved: LibraryElement) => void
|
|
71
|
+
upsertConnector: (connector: Connector) => void
|
|
72
|
+
replaceConnector: (connector: Connector) => void
|
|
73
|
+
removeConnector: (connectorId: number) => void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const emptyViewEditorUiState: ViewEditorUiState = {
|
|
77
|
+
viewId: null,
|
|
78
|
+
canEdit: false,
|
|
79
|
+
isOwner: false,
|
|
80
|
+
isFreePlan: false,
|
|
81
|
+
snapToGrid: false,
|
|
82
|
+
selectedElement: null,
|
|
83
|
+
selectedConnector: null,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveSetter<T>(next: StoreSetter<T>, previous: T): T {
|
|
87
|
+
return typeof next === 'function' ? (next as (previous: T) => T)(previous) : next
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function findViewByOwner(nodes: ViewTreeNode[], elementId: number): ViewTreeNode | null {
|
|
91
|
+
for (const node of nodes) {
|
|
92
|
+
if (node.owner_element_id !== null && Number(node.owner_element_id) === Number(elementId)) return node
|
|
93
|
+
const found = findViewByOwner(node.children, elementId)
|
|
94
|
+
if (found) return found
|
|
95
|
+
}
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function findViewPath(nodes: ViewTreeNode[], targetId: number, path: ViewTreeNode[] = []): ViewTreeNode[] | null {
|
|
100
|
+
for (const node of nodes) {
|
|
101
|
+
if (node.id === targetId) return [...path, node]
|
|
102
|
+
const found = findViewPath(node.children, targetId, [...path, node])
|
|
103
|
+
if (found) return found
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildViewContentLinks(tree: ViewTreeNode[], viewId: number, viewElements: PlacedElement[]): ViewContentLinks {
|
|
109
|
+
const linksMap: Record<number, ViewConnector[]> = {}
|
|
110
|
+
const parentLinksMap: Record<number, ViewConnector[]> = {}
|
|
111
|
+
|
|
112
|
+
const viewPath = findViewPath(tree, viewId)
|
|
113
|
+
const parentView = viewPath && viewPath.length > 1 ? viewPath[viewPath.length - 2] : null
|
|
114
|
+
const currentViewInTree = viewPath ? viewPath[viewPath.length - 1] : null
|
|
115
|
+
|
|
116
|
+
const incomingLinks: IncomingViewConnector[] = []
|
|
117
|
+
if (parentView && currentViewInTree?.owner_element_id) {
|
|
118
|
+
incomingLinks.push({
|
|
119
|
+
id: 0,
|
|
120
|
+
element_id: currentViewInTree.owner_element_id,
|
|
121
|
+
element_name: 'Parent',
|
|
122
|
+
from_view_id: parentView.id,
|
|
123
|
+
from_view_name: parentView.name,
|
|
124
|
+
to_view_id: viewId,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const element of viewElements) {
|
|
129
|
+
const childView = findViewByOwner(tree, element.element_id)
|
|
130
|
+
if (childView) {
|
|
131
|
+
linksMap[element.element_id] = [{
|
|
132
|
+
id: 0,
|
|
133
|
+
element_id: element.element_id,
|
|
134
|
+
from_view_id: viewId,
|
|
135
|
+
to_view_id: childView.id,
|
|
136
|
+
to_view_name: childView.name,
|
|
137
|
+
relation_type: 'child',
|
|
138
|
+
}]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (parentView) {
|
|
142
|
+
parentLinksMap[element.element_id] = [{
|
|
143
|
+
id: 0,
|
|
144
|
+
element_id: element.element_id,
|
|
145
|
+
from_view_id: parentView.id,
|
|
146
|
+
to_view_id: parentView.id,
|
|
147
|
+
to_view_name: parentView.name,
|
|
148
|
+
relation_type: 'parent',
|
|
149
|
+
}]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { linksMap, parentLinksMap, incomingLinks }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function selectExistingElementIds(state: Pick<CanvasStoreState, 'viewElements'>): Set<number> {
|
|
157
|
+
return new Set(state.viewElements.map((element) => element.element_id))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function selectElementById(state: Pick<CanvasStoreState, 'viewElements'>, elementId: number): PlacedElement | undefined {
|
|
161
|
+
return state.viewElements.find((element) => element.element_id === elementId)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function selectConnectorById(state: Pick<CanvasStoreState, 'connectors'>, connectorId: number): Connector | undefined {
|
|
165
|
+
return state.connectors.find((connector) => connector.id === connectorId)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function updatePlacedElementPosition(elements: PlacedElement[], elementId: number, x: number, y: number): PlacedElement[] {
|
|
169
|
+
let changed = false
|
|
170
|
+
const next = elements.map((element) => {
|
|
171
|
+
if (element.element_id !== elementId) return element
|
|
172
|
+
if (element.position_x === x && element.position_y === y) return element
|
|
173
|
+
changed = true
|
|
174
|
+
return { ...element, position_x: x, position_y: y }
|
|
175
|
+
})
|
|
176
|
+
return changed ? next : elements
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function removePlacedElement(elements: PlacedElement[], elementId: number): PlacedElement[] {
|
|
180
|
+
return elements.filter((element) => element.element_id !== elementId)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[] {
|
|
184
|
+
return elements.map((element) =>
|
|
185
|
+
element.element_id === saved.id
|
|
186
|
+
? {
|
|
187
|
+
...element,
|
|
188
|
+
name: saved.name,
|
|
189
|
+
description: saved.description,
|
|
190
|
+
kind: saved.kind,
|
|
191
|
+
technology: saved.technology,
|
|
192
|
+
url: saved.url,
|
|
193
|
+
logo_url: saved.logo_url,
|
|
194
|
+
technology_connectors: saved.technology_connectors,
|
|
195
|
+
tags: saved.tags,
|
|
196
|
+
repo: saved.repo,
|
|
197
|
+
branch: saved.branch,
|
|
198
|
+
file_path: saved.file_path,
|
|
199
|
+
language: saved.language,
|
|
200
|
+
}
|
|
201
|
+
: element,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function upsertConnectorInList(connectors: Connector[], connector: Connector): Connector[] {
|
|
206
|
+
const index = connectors.findIndex((candidate) => candidate.id === connector.id)
|
|
207
|
+
if (index === -1) return [...connectors, connector]
|
|
208
|
+
const next = connectors.slice()
|
|
209
|
+
next[index] = connector
|
|
210
|
+
return next
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function removeConnectorFromList(connectors: Connector[], connectorId: number): Connector[] {
|
|
214
|
+
return connectors.filter((connector) => connector.id !== connectorId)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const useStore = create<CanvasStoreState>((set) => ({
|
|
218
|
+
...emptyViewEditorUiState,
|
|
219
|
+
view: undefined,
|
|
220
|
+
viewElements: [],
|
|
221
|
+
connectors: [],
|
|
222
|
+
nodes: [],
|
|
223
|
+
edges: [],
|
|
224
|
+
linksMap: {},
|
|
225
|
+
parentLinksMap: {},
|
|
226
|
+
incomingLinks: [],
|
|
227
|
+
treeData: [],
|
|
228
|
+
allElements: [],
|
|
229
|
+
libraryRefresh: 0,
|
|
230
|
+
|
|
231
|
+
setViewEditorUi: (patch) => set((state) => ({ ...state, ...patch })),
|
|
232
|
+
setSnapToGrid: (snapToGrid) => set({ snapToGrid }),
|
|
233
|
+
setSelectedElement: (selectedElement) => set({ selectedElement }),
|
|
234
|
+
setSelectedConnector: (selectedConnector) => set({ selectedConnector }),
|
|
235
|
+
setView: (view) => set({ view }),
|
|
236
|
+
setViewElements: (next) => set((state) => ({ viewElements: resolveSetter(next, state.viewElements) })),
|
|
237
|
+
setConnectors: (next) => set((state) => ({ connectors: resolveSetter(next, state.connectors) })),
|
|
238
|
+
setNodes: (next) => set((state) => ({ nodes: resolveSetter(next, state.nodes) })),
|
|
239
|
+
setEdges: (next) => set((state) => ({ edges: resolveSetter(next, state.edges) })),
|
|
240
|
+
setLinksMap: (next) => set((state) => ({ linksMap: resolveSetter(next, state.linksMap) })),
|
|
241
|
+
setParentLinksMap: (next) => set((state) => ({ parentLinksMap: resolveSetter(next, state.parentLinksMap) })),
|
|
242
|
+
setIncomingLinks: (next) => set((state) => ({ incomingLinks: resolveSetter(next, state.incomingLinks) })),
|
|
243
|
+
setTreeData: (next) => set((state) => ({ treeData: resolveSetter(next, state.treeData) })),
|
|
244
|
+
setAllElements: (next) => set((state) => ({ allElements: resolveSetter(next, state.allElements) })),
|
|
245
|
+
setLibraryRefresh: (next) => set((state) => ({ libraryRefresh: resolveSetter(next, state.libraryRefresh) })),
|
|
246
|
+
resetCanvas: () => set({ nodes: [], edges: [] }),
|
|
247
|
+
hydrateViewContent: (payload) => set({
|
|
248
|
+
view: payload.view,
|
|
249
|
+
viewElements: payload.viewElements,
|
|
250
|
+
connectors: payload.connectors,
|
|
251
|
+
linksMap: payload.linksMap,
|
|
252
|
+
parentLinksMap: payload.parentLinksMap,
|
|
253
|
+
incomingLinks: payload.incomingLinks,
|
|
254
|
+
treeData: payload.treeData,
|
|
255
|
+
}),
|
|
256
|
+
updateElementPosition: (elementId, x, y) => set((state) => ({
|
|
257
|
+
viewElements: updatePlacedElementPosition(state.viewElements, elementId, x, y),
|
|
258
|
+
})),
|
|
259
|
+
removeElementPlacement: (elementId) => set((state) => ({
|
|
260
|
+
viewElements: removePlacedElement(state.viewElements, elementId),
|
|
261
|
+
})),
|
|
262
|
+
removeElementEverywhere: (elementId) => set((state) => ({
|
|
263
|
+
viewElements: removePlacedElement(state.viewElements, elementId),
|
|
264
|
+
libraryRefresh: state.libraryRefresh + 1,
|
|
265
|
+
})),
|
|
266
|
+
mergeSavedElement: (saved) => set((state) => ({
|
|
267
|
+
viewElements: mergeSavedElementIntoPlacements(state.viewElements, saved),
|
|
268
|
+
libraryRefresh: state.libraryRefresh + 1,
|
|
269
|
+
})),
|
|
270
|
+
upsertConnector: (connector) => set((state) => ({
|
|
271
|
+
connectors: upsertConnectorInList(state.connectors, connector),
|
|
272
|
+
})),
|
|
273
|
+
replaceConnector: (connector) => set((state) => ({
|
|
274
|
+
connectors: upsertConnectorInList(state.connectors, connector),
|
|
275
|
+
})),
|
|
276
|
+
removeConnector: (connectorId) => set((state) => ({
|
|
277
|
+
connectors: removeConnectorFromList(state.connectors, connectorId),
|
|
278
|
+
})),
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
export const canvasSelectors = {
|
|
282
|
+
existingElementIds: selectExistingElementIds,
|
|
283
|
+
elementById: selectElementById,
|
|
284
|
+
connectorById: selectConnectorById,
|
|
285
|
+
}
|
package/dist/demo/DemoPage.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Demo entry point.
|
|
3
|
-
* Overrides the real `api` singleton with localStorage-backed implementations
|
|
4
|
-
* and patches window.history to redirect /views/:id → /demo/:id, all for the
|
|
5
|
-
* lifetime of this component. Restores everything on unmount.
|
|
6
|
-
* No auth required; served at /demo and /demo/:id routes.
|
|
7
|
-
*/
|
|
8
|
-
export declare function DemoNavigator(): null;
|
|
9
|
-
export default function DemoPage(): import("react/jsx-runtime").JSX.Element;
|
package/dist/demo/seed.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer } from '../types';
|
|
2
|
-
export declare const DEMO_ELEMENTS: LibraryElement[];
|
|
3
|
-
export declare const DEMO_VIEWS: ViewTreeNode[];
|
|
4
|
-
type ViewPlacements = Record<number, PlacedElement[]>;
|
|
5
|
-
type ViewConnectors = Record<number, Connector[]>;
|
|
6
|
-
export declare const DEMO_PLACEMENTS: ViewPlacements;
|
|
7
|
-
export declare const DEMO_CONNECTORS: ViewConnectors;
|
|
8
|
-
export declare const DEMO_LAYERS: Record<number, ViewLayer[]>;
|
|
9
|
-
export {};
|
package/dist/demo/store.d.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* localStorage-backed store for the demo mode.
|
|
3
|
-
* Implements the subset of the `api` interface used by ViewEditor and its hooks.
|
|
4
|
-
* Data is scoped under the `diag:demo:*` key namespace to avoid colliding
|
|
5
|
-
* with a real logged-in session.
|
|
6
|
-
*/
|
|
7
|
-
import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer, Tag, ElementPlacement, ExploreData } from '../types';
|
|
8
|
-
export declare function initDemoStore(): void;
|
|
9
|
-
export declare function resetDemoStore(): void;
|
|
10
|
-
export declare const demoApi: {
|
|
11
|
-
explore: {
|
|
12
|
-
load: () => Promise<ExploreData>;
|
|
13
|
-
};
|
|
14
|
-
elements: {
|
|
15
|
-
list: (_params?: unknown) => Promise<LibraryElement[]>;
|
|
16
|
-
get: (id: number) => Promise<LibraryElement>;
|
|
17
|
-
create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
|
|
18
|
-
update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
|
|
19
|
-
delete: (_orgId: string, id: number) => Promise<void>;
|
|
20
|
-
placements: (id: number) => Promise<ElementPlacement[]>;
|
|
21
|
-
};
|
|
22
|
-
workspace: {
|
|
23
|
-
orgs: {
|
|
24
|
-
tagColors: {
|
|
25
|
-
list: () => Promise<Record<string, Tag>>;
|
|
26
|
-
set: (tag: string, color: string, description?: string) => Promise<void>;
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
views: {
|
|
30
|
-
list: () => Promise<{
|
|
31
|
-
id: number;
|
|
32
|
-
owner_element_id: number | null;
|
|
33
|
-
name: string;
|
|
34
|
-
label: string | null;
|
|
35
|
-
is_root: boolean;
|
|
36
|
-
created_at: string;
|
|
37
|
-
updated_at: string;
|
|
38
|
-
}[]>;
|
|
39
|
-
get: (id: number) => Promise<ViewTreeNode>;
|
|
40
|
-
content: (id: number) => Promise<{
|
|
41
|
-
placements: PlacedElement[];
|
|
42
|
-
connectors: Connector[];
|
|
43
|
-
}>;
|
|
44
|
-
tree: () => Promise<ViewTreeNode[]>;
|
|
45
|
-
create: (data: {
|
|
46
|
-
name: string;
|
|
47
|
-
label?: string;
|
|
48
|
-
parent_view_id?: number | null;
|
|
49
|
-
}) => Promise<{
|
|
50
|
-
id: number;
|
|
51
|
-
owner_element_id: number | null;
|
|
52
|
-
name: string;
|
|
53
|
-
label: string | null;
|
|
54
|
-
is_root: boolean;
|
|
55
|
-
created_at: string;
|
|
56
|
-
updated_at: string;
|
|
57
|
-
}>;
|
|
58
|
-
update: (id: number, data: {
|
|
59
|
-
name: string;
|
|
60
|
-
label?: string;
|
|
61
|
-
}) => Promise<{
|
|
62
|
-
id: number;
|
|
63
|
-
owner_element_id: number | null;
|
|
64
|
-
name: string;
|
|
65
|
-
label: string | null;
|
|
66
|
-
is_root: boolean;
|
|
67
|
-
created_at: string;
|
|
68
|
-
updated_at: string;
|
|
69
|
-
}>;
|
|
70
|
-
delete: (_orgId: string, id: number) => Promise<void>;
|
|
71
|
-
placements: {
|
|
72
|
-
list: (viewId: number) => Promise<ElementPlacement[]>;
|
|
73
|
-
add: (viewId: number, elementId: number, x?: number, y?: number) => Promise<ElementPlacement>;
|
|
74
|
-
updatePosition: (viewId: number, elementId: number, x: number, y: number) => Promise<void>;
|
|
75
|
-
remove: (viewId: number, elementId: number) => Promise<void>;
|
|
76
|
-
};
|
|
77
|
-
layers: {
|
|
78
|
-
list: (viewId: number) => Promise<ViewLayer[]>;
|
|
79
|
-
create: (viewId: number, data: {
|
|
80
|
-
name: string;
|
|
81
|
-
tags: string[];
|
|
82
|
-
color?: string;
|
|
83
|
-
}) => Promise<ViewLayer>;
|
|
84
|
-
update: (viewId: number, layerId: number, data: Partial<ViewLayer>) => Promise<ViewLayer>;
|
|
85
|
-
delete: (viewId: number, layerId: number) => Promise<void>;
|
|
86
|
-
};
|
|
87
|
-
reactions: {
|
|
88
|
-
list: (_viewId: number) => Promise<never[]>;
|
|
89
|
-
};
|
|
90
|
-
threads: {
|
|
91
|
-
listForElement: () => Promise<never[]>;
|
|
92
|
-
listForConnector: () => Promise<never[]>;
|
|
93
|
-
createForElement: () => Promise<never>;
|
|
94
|
-
createForConnector: () => Promise<never>;
|
|
95
|
-
addComment: () => Promise<never>;
|
|
96
|
-
resolve: () => Promise<void>;
|
|
97
|
-
};
|
|
98
|
-
thumbnail: (_id: number) => Promise<null>;
|
|
99
|
-
rename: (id: number, name: string) => Promise<{
|
|
100
|
-
id: number;
|
|
101
|
-
owner_element_id: number | null;
|
|
102
|
-
name: string;
|
|
103
|
-
label: string | null;
|
|
104
|
-
is_root: boolean;
|
|
105
|
-
created_at: string;
|
|
106
|
-
updated_at: string;
|
|
107
|
-
}>;
|
|
108
|
-
setLevel: () => Promise<void>;
|
|
109
|
-
reparent: () => Promise<never>;
|
|
110
|
-
};
|
|
111
|
-
connectors: {
|
|
112
|
-
list: (viewId: number) => Promise<Connector[]>;
|
|
113
|
-
create: (viewId: number, data: {
|
|
114
|
-
source_element_id: number;
|
|
115
|
-
target_element_id: number;
|
|
116
|
-
label?: string;
|
|
117
|
-
description?: string;
|
|
118
|
-
relationship?: string;
|
|
119
|
-
direction?: string;
|
|
120
|
-
style?: string;
|
|
121
|
-
url?: string;
|
|
122
|
-
source_handle?: string | null;
|
|
123
|
-
target_handle?: string | null;
|
|
124
|
-
}) => Promise<Connector>;
|
|
125
|
-
update: (viewId: number, connectorId: number, data: Partial<Connector>) => Promise<Connector>;
|
|
126
|
-
delete: (_orgId: string, connectorId: number) => Promise<void>;
|
|
127
|
-
};
|
|
128
|
-
elements: {
|
|
129
|
-
list: (params?: unknown) => Promise<LibraryElement[]>;
|
|
130
|
-
get: (id: number) => Promise<LibraryElement>;
|
|
131
|
-
create: (data: Partial<LibraryElement>) => Promise<LibraryElement>;
|
|
132
|
-
update: (id: number, data: Partial<LibraryElement>) => Promise<LibraryElement>;
|
|
133
|
-
delete: (orgId: string, id: number) => Promise<void>;
|
|
134
|
-
placements: (id: number) => Promise<ElementPlacement[]>;
|
|
135
|
-
};
|
|
136
|
-
};
|
|
137
|
-
};
|
package/src/demo/DemoPage.tsx
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Demo entry point.
|
|
3
|
-
* Overrides the real `api` singleton with localStorage-backed implementations
|
|
4
|
-
* and patches window.history to redirect /views/:id → /demo/:id, all for the
|
|
5
|
-
* lifetime of this component. Restores everything on unmount.
|
|
6
|
-
* No auth required; served at /demo and /demo/:id routes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import React, { useEffect, useState } from 'react'
|
|
10
|
-
import { useNavigate } from 'react-router-dom'
|
|
11
|
-
import { Box } from '@chakra-ui/react'
|
|
12
|
-
import { api } from '../api/client'
|
|
13
|
-
import ViewEditor from '../pages/ViewEditor'
|
|
14
|
-
import { HeaderProvider } from '../components/HeaderContext'
|
|
15
|
-
import { ThemeProvider } from '../context/ThemeContext'
|
|
16
|
-
import { demoApi, initDemoStore } from './store'
|
|
17
|
-
import { DEMO_VIEW_EDITOR_OPTIONS } from './viewEditor'
|
|
18
|
-
|
|
19
|
-
// ── Override helpers ──────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
type ApiOverride = Record<string, unknown>
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Swap api methods + patch window.history so any /views/:id navigation is
|
|
25
|
-
* silently rewritten to /demo/:id before React Router processes it.
|
|
26
|
-
* Returns a function that restores everything.
|
|
27
|
-
*/
|
|
28
|
-
function applyOverrides(): () => void {
|
|
29
|
-
const originals: [obj: ApiOverride, key: string, original: unknown][] = []
|
|
30
|
-
|
|
31
|
-
function swap(obj: ApiOverride, key: string, replacement: unknown) {
|
|
32
|
-
originals.push([obj, key, obj[key]])
|
|
33
|
-
obj[key] = replacement
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// elements
|
|
37
|
-
const realElements = api.elements as unknown as ApiOverride
|
|
38
|
-
const demoElements = demoApi.elements as ApiOverride
|
|
39
|
-
for (const key of ['list', 'get', 'create', 'update', 'delete', 'placements']) {
|
|
40
|
-
swap(realElements, key, demoElements[key])
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// workspace.elements
|
|
44
|
-
const realWsElements = (api.workspace as unknown as ApiOverride).elements as ApiOverride
|
|
45
|
-
const demoWsElements = demoApi.workspace.elements as ApiOverride
|
|
46
|
-
for (const key of ['list', 'get', 'create', 'update', 'delete', 'placements']) {
|
|
47
|
-
swap(realWsElements, key, demoWsElements[key])
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// workspace.orgs.tagColors
|
|
51
|
-
const realTagColors = ((api.workspace as unknown as ApiOverride).orgs as ApiOverride).tagColors as ApiOverride
|
|
52
|
-
const demoTagColors = demoApi.workspace.orgs.tagColors as ApiOverride
|
|
53
|
-
for (const key of ['list', 'set']) {
|
|
54
|
-
swap(realTagColors, key, demoTagColors[key])
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// workspace.views top-level methods + nested namespaces
|
|
58
|
-
const realViews = (api.workspace as unknown as ApiOverride).views as ApiOverride
|
|
59
|
-
const demoViews = demoApi.workspace.views as ApiOverride
|
|
60
|
-
for (const key of ['list', 'get', 'content', 'tree', 'create', 'update', 'delete', 'thumbnail', 'rename', 'setLevel', 'reparent']) {
|
|
61
|
-
swap(realViews, key, demoViews[key])
|
|
62
|
-
}
|
|
63
|
-
for (const ns of ['placements', 'layers', 'reactions', 'threads']) {
|
|
64
|
-
const realNs = realViews[ns] as ApiOverride
|
|
65
|
-
const demoNs = demoViews[ns] as ApiOverride
|
|
66
|
-
for (const key of Object.keys(demoNs)) {
|
|
67
|
-
swap(realNs, key, demoNs[key])
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// workspace.connectors
|
|
72
|
-
const realConnectors = (api.workspace as unknown as ApiOverride).connectors as ApiOverride
|
|
73
|
-
const demoConnectors = demoApi.workspace.connectors as ApiOverride
|
|
74
|
-
for (const key of ['list', 'create', 'update', 'delete']) {
|
|
75
|
-
swap(realConnectors, key, demoConnectors[key])
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// explore.load (cross-branch graph snapshot)
|
|
79
|
-
const realExplore = (api as unknown as ApiOverride).explore as ApiOverride
|
|
80
|
-
swap(realExplore, 'load', demoApi.explore.load)
|
|
81
|
-
|
|
82
|
-
// ── Patch window.history so /views/:id → /demo/:id before React Router sees it
|
|
83
|
-
const origPush = window.history.pushState.bind(window.history)
|
|
84
|
-
const origReplace = window.history.replaceState.bind(window.history)
|
|
85
|
-
|
|
86
|
-
function rewriteUrl(url: string | URL | null | undefined): string | URL | null | undefined {
|
|
87
|
-
if (typeof url !== 'string') return url
|
|
88
|
-
return url.replace(/\/views\/(\d+)/, '/demo/$1')
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
window.history.pushState = (state, title, url) => origPush(state, title, rewriteUrl(url))
|
|
92
|
-
window.history.replaceState = (state, title, url) => origReplace(state, title, rewriteUrl(url))
|
|
93
|
-
|
|
94
|
-
return () => {
|
|
95
|
-
for (const [obj, key, original] of originals) {
|
|
96
|
-
obj[key] = original
|
|
97
|
-
}
|
|
98
|
-
window.history.pushState = origPush
|
|
99
|
-
window.history.replaceState = origReplace
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Inner component overrides applied before children mount ─────────────────
|
|
104
|
-
|
|
105
|
-
function DemoApp({
|
|
106
|
-
revealProgress,
|
|
107
|
-
}: {
|
|
108
|
-
revealProgress: number
|
|
109
|
-
}) {
|
|
110
|
-
// useState initializer runs synchronously during the first render of this
|
|
111
|
-
// component instance, before any child component mounts or any hook effect
|
|
112
|
-
// runs. This guarantees api overrides are in place for ViewEditor's first fetch.
|
|
113
|
-
const [restore] = useState<() => void>(() => {
|
|
114
|
-
initDemoStore()
|
|
115
|
-
return applyOverrides()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
useEffect(() => restore, [restore])
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<ViewEditor
|
|
122
|
-
demoOptions={{ ...DEMO_VIEW_EDITOR_OPTIONS, revealProgress }}
|
|
123
|
-
/>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ── DemoNavigator: redirects bare /demo to the first root view ────────────────
|
|
128
|
-
|
|
129
|
-
export function DemoNavigator() {
|
|
130
|
-
const navigate = useNavigate()
|
|
131
|
-
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
// Call demoApi directly no need to apply global overrides here, which
|
|
134
|
-
// would otherwise be cleaned up after DemoApp mounts and clobber its patches.
|
|
135
|
-
initDemoStore()
|
|
136
|
-
demoApi.workspace.views.tree().then((tree) => {
|
|
137
|
-
const root = tree.find((v) => v.parent_view_id === null)
|
|
138
|
-
navigate(root ? `/demo/${root.id}` : '/demo/1', { replace: true })
|
|
139
|
-
}).catch(() => navigate('/demo/1', { replace: true }))
|
|
140
|
-
}, [navigate])
|
|
141
|
-
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── DemoPage ──────────────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
export default function DemoPage() {
|
|
148
|
-
const [revealProgress, setRevealProgress] = useState(() => (window.self === window.top ? 1 : 0))
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
if (window.self === window.top) {
|
|
152
|
-
setRevealProgress(1)
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const handleMessage = (event: MessageEvent) => {
|
|
157
|
-
const data = event.data as { type?: unknown; progress?: unknown } | null
|
|
158
|
-
if (!data || data.type !== 'tldiagram-demo-progress') return
|
|
159
|
-
|
|
160
|
-
const nextProgress = Number(data.progress)
|
|
161
|
-
if (!Number.isFinite(nextProgress)) return
|
|
162
|
-
|
|
163
|
-
setRevealProgress(Math.max(0, Math.min(1, nextProgress)))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
window.addEventListener('message', handleMessage)
|
|
167
|
-
return () => window.removeEventListener('message', handleMessage)
|
|
168
|
-
}, [])
|
|
169
|
-
|
|
170
|
-
return (
|
|
171
|
-
<ThemeProvider
|
|
172
|
-
storagePrefix="diag:demo"
|
|
173
|
-
defaultAccent="#63b3ed"
|
|
174
|
-
defaultBackground="#0d121e"
|
|
175
|
-
defaultElementColor="#2d3748"
|
|
176
|
-
>
|
|
177
|
-
<Box minH="100vh" bg="var(--bg-canvas)" style={{ '--editor-top-offset': '0px' } as React.CSSProperties}>
|
|
178
|
-
<HeaderProvider>
|
|
179
|
-
<DemoApp revealProgress={revealProgress} />
|
|
180
|
-
</HeaderProvider>
|
|
181
|
-
</Box>
|
|
182
|
-
</ThemeProvider>
|
|
183
|
-
)
|
|
184
|
-
}
|