@tldiagram/core-ui 2.0.1 → 2.0.2
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 +1 -0
- package/dist/api/client.test.d.ts +1 -0
- package/dist/components/ZUI/proxy.d.ts +13 -0
- package/dist/components/ZUI/proxy.test.d.ts +1 -0
- package/dist/crossBranch/resolve.d.ts +2 -0
- package/dist/index.js +4396 -4251
- package/dist/store/useStore.d.ts +1 -0
- package/dist/types/vscode-messages.d.ts +12 -0
- package/package.json +3 -3
- package/src/api/client.test.ts +17 -0
- package/src/api/client.ts +9 -3
- package/src/components/LayoutSection.tsx +12 -12
- package/src/components/ViewExplorer/TagManager/TagItem.tsx +2 -0
- package/src/components/ZUI/ZUICanvas.tsx +8 -1
- package/src/components/ZUI/focus.test.ts +20 -0
- package/src/components/ZUI/proxy.test.ts +101 -0
- package/src/components/ZUI/proxy.ts +113 -14
- package/src/components/ZUI/useZUIInteraction.ts +148 -25
- package/src/crossBranch/resolve.ts +13 -4
- package/src/pages/ViewEditor/index.tsx +3 -3
- package/src/store/useStore.test.ts +11 -0
- package/src/store/useStore.ts +15 -0
- package/src/types/vscode-messages.ts +16 -0
- package/src/utils/elementIcon.test.ts +6 -0
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react'
|
|
4
4
|
import type { BBox, DiagramGroupLayout, LayoutNode, ZUIViewState, HoveredItem } from './types'
|
|
5
5
|
import { getExpandThresholds, screenToWorldX, screenToWorldY, viewOriginX, viewOriginY } from './renderer'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SOURCE_HANDLE_SIDE,
|
|
8
|
+
DEFAULT_TARGET_HANDLE_SIDE,
|
|
9
|
+
getHandleFlowPosition,
|
|
10
|
+
} from '../../utils/edgeDistribution'
|
|
6
11
|
|
|
7
12
|
export function constrainViewState(view: ZUIViewState, canvasW: number, canvasH: number, bbox: BBox): ZUIViewState {
|
|
8
13
|
const padding = Math.min(600, canvasW * 0.45, canvasH * 0.45)
|
|
@@ -46,6 +51,8 @@ interface DeepestNodeResult {
|
|
|
46
51
|
cumulativeScale: number
|
|
47
52
|
}
|
|
48
53
|
|
|
54
|
+
type EdgeRoutePoint = { x: number; y: number }
|
|
55
|
+
|
|
49
56
|
interface NodeSpatialIndex {
|
|
50
57
|
cellSize: number
|
|
51
58
|
cells: Map<string, LayoutNode[]>
|
|
@@ -194,6 +201,7 @@ type IndexedEdge =
|
|
|
194
201
|
y2: number
|
|
195
202
|
midX: number
|
|
196
203
|
midY: number
|
|
204
|
+
points: EdgeRoutePoint[]
|
|
197
205
|
sourceLabel: string
|
|
198
206
|
targetLabel: string
|
|
199
207
|
label: string
|
|
@@ -209,6 +217,7 @@ type IndexedEdge =
|
|
|
209
217
|
y2: number
|
|
210
218
|
midX: number
|
|
211
219
|
midY: number
|
|
220
|
+
points: EdgeRoutePoint[]
|
|
212
221
|
sourceLabel: string
|
|
213
222
|
targetLabel: string
|
|
214
223
|
diagramId: number
|
|
@@ -227,10 +236,11 @@ function cellKey(cx: number, cy: number): string {
|
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
function addEdgeToSpatialIndex(index: EdgeSpatialIndex, edge: IndexedEdge): void {
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
const
|
|
239
|
+
const points = edge.points.length > 0 ? edge.points : [{ x: edge.x1, y: edge.y1 }, { x: edge.x2, y: edge.y2 }]
|
|
240
|
+
const minX = Math.min(...points.map((point) => point.x))
|
|
241
|
+
const maxX = Math.max(...points.map((point) => point.x))
|
|
242
|
+
const minY = Math.min(...points.map((point) => point.y))
|
|
243
|
+
const maxY = Math.max(...points.map((point) => point.y))
|
|
234
244
|
const startX = Math.floor(minX / index.cellSize)
|
|
235
245
|
const endX = Math.floor(maxX / index.cellSize)
|
|
236
246
|
const startY = Math.floor(minY / index.cellSize)
|
|
@@ -249,6 +259,128 @@ function addEdgeToSpatialIndex(index: EdgeSpatialIndex, edge: IndexedEdge): void
|
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
|
|
262
|
+
function normalizeEdgeRouteType(type: string | null | undefined): 'bezier' | 'straight' | 'step' | 'smoothstep' {
|
|
263
|
+
if (type === 'straight' || type === 'step' || type === 'smoothstep') return type
|
|
264
|
+
return 'bezier'
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function cubicBezierPoint(
|
|
268
|
+
p0: EdgeRoutePoint,
|
|
269
|
+
p1: EdgeRoutePoint,
|
|
270
|
+
p2: EdgeRoutePoint,
|
|
271
|
+
p3: EdgeRoutePoint,
|
|
272
|
+
t: number,
|
|
273
|
+
): EdgeRoutePoint {
|
|
274
|
+
const mt = 1 - t
|
|
275
|
+
const mt2 = mt * mt
|
|
276
|
+
const t2 = t * t
|
|
277
|
+
return {
|
|
278
|
+
x: mt2 * mt * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t2 * t * p3.x,
|
|
279
|
+
y: mt2 * mt * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t2 * t * p3.y,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function buildEdgeRoutePoints(
|
|
284
|
+
source: LayoutNode,
|
|
285
|
+
target: LayoutNode,
|
|
286
|
+
edge: DiagramGroupLayout['edges'][number],
|
|
287
|
+
): { points: EdgeRoutePoint[]; midX: number; midY: number } {
|
|
288
|
+
const sourcePoint = getHandleFlowPosition(
|
|
289
|
+
source.worldX,
|
|
290
|
+
source.worldY,
|
|
291
|
+
source.worldW,
|
|
292
|
+
source.worldH,
|
|
293
|
+
edge.sourceHandle,
|
|
294
|
+
DEFAULT_SOURCE_HANDLE_SIDE,
|
|
295
|
+
)
|
|
296
|
+
const targetPoint = getHandleFlowPosition(
|
|
297
|
+
target.worldX,
|
|
298
|
+
target.worldY,
|
|
299
|
+
target.worldW,
|
|
300
|
+
target.worldH,
|
|
301
|
+
edge.targetHandle,
|
|
302
|
+
DEFAULT_TARGET_HANDLE_SIDE,
|
|
303
|
+
)
|
|
304
|
+
const type = normalizeEdgeRouteType(edge.type)
|
|
305
|
+
|
|
306
|
+
if (type === 'bezier') {
|
|
307
|
+
const dx = Math.abs(targetPoint.x - sourcePoint.x)
|
|
308
|
+
const dy = Math.abs(targetPoint.y - sourcePoint.y)
|
|
309
|
+
const sourceStem = Math.max(
|
|
310
|
+
(sourcePoint.side === 'left' || sourcePoint.side === 'right' ? dx : dy) * 0.5,
|
|
311
|
+
(sourcePoint.side === 'left' || sourcePoint.side === 'right' ? source.worldW : source.worldH) * 0.5,
|
|
312
|
+
)
|
|
313
|
+
const targetStem = Math.max(
|
|
314
|
+
(targetPoint.side === 'left' || targetPoint.side === 'right' ? dx : dy) * 0.5,
|
|
315
|
+
(targetPoint.side === 'left' || targetPoint.side === 'right' ? target.worldW : target.worldH) * 0.5,
|
|
316
|
+
)
|
|
317
|
+
const cp1 = {
|
|
318
|
+
x: sourcePoint.x + (sourcePoint.side === 'left' ? -sourceStem : sourcePoint.side === 'right' ? sourceStem : 0),
|
|
319
|
+
y: sourcePoint.y + (sourcePoint.side === 'top' ? -sourceStem : sourcePoint.side === 'bottom' ? sourceStem : 0),
|
|
320
|
+
}
|
|
321
|
+
const cp2 = {
|
|
322
|
+
x: targetPoint.x + (targetPoint.side === 'left' ? -targetStem : targetPoint.side === 'right' ? targetStem : 0),
|
|
323
|
+
y: targetPoint.y + (targetPoint.side === 'top' ? -targetStem : targetPoint.side === 'bottom' ? targetStem : 0),
|
|
324
|
+
}
|
|
325
|
+
const p0 = { x: sourcePoint.x, y: sourcePoint.y }
|
|
326
|
+
const p3 = { x: targetPoint.x, y: targetPoint.y }
|
|
327
|
+
const points = Array.from({ length: 17 }, (_, index) => cubicBezierPoint(p0, cp1, cp2, p3, index / 16))
|
|
328
|
+
const mid = cubicBezierPoint(p0, cp1, cp2, p3, 0.5)
|
|
329
|
+
return { points, midX: mid.x, midY: mid.y }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (type === 'step' || type === 'smoothstep') {
|
|
333
|
+
const midX = (sourcePoint.x + targetPoint.x) / 2
|
|
334
|
+
const midY = (sourcePoint.y + targetPoint.y) / 2
|
|
335
|
+
const sourceOrth = sourcePoint.side === 'left' || sourcePoint.side === 'right' ? 'h' : 'v'
|
|
336
|
+
const targetOrth = targetPoint.side === 'left' || targetPoint.side === 'right' ? 'h' : 'v'
|
|
337
|
+
const points: EdgeRoutePoint[] = [{ x: sourcePoint.x, y: sourcePoint.y }]
|
|
338
|
+
|
|
339
|
+
if (sourceOrth === 'h' && targetOrth === 'h') {
|
|
340
|
+
points.push({ x: midX, y: sourcePoint.y }, { x: midX, y: targetPoint.y })
|
|
341
|
+
} else if (sourceOrth === 'v' && targetOrth === 'v') {
|
|
342
|
+
points.push({ x: sourcePoint.x, y: midY }, { x: targetPoint.x, y: midY })
|
|
343
|
+
} else if (sourceOrth === 'h' && targetOrth === 'v') {
|
|
344
|
+
points.push({ x: targetPoint.x, y: sourcePoint.y })
|
|
345
|
+
} else {
|
|
346
|
+
points.push({ x: sourcePoint.x, y: targetPoint.y })
|
|
347
|
+
}
|
|
348
|
+
points.push({ x: targetPoint.x, y: targetPoint.y })
|
|
349
|
+
const midIndex = Math.floor((points.length - 1) / 2)
|
|
350
|
+
return {
|
|
351
|
+
points,
|
|
352
|
+
midX: (points[midIndex].x + points[midIndex + 1].x) / 2,
|
|
353
|
+
midY: (points[midIndex].y + points[midIndex + 1].y) / 2,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const points = [{ x: sourcePoint.x, y: sourcePoint.y }, { x: targetPoint.x, y: targetPoint.y }]
|
|
358
|
+
return {
|
|
359
|
+
points,
|
|
360
|
+
midX: (sourcePoint.x + targetPoint.x) / 2,
|
|
361
|
+
midY: (sourcePoint.y + targetPoint.y) / 2,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function nearestDistanceSquaredToRoute(worldX: number, worldY: number, points: EdgeRoutePoint[]): number {
|
|
366
|
+
let best = Number.POSITIVE_INFINITY
|
|
367
|
+
for (let index = 1; index < points.length; index++) {
|
|
368
|
+
const start = points[index - 1]
|
|
369
|
+
const end = points[index]
|
|
370
|
+
const dx = end.x - start.x
|
|
371
|
+
const dy = end.y - start.y
|
|
372
|
+
const lengthSquared = dx * dx + dy * dy
|
|
373
|
+
if (lengthSquared === 0) continue
|
|
374
|
+
|
|
375
|
+
let t = ((worldX - start.x) * dx + (worldY - start.y) * dy) / lengthSquared
|
|
376
|
+
t = Math.max(0, Math.min(1, t))
|
|
377
|
+
const nearestX = start.x + t * dx
|
|
378
|
+
const nearestY = start.y + t * dy
|
|
379
|
+
best = Math.min(best, (worldX - nearestX) ** 2 + (worldY - nearestY) ** 2)
|
|
380
|
+
}
|
|
381
|
+
return best
|
|
382
|
+
}
|
|
383
|
+
|
|
252
384
|
function buildEdgeSpatialIndex(groups: DiagramGroupLayout[]): EdgeSpatialIndex {
|
|
253
385
|
const index: EdgeSpatialIndex = { cellSize: EDGE_INDEX_CELL_SIZE, cells: new Map() }
|
|
254
386
|
|
|
@@ -263,18 +395,18 @@ function buildEdgeSpatialIndex(groups: DiagramGroupLayout[]): EdgeSpatialIndex {
|
|
|
263
395
|
const target = nodeMap.get(edge.targetId)
|
|
264
396
|
if (!source || !target) continue
|
|
265
397
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
const y2 = target.worldY + target.worldH / 2
|
|
398
|
+
const route = buildEdgeRoutePoints(source, target, edge)
|
|
399
|
+
const first = route.points[0]
|
|
400
|
+
const last = route.points[route.points.length - 1]
|
|
270
401
|
addEdgeToSpatialIndex(index, {
|
|
271
402
|
kind: 'edge',
|
|
272
|
-
x1,
|
|
273
|
-
y1,
|
|
274
|
-
x2,
|
|
275
|
-
y2,
|
|
276
|
-
midX:
|
|
277
|
-
midY:
|
|
403
|
+
x1: first.x,
|
|
404
|
+
y1: first.y,
|
|
405
|
+
x2: last.x,
|
|
406
|
+
y2: last.y,
|
|
407
|
+
midX: route.midX,
|
|
408
|
+
midY: route.midY,
|
|
409
|
+
points: route.points,
|
|
278
410
|
sourceLabel: source.label,
|
|
279
411
|
targetLabel: target.label,
|
|
280
412
|
label: edge.label || 'Connection',
|
|
@@ -298,6 +430,7 @@ function buildEdgeSpatialIndex(groups: DiagramGroupLayout[]): EdgeSpatialIndex {
|
|
|
298
430
|
y2,
|
|
299
431
|
midX: (x1 + x2) / 2,
|
|
300
432
|
midY: (y1 + y2) / 2,
|
|
433
|
+
points: [{ x: x1, y: y1 }, { x: x2, y: y2 }],
|
|
301
434
|
sourceLabel: group.label,
|
|
302
435
|
targetLabel: node.label,
|
|
303
436
|
diagramId: group.diagramId,
|
|
@@ -330,17 +463,7 @@ function findHoveredEdge(
|
|
|
330
463
|
if (!bucket) continue
|
|
331
464
|
|
|
332
465
|
for (const edge of bucket) {
|
|
333
|
-
const
|
|
334
|
-
const dy = edge.y2 - edge.y1
|
|
335
|
-
const l2 = dx * dx + dy * dy
|
|
336
|
-
if (l2 === 0) continue
|
|
337
|
-
|
|
338
|
-
let t = ((worldX - edge.x1) * dx + (worldY - edge.y1) * dy) / l2
|
|
339
|
-
t = Math.max(0, Math.min(1, t))
|
|
340
|
-
|
|
341
|
-
const nearestX = edge.x1 + t * dx
|
|
342
|
-
const nearestY = edge.y1 + t * dy
|
|
343
|
-
const distSquared = (worldX - nearestX) ** 2 + (worldY - nearestY) ** 2
|
|
466
|
+
const distSquared = nearestDistanceSquaredToRoute(worldX, worldY, edge.points)
|
|
344
467
|
if (distSquared < bestDistSquared) {
|
|
345
468
|
bestDistSquared = distSquared
|
|
346
469
|
bestEdge = edge
|
|
@@ -552,6 +552,8 @@ export interface ZUIHiddenProxyBadge {
|
|
|
552
552
|
targetAnchorElementId: number
|
|
553
553
|
sourceNodeId: string
|
|
554
554
|
targetNodeId: string
|
|
555
|
+
sourceHandle: string | null
|
|
556
|
+
targetHandle: string | null
|
|
555
557
|
count: number
|
|
556
558
|
details: ProxyConnectorDetails
|
|
557
559
|
}
|
|
@@ -754,13 +756,13 @@ export function resolveZUIProxyConnectors(
|
|
|
754
756
|
return candidates
|
|
755
757
|
}
|
|
756
758
|
const grouped = new Map<string, ProxyConnectorLeaf[]>()
|
|
757
|
-
const nativeVisiblePairs = new
|
|
759
|
+
const nativeVisiblePairs = new Map<string, Connector>()
|
|
758
760
|
|
|
759
761
|
for (const connector of connectors) {
|
|
760
762
|
if (!visibleElements.has(connector.source_element_id) || !visibleElements.has(connector.target_element_id)) continue
|
|
761
763
|
if (!isNativelyRenderedInZUI(connector, connector.source_element_id, connector.target_element_id, visibleNodeIdsByElementId)) continue
|
|
762
764
|
const [leftAnchorElementId, rightAnchorElementId] = canonicalPairElements(connector.source_element_id, connector.target_element_id)
|
|
763
|
-
nativeVisiblePairs.
|
|
765
|
+
nativeVisiblePairs.set([leftAnchorElementId, rightAnchorElementId].join('::'), connector)
|
|
764
766
|
}
|
|
765
767
|
|
|
766
768
|
for (const connector of connectors) {
|
|
@@ -871,15 +873,22 @@ export function resolveZUIProxyConnectors(
|
|
|
871
873
|
endpointCandidates(leaf.ownerViewId, leaf.target.actualElementId)[0]?.anchorElementId === leaf.target.anchorElementId
|
|
872
874
|
return sourceOk && targetOk
|
|
873
875
|
})
|
|
874
|
-
const
|
|
875
|
-
if (
|
|
876
|
+
const nativeDirectConnector = nativeVisiblePairs.get(key)
|
|
877
|
+
if (nativeDirectConnector) {
|
|
876
878
|
if (isDirectChildBadgeOnly) {
|
|
879
|
+
const nativeSourceIsCanonical = nativeDirectConnector.source_element_id === canonicalSourceAnchorElementId
|
|
877
880
|
hiddenBadges.push({
|
|
878
881
|
key: `badge:${key}`,
|
|
879
882
|
sourceAnchorElementId: canonicalSourceAnchorElementId,
|
|
880
883
|
targetAnchorElementId: canonicalTargetAnchorElementId,
|
|
881
884
|
sourceNodeId: visibleNodeIdsByElementId.get(canonicalSourceAnchorElementId) ?? '',
|
|
882
885
|
targetNodeId: visibleNodeIdsByElementId.get(canonicalTargetAnchorElementId) ?? '',
|
|
886
|
+
sourceHandle: nativeSourceIsCanonical
|
|
887
|
+
? (nativeDirectConnector.source_handle ?? 'right')
|
|
888
|
+
: (nativeDirectConnector.target_handle ?? 'left'),
|
|
889
|
+
targetHandle: nativeSourceIsCanonical
|
|
890
|
+
? (nativeDirectConnector.target_handle ?? 'left')
|
|
891
|
+
: (nativeDirectConnector.source_handle ?? 'right'),
|
|
883
892
|
count: details.count,
|
|
884
893
|
details,
|
|
885
894
|
})
|
|
@@ -88,7 +88,7 @@ import { useCrossBranchContextSettings } from '../../crossBranch/settings'
|
|
|
88
88
|
import { removeConnectorGraphSnapshot, upsertConnectorGraphSnapshot, useWorkspaceGraphSnapshot } from '../../crossBranch/store'
|
|
89
89
|
import type { ProxyConnectorDetails } from '../../crossBranch/types'
|
|
90
90
|
import { useDemoRevealViewport, type ViewEditorDemoOptions } from '../../demo/viewEditor'
|
|
91
|
-
import { buildElementLibraryItems, useStore, placedElementToLibraryElement } from '../../store/useStore'
|
|
91
|
+
import { buildElementLibraryItems, useStore, placedElementToLibraryElement, resolveElementForUpdate } from '../../store/useStore'
|
|
92
92
|
import { useWorkspaceVersionPreview } from '../../context/WorkspaceVersionContext'
|
|
93
93
|
import { WATCH_REPRESENTATION_UPDATED_EVENT } from '../../components/WorkspacePanel'
|
|
94
94
|
|
|
@@ -786,7 +786,7 @@ function ViewEditorInner({
|
|
|
786
786
|
|
|
787
787
|
const handleUpdateTags = useCallback(async (elementId: number, tags: string[]) => {
|
|
788
788
|
if (!canEdit) return
|
|
789
|
-
const obj =
|
|
789
|
+
const obj = resolveElementForUpdate(elementId, selectedElement, allElements, viewElements)
|
|
790
790
|
if (!obj) return
|
|
791
791
|
try {
|
|
792
792
|
const saved = await api.elements.update(elementId, {
|
|
@@ -801,7 +801,7 @@ function ViewEditorInner({
|
|
|
801
801
|
} catch (err) {
|
|
802
802
|
console.error('Failed to update tags:', err)
|
|
803
803
|
}
|
|
804
|
-
}, [canEdit, selectedElement, allElements, applyElementSaved, pushElementEditAction, setSelectedElement])
|
|
804
|
+
}, [canEdit, selectedElement, allElements, viewElements, applyElementSaved, pushElementEditAction, setSelectedElement])
|
|
805
805
|
|
|
806
806
|
const pushPlacementMoveAction = useCallback((before: PlacedElement, after: PlacedElement) => {
|
|
807
807
|
if (placementSnapshotsEqual(before, after)) return
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
mergeSavedElementIntoPlacements,
|
|
11
11
|
removeConnectorFromList,
|
|
12
12
|
removePlacedElement,
|
|
13
|
+
resolveElementForUpdate,
|
|
13
14
|
selectConnectorById,
|
|
14
15
|
selectElementById,
|
|
15
16
|
selectExistingElementIds,
|
|
@@ -163,6 +164,16 @@ describe('pure view helpers', () => {
|
|
|
163
164
|
expect(afterRemoval[0]).toMatchObject({ id: 10, name: 'Saved' })
|
|
164
165
|
})
|
|
165
166
|
|
|
167
|
+
it('resolves update payloads from placed elements when the library store is empty', () => {
|
|
168
|
+
const placed = { ...element(10), tags: ['current'] }
|
|
169
|
+
const resolved = resolveElementForUpdate(10, null, [], [placed])
|
|
170
|
+
|
|
171
|
+
expect(resolved).toMatchObject({ id: 10, name: 'Element 10', tags: ['current'] })
|
|
172
|
+
expect(resolveElementForUpdate(20, libraryElement(20), [], [placed])?.id).toBe(20)
|
|
173
|
+
expect(resolveElementForUpdate(30, null, [libraryElement(30)], [placed])?.id).toBe(30)
|
|
174
|
+
expect(resolveElementForUpdate(40, null, [], [placed])).toBeNull()
|
|
175
|
+
})
|
|
176
|
+
|
|
166
177
|
it('upserts and removes connectors', () => {
|
|
167
178
|
const first = connector(1)
|
|
168
179
|
const second = { ...connector(2), label: 'two' }
|
package/src/store/useStore.ts
CHANGED
|
@@ -221,6 +221,21 @@ export function buildElementLibraryItems(allElements: LibraryElement[], viewElem
|
|
|
221
221
|
return Array.from(byId.values())
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
export function resolveElementForUpdate(
|
|
225
|
+
elementId: number,
|
|
226
|
+
selectedElement: LibraryElement | null,
|
|
227
|
+
allElements: LibraryElement[],
|
|
228
|
+
viewElements: PlacedElement[],
|
|
229
|
+
): LibraryElement | null {
|
|
230
|
+
if (selectedElement?.id === elementId) return selectedElement
|
|
231
|
+
|
|
232
|
+
const libraryElement = allElements.find((element) => element.id === elementId)
|
|
233
|
+
if (libraryElement) return libraryElement
|
|
234
|
+
|
|
235
|
+
const placedElement = viewElements.find((element) => element.element_id === elementId)
|
|
236
|
+
return placedElement ? placedElementToLibraryElement(placedElement) : null
|
|
237
|
+
}
|
|
238
|
+
|
|
224
239
|
export function mergeSavedElementIntoPlacements(elements: PlacedElement[], saved: LibraryElement): PlacedElement[] {
|
|
225
240
|
return elements.map((element) =>
|
|
226
241
|
element.element_id === saved.id
|
|
@@ -30,3 +30,19 @@ export type WebviewToExtensionMessage =
|
|
|
30
30
|
| { type: 'request-symbol-list-for-file'; requestId: string; filePath: string }
|
|
31
31
|
| { type: 'diagram-loaded'; diagramId: number; elements: LibraryElement[] }
|
|
32
32
|
| { type: 'request-file-content'; requestId: string; filePath: string; startLine: number }
|
|
33
|
+
|
|
34
|
+
// Watch event (extension → webview)
|
|
35
|
+
export interface WatchEventDetail {
|
|
36
|
+
type: string
|
|
37
|
+
repository_id?: number
|
|
38
|
+
message?: string
|
|
39
|
+
at: string
|
|
40
|
+
data?: any
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Sync status payload
|
|
44
|
+
export interface SyncStatusPayload {
|
|
45
|
+
localChanges: number
|
|
46
|
+
needsPush: boolean
|
|
47
|
+
needsPull: boolean
|
|
48
|
+
}
|
|
@@ -25,4 +25,10 @@ describe('resolveElementIconUrl', () => {
|
|
|
25
25
|
{ type: 'catalog', slug: 'golang', label: 'Go', is_primary_icon: true },
|
|
26
26
|
])).toBeNull()
|
|
27
27
|
})
|
|
28
|
+
|
|
29
|
+
it('does not infer icons from custom technology labels when logo_url is missing', () => {
|
|
30
|
+
expect(resolveElementIconUrl(null, [
|
|
31
|
+
{ type: 'custom', label: 'kafka' },
|
|
32
|
+
])).toBeNull()
|
|
33
|
+
})
|
|
28
34
|
})
|