@formicoidea/labre-framework-uml 0.42.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/LICENSE +384 -0
- package/dist/actions.d.ts +430 -0
- package/dist/actions.js +724 -0
- package/dist/background.d.ts +153 -0
- package/dist/background.js +557 -0
- package/dist/board-hit.d.ts +137 -0
- package/dist/board-hit.js +191 -0
- package/dist/commands-manifest.d.ts +19 -0
- package/dist/commands-manifest.js +571 -0
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +983 -0
- package/dist/component.d.ts +521 -0
- package/dist/component.js +708 -0
- package/dist/consts.d.ts +365 -0
- package/dist/consts.js +463 -0
- package/dist/descriptor.d.ts +12 -0
- package/dist/descriptor.js +10 -0
- package/dist/drawio-decode.d.ts +32 -0
- package/dist/drawio-decode.js +96 -0
- package/dist/drawio-import.d.ts +133 -0
- package/dist/drawio-import.js +885 -0
- package/dist/edge-morph.d.ts +154 -0
- package/dist/edge-morph.js +153 -0
- package/dist/edge-styles.d.ts +78 -0
- package/dist/edge-styles.js +210 -0
- package/dist/effects.d.ts +9 -0
- package/dist/effects.js +6 -0
- package/dist/element-renderer.d.ts +64 -0
- package/dist/element-renderer.js +114 -0
- package/dist/element-view.d.ts +234 -0
- package/dist/element-view.js +430 -0
- package/dist/export.d.ts +61 -0
- package/dist/export.js +41 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +26 -0
- package/dist/frame-tag.d.ts +70 -0
- package/dist/frame-tag.js +78 -0
- package/dist/grammar.d.ts +695 -0
- package/dist/grammar.js +1436 -0
- package/dist/import.d.ts +306 -0
- package/dist/import.js +1270 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +67 -0
- package/dist/interchange.d.ts +125 -0
- package/dist/interchange.js +372 -0
- package/dist/keywords.d.ts +204 -0
- package/dist/keywords.js +361 -0
- package/dist/kinds.d.ts +73 -0
- package/dist/kinds.js +91 -0
- package/dist/legend.d.ts +30 -0
- package/dist/legend.js +255 -0
- package/dist/model.d.ts +862 -0
- package/dist/model.js +1316 -0
- package/dist/morph.d.ts +178 -0
- package/dist/morph.js +496 -0
- package/dist/node/act-glyphs.d.ts +30 -0
- package/dist/node/act-glyphs.js +142 -0
- package/dist/node/compartment-watcher.d.ts +107 -0
- package/dist/node/compartment-watcher.js +163 -0
- package/dist/node/fit.d.ts +85 -0
- package/dist/node/fit.js +129 -0
- package/dist/node/node-renderer.d.ts +49 -0
- package/dist/node/node-renderer.js +611 -0
- package/dist/node/node-view.d.ts +51 -0
- package/dist/node/node-view.js +89 -0
- package/dist/node/paint.d.ts +84 -0
- package/dist/node/paint.js +143 -0
- package/dist/node/sd-glyphs.d.ts +36 -0
- package/dist/node/sd-glyphs.js +79 -0
- package/dist/node/stm-glyphs.d.ts +22 -0
- package/dist/node/stm-glyphs.js +111 -0
- package/dist/node/tier-metrics.d.ts +38 -0
- package/dist/node/tier-metrics.js +45 -0
- package/dist/operators.d.ts +45 -0
- package/dist/operators.js +75 -0
- package/dist/plantuml-import.d.ts +113 -0
- package/dist/plantuml-import.js +1723 -0
- package/dist/plantuml.d.ts +94 -0
- package/dist/plantuml.js +985 -0
- package/dist/presets.d.ts +149 -0
- package/dist/presets.js +302 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +358 -0
- package/dist/reading.d.ts +160 -0
- package/dist/reading.js +648 -0
- package/dist/roles.d.ts +257 -0
- package/dist/roles.js +997 -0
- package/dist/rules.d.ts +267 -0
- package/dist/rules.js +3090 -0
- package/dist/templates.d.ts +2 -0
- package/dist/templates.js +197 -0
- package/dist/toolbar/config.d.ts +228 -0
- package/dist/toolbar/config.js +833 -0
- package/dist/toolbar/icons.d.ts +440 -0
- package/dist/toolbar/icons.js +711 -0
- package/dist/toolbar/senior-tool.d.ts +11 -0
- package/dist/toolbar/senior-tool.js +21 -0
- package/dist/toolbar/uml-menu.d.ts +17 -0
- package/dist/toolbar/uml-menu.js +20 -0
- package/dist/toolbar/uml-senior-button.d.ts +18 -0
- package/dist/toolbar/uml-senior-button.js +22 -0
- package/dist/translations.d.ts +27 -0
- package/dist/translations.js +117 -0
- package/dist/view.d.ts +33 -0
- package/dist/view.js +236 -0
- package/dist/xmi-import.d.ts +174 -0
- package/dist/xmi-import.js +2268 -0
- package/dist/xmi.d.ts +61 -0
- package/dist/xmi.js +1860 -0
- package/dist/xml-reader.d.ts +157 -0
- package/dist/xml-reader.js +430 -0
- package/dist/xml.d.ts +72 -0
- package/dist/xml.js +96 -0
- package/package.json +38 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
import { EdgelessCRUDIdentifier, generateElementId, legendToolbarAction, validationToolbarConfig, } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { UmlDiagramElementModel, UmlFragmentElementModel, UmlPartitionElementModel, UmlRegionElementModel, } from '@formicoidea/labre-core/model';
|
|
3
|
+
import { BOARD_ADD_BAND, BOARD_ORIENTATION_TOGGLE, BOARD_RESIZE_TOGGLE, commandMoreAction, TelemetryProvider, ToolbarModuleExtension, translateKey, } from '@formicoidea/labre-core/shared/services';
|
|
4
|
+
import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
|
|
5
|
+
import { html, nothing } from 'lit';
|
|
6
|
+
import { UML_DIAGRAM_KIND_MENU, UML_FRAGMENT_OPERATOR_MENU, } from '../kinds.js';
|
|
7
|
+
import { umlExportPlantumlIcon, umlExportXmiIcon } from './icons.js';
|
|
8
|
+
const ResizeIcon = html `<svg
|
|
9
|
+
width="24"
|
|
10
|
+
height="24"
|
|
11
|
+
viewBox="0 0 24 24"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
stroke-width="1.6"
|
|
15
|
+
stroke-linecap="round"
|
|
16
|
+
stroke-linejoin="round"
|
|
17
|
+
>
|
|
18
|
+
<path d="M9 5H5v4M15 19h4v-4" />
|
|
19
|
+
<path d="M5 5l6 6M19 19l-6-6" />
|
|
20
|
+
</svg>`;
|
|
21
|
+
/**
|
|
22
|
+
* The two glyphs the kind picker needs, drawn inline rather than pulled from
|
|
23
|
+
* `@labre/affine-components` — a dependency this package does not have and which
|
|
24
|
+
* is not worth adding for two paths. The same call the C4 board's row makes for
|
|
25
|
+
* its own, and the same call {@link ResizeIcon} above already makes.
|
|
26
|
+
*/
|
|
27
|
+
const ChevronDownIcon = html `<svg
|
|
28
|
+
width="16"
|
|
29
|
+
height="16"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
fill="none"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
stroke-width="2"
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
>
|
|
37
|
+
<path d="M6 9l6 6 6-6" />
|
|
38
|
+
</svg>`;
|
|
39
|
+
/** The tick on the kind in force — rendered at the END of the row. */
|
|
40
|
+
const CheckIcon = html `<svg
|
|
41
|
+
width="20"
|
|
42
|
+
height="20"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
stroke-width="2"
|
|
47
|
+
stroke-linecap="round"
|
|
48
|
+
stroke-linejoin="round"
|
|
49
|
+
>
|
|
50
|
+
<path d="M5 12.5l4.5 4.5L19 7.5" />
|
|
51
|
+
</svg>`;
|
|
52
|
+
/**
|
|
53
|
+
* The selected UML diagram frame's contextual toolbar: the resize toggle, and
|
|
54
|
+
* the two exports in the "⋮".
|
|
55
|
+
*
|
|
56
|
+
* ## Why the exports sit in the ALWAYS-ON half
|
|
57
|
+
*
|
|
58
|
+
* `ActionPlacement.More` partitions an entry out of the row and into the
|
|
59
|
+
* overflow menu, and the entries it collects come from EVERY module contributing
|
|
60
|
+
* to this element — so a slice adds itself there without owning this file. The
|
|
61
|
+
* two interchange exports are the rarest thing anybody does to a frame, and the
|
|
62
|
+
* `y.` / `z.` id prefixes are the sort keys that keep them last, in that order:
|
|
63
|
+
* PlantUML is what an architect pastes into a renderer, XMI is what they hand to
|
|
64
|
+
* another tool. The generic board export (`z.z-export-svg`, contributed by the
|
|
65
|
+
* surface's wildcard module) sorts after both.
|
|
66
|
+
*
|
|
67
|
+
* Each entry is built by the toolbar service's `commandMoreAction`, the one
|
|
68
|
+
* shape every framework's "⋮" shares (`docs/adr/0010` M3): it invokes the
|
|
69
|
+
* registered command rather than restating what it does.
|
|
70
|
+
*
|
|
71
|
+
* They are declared HERE, in the always-on module, rather than beside the legend
|
|
72
|
+
* in the flag-gated one — which is exactly where the C4 board puts its own
|
|
73
|
+
* export. The two kinds of entry look alike and are gated by different
|
|
74
|
+
* mechanisms, and that is the point: the legend button CALLS an action directly,
|
|
75
|
+
* so nothing but the module's registration can hide it, while these ask the
|
|
76
|
+
* registry for a command that only the flag-gated half registers and hide
|
|
77
|
+
* themselves when it is not there. With the `uml` button off the row is the
|
|
78
|
+
* resize toggle alone either way, and nothing on it can be clicked into a no-op.
|
|
79
|
+
*
|
|
80
|
+
* Declaring them in the always-on module is what makes that guard the SINGLE
|
|
81
|
+
* thing deciding whether an export is offered — the same rule the palette, the
|
|
82
|
+
* catalogue and the agent read — instead of the guard plus a second registration
|
|
83
|
+
* that could one day disagree with it.
|
|
84
|
+
*
|
|
85
|
+
* There is no rename entry, and there is nothing missing: both UML frames edit
|
|
86
|
+
* their one word in place on a double-click (`element-view.ts`).
|
|
87
|
+
*/
|
|
88
|
+
export const umlDiagramToolbarConfig = {
|
|
89
|
+
actions: [
|
|
90
|
+
{
|
|
91
|
+
id: 'a.toggle-resize',
|
|
92
|
+
tooltipWording: BOARD_RESIZE_TOGGLE,
|
|
93
|
+
icon: ResizeIcon,
|
|
94
|
+
active(ctx) {
|
|
95
|
+
const models = ctx.getSurfaceModelsByType(UmlDiagramElementModel);
|
|
96
|
+
return models.length > 0 && models.every(model => model.resizeEnabled);
|
|
97
|
+
},
|
|
98
|
+
run(ctx) {
|
|
99
|
+
const models = ctx.getSurfaceModelsByType(UmlDiagramElementModel);
|
|
100
|
+
if (!models.length)
|
|
101
|
+
return;
|
|
102
|
+
const enable = !models.every(model => model.resizeEnabled);
|
|
103
|
+
ctx.std.store.captureSync();
|
|
104
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
105
|
+
for (const model of models) {
|
|
106
|
+
crud.updateElement(model.id, { resizeEnabled: enable });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
// …and, in the "⋮", the two things you do to a finished diagram rather than
|
|
111
|
+
// to the frame it is drawn on: take it away as a file.
|
|
112
|
+
commandMoreAction('y.export-plantuml', 'uml.exportPlantuml', 'com.labre.commands.uml.exportPlantuml', 'Export as PlantUML', umlExportPlantumlIcon),
|
|
113
|
+
commandMoreAction('z.export-xmi', 'uml.exportXmi', 'com.labre.commands.uml.exportXmi', 'Export as XMI', umlExportXmiIcon),
|
|
114
|
+
],
|
|
115
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlDiagramElementModel).length > 0,
|
|
116
|
+
};
|
|
117
|
+
export const umlDiagramToolbarExtension = ToolbarModuleExtension({
|
|
118
|
+
id: BlockFlavourIdentifier('affine:surface:umlDiagram'),
|
|
119
|
+
config: umlDiagramToolbarConfig,
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* The frame the kind picker is about, or `null`.
|
|
123
|
+
*
|
|
124
|
+
* ONE frame, deliberately: a kind is one statement about one sheet, and a
|
|
125
|
+
* selection spanning two frames has no honest current value to show — the same
|
|
126
|
+
* call the C4 level picker and the generic Validation dropdown both make.
|
|
127
|
+
*/
|
|
128
|
+
function selectedDiagram(ctx) {
|
|
129
|
+
const models = ctx.getSurfaceModels();
|
|
130
|
+
if (models.length !== 1)
|
|
131
|
+
return null;
|
|
132
|
+
const [model] = models;
|
|
133
|
+
return model instanceof UmlDiagramElementModel ? model : null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Put the frame on `option`, and report it.
|
|
137
|
+
*
|
|
138
|
+
* `captureSync` first, so one click is one undo — the rule every write on this
|
|
139
|
+
* toolbar follows. Written through {@link EdgelessCRUDIdentifier} rather than by
|
|
140
|
+
* assigning the field, because the heading is DERIVED from it: the CRUD write is
|
|
141
|
+
* what the surface observes, and the frame has to repaint its tag.
|
|
142
|
+
*
|
|
143
|
+
* A choice that changes nothing writes nothing and reports nothing — an undo
|
|
144
|
+
* checkpoint for a no-op is a click the user has to press twice to get back
|
|
145
|
+
* past.
|
|
146
|
+
*
|
|
147
|
+
* There is no "clear" here, unlike the C4 level picker: a UML frame's kind is
|
|
148
|
+
* required (`UmlDiagramElementModel.kind`), because Annex A's heading is
|
|
149
|
+
* `<kind> <name>` and a frame with no kind has no heading to draw.
|
|
150
|
+
*/
|
|
151
|
+
function pickKind(ctx, option) {
|
|
152
|
+
const diagram = selectedDiagram(ctx);
|
|
153
|
+
if (!diagram)
|
|
154
|
+
return;
|
|
155
|
+
const previous = diagram.kind;
|
|
156
|
+
if (previous === option.kind)
|
|
157
|
+
return;
|
|
158
|
+
ctx.std.store.captureSync();
|
|
159
|
+
ctx.std
|
|
160
|
+
.get(EdgelessCRUDIdentifier)
|
|
161
|
+
.updateElement(diagram.id, { kind: option.kind });
|
|
162
|
+
// The same event the C4 level picker emits, with the same wire values: the
|
|
163
|
+
// two frameworks' "the author said which diagram this is" gesture is one
|
|
164
|
+
// metric on a dashboard, and `level` is the field it is reported in.
|
|
165
|
+
ctx.std.getOptional(TelemetryProvider)?.track('FrameworkViewLevelSet', {
|
|
166
|
+
page: 'whiteboard editor',
|
|
167
|
+
segment: 'element toolbar',
|
|
168
|
+
module: 'uml toolbox',
|
|
169
|
+
control: 'kind',
|
|
170
|
+
framework: 'uml',
|
|
171
|
+
level: option.kind,
|
|
172
|
+
previousLevel: previous,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The kind picker: which of UML's diagrams this frame draws.
|
|
177
|
+
*
|
|
178
|
+
* ## Why it is a DECLARED fact and not a rename
|
|
179
|
+
*
|
|
180
|
+
* A frame's name is free text — "Orders", "Checkout", whatever the author writes
|
|
181
|
+
* — and nothing in it says which kind of diagram it is. The kind is a second,
|
|
182
|
+
* small, closed statement, and picking one leaves the words alone: the author
|
|
183
|
+
* keeps their name, the heading gains its tag, and the exporters get a fact they
|
|
184
|
+
* can write into a file (`<kind>` is what tells PlantUML and XMI what they are
|
|
185
|
+
* looking at).
|
|
186
|
+
*
|
|
187
|
+
* ## Why it is flag-gated tooling
|
|
188
|
+
*
|
|
189
|
+
* Declaring a kind is deciding how the diagram is to be READ, which is exactly
|
|
190
|
+
* what `docs/adr/0009` calls tooling: with the `uml` flag off, a frame already
|
|
191
|
+
* carrying a kind keeps it written, keeps painting its heading and simply stops
|
|
192
|
+
* being offered the choice. Nothing a stored document needs to load or paint is
|
|
193
|
+
* behind it.
|
|
194
|
+
*
|
|
195
|
+
* ## Shape
|
|
196
|
+
*
|
|
197
|
+
* A dropdown: four mutually exclusive options with a current value are a menu,
|
|
198
|
+
* not four buttons competing for a toolbar's width. The trigger NAMES the kind
|
|
199
|
+
* in force, so a reader of the row knows what the sheet claims without opening
|
|
200
|
+
* anything.
|
|
201
|
+
*/
|
|
202
|
+
const kindPickerAction = {
|
|
203
|
+
// After the legend (`b.`) and before the Validation dropdown (`z.`): the kind
|
|
204
|
+
// is a statement about the sheet, read before the level of requirement
|
|
205
|
+
// applied to it.
|
|
206
|
+
id: 'c.kind',
|
|
207
|
+
when: (ctx) => selectedDiagram(ctx) !== null,
|
|
208
|
+
content(ctx) {
|
|
209
|
+
const diagram = selectedDiagram(ctx);
|
|
210
|
+
if (!diagram)
|
|
211
|
+
return null;
|
|
212
|
+
const menuLabel = translateKey(ctx.std, UML_DIAGRAM_KIND_MENU.labelKey, UML_DIAGRAM_KIND_MENU.labelFallback);
|
|
213
|
+
const wordsFor = (option) => translateKey(ctx.std, option.labelKey, option.labelFallback);
|
|
214
|
+
// A frame carrying a kind this build does not know — a phase-2 value on a
|
|
215
|
+
// phase-1 build, an import — shows the menu's own heading rather than
|
|
216
|
+
// silently reading as a class diagram.
|
|
217
|
+
const current = UML_DIAGRAM_KIND_MENU.options.find(option => option.kind === diagram.kind);
|
|
218
|
+
const options = UML_DIAGRAM_KIND_MENU.options.map(option => {
|
|
219
|
+
const selected = option.kind === diagram.kind;
|
|
220
|
+
return html `<editor-menu-action
|
|
221
|
+
data-option
|
|
222
|
+
data-testid="uml-kind-option"
|
|
223
|
+
data-kind=${option.kind}
|
|
224
|
+
data-selected=${selected ? 'true' : nothing}
|
|
225
|
+
aria-label=${wordsFor(option)}
|
|
226
|
+
aria-pressed=${selected}
|
|
227
|
+
@click=${() => pickKind(ctx, option)}
|
|
228
|
+
>
|
|
229
|
+
<span class="label">${wordsFor(option)}</span>
|
|
230
|
+
${selected ? CheckIcon : nothing}
|
|
231
|
+
</editor-menu-action>`;
|
|
232
|
+
});
|
|
233
|
+
return html `<editor-menu-button
|
|
234
|
+
data-testid="uml-kind-entry"
|
|
235
|
+
.contentPadding=${'8px'}
|
|
236
|
+
.button=${html `
|
|
237
|
+
<editor-icon-button
|
|
238
|
+
data-testid="uml-kind-button"
|
|
239
|
+
aria-label=${menuLabel}
|
|
240
|
+
.tooltip=${menuLabel}
|
|
241
|
+
.justify=${'space-between'}
|
|
242
|
+
.labelHeight=${'20px'}
|
|
243
|
+
>
|
|
244
|
+
<span class="label"
|
|
245
|
+
>${current === undefined ? menuLabel : wordsFor(current)}</span
|
|
246
|
+
>
|
|
247
|
+
${ChevronDownIcon}
|
|
248
|
+
</editor-icon-button>
|
|
249
|
+
`}
|
|
250
|
+
>
|
|
251
|
+
<div
|
|
252
|
+
data-testid="uml-kind-menu"
|
|
253
|
+
data-orientation="vertical"
|
|
254
|
+
data-size="large"
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
role="group"
|
|
258
|
+
aria-label=${menuLabel}
|
|
259
|
+
style="display: flex; flex-direction: column;"
|
|
260
|
+
>
|
|
261
|
+
${options}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</editor-menu-button>`;
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* The legend button — the flag-gated half of the row.
|
|
269
|
+
*
|
|
270
|
+
* ## The legend is a BUTTON, not a command
|
|
271
|
+
*
|
|
272
|
+
* Every other UML gesture is a registered `CommandDescriptor`, which is the
|
|
273
|
+
* bottleneck `docs/adr/0008` asks for. The legend is the arbitrated exception,
|
|
274
|
+
* carried over from C4 and the Context Map (PO arbitration, 27/08/2026):
|
|
275
|
+
* generating one belongs to a frame you have SELECTED and to nothing else — it
|
|
276
|
+
* is not an artefact to pick off a palette, and an entry in a catalogue of
|
|
277
|
+
* things UML draws would offer it to a user with no diagram in front of them. So
|
|
278
|
+
* there is no `uml.legend` command, and this button is the only way to reach it.
|
|
279
|
+
*
|
|
280
|
+
* The cost is the one the bottleneck exists to avoid: the telemetry it owes is
|
|
281
|
+
* emitted outside `runCommand`. It is emitted ONCE, by the shared factory
|
|
282
|
+
* (`legend-toolbar.ts`), so the seven copies of those wire values that used to
|
|
283
|
+
* sit in seven files can no longer drift apart.
|
|
284
|
+
*
|
|
285
|
+
* `b.` — the factory's default — sorts it after the resize toggle, so the two
|
|
286
|
+
* modules render as the one row a user sees rather than in registration order.
|
|
287
|
+
*/
|
|
288
|
+
export const umlLegendToolbarConfig = {
|
|
289
|
+
actions: [
|
|
290
|
+
// The SHARED factory, not a UML copy of it: the button, the first selected
|
|
291
|
+
// frame and no other, the scan, the placement, the box and the one
|
|
292
|
+
// `FrameworkLegendCreated` emission all live in the surface block
|
|
293
|
+
// (`legend-toolbar.ts`, `docs/adr/0026`). UML hands it the frame model to
|
|
294
|
+
// look for, whose commands to read the rows off, and the two historical wire
|
|
295
|
+
// values — `module` is `uml toolbox` here where the other six say
|
|
296
|
+
// `<framework> toolbar`, so it is passed rather than derived.
|
|
297
|
+
legendToolbarAction({
|
|
298
|
+
Model: UmlDiagramElementModel,
|
|
299
|
+
owner: 'uml',
|
|
300
|
+
framework: 'uml',
|
|
301
|
+
module: 'uml toolbox',
|
|
302
|
+
}),
|
|
303
|
+
],
|
|
304
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlDiagramElementModel).length > 0,
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* The frame's flag-gated row, WHOLE: the legend button and the kind picker, in
|
|
308
|
+
* ONE module.
|
|
309
|
+
*
|
|
310
|
+
* ## Why they cannot be two modules
|
|
311
|
+
*
|
|
312
|
+
* `renderToolbar` merges exactly four slots per element — `<flavour>`,
|
|
313
|
+
* `custom:<flavour>`, and the two `affine:surface:*` wildcards — and
|
|
314
|
+
* `ToolbarModuleExtension` binds by DI variant, so a second module claiming
|
|
315
|
+
* `custom:affine:surface:umlDiagram` throws `DuplicateServiceDefinitionError`
|
|
316
|
+
* before the editor finishes setting up. Two slots, and UML has three things to
|
|
317
|
+
* put on a selected frame: the resize toggle (always-on, `<flavour>`), the
|
|
318
|
+
* legend and the kind.
|
|
319
|
+
*
|
|
320
|
+
* Both of the latter are gated by the same flag and appear together or not at
|
|
321
|
+
* all, so one module is the honest grouping rather than a workaround: `uml` off
|
|
322
|
+
* takes away the gesture that CREATES legend elements and the choice of which
|
|
323
|
+
* kind of diagram this sheet is, and leaves the stored frame its handles, its
|
|
324
|
+
* heading and everything already drawn on it.
|
|
325
|
+
*
|
|
326
|
+
* Sorting keeps the row readable across the merge — `b.legend`, `c.kind`, then
|
|
327
|
+
* `z.validation` from {@link validationToolbarConfig} — so the user sees resize,
|
|
328
|
+
* legend, the diagram's KIND, then the level of requirement, whatever order the
|
|
329
|
+
* modules were registered in.
|
|
330
|
+
*/
|
|
331
|
+
export const umlDiagramToolingToolbarConfig = {
|
|
332
|
+
actions: [
|
|
333
|
+
...umlLegendToolbarConfig.actions,
|
|
334
|
+
// Which of the four UML diagrams this sheet draws — a fact about the frame,
|
|
335
|
+
// written on the frame, read by `uml.not-admissible-on-kind`.
|
|
336
|
+
kindPickerAction,
|
|
337
|
+
// The generic dropdown, not a UML variant of it: the config names no
|
|
338
|
+
// framework — it reads the registered rules and profiles — so this is the
|
|
339
|
+
// very same object wardley, bpmn, the context map and C4 register.
|
|
340
|
+
...validationToolbarConfig.actions,
|
|
341
|
+
],
|
|
342
|
+
when: umlLegendToolbarConfig.when,
|
|
343
|
+
};
|
|
344
|
+
export const umlDiagramToolingToolbarExtension = ToolbarModuleExtension({
|
|
345
|
+
id: BlockFlavourIdentifier('custom:affine:surface:umlDiagram'),
|
|
346
|
+
config: umlDiagramToolingToolbarConfig,
|
|
347
|
+
});
|
|
348
|
+
/**
|
|
349
|
+
* The SUBJECT claims no toolbar flavour at all, and that is a decision rather
|
|
350
|
+
* than an omission.
|
|
351
|
+
*
|
|
352
|
+
* The same arbitration the C4 boundary records (PO, 28/08/2026): **the sheet
|
|
353
|
+
* alone arbitrates the checklist.** One diagram, one level of requirement, one
|
|
354
|
+
* place to set it — a second picker on a frame drawn INSIDE the first is a way
|
|
355
|
+
* to make a diagram disagree with itself, and no reader could tell which answer
|
|
356
|
+
* it was being held to. `inheritChosenProfiles` makes a frame that names no
|
|
357
|
+
* profile inherit the innermost containing frame's choice, so a subject drawn on
|
|
358
|
+
* a diagram set to Review checklist is itself on Review checklist.
|
|
359
|
+
*
|
|
360
|
+
* It has no kind picker either, and for a plainer reason: §18.1.4 gives the
|
|
361
|
+
* subject one rectangle with one name and nothing to choose between.
|
|
362
|
+
*/
|
|
363
|
+
/* ── Phase 2: the two banded frames of the behaviour diagrams ───────────── */
|
|
364
|
+
/**
|
|
365
|
+
* The two arrows of the orientation toggle — the bands turned a quarter turn.
|
|
366
|
+
*
|
|
367
|
+
* Drawn inline, as every other glyph on this row is and for the same reason:
|
|
368
|
+
* `@labre/affine-components` is not a dependency of this package and two paths
|
|
369
|
+
* are not worth making it one.
|
|
370
|
+
*/
|
|
371
|
+
const OrientationIcon = html `<svg
|
|
372
|
+
width="24"
|
|
373
|
+
height="24"
|
|
374
|
+
viewBox="0 0 24 24"
|
|
375
|
+
fill="none"
|
|
376
|
+
stroke="currentColor"
|
|
377
|
+
stroke-width="1.6"
|
|
378
|
+
stroke-linecap="round"
|
|
379
|
+
stroke-linejoin="round"
|
|
380
|
+
>
|
|
381
|
+
<path d="M4 4h5v16H4zM15 4h5v16h-5z" />
|
|
382
|
+
<path d="M11.5 9.5l1 1 1-1M12.5 10.5v3" />
|
|
383
|
+
</svg>`;
|
|
384
|
+
/**
|
|
385
|
+
* The resize toggle, for a frame that is neither the diagram nor the subject.
|
|
386
|
+
*
|
|
387
|
+
* Written once for the two phase-2 backgrounds rather than a third time inline:
|
|
388
|
+
* a partition and a region toggle the very same declared field on the very same
|
|
389
|
+
* primitive, and the only thing that differs between their two rows is which
|
|
390
|
+
* model class the selection is filtered by.
|
|
391
|
+
*
|
|
392
|
+
* `a.` sorts it first, exactly as it does on the diagram's row, so the merged
|
|
393
|
+
* row reads the same wherever the modules were registered.
|
|
394
|
+
*/
|
|
395
|
+
function resizeToggle(klass) {
|
|
396
|
+
return {
|
|
397
|
+
id: 'a.toggle-resize',
|
|
398
|
+
tooltipWording: BOARD_RESIZE_TOGGLE,
|
|
399
|
+
icon: ResizeIcon,
|
|
400
|
+
active(ctx) {
|
|
401
|
+
const models = ctx.getSurfaceModelsByType(klass);
|
|
402
|
+
return models.length > 0 && models.every(model => model.resizeEnabled);
|
|
403
|
+
},
|
|
404
|
+
run(ctx) {
|
|
405
|
+
const models = ctx.getSurfaceModelsByType(klass);
|
|
406
|
+
if (!models.length)
|
|
407
|
+
return;
|
|
408
|
+
const enable = !models.every(model => model.resizeEnabled);
|
|
409
|
+
ctx.std.store.captureSync();
|
|
410
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
411
|
+
for (const model of models) {
|
|
412
|
+
crud.updateElement(model.id, { resizeEnabled: enable });
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The ACTIVITY PARTITION's row: the resize toggle, and the one thing a swimlane
|
|
419
|
+
* can be said differently — which way its bands run.
|
|
420
|
+
*
|
|
421
|
+
* ## Why the orientation toggle is ALWAYS-ON
|
|
422
|
+
*
|
|
423
|
+
* It writes a stored field, so it is not obviously tooling — and that is exactly
|
|
424
|
+
* the question `docs/adr/0009` makes people ask. The answer is the one the
|
|
425
|
+
* resize toggle already gives: this is a GESTURE ON A SELECTED ELEMENT, not a
|
|
426
|
+
* way to put a new one on the canvas. A partition drawn while the `uml` flag was
|
|
427
|
+
* on must stay usable with it off — moved, stretched, renamed, and turned the
|
|
428
|
+
* right way round for the sheet it sits on — because every one of those is
|
|
429
|
+
* something the author does to a thing that is already there.
|
|
430
|
+
*
|
|
431
|
+
* The line the ADR actually draws is between the ways to CREATE and the ways to
|
|
432
|
+
* work with what exists. `resizeEnabled` writes content too (it is a field of
|
|
433
|
+
* the model, undoable like any other); so does the rename on a double-click; so
|
|
434
|
+
* does dragging the frame. What the flag takes away is the button that adds a
|
|
435
|
+
* partition in the first place, and the menu it lives in.
|
|
436
|
+
*
|
|
437
|
+
* ## Why a toggle and not a two-option dropdown
|
|
438
|
+
*
|
|
439
|
+
* §15.6.4 gives the orientation no meaning: a partition drawn in columns and the
|
|
440
|
+
* same partition drawn in rows say the same thing, and the choice is made
|
|
441
|
+
* against the sheet — a wide activity diagram wants rows, a tall one wants
|
|
442
|
+
* columns. Two mutually exclusive values with no semantics between them are a
|
|
443
|
+
* flip, and a dropdown would spend a menu on a question whose two answers are
|
|
444
|
+
* "this way" and "the other way". The C4 level picker is a dropdown because
|
|
445
|
+
* there are four levels and each MEANS something.
|
|
446
|
+
*
|
|
447
|
+
* `active` reports HORIZONTAL — the value that is not the default — so the
|
|
448
|
+
* button reads as pressed exactly when the author has turned the bands.
|
|
449
|
+
*
|
|
450
|
+
* ponytail: the flip writes `orientation` and nothing else. It does not swap the
|
|
451
|
+
* frame's width and height, so a tall partition turned horizontal is a wide band
|
|
452
|
+
* stack in a tall box until the author drags it — the notation has nothing to
|
|
453
|
+
* say about the box, and guessing a new one is a gesture the user did not ask
|
|
454
|
+
* for and cannot half-undo.
|
|
455
|
+
*/
|
|
456
|
+
export const umlPartitionToolbarConfig = {
|
|
457
|
+
actions: [
|
|
458
|
+
resizeToggle(UmlPartitionElementModel),
|
|
459
|
+
{
|
|
460
|
+
// After the resize toggle, before anything a later phase hangs off the
|
|
461
|
+
// `custom:` twin — the row reads resize, then orientation.
|
|
462
|
+
id: 'b.orientation',
|
|
463
|
+
tooltipWording: BOARD_ORIENTATION_TOGGLE,
|
|
464
|
+
icon: OrientationIcon,
|
|
465
|
+
active(ctx) {
|
|
466
|
+
const models = ctx.getSurfaceModelsByType(UmlPartitionElementModel);
|
|
467
|
+
return (models.length > 0 &&
|
|
468
|
+
models.every(model => model.orientation === 'horizontal'));
|
|
469
|
+
},
|
|
470
|
+
run(ctx) {
|
|
471
|
+
const models = ctx.getSurfaceModelsByType(UmlPartitionElementModel);
|
|
472
|
+
if (!models.length)
|
|
473
|
+
return;
|
|
474
|
+
// One click, one answer for the whole selection: turn them all
|
|
475
|
+
// horizontal unless they already all are, in which case turn them back.
|
|
476
|
+
// The same rule the resize toggle follows, and the reason a mixed
|
|
477
|
+
// selection converges instead of alternating.
|
|
478
|
+
const next = models.every(model => model.orientation === 'horizontal')
|
|
479
|
+
? 'vertical'
|
|
480
|
+
: 'horizontal';
|
|
481
|
+
ctx.std.store.captureSync();
|
|
482
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
483
|
+
for (const model of models) {
|
|
484
|
+
crud.updateElement(model.id, { orientation: next });
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlPartitionElementModel).length > 0,
|
|
490
|
+
};
|
|
491
|
+
export const umlPartitionToolbarExtension = ToolbarModuleExtension({
|
|
492
|
+
id: BlockFlavourIdentifier('affine:surface:umlPartition'),
|
|
493
|
+
config: umlPartitionToolbarConfig,
|
|
494
|
+
});
|
|
495
|
+
/**
|
|
496
|
+
* The REGION's row: the resize toggle, and nothing else.
|
|
497
|
+
*
|
|
498
|
+
* A region is a composite state's inside (§14.2.4) — one rounded rectangle with
|
|
499
|
+
* one name compartment and sub-states drawn in it. It has no orientation: its
|
|
500
|
+
* band is its name compartment, which §14.2.4 puts at the top and nowhere else,
|
|
501
|
+
* and there is no second reading of the same picture to flip to. So the row is
|
|
502
|
+
* the one entry every framework background carries, and the rename is on the
|
|
503
|
+
* band under a double-click (`element-view.ts`) as it is on all the others.
|
|
504
|
+
*
|
|
505
|
+
* Registered as its own module rather than folded into the partition's because
|
|
506
|
+
* `renderToolbar` merges BY FLAVOUR: these are two element types, so two rows.
|
|
507
|
+
*/
|
|
508
|
+
export const umlRegionToolbarConfig = {
|
|
509
|
+
actions: [resizeToggle(UmlRegionElementModel)],
|
|
510
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlRegionElementModel).length > 0,
|
|
511
|
+
};
|
|
512
|
+
export const umlRegionToolbarExtension = ToolbarModuleExtension({
|
|
513
|
+
id: BlockFlavourIdentifier('affine:surface:umlRegion'),
|
|
514
|
+
config: umlRegionToolbarConfig,
|
|
515
|
+
});
|
|
516
|
+
/* ── Phase 3: the COMBINED FRAGMENT's two rows (§17.6.4) ────────────────── */
|
|
517
|
+
/**
|
|
518
|
+
* The COMBINED FRAGMENT's always-on row: the resize toggle, and nothing else.
|
|
519
|
+
*
|
|
520
|
+
* The same argument the region's makes, and the same one entry: a fragment is a
|
|
521
|
+
* rectangle drawn round part of an interaction, and a document that holds one
|
|
522
|
+
* must keep it movable and stretchable with the UML button switched off
|
|
523
|
+
* (`docs/adr/0009`). What the flag takes away is the two gestures below.
|
|
524
|
+
*/
|
|
525
|
+
export const umlFragmentToolbarConfig = {
|
|
526
|
+
actions: [resizeToggle(UmlFragmentElementModel)],
|
|
527
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlFragmentElementModel).length > 0,
|
|
528
|
+
};
|
|
529
|
+
export const umlFragmentToolbarExtension = ToolbarModuleExtension({
|
|
530
|
+
id: BlockFlavourIdentifier('affine:surface:umlFragment'),
|
|
531
|
+
config: umlFragmentToolbarConfig,
|
|
532
|
+
});
|
|
533
|
+
/**
|
|
534
|
+
* The fragments an operand gesture acts on: every one of the current selection
|
|
535
|
+
* that is not locked, on an editable document.
|
|
536
|
+
*
|
|
537
|
+
* Lifted from `bpmnPoolsForLaneEdit`, filter for filter and for the same
|
|
538
|
+
* reasons: EVERY selected fragment rather than the single one (a gesture that
|
|
539
|
+
* says what it did on each beats an entry that vanishes on a lasso), and the
|
|
540
|
+
* read-only and lock filters because this is about to write.
|
|
541
|
+
*/
|
|
542
|
+
function umlFragmentsForOperandEdit(ctx) {
|
|
543
|
+
if (ctx.std.store.readonly)
|
|
544
|
+
return [];
|
|
545
|
+
return ctx
|
|
546
|
+
.getSurfaceModelsByType(UmlFragmentElementModel)
|
|
547
|
+
.filter(model => !model.isLocked());
|
|
548
|
+
}
|
|
549
|
+
/** A fragment's operand list, as an array whatever the document holds. */
|
|
550
|
+
const operandsOf = (model) => Array.isArray(model.operands) ? model.operands : [];
|
|
551
|
+
/**
|
|
552
|
+
* Append an operand to every selected fragment — the second branch of an
|
|
553
|
+
* `alt`, the `else` of a `par`, the next case of a fragment that has one
|
|
554
|
+
* (§17.6.4).
|
|
555
|
+
*
|
|
556
|
+
* ## Why the first click writes TWO
|
|
557
|
+
*
|
|
558
|
+
* Because §17.6.4's picture is unambiguous and BPMN's lane gesture is the wrong
|
|
559
|
+
* analogy here. A pool with no lanes is a pool; a pool with one lane is a pool
|
|
560
|
+
* with a lane drawn in it, and both are legal BPMN. A combined fragment with no
|
|
561
|
+
* `operands` is the ONE-operand fragment §17.6.4 draws — a box with a tag and
|
|
562
|
+
* no dashed rule — and what an author asks for by pressing this is the SECOND
|
|
563
|
+
* branch, with the separator between the two. Writing one zone would divide the
|
|
564
|
+
* fragment into a single band and draw nothing at all.
|
|
565
|
+
*
|
|
566
|
+
* So the first press seeds the operand that was implicit plus the new one, and
|
|
567
|
+
* every press after it appends a single band. Equal shares throughout: the
|
|
568
|
+
* average of what is there is the weight that leaves the existing operands the
|
|
569
|
+
* same size relative to each other, and gives the newcomer the room a typical
|
|
570
|
+
* one has.
|
|
571
|
+
*
|
|
572
|
+
* ## No guard is invented, and the operator is not restated
|
|
573
|
+
*
|
|
574
|
+
* A new band arrives with no `name` at all, which is the decision the model
|
|
575
|
+
* records for the fragment's own guard and the same one for the same reason:
|
|
576
|
+
* §17.6.4 writes a condition in an operand's corner only where there is one,
|
|
577
|
+
* and a band born carrying `[condition]` would be a guard the author never
|
|
578
|
+
* wrote. The first operand, made explicit here, carries whatever guard the
|
|
579
|
+
* fragment already had — nothing is invented and nothing is lost.
|
|
580
|
+
*
|
|
581
|
+
* Nothing here touches `operator` either: which kind of fragment this is is one
|
|
582
|
+
* statement about the whole box, and the picker beside this button is where it
|
|
583
|
+
* is made.
|
|
584
|
+
*
|
|
585
|
+
* ## Nothing moves
|
|
586
|
+
*
|
|
587
|
+
* Messages already drawn inside the fragment do NOT move. Membership here is
|
|
588
|
+
* geometric — an occurrence is "in" an operand because it falls in that band —
|
|
589
|
+
* so the bands simply redraw around what is there, and the line an author drew
|
|
590
|
+
* still lands where they drew it. The same promise `removeBpmnLane` makes, for
|
|
591
|
+
* the same reason.
|
|
592
|
+
*/
|
|
593
|
+
function addUmlOperand(ctx) {
|
|
594
|
+
const fragments = umlFragmentsForOperandEdit(ctx);
|
|
595
|
+
if (fragments.length === 0)
|
|
596
|
+
return;
|
|
597
|
+
// Before the writes: one capture for the whole gesture is what makes several
|
|
598
|
+
// fragments take their operand in a single undo step.
|
|
599
|
+
ctx.std.store.captureSync();
|
|
600
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
601
|
+
for (const model of fragments) {
|
|
602
|
+
const operands = operandsOf(model);
|
|
603
|
+
const size = operands.length
|
|
604
|
+
? operands.reduce((sum, operand) => sum + operand.size, 0) /
|
|
605
|
+
operands.length
|
|
606
|
+
: 1;
|
|
607
|
+
// The implicit first operand, made explicit — see the header. A fragment
|
|
608
|
+
// that already declares its zones simply gains one.
|
|
609
|
+
const existing = operands.length
|
|
610
|
+
? operands
|
|
611
|
+
: [{ id: generateElementId(), name: model.name || undefined, size }];
|
|
612
|
+
crud.updateElement(model.id, {
|
|
613
|
+
// …and the guard is MOVED, never copied. The declared `guard` zone and
|
|
614
|
+
// operand zero's label are anchored in the same corner (`background.ts`),
|
|
615
|
+
// so a fragment left carrying both would paint the condition twice, one
|
|
616
|
+
// string over the other. One guard, one place.
|
|
617
|
+
name: '',
|
|
618
|
+
operands: [...existing, { id: generateElementId(), size }],
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* The two arrows and the rule between them: an operand being added under the
|
|
624
|
+
* one that is there.
|
|
625
|
+
*
|
|
626
|
+
* Drawn inline like every other glyph on this row, for the reason
|
|
627
|
+
* {@link ResizeIcon} and {@link OrientationIcon} give — this package does not
|
|
628
|
+
* depend on `@labre/affine-components`, and one path is not worth making it.
|
|
629
|
+
*/
|
|
630
|
+
const AddOperandIcon = html `<svg
|
|
631
|
+
width="24"
|
|
632
|
+
height="24"
|
|
633
|
+
viewBox="0 0 24 24"
|
|
634
|
+
fill="none"
|
|
635
|
+
stroke="currentColor"
|
|
636
|
+
stroke-width="1.6"
|
|
637
|
+
stroke-linecap="round"
|
|
638
|
+
stroke-linejoin="round"
|
|
639
|
+
>
|
|
640
|
+
<path d="M4 4h16v16H4z" />
|
|
641
|
+
<path d="M4 12h16" stroke-dasharray="2.5 2.5" />
|
|
642
|
+
<path d="M12 14.5v4M10 16.5h4" />
|
|
643
|
+
</svg>`;
|
|
644
|
+
/**
|
|
645
|
+
* Which kind of combined fragment this is — the operator picker (§17.6.4).
|
|
646
|
+
*
|
|
647
|
+
* The same control as the diagram frame's kind picker, on the other frame, and
|
|
648
|
+
* it is the same argument: a closed set of mutually exclusive values with a
|
|
649
|
+
* current one is a MENU, not a row of buttons, and the trigger names the value
|
|
650
|
+
* in force so a reader of the row knows what the box claims without opening
|
|
651
|
+
* anything. `UML_FRAGMENT_OPERATOR_MENU` is DATA (`operators.ts`), walked by the
|
|
652
|
+
* translation manifest exactly as the diagram's table is.
|
|
653
|
+
*
|
|
654
|
+
* ## Why the operator is a picker and not a morph
|
|
655
|
+
*
|
|
656
|
+
* A morph retypes an ELEMENT — it rewrites the props that make a rectangle look
|
|
657
|
+
* like an interface rather than a class. An operator changes one field of one
|
|
658
|
+
* element and changes no prop at all: the box is the same box, the border is
|
|
659
|
+
* the same border, and what moves is the word in the pentagon. There is nothing
|
|
660
|
+
* for a family declaration to declare.
|
|
661
|
+
*
|
|
662
|
+
* ## `ref` is in the menu, and that is deliberate
|
|
663
|
+
*
|
|
664
|
+
* An interaction use is this element with `ref` in the tag (§17.7.4), so it is
|
|
665
|
+
* one of the values the field takes and hiding it here would mean an author who
|
|
666
|
+
* drew an `alt` and meant a `ref` has to delete the box and draw another. The
|
|
667
|
+
* command that creates one is separate because it is a different modelling act
|
|
668
|
+
* (`actions.ts`); the field is one field.
|
|
669
|
+
*/
|
|
670
|
+
const operatorPickerAction = {
|
|
671
|
+
// After the add-operand button (`b.`) and before the Validation dropdown
|
|
672
|
+
// (`z.`), which is the diagram frame's own ordering: what the frame IS, then
|
|
673
|
+
// the level of requirement applied to it.
|
|
674
|
+
id: 'c.operator',
|
|
675
|
+
when: (ctx) => selectedFragment(ctx) !== null,
|
|
676
|
+
content(ctx) {
|
|
677
|
+
const fragment = selectedFragment(ctx);
|
|
678
|
+
if (!fragment)
|
|
679
|
+
return null;
|
|
680
|
+
const menuLabel = translateKey(ctx.std, UML_FRAGMENT_OPERATOR_MENU.labelKey, UML_FRAGMENT_OPERATOR_MENU.labelFallback);
|
|
681
|
+
const wordsFor = (option) => translateKey(ctx.std, option.labelKey, option.labelFallback);
|
|
682
|
+
// A fragment carrying an operator this build does not know — an import, a
|
|
683
|
+
// later phase's value — shows the menu's own heading rather than silently
|
|
684
|
+
// reading as an `alt`.
|
|
685
|
+
const current = UML_FRAGMENT_OPERATOR_MENU.options.find(option => option.operator === fragment.operator);
|
|
686
|
+
const options = UML_FRAGMENT_OPERATOR_MENU.options.map(option => {
|
|
687
|
+
const selected = option.operator === fragment.operator;
|
|
688
|
+
return html `<editor-menu-action
|
|
689
|
+
data-option
|
|
690
|
+
data-testid="uml-operator-option"
|
|
691
|
+
data-operator=${option.operator}
|
|
692
|
+
data-selected=${selected ? 'true' : nothing}
|
|
693
|
+
aria-label=${wordsFor(option)}
|
|
694
|
+
aria-pressed=${selected}
|
|
695
|
+
@click=${() => pickOperator(ctx, option)}
|
|
696
|
+
>
|
|
697
|
+
<span class="label">${wordsFor(option)}</span>
|
|
698
|
+
${selected ? CheckIcon : nothing}
|
|
699
|
+
</editor-menu-action>`;
|
|
700
|
+
});
|
|
701
|
+
return html `<editor-menu-button
|
|
702
|
+
data-testid="uml-operator-entry"
|
|
703
|
+
.contentPadding=${'8px'}
|
|
704
|
+
.button=${html `
|
|
705
|
+
<editor-icon-button
|
|
706
|
+
data-testid="uml-operator-button"
|
|
707
|
+
aria-label=${menuLabel}
|
|
708
|
+
.tooltip=${menuLabel}
|
|
709
|
+
.justify=${'space-between'}
|
|
710
|
+
.labelHeight=${'20px'}
|
|
711
|
+
>
|
|
712
|
+
<span class="label"
|
|
713
|
+
>${current === undefined ? menuLabel : wordsFor(current)}</span
|
|
714
|
+
>
|
|
715
|
+
${ChevronDownIcon}
|
|
716
|
+
</editor-icon-button>
|
|
717
|
+
`}
|
|
718
|
+
>
|
|
719
|
+
<div
|
|
720
|
+
data-testid="uml-operator-menu"
|
|
721
|
+
data-orientation="vertical"
|
|
722
|
+
data-size="large"
|
|
723
|
+
>
|
|
724
|
+
<div
|
|
725
|
+
role="group"
|
|
726
|
+
aria-label=${menuLabel}
|
|
727
|
+
style="display: flex; flex-direction: column;"
|
|
728
|
+
>
|
|
729
|
+
${options}
|
|
730
|
+
</div>
|
|
731
|
+
</div>
|
|
732
|
+
</editor-menu-button>`;
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* The fragment the operator picker is about, or `null` — ONE of them, for the
|
|
737
|
+
* reason {@link selectedDiagram} gives: an operator is one statement about one
|
|
738
|
+
* box, and a selection spanning two has no honest current value to show.
|
|
739
|
+
*/
|
|
740
|
+
function selectedFragment(ctx) {
|
|
741
|
+
const models = ctx.getSurfaceModels();
|
|
742
|
+
if (models.length !== 1)
|
|
743
|
+
return null;
|
|
744
|
+
const [model] = models;
|
|
745
|
+
return model instanceof UmlFragmentElementModel ? model : null;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Put the fragment on `option`, and report it.
|
|
749
|
+
*
|
|
750
|
+
* The same three rules `pickKind` follows: `captureSync` first so one click is
|
|
751
|
+
* one undo, written through {@link EdgelessCRUDIdentifier} because the tag is
|
|
752
|
+
* DERIVED from the field and the surface has to repaint it, and a choice that
|
|
753
|
+
* changes nothing writes nothing and reports nothing.
|
|
754
|
+
*
|
|
755
|
+
* The guard is NOT rewritten. `name` holds the author's own `[condition]` (or
|
|
756
|
+
* the name of the interaction a `ref` points at), and a picker that cleared it
|
|
757
|
+
* on the way from `alt` to `opt` would be taking away words somebody typed —
|
|
758
|
+
* the very rule the node morph's `afterMorph` is written round.
|
|
759
|
+
*/
|
|
760
|
+
function pickOperator(ctx, option) {
|
|
761
|
+
const fragment = selectedFragment(ctx);
|
|
762
|
+
if (!fragment)
|
|
763
|
+
return;
|
|
764
|
+
const previous = fragment.operator;
|
|
765
|
+
if (previous === option.operator)
|
|
766
|
+
return;
|
|
767
|
+
ctx.std.store.captureSync();
|
|
768
|
+
ctx.std
|
|
769
|
+
.get(EdgelessCRUDIdentifier)
|
|
770
|
+
.updateElement(fragment.id, { operator: option.operator });
|
|
771
|
+
// The same event and the same wire values the diagram's kind picker emits:
|
|
772
|
+
// "the author said which kind of thing this frame is" is one metric on a
|
|
773
|
+
// dashboard, and `level` is the field it is reported in. `control` is what
|
|
774
|
+
// separates the two — `kind` for the sheet, `operator` for the fragment.
|
|
775
|
+
ctx.std.getOptional(TelemetryProvider)?.track('FrameworkViewLevelSet', {
|
|
776
|
+
page: 'whiteboard editor',
|
|
777
|
+
segment: 'element toolbar',
|
|
778
|
+
module: 'uml toolbox',
|
|
779
|
+
control: 'operator',
|
|
780
|
+
framework: 'uml',
|
|
781
|
+
level: option.operator,
|
|
782
|
+
previousLevel: previous,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* The fragment's FLAG-GATED row: add an operand, pick the operator, and the
|
|
787
|
+
* generic Validation dropdown.
|
|
788
|
+
*
|
|
789
|
+
* ## Why the two gestures are here and the resize toggle is not
|
|
790
|
+
*
|
|
791
|
+
* The line `docs/adr/0009` draws is between the ways to CREATE and the ways to
|
|
792
|
+
* work with what already exists, and these two fall on the creating side of it:
|
|
793
|
+
* adding an operand puts a band on the canvas that was not there (the same
|
|
794
|
+
* thing `bpmn.addLane` does, and BPMN gates it with its own flag through the
|
|
795
|
+
* command registry), and the operator picker declares how the drawing is to be
|
|
796
|
+
* READ — which is what the ADR calls tooling in as many words, and where the
|
|
797
|
+
* diagram frame's kind picker already sits.
|
|
798
|
+
*
|
|
799
|
+
* With the `uml` flag off a stored fragment keeps its operator written, keeps
|
|
800
|
+
* painting its pentagon and its dashed rules, keeps every band it has and keeps
|
|
801
|
+
* its handles. What goes away is the button that adds a band and the menu that
|
|
802
|
+
* changes the word.
|
|
803
|
+
*
|
|
804
|
+
* ## One module, for the reason the diagram's row is one module
|
|
805
|
+
*
|
|
806
|
+
* `renderToolbar` merges four slots per element and `ToolbarModuleExtension`
|
|
807
|
+
* binds by DI variant, so a second module claiming
|
|
808
|
+
* `custom:affine:surface:umlFragment` throws before the editor finishes setting
|
|
809
|
+
* up. Both entries are gated by the same flag and appear together or not at
|
|
810
|
+
* all, so one module is the honest grouping rather than a workaround.
|
|
811
|
+
*/
|
|
812
|
+
export const umlFragmentToolingToolbarConfig = {
|
|
813
|
+
actions: [
|
|
814
|
+
{
|
|
815
|
+
// After the resize toggle (`a.`, from the always-on module) and before
|
|
816
|
+
// the operator picker — the row reads resize, add a branch, say which
|
|
817
|
+
// kind of branching this is.
|
|
818
|
+
id: 'b.add-operand',
|
|
819
|
+
tooltipWording: BOARD_ADD_BAND,
|
|
820
|
+
icon: AddOperandIcon,
|
|
821
|
+
run: addUmlOperand,
|
|
822
|
+
},
|
|
823
|
+
operatorPickerAction,
|
|
824
|
+
// The generic dropdown, not a UML variant of it — the very same object
|
|
825
|
+
// every framework frame registers.
|
|
826
|
+
...validationToolbarConfig.actions,
|
|
827
|
+
],
|
|
828
|
+
when: (ctx) => ctx.getSurfaceModelsByType(UmlFragmentElementModel).length > 0,
|
|
829
|
+
};
|
|
830
|
+
export const umlFragmentToolingToolbarExtension = ToolbarModuleExtension({
|
|
831
|
+
id: BlockFlavourIdentifier('custom:affine:surface:umlFragment'),
|
|
832
|
+
config: umlFragmentToolingToolbarConfig,
|
|
833
|
+
});
|