ea 0.6.2 → 0.6.4
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 +33 -0
- 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/spa/output/single_file_strategy.rb +1 -1
- data/lib/ea/spa/projector.rb +108 -6
- data/lib/ea/version.rb +1 -1
- data/lib/ea/xmi/liquid_drops/data_type_drop.rb +31 -0
- data/lib/ea/xmi/parser.rb +5 -0
- metadata +4 -10
data/frontend/src/App.vue
CHANGED
|
@@ -17,8 +17,12 @@ onMounted(async () => {
|
|
|
17
17
|
const win = window as any
|
|
18
18
|
if (win.__SPA_DATA__) {
|
|
19
19
|
data.loadFromEmbedded()
|
|
20
|
-
} else if (win.
|
|
21
|
-
await data.
|
|
20
|
+
} else if (win.__SPA_SKELETON_URL__) {
|
|
21
|
+
await data.loadFromSharded(
|
|
22
|
+
win.__SPA_SKELETON_URL__,
|
|
23
|
+
win.__SPA_SEARCH_URL__,
|
|
24
|
+
win.__SPA_SHARD_BASE__,
|
|
25
|
+
)
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
ui.navigateToHash()
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, onMounted, onUnmounted } from 'vue'
|
|
2
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
3
3
|
import { useUiStore } from '../stores/uiStore'
|
|
4
|
+
import { useDataStore } from '../stores/dataStore'
|
|
4
5
|
|
|
5
6
|
const ui = useUiStore()
|
|
7
|
+
const data = useDataStore()
|
|
6
8
|
const searchInput = ref<HTMLInputElement | null>(null)
|
|
7
9
|
const showSearchModal = ref(false)
|
|
8
10
|
const searchQuery = ref('')
|
|
9
|
-
const searchResults = ref<any[]>([])
|
|
10
11
|
const selectedIndex = ref(0)
|
|
11
12
|
|
|
13
|
+
const searchResults = computed(() => {
|
|
14
|
+
const q = searchQuery.value.trim().toLowerCase()
|
|
15
|
+
if (!q) return []
|
|
16
|
+
return data.searchEntries
|
|
17
|
+
.filter((e) =>
|
|
18
|
+
e.name.toLowerCase().includes(q) ||
|
|
19
|
+
(e.qualifiedName || '').toLowerCase().includes(q) ||
|
|
20
|
+
(e.content || '').toLowerCase().includes(q))
|
|
21
|
+
.sort((a, b) => (b.boost || 1) - (a.boost || 1))
|
|
22
|
+
.slice(0, 20)
|
|
23
|
+
})
|
|
24
|
+
|
|
12
25
|
function openSearch() {
|
|
13
26
|
showSearchModal.value = true
|
|
14
27
|
setTimeout(() => searchInput.value?.focus(), 50)
|
|
@@ -17,7 +30,20 @@ function openSearch() {
|
|
|
17
30
|
function closeSearch() {
|
|
18
31
|
showSearchModal.value = false
|
|
19
32
|
searchQuery.value = ''
|
|
20
|
-
|
|
33
|
+
selectedIndex.value = 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onSearchKeydown(e: KeyboardEvent) {
|
|
37
|
+
if (e.key === 'ArrowDown') {
|
|
38
|
+
e.preventDefault()
|
|
39
|
+
selectedIndex.value = Math.min(selectedIndex.value + 1, searchResults.value.length - 1)
|
|
40
|
+
} else if (e.key === 'ArrowUp') {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
selectedIndex.value = Math.max(selectedIndex.value - 1, 0)
|
|
43
|
+
} else if (e.key === 'Enter') {
|
|
44
|
+
const result = searchResults.value[selectedIndex.value]
|
|
45
|
+
if (result) navigateTo(result.id, result.kind)
|
|
46
|
+
}
|
|
21
47
|
}
|
|
22
48
|
|
|
23
49
|
function handleKeydown(e: KeyboardEvent) {
|
|
@@ -35,8 +61,9 @@ function handleKeydown(e: KeyboardEvent) {
|
|
|
35
61
|
}
|
|
36
62
|
|
|
37
63
|
function navigateTo(id: string, type: string) {
|
|
38
|
-
if (type === '
|
|
39
|
-
else if (type === '
|
|
64
|
+
if (type === 'package') ui.selectPackage(id, data.entriesById[id]?.name)
|
|
65
|
+
else if (type === 'diagram') ui.selectDiagram(id, data.entriesById[id]?.name)
|
|
66
|
+
else if (type !== 'property') ui.selectClass(id, data.entriesById[id]?.name)
|
|
40
67
|
closeSearch()
|
|
41
68
|
}
|
|
42
69
|
|
|
@@ -97,12 +124,12 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
|
|
|
97
124
|
<div class="search-modal">
|
|
98
125
|
<div class="search-input-wrapper">
|
|
99
126
|
<input ref="searchInput" v-model="searchQuery" placeholder="Search classes, packages, attributes..."
|
|
100
|
-
@keydown.escape="closeSearch" autofocus />
|
|
127
|
+
@keydown.escape="closeSearch" @keydown="onSearchKeydown" autofocus />
|
|
101
128
|
</div>
|
|
102
129
|
<div class="search-results" v-if="searchResults.length">
|
|
103
130
|
<div v-for="(result, i) in searchResults" :key="result.id"
|
|
104
131
|
class="search-result" :class="{ focused: i === selectedIndex }"
|
|
105
|
-
@click="navigateTo(result.
|
|
132
|
+
@click="navigateTo(result.id, result.kind)">
|
|
106
133
|
<span class="result-icon">
|
|
107
134
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
108
135
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.2"/>
|
|
@@ -110,7 +137,7 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
|
|
|
110
137
|
</span>
|
|
111
138
|
<div class="result-content">
|
|
112
139
|
<span class="result-name">{{ result.name }}</span>
|
|
113
|
-
<span class="result-path">{{ result.
|
|
140
|
+
<span class="result-path">{{ result.kind }}</span>
|
|
114
141
|
</div>
|
|
115
142
|
</div>
|
|
116
143
|
</div>
|
|
@@ -78,13 +78,13 @@ function formatDate(isoString?: string): string {
|
|
|
78
78
|
</div>
|
|
79
79
|
</div>
|
|
80
80
|
<div class="package-tree" v-if="data.packageTree">
|
|
81
|
-
<PackageTreeNode
|
|
81
|
+
<PackageTreeNode v-for="id in data.packageTree.rootIds" :key="id" :node-id="id" />
|
|
82
82
|
</div>
|
|
83
83
|
<div v-else class="loading">Loading...</div>
|
|
84
84
|
</div>
|
|
85
85
|
|
|
86
86
|
<!-- Stats -->
|
|
87
|
-
<div class="sidebar-section stats-section" v-if="data.metadata">
|
|
87
|
+
<div class="sidebar-section stats-section" v-if="data.metadata?.statistics">
|
|
88
88
|
<div class="stats-grid">
|
|
89
89
|
<div class="stat-item">
|
|
90
90
|
<span class="stat-value">{{ data.metadata.statistics.packages }}</span>
|
|
@@ -1,39 +1,67 @@
|
|
|
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 { SpaClassifierPayload, SpaGeneralizationStub } from '../types'
|
|
5
6
|
|
|
6
7
|
const data = useDataStore()
|
|
7
8
|
const ui = useUiStore()
|
|
8
9
|
|
|
10
|
+
const clsId = computed(() => ui.currentClassId)
|
|
9
11
|
const cls = computed(() =>
|
|
10
|
-
|
|
12
|
+
clsId.value ? (data.elementFor(clsId.value)?.payload as SpaClassifierPayload | undefined) ?? null : null,
|
|
11
13
|
)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
watch(
|
|
16
|
+
clsId,
|
|
17
|
+
async (id) => {
|
|
18
|
+
if (!id) return
|
|
19
|
+
const shard = await data.ensureElement(id)
|
|
20
|
+
if (!shard) return
|
|
21
|
+
const parents = (shard.payload.generalizations || []).map(
|
|
22
|
+
(g: SpaGeneralizationStub) => g.targetId,
|
|
23
|
+
)
|
|
24
|
+
await data.ensureElements(parents)
|
|
25
|
+
},
|
|
26
|
+
{ immediate: true },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const definition = computed(
|
|
30
|
+
() => cls.value?.annotations?.find((a) => a.kind === 'documentation')?.body || '',
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const BASIC_TYPES = new Set([
|
|
34
|
+
'String', 'Integer', 'Boolean', 'Real', 'UnlimitedNatural',
|
|
35
|
+
'DateTime', 'URI', 'Any', 'Object',
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
function resolveTypeEntry(typeName?: string) {
|
|
39
|
+
if (!typeName) return null
|
|
40
|
+
return data.classifierEntryByName(typeName)
|
|
16
41
|
}
|
|
17
42
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
'String', 'Integer', 'Boolean', 'Real', 'UnlimitedNatural',
|
|
21
|
-
'DateTime', 'URI', 'Any', 'Object',
|
|
22
|
-
])
|
|
23
|
-
if (basicTypes.has(typeName)) return { isBasic: true, classId: null }
|
|
24
|
-
const found = data.findClassByName(typeName)
|
|
25
|
-
return { isBasic: false, classId: found ? found.id : null }
|
|
43
|
+
function isBasicType(typeName?: string): boolean {
|
|
44
|
+
return !!typeName && BASIC_TYPES.has(typeName)
|
|
26
45
|
}
|
|
27
46
|
|
|
28
|
-
function
|
|
29
|
-
if (!
|
|
30
|
-
|
|
31
|
-
return assoc.target
|
|
47
|
+
function entryName(id?: string): string {
|
|
48
|
+
if (!id) return ''
|
|
49
|
+
return data.entriesById[id]?.name || id
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
function
|
|
35
|
-
if (
|
|
36
|
-
return
|
|
52
|
+
function formatCardinality(m?: [number?, number?]): string {
|
|
53
|
+
if (!m) return ''
|
|
54
|
+
return `${m[0] ?? 0}..${m[1] ?? '*'}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatParameters(op: { parameters: { name?: string; typeName?: string }[] }): string {
|
|
58
|
+
return op.parameters
|
|
59
|
+
.map((p) => `${p.name || ''}: ${p.typeName || '?'}`)
|
|
60
|
+
.join(', ')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function badgeType(c: SpaClassifierPayload): string {
|
|
64
|
+
return (c.modelKind || 'class').toUpperCase()
|
|
37
65
|
}
|
|
38
66
|
</script>
|
|
39
67
|
|
|
@@ -42,51 +70,52 @@ function badgeType(c: any): string {
|
|
|
42
70
|
<div class="entity-header">
|
|
43
71
|
<div class="entity-title">
|
|
44
72
|
<h2 class="entity-name">{{ cls.qualifiedName }}</h2>
|
|
45
|
-
<div class="entity-subtitle" v-if="cls.
|
|
46
|
-
<a href="#" class="link-button"
|
|
47
|
-
|
|
73
|
+
<div class="entity-subtitle" v-if="cls.packageId">
|
|
74
|
+
<a href="#" class="link-button"
|
|
75
|
+
@click.prevent="ui.selectPackage(cls.packageId, data.nodesById[cls.packageId]?.name)">
|
|
76
|
+
{{ data.nodesById[cls.packageId]?.name || cls.packageName || cls.packageId }}
|
|
48
77
|
</a>
|
|
49
78
|
</div>
|
|
50
79
|
</div>
|
|
51
|
-
<span class="entity-badge" :class="'badge-' +
|
|
80
|
+
<span class="entity-badge" :class="'badge-' + cls.modelKind">{{ badgeType(cls) }}</span>
|
|
52
81
|
<span class="entity-badge badge-abstract" v-if="cls.isAbstract">abstract</span>
|
|
53
82
|
</div>
|
|
54
83
|
|
|
55
|
-
<div class="entity-metadata" v-if="cls.
|
|
84
|
+
<div class="entity-metadata" v-if="cls.stereotypeRefs.length">
|
|
56
85
|
<div class="metadata-item">
|
|
57
86
|
<span class="metadata-label">Stereotypes</span>
|
|
58
87
|
<span class="metadata-value">
|
|
59
|
-
<span v-for="s in cls.
|
|
88
|
+
<span v-for="s in cls.stereotypeRefs" :key="s" class="stereotype-tag">«{{ s }}»</span>
|
|
60
89
|
</span>
|
|
61
90
|
</div>
|
|
62
91
|
</div>
|
|
63
92
|
|
|
64
|
-
<div class="entity-definition" v-if="
|
|
65
|
-
<div class="definition-content">{{
|
|
93
|
+
<div class="entity-definition" v-if="definition">
|
|
94
|
+
<div class="definition-content">{{ definition }}</div>
|
|
66
95
|
</div>
|
|
67
96
|
|
|
68
97
|
<!-- Inheritance -->
|
|
69
|
-
<div class="section" v-if="cls.generalizations
|
|
98
|
+
<div class="section" v-if="cls.generalizations?.length || cls.specializations?.length">
|
|
70
99
|
<h3 class="section-title">Inheritance</h3>
|
|
71
|
-
<div v-if="cls.generalizations
|
|
100
|
+
<div v-if="cls.generalizations?.length" class="inheritance-group">
|
|
72
101
|
<div class="inheritance-header">↑ Extends</div>
|
|
73
|
-
<div v-for="
|
|
74
|
-
@click="ui.selectClass(
|
|
75
|
-
<span class="list-item-name">{{
|
|
102
|
+
<div v-for="g in cls.generalizations" :key="g.id" class="list-item clickable-row"
|
|
103
|
+
@click="ui.selectClass(g.targetId, entryName(g.targetId))">
|
|
104
|
+
<span class="list-item-name">{{ entryName(g.targetId) }}</span>
|
|
76
105
|
</div>
|
|
77
106
|
</div>
|
|
78
|
-
<div v-if="cls.specializations
|
|
107
|
+
<div v-if="cls.specializations?.length" class="inheritance-group">
|
|
79
108
|
<div class="inheritance-header">↓ Extended by</div>
|
|
80
|
-
<div v-for="
|
|
81
|
-
@click="ui.selectClass(
|
|
82
|
-
<span class="list-item-name">{{
|
|
109
|
+
<div v-for="g in cls.specializations" :key="g.id" class="list-item clickable-row"
|
|
110
|
+
@click="ui.selectClass(g.targetId, entryName(g.targetId))">
|
|
111
|
+
<span class="list-item-name">{{ entryName(g.targetId) }}</span>
|
|
83
112
|
</div>
|
|
84
113
|
</div>
|
|
85
114
|
</div>
|
|
86
115
|
|
|
87
116
|
<!-- Attributes -->
|
|
88
|
-
<div class="section" v-if="cls.
|
|
89
|
-
<h3 class="section-title">Attributes <span class="section-count">{{ cls.
|
|
117
|
+
<div class="section" v-if="cls.properties.length">
|
|
118
|
+
<h3 class="section-title">Attributes <span class="section-count">{{ cls.properties.length }}</span></h3>
|
|
90
119
|
<div class="table-wrapper">
|
|
91
120
|
<table class="data-table">
|
|
92
121
|
<thead>
|
|
@@ -99,33 +128,30 @@ function badgeType(c: any): string {
|
|
|
99
128
|
</tr>
|
|
100
129
|
</thead>
|
|
101
130
|
<tbody>
|
|
102
|
-
<tr v-for="
|
|
103
|
-
<td>{{
|
|
131
|
+
<tr v-for="attr in cls.properties" :key="attr.id" class="clickable-row">
|
|
132
|
+
<td>{{ attr.name }}</td>
|
|
104
133
|
<td>
|
|
105
|
-
<template v-if="
|
|
106
|
-
<a v-if="
|
|
134
|
+
<template v-if="attr.typeName">
|
|
135
|
+
<a v-if="resolveTypeEntry(attr.typeName)"
|
|
107
136
|
href="#" class="type-link"
|
|
108
|
-
@click.prevent="ui.selectClass(
|
|
109
|
-
{{
|
|
137
|
+
@click.prevent="ui.selectClass(resolveTypeEntry(attr.typeName)!.id, resolveTypeEntry(attr.typeName)!.name)">
|
|
138
|
+
{{ attr.typeName }}
|
|
110
139
|
</a>
|
|
111
|
-
<span v-else-if="
|
|
112
|
-
|
|
113
|
-
{{ data.getAttributeById(attrId)?.type }}
|
|
140
|
+
<span v-else-if="isBasicType(attr.typeName)" class="uml-basic-type">
|
|
141
|
+
{{ attr.typeName }}
|
|
114
142
|
</span>
|
|
115
|
-
<span v-else class="type-unresolved">{{
|
|
143
|
+
<span v-else class="type-unresolved">{{ attr.typeName }}</span>
|
|
116
144
|
</template>
|
|
117
145
|
</td>
|
|
118
146
|
<td>
|
|
119
|
-
<span v-if="data.
|
|
120
|
-
|
|
121
|
-
:data-visibility="data.getAttributeById(attrId)?.visibility">
|
|
122
|
-
{{ data.getAttributeById(attrId)?.visibility }}
|
|
147
|
+
<span v-if="attr.visibility" class="visibility-badge" :data-visibility="attr.visibility">
|
|
148
|
+
{{ attr.visibility }}
|
|
123
149
|
</span>
|
|
124
150
|
</td>
|
|
125
|
-
<td>{{ formatCardinality(
|
|
151
|
+
<td>{{ formatCardinality([attr.multiplicityLower, attr.multiplicityUpper]) }}</td>
|
|
126
152
|
<td>
|
|
127
|
-
<span v-if="
|
|
128
|
-
<span v-if="
|
|
153
|
+
<span v-if="attr.isReadonly" class="modifier-badge">readonly</span>
|
|
154
|
+
<span v-if="attr.isDerived" class="modifier-badge">derived</span>
|
|
129
155
|
</td>
|
|
130
156
|
</tr>
|
|
131
157
|
</tbody>
|
|
@@ -133,28 +159,6 @@ function badgeType(c: any): string {
|
|
|
133
159
|
</div>
|
|
134
160
|
</div>
|
|
135
161
|
|
|
136
|
-
<!-- Inherited Attributes -->
|
|
137
|
-
<div class="section" v-if="cls.inheritedAttributes.length">
|
|
138
|
-
<h3 class="section-title">Inherited Attributes</h3>
|
|
139
|
-
<div v-for="ia in cls.inheritedAttributes" :key="ia.attributeId" class="inheritance-group">
|
|
140
|
-
<div class="inheritance-header">From {{ ia.inheritedFromName }}</div>
|
|
141
|
-
<div class="table-wrapper">
|
|
142
|
-
<table class="data-table inherited-table">
|
|
143
|
-
<thead>
|
|
144
|
-
<tr><th>Name</th><th>Type</th><th>Cardinality</th></tr>
|
|
145
|
-
</thead>
|
|
146
|
-
<tbody>
|
|
147
|
-
<tr>
|
|
148
|
-
<td>{{ ia.attribute.name }}</td>
|
|
149
|
-
<td>{{ ia.attribute.type }}</td>
|
|
150
|
-
<td>{{ formatCardinality(ia.attribute.cardinality) }}</td>
|
|
151
|
-
</tr>
|
|
152
|
-
</tbody>
|
|
153
|
-
</table>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
162
|
<!-- Operations -->
|
|
159
163
|
<div class="section" v-if="cls.operations.length">
|
|
160
164
|
<h3 class="section-title">Operations <span class="section-count">{{ cls.operations.length }}</span></h3>
|
|
@@ -164,25 +168,21 @@ function badgeType(c: any): string {
|
|
|
164
168
|
<tr><th>Name</th><th>Return</th><th>Visibility</th><th>Modifiers</th></tr>
|
|
165
169
|
</thead>
|
|
166
170
|
<tbody>
|
|
167
|
-
<tr v-for="
|
|
171
|
+
<tr v-for="op in cls.operations" :key="op.id" class="clickable-row">
|
|
168
172
|
<td>
|
|
169
|
-
{{
|
|
170
|
-
<span v-if="
|
|
171
|
-
{{ data.getOperationById(opId)?.parameters.map(p => `${p.name}: ${p.type || '?'}`).join(', ') }}
|
|
172
|
-
</span>
|
|
173
|
+
{{ op.name }}(
|
|
174
|
+
<span v-if="op.parameters.length">{{ formatParameters(op) }}</span>
|
|
173
175
|
)
|
|
174
176
|
</td>
|
|
175
|
-
<td>{{
|
|
177
|
+
<td>{{ op.returnTypeName || 'void' }}</td>
|
|
176
178
|
<td>
|
|
177
|
-
<span v-if="data.
|
|
178
|
-
|
|
179
|
-
:data-visibility="data.getOperationById(opId)?.visibility">
|
|
180
|
-
{{ data.getOperationById(opId)?.visibility }}
|
|
179
|
+
<span v-if="op.visibility" class="visibility-badge" :data-visibility="op.visibility">
|
|
180
|
+
{{ op.visibility }}
|
|
181
181
|
</span>
|
|
182
182
|
</td>
|
|
183
183
|
<td>
|
|
184
|
-
<span v-if="
|
|
185
|
-
<span v-if="
|
|
184
|
+
<span v-if="op.isStatic" class="modifier-badge">static</span>
|
|
185
|
+
<span v-if="op.isAbstract" class="modifier-badge">abstract</span>
|
|
186
186
|
</td>
|
|
187
187
|
</tr>
|
|
188
188
|
</tbody>
|
|
@@ -191,7 +191,7 @@ function badgeType(c: any): string {
|
|
|
191
191
|
</div>
|
|
192
192
|
|
|
193
193
|
<!-- Associations -->
|
|
194
|
-
<div class="section" v-if="cls.associations
|
|
194
|
+
<div class="section" v-if="cls.associations?.length">
|
|
195
195
|
<h3 class="section-title">Associations <span class="section-count">{{ cls.associations.length }}</span></h3>
|
|
196
196
|
<div class="table-wrapper">
|
|
197
197
|
<table class="data-table">
|
|
@@ -199,47 +199,30 @@ function badgeType(c: any): string {
|
|
|
199
199
|
<tr><th>Name</th><th>Target</th><th>Cardinality</th><th>Aggregation</th></tr>
|
|
200
200
|
</thead>
|
|
201
201
|
<tbody>
|
|
202
|
-
<tr v-for="
|
|
203
|
-
<td>{{
|
|
202
|
+
<tr v-for="assoc in cls.associations" :key="assoc.id">
|
|
203
|
+
<td>{{ assoc.name || assoc.thisEndRoleName || '' }}</td>
|
|
204
204
|
<td>
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (found) ui.selectClass(found.id)
|
|
211
|
-
}">
|
|
212
|
-
{{ associationTarget(data.getAssociationById(assocId))?.className }}
|
|
213
|
-
</a>
|
|
214
|
-
</template>
|
|
205
|
+
<a v-if="data.entriesById[assoc.otherEndId]" href="#" class="type-link"
|
|
206
|
+
@click.prevent="ui.selectClass(assoc.otherEndId, entryName(assoc.otherEndId))">
|
|
207
|
+
{{ entryName(assoc.otherEndId) }}
|
|
208
|
+
</a>
|
|
209
|
+
<span v-else>{{ assoc.otherEndId }}</span>
|
|
215
210
|
</td>
|
|
216
|
-
<td>{{ formatCardinality(
|
|
217
|
-
<td>{{
|
|
211
|
+
<td>{{ formatCardinality(assoc.otherEndMultiplicity) }}</td>
|
|
212
|
+
<td>{{ assoc.otherEndAggregation }}</td>
|
|
218
213
|
</tr>
|
|
219
214
|
</tbody>
|
|
220
215
|
</table>
|
|
221
216
|
</div>
|
|
222
217
|
</div>
|
|
223
218
|
|
|
224
|
-
<!-- Inherited Associations -->
|
|
225
|
-
<div class="section" v-if="cls.inheritedAssociations.length">
|
|
226
|
-
<h3 class="section-title">Inherited Associations</h3>
|
|
227
|
-
<div v-for="ia in cls.inheritedAssociations" :key="ia.associationId" class="inheritance-group">
|
|
228
|
-
<div class="inheritance-header">From {{ ia.inheritedFromName }}</div>
|
|
229
|
-
<div class="list-item">
|
|
230
|
-
<span class="list-item-name">{{ data.getAssociationById(ia.associationId)?.name }}</span>
|
|
231
|
-
<span class="list-item-meta">{{ ia.localRole }}</span>
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
</div>
|
|
235
|
-
|
|
236
219
|
<!-- Enum Literals -->
|
|
237
|
-
<div class="section" v-if="cls.literals
|
|
220
|
+
<div class="section" v-if="cls.literals?.length">
|
|
238
221
|
<h3 class="section-title">Literals <span class="section-count">{{ cls.literals.length }}</span></h3>
|
|
239
222
|
<div class="item-list">
|
|
240
|
-
<div v-for="lit in cls.literals" :key="lit.
|
|
223
|
+
<div v-for="lit in cls.literals" :key="lit.id" class="list-item">
|
|
241
224
|
<span class="list-item-name">{{ lit.name }}</span>
|
|
242
|
-
<span v-if="lit.
|
|
225
|
+
<span v-if="lit.value && lit.value !== lit.name" class="list-item-meta">{{ lit.value }}</span>
|
|
243
226
|
</div>
|
|
244
227
|
</div>
|
|
245
228
|
</div>
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import { useDataStore } from '../stores/dataStore'
|
|
4
4
|
import { useUiStore } from '../stores/uiStore'
|
|
5
|
+
import type { SpaDiagramPayload } from '../types'
|
|
5
6
|
|
|
6
7
|
const data = useDataStore()
|
|
7
8
|
const ui = useUiStore()
|
|
8
9
|
|
|
10
|
+
const diagId = computed(() => ui.currentDiagramId)
|
|
9
11
|
const diagram = computed(() =>
|
|
10
|
-
|
|
12
|
+
diagId.value ? (data.elementFor(diagId.value)?.payload as SpaDiagramPayload | undefined) ?? null : null,
|
|
11
13
|
)
|
|
12
14
|
|
|
15
|
+
watch(
|
|
16
|
+
diagId,
|
|
17
|
+
(id) => {
|
|
18
|
+
if (id) data.ensureElement(id)
|
|
19
|
+
},
|
|
20
|
+
{ immediate: true },
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const objectCount = computed(() => diagram.value?.elements?.length ?? 0)
|
|
24
|
+
const linkCount = computed(() => diagram.value?.connectors?.length ?? 0)
|
|
25
|
+
|
|
13
26
|
const zoom = ref(1)
|
|
14
27
|
const panX = ref(0)
|
|
15
28
|
const panY = ref(0)
|
|
@@ -50,17 +63,17 @@ const svgTransform = computed(() =>
|
|
|
50
63
|
</div>
|
|
51
64
|
|
|
52
65
|
<div class="entity-metadata">
|
|
53
|
-
<div class="metadata-item" v-if="diagram.
|
|
66
|
+
<div class="metadata-item" v-if="diagram.diagramType">
|
|
54
67
|
<span class="metadata-label">Type</span>
|
|
55
|
-
<span class="metadata-value">{{ diagram.
|
|
68
|
+
<span class="metadata-value">{{ diagram.diagramType }}</span>
|
|
56
69
|
</div>
|
|
57
70
|
<div class="metadata-item">
|
|
58
71
|
<span class="metadata-label">Elements</span>
|
|
59
|
-
<span class="metadata-value">{{
|
|
72
|
+
<span class="metadata-value">{{ objectCount }}</span>
|
|
60
73
|
</div>
|
|
61
74
|
<div class="metadata-item">
|
|
62
75
|
<span class="metadata-label">Connectors</span>
|
|
63
|
-
<span class="metadata-value">{{
|
|
76
|
+
<span class="metadata-value">{{ linkCount }}</span>
|
|
64
77
|
</div>
|
|
65
78
|
</div>
|
|
66
79
|
|