@formicoidea/labre-framework-bpmn 0.43.1 → 0.46.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/dist/actions.d.ts +11 -1
- package/dist/actions.js +41 -11
- package/dist/consts.d.ts +72 -27
- package/dist/consts.js +106 -42
- package/dist/element-renderer.d.ts +1 -1
- package/dist/export.d.ts +12 -0
- package/dist/export.js +3 -1
- package/dist/import.d.ts +0 -11
- package/dist/import.js +98 -30
- package/dist/interchange.d.ts +7 -12
- package/dist/interchange.js +61 -22
- package/dist/morph.d.ts +37 -1
- package/dist/morph.js +124 -2
- package/dist/node/node-renderer.d.ts +1 -1
- package/dist/node/node-view.d.ts +24 -3
- package/dist/node/node-view.js +31 -4
- package/dist/presets.d.ts +32 -1
- package/dist/presets.js +44 -3
- package/dist/reading.js +41 -3
- package/dist/roles.d.ts +2 -1
- package/dist/roles.js +23 -0
- package/dist/rules.d.ts +4 -1
- package/dist/rules.js +24 -8
- package/dist/templates/index.d.ts +20 -0
- package/dist/templates/index.js +74 -14
- package/dist/toolbar/config.d.ts +2 -2
- package/dist/toolbar/node-config.d.ts +21 -0
- package/dist/toolbar/node-config.js +88 -0
- package/dist/toolbar/senior-tool.d.ts +1 -1
- package/dist/view.js +38 -16
- package/package.json +2 -2
package/dist/import.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { InterchangeImportError, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
|
|
3
3
|
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
4
|
-
import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
|
|
4
|
+
import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, bpmnLabelMode, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
|
|
5
5
|
import { BPMN_FORMAT_ID, BPMN_NS, BPMN_OWN_DECLARATIONS, BPMN_SCOPE, BPMN_XML_OF_KIND, escapeAttr, escapeText, } from './export.js';
|
|
6
|
-
import { bpmnNodeProps } from './presets.js';
|
|
6
|
+
import { bpmnLabelBoxUnder, bpmnLabelProps, bpmnNodeProps } from './presets.js';
|
|
7
7
|
import { BPMN_ROLE } from './roles.js';
|
|
8
8
|
/**
|
|
9
9
|
* The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
|
|
@@ -158,8 +158,23 @@ export const BPMN_IMPORT_ERRORS = {
|
|
|
158
158
|
* `source` / `target` below name the **source file's** ids rather than surface
|
|
159
159
|
* ones. The caller creates the elements, folds the array into a map from each
|
|
160
160
|
* element's `interchange.bpmn.id` to the id the surface minted for it, and
|
|
161
|
-
* rewrites the two endpoints. Every element carries that id, so
|
|
162
|
-
* fold over the very array this returned
|
|
161
|
+
* rewrites the two endpoints. Every element a flow can name carries that id, so
|
|
162
|
+
* the map is a fold over the very array this returned.
|
|
163
|
+
*
|
|
164
|
+
* ## The gravitating label, and the second name it needs
|
|
165
|
+
*
|
|
166
|
+
* An event, a gateway or a data shape wears its `name` UNDER the symbol, as a
|
|
167
|
+
* free `bpmn:label` text grouped with it (R38, `bpmnLabelMode`) — the element
|
|
168
|
+
* the palette draws. Imported, that is three elements where the file wrote
|
|
169
|
+
* one: the node (no `text`), the label, and a native `group` holding the two.
|
|
170
|
+
* The label and the group are Labre's and no file ever named them, so they
|
|
171
|
+
* carry no `interchange` payload, and the group cannot point at its children
|
|
172
|
+
* by a source id. The three therefore carry a PROVISIONAL `id`
|
|
173
|
+
* (`bpmn-import-<n>`, UML's arrangement): `surface.addElement` overwrites it,
|
|
174
|
+
* and `materializeInterchangeImport` rewrites the group's `children` through
|
|
175
|
+
* it. The group is written LAST, after what it holds. An export reads the name
|
|
176
|
+
* back through the group (`bpmnBoardFrom`), which is what closes the round
|
|
177
|
+
* trip. An unnamed event stays a bare node: no label is invented.
|
|
163
178
|
*
|
|
164
179
|
* ## Three states, and no fourth
|
|
165
180
|
*
|
|
@@ -269,16 +284,6 @@ export const BPMN_KIND_OF_XML = new Map(Object.entries(BPMN_XML_OF_KIND).map(([k
|
|
|
269
284
|
xmlKindKey(mapping.element, mapping.eventDefinition),
|
|
270
285
|
kind,
|
|
271
286
|
]));
|
|
272
|
-
/**
|
|
273
|
-
* `.bpmn`'s scope vocabulary — where a carried fragment came off (D2, as
|
|
274
|
-
* amended in #157), and where the writer puts it back.
|
|
275
|
-
*
|
|
276
|
-
* Declared in `export.ts` and re-exported here, for the reason
|
|
277
|
-
* {@link BPMN_FORMAT_ID} is: the reader files a fragment under a scope and the
|
|
278
|
-
* writer looks it up under one, and a table written twice is a table that
|
|
279
|
-
* drifts. See its doc comment there for what a scope means.
|
|
280
|
-
*/
|
|
281
|
-
export { BPMN_SCOPE };
|
|
282
287
|
/**
|
|
283
288
|
* The three edge elements, and the role each one IS.
|
|
284
289
|
*
|
|
@@ -671,9 +676,14 @@ function boundOf(draft) {
|
|
|
671
676
|
* did not.
|
|
672
677
|
*/
|
|
673
678
|
function layOutTheUndrawn(drafts, minted) {
|
|
679
|
+
// A label counts as placed when its node was: it hangs under a box the file
|
|
680
|
+
// drew, and the sweep must not land something on top of it. A group has no
|
|
681
|
+
// box of its own, and an edge is routed between its ends.
|
|
674
682
|
const placed = drafts.filter(draft => draft.needsLayout === undefined &&
|
|
683
|
+
draft.follows?.needsLayout === undefined &&
|
|
675
684
|
draft !== minted &&
|
|
676
|
-
draft.kind !== 'edge'
|
|
685
|
+
draft.kind !== 'edge' &&
|
|
686
|
+
draft.kind !== 'group');
|
|
677
687
|
const boxes = placed.map(boundOf);
|
|
678
688
|
const maxX = boxes.length > 0 ? Math.max(...boxes.map(box => box.x + box.w)) : 0;
|
|
679
689
|
const minY = boxes.length > 0 ? Math.min(...boxes.map(box => box.y)) : 0;
|
|
@@ -686,14 +696,25 @@ function layOutTheUndrawn(drafts, minted) {
|
|
|
686
696
|
seat++;
|
|
687
697
|
draft.props.xywh = new Bound(maxX + SWEEP_GAP + column * SWEEP_STEP, minY + row * SWEEP_STEP, draft.needsLayout.w, draft.needsLayout.h).serialize();
|
|
688
698
|
}
|
|
699
|
+
// A label follows its node AFTER the sweep, so a swept event keeps its name
|
|
700
|
+
// under it rather than at the origin it was drafted at.
|
|
701
|
+
for (const draft of drafts) {
|
|
702
|
+
if (!draft.follows?.needsLayout)
|
|
703
|
+
continue;
|
|
704
|
+
const node = boundOf(draft.follows);
|
|
705
|
+
const { x, y } = bpmnLabelBoxUnder(node.x + node.w / 2, node.y + node.h);
|
|
706
|
+
const label = boundOf(draft);
|
|
707
|
+
draft.props.xywh = new Bound(x, y, label.w, label.h).serialize();
|
|
708
|
+
}
|
|
689
709
|
// The pool minted for a file that had no participant (D6) is sized LAST, to
|
|
690
710
|
// hold everything: a pool's plot is what decides which artefacts are in it,
|
|
691
711
|
// and an artefact drawn outside every plot would be exported back into a
|
|
692
|
-
// process of its own.
|
|
712
|
+
// process of its own. The labels are inside too, or a name would hang below
|
|
713
|
+
// the frame of the process it names.
|
|
693
714
|
if (!minted)
|
|
694
715
|
return;
|
|
695
716
|
const inside = drafts
|
|
696
|
-
.filter(draft => draft !== minted && draft.kind === 'node')
|
|
717
|
+
.filter(draft => draft !== minted && (draft.kind === 'node' || draft.kind === 'label'))
|
|
697
718
|
.map(boundOf);
|
|
698
719
|
if (inside.length === 0)
|
|
699
720
|
return;
|
|
@@ -726,6 +747,9 @@ export function importBpmnXml(source, context = {}) {
|
|
|
726
747
|
let quarantined = 0;
|
|
727
748
|
let explicitRoutes = 0;
|
|
728
749
|
const drafts = [];
|
|
750
|
+
/** The provisional names a gravitating label's group is wired by. */
|
|
751
|
+
let provisional = 0;
|
|
752
|
+
const mint = () => `bpmn-import-${++provisional}`;
|
|
729
753
|
const seenSourceIds = new Set();
|
|
730
754
|
/** Source ids that became an artefact a flow may attach to: pools and nodes. */
|
|
731
755
|
const mappedSourceIds = new Set();
|
|
@@ -1287,23 +1311,64 @@ export function importBpmnXml(source, context = {}) {
|
|
|
1287
1311
|
carryChild(payload, BPMN_SCOPE.self, child, sourceId);
|
|
1288
1312
|
}
|
|
1289
1313
|
const size = NODE_SIZE[kind];
|
|
1290
|
-
|
|
1314
|
+
const box = bounds
|
|
1315
|
+
? new Bound(bounds.x, bounds.y, bounds.w, bounds.h)
|
|
1316
|
+
: new Bound(0, 0, size.w, size.h);
|
|
1317
|
+
// An external kind's name is not the shape's to hold (R38): it goes to a
|
|
1318
|
+
// grouped label under the symbol, and the node is drawn bare.
|
|
1319
|
+
const gravitating = text !== '' && bpmnLabelMode(kind) === 'external';
|
|
1320
|
+
const order = shape?.index ?? Number.POSITIVE_INFINITY;
|
|
1321
|
+
const nodeDraft = {
|
|
1291
1322
|
props: bpmnNodeProps(kind, {
|
|
1292
|
-
xywh: (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1323
|
+
xywh: box.serialize(),
|
|
1324
|
+
...(gravitating
|
|
1325
|
+
? {}
|
|
1326
|
+
: {
|
|
1327
|
+
text: text || undefined,
|
|
1328
|
+
// The file's own box, which this reader may not rewrite — so the
|
|
1329
|
+
// label is fitted to it rather than painted past it (#184). A
|
|
1330
|
+
// shape the file did not draw is laid out at the pack's own size
|
|
1331
|
+
// below, and wants the pack's own type: no fit, and an element
|
|
1332
|
+
// identical to a drawn one.
|
|
1333
|
+
...(bounds ? { fitLabel: true } : {}),
|
|
1334
|
+
}),
|
|
1301
1335
|
}),
|
|
1302
1336
|
payload,
|
|
1303
|
-
order
|
|
1337
|
+
order,
|
|
1304
1338
|
kind: 'node',
|
|
1305
1339
|
...(bounds ? {} : { needsLayout: size }),
|
|
1306
|
-
}
|
|
1340
|
+
};
|
|
1341
|
+
drafts.push(nodeDraft);
|
|
1342
|
+
if (gravitating) {
|
|
1343
|
+
// ponytail: the label is always placed centred under the symbol, and a
|
|
1344
|
+
// `BPMNLabel`'s own bounds are ignored for placement (they are still
|
|
1345
|
+
// carried verbatim as DI, so the file gets them back). Ceiling: a file
|
|
1346
|
+
// that set its label beside an event lands it under. Upgrade: read the
|
|
1347
|
+
// `dc:Bounds` of the shape's `BPMNLabel` and place the text there.
|
|
1348
|
+
const nodeId = mint();
|
|
1349
|
+
const labelId = mint();
|
|
1350
|
+
nodeDraft.props.id = nodeId;
|
|
1351
|
+
const under = bpmnLabelBoxUnder(box.x + box.w / 2, box.y + box.h);
|
|
1352
|
+
// Same order as the node, pushed after it: the stable sort keeps the
|
|
1353
|
+
// node, then its label, then the group that holds both.
|
|
1354
|
+
drafts.push({
|
|
1355
|
+
props: { ...bpmnLabelProps(text, under.x, under.y), id: labelId },
|
|
1356
|
+
payload: {},
|
|
1357
|
+
order,
|
|
1358
|
+
kind: 'label',
|
|
1359
|
+
follows: nodeDraft,
|
|
1360
|
+
});
|
|
1361
|
+
drafts.push({
|
|
1362
|
+
props: {
|
|
1363
|
+
type: 'group',
|
|
1364
|
+
id: mint(),
|
|
1365
|
+
children: { [nodeId]: true, [labelId]: true },
|
|
1366
|
+
},
|
|
1367
|
+
payload: {},
|
|
1368
|
+
order,
|
|
1369
|
+
kind: 'group',
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1307
1372
|
if (sourceId !== undefined && bounds)
|
|
1308
1373
|
nodeBounds.set(sourceId, bounds);
|
|
1309
1374
|
if (!bounds) {
|
|
@@ -1621,6 +1686,9 @@ export function importBpmnXml(source, context = {}) {
|
|
|
1621
1686
|
: { ...draft.props, interchange: { [BPMN_FORMAT_ID]: payload } };
|
|
1622
1687
|
});
|
|
1623
1688
|
const lanes = ordered.reduce((total, draft) => total + (Array.isArray(draft.props.lanes) ? draft.props.lanes.length : 0), 0);
|
|
1689
|
+
// A gravitating label and its group are how a node wears its name, not
|
|
1690
|
+
// artefacts of the file: counting them would report one event as three.
|
|
1691
|
+
const artefacts = ordered.filter(draft => draft.kind !== 'label' && draft.kind !== 'group').length;
|
|
1624
1692
|
const sourceVersion = sourceVersionOf(definitions);
|
|
1625
1693
|
return {
|
|
1626
1694
|
elements,
|
|
@@ -1628,7 +1696,7 @@ export function importBpmnXml(source, context = {}) {
|
|
|
1628
1696
|
// Everything that became a drawn, editable artefact: the pools, the flow
|
|
1629
1697
|
// objects, the arrows — and the LANES, which are drawn and editable and
|
|
1630
1698
|
// are not elements of their own.
|
|
1631
|
-
mapped:
|
|
1699
|
+
mapped: artefacts + lanes,
|
|
1632
1700
|
carried,
|
|
1633
1701
|
quarantined,
|
|
1634
1702
|
notes,
|
package/dist/interchange.d.ts
CHANGED
|
@@ -81,21 +81,16 @@ export declare const BPMN_XML_FORMAT: InterchangeFormat;
|
|
|
81
81
|
* Connectors are deliberately NOT counted: a neutral arrow states nothing
|
|
82
82
|
* (`docs/adr/0010`), so there is nothing to lose, which is the reason
|
|
83
83
|
* `export.ts` has always dropped one in silence. Neither is a generated legend
|
|
84
|
-
* — see {@link legendMemberIds}
|
|
84
|
+
* — see {@link legendMemberIds} — nor a gravitating label and the group that
|
|
85
|
+
* binds it to its node, which the file DOES carry, as the node's `name` — see
|
|
86
|
+
* {@link gravitatingLabels}.
|
|
85
87
|
*/
|
|
86
88
|
export declare function bpmnBoardFrom(elements: readonly GfxPrimitiveElementModel[]): BpmnExportBoard;
|
|
87
89
|
/**
|
|
88
|
-
* A name a file system will accept, minus the extension
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* silently refuses.
|
|
93
|
-
*
|
|
94
|
-
* The trailing `[. ]` trim is the Windows tail case and it is not decorative: a
|
|
95
|
-
* board called "Order to cash." would otherwise download as
|
|
96
|
-
* `Order to cash..bpmn`, and Windows strips trailing dots and spaces from a
|
|
97
|
-
* name anyway — so the extension is what would get eaten. Trimmed AFTER the
|
|
98
|
-
* cap, because the cap can create one.
|
|
90
|
+
* A name a file system will accept, minus the extension: the shared
|
|
91
|
+
* `safeFilename` with BPMN's fallback — `process` is a better download than
|
|
92
|
+
* one a browser silently refuses. Held to it by
|
|
93
|
+
* `safe-filename-parity.unit.spec.ts`.
|
|
99
94
|
*/
|
|
100
95
|
export declare function bpmnSafeFilename(raw: string | undefined): string;
|
|
101
96
|
/** `bpmn:bpmn:export` — the registry's first capability. */
|
package/dist/interchange.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { interchangeCapabilityId, LEGEND_ROLE, parseSvgSketch, SVG_SKETCH_EXTENSION, SVG_SKETCH_FORMAT_ID, SVG_SKETCH_MIME, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
-
import { BpmnNodeElementModel, BpmnPoolElementModel, ConnectorElementModel, } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { BpmnNodeElementModel, BpmnPoolElementModel, ConnectorElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
|
|
3
3
|
import { fillPlaceholders } from '@formicoidea/labre-core/shared/services';
|
|
4
|
+
import { safeFilename } from '@formicoidea/labre-core/shared/utils';
|
|
4
5
|
import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
|
|
5
6
|
import { bpmnPoolOf } from './facts.js';
|
|
6
7
|
import { importBpmnXml } from './import.js';
|
|
@@ -86,6 +87,48 @@ function legendMemberIds(elements) {
|
|
|
86
87
|
}
|
|
87
88
|
return members;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Every node's gravitating name (R38), and the elements that carried it.
|
|
92
|
+
*
|
|
93
|
+
* A native group holding exactly one picked node and exactly one `bpmn:label`
|
|
94
|
+
* text binds the two: the text is the node's name, whatever the geometry says
|
|
95
|
+
* (the palette and the importer both write that pair). GROUP membership is the
|
|
96
|
+
* binding, as it is for Wardley's composites (`labelOfComposite`, `morph.ts`),
|
|
97
|
+
* because a label dragged aside is still the name and a stray text that
|
|
98
|
+
* happens to sit under an event is not.
|
|
99
|
+
*
|
|
100
|
+
* `consumed` is what the file does carry after all — the bound label, and the
|
|
101
|
+
* group when it holds nothing but BPMN nodes and their labels — so the
|
|
102
|
+
* left-out count does not report a name that IS written. An unbound
|
|
103
|
+
* `bpmn:label` stays counted: its words are in no file.
|
|
104
|
+
*
|
|
105
|
+
* Direct children only, read defensively: they came out of a Y.Map.
|
|
106
|
+
*/
|
|
107
|
+
function gravitatingLabels(elements, picked) {
|
|
108
|
+
const byId = new Map(elements.map(element => [element.id, element]));
|
|
109
|
+
const labels = new Map();
|
|
110
|
+
const consumed = new Set();
|
|
111
|
+
const isLabel = (element) => element instanceof TextElementModel && element.role === BPMN_ROLE.label;
|
|
112
|
+
for (const group of elements) {
|
|
113
|
+
if (!(group instanceof GroupElementModel))
|
|
114
|
+
continue;
|
|
115
|
+
const ids = group.childIds;
|
|
116
|
+
if (!Array.isArray(ids))
|
|
117
|
+
continue;
|
|
118
|
+
const members = ids.map(id => typeof id === 'string' ? byId.get(id) : undefined);
|
|
119
|
+
const nodes = members.filter((member) => member !== undefined && picked.has(member.id));
|
|
120
|
+
const texts = members.filter((member) => isLabel(member));
|
|
121
|
+
if (nodes.length === 1 && texts.length === 1) {
|
|
122
|
+
labels.set(nodes[0].id, texts[0].text?.toString() ?? '');
|
|
123
|
+
consumed.add(texts[0].id);
|
|
124
|
+
}
|
|
125
|
+
if (members.length > 0 &&
|
|
126
|
+
members.every(member => member && (picked.has(member.id) || isLabel(member)))) {
|
|
127
|
+
consumed.add(group.id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { labels, consumed };
|
|
131
|
+
}
|
|
89
132
|
/**
|
|
90
133
|
* The artefacts the exporter speaks about, picked out of a surface's elements
|
|
91
134
|
* and kept in the order they were given.
|
|
@@ -127,7 +170,9 @@ function legendMemberIds(elements) {
|
|
|
127
170
|
* Connectors are deliberately NOT counted: a neutral arrow states nothing
|
|
128
171
|
* (`docs/adr/0010`), so there is nothing to lose, which is the reason
|
|
129
172
|
* `export.ts` has always dropped one in silence. Neither is a generated legend
|
|
130
|
-
* — see {@link legendMemberIds}
|
|
173
|
+
* — see {@link legendMemberIds} — nor a gravitating label and the group that
|
|
174
|
+
* binds it to its node, which the file DOES carry, as the node's `name` — see
|
|
175
|
+
* {@link gravitatingLabels}.
|
|
131
176
|
*/
|
|
132
177
|
export function bpmnBoardFrom(elements) {
|
|
133
178
|
const pools = [];
|
|
@@ -160,32 +205,26 @@ export function bpmnBoardFrom(elements) {
|
|
|
160
205
|
// audit and the serializer use, so what is reported left out is what the rest
|
|
161
206
|
// of the framework agrees is in there.
|
|
162
207
|
const legend = legendMemberIds(elements);
|
|
208
|
+
const { labels, consumed } = gravitatingLabels(elements, new Set(nodes.map(node => node.id)));
|
|
163
209
|
const leftOut = roleless.filter(element => !legend.has(element.id) &&
|
|
210
|
+
!consumed.has(element.id) &&
|
|
164
211
|
bpmnPoolOf(pools, element.elementBound) !== null).length;
|
|
165
|
-
return {
|
|
212
|
+
return {
|
|
213
|
+
pools,
|
|
214
|
+
nodes,
|
|
215
|
+
connectors,
|
|
216
|
+
...(labels.size > 0 ? { labels } : {}),
|
|
217
|
+
...(leftOut > 0 ? { leftOut } : {}),
|
|
218
|
+
};
|
|
166
219
|
}
|
|
167
220
|
/**
|
|
168
|
-
* A name a file system will accept, minus the extension
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* silently refuses.
|
|
173
|
-
*
|
|
174
|
-
* The trailing `[. ]` trim is the Windows tail case and it is not decorative: a
|
|
175
|
-
* board called "Order to cash." would otherwise download as
|
|
176
|
-
* `Order to cash..bpmn`, and Windows strips trailing dots and spaces from a
|
|
177
|
-
* name anyway — so the extension is what would get eaten. Trimmed AFTER the
|
|
178
|
-
* cap, because the cap can create one.
|
|
221
|
+
* A name a file system will accept, minus the extension: the shared
|
|
222
|
+
* `safeFilename` with BPMN's fallback — `process` is a better download than
|
|
223
|
+
* one a browser silently refuses. Held to it by
|
|
224
|
+
* `safe-filename-parity.unit.spec.ts`.
|
|
179
225
|
*/
|
|
180
226
|
export function bpmnSafeFilename(raw) {
|
|
181
|
-
|
|
182
|
-
.trim()
|
|
183
|
-
.replaceAll(/[\\/:*?"<>|]/g, '-')
|
|
184
|
-
.replaceAll(/\s+/g, ' ')
|
|
185
|
-
.trim()
|
|
186
|
-
.slice(0, 120)
|
|
187
|
-
.replace(/[. ]+$/, '');
|
|
188
|
-
return safe || 'process';
|
|
227
|
+
return safeFilename(raw, 'process');
|
|
189
228
|
}
|
|
190
229
|
/* ── The capability ───────────────────────────────────────────────────── */
|
|
191
230
|
/**
|
package/dist/morph.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type MorphSpec } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
-
import { type BpmnNodeKind } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { BpmnNodeElementModel, type BpmnNodeKind } from '@formicoidea/labre-core/model';
|
|
3
|
+
import type { BlockStdScope } from '@formicoidea/labre-core/std';
|
|
4
|
+
import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
|
|
3
5
|
/**
|
|
4
6
|
* What a BPMN artefact may BECOME — the declaration behind the "Change type"
|
|
5
7
|
* dropdown on a selected node's contextual toolbar.
|
|
@@ -42,6 +44,31 @@ import { type BpmnNodeKind } from '@formicoidea/labre-core/model';
|
|
|
42
44
|
* `roles.ts` already gives them in the role tree.
|
|
43
45
|
*/
|
|
44
46
|
export declare const BPMN_MORPH_FAMILIES: readonly (readonly BpmnNodeKind[])[];
|
|
47
|
+
/**
|
|
48
|
+
* The BPMN node a selected GROUP is the artefact of — `undefined` when it is
|
|
49
|
+
* not one.
|
|
50
|
+
*
|
|
51
|
+
* An event, a gateway or a data shape is born as a native group of the symbol
|
|
52
|
+
* and its `bpmn:label` (`createBpmnNode`, R38), so a click selects the GROUP
|
|
53
|
+
* and the `kind` lives on the node inside it. Mirrors
|
|
54
|
+
* `wardleyNodeOfComponent`: only a `BpmnNodeElementModel` of an EXTERNAL kind
|
|
55
|
+
* is a candidate (the label, a connector or an activity somebody grouped in
|
|
56
|
+
* beside it are not), and TWO candidates is a refusal rather than a first-wins
|
|
57
|
+
* pick — morphing "it" in a group of two events would mean choosing one by
|
|
58
|
+
* document order.
|
|
59
|
+
*/
|
|
60
|
+
export declare function bpmnNodeOfComposite(model: GfxPrimitiveElementModel): BpmnNodeElementModel | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The name an artefact should carry once it has morphed — or `null` when the
|
|
63
|
+
* name is the AUTHOR's.
|
|
64
|
+
*
|
|
65
|
+
* Exactly one case rewrites: the label still says the SOURCE kind's seed,
|
|
66
|
+
* English or translated, letter for letter. A "Start event" that became a
|
|
67
|
+
* timer start and still read "Start event" would contradict its own glyph;
|
|
68
|
+
* anything typed over the seed is content and survives the morph untouched —
|
|
69
|
+
* the rule `wardleyMorphedLabel` states, for the same reason.
|
|
70
|
+
*/
|
|
71
|
+
export declare function bpmnMorphedLabel(from: BpmnNodeKind, to: BpmnNodeKind, rawText: string | null | undefined, std?: BlockStdScope): string | null;
|
|
45
72
|
/**
|
|
46
73
|
* BPMN's morph declaration, handed to the generic `morphToolbarConfig`.
|
|
47
74
|
*
|
|
@@ -59,3 +86,12 @@ export declare const BPMN_MORPH_FAMILIES: readonly (readonly BpmnNodeKind[])[];
|
|
|
59
86
|
* the presets on the day one gains a member that styles itself differently.
|
|
60
87
|
*/
|
|
61
88
|
export declare const BPMN_MORPH_SPEC: MorphSpec<BpmnNodeKind>;
|
|
89
|
+
/**
|
|
90
|
+
* The same declaration on the GROUP row: an event, a gateway or a data shape
|
|
91
|
+
* is a native group of the symbol and its label (R38), and the group is what a
|
|
92
|
+
* click on one selects. `modelType` is therefore `GroupElementModel`, and
|
|
93
|
+
* everything that makes that safe is {@link bpmnNodeOfComposite} — the
|
|
94
|
+
* toolbar's homogeneity test only proves each selected element is A group.
|
|
95
|
+
* Mirrors `WARDLEY_MORPH_SPEC`.
|
|
96
|
+
*/
|
|
97
|
+
export declare const BPMN_GROUP_MORPH_SPEC: MorphSpec<BpmnNodeKind>;
|
package/dist/morph.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { morphLabel, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
-
import { BpmnNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
|
+
import { BpmnNodeElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { translateKey } from '@formicoidea/labre-core/shared/services';
|
|
3
4
|
import { bpmnCommandIcons, bpmnCommands } from './commands.js';
|
|
5
|
+
import { bpmnLabelMode, NODE_LABEL, nodeLabelKey } from './consts.js';
|
|
4
6
|
import { bpmnMorphClears, bpmnMorphProps } from './presets.js';
|
|
5
|
-
import { BPMN_ROLE_OF_KIND } from './roles.js';
|
|
7
|
+
import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
|
|
6
8
|
/**
|
|
7
9
|
* What a BPMN artefact may BECOME — the declaration behind the "Change type"
|
|
8
10
|
* dropdown on a selected node's contextual toolbar.
|
|
@@ -88,6 +90,112 @@ function iconOf(kind) {
|
|
|
88
90
|
const iconKey = NODE_COMMANDS.get(kind)?.iconKey;
|
|
89
91
|
return ((iconKey && bpmnCommandIcons[iconKey]) || bpmnCommandIcons['bpmn.task']);
|
|
90
92
|
}
|
|
93
|
+
/* ── The gravitating label (R38) ───────────────────────────────────────── */
|
|
94
|
+
/**
|
|
95
|
+
* Every element under a group, however deeply nested — the same walk as
|
|
96
|
+
* Wardley's `descendants`, so an author who grouped a composite with
|
|
97
|
+
* something else still has it resolved.
|
|
98
|
+
*/
|
|
99
|
+
function* descendants(group) {
|
|
100
|
+
for (const child of group.childElements) {
|
|
101
|
+
yield child;
|
|
102
|
+
if (child instanceof GroupElementModel)
|
|
103
|
+
yield* descendants(child);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The BPMN node a selected GROUP is the artefact of — `undefined` when it is
|
|
108
|
+
* not one.
|
|
109
|
+
*
|
|
110
|
+
* An event, a gateway or a data shape is born as a native group of the symbol
|
|
111
|
+
* and its `bpmn:label` (`createBpmnNode`, R38), so a click selects the GROUP
|
|
112
|
+
* and the `kind` lives on the node inside it. Mirrors
|
|
113
|
+
* `wardleyNodeOfComponent`: only a `BpmnNodeElementModel` of an EXTERNAL kind
|
|
114
|
+
* is a candidate (the label, a connector or an activity somebody grouped in
|
|
115
|
+
* beside it are not), and TWO candidates is a refusal rather than a first-wins
|
|
116
|
+
* pick — morphing "it" in a group of two events would mean choosing one by
|
|
117
|
+
* document order.
|
|
118
|
+
*/
|
|
119
|
+
export function bpmnNodeOfComposite(model) {
|
|
120
|
+
if (!(model instanceof GroupElementModel))
|
|
121
|
+
return undefined;
|
|
122
|
+
let found;
|
|
123
|
+
for (const child of descendants(model)) {
|
|
124
|
+
if (!(child instanceof BpmnNodeElementModel))
|
|
125
|
+
continue;
|
|
126
|
+
if (bpmnLabelMode(child.kind) !== 'external')
|
|
127
|
+
continue;
|
|
128
|
+
if (found)
|
|
129
|
+
return undefined;
|
|
130
|
+
found = child;
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The one `bpmn:label` of the composite the selected element belongs to —
|
|
136
|
+
* the group itself when the group row morphed, the node's group when the node
|
|
137
|
+
* row did. `undefined` when there is not exactly one.
|
|
138
|
+
*/
|
|
139
|
+
function labelOfComposite(selected) {
|
|
140
|
+
const group = selected instanceof GroupElementModel
|
|
141
|
+
? selected
|
|
142
|
+
: selected.group instanceof GroupElementModel
|
|
143
|
+
? selected.group
|
|
144
|
+
: undefined;
|
|
145
|
+
if (!group)
|
|
146
|
+
return undefined;
|
|
147
|
+
let found;
|
|
148
|
+
for (const child of descendants(group)) {
|
|
149
|
+
if (!(child instanceof TextElementModel))
|
|
150
|
+
continue;
|
|
151
|
+
if (child.role !== BPMN_ROLE.label)
|
|
152
|
+
continue;
|
|
153
|
+
if (found)
|
|
154
|
+
return undefined;
|
|
155
|
+
found = child;
|
|
156
|
+
}
|
|
157
|
+
return found;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The name an artefact should carry once it has morphed — or `null` when the
|
|
161
|
+
* name is the AUTHOR's.
|
|
162
|
+
*
|
|
163
|
+
* Exactly one case rewrites: the label still says the SOURCE kind's seed,
|
|
164
|
+
* English or translated, letter for letter. A "Start event" that became a
|
|
165
|
+
* timer start and still read "Start event" would contradict its own glyph;
|
|
166
|
+
* anything typed over the seed is content and survives the morph untouched —
|
|
167
|
+
* the rule `wardleyMorphedLabel` states, for the same reason.
|
|
168
|
+
*/
|
|
169
|
+
export function bpmnMorphedLabel(from, to, rawText, std) {
|
|
170
|
+
const text = (rawText ?? '').trim();
|
|
171
|
+
const fromSeed = std
|
|
172
|
+
? translateKey(std, nodeLabelKey(from), NODE_LABEL[from])
|
|
173
|
+
: NODE_LABEL[from];
|
|
174
|
+
if (text !== NODE_LABEL[from] && text !== fromSeed)
|
|
175
|
+
return null;
|
|
176
|
+
return std
|
|
177
|
+
? translateKey(std, nodeLabelKey(to), NODE_LABEL[to])
|
|
178
|
+
: NODE_LABEL[to];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The structural half of a morph on an external kind: rewrite the grouped
|
|
182
|
+
* label when it is still the source kind's seed. The families never cross
|
|
183
|
+
* inscribed ⇄ external, so nothing moves in or out of the shape; an inscribed
|
|
184
|
+
* kind has no grouped label and this does nothing for it.
|
|
185
|
+
*/
|
|
186
|
+
function bpmnMorphLabel(selected, from, to, std) {
|
|
187
|
+
const label = labelOfComposite(selected);
|
|
188
|
+
if (!label || label.isLocked())
|
|
189
|
+
return;
|
|
190
|
+
const text = label.text.toString().trim();
|
|
191
|
+
const next = bpmnMorphedLabel(from, to, text, std);
|
|
192
|
+
if (next === null || next === text)
|
|
193
|
+
return;
|
|
194
|
+
label.surface.store.transact(() => {
|
|
195
|
+
label.text.delete(0, label.text.length);
|
|
196
|
+
label.text.insert(0, next);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
91
199
|
/**
|
|
92
200
|
* BPMN's morph declaration, handed to the generic `morphToolbarConfig`.
|
|
93
201
|
*
|
|
@@ -112,7 +220,21 @@ export const BPMN_MORPH_SPEC = {
|
|
|
112
220
|
roleOf: kind => BPMN_ROLE_OF_KIND[kind],
|
|
113
221
|
propsOf: bpmnMorphProps,
|
|
114
222
|
clearOf: bpmnMorphClears,
|
|
223
|
+
afterMorph: bpmnMorphLabel,
|
|
115
224
|
labelOf,
|
|
116
225
|
iconOf,
|
|
117
226
|
label: morphLabel('com.labre.morph.toolbar.label', 'Change type'),
|
|
118
227
|
};
|
|
228
|
+
/**
|
|
229
|
+
* The same declaration on the GROUP row: an event, a gateway or a data shape
|
|
230
|
+
* is a native group of the symbol and its label (R38), and the group is what a
|
|
231
|
+
* click on one selects. `modelType` is therefore `GroupElementModel`, and
|
|
232
|
+
* everything that makes that safe is {@link bpmnNodeOfComposite} — the
|
|
233
|
+
* toolbar's homogeneity test only proves each selected element is A group.
|
|
234
|
+
* Mirrors `WARDLEY_MORPH_SPEC`.
|
|
235
|
+
*/
|
|
236
|
+
export const BPMN_GROUP_MORPH_SPEC = {
|
|
237
|
+
...BPMN_MORPH_SPEC,
|
|
238
|
+
modelType: GroupElementModel,
|
|
239
|
+
resolveTarget: bpmnNodeOfComposite,
|
|
240
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ElementRenderer } from '@formicoidea/labre-core/blocks/surface';
|
|
2
2
|
import { type BpmnNodeElementModel } from '@formicoidea/labre-core/model';
|
|
3
3
|
export declare const bpmnNode: ElementRenderer<BpmnNodeElementModel>;
|
|
4
|
-
export declare const BpmnNodeRendererExtension: import("@formicoidea/labre-core/store").ExtensionType & {
|
|
4
|
+
export declare const BpmnNodeRendererExtension: import("@formicoidea/labre-core/_pkgs/store").ExtensionType & {
|
|
5
5
|
identifier: import("@formicoidea/labre-core/_pkgs/global/di").ServiceIdentifier<ElementRenderer<BpmnNodeElementModel>>;
|
|
6
6
|
};
|
package/dist/node/node-view.d.ts
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import { type BpmnNodeElementModel } from '@formicoidea/labre-core/model';
|
|
2
2
|
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a double-click on this node opens the shape's own inner-text editor.
|
|
5
|
+
*
|
|
6
|
+
* An INSCRIBED kind (activities, annotation, group) wears its name inside the
|
|
7
|
+
* shape, so the gesture lands there, exactly like a native shape.
|
|
8
|
+
*
|
|
9
|
+
* An EXTERNAL kind (events, gateways, data shapes) wears its name as a separate
|
|
10
|
+
* `bpmn:label` text grouped under it (R38), which is edited by double-clicking
|
|
11
|
+
* that text — `TextElementView`'s job, not this one's. The symbol itself has
|
|
12
|
+
* nothing to open, the same answer Wardley's node view gives its circles: an
|
|
13
|
+
* editor mounted here would write a second, invisible name into a field that
|
|
14
|
+
* must stay empty. No redirect to the label either: Wardley does not redirect,
|
|
15
|
+
* and the label is one click away inside the group.
|
|
16
|
+
*
|
|
17
|
+
* The one exception is a LEGACY node: an event drawn before R38 may carry inner
|
|
18
|
+
* text (nothing is migrated, `BPMN_EXTERNAL_LABEL_KINDS`). That text is still
|
|
19
|
+
* painted, so it must still be editable — hence "already has text" reopens
|
|
20
|
+
* the editor, and emptying it closes the door behind it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function bpmnNodeEditsInnerText(model: BpmnNodeElementModel): boolean;
|
|
3
23
|
/**
|
|
4
24
|
* View for a BPMN flow-object node. Registering it ensures `gfx.view.get(model)`
|
|
5
25
|
* returns a view (required so move / select / connector interactions work).
|
|
6
26
|
*
|
|
7
|
-
* BPMN nodes are native shapes, so
|
|
8
|
-
* double-click mounts the editable text overlay
|
|
9
|
-
* exactly like a native shape
|
|
27
|
+
* BPMN nodes are native shapes, so an inscribed kind reuses the shape
|
|
28
|
+
* inner-text editor: a double-click mounts the editable text overlay
|
|
29
|
+
* (`mountShapeTextEditor`), exactly like a native shape. Which kinds do is
|
|
30
|
+
* {@link bpmnNodeEditsInnerText}'s call.
|
|
10
31
|
*
|
|
11
32
|
* Mirrors {@link EdgyNodeView}.
|
|
12
33
|
*/
|
package/dist/node/node-view.js
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
import { mountShapeTextEditor } from '@formicoidea/labre-core/gfx/shape';
|
|
2
2
|
import { ShapeElementModel, } from '@formicoidea/labre-core/model';
|
|
3
3
|
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
4
|
+
import { bpmnLabelMode } from '../consts.js';
|
|
5
|
+
/**
|
|
6
|
+
* Whether a double-click on this node opens the shape's own inner-text editor.
|
|
7
|
+
*
|
|
8
|
+
* An INSCRIBED kind (activities, annotation, group) wears its name inside the
|
|
9
|
+
* shape, so the gesture lands there, exactly like a native shape.
|
|
10
|
+
*
|
|
11
|
+
* An EXTERNAL kind (events, gateways, data shapes) wears its name as a separate
|
|
12
|
+
* `bpmn:label` text grouped under it (R38), which is edited by double-clicking
|
|
13
|
+
* that text — `TextElementView`'s job, not this one's. The symbol itself has
|
|
14
|
+
* nothing to open, the same answer Wardley's node view gives its circles: an
|
|
15
|
+
* editor mounted here would write a second, invisible name into a field that
|
|
16
|
+
* must stay empty. No redirect to the label either: Wardley does not redirect,
|
|
17
|
+
* and the label is one click away inside the group.
|
|
18
|
+
*
|
|
19
|
+
* The one exception is a LEGACY node: an event drawn before R38 may carry inner
|
|
20
|
+
* text (nothing is migrated, `BPMN_EXTERNAL_LABEL_KINDS`). That text is still
|
|
21
|
+
* painted, so it must still be editable — hence "already has text" reopens
|
|
22
|
+
* the editor, and emptying it closes the door behind it.
|
|
23
|
+
*/
|
|
24
|
+
export function bpmnNodeEditsInnerText(model) {
|
|
25
|
+
if (bpmnLabelMode(model.kind) === 'inscribed')
|
|
26
|
+
return true;
|
|
27
|
+
return (model.text?.length ?? 0) > 0;
|
|
28
|
+
}
|
|
4
29
|
/**
|
|
5
30
|
* View for a BPMN flow-object node. Registering it ensures `gfx.view.get(model)`
|
|
6
31
|
* returns a view (required so move / select / connector interactions work).
|
|
7
32
|
*
|
|
8
|
-
* BPMN nodes are native shapes, so
|
|
9
|
-
* double-click mounts the editable text overlay
|
|
10
|
-
* exactly like a native shape
|
|
33
|
+
* BPMN nodes are native shapes, so an inscribed kind reuses the shape
|
|
34
|
+
* inner-text editor: a double-click mounts the editable text overlay
|
|
35
|
+
* (`mountShapeTextEditor`), exactly like a native shape. Which kinds do is
|
|
36
|
+
* {@link bpmnNodeEditsInnerText}'s call.
|
|
11
37
|
*
|
|
12
38
|
* Mirrors {@link EdgyNodeView}.
|
|
13
39
|
*/
|
|
@@ -19,7 +45,8 @@ export class BpmnNodeView extends GfxElementModelView {
|
|
|
19
45
|
const edgeless = this.std.view.getBlock(this.std.store.root.id);
|
|
20
46
|
if (edgeless &&
|
|
21
47
|
!this.model.isLocked() &&
|
|
22
|
-
this.model instanceof ShapeElementModel
|
|
48
|
+
this.model instanceof ShapeElementModel &&
|
|
49
|
+
bpmnNodeEditsInnerText(this.model)) {
|
|
23
50
|
mountShapeTextEditor(this.model, edgeless);
|
|
24
51
|
}
|
|
25
52
|
});
|