@tldiagram/core-ui 1.95.1 → 2.0.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.
Files changed (100) hide show
  1. package/dist/api/client.d.ts +184 -3
  2. package/dist/components/ConnectorPanel.d.ts +5 -1
  3. package/dist/components/CrossBranchControls.d.ts +4 -3
  4. package/dist/components/ElementNode.d.ts +5 -0
  5. package/dist/components/ElementPanel.d.ts +6 -1
  6. package/dist/components/LayoutSection.d.ts +2 -1
  7. package/dist/components/MergeDialog.d.ts +16 -0
  8. package/dist/components/NodeContainer.d.ts +2 -0
  9. package/dist/components/ProxyConnectorPanel.d.ts +4 -1
  10. package/dist/components/ViewExplorer/index.d.ts +1 -1
  11. package/dist/components/ViewFloatingMenu-vscode.d.ts +5 -0
  12. package/dist/components/ViewFloatingMenu.d.ts +8 -1
  13. package/dist/components/ViewGridNode.d.ts +3 -0
  14. package/dist/components/ViewPanel.d.ts +2 -1
  15. package/dist/components/WorkspacePanel.d.ts +2 -0
  16. package/dist/components/ZUI/ZUICanvas.d.ts +4 -0
  17. package/dist/components/ZUI/focus.d.ts +32 -0
  18. package/dist/components/ZUI/focus.test.d.ts +1 -0
  19. package/dist/components/ZUI/layout.d.ts +2 -2
  20. package/dist/components/ZUI/proxy.d.ts +20 -4
  21. package/dist/components/ZUI/renderer.d.ts +35 -1
  22. package/dist/components/ZUI/types.d.ts +6 -0
  23. package/dist/components/ZUI/useZUIInteraction.d.ts +1 -0
  24. package/dist/context/WorkspaceVersionContext.d.ts +49 -0
  25. package/dist/crossBranch/resolve.d.ts +39 -2
  26. package/dist/crossBranch/resolve.test.d.ts +1 -0
  27. package/dist/crossBranch/settings.d.ts +6 -1
  28. package/dist/crossBranch/types.d.ts +8 -0
  29. package/dist/hooks/useElementSearch.d.ts +8 -0
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +16529 -14030
  32. package/dist/pages/InfiniteZoom.d.ts +1 -0
  33. package/dist/pages/ViewEditor/hooks/useCanvasInteractions.d.ts +6 -1
  34. package/dist/pages/ViewEditor/hooks/useViewContextNeighbours.d.ts +2 -0
  35. package/dist/pages/ViewEditor/hooks/useViewData.d.ts +4 -2
  36. package/dist/pages/ViewEditor/hooks/useViewEditHistory.d.ts +13 -0
  37. package/dist/pages/viewsJumpSearch.d.ts +22 -0
  38. package/dist/pages/viewsJumpSearch.test.d.ts +1 -0
  39. package/dist/store/useStore.d.ts +3 -0
  40. package/dist/types/index.d.ts +9 -0
  41. package/dist/utils/elementIcon.d.ts +2 -0
  42. package/dist/utils/elementIcon.test.d.ts +1 -0
  43. package/dist/utils/sourceEditor.d.ts +7 -0
  44. package/dist/utils/watchDiffSummary.d.ts +34 -0
  45. package/package.json +2 -2
  46. package/src/App.tsx +12 -8
  47. package/src/api/client.ts +488 -26
  48. package/src/components/CodePreviewPanel.tsx +90 -16
  49. package/src/components/ConnectorPanel.tsx +34 -3
  50. package/src/components/ContextNeighborElement.tsx +2 -5
  51. package/src/components/CrossBranchControls.tsx +46 -17
  52. package/src/components/ElementNode.tsx +98 -47
  53. package/src/components/ElementPanel.tsx +62 -25
  54. package/src/components/InlineElementAdder.tsx +8 -3
  55. package/src/components/LayoutSection.tsx +4 -1
  56. package/src/components/MergeDialog.tsx +269 -0
  57. package/src/components/NodeContainer.tsx +55 -17
  58. package/src/components/ProxyConnectorPanel.tsx +58 -16
  59. package/src/components/ViewBezierConnector.tsx +116 -21
  60. package/src/components/ViewExplorer/index.tsx +1 -1
  61. package/src/components/ViewFloatingMenu-vscode.tsx +5 -0
  62. package/src/components/ViewFloatingMenu.tsx +110 -1
  63. package/src/components/ViewGridNode.tsx +59 -8
  64. package/src/components/ViewPanel.tsx +3 -2
  65. package/src/components/WorkspacePanel.tsx +938 -0
  66. package/src/components/ZUI/ZUICanvas.tsx +216 -122
  67. package/src/components/ZUI/focus.test.ts +534 -0
  68. package/src/components/ZUI/focus.ts +293 -0
  69. package/src/components/ZUI/layout.ts +7 -11
  70. package/src/components/ZUI/proxy.ts +470 -114
  71. package/src/components/ZUI/renderer.ts +510 -134
  72. package/src/components/ZUI/types.ts +6 -0
  73. package/src/components/ZUI/useZUIInteraction.ts +66 -29
  74. package/src/context/WorkspaceVersionContext.tsx +126 -0
  75. package/src/crossBranch/resolve.test.ts +342 -0
  76. package/src/crossBranch/resolve.ts +368 -68
  77. package/src/crossBranch/settings.ts +49 -3
  78. package/src/crossBranch/types.ts +9 -0
  79. package/src/hooks/useElementSearch.ts +45 -0
  80. package/src/index.css +11 -0
  81. package/src/index.ts +7 -0
  82. package/src/pages/AppearanceSettings.tsx +24 -1
  83. package/src/pages/Dependencies.tsx +231 -65
  84. package/src/pages/InfiniteZoom.tsx +41 -19
  85. package/src/pages/Settings.tsx +1 -1
  86. package/src/pages/ViewEditor/hooks/useCanvasInteractions.ts +103 -24
  87. package/src/pages/ViewEditor/hooks/useViewContextNeighbours.ts +102 -6
  88. package/src/pages/ViewEditor/hooks/useViewData.ts +42 -26
  89. package/src/pages/ViewEditor/hooks/useViewEditHistory.ts +62 -0
  90. package/src/pages/ViewEditor/index.tsx +549 -59
  91. package/src/pages/Views.tsx +112 -41
  92. package/src/pages/ViewsGrid.tsx +332 -113
  93. package/src/pages/viewsJumpSearch.test.ts +193 -0
  94. package/src/pages/viewsJumpSearch.ts +111 -0
  95. package/src/store/useStore.ts +58 -0
  96. package/src/types/index.ts +10 -0
  97. package/src/utils/elementIcon.test.ts +28 -0
  98. package/src/utils/elementIcon.ts +20 -0
  99. package/src/utils/sourceEditor.ts +46 -0
  100. package/src/utils/watchDiffSummary.ts +159 -0
