@tldiagram/core-ui 1.94.1 → 1.94.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/components/ElementLibrary.d.ts +1 -2
- package/dist/index.js +7779 -7676
- 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 +24 -14
- 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/ZUI/layout.ts +1 -1
- package/src/pages/ViewEditor/hooks/useViewData.ts +1 -5
- package/src/pages/ViewEditor/index.tsx +2 -2
- package/src/store/useStore.test.ts +3 -5
- package/src/store/useStore.ts +2 -6
|
@@ -56,8 +56,6 @@ export declare function useViewData({ viewId, interactionSourceId, clickConnectM
|
|
|
56
56
|
incomingLinks: import("../../..").IncomingViewConnector[];
|
|
57
57
|
treeData: ViewTreeNode[];
|
|
58
58
|
allElements: LibraryElement[];
|
|
59
|
-
libraryRefresh: number;
|
|
60
|
-
setLibraryRefresh: (next: import("../../../store/useStore").StoreSetter<number>) => void;
|
|
61
59
|
existingElementIds: Set<number>;
|
|
62
60
|
viewElementsRef: import("react").MutableRefObject<PlacedElement[]>;
|
|
63
61
|
linksMapRef: import("react").MutableRefObject<Record<number, import("../../..").ViewConnector[]>>;
|
package/dist/store/useStore.d.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type CanvasStoreState = ViewEditorUiState & {
|
|
|
32
32
|
incomingLinks: IncomingViewConnector[];
|
|
33
33
|
treeData: ViewTreeNode[];
|
|
34
34
|
allElements: LibraryElement[];
|
|
35
|
-
libraryRefresh: number;
|
|
36
35
|
setViewEditorUi: (patch: Partial<ViewEditorUiState>) => void;
|
|
37
36
|
setSnapToGrid: (snapToGrid: boolean) => void;
|
|
38
37
|
setSelectedElement: (selectedElement: LibraryElement | null) => void;
|
|
@@ -47,7 +46,6 @@ export type CanvasStoreState = ViewEditorUiState & {
|
|
|
47
46
|
setIncomingLinks: (next: StoreSetter<IncomingViewConnector[]>) => void;
|
|
48
47
|
setTreeData: (next: StoreSetter<ViewTreeNode[]>) => void;
|
|
49
48
|
setAllElements: (next: StoreSetter<LibraryElement[]>) => void;
|
|
50
|
-
setLibraryRefresh: (next: StoreSetter<number>) => void;
|
|
51
49
|
resetCanvas: () => void;
|
|
52
50
|
hydrateViewContent: (payload: ViewContentPayload) => void;
|
|
53
51
|
updateElementPosition: (elementId: number, x: number, y: number) => void;
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -134,41 +134,51 @@ function protoElementToLibrary(e: Record<string, unknown>): LibraryElement {
|
|
|
134
134
|
description: (e.description ?? null) as string | null,
|
|
135
135
|
technology: (e.technology ?? null) as string | null,
|
|
136
136
|
url: (e.url ?? null) as string | null,
|
|
137
|
-
logo_url: (e.logo_url ?? null) as string | null,
|
|
138
|
-
technology_connectors: (e.technology_connectors ?? []) as
|
|
137
|
+
logo_url: (e.logo_url ?? e.logoUrl ?? null) as string | null,
|
|
138
|
+
technology_connectors: ((e.technology_connectors ?? e.technologyLinks ?? []) as any[]).map(tl => ({
|
|
139
|
+
type: tl.type,
|
|
140
|
+
slug: tl.slug,
|
|
141
|
+
label: tl.label,
|
|
142
|
+
is_primary_icon: !!(tl.is_primary_icon ?? tl.isPrimaryIcon),
|
|
143
|
+
})),
|
|
139
144
|
tags: (e.tags ?? []) as string[],
|
|
140
145
|
repo: (e.repo ?? null) as string | null,
|
|
141
146
|
branch: (e.branch ?? null) as string | null,
|
|
142
147
|
file_path: (e.file_path ?? null) as string | null,
|
|
143
148
|
language: (e.language ?? null) as string | null,
|
|
144
|
-
created_at: String(e.created_at ?? new Date().toISOString()),
|
|
145
|
-
updated_at: String(e.updated_at ?? new Date().toISOString()),
|
|
146
|
-
has_view: Boolean(e.has_view ?? false),
|
|
147
|
-
view_label: (e.view_label ?? null) as string | null,
|
|
149
|
+
created_at: String(e.created_at ?? e.createdAt ?? new Date().toISOString()),
|
|
150
|
+
updated_at: String(e.updated_at ?? e.updatedAt ?? new Date().toISOString()),
|
|
151
|
+
has_view: Boolean(e.has_view ?? e.hasView ?? false),
|
|
152
|
+
view_label: (e.view_label ?? e.viewLabel ?? null) as string | null,
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
function protoPlacedElement(p: Record<string, unknown>): PlacedElement {
|
|
152
157
|
return {
|
|
153
158
|
id: Number(p.id ?? 0),
|
|
154
|
-
view_id: Number(p.view_id ?? 0),
|
|
155
|
-
element_id: Number(p.element_id ?? 0),
|
|
156
|
-
position_x: Number(p.position_x ?? 0),
|
|
157
|
-
position_y: Number(p.position_y ?? 0),
|
|
159
|
+
view_id: Number(p.view_id ?? p.viewId ?? 0),
|
|
160
|
+
element_id: Number(p.element_id ?? p.elementId ?? 0),
|
|
161
|
+
position_x: Number(p.position_x ?? p.positionX ?? 0),
|
|
162
|
+
position_y: Number(p.position_y ?? p.positionY ?? 0),
|
|
158
163
|
name: String(p.name ?? ''),
|
|
159
164
|
description: (p.description ?? null) as string | null,
|
|
160
165
|
kind: (p.kind ?? null) as string | null,
|
|
161
166
|
technology: (p.technology ?? null) as string | null,
|
|
162
167
|
url: (p.url ?? null) as string | null,
|
|
163
|
-
logo_url: (p.logo_url ?? null) as string | null,
|
|
164
|
-
technology_connectors: (p.technology_connectors ?? []) as
|
|
168
|
+
logo_url: (p.logo_url ?? p.logoUrl ?? null) as string | null,
|
|
169
|
+
technology_connectors: ((p.technology_connect_ors ?? p.technology_connectors ?? p.technologyLinks ?? []) as any[]).map(tl => ({
|
|
170
|
+
type: tl.type,
|
|
171
|
+
slug: tl.slug,
|
|
172
|
+
label: tl.label,
|
|
173
|
+
is_primary_icon: !!(tl.is_primary_icon ?? tl.isPrimaryIcon),
|
|
174
|
+
})),
|
|
165
175
|
tags: (p.tags ?? []) as string[],
|
|
166
176
|
repo: (p.repo ?? null) as string | null,
|
|
167
177
|
branch: (p.branch ?? null) as string | null,
|
|
168
178
|
file_path: (p.file_path ?? null) as string | null,
|
|
169
179
|
language: (p.language ?? null) as string | null,
|
|
170
|
-
has_view: Boolean(p.has_view ?? false),
|
|
171
|
-
view_label: (p.view_label ?? null) as string | null,
|
|
180
|
+
has_view: Boolean(p.has_view ?? p.hasView ?? false),
|
|
181
|
+
view_label: (p.view_label ?? p.viewLabel ?? null) as string | null,
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
184
|
|
|
@@ -62,7 +62,7 @@ function ContextNeighborNode({ data }: Props) {
|
|
|
62
62
|
|
|
63
63
|
const logoUrl = useMemo(() => {
|
|
64
64
|
if (data.logo_url) return resolveIconPath(data.logo_url)
|
|
65
|
-
const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
|
|
65
|
+
const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)
|
|
66
66
|
if (!selected?.slug) return undefined
|
|
67
67
|
return resolveIconPath(`/icons/${selected.slug}.png`)
|
|
68
68
|
}, [data.logo_url, data.technology_connectors])
|
|
@@ -34,7 +34,6 @@ interface Props {
|
|
|
34
34
|
existingElementIds: Set<number>
|
|
35
35
|
existingElements?: LibraryElement[]
|
|
36
36
|
onCreateNew: () => void
|
|
37
|
-
refresh: number
|
|
38
37
|
isOpen: boolean
|
|
39
38
|
onClose: () => void
|
|
40
39
|
onTapAdd?: (obj: LibraryElement) => void
|
|
@@ -89,7 +88,6 @@ function ElementLibrary({
|
|
|
89
88
|
existingElementIds,
|
|
90
89
|
existingElements = [],
|
|
91
90
|
onCreateNew,
|
|
92
|
-
refresh,
|
|
93
91
|
isOpen,
|
|
94
92
|
onClose,
|
|
95
93
|
onTapAdd,
|
|
@@ -134,7 +132,7 @@ function ElementLibrary({
|
|
|
134
132
|
if (isOpen) {
|
|
135
133
|
fetchElements(0, searchRef.current, true)
|
|
136
134
|
}
|
|
137
|
-
}, [isOpen,
|
|
135
|
+
}, [isOpen, fetchElements])
|
|
138
136
|
|
|
139
137
|
// Debounced search
|
|
140
138
|
useEffect(() => {
|
|
@@ -317,7 +317,7 @@ function ElementNode({ data, selected }: Props) {
|
|
|
317
317
|
}, [data.reconnectCandidates])
|
|
318
318
|
|
|
319
319
|
const derivedPrimaryIconPath = (() => {
|
|
320
|
-
const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!link.is_primary_icon && !!link.slug)
|
|
320
|
+
const selected = data.technology_connectors?.find((link) => link.type === 'catalog' && !!(link.is_primary_icon ?? (link as any).isPrimaryIcon) && !!link.slug)
|
|
321
321
|
if (!selected?.slug) return undefined
|
|
322
322
|
return resolveIconPath(`/icons/${selected.slug}.png`)
|
|
323
323
|
})()
|
|
@@ -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
|
}
|
|
@@ -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
|
})()
|
|
@@ -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,
|