@peter.prib/renderengine 0.2.10 → 0.4.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.
Files changed (39) hide show
  1. package/README.md +22 -6
  2. package/REFERENCE.md +123 -2
  3. package/dist/renderengine-workspace.umd.js +3015 -1221
  4. package/dist/renderengine.umd.js +1220 -176
  5. package/package.json +1 -1
  6. package/src/charting/ModelCheckDialog.js +10 -3
  7. package/src/charting/ModelComparisonDialog.js +10 -3
  8. package/src/charting/ModelFileFormat.js +4 -1
  9. package/src/charting/archimate/ArchimateCanvasEditor.js +14 -2
  10. package/src/charting/archimate/ArchimateElementTemplate.js +23 -0
  11. package/src/charting/arrangement/layouts.js +9 -4
  12. package/src/charting/datamodel/DataModel.js +18 -71
  13. package/src/charting/datamodel/DataModelChecker.js +1 -5
  14. package/src/charting/datamodel/DataModelCompared.js +1 -31
  15. package/src/charting/datamodel/LevelSchema.js +23 -0
  16. package/src/charting/datamodel/SqlDdl.js +18 -13
  17. package/src/charting/diagramModel/ListCompared.js +35 -0
  18. package/src/charting/model/Benchmark.js +59 -0
  19. package/src/charting/model/EditableModel.js +104 -0
  20. package/src/charting/{datamodel/DataModelEditCommand.js → model/ModelEditCommand.js} +47 -47
  21. package/src/charting/requirements/RequirementsChecker.js +76 -0
  22. package/src/charting/requirements/RequirementsCompared.js +33 -0
  23. package/src/charting/requirements/RequirementsFileFormat.js +32 -0
  24. package/src/charting/requirements/RequirementsImportFormats.js +48 -0
  25. package/src/charting/requirements/RequirementsModel.js +305 -0
  26. package/src/charting/requirements/Storyboard.js +94 -0
  27. package/src/charting/requirements/TrackerExport.js +273 -0
  28. package/src/charting/requirements/index.js +7 -0
  29. package/src/index.js +6 -0
  30. package/src/workspace/Workspace.js +329 -321
  31. package/src/workspace/index.js +2 -0
  32. package/src/workspace/parts/ArchimateCanvasPart.js +31 -1
  33. package/src/workspace/parts/DataModelPart.js +668 -735
  34. package/src/workspace/parts/EditorPart.js +1 -1
  35. package/src/workspace/parts/ModelFile.js +3 -1
  36. package/src/workspace/parts/ModelHosted.js +176 -0
  37. package/src/workspace/parts/ModelsPart.js +146 -7
  38. package/src/workspace/parts/RequirementsPart.js +465 -0
  39. package/src/workspace/storage/ModelLibrary.js +8 -2
