@kubex/zinc 1.1.28 → 1.1.31
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/custom-elements-manifest.config.js +2 -2
- package/dist/custom-elements.json +1910 -362
- package/dist/vscode.html-custom-data.json +55 -14
- package/dist/web-types.json +185 -32
- package/dist/zn.d.ts +684 -16
- package/dist/zn.min.js +1325 -929
- package/docs/pages/components/flow-builder-troubleshooter-demo.njk +106 -78
- package/docs/pages/components/flow-builder.md +422 -66
- package/docs/pages/components/page-builder.md +167 -0
- package/docs/pages/components/settings-container.md +37 -2
- package/package.json +1 -1
- package/src/components/datepicker/datepicker.scss +0 -10
- package/src/components/flow-builder/flow-builder.component.ts +377 -16
- package/src/components/flow-builder/flow-builder.scss +398 -250
- package/src/components/flow-builder/flow-builder.test.ts +241 -4
- package/src/components/flow-builder/flow-layout.ts +42 -17
- package/src/components/flow-builder/flow.types.ts +168 -43
- package/src/components/flow-builder/modules/flow-branch-conditions/flow-branch-conditions.component.ts +300 -0
- package/src/components/flow-builder/modules/flow-branch-conditions/flow-branch-conditions.scss +222 -0
- package/src/components/flow-builder/modules/flow-branch-conditions/flow-branch-conditions.test.ts +125 -0
- package/src/components/flow-builder/modules/flow-branch-conditions/index.ts +12 -0
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.component.ts +184 -26
- package/src/components/flow-builder/modules/flow-canvas/flow-canvas.scss +170 -120
- package/src/components/flow-builder/modules/flow-node/flow-node.scss +42 -42
- package/src/components/flow-builder/modules/flow-step/flow-step.component.ts +2 -1
- package/src/components/flow-builder/modules/flow-step/flow-step.scss +25 -17
- package/src/components/header/header.scss +3 -1
- package/src/components/icon-picker/icon-picker.component.ts +20 -37
- package/src/components/icon-picker/icon-picker.scss +5 -45
- package/src/components/page-builder/index.ts +14 -0
- package/src/components/page-builder/modules/page-palette-item/index.ts +12 -0
- package/src/components/page-builder/modules/page-palette-item/page-palette-item.component.ts +71 -0
- package/src/components/page-builder/modules/page-palette-item/page-palette-item.scss +70 -0
- package/src/components/page-builder/modules/page-palette-item/page-palette-item.test.ts +20 -0
- package/src/components/page-builder/modules/page-section-card/index.ts +12 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.component.ts +93 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.scss +92 -0
- package/src/components/page-builder/modules/page-section-card/page-section-card.test.ts +50 -0
- package/src/components/page-builder/page-builder.component.ts +1053 -0
- package/src/components/page-builder/page-builder.scss +494 -0
- package/src/components/page-builder/page-builder.test.ts +464 -0
- package/src/components/page-builder/page-registry.ts +48 -0
- package/src/components/page-builder/page.types.ts +75 -0
- package/src/components/panel/panel.scss +1 -0
- package/src/components/settings-container/settings-container.component.ts +74 -9
- package/src/components/settings-container/settings-container.scss +75 -0
- package/src/components/settings-container/settings-container.test.ts +35 -0
- package/src/events/events.ts +2 -0
- package/src/events/zn-page-change.ts +9 -0
- package/src/events/zn-page-selection-change.ts +7 -0
- package/src/zinc.ts +4 -0
- package/web-test-runner.config.js +6 -1
|
@@ -15,7 +15,8 @@ The Flow Builder is a three-panel editor for visual automations, with an optiona
|
|
|
15
15
|
on a node to start a **stray branch** — the wire follows your cursor until you click a node (or its output
|
|
16
16
|
port) to attach it. Fan-in is allowed, and so are **loops** — a branch may point back to an earlier step (e.g.
|
|
17
17
|
an answer that restarts the questioning); only wiring a node directly to itself is refused. Cancel by clicking
|
|
18
|
-
empty canvas, pressing Esc, or leaving the window. A connected output port spawns a brand-new branch on click, so
|
|
18
|
+
empty canvas, pressing Esc, or leaving the window. A connected output port spawns a brand-new branch on click, so
|
|
19
|
+
extra arrows never require
|
|
19
20
|
config forms. Open outputs draw nothing while idle — their **`+`** targets appear only while dragging a step in
|
|
20
21
|
or moving one. Every existing wire has a **`+`** at its midpoint to insert a step between two nodes. New
|
|
21
22
|
steps are created by dragging them from the panel onto the canvas or a `+`. Reposition nodes by dragging, and add
|
|
@@ -25,8 +26,13 @@ The Flow Builder is a three-panel editor for visual automations, with an optiona
|
|
|
25
26
|
- **Right panel** — the sidebar by default: a slot (`slot="sidebar"`) for status / version history, with a built-in
|
|
26
27
|
Configuration Errors summary derived from `errorNodes`. Selecting a node slides that step's configuration in from
|
|
27
28
|
the right (driven by the node type's `renderConfig`); clicking a **branch pill** on a wire slides in the branch
|
|
28
|
-
editor instead — rename the branch and configure the filters/conditions for taking that path
|
|
29
|
-
|
|
29
|
+
editor instead — rename the branch and configure the filters/conditions for taking that path. Node types that
|
|
30
|
+
declare **`branchFilters`** (in markup or JS) get the built-in conditions editor — pick filters from a searchable
|
|
31
|
+
list and combine them into AND groups joined by OR, saved onto the output port's `data.conditions`. A custom
|
|
32
|
+
`renderBranchConfig` takes precedence when supplied.
|
|
33
|
+
|
|
34
|
+
Both side panels can be **tucked away** with the chevrons on the canvas edges, giving the canvas the full
|
|
35
|
+
width; selecting a node or branch brings the right panel back automatically.
|
|
30
36
|
|
|
31
37
|
A node's **`…`** menu offers Delete, Duplicate, and **Move** — Move detaches the step (with its subtree) so you can
|
|
32
38
|
re-attach it to a different `+` slot.
|
|
@@ -170,62 +176,145 @@ component generic and lets it be extended for any future workflow without changi
|
|
|
170
176
|
// edits node.outputs (add / rename / remove branches). The canvas re-renders the
|
|
171
177
|
// outputs and the builder prunes connections to any removed branch.
|
|
172
178
|
const splitType = {
|
|
173
|
-
type:
|
|
174
|
-
label:
|
|
175
|
-
group:
|
|
176
|
-
category:
|
|
177
|
-
icon:
|
|
178
|
-
color:
|
|
179
|
-
outputs:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
179
|
+
type: 'split',
|
|
180
|
+
label: 'Conditional Split',
|
|
181
|
+
group: 'rule',
|
|
182
|
+
category: 'Logic',
|
|
183
|
+
icon: 'split@lu',
|
|
184
|
+
color: 'rgb(105, 54, 245)',
|
|
185
|
+
outputs: [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}],
|
|
186
|
+
// Clicking one of this step's branch pills opens the built-in conditions
|
|
187
|
+
// editor, offering these filters (AND/OR groups saved on the port's data).
|
|
188
|
+
branchFilters: [
|
|
189
|
+
{
|
|
190
|
+
id: 'engagement',
|
|
191
|
+
label: 'Email engagement',
|
|
192
|
+
fields: [
|
|
193
|
+
{
|
|
194
|
+
id: 'count',
|
|
195
|
+
type: 'number',
|
|
196
|
+
suffix: 'time(s)',
|
|
197
|
+
value: 2,
|
|
198
|
+
operators: [
|
|
199
|
+
{value: 'at-least', label: 'at least'},
|
|
200
|
+
{value: 'at-most', label: 'at most'},
|
|
201
|
+
{value: 'exactly', label: 'exactly'}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'window',
|
|
206
|
+
type: 'number',
|
|
207
|
+
value: 6,
|
|
208
|
+
operators: [
|
|
209
|
+
{value: 'in-last', label: 'in the last'},
|
|
210
|
+
{value: 'before-last', label: 'before the last'}
|
|
211
|
+
],
|
|
212
|
+
units: [
|
|
213
|
+
{value: 'days', label: 'day(s)'},
|
|
214
|
+
{value: 'weeks', label: 'week(s)'},
|
|
215
|
+
{value: 'months', label: 'month(s)'}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 'lost-reason',
|
|
222
|
+
label: 'Lost reason',
|
|
223
|
+
fields: [
|
|
224
|
+
{
|
|
225
|
+
id: 'reason',
|
|
226
|
+
operators: [
|
|
227
|
+
{value: 'eq', label: 'Is equal to'},
|
|
228
|
+
{value: 'neq', label: 'Is not equal to'}
|
|
229
|
+
],
|
|
230
|
+
options: [
|
|
231
|
+
{value: 'price', label: 'Price'},
|
|
232
|
+
{value: 'competitor', label: 'Competitor'},
|
|
233
|
+
{value: 'no-need', label: 'No need'},
|
|
234
|
+
{value: 'other', label: 'Other'}
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'arr',
|
|
241
|
+
label: 'Annual recurring revenue',
|
|
242
|
+
fields: [
|
|
243
|
+
{
|
|
244
|
+
id: 'amount',
|
|
245
|
+
type: 'number',
|
|
246
|
+
suffix: 'USD',
|
|
247
|
+
operators: [
|
|
248
|
+
{value: 'gte', label: 'is at least'},
|
|
249
|
+
{value: 'lte', label: 'is at most'}
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
],
|
|
255
|
+
renderConfig: (node, update) =>
|
|
256
|
+
{
|
|
257
|
+
const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}];
|
|
258
|
+
const setBranches = (next) =>
|
|
259
|
+
{
|
|
260
|
+
node.outputs = next;
|
|
261
|
+
update({});
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const wrap = document.createElement('div');
|
|
265
|
+
wrap.style.cssText = 'display:flex;flex-direction:column;gap:8px';
|
|
266
|
+
|
|
267
|
+
// Branches are created on the canvas; the manager only shows once the
|
|
268
|
+
// node has real ones — connected, or named and waiting to be attached.
|
|
269
|
+
const el = document.getElementById('flow-demo');
|
|
270
|
+
const connected = new Set(
|
|
271
|
+
el.getState().connections.filter(c => c.source.node === node.id).map(c => c.source.port));
|
|
272
|
+
if(!branches.some(b => b.label || connected.has(b.id))) return wrap;
|
|
273
|
+
|
|
274
|
+
const heading = document.createElement('p');
|
|
275
|
+
heading.textContent = 'Branches';
|
|
276
|
+
heading.style.cssText = 'margin:0;font-weight:600;font-size:0.8125rem';
|
|
277
|
+
wrap.appendChild(heading);
|
|
278
|
+
|
|
279
|
+
branches.forEach((branch, i) =>
|
|
280
|
+
{
|
|
281
|
+
const row = document.createElement('div');
|
|
282
|
+
row.style.cssText = 'display:flex;gap:6px;align-items:center';
|
|
283
|
+
|
|
284
|
+
const input = document.createElement('zn-input');
|
|
285
|
+
input.setAttribute('size', 'small');
|
|
286
|
+
input.value = branch.label ?? branch.id;
|
|
287
|
+
input.style.flex = '1';
|
|
288
|
+
input.addEventListener('change', () =>
|
|
289
|
+
setBranches(branches.map((b, j) => (j === i ? {...b, label: input.value} : b))));
|
|
290
|
+
row.appendChild(input);
|
|
291
|
+
|
|
292
|
+
const remove = document.createElement('zn-button');
|
|
293
|
+
remove.setAttribute('icon', 'x@lu');
|
|
294
|
+
remove.setAttribute('icon-button', 'small');
|
|
295
|
+
remove.setAttribute('plain', '');
|
|
296
|
+
remove.addEventListener('click', () =>
|
|
297
|
+
{
|
|
298
|
+
const next = branches.filter((_, j) => j !== i);
|
|
299
|
+
// Deleting the last branch leaves a plain open output (a fresh id, so
|
|
300
|
+
// the removed branch's wire is pruned) — the node stays attachable.
|
|
301
|
+
setBranches(next.length ? next : [{id: 'branch-' + Date.now().toString(36)}]);
|
|
302
|
+
});
|
|
303
|
+
row.appendChild(remove);
|
|
304
|
+
|
|
305
|
+
wrap.appendChild(row);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const add = document.createElement('zn-button');
|
|
309
|
+
add.setAttribute('size', 'small');
|
|
310
|
+
add.setAttribute('icon', 'plus@lu');
|
|
311
|
+
add.textContent = 'Add branch';
|
|
312
|
+
add.addEventListener('click', () =>
|
|
313
|
+
setBranches([...branches, {id: 'branch-' + (branches.length + 1), label: 'New branch'}]));
|
|
314
|
+
wrap.appendChild(add);
|
|
315
|
+
|
|
316
|
+
return wrap;
|
|
317
|
+
}
|
|
229
318
|
};
|
|
230
319
|
|
|
231
320
|
customElements.whenDefined('zn-flow-builder').then(() =>
|
|
@@ -273,20 +362,205 @@ they're declarations that drive the rendered panel):
|
|
|
273
362
|
|
|
274
363
|
Set an optional per-tab hint with the `triggers-hint` / `actions-hint` / `rules-hint` attributes on the
|
|
275
364
|
builder. (A node's inspector `renderConfig` can't be expressed in markup — supply it via
|
|
276
|
-
`registerNodeTypes()` for the same `type` id if you need a custom config panel.
|
|
365
|
+
`registerNodeTypes()` for the same `type` id if you need a custom config panel. Branch conditions *can*
|
|
366
|
+
be expressed in markup — see the next section.)
|
|
277
367
|
|
|
278
368
|
```html
|
|
279
369
|
|
|
280
370
|
<zn-flow-builder triggers-hint="Choose a trigger and drag it to the canvas.">
|
|
281
371
|
<zn-flow-step type="webhook" group="trigger" category="Events"
|
|
282
|
-
|
|
283
|
-
|
|
372
|
+
icon="webhook@lu" color="rgb(236,68,91)" inputs=""
|
|
373
|
+
description="User hits webhook XYZ">Cancellation Requested
|
|
374
|
+
</zn-flow-step>
|
|
284
375
|
|
|
285
376
|
<zn-flow-step type="split" group="rule" category="Logic" icon="split@lu"
|
|
286
|
-
|
|
377
|
+
outputs='[{"id":"true","label":"TRUE"},{"id":"false","label":"FALSE"}]'>Conditional Split
|
|
378
|
+
</zn-flow-step>
|
|
287
379
|
</zn-flow-builder>
|
|
288
380
|
```
|
|
289
381
|
|
|
382
|
+
### Branch conditions in markup
|
|
383
|
+
|
|
384
|
+
Clicking a **branch pill** opens the branch editor. For step types that declare **branch filters**, it
|
|
385
|
+
renders the built-in conditions editor: a searchable filter picker, then the picked filters combined
|
|
386
|
+
into **AND groups joined by OR** — with Save / Cancel, so edits only apply (undoably) on Save.
|
|
387
|
+
|
|
388
|
+
Declare the filters when instantiating the builder by nesting `<zn-flow-filter>` elements inside a
|
|
389
|
+
`<zn-flow-step>` (use the `label` attribute for the step's name when nesting). Each filter holds one
|
|
390
|
+
`<zn-flow-filter-field>` per control row, and each field declares its choices as nested
|
|
391
|
+
`<zn-flow-operator>` / `<zn-flow-option>` elements — the element text is the label, with an optional
|
|
392
|
+
`value` attribute for the stored value.
|
|
393
|
+
|
|
394
|
+
Try it: click the **High intent** or **Everyone else** pill to open the conditions editor.
|
|
395
|
+
|
|
396
|
+
```html:preview
|
|
397
|
+
|
|
398
|
+
<div style="height: 640px;">
|
|
399
|
+
<zn-flow-builder id="branch-filter-demo" heading="Win-back Campaign" subheading="Branch filters demo"
|
|
400
|
+
rules-hint="Rules branch the flow based on conditions configured on each branch pill.">
|
|
401
|
+
|
|
402
|
+
<zn-flow-step type="cancel-requested" group="entrypoint" category="Events"
|
|
403
|
+
icon="webhook@lu" color="rgb(236, 68, 91)" inputs="">Cancellation Requested
|
|
404
|
+
</zn-flow-step>
|
|
405
|
+
|
|
406
|
+
<zn-flow-step type="split" group="rule" category="Logic"
|
|
407
|
+
icon="split@lu" color="rgb(105, 54, 245)" label="Conditional Split"
|
|
408
|
+
outputs='[{"id":"a","label":"Branch A"},{"id":"b","label":"Branch B"}]'>
|
|
409
|
+
|
|
410
|
+
<zn-flow-filter id="engagement" label="Email engagement"
|
|
411
|
+
description="How often the contact opened a campaign email">
|
|
412
|
+
<zn-flow-filter-field id="count" type="number" suffix="time(s)" value="2">
|
|
413
|
+
<zn-flow-operator value="gte">at least</zn-flow-operator>
|
|
414
|
+
<zn-flow-operator value="lte">at most</zn-flow-operator>
|
|
415
|
+
<zn-flow-operator value="eq">exactly</zn-flow-operator>
|
|
416
|
+
</zn-flow-filter-field>
|
|
417
|
+
<!-- Three adjustable controls: operator, amount, and time-frame unit. -->
|
|
418
|
+
<zn-flow-filter-field id="window" type="number" value="6">
|
|
419
|
+
<zn-flow-operator value="within">in the last</zn-flow-operator>
|
|
420
|
+
<zn-flow-operator value="before">before the last</zn-flow-operator>
|
|
421
|
+
<zn-flow-unit value="days">day(s)</zn-flow-unit>
|
|
422
|
+
<zn-flow-unit value="weeks">week(s)</zn-flow-unit>
|
|
423
|
+
<zn-flow-unit value="months">month(s)</zn-flow-unit>
|
|
424
|
+
</zn-flow-filter-field>
|
|
425
|
+
</zn-flow-filter>
|
|
426
|
+
|
|
427
|
+
<zn-flow-filter id="lost-reason" label="Lost reason"
|
|
428
|
+
description="Why the customer said they cancelled">
|
|
429
|
+
<zn-flow-filter-field id="reason">
|
|
430
|
+
<zn-flow-operator value="eq">Is equal to</zn-flow-operator>
|
|
431
|
+
<zn-flow-operator value="neq">Is not equal to</zn-flow-operator>
|
|
432
|
+
<zn-flow-option value="price">Price</zn-flow-option>
|
|
433
|
+
<zn-flow-option value="competitor">Competitor</zn-flow-option>
|
|
434
|
+
<zn-flow-option value="no-need">No need</zn-flow-option>
|
|
435
|
+
<zn-flow-option value="other">Other</zn-flow-option>
|
|
436
|
+
</zn-flow-filter-field>
|
|
437
|
+
</zn-flow-filter>
|
|
438
|
+
|
|
439
|
+
<zn-flow-filter id="plan" label="Subscription plan">
|
|
440
|
+
<zn-flow-filter-field id="name">
|
|
441
|
+
<zn-flow-operator value="is">is</zn-flow-operator>
|
|
442
|
+
<zn-flow-operator value="is-not">is not</zn-flow-operator>
|
|
443
|
+
<zn-flow-option value="starter">Starter</zn-flow-option>
|
|
444
|
+
<zn-flow-option value="growth">Growth</zn-flow-option>
|
|
445
|
+
<zn-flow-option value="enterprise">Enterprise</zn-flow-option>
|
|
446
|
+
</zn-flow-filter-field>
|
|
447
|
+
</zn-flow-filter>
|
|
448
|
+
|
|
449
|
+
<!-- Well-known operator keys don't need text — gte/lte get default labels. -->
|
|
450
|
+
<zn-flow-filter id="arr" label="Annual recurring revenue">
|
|
451
|
+
<zn-flow-filter-field id="amount" type="number" suffix="USD" placeholder="0">
|
|
452
|
+
<zn-flow-operator value="gte"></zn-flow-operator>
|
|
453
|
+
<zn-flow-operator value="lte"></zn-flow-operator>
|
|
454
|
+
</zn-flow-filter-field>
|
|
455
|
+
</zn-flow-filter>
|
|
456
|
+
|
|
457
|
+
<zn-flow-filter id="last-order" label="Last order"
|
|
458
|
+
description="Time since the contact last placed an order">
|
|
459
|
+
<zn-flow-filter-field id="days" type="number" suffix="day(s) ago" value="30">
|
|
460
|
+
<zn-flow-operator value="within">within the last</zn-flow-operator>
|
|
461
|
+
<zn-flow-operator value="over">more than</zn-flow-operator>
|
|
462
|
+
</zn-flow-filter-field>
|
|
463
|
+
</zn-flow-filter>
|
|
464
|
+
|
|
465
|
+
<zn-flow-filter id="tag" label="Contact tag">
|
|
466
|
+
<zn-flow-filter-field id="name" type="text" placeholder="e.g. vip">
|
|
467
|
+
<zn-flow-operator value="has">contains</zn-flow-operator>
|
|
468
|
+
<zn-flow-operator value="not-has">does not contain</zn-flow-operator>
|
|
469
|
+
</zn-flow-filter-field>
|
|
470
|
+
</zn-flow-filter>
|
|
471
|
+
</zn-flow-step>
|
|
472
|
+
|
|
473
|
+
<zn-flow-step type="send-offer" group="action" category="Email"
|
|
474
|
+
icon="mail@lu" color="rgb(43, 192, 145)">Send Win-back Offer
|
|
475
|
+
</zn-flow-step>
|
|
476
|
+
<zn-flow-step type="notify-csm" group="action" category="Support"
|
|
477
|
+
icon="user@lu" color="rgb(43, 192, 145)">Notify Account Manager
|
|
478
|
+
</zn-flow-step>
|
|
479
|
+
</zn-flow-builder>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<script type="module">
|
|
483
|
+
customElements.whenDefined('zn-flow-builder').then(() =>
|
|
484
|
+
{
|
|
485
|
+
const el = document.getElementById('branch-filter-demo');
|
|
486
|
+
el.setState({
|
|
487
|
+
nodes: [
|
|
488
|
+
{id: 'n1', type: 'cancel-requested', x: 300, y: 40, data: {}},
|
|
489
|
+
{
|
|
490
|
+
id: 'n2', type: 'split', x: 300, y: 280, data: {subtitle: 'Qualify the save attempt'},
|
|
491
|
+
// The branch pills carry their saved conditions on the port's data.
|
|
492
|
+
outputs: [
|
|
493
|
+
{
|
|
494
|
+
id: 'a', label: 'High intent',
|
|
495
|
+
data: {
|
|
496
|
+
conditions: [
|
|
497
|
+
[
|
|
498
|
+
{filter: 'engagement', values: {count: {operator: 'gte', value: 2}, window: {operator: 'within', value: 6, unit: 'months'}}},
|
|
499
|
+
{filter: 'lost-reason', values: {reason: {operator: 'eq', value: 'price'}}}
|
|
500
|
+
],
|
|
501
|
+
[
|
|
502
|
+
{filter: 'arr', values: {amount: {operator: 'gte', value: 5000}}}
|
|
503
|
+
]
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{id: 'b', label: 'Everyone else'}
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
{id: 'n3', type: 'send-offer', x: 100, y: 580, data: {subtitle: '20% off for 3 months'}},
|
|
511
|
+
{id: 'n4', type: 'notify-csm', x: 500, y: 580, data: {subtitle: 'Manual follow-up'}}
|
|
512
|
+
],
|
|
513
|
+
connections: [
|
|
514
|
+
{id: 'c1', source: {node: 'n1', port: 'out'}, target: {node: 'n2', port: 'in'}},
|
|
515
|
+
{id: 'c2', source: {node: 'n2', port: 'a'}, target: {node: 'n3', port: 'in'}},
|
|
516
|
+
{id: 'c3', source: {node: 'n2', port: 'b'}, target: {node: 'n4', port: 'in'}}
|
|
517
|
+
],
|
|
518
|
+
notes: []
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
</script>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
`<zn-flow-filter>` attributes: `id` (required), `label`, `description` (shown under the label in the
|
|
525
|
+
picker). `<zn-flow-filter-field>` attributes:
|
|
526
|
+
|
|
527
|
+
- `id` (required) — key the field's operator/value are stored under.
|
|
528
|
+
- `type` — `select` | `number` | `text`. Defaults to `select` when options are given, else `text`.
|
|
529
|
+
- `suffix` — trailing unit text (e.g. `time(s)`) when the unit isn't adjustable.
|
|
530
|
+
- `label`, `placeholder`, `value` (the initial value when the filter is added).
|
|
531
|
+
|
|
532
|
+
Inside a field, `<zn-flow-operator>` elements declare the operator dropdown (shown before the value —
|
|
533
|
+
"at least", "is equal to"), `<zn-flow-option>` elements the value choices of a `select` field, and
|
|
534
|
+
`<zn-flow-unit>` elements an **adjustable unit dropdown** after the value (day(s) / week(s) / month(s)) —
|
|
535
|
+
so a row can have three adjustable controls: operator, amount, and time frame. The
|
|
536
|
+
element text is the display label; add `value="…"` to store a stable key instead of the text. Empty
|
|
537
|
+
operators with a **well-known key** get a default label — `<zn-flow-operator value="gte"></zn-flow-operator>`
|
|
538
|
+
renders as "Greater Than or Equal To". Recognised keys: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `is`,
|
|
539
|
+
`is-not`, `in`, `not-in`, `contains`, `not-contains`, `starts-with`, `ends-with`, `empty`, `not-empty`,
|
|
540
|
+
`within`, `before`, `between`, `matches`.
|
|
541
|
+
|
|
542
|
+
For programmatic generation the same lists also fit in `operators` / `options` **attributes**
|
|
543
|
+
(comma-separated values, or a JSON array of strings and/or `{"value","label"}` objects), a whole
|
|
544
|
+
catalog fits in a **`branch-filters` JSON attribute** on the step, and JS registration takes a
|
|
545
|
+
`branchFilters` array on the `FlowNodeType` — as in the full demo at the top of this page. A type
|
|
546
|
+
supplying its own `renderBranchConfig` overrides the built-in editor.
|
|
547
|
+
|
|
548
|
+
Saved conditions live on the output port as `port.data.conditions`, an array of OR-ed groups, each an
|
|
549
|
+
array of AND-ed conditions:
|
|
550
|
+
|
|
551
|
+
```js
|
|
552
|
+
[
|
|
553
|
+
[ // group 1 (AND-ed)…
|
|
554
|
+
{filter: 'engagement',
|
|
555
|
+
values: {count: {operator: 'gte', value: 2}, window: {operator: 'within', value: 6, unit: 'months'}}
|
|
556
|
+
},
|
|
557
|
+
{filter: 'lost-reason', values: {reason: {operator: 'eq', value: 'price'}}}
|
|
558
|
+
],
|
|
559
|
+
// …OR group 2
|
|
560
|
+
[{filter: 'arr', values: {amount: {operator: 'gte', value: 5000}}}]
|
|
561
|
+
]
|
|
562
|
+
```
|
|
563
|
+
|
|
290
564
|
### Registering custom node types
|
|
291
565
|
|
|
292
566
|
Node types are the modular extension point. Instead of (or alongside) slotted steps, register an
|
|
@@ -338,9 +612,11 @@ the builder prunes any connection whose branch is removed. `node.inputs` works t
|
|
|
338
612
|
ports then call `update({})` to commit:
|
|
339
613
|
|
|
340
614
|
```js
|
|
341
|
-
renderConfig: (node, update) =>
|
|
615
|
+
renderConfig: (node, update) =>
|
|
616
|
+
{
|
|
342
617
|
const branches = node.outputs ?? [{id: 'true', label: 'TRUE'}, {id: 'false', label: 'FALSE'}];
|
|
343
|
-
const addBranch = () =>
|
|
618
|
+
const addBranch = () =>
|
|
619
|
+
{
|
|
344
620
|
node.outputs = [...branches, {id: crypto.randomUUID(), label: 'New branch'}];
|
|
345
621
|
update({}); // commit — re-renders the outputs, prunes dropped branches' connections
|
|
346
622
|
};
|
|
@@ -371,7 +647,87 @@ builder.addEventListener('zn-flow-connect', e =>
|
|
|
371
647
|
console.log('new connection', e.detail.connection);
|
|
372
648
|
});
|
|
373
649
|
|
|
374
|
-
// Persist / restore:
|
|
650
|
+
// Persist / restore — `value` is the full FlowState, node positions included:
|
|
375
651
|
localStorage.setItem('flow', builder.value);
|
|
376
652
|
builder.value = localStorage.getItem('flow');
|
|
653
|
+
|
|
654
|
+
// Saving to a server: the builder serialises directly (toJSON returns the
|
|
655
|
+
// FlowState), so the POST body is one expression either way:
|
|
656
|
+
await fetch('/api/flows/123', {
|
|
657
|
+
method: 'POST',
|
|
658
|
+
headers: {'Content-Type': 'application/json'},
|
|
659
|
+
body: JSON.stringify(builder), // === builder.value
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Auto-save
|
|
664
|
+
|
|
665
|
+
Add the `auto-save` attribute to periodically snapshot the flow (positions included) into
|
|
666
|
+
localStorage, keyed by the builder's `id` (falling back to its `heading`), with a **1-day TTL**:
|
|
667
|
+
|
|
668
|
+
```html
|
|
669
|
+
|
|
670
|
+
<zn-flow-builder id="my-flow" auto-save></zn-flow-builder> <!-- every 5 minutes -->
|
|
671
|
+
<zn-flow-builder id="my-flow" auto-save="2"></zn-flow-builder> <!-- every 2 minutes -->
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Without the attribute nothing is saved. An empty canvas is never written (so a fresh session can't
|
|
675
|
+
clobber a stored flow), and expired snapshots are purged when encountered.
|
|
676
|
+
|
|
677
|
+
While auto-save is on, a **status pill** in the canvas's bottom-left flashes "Auto-saved" as each
|
|
678
|
+
snapshot lands and otherwise shows how long ago the last one happened ("Last saved 3m ago"). And when
|
|
679
|
+
a flow is loaded (`setState` / `value`) that **differs from a fresh auto-saved draft**, a banner offers
|
|
680
|
+
to restore the draft — editing the loaded flow or dismissing the banner keeps what was loaded. Restoring
|
|
681
|
+
can also be done programmatically:
|
|
682
|
+
|
|
683
|
+
```js
|
|
684
|
+
const builder = document.querySelector('zn-flow-builder');
|
|
685
|
+
if(builder.restoreAutoSave())
|
|
686
|
+
{
|
|
687
|
+
console.log('Restored an auto-saved draft'); // false when none exists / it expired
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
Try it below — the demo saves every **6 seconds** (`auto-save="0.1"`; real flows would use minutes).
|
|
692
|
+
Drag a node and watch the pill flash **Auto-saved**. Then **reload the page**: the demo always loads
|
|
693
|
+
its original flow, so if your draft differs a banner offers to restore it.
|
|
694
|
+
|
|
695
|
+
```html:preview
|
|
696
|
+
|
|
697
|
+
<div style="height: 480px;">
|
|
698
|
+
<zn-flow-builder id="autosave-demo" auto-save="0.1"
|
|
699
|
+
heading="Auto-save" subheading="Saves every 6s in this demo">
|
|
700
|
+
|
|
701
|
+
<zn-flow-step type="webhook" group="entrypoint" category="Events"
|
|
702
|
+
icon="webhook@lu" color="rgb(236, 68, 91)" inputs="">Order Placed
|
|
703
|
+
</zn-flow-step>
|
|
704
|
+
<zn-flow-step type="send-email" group="action" category="Email"
|
|
705
|
+
icon="mail@lu" color="rgb(43, 192, 145)">Send Confirmation
|
|
706
|
+
</zn-flow-step>
|
|
707
|
+
<zn-flow-step type="notify" group="action" category="Support"
|
|
708
|
+
icon="user@lu" color="rgb(43, 192, 145)">Notify Team
|
|
709
|
+
</zn-flow-step>
|
|
710
|
+
</zn-flow-builder>
|
|
711
|
+
</div>
|
|
712
|
+
|
|
713
|
+
<script type="module">
|
|
714
|
+
customElements.whenDefined('zn-flow-builder').then(() =>
|
|
715
|
+
{
|
|
716
|
+
const el = document.getElementById('autosave-demo');
|
|
717
|
+
// Always load the same baseline — after editing and reloading, the
|
|
718
|
+
// auto-saved draft differs from it and the restore banner appears.
|
|
719
|
+
el.setState({
|
|
720
|
+
nodes: [
|
|
721
|
+
{id: 'n1', type: 'webhook', x: 300, y: 40, data: {subtitle: 'Webhook fires'}},
|
|
722
|
+
{id: 'n2', type: 'send-email', x: 300, y: 260, data: {subtitle: 'Order confirmation'}},
|
|
723
|
+
{id: 'n3', type: 'notify', x: 300, y: 480, data: {subtitle: 'Ping #orders'}}
|
|
724
|
+
],
|
|
725
|
+
connections: [
|
|
726
|
+
{id: 'c1', source: {node: 'n1', port: 'out'}, target: {node: 'n2', port: 'in'}},
|
|
727
|
+
{id: 'c2', source: {node: 'n2', port: 'out'}, target: {node: 'n3', port: 'in'}}
|
|
728
|
+
],
|
|
729
|
+
notes: []
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
</script>
|
|
377
733
|
```
|