ea 0.6.3 → 0.6.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/config/themes/119.yml +4 -4
- data/frontend/dist/app.iife.js +8 -8
- data/frontend/src/App.vue +6 -2
- data/frontend/src/components/AppHeader.vue +35 -8
- data/frontend/src/components/AppSidebar.vue +2 -2
- data/frontend/src/components/ClassDetails.vue +103 -120
- data/frontend/src/components/DiagramView.vue +19 -6
- data/frontend/src/components/PackageDetails.vue +67 -26
- data/frontend/src/components/PackageTreeNode.vue +25 -20
- data/frontend/src/components/SearchResults.vue +21 -8
- data/frontend/src/components/WelcomeView.vue +2 -2
- data/frontend/src/stores/dataStore.ts +142 -48
- data/frontend/src/stores/uiStore.ts +3 -4
- data/frontend/src/types.ts +160 -163
- data/lib/ea/model/diagram_connector.rb +9 -0
- data/lib/ea/model/property.rb +6 -1
- data/lib/ea/sources/qea/classifier_builder.rb +3 -1
- data/lib/ea/sources/qea/diagram_builder.rb +91 -9
- data/lib/ea/sources/qea/property_builder.rb +12 -2
- data/lib/ea/spa/output/single_file_strategy.rb +1 -1
- data/lib/ea/spa/projector.rb +108 -6
- data/lib/ea/svg/ea_emitter/compartment/attributes.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/constraints.rb +1 -0
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +9 -25
- data/lib/ea/svg/ea_emitter/compartment/header.rb +1 -0
- data/lib/ea/svg/ea_emitter/element/attribute_line_builder.rb +5 -0
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +51 -11
- data/lib/ea/svg/ea_emitter/element/compartment_geometry.rb +12 -10
- data/lib/ea/svg/ea_emitter/element/header_line_provider/name.rb +6 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +25 -8
- data/lib/ea/svg/ea_emitter/element/header_renderer.rb +16 -4
- data/lib/ea/svg/ea_emitter/element/legend_renderer.rb +29 -4
- data/lib/ea/svg/ea_emitter/elements.rb +70 -20
- data/lib/ea/svg/ea_emitter/font_resolver.rb +5 -6
- data/lib/ea/svg/ea_emitter/label/end_label.rb +3 -1
- data/lib/ea/svg/ea_emitter/labels.rb +38 -2
- data/lib/ea/svg/ea_emitter/text_renderer.rb +97 -8
- data/lib/ea/svg/parity/checker.rb +19 -9
- data/lib/ea/svg/parity/corpus_report.rb +83 -0
- data/lib/ea/svg/parity/differ.rb +231 -0
- data/lib/ea/svg/parity.rb +2 -0
- data/lib/ea/theme/definition.rb +3 -3
- data/lib/ea/theme/loader.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, watch } from 'vue'
|
|
3
3
|
import { useDataStore } from '../stores/dataStore'
|
|
4
4
|
import { useUiStore } from '../stores/uiStore'
|
|
5
|
+
import type { SpaPackagePayload } from '../types'
|
|
5
6
|
|
|
6
7
|
const data = useDataStore()
|
|
7
8
|
const ui = useUiStore()
|
|
8
9
|
|
|
10
|
+
const pkgId = computed(() => ui.currentPackageId)
|
|
9
11
|
const pkg = computed(() =>
|
|
10
|
-
|
|
12
|
+
pkgId.value ? (data.elementFor(pkgId.value)?.payload as SpaPackagePayload | undefined) ?? null : null,
|
|
11
13
|
)
|
|
14
|
+
|
|
15
|
+
watch(
|
|
16
|
+
pkgId,
|
|
17
|
+
async (id) => {
|
|
18
|
+
if (!id) return
|
|
19
|
+
const shard = await data.ensureElement(id)
|
|
20
|
+
if (!shard || ui.currentPackageId !== id) return
|
|
21
|
+
const p = shard.payload
|
|
22
|
+
await data.ensureElements([
|
|
23
|
+
...(p.classifierIds || []),
|
|
24
|
+
...(p.subPackageIds || []),
|
|
25
|
+
...(p.diagramIds || []),
|
|
26
|
+
])
|
|
27
|
+
},
|
|
28
|
+
{ immediate: true },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
function definitionOf(p: SpaPackagePayload | null): string {
|
|
32
|
+
return p?.annotations?.find((a) => a.kind === 'documentation')?.body || ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const subPackageIds = computed(() => pkg.value?.subPackageIds ?? [])
|
|
36
|
+
const diagramIds = computed(() => pkg.value?.diagramIds ?? [])
|
|
37
|
+
const classifierIds = computed(() => pkg.value?.classifierIds ?? [])
|
|
38
|
+
|
|
39
|
+
function pkgName(id: string): string {
|
|
40
|
+
return data.nodesById[id]?.name || id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function diagramName(id: string): string {
|
|
44
|
+
return data.elementFor(id)?.payload?.name || id
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function entryFor(id: string) {
|
|
48
|
+
return data.entriesById[id]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function elementFor(id: string) {
|
|
52
|
+
return data.elementFor(id)?.payload
|
|
53
|
+
}
|
|
12
54
|
</script>
|
|
13
55
|
|
|
14
56
|
<template>
|
|
@@ -16,49 +58,48 @@ const pkg = computed(() =>
|
|
|
16
58
|
<div class="entity-header">
|
|
17
59
|
<div class="entity-title">
|
|
18
60
|
<h2 class="entity-name">{{ pkg.name }}</h2>
|
|
19
|
-
<div class="entity-subtitle" v-if="pkg.path">{{ pkg.path }}</div>
|
|
20
61
|
</div>
|
|
21
62
|
<span class="entity-badge badge-package">Package</span>
|
|
22
63
|
</div>
|
|
23
64
|
|
|
24
|
-
<div class="entity-metadata" v-if="pkg.
|
|
65
|
+
<div class="entity-metadata" v-if="pkg.stereotypeRefs.length">
|
|
25
66
|
<div class="metadata-item">
|
|
26
67
|
<span class="metadata-label">Stereotypes</span>
|
|
27
68
|
<span class="metadata-value">
|
|
28
|
-
<span v-for="s in pkg.
|
|
69
|
+
<span v-for="s in pkg.stereotypeRefs" :key="s" class="stereotype-tag">«{{ s }}»</span>
|
|
29
70
|
</span>
|
|
30
71
|
</div>
|
|
31
72
|
</div>
|
|
32
73
|
|
|
33
|
-
<div class="entity-definition" v-if="pkg
|
|
34
|
-
<div class="definition-content">{{ pkg
|
|
74
|
+
<div class="entity-definition" v-if="definitionOf(pkg)">
|
|
75
|
+
<div class="definition-content">{{ definitionOf(pkg) }}</div>
|
|
35
76
|
</div>
|
|
36
77
|
|
|
37
|
-
<div class="section" v-if="
|
|
38
|
-
<h3 class="section-title">Diagrams <span class="section-count">{{
|
|
78
|
+
<div class="section" v-if="diagramIds.length">
|
|
79
|
+
<h3 class="section-title">Diagrams <span class="section-count">{{ diagramIds.length }}</span></h3>
|
|
39
80
|
<div class="item-list">
|
|
40
|
-
<div v-for="diagId in
|
|
81
|
+
<div v-for="diagId in diagramIds" :key="diagId"
|
|
41
82
|
class="list-item clickable-row" @click="ui.selectDiagram(diagId)">
|
|
42
83
|
<span class="list-item-icon">📊</span>
|
|
43
|
-
<span class="list-item-name">{{
|
|
84
|
+
<span class="list-item-name">{{ diagramName(diagId) }}</span>
|
|
44
85
|
</div>
|
|
45
86
|
</div>
|
|
46
87
|
</div>
|
|
47
88
|
|
|
48
|
-
<div class="section" v-if="
|
|
49
|
-
<h3 class="section-title">Sub-Packages <span class="section-count">{{
|
|
89
|
+
<div class="section" v-if="subPackageIds.length">
|
|
90
|
+
<h3 class="section-title">Sub-Packages <span class="section-count">{{ subPackageIds.length }}</span></h3>
|
|
50
91
|
<div class="item-list">
|
|
51
|
-
<div v-for="subId in
|
|
52
|
-
class="list-item clickable-row" @click="ui.selectPackage(subId)">
|
|
92
|
+
<div v-for="subId in subPackageIds" :key="subId"
|
|
93
|
+
class="list-item clickable-row" @click="ui.selectPackage(subId, pkgName(subId))">
|
|
53
94
|
<span class="list-item-icon">📁</span>
|
|
54
|
-
<span class="list-item-name">{{
|
|
55
|
-
<span class="tree-count">{{ data.
|
|
95
|
+
<span class="list-item-name">{{ pkgName(subId) }}</span>
|
|
96
|
+
<span class="tree-count">{{ data.nodesById[subId]?.classifierIds.length || 0 }}</span>
|
|
56
97
|
</div>
|
|
57
98
|
</div>
|
|
58
99
|
</div>
|
|
59
100
|
|
|
60
|
-
<div class="section" v-if="
|
|
61
|
-
<h3 class="section-title">Classes <span class="section-count">{{
|
|
101
|
+
<div class="section" v-if="classifierIds.length">
|
|
102
|
+
<h3 class="section-title">Classes <span class="section-count">{{ classifierIds.length }}</span></h3>
|
|
62
103
|
<div class="table-wrapper">
|
|
63
104
|
<table class="data-table">
|
|
64
105
|
<thead>
|
|
@@ -70,22 +111,22 @@ const pkg = computed(() =>
|
|
|
70
111
|
</tr>
|
|
71
112
|
</thead>
|
|
72
113
|
<tbody>
|
|
73
|
-
<tr v-for="clsId in
|
|
74
|
-
@click="ui.selectClass(clsId)">
|
|
75
|
-
<td>{{
|
|
76
|
-
<td><span class="entity-badge" :class="'badge-' + (
|
|
114
|
+
<tr v-for="clsId in classifierIds" :key="clsId" class="clickable-row"
|
|
115
|
+
@click="ui.selectClass(clsId, entryFor(clsId)?.name)">
|
|
116
|
+
<td>{{ entryFor(clsId)?.name || elementFor(clsId)?.name || clsId }}</td>
|
|
117
|
+
<td><span class="entity-badge" :class="'badge-' + (entryFor(clsId)?.kind || 'class')">{{ entryFor(clsId)?.kind || 'class' }}</span></td>
|
|
77
118
|
<td>
|
|
78
|
-
<span v-for="s in (
|
|
119
|
+
<span v-for="s in (elementFor(clsId)?.stereotypeRefs || [])" :key="s"
|
|
79
120
|
class="stereotype-tag">«{{ s }}»</span>
|
|
80
121
|
</td>
|
|
81
|
-
<td>{{
|
|
122
|
+
<td>{{ elementFor(clsId)?.properties?.length || 0 }}</td>
|
|
82
123
|
</tr>
|
|
83
124
|
</tbody>
|
|
84
125
|
</table>
|
|
85
126
|
</div>
|
|
86
127
|
</div>
|
|
87
128
|
|
|
88
|
-
<div class="empty-state" v-if="!
|
|
129
|
+
<div class="empty-state" v-if="!classifierIds.length && !subPackageIds.length && !diagramIds.length">
|
|
89
130
|
<p>This package is empty.</p>
|
|
90
131
|
</div>
|
|
91
132
|
</div>
|
|
@@ -2,32 +2,40 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import { useDataStore } from '../stores/dataStore'
|
|
4
4
|
import { useUiStore } from '../stores/uiStore'
|
|
5
|
-
import type { SpaPackageTreeNode } from '../types'
|
|
6
5
|
|
|
7
|
-
const props = defineProps<{
|
|
6
|
+
const props = defineProps<{ nodeId: string }>()
|
|
8
7
|
const data = useDataStore()
|
|
9
8
|
const ui = useUiStore()
|
|
10
9
|
|
|
11
|
-
const
|
|
10
|
+
const node = computed(() => data.nodesById[props.nodeId])
|
|
11
|
+
const isExpanded = computed(() => ui.expandedNodes.has(props.nodeId))
|
|
12
12
|
const isSelected = computed(() =>
|
|
13
|
-
ui.currentView === 'package' && ui.currentPackageId === props.
|
|
13
|
+
ui.currentView === 'package' && ui.currentPackageId === props.nodeId,
|
|
14
14
|
)
|
|
15
|
-
const children = computed(() =>
|
|
16
|
-
const
|
|
15
|
+
const children = computed(() => node.value?.childIds ?? [])
|
|
16
|
+
const childNodes = computed(() =>
|
|
17
|
+
children.value.map((id) => data.nodesById[id]).filter((n) => !!n),
|
|
18
|
+
)
|
|
19
|
+
const classEntries = computed(() =>
|
|
20
|
+
(node.value?.classifierIds ?? [])
|
|
21
|
+
.map((id) => data.entriesById[id])
|
|
22
|
+
.filter((e) => !!e),
|
|
23
|
+
)
|
|
24
|
+
const classCount = computed(() => node.value?.classifierIds.length ?? 0)
|
|
17
25
|
|
|
18
26
|
function toggle() {
|
|
19
|
-
ui.toggleNode(props.
|
|
27
|
+
ui.toggleNode(props.nodeId)
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
function select() {
|
|
23
|
-
ui.selectPackage(props.
|
|
31
|
+
ui.selectPackage(props.nodeId, node.value?.name)
|
|
24
32
|
}
|
|
25
33
|
</script>
|
|
26
34
|
|
|
27
35
|
<template>
|
|
28
|
-
<div class="tree-node">
|
|
36
|
+
<div class="tree-node" v-if="node">
|
|
29
37
|
<div class="tree-node-content" :class="{ selected: isSelected }">
|
|
30
|
-
<button class="tree-toggle" v-if="children.length ||
|
|
38
|
+
<button class="tree-toggle" v-if="children.length || classEntries.length" @click.stop="toggle">
|
|
31
39
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" :class="{ expanded: isExpanded }">
|
|
32
40
|
<path d="M4 3l3 3-3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
33
41
|
</svg>
|
|
@@ -41,23 +49,20 @@ function select() {
|
|
|
41
49
|
</span>
|
|
42
50
|
|
|
43
51
|
<span class="tree-label-group" @click="select">
|
|
44
|
-
<span class="tree-stereotype" v-if="node.stereotypes && node.stereotypes.length">
|
|
45
|
-
<span v-for="s in node.stereotypes" :key="s">«{{ s }}»</span>
|
|
46
|
-
</span>
|
|
47
52
|
<span class="tree-label">{{ node.name }}</span>
|
|
48
53
|
</span>
|
|
49
54
|
|
|
50
|
-
<span class="tree-count" v-if="
|
|
55
|
+
<span class="tree-count" v-if="classCount">{{ classCount }}</span>
|
|
51
56
|
</div>
|
|
52
57
|
|
|
53
58
|
<div class="tree-children" v-if="isExpanded">
|
|
54
|
-
<PackageTreeNode v-for="child in
|
|
59
|
+
<PackageTreeNode v-for="child in childNodes" :key="child.id" :node-id="child.id" />
|
|
55
60
|
|
|
56
|
-
<div v-for="
|
|
57
|
-
class="tree-item" :class="{ selected: ui.currentClassId ===
|
|
58
|
-
@click="ui.selectClass(
|
|
59
|
-
<span class="badge badge-class">{{
|
|
60
|
-
<span class="tree-item-label">{{
|
|
61
|
+
<div v-for="entry in classEntries" :key="entry.id"
|
|
62
|
+
class="tree-item" :class="{ selected: ui.currentClassId === entry.id }"
|
|
63
|
+
@click="ui.selectClass(entry.id, entry.name)">
|
|
64
|
+
<span class="badge badge-class">{{ entry.kind.charAt(0).toUpperCase() }}</span>
|
|
65
|
+
<span class="tree-item-label">{{ entry.name }}</span>
|
|
61
66
|
</div>
|
|
62
67
|
</div>
|
|
63
68
|
</div>
|
|
@@ -6,14 +6,26 @@ import { useUiStore } from '../stores/uiStore'
|
|
|
6
6
|
const data = useDataStore()
|
|
7
7
|
const ui = useUiStore()
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const resultsByKind = computed(() => {
|
|
10
10
|
const groups: Record<string, typeof data.searchEntries> = {}
|
|
11
11
|
for (const entry of data.searchEntries) {
|
|
12
|
-
|
|
13
|
-
;(groups[type] ??= []).push(entry)
|
|
12
|
+
;(groups[entry.kind] ??= []).push(entry)
|
|
14
13
|
}
|
|
15
14
|
return groups
|
|
16
15
|
})
|
|
16
|
+
|
|
17
|
+
function isClickable(kind: string): boolean {
|
|
18
|
+
return kind !== 'property'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function open(entry: { id: string; kind: string; name: string }) {
|
|
22
|
+
if (!isClickable(entry.kind)) return
|
|
23
|
+
if (entry.kind === 'package') {
|
|
24
|
+
ui.selectPackage(entry.id, entry.name)
|
|
25
|
+
} else {
|
|
26
|
+
ui.selectClass(entry.id, entry.name)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
17
29
|
</script>
|
|
18
30
|
|
|
19
31
|
<template>
|
|
@@ -23,19 +35,20 @@ const resultsByType = computed(() => {
|
|
|
23
35
|
<h2 class="entity-name">Search Results</h2>
|
|
24
36
|
</div>
|
|
25
37
|
</div>
|
|
26
|
-
<div v-for="(entries,
|
|
27
|
-
<h3 class="section-title">{{
|
|
38
|
+
<div v-for="(entries, kind) in resultsByKind" :key="kind" class="section">
|
|
39
|
+
<h3 class="section-title">{{ kind }}s <span class="section-count">{{ entries.length }}</span></h3>
|
|
28
40
|
<div class="table-wrapper">
|
|
29
41
|
<table class="data-table">
|
|
30
42
|
<thead>
|
|
31
43
|
<tr><th>Name</th><th>Package</th><th>Type</th></tr>
|
|
32
44
|
</thead>
|
|
33
45
|
<tbody>
|
|
34
|
-
<tr v-for="entry in entries" :key="entry.id" class="
|
|
35
|
-
|
|
46
|
+
<tr v-for="entry in entries" :key="entry.id" class="data-row"
|
|
47
|
+
:class="{ 'clickable-row': isClickable(entry.kind) }"
|
|
48
|
+
@click="open(entry)">
|
|
36
49
|
<td>{{ entry.name }}</td>
|
|
37
50
|
<td>{{ entry.package }}</td>
|
|
38
|
-
<td><span class="entity-badge" :class="'badge-' +
|
|
51
|
+
<td><span class="entity-badge" :class="'badge-' + entry.kind">{{ entry.kind }}</span></td>
|
|
39
52
|
</tr>
|
|
40
53
|
</tbody>
|
|
41
54
|
</table>
|
|
@@ -48,7 +48,7 @@ function formatDate(isoString?: string): string {
|
|
|
48
48
|
<h1 class="welcome-title">{{ data.metadata?.title || 'UML Model Browser' }}</h1>
|
|
49
49
|
<p class="welcome-subtitle">Explore UML model packages, classes, and associations</p>
|
|
50
50
|
|
|
51
|
-
<div class="welcome-stats" v-if="data.metadata">
|
|
51
|
+
<div class="welcome-stats" v-if="data.metadata?.statistics">
|
|
52
52
|
<div class="welcome-stat">
|
|
53
53
|
<div class="welcome-stat-value">{{ data.metadata.statistics.packages }}</div>
|
|
54
54
|
<div class="welcome-stat-label">Packages</div>
|
|
@@ -68,7 +68,7 @@ function formatDate(isoString?: string): string {
|
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
70
|
<div class="welcome-meta" v-if="data.metadata">
|
|
71
|
-
<div class="meta-item">
|
|
71
|
+
<div class="meta-item" v-if="data.metadata.generator">
|
|
72
72
|
<span class="meta-label">Generator</span>
|
|
73
73
|
<span class="meta-value">{{ data.metadata.generator }}</span>
|
|
74
74
|
</div>
|
|
@@ -1,79 +1,173 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import type {
|
|
3
3
|
SpaData,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
SpaMetadata,
|
|
5
|
+
SpaPackageTreeNode,
|
|
6
|
+
SpaPackageTree,
|
|
6
7
|
SpaSearchEntry,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SpaAssociation,
|
|
11
|
-
SpaOperation,
|
|
12
|
-
SpaDiagram,
|
|
8
|
+
SpaSearchIndex,
|
|
9
|
+
SpaShard,
|
|
10
|
+
SpaSkeletonEntry,
|
|
13
11
|
} from '../types'
|
|
14
12
|
|
|
13
|
+
const SHARD_DIRS: Record<string, string> = {
|
|
14
|
+
class: 'classes',
|
|
15
|
+
enumeration: 'enumerations',
|
|
16
|
+
data_type: 'data_types',
|
|
17
|
+
primitive_type: 'primitive_types',
|
|
18
|
+
interface: 'interfaces',
|
|
19
|
+
package: 'packages',
|
|
20
|
+
diagram: 'diagrams',
|
|
21
|
+
property: 'properties',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FETCH_CONCURRENCY = 12
|
|
25
|
+
|
|
26
|
+
const inflight = new Map<string, Promise<SpaShard | null>>()
|
|
27
|
+
|
|
15
28
|
export const useDataStore = defineStore('data', {
|
|
16
29
|
state: () => ({
|
|
17
|
-
metadata: null as
|
|
18
|
-
packageTree: null as
|
|
19
|
-
|
|
20
|
-
classes: {} as Record<string, SpaClass>,
|
|
21
|
-
attributes: {} as Record<string, SpaAttribute>,
|
|
22
|
-
associations: {} as Record<string, SpaAssociation>,
|
|
23
|
-
operations: {} as Record<string, SpaOperation>,
|
|
24
|
-
diagrams: {} as Record<string, SpaDiagram>,
|
|
30
|
+
metadata: null as SpaMetadata | null,
|
|
31
|
+
packageTree: null as SpaPackageTree | null,
|
|
32
|
+
entries: [] as SpaSkeletonEntry[],
|
|
25
33
|
searchEntries: [] as SpaSearchEntry[],
|
|
34
|
+
elements: {} as Record<string, SpaShard>,
|
|
35
|
+
shardBase: '',
|
|
26
36
|
loaded: false,
|
|
27
37
|
}),
|
|
28
38
|
|
|
29
39
|
getters: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
entriesById(): Record<string, SpaSkeletonEntry> {
|
|
41
|
+
const map: Record<string, SpaSkeletonEntry> = {}
|
|
42
|
+
for (const e of this.entries) map[e.id] = e
|
|
43
|
+
return map
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
nodesById(): Record<string, SpaPackageTreeNode> {
|
|
47
|
+
const map: Record<string, SpaPackageTreeNode> = {}
|
|
48
|
+
if (this.packageTree) {
|
|
49
|
+
for (const n of this.packageTree.nodes) map[n.id] = n
|
|
50
|
+
}
|
|
51
|
+
return map
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
rootNodes(): SpaPackageTreeNode[] {
|
|
55
|
+
const tree = this.packageTree
|
|
56
|
+
if (!tree) return []
|
|
57
|
+
const map = this.nodesById
|
|
58
|
+
return tree.rootIds.map((id) => map[id]).filter((n) => !!n)
|
|
59
|
+
},
|
|
41
60
|
},
|
|
42
61
|
|
|
43
62
|
actions: {
|
|
44
63
|
loadFromEmbedded() {
|
|
45
64
|
const win = window as any
|
|
46
|
-
|
|
65
|
+
const raw = win.__SPA_DATA__ as SpaData
|
|
66
|
+
if (!raw) {
|
|
47
67
|
throw new Error('No embedded SPA data found in window.__SPA_DATA__')
|
|
48
68
|
}
|
|
49
|
-
this.
|
|
69
|
+
this.metadata = raw.metadata
|
|
70
|
+
this.packageTree = raw.packageTree
|
|
71
|
+
this.entries = raw.entries || []
|
|
72
|
+
this.searchEntries = raw.searchIndex?.entries || []
|
|
73
|
+
this.hydrateShards(raw.shards || [])
|
|
74
|
+
this.loaded = true
|
|
50
75
|
},
|
|
51
76
|
|
|
52
|
-
async
|
|
53
|
-
const [
|
|
54
|
-
fetch(
|
|
77
|
+
async loadFromSharded(skeletonUrl: string, searchUrl: string, shardBase: string) {
|
|
78
|
+
const [skeletonRes, searchRes] = await Promise.all([
|
|
79
|
+
fetch(skeletonUrl),
|
|
55
80
|
fetch(searchUrl),
|
|
56
81
|
])
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
if (!skeletonRes.ok) {
|
|
83
|
+
throw new Error(`Failed to load ${skeletonUrl}: ${skeletonRes.status}`)
|
|
84
|
+
}
|
|
85
|
+
if (!searchRes.ok) {
|
|
86
|
+
throw new Error(`Failed to load ${searchUrl}: ${searchRes.status}`)
|
|
87
|
+
}
|
|
88
|
+
const skeleton = await skeletonRes.json()
|
|
89
|
+
const searchIndex: SpaSearchIndex = await searchRes.json()
|
|
90
|
+
this.metadata = skeleton.metadata
|
|
91
|
+
this.packageTree = skeleton.packageTree
|
|
92
|
+
this.entries = skeleton.entries || []
|
|
93
|
+
this.searchEntries = searchIndex.entries || []
|
|
94
|
+
this.shardBase = shardBase || ''
|
|
95
|
+
this.loaded = true
|
|
60
96
|
},
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
hydrateShards(shards: SpaShard[]) {
|
|
99
|
+
for (const shard of shards) this.elements[shard.id] = shard
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
elementFor(id: string): SpaShard | null {
|
|
103
|
+
return this.elements[id] || null
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
kindFor(id: string): string | null {
|
|
107
|
+
const entry = this.entriesById[id]
|
|
108
|
+
if (entry) return entry.kind
|
|
109
|
+
if (this.nodesById[id]) return 'package'
|
|
110
|
+
if (this.packageTree?.nodes.some((n) => n.diagramIds.includes(id))) {
|
|
111
|
+
return 'diagram'
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
shardUrlFor(id: string, kind: string): string {
|
|
117
|
+
const dir = SHARD_DIRS[kind] || `${kind}s`
|
|
118
|
+
return `${this.shardBase}${dir}/${id}.json`
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async ensureElement(id: string): Promise<SpaShard | null> {
|
|
122
|
+
const cached = this.elements[id]
|
|
123
|
+
if (cached) return cached
|
|
124
|
+
|
|
125
|
+
const kind = this.kindFor(id)
|
|
126
|
+
if (!kind) return null
|
|
127
|
+
|
|
128
|
+
const existing = inflight.get(id)
|
|
129
|
+
if (existing) return existing
|
|
130
|
+
|
|
131
|
+
const url = this.shardUrlFor(id, kind)
|
|
132
|
+
const pending = fetch(url)
|
|
133
|
+
.then(async (res) => {
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw new Error(`Failed to load ${url}: ${res.status}`)
|
|
136
|
+
}
|
|
137
|
+
const shard: SpaShard = await res.json()
|
|
138
|
+
this.elements[shard.id] = shard
|
|
139
|
+
return shard
|
|
140
|
+
})
|
|
141
|
+
.finally(() => {
|
|
142
|
+
inflight.delete(id)
|
|
143
|
+
})
|
|
144
|
+
inflight.set(id, pending)
|
|
145
|
+
return pending
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async ensureElements(ids: string[]): Promise<void> {
|
|
149
|
+
const queue = [...ids]
|
|
150
|
+
const workers = Array.from(
|
|
151
|
+
{ length: Math.min(FETCH_CONCURRENCY, queue.length) },
|
|
152
|
+
async () => {
|
|
153
|
+
for (;;) {
|
|
154
|
+
const id = queue.shift()
|
|
155
|
+
if (!id) return
|
|
156
|
+
try {
|
|
157
|
+
await this.ensureElement(id)
|
|
158
|
+
} catch {
|
|
159
|
+
// leave the element missing; views render what they have
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
await Promise.all(workers)
|
|
73
165
|
},
|
|
74
166
|
|
|
75
|
-
|
|
76
|
-
return
|
|
167
|
+
classifierEntryByName(name: string): SpaSkeletonEntry | undefined {
|
|
168
|
+
return this.entries.find(
|
|
169
|
+
(e) => e.qualifiedName === name || e.name === name,
|
|
170
|
+
)
|
|
77
171
|
},
|
|
78
172
|
},
|
|
79
173
|
})
|
|
@@ -132,16 +132,15 @@ export const useUiStore = defineStore('ui', {
|
|
|
132
132
|
|
|
133
133
|
if (hash.startsWith('/package/')) {
|
|
134
134
|
const id = hash.slice('/package/'.length)
|
|
135
|
-
const pkg = data.
|
|
135
|
+
const pkg = data.nodesById[id]
|
|
136
136
|
this.selectPackage(id, pkg?.name)
|
|
137
137
|
} else if (hash.startsWith('/class/')) {
|
|
138
138
|
const id = hash.slice('/class/'.length)
|
|
139
|
-
const cls = data.
|
|
139
|
+
const cls = data.entriesById[id]
|
|
140
140
|
this.selectClass(id, cls?.name)
|
|
141
141
|
} else if (hash.startsWith('/diagram/')) {
|
|
142
142
|
const id = hash.slice('/diagram/'.length)
|
|
143
|
-
|
|
144
|
-
this.selectDiagram(id, diag?.name)
|
|
143
|
+
this.selectDiagram(id)
|
|
145
144
|
}
|
|
146
145
|
},
|
|
147
146
|
|