package/src/api/client.ts CHANGED
@@ -9,11 +9,13 @@ import type {
9
9
  LibraryElement,
10
10
  PlacedElement,
11
11
  Tag,
12
+ TechnologyConnector,
12
13
  View,
13
14
  ViewConnector,
14
15
  ViewLayer,
15
16
  ViewPlacement,
16
17
  ViewTreeNode,
18
+ VisibilityOverride,
17
19
  } from '../types'
18
20
  import {
19
21
  WorkspaceService,
@@ -46,19 +48,161 @@ import {
46
48
  import {
47
49
  ImportService,
48
50
  } from '@buf/tldiagramcom_diagram.bufbuild_es/diag/v1/import_service_pb'
51
+ import {
52
+ WorkspaceVersionService,
53
+ type WorkspaceVersionInfo,
54
+ } from '@buf/tldiagramcom_diagram.bufbuild_es/diag/v1/workspace_version_service_pb'
55
+ import {
56
+ OrgService,
57
+ ListTagColorsResponseSchema,
58
+ } from '@buf/tldiagramcom_diagram.bufbuild_es/diag/v1/org_service_pb'
49
59
  import { transport } from './transport'
50
60
  import { apiUrl, fetchApiAsset } from '../config/runtime'
51
61
 
62
+ async function responseError(res: Response, fallback: string): Promise<Error> {
63
+ const body = await res.json().catch(() => null) as { error?: string } | null
64
+ return new Error(body?.error || `${fallback}: ${res.statusText}`)
65
+ }
66
+
52
67
  export interface DependenciesResponse {
53
68
  elements: DependencyElement[]
54
69
  connectors: DependencyConnector[]
70
+ totalCount?: number
71
+ }
72
+
73
+ export interface WatchRepository {
74
+ id: number
75
+ remote_url: string | null
76
+ repo_root: string
77
+ display_name: string
78
+ branch: string | null
79
+ head_commit: string | null
80
+ identity_status: string
81
+ }
82
+
83
+ export interface WatchLock {
84
+ id: number
85
+ repository_id: number
86
+ pid: number
87
+ started_at: string
88
+ heartbeat_at: string
89
+ status: 'active' | 'paused' | 'stopping' | 'stale' | 'released' | string
90
+ }
91
+
92
+ export interface WatchStatus {
93
+ active: boolean
94
+ repository?: WatchRepository
95
+ lock?: WatchLock
96
+ connected_clients?: number
97
+ }
98
+
99
+ export interface WatchRepresentationSummary {
100
+ repository_id: number
101
+ raw_graph_hash?: string
102
+ filter_settings_hash?: string
103
+ representation_hash?: string
104
+ last_status?: string
105
+ last_started_at?: string
106
+ last_finished_at?: string
107
+ elements_created: number
108
+ elements_updated: number
109
+ connectors_created: number
110
+ connectors_updated: number
111
+ views_created: number
112
+ diffs?: WatchDiff[]
113
+ }
114
+
115
+ export interface WatchContextActionResponse {
116
+ repository_id: number
117
+ action: 'show' | 'hide' | 'clean' | string
118
+ policies_created: number
119
+ policies_updated: number
120
+ policies_deactivated: number
121
+ owners_affected: number
122
+ tier_before: number
123
+ tier_after: number
124
+ max_tier: number
125
+ elements_added: number
126
+ connectors_added: number
127
+ views_added: number
128
+ elements_removed: number
129
+ connectors_removed: number
130
+ views_removed: number
131
+ representation: {
132
+ repository_id: number
133
+ representation_run_id: number
134
+ filter_run_id: number
135
+ raw_graph_hash: string
136
+ filter_settings_hash: string
137
+ representation_hash: string
138
+ }
139
+ summary: WatchRepresentationSummary
140
+ }
141
+
142
+ export interface WatchEvent {
143
+ type: string
144
+ repository_id?: number
145
+ message?: string
146
+ at: string
147
+ data?: unknown
148
+ phase?: string
149
+ watcher_mode?: string
150
+ languages?: string[]
151
+ changed_files?: number
152
+ warnings?: string[]
55
153
  }
56
154
 
155
+ export interface WatchVersion {
156
+ id: number
157
+ repository_id: number
158
+ commit_hash: string
159
+ commit_message?: string
160
+ parent_commit_hash?: string
161
+ branch?: string
162
+ representation_hash: string
163
+ workspace_version_id?: number
164
+ created_at: string
165
+ }
166
+
167
+ export interface WatchDiff {
168
+ id: number
169
+ version_id: number
170
+ owner_type: string
171
+ owner_key: string
172
+ change_type: string
173
+ before_hash?: string
174
+ after_hash?: string
175
+ resource_type?: string
176
+ resource_id?: number
177
+ language?: string
178
+ summary?: string
179
+ added_lines?: number
180
+ removed_lines?: number
181
+ }
182
+
183
+ export interface WorkspaceVersion {
184
+ id: string
185
+ version_id: string
186
+ source: string
187
+ parent_version_id?: string
188
+ view_count: number
189
+ element_count: number
190
+ connector_count: number
191
+ description?: string
192
+ workspace_hash?: string
193
+ created_at: string
194
+ }
195
+
196
+ export type SourceEditor = 'zed' | 'vscode'
197
+
57
198
  // ─── RPC clients ─────────────────────────────────────────────────────────────
58
199
 
59
200
  const workspaceClient = createClient(WorkspaceService, transport)
60
201
  const dependencyClient = createClient(DependencyService, transport)
61
202
  const importClient = createClient(ImportService, transport)
203
+ const workspaceVersionClient = createClient(WorkspaceVersionService, transport)
204
+ const orgClient = createClient(OrgService, transport)
205
+ let dependencyConnectorsCache: Promise<DependencyConnector[]> | null = null
62
206
 
63
207
  // ─── Helpers ─────────────────────────────────────────────────────────────────
64
208
 
@@ -75,6 +219,49 @@ function j<T>(schema: Parameters<typeof toJson>[0], msg: Parameters<typeof toJso
75
219
  return toJson(schema, msg, { useProtoFieldName: true, emitDefaultValues: true }) as unknown as T
76
220
  }
77
221
 
222
+ function timestampToISOString(value: WorkspaceVersionInfo['createdAt']): string {
223
+ if (!value) return ''
224
+ const seconds = typeof value.seconds === 'bigint' ? Number(value.seconds) : Number(value.seconds ?? 0)
225
+ const nanos = Number(value.nanos ?? 0)
226
+ return new Date(seconds * 1000 + Math.floor(nanos / 1_000_000)).toISOString()
227
+ }
228
+
229
+ function mapWorkspaceVersion(version: WorkspaceVersionInfo): WorkspaceVersion {
230
+ return {
231
+ id: version.id,
232
+ version_id: version.versionId,
233
+ source: version.source,
234
+ parent_version_id: version.parentVersionId,
235
+ view_count: version.viewCount,
236
+ element_count: version.elementCount,
237
+ connector_count: version.connectorCount,
238
+ description: version.description,
239
+ workspace_hash: version.workspaceHash,
240
+ created_at: timestampToISOString(version.createdAt),
241
+ }
242
+ }
243
+
244
+ async function fetchWorkspaceRaw(body: Record<string, unknown>) {
245
+ const res = await fetchApiAsset(apiUrl('/diag.v1.WorkspaceService/GetWorkspace'), {
246
+ method: 'POST',
247
+ headers: {
248
+ 'Content-Type': 'application/json',
249
+ 'Connect-Protocol-Version': '1',
250
+ },
251
+ body: JSON.stringify(body),
252
+ })
253
+ if (!res.ok) {
254
+ throw new Error(`GetWorkspace failed: ${res.statusText}`)
255
+ }
256
+ return res.json() as Promise<{
257
+ views?: ProtoDiagram[]
258
+ total_count?: number
259
+ totalCount?: number
260
+ content?: Record<string, { placements?: Record<string, unknown>[]; connectors?: Record<string, unknown>[] }>
261
+ navigations?: Record<string, unknown>[]
262
+ }>
263
+ }
264
+
78
265
  // ─── Proto → frontend type mappers ───────────────────────────────────────────
79
266
 
80
267
  interface ProtoDiagram {
@@ -139,10 +326,10 @@ function protoElementToLibrary(e: Record<string, unknown>): LibraryElement {
139
326
  technology: (e.technology ?? null) as string | null,
140
327
  url: (e.url ?? null) as string | null,
141
328
  logo_url: (e.logo_url ?? e.logoUrl ?? null) as string | null,
142
- technology_connectors: ((e.technology_connectors ?? e.technologyLinks ?? []) as any[]).map(tl => ({
143
- type: tl.type,
329
+ technology_connectors: ((e.technology_connectors ?? e.technologyLinks ?? []) as Array<{ type?: string; slug?: string; label?: string; is_primary_icon?: boolean; isPrimaryIcon?: boolean }>).map(tl => ({
330
+ type: (tl.type ?? 'custom') as TechnologyConnector['type'],
144
331
  slug: tl.slug,
145
- label: tl.label,
332
+ label: tl.label ?? '',
146
333
  is_primary_icon: !!(tl.is_primary_icon ?? tl.isPrimaryIcon),
147
334
  })),
148
335
  tags: (e.tags ?? []) as string[],
@@ -157,6 +344,26 @@ function protoElementToLibrary(e: Record<string, unknown>): LibraryElement {
157
344
  }
158
345
  }
159
346
 
347
+ function libraryElementToDependency(element: LibraryElement): DependencyElement {
348
+ return {
349
+ id: String(element.id),
350
+ name: element.name,
351
+ type: element.kind,
352
+ description: element.description,
353
+ technology: element.technology,
354
+ url: element.url,
355
+ logo_url: element.logo_url,
356
+ technology_connectors: element.technology_connectors,
357
+ tags: element.tags,
358
+ repo: element.repo,
359
+ branch: element.branch,
360
+ language: element.language,
361
+ file_path: element.file_path,
362
+ created_at: element.created_at,
363
+ updated_at: element.updated_at,
364
+ }
365
+ }
366
+
160
367
  function protoPlacedElement(p: Record<string, unknown>): PlacedElement {
161
368
  return {
162
369
  id: Number(p.id ?? 0),
@@ -170,10 +377,10 @@ function protoPlacedElement(p: Record<string, unknown>): PlacedElement {
170
377
  technology: (p.technology ?? null) as string | null,
171
378
  url: (p.url ?? null) as string | null,
172
379
  logo_url: (p.logo_url ?? p.logoUrl ?? null) as string | null,
173
- technology_connectors: ((p.technology_connect_ors ?? p.technology_connectors ?? p.technologyLinks ?? []) as any[]).map(tl => ({
174
- type: tl.type,
380
+ technology_connectors: ((p.technology_connect_ors ?? p.technology_connectors ?? p.technologyLinks ?? []) as Array<{ type?: string; slug?: string; label?: string; is_primary_icon?: boolean; isPrimaryIcon?: boolean }>).map(tl => ({
381
+ type: (tl.type ?? 'custom') as TechnologyConnector['type'],
175
382
  slug: tl.slug,
176
- label: tl.label,
383
+ label: tl.label ?? '',
177
384
  is_primary_icon: !!(tl.is_primary_icon ?? tl.isPrimaryIcon),
178
385
  })),
179
386
  tags: (p.tags ?? []) as string[],
@@ -205,6 +412,25 @@ function protoConnector(e: Record<string, unknown>): Connector {
205
412
  }
206
413
  }
207
414
 
415
+ function protoDependencyConnector(e: Record<string, unknown>): DependencyConnector {
416
+ return {
417
+ id: String(e.id ?? 0),
418
+ view_id: String(e.view_id ?? e.viewId ?? 0),
419
+ source_element_id: String(e.source_element_id ?? e.sourceElementId ?? 0),
420
+ target_element_id: String(e.target_element_id ?? e.targetElementId ?? 0),
421
+ label: (e.label ?? null) as string | null,
422
+ description: (e.description ?? null) as string | null,
423
+ relationship_type: (e.relationship_type ?? e.relationshipType ?? e.relationship ?? null) as string | null,
424
+ direction: String(e.direction ?? 'forward'),
425
+ connector_type: String(e.connector_type ?? e.connectorType ?? e.style ?? 'solid'),
426
+ url: (e.url ?? null) as string | null,
427
+ source_handle: (e.source_handle ?? e.sourceHandle ?? null) as string | null,
428
+ target_handle: (e.target_handle ?? e.targetHandle ?? null) as string | null,
429
+ created_at: String(e.created_at ?? e.createdAt ?? ''),
430
+ updated_at: String(e.updated_at ?? e.updatedAt ?? ''),
431
+ }
432
+ }
433
+
208
434
  function protoNavigation(n: Record<string, unknown>): ViewConnector {
209
435
  return {
210
436
  id: Number(n.id ?? 0),
@@ -328,6 +554,27 @@ export const api = {
328
554
  delete: (_orgId: string, id: number): Promise<void> =>
329
555
  rpc(async () => { await workspaceClient.deleteElement({ orgId: '', elementId: id }) }),
330
556
 
557
+ merge: (sourceId: number, survivorId: number, resolved: Partial<{
558
+ kind: string | null
559
+ description: string | null
560
+ repo: string | null
561
+ branch: string | null
562
+ file_path: string | null
563
+ language: string | null
564
+ }>): Promise<{ survivor: LibraryElement; deleted_id: number }> =>
565
+ rpc(async () => {
566
+ const res = await fetch(apiUrl('/elements/merge'), {
567
+ method: 'POST',
568
+ headers: { 'Content-Type': 'application/json' },
569
+ body: JSON.stringify({ source_id: sourceId, survivor_id: survivorId, resolved }),
570
+ })
571
+ if (!res.ok) {
572
+ throw await responseError(res, 'Merge failed')
573
+ }
574
+ const json = await res.json() as { survivor: Record<string, unknown>; deleted_id: number }
575
+ return { survivor: protoElementToLibrary(json.survivor), deleted_id: json.deleted_id }
576
+ }),
577
+
331
578
  placements: (id: number): Promise<ViewPlacement[]> =>
332
579
  rpc(async () => {
333
580
  const res = await workspaceClient.listElementPlacements({ elementId: id })
@@ -339,7 +586,20 @@ export const api = {
339
586
  workspace: {
340
587
  orgs: {
341
588
  tagColors: {
342
- list: (): Promise<Tag[]> => Promise.resolve([]),
589
+ list: (): Promise<Record<string, Tag>> =>
590
+ rpc(async () => {
591
+ const res = await orgClient.listTagColors({})
592
+ const json = j<{ tags?: Record<string, { color?: string; description?: string | null }> }>(ListTagColorsResponseSchema, res)
593
+ const tags: Record<string, Tag> = {}
594
+ Object.entries(json.tags ?? {}).forEach(([name, tag]) => {
595
+ tags[name] = { name, color: tag.color ?? '#A0AEC0', description: tag.description ?? null }
596
+ })
597
+ return tags
598
+ }),
599
+ update: (name: string, color: string, description?: string | null): Promise<void> =>
600
+ rpc(async () => {
601
+ await orgClient.updateTag({ tag: name, color, description: description ?? undefined })
602
+ }),
343
603
  },
344
604
  },
345
605
 
@@ -384,18 +644,15 @@ export const api = {
384
644
  }),
385
645
 
386
646
  content: (id: number): Promise<{ placements: PlacedElement[]; connectors: Connector[] }> =>
387
- rpc(async () => {
388
- const [placementsRes, connectorsRes] = await Promise.all([
389
- workspaceClient.listPlacements({ viewId: id }),
390
- workspaceClient.listConnectors({ viewId: id }),
391
- ])
392
- const placementJson = j<{ placements: Record<string, unknown>[] }>(ListPlacementsResponseSchema, placementsRes)
393
- const connectorJson = j<{ connectors: Record<string, unknown>[] }>(ListConnectorsResponseSchema, connectorsRes)
647
+ (async () => {
648
+ const res = await fetch(apiUrl(`/views/${id}/projected-content`))
649
+ if (!res.ok) throw new Error('Failed to load view content')
650
+ const json = await res.json() as { placements?: Record<string, unknown>[]; connectors?: Record<string, unknown>[] }
394
651
  return {
395
- placements: (placementJson.placements ?? []).map(protoPlacedElement),
396
- connectors: (connectorJson.connectors ?? []).map(protoConnector),
652
+ placements: (json.placements ?? []).map(protoPlacedElement),
653
+ connectors: (json.connectors ?? []).map(protoConnector),
397
654
  }
398
- }),
655
+ })(),
399
656
 
400
657
  tree: (): Promise<ViewTreeNode[]> =>
401
658
  rpc(async () => {
@@ -434,6 +691,60 @@ export const api = {
434
691
  return (json.views ?? []).map(mapDiagram)
435
692
  }),
436
693
 
694
+ treeAround: async (
695
+ viewId: number,
696
+ opts: { ancestorLevels?: number; descendantLevels?: number } = {},
697
+ ): Promise<ViewTreeNode[]> => {
698
+ const ancestorLevels = opts.ancestorLevels ?? 2
699
+ const descendantLevels = opts.descendantLevels ?? 2
700
+ const current = await api.workspace.views.get(viewId)
701
+
702
+ const ancestors: ViewTreeNode[] = []
703
+ let cursor: ViewTreeNode = current
704
+ for (let depth = 0; depth < ancestorLevels && cursor.parent_view_id != null; depth += 1) {
705
+ const parent = await api.workspace.views.get(cursor.parent_view_id)
706
+ ancestors.unshift(parent)
707
+ cursor = parent
708
+ }
709
+
710
+ const withDescendants = async (node: ViewTreeNode, remainingDepth: number): Promise<ViewTreeNode> => {
711
+ const scoped: ViewTreeNode = { ...node, children: [] }
712
+ if (remainingDepth <= 0) return scoped
713
+ const children = await api.workspace.views.treeChildren(node.id)
714
+ scoped.children = await Promise.all(children.map((child) => withDescendants(child, remainingDepth - 1)))
715
+ return scoped
716
+ }
717
+
718
+ let scoped = await withDescendants(current, descendantLevels)
719
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
720
+ scoped = { ...ancestors[index], children: [scoped] }
721
+ }
722
+ return [scoped]
723
+ },
724
+
725
+ gridData: (): Promise<{
726
+ views: ViewTreeNode[]
727
+ content: Record<number, { placements: PlacedElement[]; connectors: Connector[] }>
728
+ }> =>
729
+ rpc(async () => {
730
+ const json = await fetchWorkspaceRaw({
731
+ includeContent: true,
732
+ hasView: true,
733
+ })
734
+ return {
735
+ views: (json.views ?? []).map(mapDiagram),
736
+ content: Object.fromEntries(
737
+ Object.entries(json.content ?? {}).map(([key, value]) => [
738
+ Number(key),
739
+ {
740
+ placements: (value.placements ?? []).map(protoPlacedElement),
741
+ connectors: (value.connectors ?? []).map(protoConnector),
742
+ },
743
+ ])
744
+ ),
745
+ }
746
+ }),
747
+
437
748
  get: (id: number): Promise<ViewTreeNode> =>
438
749
  rpc(async () => {
439
750
  const res = await workspaceClient.getView({ viewId: id })
@@ -470,6 +781,60 @@ export const api = {
470
781
  setLevel: (id: number, level: number): Promise<void> =>
471
782
  rpc(async () => { await workspaceClient.setViewLevel({ viewId: id, level }) }),
472
783
 
784
+ density: {
785
+ get: async (id: number): Promise<number> => {
786
+ const res = await fetch(apiUrl(`/views/${id}/density`))
787
+ if (!res.ok) throw new Error('Failed to load density')
788
+ const json = await res.json() as { density_level?: number }
789
+ return Number(json.density_level ?? 0)
790
+ },
791
+ set: async (id: number, densityLevel: number): Promise<number> => {
792
+ const res = await fetch(apiUrl(`/views/${id}/density`), {
793
+ method: 'PUT',
794
+ headers: { 'Content-Type': 'application/json' },
795
+ body: JSON.stringify({ density_level: densityLevel }),
796
+ })
797
+ if (!res.ok) throw new Error('Failed to save density')
798
+ const json = await res.json() as { density_level?: number }
799
+ return Number(json.density_level ?? densityLevel)
800
+ },
801
+ },
802
+
803
+ visibilityOverrides: {
804
+ list: async (id: number): Promise<VisibilityOverride[]> => {
805
+ const res = await fetch(apiUrl(`/views/${id}/visibility-overrides`))
806
+ if (!res.ok) throw new Error('Failed to load visibility overrides')
807
+ const json = await res.json() as { overrides?: VisibilityOverride[] }
808
+ return json.overrides ?? []
809
+ },
810
+ set: async (id: number, resourceType: VisibilityOverride['resource_type'], resourceId: number, levelDelta: number): Promise<VisibilityOverride> => {
811
+ const res = await fetch(apiUrl(`/views/${id}/visibility-overrides`), {
812
+ method: 'PUT',
813
+ headers: { 'Content-Type': 'application/json' },
814
+ body: JSON.stringify({ resource_type: resourceType, resource_id: resourceId, level_delta: levelDelta }),
815
+ })
816
+ if (!res.ok) throw new Error('Failed to save visibility override')
817
+ const json = await res.json() as { override?: VisibilityOverride }
818
+ return json.override ?? { view_id: id, resource_type: resourceType, resource_id: resourceId, level_delta: levelDelta }
819
+ },
820
+ promote: async (id: number, resourceType: VisibilityOverride['resource_type'], resourceId: number): Promise<VisibilityOverride> => {
821
+ const res = await fetch(apiUrl(`/views/${id}/visibility-overrides/${resourceType}/${resourceId}/promote`), { method: 'POST' })
822
+ if (!res.ok) throw new Error('Failed to promote visibility')
823
+ const json = await res.json() as { override?: VisibilityOverride }
824
+ return json.override ?? { view_id: id, resource_type: resourceType, resource_id: resourceId, level_delta: 1 }
825
+ },
826
+ demote: async (id: number, resourceType: VisibilityOverride['resource_type'], resourceId: number): Promise<VisibilityOverride> => {
827
+ const res = await fetch(apiUrl(`/views/${id}/visibility-overrides/${resourceType}/${resourceId}/demote`), { method: 'POST' })
828
+ if (!res.ok) throw new Error('Failed to demote visibility')
829
+ const json = await res.json() as { override?: VisibilityOverride }
830
+ return json.override ?? { view_id: id, resource_type: resourceType, resource_id: resourceId, level_delta: -1 }
831
+ },
832
+ reset: async (id: number, resourceType: VisibilityOverride['resource_type'], resourceId: number): Promise<void> => {
833
+ const res = await fetch(apiUrl(`/views/${id}/visibility-overrides/${resourceType}/${resourceId}`), { method: 'DELETE' })
834
+ if (!res.ok) throw new Error('Failed to reset visibility override')
835
+ },
836
+ },
837
+
473
838
  delete: (_orgId: string, id: number): Promise<void> =>
474
839
  rpc(async () => { await workspaceClient.deleteView({ orgId: '', viewId: id }) }),
475
840
 
@@ -620,8 +985,36 @@ export const api = {
620
985
  },
621
986
 
622
987
  dependencies: {
623
- list: (): Promise<DependenciesResponse> =>
988
+ list: (params?: { limit?: number; offset?: number; search?: string }): Promise<DependenciesResponse> =>
624
989
  rpc(async () => {
990
+ if (params) {
991
+ if (!dependencyConnectorsCache) {
992
+ dependencyConnectorsCache = workspaceClient.listConnectors({ viewId: 0 })
993
+ .then((res) => {
994
+ const connectorJson = j<{ connectors: Record<string, unknown>[] }>(ListConnectorsResponseSchema, res)
995
+ return (connectorJson.connectors ?? []).map(protoDependencyConnector)
996
+ })
997
+ }
998
+ const [elements, connectors] = await Promise.all([
999
+ workspaceClient.listElements({
1000
+ limit: params.limit ?? 0,
1001
+ offset: params.offset ?? 0,
1002
+ search: params.search ?? '',
1003
+ }).then((res) => {
1004
+ const json = j<{ elements: Record<string, unknown>[] }>(ListElementsResponseSchema, res)
1005
+ return {
1006
+ elements: (json.elements ?? []).map(protoElementToLibrary),
1007
+ totalCount: res.pagination ? Number(res.pagination.totalCount) : undefined,
1008
+ }
1009
+ }),
1010
+ dependencyConnectorsCache,
1011
+ ])
1012
+ return {
1013
+ elements: elements.elements.map(libraryElementToDependency),
1014
+ connectors,
1015
+ totalCount: elements.totalCount,
1016
+ }
1017
+ }
625
1018
  const res = await dependencyClient.listDependencies({})
626
1019
  return j<DependenciesResponse>(ListDependenciesResponseSchema, res)
627
1020
  }),
@@ -665,8 +1058,8 @@ export const api = {
665
1058
  throw new Error(`Failed to load shared diagram: ${res.statusText}`)
666
1059
  }
667
1060
  const data = await res.json() as {
668
- tree: any[]
669
- views: Record<string, { elements: any[]; connectors: any[] }>
1061
+ tree: ProtoDiagram[]
1062
+ views: Record<string, { elements: Record<string, unknown>[]; connectors: Record<string, unknown>[] }>
670
1063
  password_required?: boolean
671
1064
  }
672
1065
 
@@ -683,7 +1076,7 @@ export const api = {
683
1076
 
684
1077
  // Ensure that the share root is treated as a root (no parent) so that computeLayout
685
1078
  // picks it up even if it was nested in the original workspace.
686
- const sharedRoot = tree.find(n => String(n.id) === String(data.views[token]?.elements?.[0]?.view_id ?? ''))
1079
+ const _sharedRoot = tree.find(n => String(n.id) === String(data.views[token]?.elements?.[0]?.view_id ?? ''))
687
1080
  // Backend actually returns the shareToken.ViewID as the root of the tree it builds.
688
1081
  // We should find the node in 'tree' that has no parent *within the returned set*.
689
1082
  // For shared explore, the backend typically returns a tree starting at the shared view.
@@ -695,9 +1088,9 @@ export const api = {
695
1088
  }
696
1089
  })
697
1090
  const navigations: ViewConnector[] = []
698
- const elementToChildView = new Map<number, any>()
699
- const allViews: any[] = []
700
- const flatTree = (nodes: any[]) => {
1091
+ const elementToChildView = new Map<number, ViewTreeNode>()
1092
+ const allViews: ViewTreeNode[] = []
1093
+ const flatTree = (nodes: ViewTreeNode[]) => {
701
1094
  nodes.forEach(n => {
702
1095
  allViews.push(n)
703
1096
  if (n.owner_element_id) elementToChildView.set(n.owner_element_id, n)
@@ -706,8 +1099,8 @@ export const api = {
706
1099
  }
707
1100
  flatTree(tree)
708
1101
 
709
- Object.values(views).forEach((v: any) => {
710
- v.placements.forEach((p: any) => {
1102
+ Object.values(views).forEach((v) => {
1103
+ v.placements.forEach((p) => {
711
1104
  const childView = elementToChildView.get(p.element_id)
712
1105
  if (childView) {
713
1106
  navigations.push({
@@ -751,4 +1144,73 @@ export const api = {
751
1144
  }
752
1145
  }),
753
1146
  },
1147
+
1148
+ versions: {
1149
+ list: (limit = 50): Promise<WorkspaceVersion[]> =>
1150
+ rpc(async () => {
1151
+ const res = await workspaceVersionClient.listVersions({ limit })
1152
+ return (res.versions ?? []).map(mapWorkspaceVersion)
1153
+ }),
1154
+ },
1155
+
1156
+ watch: {
1157
+ status: async (): Promise<WatchStatus> => {
1158
+ const res = await fetch(apiUrl('/watch/status'))
1159
+ if (!res.ok) throw new Error(`Failed to load watch status: ${res.statusText}`)
1160
+ return res.json()
1161
+ },
1162
+ websocketUrl: (): string => {
1163
+ const url = new URL(apiUrl('/watch/ws'), window.location.href)
1164
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
1165
+ return url.toString()
1166
+ },
1167
+ repositories: async (): Promise<WatchRepository[]> => {
1168
+ const res = await fetch(apiUrl('/watch/repositories'))
1169
+ if (!res.ok) throw new Error(`Failed to load watch repositories: ${res.statusText}`)
1170
+ return res.json()
1171
+ },
1172
+ versions: async (repositoryId: number): Promise<WatchVersion[]> => {
1173
+ const res = await fetch(apiUrl(`/watch/repositories/${repositoryId}/versions`))
1174
+ if (!res.ok) throw new Error(`Failed to load watch versions: ${res.statusText}`)
1175
+ return res.json()
1176
+ },
1177
+ diffs: async (versionId: number, filters?: { owner_type?: string; change_type?: string; resource_type?: string; language?: string }): Promise<WatchDiff[]> => {
1178
+ const params = new URLSearchParams()
1179
+ if (filters?.owner_type) params.set('owner_type', filters.owner_type)
1180
+ if (filters?.change_type) params.set('change_type', filters.change_type)
1181
+ if (filters?.resource_type) params.set('resource_type', filters.resource_type)
1182
+ if (filters?.language) params.set('language', filters.language)
1183
+ const suffix = params.toString() ? `?${params}` : ''
1184
+ const res = await fetch(apiUrl(`/watch/versions/${versionId}/diffs${suffix}`))
1185
+ if (!res.ok) throw new Error(`Failed to load watch diffs: ${res.statusText}`)
1186
+ return res.json()
1187
+ },
1188
+ cleanContext: async (repositoryId: number, input: { resource_type: 'element' | 'view'; resource_id: number }): Promise<WatchContextActionResponse> => {
1189
+ const res = await fetch(apiUrl(`/watch/repositories/${repositoryId}/context/clean`), {
1190
+ method: 'POST',
1191
+ headers: { 'Content-Type': 'application/json' },
1192
+ body: JSON.stringify(input),
1193
+ })
1194
+ if (!res.ok) throw await responseError(res, 'Failed to clean watch context')
1195
+ return res.json()
1196
+ },
1197
+ },
1198
+
1199
+ editor: {
1200
+ open: async (input: { editor: SourceEditor; repo?: string | null; file_path: string; line?: number | null }): Promise<void> => {
1201
+ const res = await fetch(apiUrl('/editor/open'), {
1202
+ method: 'POST',
1203
+ headers: { 'Content-Type': 'application/json' },
1204
+ body: JSON.stringify({
1205
+ editor: input.editor,
1206
+ repo: input.repo ?? '',
1207
+ file_path: input.file_path,
1208
+ line: input.line ?? 0,
1209
+ }),
1210
+ })
1211
+ if (!res.ok) {
1212
+ throw await responseError(res, 'Failed to open editor')
1213
+ }
1214
+ },
1215
+ },
754
1216
  }