@peter.prib/renderengine 0.4.0 → 0.7.0
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/README.md +4 -0
- package/REFERENCE.md +267 -17
- package/dist/renderengine-workspace.umd.js +5289 -2895
- package/dist/renderengine.umd.js +4243 -2120
- package/examples/archimate.html +8 -28
- package/examples/data-model.html +45 -556
- package/package.json +70 -70
- package/src/archimateArchiFormat.js +11 -3
- package/src/archimateExchangeFormat.js +17 -9
- package/src/charting/ArchimateDiagramEditor.js +1 -1
- package/src/charting/Axis.js +20 -0
- package/src/charting/DiagramEditor.js +35 -3
- package/src/charting/Dialog.js +195 -0
- package/src/charting/DuplicatesDialog.js +108 -0
- package/src/charting/FormDialog.js +28 -97
- package/src/charting/InteractionManager.js +3 -0
- package/src/charting/Legend.js +45 -2
- package/src/charting/MergeCompareDialog.js +81 -0
- package/src/charting/PopupMenu.js +12 -3
- package/src/charting/SettingsDialog.js +6 -2
- package/src/charting/SettingsManager.js +14 -1
- package/src/charting/archimate/ArchimateCanvasEditor.js +190 -15
- package/src/charting/archimate/ArchimatePalette.js +1 -1
- package/src/charting/charts/AreaChart.js +5 -2
- package/src/charting/charts/BarChart.js +5 -3
- package/src/charting/charts/BarChart3DOrbit.js +5 -7
- package/src/charting/charts/BubbleChart.js +5 -7
- package/src/charting/charts/BubbleChart3D.js +5 -7
- package/src/charting/charts/CandlestickChart.js +3 -1
- package/src/charting/charts/DensityChart.js +4 -6
- package/src/charting/charts/GroupedBarChart.js +2 -2
- package/src/charting/charts/LineChart.js +5 -2
- package/src/charting/charts/ParallelCoordinatesChart.js +2 -2
- package/src/charting/charts/PipeChart.js +4 -6
- package/src/charting/charts/RadarChart.js +5 -6
- package/src/charting/charts/ScatterChart.js +5 -7
- package/src/charting/charts/StackedBarChart.js +4 -2
- package/src/charting/charts/StackedBarChart3DOrbit.js +5 -7
- package/src/charting/charts/StreamGraphChart.js +5 -2
- package/src/charting/charts/WaterfallChart.js +2 -1
- package/src/charting/content/ContentSource.js +129 -0
- package/src/charting/content/ContentSources.js +77 -0
- package/src/charting/datamodel/DataModel.js +116 -0
- package/src/charting/diagrams/ArchimateModel.js +38 -3
- package/src/charting/diagrams/ArchimateModelChecker.js +13 -4
- package/src/charting/diagrams/MergeElementsCommand.js +124 -101
- package/src/charting/diagrams/SvgZoom.js +20 -1
- package/src/charting/diagrams/duplicates.js +95 -13
- package/src/charting/duplicateSearchForm.js +58 -0
- package/src/charting/modelTree/ModelTree.js +239 -202
- package/src/charting/modelTree/ModelTreePanel.js +955 -919
- package/src/charting/modelTree/TreeBuilder.js +93 -45
- package/src/charting/modelTree/TreeViewMode.js +9 -6
- package/src/charting/modelTree/viewFolders.js +15 -6
- package/src/charting/requirements/AzureDevOpsApi.js +55 -0
- package/src/charting/requirements/GitLabApi.js +43 -0
- package/src/charting/requirements/JiraApi.js +69 -0
- package/src/charting/requirements/TrackerApi.js +142 -0
- package/src/charting/requirements/TrackerExport.js +3 -2
- package/src/charting/requirements/index.js +5 -0
- package/src/charting/requirements/trackerApis.js +21 -0
- package/src/icons/IconSet.js +86 -0
- package/src/icons/index.js +59 -0
- package/src/index.js +67 -10
- package/src/workspace/dom.js +56 -42
- package/src/workspace/index.js +1 -1
- package/src/workspace/nav/NavEntry.js +42 -0
- package/src/workspace/nav/NavGroup.js +43 -0
- package/src/workspace/nav/NavItem.js +53 -0
- package/src/workspace/nav/NavMenu.js +143 -0
- package/src/workspace/nav/NavSection.js +17 -0
- package/src/workspace/nav/index.js +5 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +8 -3
- package/src/workspace/parts/DataModelPart.js +91 -0
- package/src/workspace/parts/EditorPart.js +8 -0
- package/src/workspace/parts/ModelHosted.js +1 -0
- package/src/workspace/parts/ModelsPart.js +765 -622
- package/src/workspace/styles.js +28 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { render as renderSpecs } from '../dom.js';
|
|
2
|
+
import { NavEntry } from './NavEntry.js';
|
|
3
|
+
import { NavItem } from './NavItem.js';
|
|
4
|
+
import { NavGroup } from './NavGroup.js';
|
|
5
|
+
import { NavSection } from './NavSection.js';
|
|
6
|
+
import { ICONS } from '../../icons/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* NavMenu — a vertical navigation menu (a sidebar's), written as data:
|
|
10
|
+
*
|
|
11
|
+
* new NavMenu({
|
|
12
|
+
* label: 'Workbench',
|
|
13
|
+
* items: [
|
|
14
|
+
* { section: 'Notebooks' },
|
|
15
|
+
* { id: 'new-nb', label: '+ New Notebook', action: newNotebook, selectable: false },
|
|
16
|
+
* { group: 'Dev Tools', id: 'devtools', items: [
|
|
17
|
+
* { id: 'datamodel', icon: '◫', label: 'Data Model', action: (e) => open('datamodel', e) },
|
|
18
|
+
* ] },
|
|
19
|
+
* { id: 'admin', icon: '⚙', label: 'Admin', href: '/admin', visible: (ctx) => ctx.role === 'admin' },
|
|
20
|
+
* ],
|
|
21
|
+
* context: { role },
|
|
22
|
+
* }).render(host);
|
|
23
|
+
*
|
|
24
|
+
* An entry is an item (`label`), a collapsible group (`group`, `items`) or a
|
|
25
|
+
* section heading (`section`) — NavItem, NavGroup, NavSection, built from the
|
|
26
|
+
* JSON by NavMenu.entry. The menu keeps which item is active (`activeId`,
|
|
27
|
+
* marked with `aria-current`) and which groups are open; serialize() returns
|
|
28
|
+
* both as JSON for a host to keep, and the constructor takes them back.
|
|
29
|
+
*/
|
|
30
|
+
export class NavMenu {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {Array<Object|NavEntry>} options.items
|
|
34
|
+
* @param {string} [options.label] - The menu's accessible name
|
|
35
|
+
* @param {string} [options.activeId]
|
|
36
|
+
* @param {Object<string, boolean>} [options.open] - Group id → open, overriding each group's own
|
|
37
|
+
* @param {Object} [options.context] - What `visible` functions read (`{ role }`)
|
|
38
|
+
* @param {(item: NavItem, event: Event) => void} [options.onSelect] - After an item's own action
|
|
39
|
+
* @param {(groupId: string, open: boolean) => void} [options.onToggle]
|
|
40
|
+
* @param {import('../../icons/IconSet.js').IconSet} [options.icons] - Where an item's `icon` is looked up (default: the built-in ICONS)
|
|
41
|
+
* @param {Document} [options.document]
|
|
42
|
+
*/
|
|
43
|
+
constructor({ items = [], label = null, activeId = null, open = {}, context = {}, onSelect = null, onToggle = null, icons = ICONS, document: doc = globalThis.document } = {}) {
|
|
44
|
+
this.entries = items.map(NavMenu.entry);
|
|
45
|
+
this.label = label;
|
|
46
|
+
this.activeId = activeId;
|
|
47
|
+
this.context = context;
|
|
48
|
+
this.onSelect = onSelect;
|
|
49
|
+
this.onToggle = onToggle;
|
|
50
|
+
this.icons = icons;
|
|
51
|
+
this.doc = doc;
|
|
52
|
+
this.host = null;
|
|
53
|
+
/** Rendered item → its element, for marking the active one in place. */
|
|
54
|
+
this.nodes = new Map();
|
|
55
|
+
for (const group of this.groups()) {
|
|
56
|
+
if (group.id != null && group.id in open) group.open = !!open[group.id];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** An entry from its JSON: a group, a section heading or an item. */
|
|
61
|
+
static entry(json) {
|
|
62
|
+
if (json instanceof NavEntry) return json;
|
|
63
|
+
if (json == null || typeof json !== 'object') throw new Error(`NavMenu: an entry is an object, not ${json}`);
|
|
64
|
+
if ('group' in json) return new NavGroup(json, NavMenu.entry);
|
|
65
|
+
if ('section' in json) return new NavSection(json);
|
|
66
|
+
if (json.label == null) throw new Error(`NavMenu: an item needs a label (or "group", or "section")`);
|
|
67
|
+
return new NavItem(json);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Render into `host`, replacing what it held. */
|
|
71
|
+
render(host = this.host) {
|
|
72
|
+
if (!host) throw new Error('NavMenu: no host to render into');
|
|
73
|
+
this.host = host;
|
|
74
|
+
this.nodes = new Map();
|
|
75
|
+
renderSpecs(host, {
|
|
76
|
+
tag: 'nav',
|
|
77
|
+
cls: 'ws-nav',
|
|
78
|
+
attrs: { 'aria-label': this.label },
|
|
79
|
+
children: this.specsOf(this.entries),
|
|
80
|
+
}, this.doc);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The specs of the entries that show. */
|
|
85
|
+
specsOf(entries) {
|
|
86
|
+
return entries.filter(e => e.isVisible(this)).map(e => e.spec(this));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A click on `item`: its action, then (if it is selectable) it becomes active. */
|
|
90
|
+
select(item, event) {
|
|
91
|
+
item.action?.(event, item, this);
|
|
92
|
+
if (item.selectable && item.id != null) this.setActive(item.id);
|
|
93
|
+
this.onSelect?.(item, event);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Mark the item `id` as the active one (null: none), in place. */
|
|
97
|
+
setActive(id) {
|
|
98
|
+
this.activeId = id;
|
|
99
|
+
for (const [item, node] of this.nodes) {
|
|
100
|
+
const on = id != null && item.id === id;
|
|
101
|
+
node.classList.toggle('ws-nav-item-active', on);
|
|
102
|
+
if (on) node.setAttribute('aria-current', 'page');
|
|
103
|
+
else node.removeAttribute('aria-current');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A group opened or closed (by the user, or by setOpen). */
|
|
108
|
+
toggled(group, open) {
|
|
109
|
+
if (group.open === open) return;
|
|
110
|
+
group.open = open;
|
|
111
|
+
this.onToggle?.(group.id, open);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Change what `visible` functions read, and re-render if rendered. */
|
|
115
|
+
setContext(context) {
|
|
116
|
+
this.context = { ...this.context, ...context };
|
|
117
|
+
if (this.host) this.render();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The entry with this id, anywhere in the menu. */
|
|
122
|
+
find(id) {
|
|
123
|
+
return this.all().find(e => e.id === id) ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Every entry, depth first. */
|
|
127
|
+
all(entries = this.entries, out = []) {
|
|
128
|
+
for (const e of entries) { out.push(e); this.all(e.children, out); }
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
groups() {
|
|
133
|
+
return this.all().filter(e => e instanceof NavGroup);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The menu's state as JSON: the active item and each group's open state. */
|
|
137
|
+
serialize() {
|
|
138
|
+
return {
|
|
139
|
+
activeId: this.activeId,
|
|
140
|
+
open: Object.fromEntries(this.groups().filter(g => g.id != null).map(g => [g.id, g.open])),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NavEntry } from './NavEntry.js';
|
|
2
|
+
|
|
3
|
+
/** NavSection — a heading between entries ("Notebooks"); not clickable. */
|
|
4
|
+
export class NavSection extends NavEntry {
|
|
5
|
+
/**
|
|
6
|
+
* @param {Object} options
|
|
7
|
+
* @param {string} options.section - The heading's text
|
|
8
|
+
*/
|
|
9
|
+
constructor({ section, ...rest }) {
|
|
10
|
+
super(rest);
|
|
11
|
+
this.label = section;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
spec() {
|
|
15
|
+
return { cls: ['ws-nav-section', ...this.extraClasses()], text: this.label, attrs: { role: 'heading', title: this.title } };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -57,8 +57,13 @@ export class ArchimateCanvasPart extends EditorPart {
|
|
|
57
57
|
/** Whether a line to a box nested in its source is drawn (Archi leaves it out). */
|
|
58
58
|
this.showNestedLines = !!spec.showNestedLines;
|
|
59
59
|
this.settings = new SettingsManager();
|
|
60
|
-
// A settings change (Model settings…, a view's properties) redraws the canvas.
|
|
61
|
-
this._unsubscribeSettings = this.settings.onSettingsChange(() => {
|
|
60
|
+
// A settings change (Model settings…, a view's properties) redraws the canvas; the tool's, the tree too.
|
|
61
|
+
this._unsubscribeSettings = this.settings.onSettingsChange((event) => {
|
|
62
|
+
if (event?.type === 'tool') this.tree?.setOptions(this.settings.treeOptions());
|
|
63
|
+
if (!this.mounted) return;
|
|
64
|
+
this.editor?.render();
|
|
65
|
+
if (event?.type === 'tool') this._treePanel?.refresh();
|
|
66
|
+
});
|
|
62
67
|
// The user's specializations (or the defaults), as the main editor offers them; one already loaded is kept.
|
|
63
68
|
if (!ArchimateSpecializationRegistry.list().length) ArchimateSpecializationRegistry.loadFrom();
|
|
64
69
|
this._follow(this.model);
|
|
@@ -124,7 +129,7 @@ export class ArchimateCanvasPart extends EditorPart {
|
|
|
124
129
|
/** The model tree over the current model (kept across renders, so its mode and selection stay). */
|
|
125
130
|
get modelTree() {
|
|
126
131
|
if (this.tree?.model !== this.model) {
|
|
127
|
-
this.tree = new ModelTree(this.model, { mode: 'byLayer' });
|
|
132
|
+
this.tree = new ModelTree(this.model, { mode: 'byLayer', ...this.settings.treeOptions() });
|
|
128
133
|
// A view chosen in the tree opens it; an element chosen there is selected on the canvas.
|
|
129
134
|
this.tree.onSelectionChange((data) => {
|
|
130
135
|
if (data.kind === 'view' && data.view.id !== this.model.currentViewId) this.model.switchTo(data.view.id);
|
|
@@ -4,6 +4,9 @@ import { MenuBar } from '../../charting/MenuBar.js';
|
|
|
4
4
|
import { ToolbarSelect, ToolbarMenu, ToolbarText } from '../../charting/toolbar/index.js';
|
|
5
5
|
import { FormDialog } from '../../charting/FormDialog.js';
|
|
6
6
|
import { ModelCheckDialog } from '../../charting/ModelCheckDialog.js';
|
|
7
|
+
import { duplicateSearchFields, duplicateSearchOptions, validateDuplicateSearch } from '../../charting/duplicateSearchForm.js';
|
|
8
|
+
import { DuplicatesDialog } from '../../charting/DuplicatesDialog.js';
|
|
9
|
+
import { MergeCompareDialog } from '../../charting/MergeCompareDialog.js';
|
|
7
10
|
import { DataModelNotation } from '../../datamodel.js';
|
|
8
11
|
import {
|
|
9
12
|
DataModel, DataModelPerspective, DataModelContext, DataModelFileFormat, ServiceDomain, ServiceLandscape, BianLandscapeReader,
|
|
@@ -364,6 +367,7 @@ export class DataModelPart extends ModelHosted(ChartPart) {
|
|
|
364
367
|
{ label: 'Delete context', disabled: !this.context || undefined, action: () => this.removeContext(this.contextId) },
|
|
365
368
|
{ separator: true },
|
|
366
369
|
{ label: 'Check model…', action: () => this.checkModel() },
|
|
370
|
+
...this.modelMenuItems(),
|
|
367
371
|
{ label: 'Scorecard…', action: () => this.scoreModel() },
|
|
368
372
|
...EditorPart.menuGroup('Naming standard', this.namingItems()),
|
|
369
373
|
{ label: 'Classification scheme…', action: () => this.editClassificationScheme() },
|
|
@@ -509,6 +513,93 @@ export class DataModelPart extends ModelHosted(ChartPart) {
|
|
|
509
513
|
return findings;
|
|
510
514
|
}
|
|
511
515
|
|
|
516
|
+
/** What can be done to the whole model: Find likely duplicates… (the Tools menu and the model's tab show it too). */
|
|
517
|
+
modelMenuItems() {
|
|
518
|
+
return [{ label: 'Find likely duplicates…', action: () => this.askDuplicates() }];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Find likely duplicates…: at which level (the one in front first, when it
|
|
523
|
+
* is one), how to compare (names, definitions or attributes; exactly or
|
|
524
|
+
* by their words) and what noise to leave out (duplicateSearchFields),
|
|
525
|
+
* then the groups found, each with Merge… (showDuplicates).
|
|
526
|
+
*/
|
|
527
|
+
askDuplicates() {
|
|
528
|
+
const levels = DataModel.LEVELS.filter(l => this.model.level(l).entities.length > 1);
|
|
529
|
+
const ordered = levels.includes(this.level) ? [this.level, ...levels.filter(l => l !== this.level)] : levels;
|
|
530
|
+
const entities = levels.flatMap(l => this.model.level(l).entities);
|
|
531
|
+
const properties = [...new Set(entities.flatMap(e => (e.properties ?? []).map(p => p.name ?? p.key)).filter(Boolean))].sort();
|
|
532
|
+
return new FormDialog({
|
|
533
|
+
title: 'Find likely duplicates',
|
|
534
|
+
description: 'Entities at one level are compared by their words: case, punctuation, word order and the noise words do not count.',
|
|
535
|
+
fields: duplicateSearchFields({
|
|
536
|
+
scope: { label: 'Level', choices: (ordered.length ? ordered : [this.level]).map(l => [l, DataModelNotation.level(l)?.label ?? l]) },
|
|
537
|
+
compare: [['name', 'Names'], ['documentation', 'Definitions'], ['attributes', 'Attributes']],
|
|
538
|
+
properties,
|
|
539
|
+
}).filter(f => f.key !== 'acrossTypes'),
|
|
540
|
+
submitLabel: 'Find', idPrefix: 'duplicates-',
|
|
541
|
+
document: this.container?.ownerDocument ?? globalThis.document,
|
|
542
|
+
validate: validateDuplicateSearch,
|
|
543
|
+
onApply: (values) => {
|
|
544
|
+
const { options, scope } = duplicateSearchOptions(values);
|
|
545
|
+
this.showDuplicates(scope, options);
|
|
546
|
+
},
|
|
547
|
+
}).open();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The entities at `level` that look like one thing twice (DataModel#duplicateEntities), listed, each group with Merge…. */
|
|
551
|
+
showDuplicates(level, options = {}) {
|
|
552
|
+
const find = () => this.model.duplicateEntities(level, options);
|
|
553
|
+
const groups = find();
|
|
554
|
+
new DuplicatesDialog({
|
|
555
|
+
groups,
|
|
556
|
+
refind: find,
|
|
557
|
+
merge: async (group) => !!(await this.askMergeEntities(group.level, group.ids)),
|
|
558
|
+
onReveal: (group) => this.revealFinding(group),
|
|
559
|
+
empty: 'No entities look like the same thing twice.',
|
|
560
|
+
document: this.container?.ownerDocument ?? globalThis.document,
|
|
561
|
+
}).open();
|
|
562
|
+
return groups;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Merge some entities at a level into one (DataModel#mergeEntitiesCommand),
|
|
567
|
+
* one undo step. When they differ, they are shown side by side
|
|
568
|
+
* (DataModel#entityComparison, MergeCompareDialog) to choose which is kept
|
|
569
|
+
* and, when their definitions differ, which (or all of them joined); OK
|
|
570
|
+
* merges, Cancel leaves them. Alike, the first is kept without asking.
|
|
571
|
+
* Resolves to the merge's command, or null.
|
|
572
|
+
*/
|
|
573
|
+
async askMergeEntities(level, ids) {
|
|
574
|
+
const schema = this.model.level(level);
|
|
575
|
+
const entities = ids.map(id => schema.entity(id)).filter(Boolean);
|
|
576
|
+
if (entities.length < 2) return null;
|
|
577
|
+
const count = (n, noun) => `${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
578
|
+
const used = (e) => `${count((e.attributes ?? []).length, 'attribute')}, ${count(schema.relationships.filter(r => r.source === e.id || r.target === e.id).length, 'relationship')}`;
|
|
579
|
+
const kept = entities.map(e => e.id);
|
|
580
|
+
const merge = (keep, choices = {}) => this.runModelEdit(this.model.mergeEntitiesCommand(level, keep, kept.filter(id => id !== keep), choices));
|
|
581
|
+
const rows = this.model.entityComparison(level, kept);
|
|
582
|
+
if (!rows.some(row => MergeCompareDialog.differs(row))) return merge(kept[0]);
|
|
583
|
+
const texts = [...new Set(entities.map(e => String(e.description ?? '').trim()).filter(Boolean))];
|
|
584
|
+
const JOINED = '\u0000joined';
|
|
585
|
+
const values = await new MergeCompareDialog({
|
|
586
|
+
title: `Merge ${entities.length} entities`,
|
|
587
|
+
description: 'The one kept takes the others\' attributes it lacks, their relationships and their mappings; the others go. Where they differ (marked ≠), choose.',
|
|
588
|
+
columns: entities.map(e => `${e.name || e.id} (${used(e)})`),
|
|
589
|
+
rows,
|
|
590
|
+
fields: [
|
|
591
|
+
{ key: 'keep', label: 'Keep', type: 'select', value: kept[0], choices: entities.map(e => [e.id, `${e.name || e.id} (${used(e)})`]) },
|
|
592
|
+
...(texts.length > 1 ? [{
|
|
593
|
+
key: 'description', label: 'Definition', type: 'select', value: JOINED,
|
|
594
|
+
choices: [[JOINED, 'All of them, joined'], ...texts.map(t => [t, t.length > 60 ? `${t.slice(0, 60)}…` : t])],
|
|
595
|
+
}] : []),
|
|
596
|
+
],
|
|
597
|
+
document: this.container?.ownerDocument ?? globalThis.document,
|
|
598
|
+
}).ask();
|
|
599
|
+
if (!values) return null;
|
|
600
|
+
return merge(values.keep, texts.length > 1 ? { description: values.description === JOINED ? texts.join('\n\n') : values.description } : {});
|
|
601
|
+
}
|
|
602
|
+
|
|
512
603
|
/** Show what a finding names: an entity's level, a service domain seen through at the logical level, the mapping or data types view. */
|
|
513
604
|
revealFinding(finding) {
|
|
514
605
|
if (finding.kind === 'perspective') {
|
|
@@ -181,9 +181,16 @@ export class EditorPart extends Part {
|
|
|
181
181
|
{ label: 'Transform names…', action: () => this.askTransformNames() },
|
|
182
182
|
...(this.model.search().propertyNames().length ? [{ label: 'Rename property…', action: () => this.askRenameProperty() }] : []),
|
|
183
183
|
] : []),
|
|
184
|
+
// The editor's own edits of the whole model (Find likely duplicates…), when no Tools menu has them.
|
|
185
|
+
...(() => { const own = this.modelsHost ? [] : this.modelMenuItems(); return own.length ? [{ separator: true }, ...own] : []; })(),
|
|
184
186
|
];
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
/** What can be done to the whole model (Find likely duplicates…): the editor's; the Tools menu and the model's tab show them. */
|
|
190
|
+
modelMenuItems() {
|
|
191
|
+
return this.editor?.modelMenuItems?.() ?? [];
|
|
192
|
+
}
|
|
193
|
+
|
|
187
194
|
/** Run a model-wide edit (null: nothing to do; a rename, a merge) as one undo step, then redraw. */
|
|
188
195
|
runModelEdit(command) {
|
|
189
196
|
if (!command) return null;
|
|
@@ -305,6 +312,7 @@ export class EditorPart extends Part {
|
|
|
305
312
|
{ separator: true },
|
|
306
313
|
...some(open, models['Open… in a new tab'], ...saving, models['Duplicate model'], models['Close model']),
|
|
307
314
|
...EditorPart.menuGroup(this.modelsHost?.keptWhere ?? 'This browser', this.modelsHost?.keepItems?.(this) ?? []),
|
|
315
|
+
...(this.modelsHost?.contentItems ? [{ label: 'Open from', items: this.modelsHost.contentItems() }] : []),
|
|
308
316
|
{ separator: true },
|
|
309
317
|
...EditorPart.menuGroup('Import', this.importItems(), { prefix: true }),
|
|
310
318
|
...EditorPart.menuGroup('Export', this.exportItems(), { prefix: true }),
|
|
@@ -114,6 +114,7 @@ export const ModelHosted = (Base) => class extends Base {
|
|
|
114
114
|
{ label: 'Duplicate model', action: () => host.duplicate(this) },
|
|
115
115
|
{ label: 'Close model', action: () => host.closeModel(this.id) },
|
|
116
116
|
...EditorPart.menuGroup(host.keptWhere ?? 'This browser', host.keepItems?.(this) ?? []),
|
|
117
|
+
...(host.contentItems ? [{ label: 'Open from', items: host.contentItems() }] : []),
|
|
117
118
|
] : []),
|
|
118
119
|
]);
|
|
119
120
|
}
|