@peter.prib/renderengine 0.2.5 → 0.2.7

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.
Files changed (67) hide show
  1. package/README.md +219 -213
  2. package/REFERENCE.md +272 -6
  3. package/dist/renderengine-workspace.umd.js +11980 -7421
  4. package/dist/renderengine.umd.js +8991 -5784
  5. package/examples/archimate.html +68 -66
  6. package/examples/data-model.html +534 -0
  7. package/package.json +70 -68
  8. package/src/bian.js +182 -0
  9. package/src/charting/CheckboxTreeSelector.js +373 -304
  10. package/src/charting/DiagramEditor.js +53 -32
  11. package/src/charting/DiagramTypeRegistry.js +13 -3
  12. package/src/charting/ModelCheckDialog.js +63 -0
  13. package/src/charting/ModelChecker.js +26 -0
  14. package/src/charting/ViewTabStrip.js +94 -81
  15. package/src/charting/archimate/ArchimateCanvasEditor.js +825 -815
  16. package/src/charting/archimate/ArchimateElementTemplate.js +208 -195
  17. package/src/charting/datamodel/BianLandscapeReader.js +185 -0
  18. package/src/charting/datamodel/BianSemanticApiReader.js +183 -0
  19. package/src/charting/datamodel/DataModel.js +645 -0
  20. package/src/charting/datamodel/DataModelChecker.js +92 -0
  21. package/src/charting/datamodel/DataModelCompared.js +127 -0
  22. package/src/charting/datamodel/DataModelContext.js +108 -0
  23. package/src/charting/datamodel/DataModelFileFormat.js +33 -0
  24. package/src/charting/datamodel/DataModelPerspective.js +129 -0
  25. package/src/charting/datamodel/DataType.js +108 -0
  26. package/src/charting/datamodel/EnumerationType.js +51 -0
  27. package/src/charting/datamodel/LevelMapping.js +38 -0
  28. package/src/charting/datamodel/LevelSchema.js +41 -0
  29. package/src/charting/datamodel/ServiceDomain.js +177 -0
  30. package/src/charting/datamodel/ServiceLandscape.js +104 -0
  31. package/src/charting/datamodel/StructuredType.js +63 -0
  32. package/src/charting/datamodel/index.js +15 -0
  33. package/src/charting/diagramModel/MergeModelCommand.js +43 -37
  34. package/src/charting/diagramModel/OpenViewList.js +75 -0
  35. package/src/charting/diagramModel/ViewSetChecker.js +22 -34
  36. package/src/charting/diagrams/DataModelDiagram.js +160 -86
  37. package/src/charting/diagrams/DataModelRelationshipStrategy.js +94 -38
  38. package/src/charting/diagrams/EdgeGeometry.js +578 -553
  39. package/src/charting/diagrams/GraphDiagram.js +11 -1
  40. package/src/charting/diagrams/NodeGeometry.js +169 -158
  41. package/src/charting/diagrams/nesting.js +31 -0
  42. package/src/charting/glyphs/PersonGlyph.js +24 -23
  43. package/src/charting/modelTree/ModelTree.js +202 -164
  44. package/src/charting/modelTree/ModelTreePanel.js +36 -3
  45. package/src/charting/modelTree/TreeViewMode.js +131 -110
  46. package/src/charting/modelTree/modes/FlatTreeMode.js +32 -46
  47. package/src/charting/modelTree/modes/LayerGroupedTreeMode.js +47 -62
  48. package/src/charting/modelTree/modes/TypeGroupedTreeMode.js +46 -60
  49. package/src/datamodel.js +132 -122
  50. package/src/diagramCatalog.js +35 -30
  51. package/src/index.js +2666 -2659
  52. package/src/workspace/EditorShell.js +166 -67
  53. package/src/workspace/PanelLayout.js +56 -0
  54. package/src/workspace/index.js +1 -0
  55. package/src/workspace/layout/TabsNode.js +205 -191
  56. package/src/workspace/parts/ArchimateCanvasPart.js +10 -9
  57. package/src/workspace/parts/ChartPart.js +6 -1
  58. package/src/workspace/parts/DataModelPart.js +544 -0
  59. package/src/workspace/parts/DiagramEditorPart.js +2 -8
  60. package/src/workspace/parts/EditorPart.js +461 -407
  61. package/src/workspace/parts/ModelFile.js +216 -187
  62. package/src/workspace/parts/ModelsPart.js +483 -192
  63. package/src/workspace/storage/BackupFolder.js +145 -0
  64. package/src/workspace/storage/KeyValueStore.js +92 -0
  65. package/src/workspace/storage/ModelLibrary.js +65 -0
  66. package/src/workspace/workArea.js +417 -413
  67. package/src/yaml/YamlReader.js +372 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * LevelMapping — which entities of one level realise which of another
