@tldiagram/core-ui 1.94.1 → 1.94.3
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 +9 -1
- package/dist/components/ElementLibrary.d.ts +1 -2
- package/dist/components/NodeHoverCard.d.ts +3 -2
- package/dist/index.js +7612 -7440
- package/dist/pages/InfiniteZoom.d.ts +1 -1
- package/dist/pages/ViewEditor/hooks/useViewData.d.ts +0 -2
- package/dist/store/useStore.d.ts +0 -2
- package/package.json +1 -1
- package/src/api/client.ts +113 -15
- package/src/components/ContextNeighborElement.tsx +1 -1
- package/src/components/ElementLibrary.tsx +1 -3
- package/src/components/ElementNode.tsx +1 -1
- package/src/components/ElementPanel.tsx +243 -88
- package/src/components/Icons.tsx +2 -7
- package/src/components/NodeHoverCard.tsx +20 -15
- package/src/components/ZUI/layout.ts +1 -1
- package/src/pages/InfiniteZoom.tsx +19 -14
- package/src/pages/ViewEditor/hooks/useViewData.ts +1 -5
- package/src/pages/ViewEditor/index.tsx +11 -3
- package/src/store/useStore.test.ts +3 -5
- package/src/store/useStore.ts +2 -6
|
@@ -18,8 +18,6 @@ import {
|
|
|
18
18
|
PopoverBody,
|
|
19
19
|
PopoverContent,
|
|
20
20
|
PopoverTrigger,
|
|
21
|
-
Radio,
|
|
22
|
-
RadioGroup,
|
|
23
21
|
Tag,
|
|
24
22
|
TagCloseButton,
|
|
25
23
|
TagLabel,
|
|
@@ -45,6 +43,173 @@ import TagUpsert from './TagUpsert'
|
|
|
45
43
|
|
|
46
44
|
import { useViewEditorContext } from '../pages/ViewEditor/context'
|
|
47
45
|
|
|
46
|
+
function normalizeTechnologyLabel(value: string): string {
|
|
47
|
+
return value.trim().replace(/\s+/g, ' ').toLowerCase()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function splitTechnologyLabel(value: string): string[] {
|
|
51
|
+
return value.split(',').map((part) => part.trim()).filter(Boolean)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findCatalogItemByLabel(index: Awaited<ReturnType<typeof getTechnologyCatalogIndex>>, label: string): TechnologyCatalogItem | null {
|
|
55
|
+
const normalized = normalizeTechnologyLabel(label)
|
|
56
|
+
if (!normalized) return null
|
|
57
|
+
|
|
58
|
+
const bySlugMatch = index.bySlug.get(label.trim())
|
|
59
|
+
if (bySlugMatch) return bySlugMatch
|
|
60
|
+
|
|
61
|
+
return index.items.find((item) => (
|
|
62
|
+
normalizeTechnologyLabel(item.name) === normalized ||
|
|
63
|
+
normalizeTechnologyLabel(item.nameShort) === normalized ||
|
|
64
|
+
normalizeTechnologyLabel(item.defaultSlug) === normalized
|
|
65
|
+
)) ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dedupeTechnologyLinks(links: TechnologyConnector[]): TechnologyConnector[] {
|
|
69
|
+
const seenCatalog = new Set<string>()
|
|
70
|
+
const seenCustom = new Set<string>()
|
|
71
|
+
const result: TechnologyConnector[] = []
|
|
72
|
+
let primarySet = false
|
|
73
|
+
|
|
74
|
+
// Sort links to process primary ones first, ensuring they are preserved during deduping
|
|
75
|
+
const sortedLinks = [...links].sort((a, b) => {
|
|
76
|
+
const aPrimary = !!(a.is_primary_icon ?? (a as any).isPrimaryIcon)
|
|
77
|
+
const bPrimary = !!(b.is_primary_icon ?? (b as any).isPrimaryIcon)
|
|
78
|
+
if (aPrimary && !bPrimary) return -1
|
|
79
|
+
if (!aPrimary && bPrimary) return 1
|
|
80
|
+
return 0
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
for (const link of sortedLinks) {
|
|
84
|
+
const label = link.label.trim()
|
|
85
|
+
if (!label) continue
|
|
86
|
+
|
|
87
|
+
const isPrimary = !!(link.is_primary_icon ?? (link as any).isPrimaryIcon)
|
|
88
|
+
|
|
89
|
+
if (link.type === 'catalog' && link.slug) {
|
|
90
|
+
const slug = link.slug.trim()
|
|
91
|
+
const key = slug.toLowerCase()
|
|
92
|
+
if (seenCatalog.has(key)) continue
|
|
93
|
+
seenCatalog.add(key)
|
|
94
|
+
result.push({
|
|
95
|
+
type: 'catalog',
|
|
96
|
+
slug,
|
|
97
|
+
label,
|
|
98
|
+
is_primary_icon: !primarySet && isPrimary,
|
|
99
|
+
})
|
|
100
|
+
if (isPrimary) primarySet = true
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const key = normalizeTechnologyLabel(label)
|
|
105
|
+
if (seenCustom.has(key)) continue
|
|
106
|
+
seenCustom.add(key)
|
|
107
|
+
result.push({ type: 'custom', label, is_primary_icon: false })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return result.slice(0, 3)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function normalizeInitialTechnologyLinks(element: LibraryElement): Promise<TechnologyConnector[]> {
|
|
114
|
+
const rawLinks = element.technology_connectors ?? []
|
|
115
|
+
const legacyLabels = splitTechnologyLabel(element.technology ?? '')
|
|
116
|
+
|
|
117
|
+
if (rawLinks.length === 0 && legacyLabels.length === 0) return []
|
|
118
|
+
|
|
119
|
+
const index = await getTechnologyCatalogIndex()
|
|
120
|
+
const normalized: TechnologyConnector[] = []
|
|
121
|
+
|
|
122
|
+
const pushLabel = (label: string, isPrimaryIcon = false) => {
|
|
123
|
+
const match = findCatalogItemByLabel(index, label)
|
|
124
|
+
if (match) {
|
|
125
|
+
normalized.push({
|
|
126
|
+
type: 'catalog',
|
|
127
|
+
slug: match.defaultSlug,
|
|
128
|
+
label: match.name,
|
|
129
|
+
is_primary_icon: isPrimaryIcon,
|
|
130
|
+
})
|
|
131
|
+
} else {
|
|
132
|
+
normalized.push({ type: 'custom', label: label.trim(), is_primary_icon: false })
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (rawLinks.length > 0) {
|
|
137
|
+
for (const link of rawLinks) {
|
|
138
|
+
if (link.type === 'catalog') {
|
|
139
|
+
const match = link.slug ? index.bySlug.get(link.slug) : null
|
|
140
|
+
normalized.push({
|
|
141
|
+
type: 'catalog',
|
|
142
|
+
slug: link.slug,
|
|
143
|
+
label: match?.name ?? link.label,
|
|
144
|
+
is_primary_icon: !!(link.is_primary_icon ?? (link as any).isPrimaryIcon),
|
|
145
|
+
})
|
|
146
|
+
} else {
|
|
147
|
+
const parts = splitTechnologyLabel(link.label)
|
|
148
|
+
if (parts.length > 1) {
|
|
149
|
+
for (const part of parts) pushLabel(part)
|
|
150
|
+
} else {
|
|
151
|
+
pushLabel(link.label)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
for (const label of legacyLabels) {
|
|
157
|
+
pushLabel(label)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If no catalog item is primary, try to match against element.logo_url or fallback to first catalog item
|
|
162
|
+
const deduped = dedupeTechnologyLinks(normalized)
|
|
163
|
+
const hasPrimary = deduped.some(l => l.type === 'catalog' && l.is_primary_icon)
|
|
164
|
+
if (!hasPrimary) {
|
|
165
|
+
let bestMatchIndex = -1
|
|
166
|
+
if (element.logo_url) {
|
|
167
|
+
bestMatchIndex = deduped.findIndex(l => l.type === 'catalog' && l.slug && element.logo_url?.toLowerCase().includes(l.slug.toLowerCase()))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (bestMatchIndex !== -1) {
|
|
171
|
+
deduped[bestMatchIndex].is_primary_icon = true
|
|
172
|
+
} else {
|
|
173
|
+
const firstCatalog = deduped.find(l => l.type === 'catalog')
|
|
174
|
+
if (firstCatalog) {
|
|
175
|
+
firstCatalog.is_primary_icon = true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return deduped
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildTechnologyFingerprintPayload(
|
|
184
|
+
element: LibraryElement,
|
|
185
|
+
links: TechnologyConnector[],
|
|
186
|
+
type: string,
|
|
187
|
+
) {
|
|
188
|
+
const normalizedLinks = links.map((link) => ({
|
|
189
|
+
type: link.type,
|
|
190
|
+
slug: link.type === 'catalog' ? link.slug : undefined,
|
|
191
|
+
label: link.label,
|
|
192
|
+
is_primary_icon: !!link.is_primary_icon,
|
|
193
|
+
}))
|
|
194
|
+
const normalizedType = type.trim().toLowerCase()
|
|
195
|
+
const technology = links.map((link) => link.label).join(', ')
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
name: element.name,
|
|
199
|
+
description: element.description ?? '',
|
|
200
|
+
kind: normalizedType,
|
|
201
|
+
technology,
|
|
202
|
+
url: element.url ?? '',
|
|
203
|
+
logo_url: element.logo_url ?? '',
|
|
204
|
+
technology_connectors: normalizedLinks,
|
|
205
|
+
tags: element.tags ?? [],
|
|
206
|
+
repo: element.repo,
|
|
207
|
+
branch: element.branch,
|
|
208
|
+
file_path: element.file_path,
|
|
209
|
+
language: element.language,
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
48
213
|
export interface ElementPanelProps extends ElementPanelSlots {
|
|
49
214
|
isOpen: boolean
|
|
50
215
|
onClose: () => void
|
|
@@ -101,6 +266,8 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
101
266
|
}, [technologyQuery])
|
|
102
267
|
|
|
103
268
|
useEffect(() => {
|
|
269
|
+
let cancelled = false
|
|
270
|
+
|
|
104
271
|
if (element) {
|
|
105
272
|
setName(element.name)
|
|
106
273
|
setDescription(element.description ?? '')
|
|
@@ -108,43 +275,42 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
108
275
|
setTypeQuery('')
|
|
109
276
|
setTypeResults([])
|
|
110
277
|
setUrl(element.url ?? '')
|
|
111
|
-
const linksFromElement = element.technology_connectors ?? []
|
|
112
|
-
if (linksFromElement.length > 0) {
|
|
113
|
-
setTechnologyConnectors(linksFromElement)
|
|
114
|
-
} else if (element.technology) {
|
|
115
|
-
setTechnologyConnectors([{ type: 'custom', label: element.technology }])
|
|
116
|
-
} else {
|
|
117
|
-
setTechnologyConnectors([])
|
|
118
|
-
}
|
|
119
278
|
setTags(element.tags ?? [])
|
|
120
279
|
setExplicitLogoClear(false)
|
|
121
280
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
: (element.technology ? [{ type: 'custom', label: element.technology }] : [])
|
|
126
|
-
const normalizedLinks = initialLinks.map((link) => ({
|
|
127
|
-
type: link.type,
|
|
128
|
-
slug: link.type === 'catalog' ? link.slug : undefined,
|
|
129
|
-
label: link.label,
|
|
130
|
-
is_primary_icon: !!link.is_primary_icon,
|
|
281
|
+
const linksFromElement = (element.technology_connectors ?? []).map(tl => ({
|
|
282
|
+
...tl,
|
|
283
|
+
is_primary_icon: !!(tl.is_primary_icon ?? (tl as any).isPrimaryIcon),
|
|
131
284
|
}))
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
285
|
+
const fallbackLinks: TechnologyConnector[] = linksFromElement.length > 0
|
|
286
|
+
? linksFromElement
|
|
287
|
+
: (element.technology ? [{ type: 'custom', label: element.technology, is_primary_icon: false }] : [])
|
|
288
|
+
setTechnologyConnectors(fallbackLinks)
|
|
289
|
+
lastSavedFingerprintRef.current = JSON.stringify(buildTechnologyFingerprintPayload(
|
|
290
|
+
element,
|
|
291
|
+
fallbackLinks,
|
|
292
|
+
element.kind ?? '',
|
|
293
|
+
))
|
|
294
|
+
|
|
295
|
+
normalizeInitialTechnologyLinks(element)
|
|
296
|
+
.then((initialLinks) => {
|
|
297
|
+
if (cancelled) return
|
|
298
|
+
setTechnologyConnectors(initialLinks)
|
|
299
|
+
lastSavedFingerprintRef.current = JSON.stringify(buildTechnologyFingerprintPayload(
|
|
300
|
+
element,
|
|
301
|
+
initialLinks,
|
|
302
|
+
element.kind ?? '',
|
|
303
|
+
))
|
|
304
|
+
})
|
|
305
|
+
.catch(() => {
|
|
306
|
+
if (cancelled) return
|
|
307
|
+
setTechnologyConnectors(fallbackLinks)
|
|
308
|
+
lastSavedFingerprintRef.current = JSON.stringify(buildTechnologyFingerprintPayload(
|
|
309
|
+
element,
|
|
310
|
+
fallbackLinks,
|
|
311
|
+
element.kind ?? '',
|
|
312
|
+
))
|
|
313
|
+
})
|
|
148
314
|
} else {
|
|
149
315
|
setName('')
|
|
150
316
|
setDescription('')
|
|
@@ -160,17 +326,21 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
160
326
|
setExplicitLogoClear(false)
|
|
161
327
|
lastSavedFingerprintRef.current = ''
|
|
162
328
|
}
|
|
329
|
+
|
|
330
|
+
return () => {
|
|
331
|
+
cancelled = true
|
|
332
|
+
}
|
|
163
333
|
}, [element, isOpen])
|
|
164
334
|
|
|
165
335
|
const buildPayloadAndFingerprint = useCallback(async () => {
|
|
166
|
-
const primaryLink = technologyLinks.find((link) => link.type === 'catalog' && link.is_primary_icon && link.slug)
|
|
336
|
+
const primaryLink = technologyLinks.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && link.slug)
|
|
167
337
|
const primarySlug = primaryLink?.slug
|
|
168
338
|
|
|
169
339
|
const normalizedLinks = technologyLinks.map((link) => ({
|
|
170
340
|
type: link.type,
|
|
171
341
|
slug: link.type === 'catalog' ? link.slug : undefined,
|
|
172
342
|
label: link.label,
|
|
173
|
-
is_primary_icon: !!link.is_primary_icon,
|
|
343
|
+
is_primary_icon: !!(link.is_primary_icon ?? (link as any).isPrimaryIcon),
|
|
174
344
|
}))
|
|
175
345
|
|
|
176
346
|
const normalizedType = type.trim().toLowerCase()
|
|
@@ -179,7 +349,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
179
349
|
if (explicitLogoClear) {
|
|
180
350
|
logoUrl = ''
|
|
181
351
|
}
|
|
182
|
-
if (primarySlug) {
|
|
352
|
+
if (!explicitLogoClear && primarySlug) {
|
|
183
353
|
const cached = technologyMeta[primarySlug]
|
|
184
354
|
if (cached?.iconUrl) {
|
|
185
355
|
logoUrl = cached.iconUrl
|
|
@@ -378,43 +548,31 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
378
548
|
}
|
|
379
549
|
|
|
380
550
|
const removeTechnology = (linkToRemove: TechnologyConnector) => {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (firstCatalogIndex === -1) return next
|
|
388
|
-
|
|
389
|
-
return next.map((link, index) => ({
|
|
390
|
-
...link,
|
|
391
|
-
is_primary_icon: index === firstCatalogIndex,
|
|
392
|
-
}))
|
|
393
|
-
})
|
|
551
|
+
if (linkToRemove.type === 'catalog' && linkToRemove.is_primary_icon) {
|
|
552
|
+
setExplicitLogoClear(true)
|
|
553
|
+
}
|
|
554
|
+
setTechnologyConnectors((prev) => prev.filter((link) => (
|
|
555
|
+
!(link.type === linkToRemove.type && link.slug === linkToRemove.slug && link.label === linkToRemove.label)
|
|
556
|
+
)))
|
|
394
557
|
scheduleAutoSave()
|
|
395
558
|
}
|
|
396
559
|
|
|
397
|
-
const
|
|
560
|
+
const togglePrimaryIcon = (selectedSlug: string) => {
|
|
561
|
+
const isDeselecting = selectedPrimarySlug === selectedSlug
|
|
398
562
|
setTechnologyConnectors((prev) => prev.map((link) => {
|
|
399
563
|
if (link.type !== 'catalog') {
|
|
400
564
|
return { ...link, is_primary_icon: false }
|
|
401
565
|
}
|
|
402
566
|
return {
|
|
403
567
|
...link,
|
|
404
|
-
is_primary_icon: link.slug === selectedSlug,
|
|
568
|
+
is_primary_icon: !isDeselecting && link.slug === selectedSlug,
|
|
405
569
|
}
|
|
406
570
|
}))
|
|
407
|
-
setExplicitLogoClear(
|
|
571
|
+
setExplicitLogoClear(isDeselecting)
|
|
408
572
|
scheduleAutoSave()
|
|
409
573
|
}
|
|
410
574
|
|
|
411
|
-
const
|
|
412
|
-
setTechnologyConnectors((prev) => prev.map((link) => ({ ...link, is_primary_icon: false })))
|
|
413
|
-
setExplicitLogoClear(true)
|
|
414
|
-
scheduleAutoSave()
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const selectedPrimarySlug = technologyLinks.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)?.slug ?? ''
|
|
575
|
+
const selectedPrimarySlug = technologyLinks.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)?.slug ?? ''
|
|
418
576
|
|
|
419
577
|
const commitTypeFromQuery = () => {
|
|
420
578
|
if (isReadOnly) return
|
|
@@ -437,7 +595,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
437
595
|
if (isReadOnly || !name.trim()) return
|
|
438
596
|
setLoading(true)
|
|
439
597
|
try {
|
|
440
|
-
const primaryLink = technologyLinks.find((link) => link.type === 'catalog' && link.is_primary_icon && link.slug)
|
|
598
|
+
const primaryLink = technologyLinks.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && link.slug)
|
|
441
599
|
const primaryMetadata = primaryLink?.slug
|
|
442
600
|
? (technologyMeta[primaryLink.slug] ?? await getTechnologyCatalogItemBySlug(primaryLink.slug))
|
|
443
601
|
: null
|
|
@@ -446,7 +604,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
446
604
|
type: link.type,
|
|
447
605
|
slug: link.type === 'catalog' ? link.slug : undefined,
|
|
448
606
|
label: link.label,
|
|
449
|
-
is_primary_icon: !!link.is_primary_icon,
|
|
607
|
+
is_primary_icon: !!(link.is_primary_icon ?? (link as any).isPrimaryIcon),
|
|
450
608
|
}))
|
|
451
609
|
|
|
452
610
|
const normalizedType = type.trim().toLowerCase()
|
|
@@ -457,7 +615,7 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
457
615
|
kind: normalizedType,
|
|
458
616
|
technology: technologyLinks.map((link) => link.label).join(', '),
|
|
459
617
|
url,
|
|
460
|
-
logo_url: primaryMetadata?.iconUrl ?? '',
|
|
618
|
+
logo_url: explicitLogoClear ? '' : (primaryMetadata?.iconUrl ?? ''),
|
|
461
619
|
technology_connectors: normalizedLinks,
|
|
462
620
|
tags,
|
|
463
621
|
repo: element?.repo,
|
|
@@ -703,11 +861,24 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
703
861
|
{technologyLinks.map((link) => {
|
|
704
862
|
const meta = link.slug ? technologyMeta[link.slug] : undefined
|
|
705
863
|
const sourceUrl = meta?.websiteUrl || meta?.docsUrl
|
|
864
|
+
const isSelectable = link.type === 'catalog' && !!link.slug && !isReadOnly
|
|
865
|
+
const isPrimaryIcon = link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug
|
|
706
866
|
return (
|
|
707
867
|
<WrapItem key={`${link.type}:${link.slug ?? link.label}`}>
|
|
708
868
|
<Popover trigger={isMobile ? 'click' : 'hover'} placement="top" closeOnBlur>
|
|
709
869
|
<PopoverTrigger>
|
|
710
|
-
<Tag
|
|
870
|
+
<Tag
|
|
871
|
+
size="sm"
|
|
872
|
+
variant="subtle"
|
|
873
|
+
bg={isPrimaryIcon ? 'blue.500' : 'whiteAlpha.100'}
|
|
874
|
+
border="1px solid"
|
|
875
|
+
borderColor={isPrimaryIcon ? 'blue.300' : 'whiteAlpha.200'}
|
|
876
|
+
color={isPrimaryIcon ? 'white' : undefined}
|
|
877
|
+
cursor={isSelectable ? 'pointer' : 'default'}
|
|
878
|
+
onClick={() => {
|
|
879
|
+
if (isSelectable && link.slug) togglePrimaryIcon(link.slug)
|
|
880
|
+
}}
|
|
881
|
+
>
|
|
711
882
|
<TagLabel color="white">
|
|
712
883
|
{link.type === 'catalog' && meta && (
|
|
713
884
|
<Box as="img" src={resolveWithBase(meta.iconUrl)} alt={link.label} boxSize="12px" objectFit="contain" display="inline-block" mr={1.5} verticalAlign="middle" />
|
|
@@ -715,7 +886,13 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
715
886
|
{link.label}
|
|
716
887
|
</TagLabel>
|
|
717
888
|
{!isReadOnly && (
|
|
718
|
-
<TagCloseButton
|
|
889
|
+
<TagCloseButton
|
|
890
|
+
onClick={(e) => {
|
|
891
|
+
e.preventDefault()
|
|
892
|
+
e.stopPropagation()
|
|
893
|
+
removeTechnology(link)
|
|
894
|
+
}}
|
|
895
|
+
/>
|
|
719
896
|
)}
|
|
720
897
|
</Tag>
|
|
721
898
|
</PopoverTrigger>
|
|
@@ -739,28 +916,6 @@ function ElementPanel({ isOpen, onClose, element, onSave, autoSave = false, onDe
|
|
|
739
916
|
})}
|
|
740
917
|
</Wrap>
|
|
741
918
|
|
|
742
|
-
<VStack align="stretch" spacing={1}>
|
|
743
|
-
<Text fontSize="xs" color="gray.400">Canvas icon (optional)</Text>
|
|
744
|
-
<Button size="xs" variant="ghost" w="fit-content" onClick={clearPrimaryIcon} isDisabled={isReadOnly}>
|
|
745
|
-
None
|
|
746
|
-
</Button>
|
|
747
|
-
<RadioGroup value={selectedPrimarySlug} onChange={markPrimaryIcon}>
|
|
748
|
-
<VStack align="stretch" spacing={1}>
|
|
749
|
-
{technologyLinks.filter((link) => link.type === 'catalog' && !!link.slug).map((link) => (
|
|
750
|
-
<Radio
|
|
751
|
-
key={`primary-${link.slug}`}
|
|
752
|
-
value={link.slug}
|
|
753
|
-
isDisabled={isReadOnly}
|
|
754
|
-
size="sm"
|
|
755
|
-
colorScheme="blue"
|
|
756
|
-
>
|
|
757
|
-
<Text fontSize="xs" color="gray.200">{link.label}</Text>
|
|
758
|
-
</Radio>
|
|
759
|
-
))}
|
|
760
|
-
</VStack>
|
|
761
|
-
</RadioGroup>
|
|
762
|
-
</VStack>
|
|
763
|
-
|
|
764
919
|
<Text fontSize="10px" color="gray.500">Maximum 3 linked technologies.</Text>
|
|
765
920
|
</VStack>
|
|
766
921
|
</FormControl>
|
package/src/components/Icons.tsx
CHANGED
|
@@ -5,9 +5,7 @@ export function ZoomOutIcon({ size = 14, strokeWidth = 3 }: { size?: number, str
|
|
|
5
5
|
return (
|
|
6
6
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
7
7
|
strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
|
|
8
|
-
<
|
|
9
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
10
|
-
<line x1="8" y1="11" x2="14" y2="11" />
|
|
8
|
+
<path d="M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0M21 21l-4.343-4.343M8 11h6" />
|
|
11
9
|
</svg>
|
|
12
10
|
)
|
|
13
11
|
}
|
|
@@ -16,10 +14,7 @@ export function ZoomInIcon({ size = 14, strokeWidth = 3 }: { size?: number, stro
|
|
|
16
14
|
return (
|
|
17
15
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
18
16
|
strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
|
|
19
|
-
<
|
|
20
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
21
|
-
<line x1="11" y1="8" x2="11" y2="14" />
|
|
22
|
-
<line x1="8" y1="11" x2="14" y2="11" />
|
|
17
|
+
<path d="M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0M21 21l-4.343-4.343M11 8v6M8 11h6" />
|
|
23
18
|
</svg>
|
|
24
19
|
)
|
|
25
20
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Box, Divider, Flex, HStack, Tag, Text, VStack } from '@chakra-ui/react'
|
|
2
|
-
import { TYPE_COLORS, type PlacedElement } from '../types'
|
|
2
|
+
import { TYPE_COLORS, type PlacedElement, type Tag as TagType } from '../types'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
data: PlacedElement & { hasChildLink?: boolean }
|
|
6
6
|
/** Bounding rect of the node element in screen (viewport) coordinates */
|
|
7
7
|
anchorRect: DOMRect
|
|
8
|
+
tagColors?: Record<string, TagType>
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export default function NodeHoverCard({ data, anchorRect }: Props) {
|
|
11
|
+
export default function NodeHoverCard({ data, anchorRect, tagColors }: Props) {
|
|
11
12
|
const color = TYPE_COLORS[data.kind ?? ''] ?? 'gray'
|
|
12
13
|
|
|
13
14
|
// Position the card centred above the node using fixed coordinates so it
|
|
@@ -101,19 +102,23 @@ export default function NodeHoverCard({ data, anchorRect }: Props) {
|
|
|
101
102
|
{/* Tags */}
|
|
102
103
|
{data.tags && data.tags.length > 0 && (
|
|
103
104
|
<HStack spacing={1} flexWrap="wrap">
|
|
104
|
-
{data.tags.map((tag) =>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
{data.tags.map((tag) => {
|
|
106
|
+
const tagColor = tagColors?.[tag]?.color
|
|
107
|
+
return (
|
|
108
|
+
<Tag
|
|
109
|
+
key={tag}
|
|
110
|
+
size="sm"
|
|
111
|
+
variant="subtle"
|
|
112
|
+
bg={tagColor ? `color-mix(in srgb, ${tagColor} 12%, transparent)` : 'whiteAlpha.100'}
|
|
113
|
+
color={tagColor || 'gray.400'}
|
|
114
|
+
fontSize="9px"
|
|
115
|
+
borderColor={tagColor ? `color-mix(in srgb, ${tagColor} 30%, transparent)` : 'rgba(255,255,255,0.1)'}
|
|
116
|
+
borderWidth="1px"
|
|
117
|
+
>
|
|
118
|
+
{tag}
|
|
119
|
+
</Tag>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
117
122
|
</HStack>
|
|
118
123
|
)}
|
|
119
124
|
|
|
@@ -168,7 +168,7 @@ function buildNodes(
|
|
|
168
168
|
}))
|
|
169
169
|
|
|
170
170
|
const derivedPrimaryIconPath = (() => {
|
|
171
|
-
const selected = obj.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
|
|
171
|
+
const selected = obj.technology_connectors?.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)
|
|
172
172
|
if (!selected?.slug) return null
|
|
173
173
|
return resolveIconPath(`/icons/${selected.slug}.png`)
|
|
174
174
|
})()
|
|
@@ -132,7 +132,7 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
132
132
|
}
|
|
133
133
|
}, [showMiniOnboarding])
|
|
134
134
|
useEffect(() => {
|
|
135
|
-
const loader = api.explore.load()
|
|
135
|
+
const loader = sharedToken ? api.explore.loadShared(sharedToken) : api.explore.load()
|
|
136
136
|
loader.then((d) => {
|
|
137
137
|
if (d.password_required) {
|
|
138
138
|
setLoading(false)
|
|
@@ -147,23 +147,27 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
147
147
|
// Fetch tag colors and layers once data is loaded (authenticated users only).
|
|
148
148
|
// Only fetch from root tree nodes child/nested diagrams would duplicate the same layers.
|
|
149
149
|
useEffect(() => {
|
|
150
|
-
if (!data) return
|
|
150
|
+
if (!data || sharedToken) return
|
|
151
151
|
let cancelled = false
|
|
152
152
|
const rootIds = (data.tree ?? []).map(n => n.id)
|
|
153
153
|
const fetchTagData = async () => {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
try {
|
|
155
|
+
const diagramLayers = await Promise.all(
|
|
156
|
+
rootIds.map(id => api.workspace.views.layers.list(id)),
|
|
157
|
+
)
|
|
158
|
+
if (!cancelled) {
|
|
159
|
+
// Deduplicate by layer ID in case of any API overlap
|
|
160
|
+
const seen = new Set<number>()
|
|
161
|
+
const unique = diagramLayers.flat().filter(l => seen.has(l.id) ? false : (seen.add(l.id), true))
|
|
162
|
+
setLayers(unique)
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// intentionally empty: layers are not available for public shared pages
|
|
162
166
|
}
|
|
163
167
|
}
|
|
164
168
|
void fetchTagData()
|
|
165
169
|
return () => { cancelled = true }
|
|
166
|
-
}, [data])
|
|
170
|
+
}, [data, sharedToken])
|
|
167
171
|
|
|
168
172
|
const handleCanvasReady = useCallback(() => {
|
|
169
173
|
setCanvasReady(true)
|
|
@@ -400,7 +404,8 @@ function InfiniteZoomInner({ sharedToken, shareSlot }: Props, ref?: React.Ref<In
|
|
|
400
404
|
const InfiniteZoom = forwardRef<InfiniteZoomHandle, Props>(InfiniteZoomInner)
|
|
401
405
|
export default InfiniteZoom
|
|
402
406
|
|
|
403
|
-
export
|
|
407
|
+
export const SharedInfiniteZoom = forwardRef<InfiniteZoomHandle, Props>((props, ref) => {
|
|
404
408
|
const { token } = useParams()
|
|
405
|
-
|
|
406
|
-
}
|
|
409
|
+
const effectiveToken = props.sharedToken ?? token
|
|
410
|
+
return <InfiniteZoom {...props} ref={ref} sharedToken={effectiveToken} />
|
|
411
|
+
})
|
|
@@ -190,8 +190,6 @@ export function useViewData({
|
|
|
190
190
|
const treeData = useStore((state) => state.treeData)
|
|
191
191
|
const allElements = useStore((state) => state.allElements)
|
|
192
192
|
const setAllElements = useStore((state) => state.setAllElements)
|
|
193
|
-
const libraryRefresh = useStore((state) => state.libraryRefresh)
|
|
194
|
-
const setLibraryRefresh = useStore((state) => state.setLibraryRefresh)
|
|
195
193
|
const hydrateViewContent = useStore((state) => state.hydrateViewContent)
|
|
196
194
|
const resetCanvas = useStore((state) => state.resetCanvas)
|
|
197
195
|
const removeElementPlacement = useStore((state) => state.removeElementPlacement)
|
|
@@ -268,7 +266,7 @@ export function useViewData({
|
|
|
268
266
|
|
|
269
267
|
// ── Keep all-org elements for inline adder ──────────────────────────────────
|
|
270
268
|
const allElementsQuery = useQuery({
|
|
271
|
-
queryKey: ['elements', 'list'
|
|
269
|
+
queryKey: ['elements', 'list'],
|
|
272
270
|
queryFn: () => api.elements.list(),
|
|
273
271
|
})
|
|
274
272
|
|
|
@@ -671,8 +669,6 @@ export function useViewData({
|
|
|
671
669
|
incomingLinks,
|
|
672
670
|
treeData,
|
|
673
671
|
allElements,
|
|
674
|
-
libraryRefresh,
|
|
675
|
-
setLibraryRefresh,
|
|
676
672
|
existingElementIds,
|
|
677
673
|
// Stable refs
|
|
678
674
|
viewElementsRef,
|
|
@@ -260,7 +260,15 @@ function ViewEditorInner({
|
|
|
260
260
|
const [tagColors, setTagColors] = useState<Record<string, Tag>>({})
|
|
261
261
|
|
|
262
262
|
useEffect(() => {
|
|
263
|
-
|
|
263
|
+
api.workspace.orgs.tagColors.list().then((res) => {
|
|
264
|
+
if (Array.isArray(res)) {
|
|
265
|
+
const next: Record<string, Tag> = {}
|
|
266
|
+
res.forEach(t => { next[t.name] = t })
|
|
267
|
+
setTagColors(next)
|
|
268
|
+
} else {
|
|
269
|
+
setTagColors(res)
|
|
270
|
+
}
|
|
271
|
+
}).catch(() => { /* skip */ })
|
|
264
272
|
}, [])
|
|
265
273
|
|
|
266
274
|
const [layers, setLayers] = useState<import('../../types').ViewLayer[]>([])
|
|
@@ -422,7 +430,7 @@ function ViewEditorInner({
|
|
|
422
430
|
view, setView, viewElements, setViewElements, connectors, setConnectors,
|
|
423
431
|
rfNodes, setRfNodes, rfEdges, setRfEdges,
|
|
424
432
|
linksMap, setLinksMap, parentLinksMap, setParentLinksMap,
|
|
425
|
-
treeData, allElements,
|
|
433
|
+
treeData, allElements,
|
|
426
434
|
existingElementIds,
|
|
427
435
|
viewElementsRef, linksMapRef, parentLinksMapRef, incomingLinksRef,
|
|
428
436
|
treeDataRef, rfNodesRef, rfEdgesRef, viewIdRef,
|
|
@@ -1376,7 +1384,7 @@ function ViewEditorInner({
|
|
|
1376
1384
|
<ElementLibrary
|
|
1377
1385
|
existingElementIds={existingElementIds}
|
|
1378
1386
|
existingElements={existingElements}
|
|
1379
|
-
onCreateNew={handleCreateNewLibrary}
|
|
1387
|
+
onCreateNew={handleCreateNewLibrary}
|
|
1380
1388
|
isOpen={libraryOpen} onClose={handleCloseLibrary}
|
|
1381
1389
|
onTapAdd={canEdit ? handleTapAdd : undefined}
|
|
1382
1390
|
onFindElement={handleFindElement}
|