@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
package/README.md
CHANGED
|
@@ -29,6 +29,10 @@ open the pages directly:
|
|
|
29
29
|
- `examples/diagram-editor.html`: the diagram editor
|
|
30
30
|
- `examples/data-model.html`: the data modeller
|
|
31
31
|
|
|
32
|
+
The models they open come from [modelengine](https://gitlab.com/peter.prib/modelengine), kept apart from the tools: with
|
|
33
|
+
it checked out beside renderengine, the pages open on its models (File ▸ Open from ▸ modelengine lists them by domain);
|
|
34
|
+
without it they start empty.
|
|
35
|
+
|
|
32
36
|
All work in current Chrome, Edge, Firefox and Safari. Save writes back to the file you opened in Chrome and Edge;
|
|
33
37
|
other browsers download the file instead.
|
|
34
38
|
|
package/REFERENCE.md
CHANGED
|
@@ -812,6 +812,22 @@ types — see `ARCHITECTURE.md`):
|
|
|
812
812
|
is not there. Model ▸ **Check model…** lists the findings in the `ModelCheckDialog` the diagram editors' check uses;
|
|
813
813
|
choosing one shows it (the entity's level, the service domain seen through, the mapping view).
|
|
814
814
|
|
|
815
|
+
**Likely duplicate entities.** **Find likely duplicates…** (Model ▸, Tools ▸ and the model's tab; the part's
|
|
816
|
+
`modelMenuItems()`) asks for a level (the one in front first) and how to compare, with the form the ArchiMate
|
|
817
|
+
modeller uses (`duplicateSearchFields`): names, definitions or attributes (their names together), exactly or by their
|
|
818
|
+
words, and the noise to leave out. The groups found are listed as the ArchiMate modeller lists them
|
|
819
|
+
(`DuplicatesDialog`, below), and merging shows the entities side by side first (`model.entityComparison(level, ids)`:
|
|
820
|
+
name, definition, each attribute, relationships, what each is mapped to; `MergeCompareDialog`): choose the entity
|
|
821
|
+
kept and, when their definitions differ, which (or all joined); entities alike merge without asking. The kept one takes the
|
|
822
|
+
attributes it lacks (by name, any case), their relationships (one left joining it to itself, or doubling one there,
|
|
823
|
+
goes) and their mapping pairs; the others go, perspectives and contexts forgetting them, one undo step
|
|
824
|
+
(`model.duplicateEntities(level, options)`, `model.mergeEntitiesCommand(level, keepId, otherIds, { description })`).
|
|
825
|
+
|
|
826
|
+
```js
|
|
827
|
+
model.duplicateEntities('logical', { by: 'attributes' }); // [{ code: 'possible-duplicate', level, ids, message }]
|
|
828
|
+
part.runModelEdit(model.mergeEntitiesCommand('logical', 'customer', ['client']));
|
|
829
|
+
```
|
|
830
|
+
|
|
815
831
|
**Checked as DMBOK2 reviews a model.** The check also finds, at the logical and physical levels: an entity with
|
|
816
832
|
attributes and no primary key (`no-primary-key`); a foreign key whose `references` (an entity, or `entity.attribute`,
|
|
817
833
|
by id or name) is not there (`reference-missing`) or has no relationship with its entity either way
|
|
@@ -949,8 +965,9 @@ types — see `ARCHITECTURE.md`):
|
|
|
949
965
|
service domains.
|
|
950
966
|
`model.serviceDomainsCommand(specs, source)` brings them in as one undo step: a new one is added; one already
|
|
951
967
|
there (by id, else by name) keeps its control record, qualifiers and the rest, taking its area and domain from
|
|
952
|
-
the landscape, and its asset and pattern where it has none.
|
|
953
|
-
|
|
968
|
+
the landscape, and its asset and pattern where it has none. modelengine's banking domain has a small excerpt in
|
|
969
|
+
that shape to try it with (`test/fixtures/bian-landscape-sample.xml` here; not BIAN's landscape, which is BIAN's to
|
|
970
|
+
publish).
|
|
954
971
|
|
|
955
972
|
```javascript
|
|
956
973
|
const specs = new BianLandscapeReader().read(BianLandscapeReader.parse(xml));
|
|
@@ -1045,7 +1062,8 @@ types — see `ARCHITECTURE.md`):
|
|
|
1045
1062
|
and Redo; Tools ▸ Compare with and Merge from (the other open models and the bases; a merge is one undo step); Model ▸ New / Edit / Delete perspective and context (those dialogs), Perspective and Context selects (when
|
|
1046
1063
|
it has some; a perspective brings its context, and the context's scope shows beside them), Level (Conceptual,
|
|
1047
1064
|
Logical, Physical, and Mapping), Notation, and Export ▸ PNG / SVG. A box or note dragged stays on the level it
|
|
1048
|
-
was dragged on (the mapping view's in its own layout).
|
|
1065
|
+
was dragged on (the mapping view's in its own layout). modelengine's enterprise ontology, which
|
|
1066
|
+
`examples/data-model.html` (`npm run datamodel`) opens when modelengine is checked out beside renderengine, uses every
|
|
1049
1067
|
feature above. A data model can also be registered like a chart:
|
|
1050
1068
|
`engine.register(id, 'dataModel', { entities, relationships, level })` then `engine.refreshChart(id)` (each
|
|
1051
1069
|
diagram catalog entry names its data options, `args`).
|
|
@@ -1077,6 +1095,13 @@ but these recur across most 2D chart types:
|
|
|
1077
1095
|
}
|
|
1078
1096
|
```
|
|
1079
1097
|
|
|
1098
|
+
- **Legend placement** (`Legend.beside`, every chart with a legend): to the right of the plot when there is
|
|
1099
|
+
room in the drawing; otherwise, since the plot usually fills it, inside the plot's top-right corner on a
|
|
1100
|
+
light backing. A chart of points (scatter, bubble) marks its entries with circles, the others with squares.
|
|
1101
|
+
- **Category labels** (line, area, bar, stacked bar, candlestick, waterfall, streamgraph): as many as fit.
|
|
1102
|
+
When they would overprint, every n-th is drawn, n from the longest label and the plot width
|
|
1103
|
+
(`Axis.categoryLabelStep(labels, width, { fontSize })`).
|
|
1104
|
+
|
|
1080
1105
|
Data-resolution options (used with `register()`/`refreshChart()`, see below):
|
|
1081
1106
|
|
|
1082
1107
|
```javascript
|
|
@@ -1112,6 +1137,8 @@ engine.setGlobalData(data) // set a global object referenceable by
|
|
|
1112
1137
|
engine.resolvePath(path, mapper?) // e.g. resolvePath('metrics.sales.0.value')
|
|
1113
1138
|
engine.register(id, type, options) // remember a {type, options} pair for a container id
|
|
1114
1139
|
engine.refreshChart(id) // re-invoke it — resolves options.data or dataPath+dataMapper fresh each time
|
|
1140
|
+
engine.refreshAll() // every registered chart
|
|
1141
|
+
engine.setData(data) // setGlobalData(data), then refreshAll(): a live page's poll
|
|
1115
1142
|
engine.niceTicks(maxVal, count=4) // readable axis tick values
|
|
1116
1143
|
engine.formatValue(v) // 1234567 -> "1.23M"
|
|
1117
1144
|
engine.bindTooltip(element, text)
|
|
@@ -1120,7 +1147,24 @@ engine.adjustColor(hex, amount) // lighten/darken, used internally for 3
|
|
|
1120
1147
|
```
|
|
1121
1148
|
|
|
1122
1149
|
`register`/`refreshChart` is the registration mechanism `addDataPoint`/`addGraphNode` (next section)
|
|
1123
|
-
build on.
|
|
1150
|
+
build on. **One engine keeps a page of charts:** each registered id is drawn in the element with that id, by
|
|
1151
|
+
the engine itself when it is its own container, otherwise by an engine of its own for that element, made with
|
|
1152
|
+
this engine's options and kept while the element is the same, so a zoom survives the next refresh. A chart
|
|
1153
|
+
whose `dataPath` resolves to nothing yet is not drawn.
|
|
1154
|
+
|
|
1155
|
+
```javascript
|
|
1156
|
+
const charts = new RenderEngine(null, { rendererType: 'svg', zoom: { wheelModifier: 'ctrl' } });
|
|
1157
|
+
charts.register('cpu-gauge', 'gauge-dial', { dataPath: 'cpuPercent' });
|
|
1158
|
+
charts.register('perf-chart', 'line', {
|
|
1159
|
+
dataPath: 'history',
|
|
1160
|
+
dataMapper: (h) => [{ name: 'CPU', values: h.map(x => ({ label: x.t, value: x.cpu })) }],
|
|
1161
|
+
});
|
|
1162
|
+
setInterval(async () => charts.setData(await (await fetch('/api/metrics')).json()), 2000);
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
**Zoom.** An SVG chart zooms with the mouse wheel. The engine option `zoom: { wheelModifier: 'ctrl' }`
|
|
1166
|
+
(or `'alt'`, `'shift'`; `ctrl` also takes ⌘) zooms only with that key held, so a plain wheel scrolls a page of
|
|
1167
|
+
charts; `zoom: false` turns it off.
|
|
1124
1168
|
|
|
1125
1169
|
## Interactivity
|
|
1126
1170
|
|
|
@@ -1359,6 +1403,24 @@ do, and a folder of one type in the Layers tree too (`typeGlyph(typeName, { size
|
|
|
1359
1403
|
`archimate/typeGlyph.js`, the one drawing both use; a 3.2 type under ArchiMate 4 is drawn as 3.2 draws it).
|
|
1360
1404
|
`TreeBuilder`'s `iconFor(node)` gives a node a picture in place of its `icon` text.
|
|
1361
1405
|
|
|
1406
|
+
**Tree rows and long folders.** A tree row is one compact line: a label too long for the panel ends in an ellipsis,
|
|
1407
|
+
with the whole of it as the row's tooltip. A folder shows its first `leafLimit` leaves (8 in the model tree,
|
|
1408
|
+
`ModelTree.LEAF_LIMIT`, set per tree by `new ModelTree(model, { leafLimit })` or `setOptions({ leafLimit })`), then
|
|
1409
|
+
a "▸ N more" row that shows the rest; the folders inside it always show. A folder asked for more keeps showing all
|
|
1410
|
+
when the tree is redrawn (`TreeBuilder`'s `shownAll`, a Set of node ids the caller keeps, as it keeps `openState`).
|
|
1411
|
+
`TreeBuilder`'s `filter(node)` says which leaves to show: a folder shows when a leaf under it does, and opens while
|
|
1412
|
+
filtering (without changing what `openState` remembers); one with nothing kept inside is left out, whatever its own
|
|
1413
|
+
name.
|
|
1414
|
+
|
|
1415
|
+
```js
|
|
1416
|
+
const tree = new TreeBuilder(document, {
|
|
1417
|
+
container, rootNodes, getNodeChildren: (node) => node.kids ?? [],
|
|
1418
|
+
leafLimit: 8, shownAll: new Set(),
|
|
1419
|
+
filter: (node) => node.label.toLowerCase().includes('order'), // null: every node
|
|
1420
|
+
});
|
|
1421
|
+
tree.render();
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1362
1424
|
**Nesting.** In the ArchiMate canvas editor, dropping a box wholly inside an element's box nests it there (Archi's
|
|
1363
1425
|
nested elements); so does dropping an element with its centre on another element's box when a relationship a
|
|
1364
1426
|
nesting stands for is allowed between them (`nestsOnDrop`), and the parent then grows to show it whole, with its
|
|
@@ -1441,9 +1503,15 @@ there (`editor.addImage(dataURL, point)`, one undo step, selected; the base edit
|
|
|
1441
1503
|
relationships) under the palette, for an element chosen on the canvas or in the tree. The tree's own filters (its
|
|
1442
1504
|
"Filters…" dialog: text, types, specializations, properties) dim what they leave out on the canvas too:
|
|
1443
1505
|
`modelTree.matchingElementIds()` (null with no filter on) and `modelTree.onFilterChange(fn)` feed
|
|
1444
|
-
`editor.setElementFilter`.
|
|
1506
|
+
`editor.setElementFilter`. The search text filters from 3 characters on (`ModelTree.MIN_FILTER_CHARS`; per tree,
|
|
1507
|
+
`minFilterChars` or `setOptions({ minFilterChars })`); shorter, nothing is filtered (`modelTree.filterText` is the
|
|
1508
|
+
text in effect, `getFilterText()` the text typed). While it filters, the box is marked (`data-filtering`, a blue
|
|
1509
|
+
border) and a line under it says what was found ("Filtering: 3 elements, 1 view found"); with fewer characters it
|
|
1510
|
+
says how many more to type. It finds views, templates and folders by name as well as
|
|
1511
|
+
elements, and leaves out the folders with nothing found in them (`modelTree.textMatches(name, documentation)`; type,
|
|
1512
|
+
specialization and property filters are the elements' only). Under the search box, **In:** chooses what the search covers, the whole model or only
|
|
1445
1513
|
the diagram in front (`modelTree.setSearchScope('model' | 'view')`, `ModelTree.SEARCH_SCOPES`), and
|
|
1446
|
-
**Descriptions** looks for the text in elements' and
|
|
1514
|
+
**Descriptions** looks for the text in elements', relationships' and views' documentation too
|
|
1447
1515
|
(`modelTree.setSearchInDescriptions(true)`). The Filters… dialog opens showing the filters on now
|
|
1448
1516
|
(`getTypeFilter()`, `getSpecializationFilter()`, `getPropertyFilter()`). Its properties start unticked (no property
|
|
1449
1517
|
filter; `OptInCheckboxTreeSelector`: a value is ticked to keep what has it), and a property with more than 10
|
|
@@ -1470,7 +1538,11 @@ leaves it alone). Any `Edge` can say so (`editable`, default true) and add to it
|
|
|
1470
1538
|
|
|
1471
1539
|
**Selecting several shapes.** In the base editor (so the template editor and the ArchiMate canvas alike): drag a
|
|
1472
1540
|
box on empty canvas to select the shapes wholly inside it (Ctrl or Cmd adds them to the selection;
|
|
1473
|
-
`editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one
|
|
1541
|
+
`editor.templatesInside(rect)`, `editor.selectTemplates(ids, { extend })`); Shift- or Ctrl-click adds one, and so
|
|
1542
|
+
does a right-click with Shift or Ctrl, before the menu opens for the whole selection; a plain right-click on a shape
|
|
1543
|
+
outside the selection selects it alone (`editor.selectForMenu(id, { extend })`). The last shape chosen has the
|
|
1544
|
+
handles; every other selected shape is marked by a dashed, tinted outline set off its border
|
|
1545
|
+
(`DiagramEditor.SELECTION_OUTLINE`: `pad`, `stroke`, `strokeWidth`, `dash`, `fill`);
|
|
1474
1546
|
**Ctrl+A** selects every shape that is drawn (`selectableIds()`). Whatever is selected acts together, each as one
|
|
1475
1547
|
undo step: dragging any of them moves them all (with their groups and what they hold); the arrow keys nudge
|
|
1476
1548
|
them a grid step, one pixel without a grid, ten times as far with Shift (`nudgeSelection(dx, dy)`); **Delete**
|
|
@@ -1634,6 +1706,28 @@ whose schema changed) gives them afresh with `models.setBases(specs)`; what is o
|
|
|
1634
1706
|
(`models.viewBase(i)`) opens a base to look at: a tab of its own copy, named "… (view)", that can be laid out and
|
|
1635
1707
|
moved about but is never kept (`part.keepOff`).
|
|
1636
1708
|
|
|
1709
|
+
**Content: models kept apart from the tools.** Models, data models and user stories by domain live apart from the
|
|
1710
|
+
modellers (modelengine, or any collection laid out like it): a `manifest.json` listing items by domain, each a file in
|
|
1711
|
+
a format a modeller already reads. A `ContentSource` reads one, fetched from its address (`url`: a folder holding
|
|
1712
|
+
`manifest.json`, or the manifest itself) or given whole (`bundle`: `{ manifest, files }`, what modelengine's
|
|
1713
|
+
`dist/modelengine.js` puts on `globalThis.ModelEngineContent` for a page opened off disk; `ContentSource.fromPage()`).
|
|
1714
|
+
Nothing in the tools lists what a collection has: `source.items({ formats })` offers the items a modeller's file
|
|
1715
|
+
formats read, each matched by its file's whole ending (`ContentSource.formatFor(item, formats)`: the one format
|
|
1716
|
+
accepting all after the name's first dot, so `.requirements.json` is not any `.json`), and `source.open(item,
|
|
1717
|
+
formats)` opens one through that format. A `ModelsPart` offers its sources (`ContentSources`: the page's bundle, its
|
|
1718
|
+
spec's `content: [{ label?, url }]`, the host's `contentSources` service, and those added with **Add a source…**,
|
|
1719
|
+
kept in this browser) under File ▸ **Open from** ▸ *source* ▸ *domain* ▸ *item* (`models.contentItems()`); a source
|
|
1720
|
+
not read says why there. An item opens in a tab of its own (`models.openContent(source, item)`) remembering where it
|
|
1721
|
+
came from (`part.contentOrigin`), so its tab menu and Tools offer **Update from *source*…**: the item read again and
|
|
1722
|
+
its changes reviewed as a merge, one undo step, keeping what was changed here (`models.updateFromContent(part)`).
|
|
1723
|
+
`openContent: 'first'` (or an item's id) in the spec opens that item on a first visit, in the empty model's place.
|
|
1724
|
+
|
|
1725
|
+
```js
|
|
1726
|
+
const source = new ContentSource({ url: 'https://example.org/modelengine/' });
|
|
1727
|
+
const [item] = await source.items({ formats: [new DataModelFileFormat()] }); // what the data modeller reads
|
|
1728
|
+
const { model } = await source.open(item, [new DataModelFileFormat()]);
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1637
1731
|
**Kept in this browser, and backed up.** With `keep: true` in its spec, a `ModelsPart` keeps every model it holds
|
|
1638
1732
|
in this browser, so work outlives the page without saving a file: a moment after each edit (`ModelsPart.KEEP_DELAY`)
|
|
1639
1733
|
and at once when imported, each in its editor's own file format (`part.keepEntry()`: the file's format, else the
|
|
@@ -1840,20 +1934,56 @@ template editor (the testbed) and both ArchiMate editors have them. The **View**
|
|
|
1840
1934
|
(`editor.viewMenuItems()`) has **Zoom to fit** (`editor.zoomToFit()`: everything drawn in sight, in the
|
|
1841
1935
|
drawing's own units; `ViewFit` computes it, as a viewBox for the graph engine or a scale and pan for the
|
|
1842
1936
|
template editor), **Reset zoom**, **Highlight…** (below), and, when the editor has a `model`, **Check model…**
|
|
1843
|
-
(`editor.showModelCheck()`). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
|
|
1937
|
+
(`editor.showModelCheck()`; `showFindings(findings, { title, empty })` shows any findings in its dialog). The checks are the model's own (`model.checker(options)`): a `ViewSetChecker` for
|
|
1844
1938
|
any view set (empty views; an empty template is not one), and for an ArchiMate model an `ArchimateModelChecker`
|
|
1845
1939
|
that adds relationships the specification does not allow, likely duplicates (elements of one type whose names
|
|
1846
|
-
have the same words, ignoring case, punctuation, word order
|
|
1940
|
+
have the same words, ignoring case, punctuation, word order, plurals ("Customers" is "Customer", `singular(word)`)
|
|
1941
|
+
and filler words such as "of" and "the":
|
|
1847
1942
|
`nameKey(name)` and `duplicateGroups(elements)` in `diagrams/duplicates.js`), elements and relationships on no
|
|
1848
1943
|
view, nested elements with no relationship, and names or documentation with unprintable characters (control
|
|
1849
1944
|
characters, zero-width spaces; tabs and line breaks are fine in documentation). Each check is a method listed by
|
|
1850
1945
|
`checks()`, so a subclass changes the set; choosing a finding shows it (`editor.revealFinding(finding)`: its view,
|
|
1851
1946
|
its shape, or in the ArchiMate tree), and a finding can offer actions beside it (`editor.findingActions(finding)`).
|
|
1852
|
-
On the ArchiMate canvas, likely duplicates have **Merge
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1947
|
+
On the ArchiMate canvas, likely duplicates have **Merge…**, and **Find likely duplicates…**, a tool for the whole
|
|
1948
|
+
model, is on the model's tab menu and in **Tools** (a part's `modelMenuItems()`, the editor's for an `EditorPart`;
|
|
1949
|
+
in its **Edit** menu when no `ModelsPart` hosts it). It asks where to look (the whole model, this view, the elements
|
|
1950
|
+
selected: `editor.duplicateScopes()`) and how to compare, then lists the groups found (a `DuplicatesDialog`, one for
|
|
1951
|
+
every modeller): each with a box to tick, its message (choosing it shows it) and **Merge…**; **Merge selected** merges
|
|
1952
|
+
the ticked groups one after another. The list stays open while they are merged, found again after each merge, so what
|
|
1953
|
+
was merged leaves it (`editor.askDuplicates()`, `editor.showDuplicates(options, { within })`, `checker.possibleDuplicates(options, { within })`).
|
|
1954
|
+
Its form is one for every modeller (`duplicateSearchFields({ scope, compare, properties, specializations })`,
|
|
1955
|
+
`duplicateSearchOptions(values)`, `validateDuplicateSearch`). The options are the jArchi dedupe scripts' (`DUPLICATE_OPTIONS`,
|
|
1956
|
+
`duplicateGroups(elements, options)`, `duplicateKey(element, options)`, `wordKey(text, options)`): compare the
|
|
1957
|
+
names, the documentation (an element with none, or only noise, is left out) or a property's value (an application
|
|
1958
|
+
id); leave out noise words (`NOISE_WORDS`: filler, "tbc", "tba" …), numbers and version numbers ("2", "v2",
|
|
1959
|
+
"1.2.3"), one-letter words and words matching a pattern (`^[Aa][0-9A-Fa-f]{5}$` for id codes); take a plural as its
|
|
1960
|
+
word; look across types; leave out a specialization (Decommissioned). **Match** `exact` compares the text as it is,
|
|
1961
|
+
case and spacing aside; `words` (the default) by its words, as above. Any other field can be compared by naming it
|
|
1962
|
+
(`by: 'attributes'`; a list is its items' names together). The model check compares names by their words with the
|
|
1963
|
+
filler words only.
|
|
1964
|
+
|
|
1965
|
+
```js
|
|
1966
|
+
editor.showDuplicates({ by: 'documentation', noiseWords: NOISE_WORDS, ignoreNumbers: true });
|
|
1967
|
+
editor.showDuplicates({ by: 'property', property: 'AppId', acrossTypes: true });
|
|
1968
|
+
```
|
|
1969
|
+
|
|
1970
|
+
With two or more elements selected, a shape's menu has **Merge N elements…** for them
|
|
1971
|
+
(`editor.elementIdsOf(boxIds)`: each box's element, once). Merging (`editor.askMerge(ids)`, resolving to the command
|
|
1972
|
+
or null) shows the elements side by side first (a `MergeCompareDialog`, one for every modeller: a column each, a row
|
|
1973
|
+
each for name, type, specialization, documentation, each property, relationships and views, the rows where they
|
|
1974
|
+
differ marked ≠; `editor.mergeComparison(elements)`), and under it asks which element to keep (each choice says how
|
|
1975
|
+
many views and relationships use it) and, where they differ, what it ends with: the documentation (all of it joined,
|
|
1976
|
+
the default, or one element's), the value of each property they have with different values, the specialization
|
|
1977
|
+
(`editor.mergeChoices(elements)`). **OK, merge** merges; Cancel leaves them. Elements that differ only in the views
|
|
1978
|
+
they are on merge into the first without asking. The others are merged into it, one undo step (`model.mergeCommand(keepId,
|
|
1979
|
+
otherIds, { documentation, properties, specialization })`, a `MergeElementsCommand`). The kept element always takes
|
|
1980
|
+
their relationships and their boxes on every view, and the properties it lacks; a relationship left joining it to
|
|
1981
|
+
itself, or the same as another, goes, its lines drawn as the other.
|
|
1982
|
+
|
|
1983
|
+
**View ▸ Remove repeated boxes (N)** takes off the view each box that only repeats another of its element: the same
|
|
1984
|
+
label shown, in the same box (or none), with the same lines drawn (each relationship, or connection, to the same
|
|
1985
|
+
thing at the other end), as one undo step with its lines (`view.repeatedBoxes()`, `editor.removeRepeatedBoxes()`).
|
|
1986
|
+
The first box stays; a box holding others is never taken off. The **File** menu (`EditorPart`'s, and the ArchiMate editor's) exports the current view
|
|
1857
1987
|
as an image: PNG, SVG or a copy to the clipboard (`editor.exportImage('png' | 'svg' | 'copy')`, without the grid
|
|
1858
1988
|
or selection; the ArchiMate editor's is off with `features.exportImage: false`). **Generate View For This**, on
|
|
1859
1989
|
an element's menu in the diagram or the tree, makes a view of the element with everything related to it
|
|
@@ -1894,7 +2024,8 @@ changed: `editor.model.duplicateView(id)` opens and shows the copy, deleting a v
|
|
|
1894
2024
|
|
|
1895
2025
|
**Canvases and templates.** As in Archi, the model tree's **Views** holds every view, canvases (`CanvasView`)
|
|
1896
2026
|
and sketches included, in the sub-folders the Archi file kept it in (`view.archiFolder`; `viewFolderNodes(views,
|
|
1897
|
-
depth)` makes a level of them)
|
|
2027
|
+
depth, emptyFolders)` makes a level of them). A sub-folder with no view in it is kept too, as its path
|
|
2028
|
+
(`model.emptyViewFolders`, read from an Archi or Exchange Format file and written back to either); templates are apart (`model.diagramViews()`, `model.canvases()`,
|
|
1898
2029
|
`model.templates()`). The **Templates** folder holds the model's own templates (canvases
|
|
1899
2030
|
marked `template`, saved in the `.archimate` file) and a **Library** of templates kept across models: a
|
|
1900
2031
|
`CanvasTemplateLibrary` (a `TemplateLibrary`, the base any kind of view's library extends: entries, built-ins
|
|
@@ -2307,7 +2438,7 @@ for something else in ArchiMate, each as one undo step:
|
|
|
2307
2438
|
|
|
2308
2439
|
`ArchimatePalette({version, onPick, document})` renders the element types grouped by layer
|
|
2309
2440
|
(`ArchimatePalette.groups(version)`: the notation's layers, then ArchiMate 4's common behaviour types),
|
|
2310
|
-
each tile only its notation glyph in its layer colour,
|
|
2441
|
+
each tile only its notation glyph in its layer colour, six to a row at 12px (`ArchimatePalette.TILE`; the name shows on hover); drag a tile onto the canvas or click it (`onPick`).
|
|
2311
2442
|
|
|
2312
2443
|
`ArchimateCanvasPart` (workspace bundle, type `archimate-canvas`; `examples/archimate.html`, and the testbed
|
|
2313
2444
|
`test/archimate-canvas-testbed.html`) is the
|
|
@@ -2421,7 +2552,11 @@ model's, then the tool's, then the default (`cascade(view, modelId)`; `getModelO
|
|
|
2421
2552
|
what the model level itself sets, `getModelSettings(modelId)` the model's resolved values). Global
|
|
2422
2553
|
Settings sets the tool's value and the model's (which may inherit the tool's); a view's Properties sets
|
|
2423
2554
|
the diagram's (which may inherit the model's). "Invalid relationships" (`invalidRelationships`:
|
|
2424
|
-
`highlight`, `tooltip`, `off`) is set this way.
|
|
2555
|
+
`highlight`, `tooltip`, `off`) is set this way. Global Settings also sets how the model tree searches and shows, for
|
|
2556
|
+
every model (tool level, `TREE_SETTINGS_PROPERTIES`): the leaves a folder shows before "more" (`treeLeafLimit`, 8)
|
|
2557
|
+
and the characters a search needs (`treeSearchMinChars`, 3). `settingsManager.treeOptions()` gives them as
|
|
2558
|
+
ModelTree's options (`{ leafLimit, minFilterChars }`); the canvas part makes its tree with them and redraws it when
|
|
2559
|
+
they change.
|
|
2425
2560
|
|
|
2426
2561
|
```js
|
|
2427
2562
|
settingsManager.saveToolSettings({ invalidRelationships: 'tooltip' }); // every model, unless it says otherwise
|
|
@@ -2614,6 +2749,16 @@ open model:
|
|
|
2614
2749
|
or title. Ids are the tracker's keys, so a newer export brought in replaces what it had. Where each tracker keeps
|
|
2615
2750
|
each field is data (`TRACKERS`); a team's own column is given with `new TrackerExport({ columns: { criteria:
|
|
2616
2751
|
['Done when'] } })` (in Jira's JSON, a custom field's key).
|
|
2752
|
+
- **A tracker, live** (`TrackerApi`; `JiraApi`, `AzureDevOpsApi`, `GitLabApi`): the issues a connection names, read
|
|
2753
|
+
through the tracker's API page by page — Jira by JQL (a project's by default; Cloud with an email and API token,
|
|
2754
|
+
Server / Data Center with a personal access token), Azure DevOps by WIQL (a project's work items but its tasks,
|
|
2755
|
+
200 at a time, each linked to its page in Boards), GitLab a project's issues (open and closed, filtered as its
|
|
2756
|
+
issues API filters). `read()` is the JSON `TrackerExport` reads, `model()` the model's spec, named as the
|
|
2757
|
+
connection is; at most `maxIssues` (5000) are read, `truncated` saying when there were more. `fetch` is given,
|
|
2758
|
+
since a tracker's token belongs where the request is made (a server); an error says what the tracker answered,
|
|
2759
|
+
never the token. What a connection asks for is each class's `fields` (`trackerConnectionFields()`: per tracker,
|
|
2760
|
+
`{key, label, type?, choices?, required?, secret?, placeholder?, hint?}`), for a host to build its form from;
|
|
2761
|
+
`problems()` what is missing before the tracker is asked.
|
|
2617
2762
|
- **A storyboard** (`StoryboardFileFormat`, `Storyboard`): Markdown — a `#` title names it, `##` are epics (or any
|
|
2618
2763
|
heading saying *Epic*), `###` stories (or *Story*); "As a … I want …" list items under an epic are stories; a
|
|
2619
2764
|
picture is kept by its caption.
|
|
@@ -2623,6 +2768,11 @@ const { RequirementsModel, TrackerExport } = window.RenderEngine;
|
|
|
2623
2768
|
const model = new RequirementsModel(new TrackerExport().read(jiraCsvText));
|
|
2624
2769
|
model.storyMap().columns.map(c => c.name); // the epics, across
|
|
2625
2770
|
model.coverage().criteriaShare; // how many stories say when they are done
|
|
2771
|
+
|
|
2772
|
+
// The same, live — on a server, where the token is kept:
|
|
2773
|
+
const api = trackerApi({ tracker: 'jira', name: 'Shop backlog', baseUrl: 'https://shop.atlassian.net',
|
|
2774
|
+
user: 'me@example.com', token: process.env.JIRA_TOKEN, project: 'SHOP' }, { fetch });
|
|
2775
|
+
const live = new RequirementsModel(await api.model());
|
|
2626
2776
|
```
|
|
2627
2777
|
|
|
2628
2778
|
`RequirementsPart` (type `requirements`, in a `ModelsPart` with `editorType: 'requirements'`) shows the model as
|
|
@@ -2655,6 +2805,11 @@ diagram). After an item acts, the bar re-renders. Items with live labels (the zo
|
|
|
2655
2805
|
| `ToolbarText(text, style)` | Live text |
|
|
2656
2806
|
| `ToolbarSeparator()` | Divider |
|
|
2657
2807
|
|
|
2808
|
+
`PopupMenu.show(x, y, items)` is the one popup or context menu (a toolbar menu's, an editor's, a host's
|
|
2809
|
+
right-click): items `{ label, action, danger?, checked?, disabled? }`, `{ label, items }` for a submenu, or
|
|
2810
|
+
`{ separator: true }`. One is open at a time; it moves back inside the window near an edge, and closes on a
|
|
2811
|
+
choice, a click elsewhere, Escape, or a scroll.
|
|
2812
|
+
|
|
2658
2813
|
```javascript
|
|
2659
2814
|
new MenuBar({ container, editor, items: [
|
|
2660
2815
|
new ToolbarMenu('File', [{ label: 'Export', action: exportIt }]),
|
|
@@ -2662,6 +2817,19 @@ new MenuBar({ container, editor, items: [
|
|
|
2662
2817
|
] }).render();
|
|
2663
2818
|
```
|
|
2664
2819
|
|
|
2820
|
+
`Dialog` is the one dialog frame, which `FormDialog` (a `<form>`) and any host's own dialog share: a title
|
|
2821
|
+
(drag it to move the dialog), an optional ✕ (`closeButton`), a body that scrolls between the title and the
|
|
2822
|
+
buttons, which stay in view, and Escape or a click outside to close. `buttons` are
|
|
2823
|
+
`[{ label, action(dialog), primary? }]`, `size` is `'tall'` or `'wide'`, and `onClose(applied)` is told once.
|
|
2824
|
+
`open()` shows it over the page; `open(host)` places it in `host` with no backdrop and no ✕, to show a dialog's
|
|
2825
|
+
content as a pane or a tab. A subclass gives its content as `content()` specs; a host fills `dialog.body` after
|
|
2826
|
+
`open()`, and adds to `dialog.titleBar` (a link) or `dialog.footer` (a pager).
|
|
2827
|
+
|
|
2828
|
+
```javascript
|
|
2829
|
+
const rows = new Dialog({ title: 'orders', closeButton: true, size: 'tall' }).open();
|
|
2830
|
+
rows.body.append(table);
|
|
2831
|
+
```
|
|
2832
|
+
|
|
2665
2833
|
`FormDialog` is the one modal form: `fields` of type `text`, `textarea`, `number`, `color`, `select`
|
|
2666
2834
|
or `checkbox`. A field with `inherit: { text, short? }` may be left unset; it then reads as `undefined` (a select
|
|
2667
2835
|
offers `text` as its first choice; any other field, a checkbox too, gets a checkbox named `short`, default
|
|
@@ -2732,6 +2900,88 @@ to it) / `deleteEdgeCommand(id)`. A subclass whose shapes and lines stand for so
|
|
|
2732
2900
|
overrides those factories (ArchimateCanvasEditor does). A template class says whether the editor
|
|
2733
2901
|
offers it a rotate handle (`static rotatable`, true by default; ArchiMate's templates are not). `pickFile(accept)` resolves with the chosen `File` (or `null`).
|
|
2734
2902
|
|
|
2903
|
+
## Navigation menus
|
|
2904
|
+
|
|
2905
|
+
`NavMenu` is a vertical navigation menu (a sidebar's), written as data: no markup. Each entry is
|
|
2906
|
+
one of three kinds, built from its JSON by `NavMenu.entry`:
|
|
2907
|
+
|
|
2908
|
+
| JSON | Class | Renders |
|
|
2909
|
+
|---|---|---|
|
|
2910
|
+
| `{ label, id?, icon?, href?, action?, selectable?, title?, cls?, visible? }` | `NavItem` | A link (`<a href>`) with `href`, otherwise a button |
|
|
2911
|
+
| `{ group, items, id?, open?, title?, cls?, visible? }` | `NavGroup` | A heading that opens and closes (`<details>`) over its items |
|
|
2912
|
+
| `{ section, title?, cls?, visible? }` | `NavSection` | A heading between entries |
|
|
2913
|
+
|
|
2914
|
+
```javascript
|
|
2915
|
+
import { NavMenu } from '@peter.prib/renderengine';
|
|
2916
|
+
|
|
2917
|
+
const menu = new NavMenu({
|
|
2918
|
+
label: 'Workbench',
|
|
2919
|
+
context: { role: 'analyst' },
|
|
2920
|
+
open: { devtools: true },
|
|
2921
|
+
items: [
|
|
2922
|
+
{ section: 'Notebooks' },
|
|
2923
|
+
{ id: 'new-nb', label: '+ New Notebook', action: newNotebook, selectable: false },
|
|
2924
|
+
{ group: 'Dev Tools', id: 'devtools', items: [
|
|
2925
|
+
{ id: 'datamodel', icon: '◫', label: 'Data Model', href: '#', action: (event) => {
|
|
2926
|
+
event.preventDefault();
|
|
2927
|
+
openTool('datamodel', { beside: event.altKey });
|
|
2928
|
+
} },
|
|
2929
|
+
] },
|
|
2930
|
+
{ id: 'admin', icon: '⚙', label: 'Admin', href: '/admin', visible: (ctx) => ctx.role === 'admin' },
|
|
2931
|
+
],
|
|
2932
|
+
onToggle: (groupId, open) => save(menu.serialize()),
|
|
2933
|
+
}).render(document.getElementById('nav'));
|
|
2934
|
+
```
|
|
2935
|
+
|
|
2936
|
+
- **A click** runs the item's `action(event, item, menu)`, then makes the item the active one (unless it
|
|
2937
|
+
has no `id`, or `selectable: false`), then calls `onSelect(item, event)`. A link still navigates unless
|
|
2938
|
+
the action calls `event.preventDefault()`.
|
|
2939
|
+
`href` may be a function of the context, for an address known only later (`setContext({ adminUrl })`). The active item has the class `ws-nav-item-active` and
|
|
2940
|
+
`aria-current="page"`; `setActive(id)` moves it in place (`null`: none).
|
|
2941
|
+
- **`visible`** is a boolean or a function of the menu's `context`. A group none of whose items shows is
|
|
2942
|
+
hidden too. `setContext({ role })` merges into the context and re-renders.
|
|
2943
|
+
- **State:** `serialize()` returns `{ activeId, open: { groupId: boolean } }` (groups with an `id`), and
|
|
2944
|
+
the constructor takes `activeId` and `open` back; `onToggle(groupId, open)` reports a group opened or
|
|
2945
|
+
closed.
|
|
2946
|
+
- `find(id)` returns an entry anywhere in the menu; `all()` lists them depth first.
|
|
2947
|
+
- **Styles** are the workspace's (`ensureStyles`): `ws-nav`, `ws-nav-item`, `ws-nav-icon`, `ws-nav-label`,
|
|
2948
|
+
`ws-nav-group`, `ws-nav-group-summary`, `ws-nav-section`. Colours are inherited, so the menu suits a
|
|
2949
|
+
dark sidebar and a light one; `cls` adds a host's own classes to an entry.
|
|
2950
|
+
|
|
2951
|
+
`NavMenu` imports nothing from the browser at module scope, so it is exported from the node entry
|
|
2952
|
+
(`src/index.js`) as well as the workspace bundle.
|
|
2953
|
+
|
|
2954
|
+
## Icons
|
|
2955
|
+
|
|
2956
|
+
`IconSet` holds named icons as data, and builds each as an `<svg>` spec for `el()`, so nothing is parsed. An
|
|
2957
|
+
icon is JSON: a `viewBox`, its `paint`, and its shapes as `[tag, attrs]` pairs.
|
|
2958
|
+
|
|
2959
|
+
```javascript
|
|
2960
|
+
import { ICONS, icon, IconSet } from '@peter.prib/renderengine';
|
|
2961
|
+
|
|
2962
|
+
el(icon('cpu')); // the built-in pool
|
|
2963
|
+
el(ICONS.spec('database', { size: 16, title: 'Catalog' }));
|
|
2964
|
+
const mine = ICONS.with({ // a host's own icons over the built-in ones
|
|
2965
|
+
ring: { viewBox: '0 0 24 24', shapes: [['circle', { cx: 12, cy: 12, r: 10 }]] },
|
|
2966
|
+
});
|
|
2967
|
+
```
|
|
2968
|
+
|
|
2969
|
+
- **`paint`:** `'stroke'` (the default) outlines the shapes in `currentColor`, so an icon takes its parent's
|
|
2970
|
+
colour. `'fill'` fills them in `currentColor`. `'none'` leaves the colours the shapes carry, as a brand mark does.
|
|
2971
|
+
- **`spec(name, { size = 24, title, cls })`:** returns an `<svg class="re-icon" data-icon="name">` spec. Without
|
|
2972
|
+
a `title` it is `aria-hidden`; with one it is `role="img"` and carries a `<title>`. An unknown name throws.
|
|
2973
|
+
- **Methods:** `define(name, icon)` checks the icon as it is added; `add(defs)`, `has(name)`, `get(name)` and
|
|
2974
|
+
`names()` work on the set; `with(defs)` returns a copy with `defs` added, leaving the original unchanged.
|
|
2975
|
+
- **The built-in icons** (`ICONS`, defined by `ICON_DEFS`): `dashboard`, `zap`, `clock`, `clipboard-list`,
|
|
2976
|
+
`book`, `cpu`, `memory`, `activity`, `database`, and the brand marks `google` and `microsoft`. They are named
|
|
2977
|
+
for the picture, not for where they are used.
|
|
2978
|
+
- **In a `NavMenu`:** an item's `icon` that names an icon in the menu's `icons` (default `ICONS`) is drawn as
|
|
2979
|
+
that icon; any other text, such as an emoji, is shown as it is.
|
|
2980
|
+
|
|
2981
|
+
`el()` builds an `svg` spec, and everything inside it, in the SVG namespace (`SVG_NS`, exported with `el` from
|
|
2982
|
+
the workspace bundle), until a `foreignObject`, whose children are HTML again. A spec's `ns` sets the namespace
|
|
2983
|
+
explicitly.
|
|
2984
|
+
|
|
2735
2985
|
## Where to go next
|
|
2736
2986
|
|
|
2737
2987
|
- **`ARCHITECTURE.md`** — module layout, the chart-type catalog mechanics, how to add a new chart type.
|