3
+ * (conceptual → logical, logical → physical): pairs of ids, any number either
4
+ * way, so one concept may become several logical entities (Party → Person,
5
+ * Organisation), several one table (Person, Organisation → party), or many
6
+ * many.
7
+ */
8
+ export class LevelMapping {
9
+ /**
10
+ * @param {{from: string, to: string, pairs?: Array<[string, string]>}} spec
11
+ * `from`/`to` levels; each pair [id at `from`, id at `to`]
12
+ */
13
+ constructor({ from, to, pairs = [] } = {}) {
14
+ if (!from || !to) throw new TypeError('LevelMapping needs the levels it maps, from and to');
15
+ this.from = from;
16
+ this.to = to;
17
+ this.pairs = pairs.map(([a, b]) => [a, b]);
18
+ }
19
+
20
+ /** What realises an entity of `from` at `to`. */
21
+ targetsOf(id) {
22
+ return this.pairs.filter(([a]) => a === id).map(([, b]) => b);
23
+ }
24
+
25
+ /** What an entity of `to` realises at `from`. */
26
+ sourcesOf(id) {
27
+ return this.pairs.filter(([, b]) => b === id).map(([a]) => a);
28
+ }
29
+
30
+ /** Map one more pair (none twice). */
31
+ add(fromId, toId) {
32
+ if (!this.pairs.some(([a, b]) => a === fromId && b === toId)) this.pairs.push([fromId, toId]);
33
+ }
34
+
35
+ toJSON() {
36
+ return { from: this.from, to: this.to, pairs: this.pairs.map(p => [...p]) };
37
+ }
38
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * LevelSchema — one level of a data model (conceptual, logical, physical) as
3
+ * its own structure: its entities (a concept; a logical entity with its
4
+ * attributes and keys; a table with its columns and types) and the
5
+ * relationships between them. The levels are not one-to-one: a concept may be
6
+ * realised by several logical entities, several by one table (LevelMapping
7
+ * says which).
8
+ *
9
+ * An entity: `{ id, name, description?, attributes?, x?, y?, color?, shape?,
10
+ * labelPosition? }` — `attributes` `[{name, type?, isPK?, isFK?}]` (none at the
11
+ * conceptual level); a relationship as dataModelDiagram takes it.
12
+ */
13
+ export class LevelSchema {
14
+ /**
15
+ * @param {string} level - 'conceptual', 'logical' or 'physical'
16
+ * @param {{entities?: Array<Object>, relationships?: Array<Object>}} [spec]
17
+ */
18
+ constructor(level, { entities = [], relationships = [] } = {}) {
19
+ if (!level) throw new TypeError('LevelSchema needs its level');
20
+ this.level = level;
21
+ this.entities = entities;
22
+ this.relationships = relationships;
23
+ }
24
+
25
+ /** An entity by id, or null. */
26
+ entity(id) {
27
+ return this.entities.find(e => e.id === id) ?? null;
28
+ }
29
+
30
+ /** Add an entity (its id unique at this level). */
31
+ addEntity(entity) {
32
+ if (!entity?.id) throw new TypeError('An entity needs an id');
33
+ if (this.entity(entity.id)) throw new TypeError(`The ${this.level} level already has "${entity.id}"`);
34
+ this.entities.push(entity);
35
+ return entity;
36
+ }
37
+
38
+ toJSON() {
39
+ return { entities: this.entities, relationships: this.relationships };
40
+ }
41
+ }
@@ -0,0 +1,177 @@
1
+ import { DataModelPerspective } from './DataModelPerspective.js';
2
+ import { BianNotation } from '../../bian.js';
3
+
4
+ /** How a service domain's control record and behaviour qualifiers are coloured where it is seen through. */
5
+ const CONTROL_RECORD_COLOR = '#fde68a';
6
+ const QUALIFIER_COLOR = '#e0f2fe';
7
+
8
+ /**
9
+ * ServiceDomain — a BIAN service domain as a kind of perspective: it sees the
10
+ * concepts it covers (as any perspective), and it is more. It sits in a business
11
+ * area and business domain, manages one kind of asset (`asset`) with one
12
+ * functional pattern (`pattern`, BianNotation), and keeps a control record (a
13
+ * logical entity: what its instances are, named for the asset and the
14
+ * pattern's generic artifact) broken down by behaviour qualifiers (each a
15
+ * logical entity, of a qualifier type). Its service operations (its semantic
16
+ * API) follow: the pattern's action terms on the control record and each
17
+ * qualifier (BianNotation.serviceOperations).
18
+ *
19
+ * Seen through at the logical level it shows its control record and qualifiers,
20
+ * whatever they realise, marked (colour, and what each is on hover), with a note
21
+ * of its service operations on the control record.
22
+ */
23
+ export class ServiceDomain extends DataModelPerspective {
24
+ static kind = 'service-domain';
25
+ static kindLabel = 'Service domain';
26
+
27
+ /**
28
+ * @param {Object} spec - As DataModelPerspective, plus:
29
+ * @param {string} [spec.businessArea]
30
+ * @param {string} [spec.businessDomain]
31
+ * @param {string} [spec.asset] - What it manages ('Current Account'); its name when unset
32
+ * @param {string} [spec.pattern] - A BIAN functional pattern ('Fulfill')
33
+ * @param {string|null} [spec.controlRecord] - The logical entity its control record is
34
+ * @param {Array<{id?: string, name: string, type?: string, entity?: string}>} [spec.qualifiers]
35
+ * Behaviour qualifiers: a name, a qualifier type (default the pattern's), the logical entity it is
36
+ * @param {string[]} [spec.owns] - Other business objects (logical entities) it owns
37
+ */
38
+ constructor({ businessArea = '', businessDomain = '', asset = '', pattern = 'Fulfill', controlRecord = null, qualifiers = [], owns = [], ...spec } = {}) {
39
+ super(spec);
40
+ /** Business objects (logical entities) it owns besides its control record and qualifiers. */
41
+ this.owns = [...owns];
42
+ this.businessArea = businessArea;
43
+ this.businessDomain = businessDomain;
44
+ this.asset = asset;
45
+ this.pattern = pattern;
46
+ this.controlRecord = controlRecord;
47
+ this.qualifiers = qualifiers.map((q, i) => ({ ...q, id: q.id ?? `${this.id}-bq-${i + 1}` }));
48
+ }
49
+
50
+ /** Its functional pattern (BianFunctionalPattern), or null for one BIAN does not have. */
51
+ get functionalPattern() {
52
+ return BianNotation.functionalPattern(this.pattern);
53
+ }
54
+
55
+ /** Its control record's name: the asset (else its name) and the pattern's generic artifact. */
56
+ get controlRecordName() {
57
+ return this.functionalPattern?.controlRecordName(this.asset || this.name) ?? (this.asset || this.name);
58
+ }
59
+
60
+ /** A qualifier's type: its own, else its pattern's. */
61
+ qualifierType(qualifier) {
62
+ return qualifier.type || this.functionalPattern?.qualifierType || '';
63
+ }
64
+
65
+ /** Its default service operations (BianNotation.serviceOperations). */
66
+ serviceOperations() {
67
+ return BianNotation.serviceOperations(this.pattern, {
68
+ serviceDomain: this.name, controlRecord: this.controlRecordName, qualifiers: this.qualifiers.map(q => q.name),
69
+ });
70
+ }
71
+
72
+ /** The business objects it owns: its control record, its qualifiers' entities, and those it names (`owns`). */
73
+ ownedEntities() {
74
+ return [...new Set([this.controlRecord, ...this.qualifiers.map(q => q.entity), ...this.owns].filter(Boolean))];
75
+ }
76
+
77
+ /** At the logical level, the business objects it owns. */
78
+ alsoShows(level) {
79
+ return level === 'logical' ? this.ownedEntities() : [];
80
+ }
81
+
82
+ /** At the logical level, its control record and qualifiers are coloured and say what they are. */
83
+ decorate(entity, level) {
84
+ if (level !== 'logical') return {};
85
+ if (entity.id === this.controlRecord) {
86
+ return { color: CONTROL_RECORD_COLOR, details: { 'control record': this.controlRecordName, 'functional pattern': this.pattern, 'service domain': this.name } };
87
+ }
88
+ const qualifier = this.qualifiers.find(q => q.entity === entity.id);
89
+ return qualifier
90
+ ? { color: QUALIFIER_COLOR, details: { 'behaviour qualifier': `${qualifier.name} (${this.qualifierType(qualifier)})`, 'service domain': this.name } }
91
+ : {};
92
+ }
93
+
94
+ /** At the logical level, a note of its service operations, on its control record. */
95
+ notes(level) {
96
+ if (level !== 'logical' || !this.controlRecord) return [];
97
+ return [{ id: `${this.id}-operations`, text: this.operationsSummary(), attachTo: [this.controlRecord] }];
98
+ }
99
+
100
+ /** Its service operations in a few lines: the terms, on what. */
101
+ operationsSummary() {
102
+ const ops = this.serviceOperations();
103
+ const terms = (pick) => [...new Set(ops.filter(pick).map(o => o.actionTerm))].join(', ');
104
+ const qualifiers = this.qualifiers.map(q => q.name);
105
+ return [
106
+ `${this.name}: ${terms(o => o.target === this.name)}`,
107
+ `${this.controlRecordName}: ${terms(o => o.target === this.controlRecordName && !o.qualifier)}`,
108
+ ...(qualifiers.length ? [`each of ${qualifiers.join(', ')}: the same`] : []),
109
+ ].join('\n');
110
+ }
111
+
112
+ toJSON() {
113
+ return {
114
+ ...super.toJSON(),
115
+ businessArea: this.businessArea, businessDomain: this.businessDomain, asset: this.asset, pattern: this.pattern,
116
+ controlRecord: this.controlRecord, qualifiers: this.qualifiers.map(q => ({ ...q })), owns: [...this.owns],
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Its dialog's fields: a perspective's, then where it sits, its asset and
122
+ * pattern (BIAN's, each described), its control record (a logical entity),
123
+ * and its qualifiers a line each: `name | type | logical entity`.
124
+ */
125
+ static fields(model, domain = null) {
126
+ const logical = model.level('logical').entities;
127
+ const entityName = (id) => logical.find(e => e.id === id)?.name ?? id ?? '';
128
+ return [
129
+ ...DataModelPerspective.fields(model, domain),
130
+ { type: 'heading', label: 'Service domain' },
131
+ { key: 'businessArea', label: 'Business area', type: 'text', value: domain?.businessArea ?? '' },
132
+ { key: 'businessDomain', label: 'Business domain', type: 'text', value: domain?.businessDomain ?? '' },
133
+ { key: 'asset', label: 'Asset it manages', type: 'text', value: domain?.asset ?? '', placeholder: 'its name' },
134
+ {
135
+ key: 'pattern', label: 'Functional pattern', type: 'select', value: domain?.pattern ?? 'Fulfill',
136
+ choices: BianNotation.functionalPatterns().map(p => [p.name, `${p.name} — ${p.artifact}`]),
137
+ },
138
+ {
139
+ key: 'controlRecord', label: 'Control record', type: 'select', value: domain?.controlRecord ?? '',
140
+ choices: [['', 'None yet'], ...logical.map(e => [e.id, e.name])],
141
+ },
142
+ {
143
+ key: 'qualifiers', label: 'Behaviour qualifiers', type: 'textarea', rows: 4,
144
+ value: (domain?.qualifiers ?? []).map(q => [q.name, q.type ?? '', entityName(q.entity)].join(' | ')).join('\n'),
145
+ hint: `One a line: name | type (${BianNotation.qualifierTypes().slice(0, 4).join(', ')}…; blank: the pattern's) | logical entity (by name)`,
146
+ },
147
+ {
148
+ key: 'owns', label: 'Also owns', type: 'checklist', value: domain?.owns ?? [],
149
+ choices: logical.map(e => [e.id, e.name]),
150
+ },
151
+ ];
152
+ }
153
+
154
+ /** A service domain's spec from those fields (a qualifier kept keeps its id; a type BIAN lacks is left to the pattern). */
155
+ static specFrom(values, model, domain = null) {
156
+ const logical = model.level('logical').entities;
157
+ const entityOf = (word) => logical.find(e => e.id === word || e.name.toLowerCase() === word.toLowerCase())?.id ?? null;
158
+ const qualifiers = String(values.qualifiers ?? '').split('\n').map(l => l.trim()).filter(Boolean).map(line => {
159
+ const [name, type = '', entity = ''] = line.split('|').map(p => p.trim());
160
+ const kept = domain?.qualifiers.find(q => q.name === name);
161
+ return {
162
+ ...(kept && { id: kept.id }), name,
163
+ ...(BianNotation.isQualifierType(type) && { type }),
164
+ ...(entityOf(entity) && { entity: entityOf(entity) }),
165
+ };
166
+ });
167
+ return {
168
+ kind: ServiceDomain.kind,
169
+ ...DataModelPerspective.specFrom(values, model),
170
+ businessArea: (values.businessArea ?? '').trim(), businessDomain: (values.businessDomain ?? '').trim(),
171
+ asset: (values.asset ?? '').trim(), pattern: values.pattern || 'Fulfill',
172
+ controlRecord: values.controlRecord || null, qualifiers, owns: [...(values.owns ?? [])],
173
+ };
174
+ }
175
+ }
176
+
177
+ DataModelPerspective.register(ServiceDomain);
@@ -0,0 +1,104 @@
1
+ import { BianNotation } from '../../bian.js';
2
+
3
+ /** The colour of a service domain's box, by its functional pattern's place among BIAN's (none: grey). */
4
+ const PATTERN_COLORS = ['#fde68a', '#bbf7d0', '#bfdbfe', '#fbcfe8', '#ddd6fe', '#fed7aa', '#a5f3fc', '#e9d5ff', '#fecaca', '#d9f99d'];
5
+ const NO_PATTERN_COLOR = '#e5e7eb';
6
+
7
+ /**
8
+ * ServiceLandscape — a model's service domains laid out as BIAN draws its
9
+ * service landscape: a column per business area, in it a frame per business
10
+ * domain, in that the service domains (each a box coloured by its functional
11
+ * pattern, and on hover its pattern, control record and asset). Service
12
+ * domains with no area or domain go under "No business area" / "No business
13
+ * domain". The layout follows from the structure, so it is made anew each time.
14
+ *
15
+ * `view(serviceDomains)` gives what dataModelDiagram draws: entities (a box per
16
+ * service domain, its id `landscape:<id>`), and the frames as graphDiagram
17
+ * folders (`folders`, `elementFolders`).
18
+ */
19
+ export class ServiceLandscape {
20
+ static BOX = Object.freeze({ width: 160, height: 56 });
21
+ static GAP = 16;
22
+ static HEADER = 24;
23
+ static PAD = 12;
24
+ /** Service domains side by side in a business domain's frame. */
25
+ static ACROSS = 2;
26
+ static NO_AREA = 'No business area';
27
+ static NO_DOMAIN = 'No business domain';
28
+ static PREFIX = 'landscape:';
29
+
30
+ /** A service domain's id from a landscape box's id, or null. */
31
+ static serviceDomainOf(nodeId) {
32
+ return String(nodeId).startsWith(ServiceLandscape.PREFIX) ? String(nodeId).slice(ServiceLandscape.PREFIX.length) : null;
33
+ }
34
+
35
+ /** The colour of a functional pattern's service domains. */
36
+ static colorOf(pattern) {
37
+ const i = BianNotation.functionalPatterns().findIndex(p => p.name === pattern);
38
+ return i < 0 ? NO_PATTERN_COLOR : PATTERN_COLORS[i % PATTERN_COLORS.length];
39
+ }
40
+
41
+ /**
42
+ * Business areas, each with its business domains, each with its service
43
+ * domains, in the order they first come.
44
+ * @param {Array<ServiceDomain>} serviceDomains
45
+ * @returns {Array<{name: string, domains: Array<{name: string, serviceDomains: Array}>}>}
46
+ */
47
+ static areas(serviceDomains) {
48
+ const areas = new Map();
49
+ for (const sd of serviceDomains) {
50
+ const area = sd.businessArea || ServiceLandscape.NO_AREA;
51
+ const domain = sd.businessDomain || ServiceLandscape.NO_DOMAIN;
52
+ if (!areas.has(area)) areas.set(area, new Map());
53
+ const domains = areas.get(area);
54
+ if (!domains.has(domain)) domains.set(domain, []);
55
+ domains.get(domain).push(sd);
56
+ }
57
+ return [...areas].map(([name, domains]) => ({ name, domains: [...domains].map(([d, list]) => ({ name: d, serviceDomains: list })) }));
58
+ }
59
+
60
+ /**
61
+ * What to draw (see the class).
62
+ * @param {Array<ServiceDomain>} serviceDomains
63
+ * @returns {{entities, relationships, annotations, links, folders, elementFolders}}
64
+ */
65
+ static view(serviceDomains) {
66
+ const { BOX, GAP, HEADER, PAD, ACROSS, PREFIX } = ServiceLandscape;
67
+ const domainWidth = ACROSS * BOX.width + (ACROSS - 1) * GAP + 2 * PAD;
68
+ const areaWidth = domainWidth + 2 * PAD;
69
+ const entities = [], areaFolders = [], domainFolders = [], elementFolders = [];
70
+ ServiceLandscape.areas(serviceDomains).forEach((area, a) => {
71
+ const areaId = `landscape-area-${a + 1}`;
72
+ const x = 20 + a * (areaWidth + GAP);
73
+ let y = 20 + HEADER + PAD;
74
+ area.domains.forEach((domain, d) => {
75
+ const domainId = `${areaId}-domain-${d + 1}`;
76
+ const rows = Math.ceil(domain.serviceDomains.length / ACROSS);
77
+ const height = HEADER + PAD + rows * BOX.height + (rows - 1) * GAP + PAD;
78
+ domainFolders.push({ id: domainId, name: domain.name, parentId: areaId, x: x + PAD, y, width: domainWidth, height });
79
+ domain.serviceDomains.forEach((sd, i) => {
80
+ const id = `${PREFIX}${sd.id}`;
81
+ const pattern = sd.functionalPattern;
82
+ entities.push({
83
+ id, name: sd.name, level: 'landscape',
84
+ x: x + 2 * PAD + (i % ACROSS) * (BOX.width + GAP) + BOX.width / 2,
85
+ y: y + HEADER + PAD + Math.floor(i / ACROSS) * (BOX.height + GAP) + BOX.height / 2,
86
+ width: BOX.width, height: BOX.height,
87
+ color: ServiceLandscape.colorOf(sd.pattern),
88
+ description: sd.description,
89
+ details: {
90
+ 'functional pattern': pattern ? `${pattern.name} (${pattern.artifact})` : (sd.pattern || 'none'),
91
+ 'control record': sd.controlRecordName,
92
+ ...(sd.asset && { asset: sd.asset }),
93
+ },
94
+ });
95
+ elementFolders.push([id, domainId]);
96
+ });
97
+ y += height + GAP;
98
+ });
99
+ areaFolders.push({ id: areaId, name: area.name, x, y: 20, width: areaWidth, height: y - 20 - GAP + PAD });
100
+ });
101
+ // Areas first, so they are drawn behind their domains.
102
+ return { entities, relationships: [], annotations: [], links: [], folders: [...areaFolders, ...domainFolders], elementFolders };
103
+ }
104
+ }
@@ -0,0 +1,63 @@
1
+ import { DataType } from './DataType.js';
2
+
3
+ /**
4
+ * StructuredType — a data type made of named fields, each of a type (a plain
5
+ * type's name or another data type's): BIAN's Amount, Datetime, Identifier.
6
+ * `fields` as `[{ name, type?, description?, iso20022? }]`. Drawn as a box with
7
+ * a row per field, joined to the data types its fields are.
8
+ */
9
+ export class StructuredType extends DataType {
10
+ static kind = 'structure';
11
+ static kindLabel = 'Structure';
12
+
13
+ /** @param {Object} spec - As DataType, plus `fields: [{ name, type?, description?, iso20022? }]` */
14
+ constructor({ fields = [], ...spec } = {}) {
15
+ super(spec);
16
+ this.fields = fields.map(f => ({
17
+ name: String(f.name),
18
+ ...(f.type && { type: String(f.type) }),
19
+ ...(f.description && { description: f.description }),
20
+ ...(f.iso20022 && { iso20022: f.iso20022 }),
21
+ }));
22
+ }
23
+
24
+ summary() {
25
+ return `${this.name}: ${this.fields.map(f => (f.type ? `${f.name} (${f.type})` : f.name)).join(', ')}`;
26
+ }
27
+
28
+ rows() {
29
+ return this.fields.map(f => ({ name: f.name, ...(f.type && { type: f.type }) }));
30
+ }
31
+
32
+ uses() {
33
+ return [...super.uses(), ...this.fields.map(f => f.type).filter(Boolean)];
34
+ }
35
+
36
+ toJSON() {
37
+ return { ...super.toJSON(), fields: this.fields.map(f => ({ ...f })) };
38
+ }
39
+
40
+ /** A plain type's fields, then its fields a line each: `name | type`. */
41
+ static fields(model, type = null) {
42
+ return [
43
+ ...DataType.fields(model, type),
44
+ {
45
+ key: 'fields', label: 'Fields', type: 'textarea', rows: 6,
46
+ value: (type?.fields ?? []).map(f => (f.type ? `${f.name} | ${f.type}` : f.name)).join('\n'),
47
+ hint: 'One a line: name | type (a plain type, or a data type by name)',
48
+ },
49
+ ];
50
+ }
51
+
52
+ static specFrom(values, model, type = null) {
53
+ return {
54
+ ...super.specFrom(values),
55
+ fields: DataType.lines(values.fields).map(([name, fieldType]) => {
56
+ const kept = type?.fields.find(f => f.name === name);
57
+ return { ...kept, name, ...(fieldType ? { type: fieldType } : { type: undefined }) };
58
+ }).map(f => JSON.parse(JSON.stringify(f))),
59
+ };
60
+ }
61
+ }
62
+
63
+ DataType.register(StructuredType);
@@ -0,0 +1,15 @@
1
+ export { DataModel } from './DataModel.js';
2
+ export { DataModelPerspective } from './DataModelPerspective.js';
3
+ export { DataModelContext } from './DataModelContext.js';
4
+ export { ServiceDomain } from './ServiceDomain.js';
5
+ export { DataModelChecker } from './DataModelChecker.js';
6
+ export { DataType } from './DataType.js';
7
+ export { EnumerationType } from './EnumerationType.js';
8
+ export { StructuredType } from './StructuredType.js';
9
+ export { ServiceLandscape } from './ServiceLandscape.js';
10
+ export { BianLandscapeReader } from './BianLandscapeReader.js';
11
+ export { BianSemanticApiReader } from './BianSemanticApiReader.js';
12
+ export { LevelSchema } from './LevelSchema.js';
13
+ export { LevelMapping } from './LevelMapping.js';
14
+ export { DataModelFileFormat } from './DataModelFileFormat.js';
15
+ export { LevelEntitiesCompared, LevelRelationshipsCompared, MappingsCompared, ContextsCompared, PerspectivesCompared, DataTypesCompared } from './DataModelCompared.js';
@@ -1,37 +1,43 @@
1
- import { Command } from '../CommandHistory.js';
2
-
3
- /**
4
- * MergeModelCommand — things from another model brought into this one
5
- * (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
6
- * order given (what is needed first). One undo step; undo takes each back,
7
- * last first.
8
- */
9
- export class MergeModelCommand extends Command {
10
- /** A merge is no edit on one view: undoing it shows no other. */
11
- viewId = null;
12
-
13
- /**
14
- * @param {ViewSetModel} model - Merged into
15
- * @param {ViewSetModel} from - Merged from
16
- * @param {Array<{kind: ComparedKind, item: Object}>} steps
17
- * @param {string} [label]
18
- */
19
- constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
20
- super(label);
21
- this.model = model;
22
- this.from = from;
23
- this.steps = steps;
24
- this._undos = [];
25
- }
26
-
27
- execute() {
28
- this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
29
- this.model._notify();
30
- }
31
-
32
- undo() {
33
- for (const undo of [...this._undos].reverse()) undo();
34
- this._undos = [];
35
- this.model._notify();
36
- }
37
- }
1
+ import { Command } from '../CommandHistory.js';
2
+
3
+ /**
4
+ * MergeModelCommand — things from another model brought into this one
5
+ * (ModelComparison#mergeCommand): each step a ComparedKind's `bring`, in the
6
+ * order given (what is needed first). One undo step; undo takes each back,
7
+ * last first.
8
+ */
9
+ export class MergeModelCommand extends Command {
10
+ /** A merge is no edit on one view: undoing it shows no other. */
11
+ viewId = null;
12
+
13
+ /**
14
+ * @param {ViewSetModel} model - Merged into
15
+ * @param {ViewSetModel} from - Merged from
16
+ * @param {Array<{kind: ComparedKind, item: Object}>} steps
17
+ * @param {string} [label]
18
+ */
19
+ constructor(model, from, steps, label = `Merge ${steps.length} item${steps.length === 1 ? '' : 's'} from "${from.name || 'another model'}"`) {
20
+ super(label);
21
+ this.model = model;
22
+ this.from = from;
23
+ this.steps = steps;
24
+ this._undos = [];
25
+ }
26
+
27
+ execute() {
28
+ this._undos = this.steps.map(({ kind, item }) => kind.bring(this.model, item, this.from));
29
+ this._changed();
30
+ }
31
+
32
+ undo() {
33
+ for (const undo of [...this._undos].reverse()) undo();
34
+ this._undos = [];
35
+ this._changed();
36
+ }
37
+
38
+ /** The model changed: a view set tells its listeners; a model without views (a DataModel) says it was edited. @private */
39
+ _changed() {
40
+ this.model._notify?.();
41
+ this.model.edited?.();
42
+ }
43
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * OpenViewList — the views of a model that have a tab, in tab order, as a
3
+ * browser keeps its tabs: closing a tab leaves the view in the model, and a
4
+ * view is opened by going to it (from the tree, a search, a new view). It
5
+ * starts with the view in front only, so a model opened or imported with many
6
+ * views does not fill the tab strip.
7
+ *
8
+ * It follows the model: `sync()` drops views since deleted and opens the one in
9
+ * front, unless paused (`while`: a report passing through every view opens none).
10
+ */
11
+ export class OpenViewList {
12
+ /**
13
+ * @param {ViewSetModel} model
14
+ * @param {Iterable<string>} [ids] - Those open to start with (default: the view in front)
15
+ */
16
+ constructor(model, ids = null) {
17
+ this.model = model;
18
+ this.ids = ids ? [...ids] : (model.currentViewId ? [model.currentViewId] : []);
19
+ this._paused = 0;
20
+ this.sync();
21
+ }
22
+
23
+ /** Follow the model: deleted views gone, the one in front open (last). Returns the open views. */
24
+ sync() {
25
+ this.ids = this.ids.filter(id => this.model.getView(id));
26
+ const current = this.model.currentViewId;
27
+ if (!this._paused && current && !this.ids.includes(current)) this.ids.push(current);
28
+ return this.views();
29
+ }
30
+
31
+ /** The open views, in tab order. */
32
+ views() {
33
+ return this.ids.map(id => this.model.getView(id)).filter(Boolean);
34
+ }
35
+
36
+ isOpen(id) {
37
+ return this.ids.includes(id);
38
+ }
39
+
40
+ /** Open a view's tab (if not open) and bring it to the front. */
41
+ open(id) {
42
+ if (!this.model.getView(id)) return false;
43
+ if (!this.ids.includes(id)) this.ids.push(id);
44
+ if (this.model.currentViewId !== id) this.model.switchTo(id);
45
+ return true;
46
+ }
47
+
48
+ /** Whether a tab may be closed: one always stays open. */
49
+ canClose(id) {
50
+ return this.ids.includes(id) && this.ids.length > 1;
51
+ }
52
+
53
+ /**
54
+ * Close a view's tab; the view stays in the model. Closing the tab in front
55
+ * brings its neighbour (the next, else the one before) to the front.
56
+ * @returns {boolean} whether it closed
57
+ */
58
+ close(id) {
59
+ if (!this.canClose(id)) return false;
60
+ const index = this.ids.indexOf(id);
61
+ this.ids.splice(index, 1);
62
+ if (this.model.currentViewId === id) this.model.switchTo(this.ids[Math.min(index, this.ids.length - 1)]);
63
+ return true;
64
+ }
65
+
66
+ /** Run `fn` with the list paused: going through views (a report's pictures) opens none. */
67
+ while(fn) {
68
+ this._paused++;
69
+ try { return fn(); } finally { this._paused--; }
70
+ }
71
+
72
+ toJSON() {
73
+ return [...this.ids];
74
+ }
75
+ }
@@ -1,34 +1,22 @@
1
- /**
2
- * ViewSetChecker — checks over any ViewSetModel (a template editor's
3
- * DiagramModel, an ArchimateModel): what is wrong or worth a look across its
4
- * views. A notation adds its own checks in a subclass (ArchimateModelChecker).
5
- * DOM-free and advisory.
6
- *
7
- * Each finding: `{ code, severity: 'warning'|'info', message, kind, ids }`;
8
- * `kind` says what the ids name ('view' here; a subclass adds its own).
9
- * Each check is a method listed by `checks()`, so a subclass can add, drop or
10
- * reorder them.
11
- */
12
- export class ViewSetChecker {
13
- /** @param {ViewSetModel} model */
14
- constructor(model) {
15
- this.model = model;
16
- }
17
-
18
- /** The checks to run, in the order their findings are listed. */
19
- checks() {
20
- return [this.emptyViews];
21
- }
22
-
23
- /** Every finding. @returns {Array<Object>} */
24
- run() {
25
- return this.checks().flatMap(check => check.call(this));
26
- }
27
-
28
- /** Views with nothing on them (a template is left out: an empty one is still a start). */
29
- emptyViews() {
30
- return this.model.views()
31
- .filter(v => !v.template && v.isEmpty())
32
- .map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
33
- }
34
- }
1
+ import { ModelChecker } from '../ModelChecker.js';
2
+
3
+ /**
4
+ * ViewSetChecker — checks over any ViewSetModel (a template editor's
5
+ * DiagramModel, an ArchimateModel): what is wrong or worth a look across its
6
+ * views (a ModelChecker: findings, checks listed by `checks()`). A notation
7
+ * adds its own checks in a subclass (ArchimateModelChecker). `kind` in a
8
+ * finding is 'view' here; a subclass adds its own.
9
+ */
10
+ export class ViewSetChecker extends ModelChecker {
11
+ /** The checks to run, in the order their findings are listed. */
12
+ checks() {
13
+ return [this.emptyViews];
14
+ }
15
+
16
+ /** Views with nothing on them (a template is left out: an empty one is still a start). */
17
+ emptyViews() {
18
+ return this.model.views()
19
+ .filter(v => !v.template && v.isEmpty())
20
+ .map(v => ({ code: 'empty-view', severity: 'info', kind: 'view', ids: [v.id], message: `View "${v.title || v.name || v.id}" is empty` }));
21
+ }
22
+ }