@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
package/src/demo/seed.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { LibraryElement, PlacedElement, Connector, ViewTreeNode, ViewLayer } from '../types'
|
|
2
|
-
|
|
3
|
-
const NOW = new Date().toISOString()
|
|
4
|
-
|
|
5
|
-
export const DEMO_ELEMENTS: LibraryElement[] = [
|
|
6
|
-
{ id: 1, name: 'User', kind: 'person', description: 'End user of the system', technology: null, url: null, logo_url: "https://tldiagram.com/app/icons/azure-users.png", technology_connectors: [], tags: ['external'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
7
|
-
{ id: 2, name: 'Web App', kind: 'service', description: 'React single-page application', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: true, view_label: null, created_at: NOW, updated_at: NOW },
|
|
8
|
-
{ id: 3, name: 'API Gateway', kind: 'service', description: 'REST API gateway', technology: 'Golang', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
9
|
-
{ id: 4, name: 'Auth Service', kind: 'service', description: 'Handles authentication & sessions', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
10
|
-
{ id: 5, name: 'Frontend', kind: 'service', description: 'React frontend bundle', technology: 'React', url: null, logo_url: null, technology_connectors: [], tags: ['frontend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
11
|
-
{ id: 6, name: 'Backend', kind: 'service', description: 'Core business logic API', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
12
|
-
{ id: 7, name: 'PostgreSQL', kind: 'database', description: 'Primary relational database', technology: 'PostgreSQL', url: null, logo_url: null, technology_connectors: [], tags: ['data'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
13
|
-
{ id: 8, name: 'Redis Cache', kind: 'database', description: 'In-memory cache and session store', technology: 'Redis', url: null, logo_url: null, technology_connectors: [], tags: ['data'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
14
|
-
{ id: 9, name: 'CDN', kind: 'service', description: 'Content delivery network', technology: 'Cloudflare', url: null, logo_url: null, technology_connectors: [], tags: ['external', 'infrastructure'], has_view: false, view_label: null, created_at: NOW, updated_at: NOW },
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
export const DEMO_VIEWS: ViewTreeNode[] = [
|
|
18
|
-
{
|
|
19
|
-
id: 1, name: 'System Context', description: 'Top-level system context view', level_label: 'Context', level: 1, depth: 0,
|
|
20
|
-
owner_element_id: null, parent_view_id: null, created_at: NOW, updated_at: NOW, children: [
|
|
21
|
-
{
|
|
22
|
-
id: 2, name: 'Web App – Containers', description: 'Container-level view of the Web App', level_label: 'Container', level: 2, depth: 1,
|
|
23
|
-
owner_element_id: 2, parent_view_id: 1, created_at: NOW, updated_at: NOW, children: [],
|
|
24
|
-
},
|
|
25
|
-
],
|
|
26
|
-
},
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
type ViewPlacements = Record<number, PlacedElement[]>
|
|
30
|
-
type ViewConnectors = Record<number, Connector[]>
|
|
31
|
-
|
|
32
|
-
export const DEMO_PLACEMENTS: ViewPlacements = {
|
|
33
|
-
1: [
|
|
34
|
-
{ id: 101, view_id: 1, element_id: 1, position_x: 80, position_y: 200, name: 'User', kind: 'person', description: 'End user of the system', technology: null, url: null, logo_url: "https://tldiagram.com/app/icons/azure-users.png", technology_connectors: [], tags: ['external'], has_view: false, view_label: null },
|
|
35
|
-
{ id: 102, view_id: 1, element_id: 2, position_x: 380, position_y: 200, name: 'Web App', kind: 'service', description: 'React single-page application', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: true, view_label: null },
|
|
36
|
-
{ id: 103, view_id: 1, element_id: 3, position_x: 680, position_y: 200, name: 'API Gateway', kind: 'service', description: 'REST API gateway', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
|
|
37
|
-
{ id: 104, view_id: 1, element_id: 4, position_x: 380, position_y: 400, name: 'Auth Service', kind: 'service', description: 'Handles authentication & sessions', technology: 'Auth0', url: null, logo_url: "https://tldiagram.com/app/icons/auth0.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
|
|
38
|
-
{ id: 105, view_id: 1, element_id: 9, position_x: 380, position_y: 0, name: 'CDN', kind: 'service', description: 'Content delivery network', technology: 'Cloudflare', url: null, logo_url: "https://tldiagram.com/app/icons/cloudflare.png", technology_connectors: [], tags: ['external', 'infrastructure'], has_view: false, view_label: null },
|
|
39
|
-
{ id: 106, view_id: 1, element_id: 10, position_x: 680, position_y: 0, name: 'Stripe', kind: 'service', description: 'Payment', technology: 'Stripe', url: null, logo_url: "https://tldiagram.com/app/icons/stripe.png", technology_connectors: [], tags: ['external', 'billing'], has_view: false, view_label: null },
|
|
40
|
-
],
|
|
41
|
-
2: [
|
|
42
|
-
{ id: 201, view_id: 2, element_id: 5, position_x: 80, position_y: 200, name: 'Frontend', kind: 'service', description: 'React frontend bundle', technology: 'React', url: null, logo_url: "https://tldiagram.com/app/icons/react.png", technology_connectors: [], tags: ['frontend'], has_view: false, view_label: null },
|
|
43
|
-
{ id: 202, view_id: 2, element_id: 6, position_x: 380, position_y: 200, name: 'Backend', kind: 'service', description: 'Core business logic API', technology: 'Go', url: null, logo_url: "https://tldiagram.com/app/icons/golang.png", technology_connectors: [], tags: ['backend'], has_view: false, view_label: null },
|
|
44
|
-
{ id: 203, view_id: 2, element_id: 7, position_x: 680, position_y: 200, name: 'PostgreSQL', kind: 'database', description: 'Primary relational database', technology: 'PostgreSQL', url: null, logo_url: "https://tldiagram.com/app/icons/postgresql.png", technology_connectors: [], tags: ['data'], has_view: false, view_label: null },
|
|
45
|
-
{ id: 204, view_id: 2, element_id: 8, position_x: 680, position_y: 400, name: 'Redis Cache', kind: 'database', description: 'In-memory cache and session store', technology: 'Redis', url: null, logo_url: "https://tldiagram.com/app/icons/redis.png", technology_connectors: [], tags: ['data'], has_view: false, view_label: null },
|
|
46
|
-
],
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const DEMO_CONNECTORS: ViewConnectors = {
|
|
50
|
-
1: [
|
|
51
|
-
{ id: 1001, view_id: 1, source_element_id: 1, target_element_id: 2, label: 'Uses', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
|
|
52
|
-
{ id: 1002, view_id: 1, source_element_id: 2, target_element_id: 3, label: 'API calls', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
|
|
53
|
-
{ id: 1003, view_id: 1, source_element_id: 2, target_element_id: 4, label: 'Auth', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'bottom', target_handle: 'top', created_at: NOW, updated_at: NOW },
|
|
54
|
-
{ id: 1004, view_id: 1, source_element_id: 2, target_element_id: 9, label: 'Serves via', description: null, relationship: null, direction: 'backward', style: 'bezier', url: null, source_handle: 'top', target_handle: 'bottom', created_at: NOW, updated_at: NOW },
|
|
55
|
-
{ id: 1005, view_id: 1, source_element_id: 3, target_element_id: 10, label: 'Billing', description: null, relationship: null, direction: 'both', style: 'bezier', url: null, source_handle: 'top', target_handle: 'bottom', created_at: NOW, updated_at: NOW },
|
|
56
|
-
],
|
|
57
|
-
2: [
|
|
58
|
-
{ id: 2001, view_id: 2, source_element_id: 5, target_element_id: 6, label: 'HTTP/JSON', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
|
|
59
|
-
{ id: 2002, view_id: 2, source_element_id: 6, target_element_id: 7, label: 'Reads / Writes', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'right', target_handle: 'left', created_at: NOW, updated_at: NOW },
|
|
60
|
-
{ id: 2003, view_id: 2, source_element_id: 6, target_element_id: 8, label: 'Cache', description: null, relationship: null, direction: 'forward', style: 'bezier', url: null, source_handle: 'bottom', target_handle: 'left', created_at: NOW, updated_at: NOW },
|
|
61
|
-
],
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const DEMO_LAYERS: Record<number, ViewLayer[]> = {
|
|
65
|
-
1: [],
|
|
66
|
-
2: [],
|
|
67
|
-
}
|
package/src/demo/store.ts
DELETED
|
@@ -1,536 +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
|
-
|
|
8
|
-
import type {
|
|
9
|
-
LibraryElement,
|
|
10
|
-
PlacedElement,
|
|
11
|
-
Connector,
|
|
12
|
-
ViewTreeNode,
|
|
13
|
-
ViewLayer,
|
|
14
|
-
Tag,
|
|
15
|
-
ElementPlacement,
|
|
16
|
-
ExploreData,
|
|
17
|
-
} from '../types'
|
|
18
|
-
import {
|
|
19
|
-
DEMO_ELEMENTS,
|
|
20
|
-
DEMO_VIEWS,
|
|
21
|
-
DEMO_PLACEMENTS,
|
|
22
|
-
DEMO_CONNECTORS,
|
|
23
|
-
DEMO_LAYERS,
|
|
24
|
-
} from './seed'
|
|
25
|
-
|
|
26
|
-
// ── Keys ──────────────────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const K = {
|
|
29
|
-
elements: 'diag:demo:elements',
|
|
30
|
-
views: 'diag:demo:views',
|
|
31
|
-
placements: (viewId: number) => `diag:demo:placements:${viewId}`,
|
|
32
|
-
connectors: (viewId: number) => `diag:demo:connectors:${viewId}`,
|
|
33
|
-
layers: (viewId: number) => `diag:demo:layers:${viewId}`,
|
|
34
|
-
tagColors: 'diag:demo:tagColors',
|
|
35
|
-
nextId: 'diag:demo:nextId',
|
|
36
|
-
} as const
|
|
37
|
-
|
|
38
|
-
// ── ID generation ──────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
function nextId(): number {
|
|
41
|
-
const current = Number(localStorage.getItem(K.nextId) ?? '9000')
|
|
42
|
-
const next = current + 1
|
|
43
|
-
localStorage.setItem(K.nextId, String(next))
|
|
44
|
-
return next
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ── Generic persistence helpers ────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
function load<T>(key: string, fallback: T): T {
|
|
50
|
-
try {
|
|
51
|
-
const raw = localStorage.getItem(key)
|
|
52
|
-
if (raw === null) return fallback
|
|
53
|
-
return JSON.parse(raw) as T
|
|
54
|
-
} catch {
|
|
55
|
-
return fallback
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function save(key: string, value: unknown): void {
|
|
60
|
-
localStorage.setItem(key, JSON.stringify(value))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── Initialisation ─────────────────────────────────────────────────────────────
|
|
64
|
-
// Seed data is written once on first visit (when the store key is absent).
|
|
65
|
-
|
|
66
|
-
export function initDemoStore(): void {
|
|
67
|
-
if (localStorage.getItem(K.elements) === null) {
|
|
68
|
-
save(K.elements, DEMO_ELEMENTS)
|
|
69
|
-
}
|
|
70
|
-
if (localStorage.getItem(K.views) === null) {
|
|
71
|
-
save(K.views, DEMO_VIEWS)
|
|
72
|
-
}
|
|
73
|
-
for (const [rawId, placements] of Object.entries(DEMO_PLACEMENTS)) {
|
|
74
|
-
const id = Number(rawId)
|
|
75
|
-
if (localStorage.getItem(K.placements(id)) === null) {
|
|
76
|
-
save(K.placements(id), placements)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
for (const [rawId, connectors] of Object.entries(DEMO_CONNECTORS)) {
|
|
80
|
-
const id = Number(rawId)
|
|
81
|
-
if (localStorage.getItem(K.connectors(id)) === null) {
|
|
82
|
-
save(K.connectors(id), connectors)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
for (const [rawId, layers] of Object.entries(DEMO_LAYERS)) {
|
|
86
|
-
const id = Number(rawId)
|
|
87
|
-
if (localStorage.getItem(K.layers(id)) === null) {
|
|
88
|
-
save(K.layers(id), layers)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function resetDemoStore(): void {
|
|
94
|
-
localStorage.removeItem(K.elements)
|
|
95
|
-
localStorage.removeItem(K.views)
|
|
96
|
-
localStorage.removeItem(K.tagColors)
|
|
97
|
-
localStorage.removeItem(K.nextId)
|
|
98
|
-
localStorage.removeItem('diag:demo:accent-color')
|
|
99
|
-
localStorage.removeItem('diag:demo:background-color')
|
|
100
|
-
localStorage.removeItem('diag:demo:element-color')
|
|
101
|
-
for (const viewId of getAllViewIds()) {
|
|
102
|
-
localStorage.removeItem(K.placements(viewId))
|
|
103
|
-
localStorage.removeItem(K.connectors(viewId))
|
|
104
|
-
localStorage.removeItem(K.layers(viewId))
|
|
105
|
-
}
|
|
106
|
-
initDemoStore()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Tree helpers ──────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
function getAllViews(): ViewTreeNode[] {
|
|
112
|
-
return load<ViewTreeNode[]>(K.views, DEMO_VIEWS)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function flattenTree(nodes: ViewTreeNode[]): ViewTreeNode[] {
|
|
116
|
-
return nodes.flatMap((n) => [n, ...flattenTree(n.children ?? [])])
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getAllViewIds(): number[] {
|
|
120
|
-
return flattenTree(getAllViews()).map((v) => v.id)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function findViewById(id: number): ViewTreeNode | null {
|
|
124
|
-
return flattenTree(getAllViews()).find((v) => v.id === id) ?? null
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function saveViews(roots: ViewTreeNode[]): void {
|
|
128
|
-
save(K.views, roots)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function insertViewIntoTree(roots: ViewTreeNode[], newView: ViewTreeNode): ViewTreeNode[] {
|
|
132
|
-
if (newView.parent_view_id === null) return [...roots, newView]
|
|
133
|
-
return roots.map((n) => {
|
|
134
|
-
if (n.id === newView.parent_view_id) return { ...n, children: [...(n.children ?? []), newView] }
|
|
135
|
-
return { ...n, children: insertViewIntoTree(n.children ?? [], newView) }
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function deleteViewFromTree(roots: ViewTreeNode[], id: number): ViewTreeNode[] {
|
|
140
|
-
return roots
|
|
141
|
-
.filter((n) => n.id !== id)
|
|
142
|
-
.map((n) => ({ ...n, children: deleteViewFromTree(n.children ?? [], id) }))
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ── api surface ───────────────────────────────────────────────────────────────
|
|
146
|
-
|
|
147
|
-
const NOW = () => new Date().toISOString()
|
|
148
|
-
|
|
149
|
-
export const demoApi = {
|
|
150
|
-
explore: {
|
|
151
|
-
load: async (): Promise<ExploreData> => {
|
|
152
|
-
const tree = getAllViews()
|
|
153
|
-
const flat = flattenTree(tree)
|
|
154
|
-
const views: ExploreData['views'] = {}
|
|
155
|
-
for (const v of flat) {
|
|
156
|
-
views[v.id] = {
|
|
157
|
-
placements: load<PlacedElement[]>(K.placements(v.id), []),
|
|
158
|
-
connectors: load<Connector[]>(K.connectors(v.id), []),
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return { tree, views, navigations: [] }
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
elements: {
|
|
166
|
-
list: async (_params?: unknown): Promise<LibraryElement[]> => {
|
|
167
|
-
return load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
get: async (id: number): Promise<LibraryElement> => {
|
|
171
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
172
|
-
const el = elements.find((e) => e.id === id)
|
|
173
|
-
if (!el) throw new Error(`Element ${id} not found`)
|
|
174
|
-
return el
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
create: async (data: Partial<LibraryElement>): Promise<LibraryElement> => {
|
|
178
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
179
|
-
const newEl: LibraryElement = {
|
|
180
|
-
id: nextId(),
|
|
181
|
-
name: data.name ?? 'New Element',
|
|
182
|
-
kind: data.kind ?? null,
|
|
183
|
-
description: data.description ?? null,
|
|
184
|
-
technology: data.technology ?? null,
|
|
185
|
-
url: data.url ?? null,
|
|
186
|
-
logo_url: data.logo_url ?? null,
|
|
187
|
-
technology_connectors: data.technology_connectors ?? [],
|
|
188
|
-
tags: data.tags ?? [],
|
|
189
|
-
repo: data.repo ?? null,
|
|
190
|
-
branch: data.branch ?? null,
|
|
191
|
-
file_path: data.file_path ?? null,
|
|
192
|
-
language: data.language ?? null,
|
|
193
|
-
created_at: NOW(),
|
|
194
|
-
updated_at: NOW(),
|
|
195
|
-
has_view: false,
|
|
196
|
-
view_label: null,
|
|
197
|
-
}
|
|
198
|
-
save(K.elements, [...elements, newEl])
|
|
199
|
-
return newEl
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
update: async (id: number, data: Partial<LibraryElement>): Promise<LibraryElement> => {
|
|
203
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
204
|
-
const idx = elements.findIndex((e) => e.id === id)
|
|
205
|
-
if (idx === -1) throw new Error(`Element ${id} not found`)
|
|
206
|
-
const updated = { ...elements[idx], ...data, id, updated_at: NOW() }
|
|
207
|
-
const next = [...elements]
|
|
208
|
-
next[idx] = updated
|
|
209
|
-
save(K.elements, next)
|
|
210
|
-
// Patch all view placements that reference this element
|
|
211
|
-
for (const viewId of getAllViewIds()) {
|
|
212
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
213
|
-
const changed = placements.map((p) =>
|
|
214
|
-
p.element_id === id
|
|
215
|
-
? { ...p, name: updated.name, kind: updated.kind, description: updated.description, technology: updated.technology, tags: updated.tags }
|
|
216
|
-
: p,
|
|
217
|
-
)
|
|
218
|
-
save(K.placements(viewId), changed)
|
|
219
|
-
}
|
|
220
|
-
return updated
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
delete: async (_orgId: string, id: number): Promise<void> => {
|
|
224
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
225
|
-
save(K.elements, elements.filter((e) => e.id !== id))
|
|
226
|
-
for (const viewId of getAllViewIds()) {
|
|
227
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
228
|
-
save(K.placements(viewId), placements.filter((p) => p.element_id !== id))
|
|
229
|
-
const connectors = load<Connector[]>(K.connectors(viewId), [])
|
|
230
|
-
save(K.connectors(viewId), connectors.filter((c) => c.source_element_id !== id && c.target_element_id !== id))
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
placements: async (id: number): Promise<ElementPlacement[]> => {
|
|
235
|
-
const result: ElementPlacement[] = []
|
|
236
|
-
for (const viewId of getAllViewIds()) {
|
|
237
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
238
|
-
const match = placements.find((p) => p.element_id === id)
|
|
239
|
-
if (match) result.push({ id: match.id, view_id: viewId, element_id: id, position_x: match.position_x, position_y: match.position_y })
|
|
240
|
-
}
|
|
241
|
-
return result
|
|
242
|
-
},
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
workspace: {
|
|
246
|
-
orgs: {
|
|
247
|
-
tagColors: {
|
|
248
|
-
list: async (): Promise<Record<string, Tag>> => {
|
|
249
|
-
return load<Record<string, Tag>>(K.tagColors, {})
|
|
250
|
-
},
|
|
251
|
-
set: async (tag: string, color: string, description?: string): Promise<void> => {
|
|
252
|
-
const tags = load<Record<string, Tag>>(K.tagColors, {})
|
|
253
|
-
save(K.tagColors, { ...tags, [tag]: { name: tag, color, description: description ?? null } })
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
|
|
258
|
-
views: {
|
|
259
|
-
list: async () => {
|
|
260
|
-
return flattenTree(getAllViews()).map((v) => ({
|
|
261
|
-
id: v.id,
|
|
262
|
-
owner_element_id: v.owner_element_id ?? null,
|
|
263
|
-
name: v.name,
|
|
264
|
-
label: v.level_label,
|
|
265
|
-
is_root: v.parent_view_id === null,
|
|
266
|
-
created_at: v.created_at,
|
|
267
|
-
updated_at: v.updated_at,
|
|
268
|
-
}))
|
|
269
|
-
},
|
|
270
|
-
|
|
271
|
-
get: async (id: number): Promise<ViewTreeNode> => {
|
|
272
|
-
const v = findViewById(id)
|
|
273
|
-
if (!v) throw new Error(`View ${id} not found`)
|
|
274
|
-
return v
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
content: async (id: number): Promise<{ placements: PlacedElement[]; connectors: Connector[] }> => {
|
|
278
|
-
return {
|
|
279
|
-
placements: load<PlacedElement[]>(K.placements(id), []),
|
|
280
|
-
connectors: load<Connector[]>(K.connectors(id), []),
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
tree: async (): Promise<ViewTreeNode[]> => {
|
|
285
|
-
return getAllViews()
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
create: async (data: { name: string; label?: string; parent_view_id?: number | null }) => {
|
|
289
|
-
const id = nextId()
|
|
290
|
-
const now = NOW()
|
|
291
|
-
const newView: ViewTreeNode = {
|
|
292
|
-
id,
|
|
293
|
-
name: data.name,
|
|
294
|
-
description: null,
|
|
295
|
-
level_label: data.label ?? null,
|
|
296
|
-
level: 0,
|
|
297
|
-
depth: 0,
|
|
298
|
-
owner_element_id: data.parent_view_id ?? null,
|
|
299
|
-
parent_view_id: data.parent_view_id ?? null,
|
|
300
|
-
created_at: now,
|
|
301
|
-
updated_at: now,
|
|
302
|
-
children: [],
|
|
303
|
-
}
|
|
304
|
-
const roots = getAllViews()
|
|
305
|
-
saveViews(insertViewIntoTree(roots, newView))
|
|
306
|
-
save(K.placements(id), [])
|
|
307
|
-
save(K.connectors(id), [])
|
|
308
|
-
save(K.layers(id), [])
|
|
309
|
-
|
|
310
|
-
// Mark the owning element as having a view
|
|
311
|
-
if (data.parent_view_id != null) {
|
|
312
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
313
|
-
const idx = elements.findIndex((e) => e.id === data.parent_view_id)
|
|
314
|
-
if (idx !== -1) {
|
|
315
|
-
const next = [...elements]
|
|
316
|
-
next[idx] = { ...next[idx], has_view: true }
|
|
317
|
-
save(K.elements, next)
|
|
318
|
-
}
|
|
319
|
-
// Patch all placements of that element to reflect has_view
|
|
320
|
-
for (const viewId of getAllViewIds()) {
|
|
321
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
322
|
-
const changed = placements.map((p) =>
|
|
323
|
-
p.element_id === data.parent_view_id ? { ...p, has_view: true } : p,
|
|
324
|
-
)
|
|
325
|
-
save(K.placements(viewId), changed)
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
id,
|
|
331
|
-
owner_element_id: data.parent_view_id ?? null,
|
|
332
|
-
name: data.name,
|
|
333
|
-
label: data.label ?? null,
|
|
334
|
-
is_root: data.parent_view_id === null,
|
|
335
|
-
created_at: now,
|
|
336
|
-
updated_at: now,
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
|
|
340
|
-
update: async (id: number, data: { name: string; label?: string }) => {
|
|
341
|
-
const roots = getAllViews()
|
|
342
|
-
const patchInTree = (nodes: ViewTreeNode[]): ViewTreeNode[] =>
|
|
343
|
-
nodes.map((n) =>
|
|
344
|
-
n.id === id
|
|
345
|
-
? { ...n, name: data.name, level_label: data.label ?? n.level_label, updated_at: NOW() }
|
|
346
|
-
: { ...n, children: patchInTree(n.children ?? []) },
|
|
347
|
-
)
|
|
348
|
-
saveViews(patchInTree(roots))
|
|
349
|
-
const v = findViewById(id)
|
|
350
|
-
return {
|
|
351
|
-
id,
|
|
352
|
-
owner_element_id: v?.owner_element_id ?? null,
|
|
353
|
-
name: data.name,
|
|
354
|
-
label: data.label ?? null,
|
|
355
|
-
is_root: v?.parent_view_id === null,
|
|
356
|
-
created_at: v?.created_at ?? NOW(),
|
|
357
|
-
updated_at: NOW(),
|
|
358
|
-
}
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
delete: async (_orgId: string, id: number): Promise<void> => {
|
|
362
|
-
const roots = getAllViews()
|
|
363
|
-
saveViews(deleteViewFromTree(roots, id))
|
|
364
|
-
localStorage.removeItem(K.placements(id))
|
|
365
|
-
localStorage.removeItem(K.connectors(id))
|
|
366
|
-
localStorage.removeItem(K.layers(id))
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
placements: {
|
|
370
|
-
list: async (viewId: number): Promise<ElementPlacement[]> => {
|
|
371
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
372
|
-
return placements.map((p) => ({ id: p.id, view_id: viewId, element_id: p.element_id, position_x: p.position_x, position_y: p.position_y }))
|
|
373
|
-
},
|
|
374
|
-
|
|
375
|
-
add: async (viewId: number, elementId: number, x = 100, y = 100): Promise<ElementPlacement> => {
|
|
376
|
-
const elements = load<LibraryElement[]>(K.elements, DEMO_ELEMENTS)
|
|
377
|
-
const el = elements.find((e) => e.id === elementId)
|
|
378
|
-
if (!el) throw new Error(`Element ${elementId} not found`)
|
|
379
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
380
|
-
const id = nextId()
|
|
381
|
-
const newPlacement: PlacedElement = {
|
|
382
|
-
id,
|
|
383
|
-
view_id: viewId,
|
|
384
|
-
element_id: elementId,
|
|
385
|
-
position_x: x,
|
|
386
|
-
position_y: y,
|
|
387
|
-
name: el.name,
|
|
388
|
-
kind: el.kind,
|
|
389
|
-
description: el.description,
|
|
390
|
-
technology: el.technology,
|
|
391
|
-
url: el.url,
|
|
392
|
-
logo_url: el.logo_url,
|
|
393
|
-
technology_connectors: el.technology_connectors,
|
|
394
|
-
tags: el.tags,
|
|
395
|
-
repo: el.repo,
|
|
396
|
-
branch: el.branch,
|
|
397
|
-
file_path: el.file_path,
|
|
398
|
-
language: el.language,
|
|
399
|
-
has_view: el.has_view,
|
|
400
|
-
view_label: el.view_label,
|
|
401
|
-
}
|
|
402
|
-
save(K.placements(viewId), [...placements, newPlacement])
|
|
403
|
-
return { id, view_id: viewId, element_id: elementId, position_x: x, position_y: y }
|
|
404
|
-
},
|
|
405
|
-
|
|
406
|
-
updatePosition: async (viewId: number, elementId: number, x: number, y: number): Promise<void> => {
|
|
407
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
408
|
-
save(K.placements(viewId), placements.map((p) => p.element_id === elementId ? { ...p, position_x: x, position_y: y } : p))
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
remove: async (viewId: number, elementId: number): Promise<void> => {
|
|
412
|
-
const placements = load<PlacedElement[]>(K.placements(viewId), [])
|
|
413
|
-
save(K.placements(viewId), placements.filter((p) => p.element_id !== elementId))
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
layers: {
|
|
418
|
-
list: async (viewId: number): Promise<ViewLayer[]> => {
|
|
419
|
-
return load<ViewLayer[]>(K.layers(viewId), [])
|
|
420
|
-
},
|
|
421
|
-
create: async (viewId: number, data: { name: string; tags: string[]; color?: string }): Promise<ViewLayer> => {
|
|
422
|
-
const layers = load<ViewLayer[]>(K.layers(viewId), [])
|
|
423
|
-
const id = nextId()
|
|
424
|
-
const newLayer: ViewLayer = { id, diagram_id: viewId, name: data.name, tags: data.tags, color: data.color ?? '#888888', created_at: NOW(), updated_at: NOW() }
|
|
425
|
-
save(K.layers(viewId), [...layers, newLayer])
|
|
426
|
-
return newLayer
|
|
427
|
-
},
|
|
428
|
-
update: async (viewId: number, layerId: number, data: Partial<ViewLayer>): Promise<ViewLayer> => {
|
|
429
|
-
const layers = load<ViewLayer[]>(K.layers(viewId), [])
|
|
430
|
-
const idx = layers.findIndex((l) => l.id === layerId)
|
|
431
|
-
if (idx === -1) throw new Error(`Layer ${layerId} not found`)
|
|
432
|
-
const updated = { ...layers[idx], ...data, id: layerId, updated_at: NOW() }
|
|
433
|
-
const next = [...layers]
|
|
434
|
-
next[idx] = updated
|
|
435
|
-
save(K.layers(viewId), next)
|
|
436
|
-
return updated
|
|
437
|
-
},
|
|
438
|
-
delete: async (viewId: number, layerId: number): Promise<void> => {
|
|
439
|
-
const layers = load<ViewLayer[]>(K.layers(viewId), [])
|
|
440
|
-
save(K.layers(viewId), layers.filter((l) => l.id !== layerId))
|
|
441
|
-
},
|
|
442
|
-
},
|
|
443
|
-
|
|
444
|
-
reactions: {
|
|
445
|
-
list: async (_viewId: number) => [],
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
threads: {
|
|
449
|
-
listForElement: async () => [],
|
|
450
|
-
listForConnector: async () => [],
|
|
451
|
-
createForElement: async () => { throw new Error('Demo: threads not supported') },
|
|
452
|
-
createForConnector: async () => { throw new Error('Demo: threads not supported') },
|
|
453
|
-
addComment: async () => { throw new Error('Demo: comments not supported') },
|
|
454
|
-
resolve: async () => { /* no-op */ },
|
|
455
|
-
},
|
|
456
|
-
|
|
457
|
-
thumbnail: (_id: number) => Promise.resolve(null),
|
|
458
|
-
rename: async (id: number, name: string) => demoApi.workspace.views.update(id, { name }),
|
|
459
|
-
setLevel: async () => { /* no-op */ },
|
|
460
|
-
reparent: async () => { throw new Error('Demo: reparent not supported') },
|
|
461
|
-
},
|
|
462
|
-
|
|
463
|
-
connectors: {
|
|
464
|
-
list: async (viewId: number): Promise<Connector[]> => {
|
|
465
|
-
return load<Connector[]>(K.connectors(viewId), [])
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
create: async (
|
|
469
|
-
viewId: number,
|
|
470
|
-
data: {
|
|
471
|
-
source_element_id: number; target_element_id: number
|
|
472
|
-
label?: string; description?: string; relationship?: string
|
|
473
|
-
direction?: string; style?: string; url?: string
|
|
474
|
-
source_handle?: string | null; target_handle?: string | null
|
|
475
|
-
},
|
|
476
|
-
): Promise<Connector> => {
|
|
477
|
-
const connectors = load<Connector[]>(K.connectors(viewId), [])
|
|
478
|
-
const id = nextId()
|
|
479
|
-
const now = NOW()
|
|
480
|
-
const newConnector: Connector = {
|
|
481
|
-
id,
|
|
482
|
-
view_id: viewId,
|
|
483
|
-
source_element_id: data.source_element_id,
|
|
484
|
-
target_element_id: data.target_element_id,
|
|
485
|
-
label: data.label ?? null,
|
|
486
|
-
description: data.description ?? null,
|
|
487
|
-
relationship: data.relationship ?? null,
|
|
488
|
-
direction: data.direction ?? 'forward',
|
|
489
|
-
style: data.style ?? 'bezier',
|
|
490
|
-
url: data.url ?? null,
|
|
491
|
-
source_handle: data.source_handle ?? null,
|
|
492
|
-
target_handle: data.target_handle ?? null,
|
|
493
|
-
created_at: now,
|
|
494
|
-
updated_at: now,
|
|
495
|
-
}
|
|
496
|
-
save(K.connectors(viewId), [...connectors, newConnector])
|
|
497
|
-
return newConnector
|
|
498
|
-
},
|
|
499
|
-
|
|
500
|
-
update: async (
|
|
501
|
-
viewId: number,
|
|
502
|
-
connectorId: number,
|
|
503
|
-
data: Partial<Connector>,
|
|
504
|
-
): Promise<Connector> => {
|
|
505
|
-
const connectors = load<Connector[]>(K.connectors(viewId), [])
|
|
506
|
-
const idx = connectors.findIndex((c) => c.id === connectorId)
|
|
507
|
-
if (idx === -1) throw new Error(`Connector ${connectorId} not found`)
|
|
508
|
-
const updated = { ...connectors[idx], ...data, id: connectorId, updated_at: NOW() }
|
|
509
|
-
const next = [...connectors]
|
|
510
|
-
next[idx] = updated
|
|
511
|
-
save(K.connectors(viewId), next)
|
|
512
|
-
return updated
|
|
513
|
-
},
|
|
514
|
-
|
|
515
|
-
delete: async (_orgId: string, connectorId: number): Promise<void> => {
|
|
516
|
-
for (const viewId of getAllViewIds()) {
|
|
517
|
-
const connectors = load<Connector[]>(K.connectors(viewId), [])
|
|
518
|
-
const filtered = connectors.filter((c) => c.id !== connectorId)
|
|
519
|
-
if (filtered.length !== connectors.length) {
|
|
520
|
-
save(K.connectors(viewId), filtered)
|
|
521
|
-
break
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
|
|
527
|
-
elements: {
|
|
528
|
-
list: (params?: unknown) => demoApi.elements.list(params),
|
|
529
|
-
get: (id: number) => demoApi.elements.get(id),
|
|
530
|
-
create: (data: Partial<LibraryElement>) => demoApi.elements.create(data),
|
|
531
|
-
update: (id: number, data: Partial<LibraryElement>) => demoApi.elements.update(id, data),
|
|
532
|
-
delete: (orgId: string, id: number) => demoApi.elements.delete(orgId, id),
|
|
533
|
-
placements: (id: number) => demoApi.elements.placements(id),
|
|
534
|
-
},
|
|
535
|
-
},
|
|
536
|
-
}
|