@@ -0,0 +1,104 @@
1
+ import { CommandHistory } from '../CommandHistory.js';
2
+ import { ModelComparison } from '../diagramModel/ModelComparison.js';
3
+ import { ModelEditCommand } from './ModelEditCommand.js';
4
+
5
+ /**
6
+ * EditableModel — base of a model edited as a whole rather than view by view
7
+ * (DataModel, RequirementsModel; a ViewSetModel keeps its views' histories
8
+ * its own way). What a part needs of any model it edits, keeps and merges:
9
+ *
10
+ * - a name;
11
+ * - whether it changed since it was saved (`edited()`, `markSaved()`,
12
+ * `isDirty()`, `onEdit(fn)`), so a part shows it, keeps it and asks before
13
+ * it is lost;
14
+ * - an undo history, every edit one step on it (`editCommand(label, change)`:
15
+ * the model before and after, as its toJSON, put back by `restore`);
16
+ * - how it differs from another of its kind (`compareWith`, over the
17
+ * subclass's `comparedKinds()`), which merges the other's into it.
18
+ *
19
+ * A subclass gives `toJSON()`, its constructor reading that back, and
20
+ * `static STATE`: the fields `restore` puts back.
21
+ */
22
+ export class EditableModel {
23
+ /** The fields a snapshot puts back (restore). A subclass lists its own, `name` among them. */
24
+ static STATE = Object.freeze(['name']);
25
+
26
+ /** @param {{name?: string}} [spec] */
27
+ constructor(spec = {}) {
28
+ if (new.target === EditableModel) throw new TypeError('EditableModel is abstract — subclass it');
29
+ /** What the model is called. */
30
+ this.name = spec.name ?? '';
31
+ this._version = 0;
32
+ this._saved = 0;
33
+ this._editListeners = [];
34
+ }
35
+
36
+ /** The kinds it is compared on and merged by (ComparedKind), in the order a merge brings them. None here. */
37
+ comparedKinds() {
38
+ return [];
39
+ }
40
+
41
+ /** How this model differs from another (ModelComparison), which can merge the other's into it. */
42
+ compareWith(other) {
43
+ return new ModelComparison(this, other);
44
+ }
45
+
46
+ /** Its undo history: a merge is one step on it. */
47
+ get history() {
48
+ return (this._history ??= new CommandHistory());
49
+ }
50
+
51
+ /** A change made as one undo step (ModelEditCommand), not yet run. */
52
+ editCommand(label, change) {
53
+ return new ModelEditCommand(this, label, change);
54
+ }
55
+
56
+ /** Put the model back as a snapshot (its toJSON) says: the fields its class lists (STATE). */
57
+ restore(json) {
58
+ const back = new this.constructor(JSON.parse(JSON.stringify(json)));
59
+ for (const key of this.constructor.STATE) this[key] = back[key];
60
+ }
61
+
62
+ // ===== Changed since saved =====
63
+
64
+ /** Something changed: listeners hear of it. */
65
+ edited() {
66
+ this._version++;
67
+ this._emitEdit();
68
+ }
69
+
70
+ /** This state is the saved one (a file opened or saved). */
71
+ markSaved() {
72
+ this._saved = this._version;
73
+ this._emitEdit();
74
+ }
75
+
76
+ /** Whether it changed since markSaved. */
77
+ isDirty() {
78
+ return this._version !== this._saved;
79
+ }
80
+
81
+ /** Call `fn(model)` after every change (and markSaved). Returns an unsubscribe function. */
82
+ onEdit(fn) {
83
+ this._editListeners.push(fn);
84
+ return () => { this._editListeners = this._editListeners.filter(l => l !== fn); };
85
+ }
86
+
87
+ /** @private */
88
+ _emitEdit() {
89
+ for (const fn of this._editListeners) fn(this);
90
+ }
91
+
92
+ /** An id made from `name`, unlike any of `taken`'s ids ('customer', 'customer-2'). */
93
+ _freshId(name, taken) {
94
+ const base = String(name ?? 'item').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'item';
95
+ let id = base;
96
+ for (let n = 2; taken.some(t => t.id === id); n++) id = `${base}-${n}`;
97
+ return id;
98
+ }
99
+
100
+ /** The model as plain JSON, read back by its constructor. @abstract */
101
+ toJSON() {
102
+ throw new Error(`${this.constructor.name} must implement toJSON()`);
103
+ }
104
+ }
@@ -1,47 +1,47 @@
1
- import { Command } from '../CommandHistory.js';
2
-
3
- /**
4
- * DataModelEditCommand — one edit of a DataModel (an entity, an attribute, a
5
- * relationship, a delete) as one undo step: run, it makes the change and keeps
6
- * the model as it was before and after (DataModel#toJSON); undone or redone, it
7
- * puts the model back as it was (DataModel#restore). Any change to a model is
8
- * undoable this way, whatever it touches.
9
- */
10
- export class DataModelEditCommand extends Command {
11
- /** A model edit is no edit on one view: undoing it shows no other. */
12
- viewId = null;
13
-
14
- /**
15
- * @param {DataModel} model
16
- * @param {string} label - What the undo menu calls it ('Edit Customer')
17
- * @param {(model: DataModel) => *} change - Makes the change
18
- */
19
- constructor(model, label, change) {
20
- super(label);
21
- this.model = model;
22
- this.change = change;
23
- this.before = null;
24
- this.after = null;
25
- }
26
-
27
- execute() {
28
- if (this.after) {
29
- this.model.restore(this.after);
30
- } else {
31
- this.before = DataModelEditCommand.snapshot(this.model);
32
- this.result = this.change(this.model);
33
- this.after = DataModelEditCommand.snapshot(this.model);
34
- }
35
- this.model.edited();
36
- }
37
-
38
- undo() {
39
- this.model.restore(this.before);
40
- this.model.edited();
41
- }
42
-
43
- /** The model as it is, a copy nothing later changes (its toJSON shares some arrays). */
44
- static snapshot(model) {
45
- return JSON.parse(JSON.stringify(model.toJSON()));
46
- }
47
- }
1
+ import { Command } from '../CommandHistory.js';
2
+
3
+ /**
4
+ * ModelEditCommand — one edit of an EditableModel (a data model's entity, a
5
+ * requirements model's story, a delete) as one undo step: run, it makes the
6
+ * change and keeps the model as it was before and after (its toJSON); undone
7
+ * or redone, it puts the model back as it was (its restore). Any change to a
8
+ * model is undoable this way, whatever it touches.
9
+ */
10
+ export class ModelEditCommand extends Command {
11
+ /** A model edit is no edit on one view: undoing it shows no other. */
12
+ viewId = null;
13
+
14
+ /**
15
+ * @param {EditableModel} model
16
+ * @param {string} label - What the undo menu calls it ('Edit Customer')
17
+ * @param {(model: EditableModel) => *} change - Makes the change
18
+ */
19
+ constructor(model, label, change) {
20
+ super(label);
21
+ this.model = model;
22
+ this.change = change;
23
+ this.before = null;
24
+ this.after = null;
25
+ }
26
+
27
+ execute() {
28
+ if (this.after) {
29
+ this.model.restore(this.after);
30
+ } else {
31
+ this.before = ModelEditCommand.snapshot(this.model);
32
+ this.result = this.change(this.model);
33
+ this.after = ModelEditCommand.snapshot(this.model);
34
+ }
35
+ this.model.edited();
36
+ }
37
+
38
+ undo() {
39
+ this.model.restore(this.before);
40
+ this.model.edited();
41
+ }
42
+
43
+ /** The model as it is, a copy nothing later changes (its toJSON may share arrays). */
44
+ static snapshot(model) {
45
+ return JSON.parse(JSON.stringify(model.toJSON()));
46
+ }
47
+ }
@@ -0,0 +1,76 @@
1
+ import { ModelChecker } from '../ModelChecker.js';
2
+
3
+ /**
4
+ * RequirementsChecker — what a requirements model is missing, as a team
5
+ * reviewing a backlog would say it: a story with no acceptance criteria
6
+ * (nothing to say when it is done), one that says neither who wants it nor
7
+ * what (no "As a … I want …"), one that traces to nothing (nothing realises
8
+ * it), one under no epic, an epic with no stories, two stories of one name.
9
+ *
10
+ * With `resolve(trace)` — the host's way to find what a trace names in its
11
+ * other models (true, false, or undefined when it cannot tell) — a trace to
12
+ * something that is not there is found too.
13
+ *
14
+ * Findings name a story or an epic (`kind`), as ModelChecker's do.
15
+ */
16
+ export class RequirementsChecker extends ModelChecker {
17
+ /**
18
+ * @param {RequirementsModel} model
19
+ * @param {{resolve?: (trace: Object) => (boolean|undefined)}} [options]
20
+ */
21
+ constructor(model, { resolve = null } = {}) {
22
+ super(model);
23
+ this.resolve = resolve;
24
+ }
25
+
26
+ checks() {
27
+ return [this.noCriteria, this.noNarrative, this.untraced, this.brokenTraces, this.noEpic, this.emptyEpics, this.sameNames];
28
+ }
29
+
30
+ /** @private */
31
+ _story(story, code, severity, message) {
32
+ return { code, severity, kind: 'story', ids: [story.id], message };
33
+ }
34
+
35
+ noCriteria() {
36
+ return this.model.stories.filter(s => !s.criteria.length)
37
+ .map(s => this._story(s, 'story-no-criteria', 'warning', `"${s.name}" has no acceptance criteria: nothing says when it is done`));
38
+ }
39
+
40
+ noNarrative() {
41
+ return this.model.stories.filter(s => !s.want && !s.description)
42
+ .map(s => this._story(s, 'story-no-narrative', 'info', `"${s.name}" says neither who wants it nor what (As a … I want … so that …)`));
43
+ }
44
+
45
+ untraced() {
46
+ return this.model.stories.filter(s => !s.traces.length)
47
+ .map(s => this._story(s, 'story-untraced', 'info', `"${s.name}" traces to nothing that realises it`));
48
+ }
49
+
50
+ brokenTraces() {
51
+ if (!this.resolve) return [];
52
+ return this.model.stories.flatMap(s => s.traces.filter(t => this.resolve(t) === false)
53
+ .map(t => this._story(s, 'trace-missing', 'warning', `"${s.name}" traces to ${t.kind} "${t.name || t.ref}", which is not there`)));
54
+ }
55
+
56
+ noEpic() {
57
+ if (!this.model.epics.length) return [];
58
+ return this.model.stories.filter(s => !this.model.epic(s.epic))
59
+ .map(s => this._story(s, 'story-no-epic', 'info', `"${s.name}" is under no epic`));
60
+ }
61
+
62
+ emptyEpics() {
63
+ return this.model.epics.filter(e => !this.model.storiesOf(e.id).length)
64
+ .map(e => ({ code: 'epic-empty', severity: 'info', kind: 'epic', ids: [e.id], message: `Epic "${e.name}" has no stories` }));
65
+ }
66
+
67
+ sameNames() {
68
+ const byName = new Map();
69
+ for (const s of this.model.stories) {
70
+ const key = s.name.trim().toLowerCase();
71
+ if (key) byName.set(key, [...(byName.get(key) ?? []), s]);
72
+ }
73
+ return [...byName.values()].filter(list => list.length > 1)
74
+ .map(list => ({ code: 'story-duplicate', severity: 'warning', kind: 'story', ids: list.map(s => s.id), message: `${list.length} stories are called "${list[0].name}"` }));
75
+ }
76
+ }
@@ -0,0 +1,33 @@
1
+ import { ListCompared } from '../diagramModel/ListCompared.js';
2
+
3
+ /**
4
+ * The kinds two RequirementsModels are compared on and merged by
5
+ * (ModelComparison): epics, releases, then stories, in the order a merge
6
+ * brings them — a story needs its epic and its release.
7
+ */
8
+
9
+ export class EpicsCompared extends ListCompared {
10
+ get key() { return 'epic'; }
11
+ get label() { return 'Epics'; }
12
+ list(model) { return model.epics; }
13
+ content(epic) { return { ...epic }; }
14
+ }
15
+
16
+ export class ReleasesCompared extends ListCompared {
17
+ get key() { return 'release'; }
18
+ get label() { return 'Releases'; }
19
+ list(model) { return model.releases; }
20
+ content(release) { return { ...release }; }
21
+ }
22
+
23
+ /** Stories: compared on everything, where they sit in the list aside. */
24
+ export class StoriesCompared extends ListCompared {
25
+ get key() { return 'story'; }
26
+ get label() { return 'Stories'; }
27
+ list(model) { return model.stories; }
28
+ content(story) { return JSON.parse(JSON.stringify(story)); }
29
+
30
+ requires(story) {
31
+ return [...(story.epic ? [['epic', story.epic]] : []), ...(story.release ? [['release', story.release]] : [])];
32
+ }
33
+ }
@@ -0,0 +1,32 @@
1
+ import { ModelFileFormat } from '../ModelFileFormat.js';
2
+ import { RequirementsModel } from './RequirementsModel.js';
3
+
4
+ /**
5
+ * RequirementsFileFormat — a requirements model as a file
6
+ * (`.requirements.json`): its epics, releases and stories
7
+ * (RequirementsModel#toJSON), read back by `new RequirementsModel(json)`.
8
+ */
9
+ export class RequirementsFileFormat extends ModelFileFormat {
10
+ get label() { return '.requirements.json'; }
11
+ get accept() { return '.requirements.json,.json'; }
12
+ get fileName() { return 'model.requirements.json'; }
13
+ get featureName() { return 'Requirements'; }
14
+
15
+ /** JSON with a requirements model's shape: stories, or epics. */
16
+ recognizes(text) {
17
+ try {
18
+ const json = JSON.parse(text);
19
+ return !!json && typeof json === 'object' && (Array.isArray(json.stories) || Array.isArray(json.epics));
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ export(model) {
26
+ return { data: JSON.stringify(model.toJSON(), null, 2), mimeType: 'application/json' };
27
+ }
28
+
29
+ import(text) {
30
+ return new RequirementsModel(JSON.parse(text));
31
+ }
32
+ }
@@ -0,0 +1,48 @@
1
+ import { ModelFileFormat } from '../ModelFileFormat.js';
2
+ import { RequirementsModel } from './RequirementsModel.js';
3
+ import { TrackerExport } from './TrackerExport.js';
4
+ import { Storyboard } from './Storyboard.js';
5
+
6
+ /**
7
+ * The files a requirements model is read from that another tool wrote — they
8
+ * import only: a tracker's export (TrackerExport: Jira, Azure DevOps,
9
+ * GitLab, CSV or JSON) and a storyboard (Storyboard: Markdown). Each reads
10
+ * the file as a new model; `spec(text)` is what it holds, for a part that
11
+ * brings it into the model open (RequirementsModel#bringCommand).
12
+ */
13
+ class RequirementsImportFormat extends ModelFileFormat {
14
+ get exports() { return false; }
15
+
16
+ /** What the file holds, as a RequirementsModel spec. @abstract */
17
+ spec(text) { throw new Error(`${this.constructor.name} must implement spec()`); }
18
+
19
+ import(text) {
20
+ return new RequirementsModel(this.spec(text));
21
+ }
22
+ }
23
+
24
+ export class TrackerExportFileFormat extends RequirementsImportFormat {
25
+ get label() { return 'Tracker export (Jira, Azure DevOps, GitLab)'; }
26
+ get accept() { return '.csv,.json'; }
27
+ get featureName() { return 'TrackerExport'; }
28
+
29
+ /** A CSV or JSON export one of the trackers wrote. */
30
+ recognizes(text) {
31
+ return !!TrackerExport.detect(text);
32
+ }
33
+
34
+ spec(text) {
35
+ const { tracker, ...spec } = new TrackerExport().read(text);
36
+ return { name: `${TrackerExport.label(tracker)} export`, ...spec };
37
+ }
38
+ }
39
+
40
+ export class StoryboardFileFormat extends RequirementsImportFormat {
41
+ get label() { return 'Storyboard (Markdown)'; }
42
+ get accept() { return '.md,.markdown,.txt'; }
43
+ get featureName() { return 'Storyboard'; }
44
+
45
+ spec(text) {
46
+ return new Storyboard().read(text);
47
+ }
48
+ }