@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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit';
|
|
2
|
+
import {property, state} from 'lit/decorators.js';
|
|
3
|
+
import ZincElement from '../../../../internal/zinc-element';
|
|
4
|
+
import ZnButton from '../../../button';
|
|
5
|
+
import ZnIcon from '../../../icon';
|
|
6
|
+
import ZnInput from '../../../input';
|
|
7
|
+
import ZnOption from '../../../option';
|
|
8
|
+
import ZnSelect from '../../../select';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type FlowBranchCondition,
|
|
12
|
+
type FlowBranchConditions,
|
|
13
|
+
type FlowBranchFilter,
|
|
14
|
+
type FlowFilterField,
|
|
15
|
+
operatorLabel,
|
|
16
|
+
} from '../../flow.types';
|
|
17
|
+
|
|
18
|
+
import styles from './flow-branch-conditions.scss';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @summary The built-in branch conditions editor: pick filters from a searchable list, then
|
|
22
|
+
* combine them into AND groups joined by OR. Rendered by `<zn-flow-builder>`'s branch editor
|
|
23
|
+
* for node types that declare `branchFilters`; edits are drafted locally and only applied
|
|
24
|
+
* on Save.
|
|
25
|
+
* @documentation https://zinc.style/components/flow-branch-conditions
|
|
26
|
+
* @status experimental
|
|
27
|
+
* @since 1.0
|
|
28
|
+
*
|
|
29
|
+
* @dependency zn-button
|
|
30
|
+
* @dependency zn-icon
|
|
31
|
+
* @dependency zn-input
|
|
32
|
+
* @dependency zn-option
|
|
33
|
+
* @dependency zn-select
|
|
34
|
+
*
|
|
35
|
+
* @event flow-conditions-save - Emitted on Save with `detail.conditions` (OR groups of AND-ed conditions).
|
|
36
|
+
* @event flow-conditions-cancel - Emitted on Cancel, with the draft discarded.
|
|
37
|
+
*
|
|
38
|
+
* @csspart base - The editor wrapper.
|
|
39
|
+
* @csspart picker - The filter picker (search + list).
|
|
40
|
+
* @csspart conditions - The configured condition groups.
|
|
41
|
+
*/
|
|
42
|
+
export default class ZnFlowBranchConditions extends ZincElement {
|
|
43
|
+
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
44
|
+
|
|
45
|
+
static dependencies = {
|
|
46
|
+
'zn-button': ZnButton,
|
|
47
|
+
'zn-icon': ZnIcon,
|
|
48
|
+
'zn-input': ZnInput,
|
|
49
|
+
'zn-option': ZnOption,
|
|
50
|
+
'zn-select': ZnSelect,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The filters available to build conditions from. */
|
|
54
|
+
@property({attribute: false}) filters: FlowBranchFilter[] = [];
|
|
55
|
+
|
|
56
|
+
/** The saved conditions being edited; changes stay in a local draft until Save. */
|
|
57
|
+
@property({attribute: false}) value: FlowBranchConditions = [];
|
|
58
|
+
|
|
59
|
+
@state() private _draft: FlowBranchConditions = [];
|
|
60
|
+
/** Group index the picker adds into (`_draft.length` starts a new OR group), or null when closed. */
|
|
61
|
+
@state() private _picker: number | null = null;
|
|
62
|
+
@state() private _search = '';
|
|
63
|
+
|
|
64
|
+
/** JSON of the last `value` the draft was built from, so re-renders that pass an
|
|
65
|
+
* equivalent value (fresh array refs) don't wipe in-progress edits. */
|
|
66
|
+
private _valueJson = '';
|
|
67
|
+
|
|
68
|
+
protected willUpdate(changed: PropertyValues) {
|
|
69
|
+
super.willUpdate(changed);
|
|
70
|
+
if (changed.has('value')) {
|
|
71
|
+
const json = JSON.stringify(this.value ?? []);
|
|
72
|
+
if (json !== this._valueJson) {
|
|
73
|
+
this._valueJson = json;
|
|
74
|
+
this._draft = JSON.parse(json) as FlowBranchConditions;
|
|
75
|
+
this._picker = null;
|
|
76
|
+
this._search = '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private _emit(name: string, detail?: Record<string, unknown>) {
|
|
82
|
+
this.dispatchEvent(new CustomEvent(name, {bubbles: true, composed: true, detail}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private _save = () => {
|
|
86
|
+
this._emit('flow-conditions-save', {conditions: JSON.parse(JSON.stringify(this._draft)) as FlowBranchConditions});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
private _cancel = () => {
|
|
90
|
+
this._emit('flow-conditions-cancel');
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// --- Draft edits ------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
private _cloneDraft(): FlowBranchConditions {
|
|
96
|
+
return this._draft.map(group => group.map(c => ({...c, values: {...c.values}})));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _addCondition(filter: FlowBranchFilter) {
|
|
100
|
+
const values: FlowBranchCondition['values'] = {};
|
|
101
|
+
filter.fields.forEach(f => {
|
|
102
|
+
values[f.id] = {
|
|
103
|
+
...(f.operators?.length ? {operator: f.operators[0].value} : {}),
|
|
104
|
+
...(f.value !== undefined ? {value: f.value} : {}),
|
|
105
|
+
...(f.units?.length ? {unit: f.units[0].value} : {}),
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
const draft = this._cloneDraft();
|
|
109
|
+
const group = Math.min(this._picker ?? 0, draft.length);
|
|
110
|
+
if (group === draft.length) draft.push([]);
|
|
111
|
+
draft[group].push({filter: filter.id, values});
|
|
112
|
+
this._draft = draft;
|
|
113
|
+
this._picker = null;
|
|
114
|
+
this._search = '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _removeCondition(group: number, index: number) {
|
|
118
|
+
const draft = this._cloneDraft();
|
|
119
|
+
draft[group].splice(index, 1);
|
|
120
|
+
this._draft = draft.filter(g => g.length);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private _setField(group: number, index: number, fieldId: string, patch: { operator?: string; value?: string | number; unit?: string }) {
|
|
124
|
+
const draft = this._cloneDraft();
|
|
125
|
+
const condition = draft[group][index];
|
|
126
|
+
condition.values[fieldId] = {...condition.values[fieldId], ...patch};
|
|
127
|
+
this._draft = draft;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Rendering --------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
private _renderPicker() {
|
|
133
|
+
const term = this._search.trim().toLowerCase();
|
|
134
|
+
const matches = this.filters.filter(f => !term || f.label.toLowerCase().includes(term));
|
|
135
|
+
|
|
136
|
+
return html`
|
|
137
|
+
<div part="picker" class="picker">
|
|
138
|
+
<div class="picker__head">
|
|
139
|
+
<span>Add Filters</span>
|
|
140
|
+
${this._draft.length ? html`
|
|
141
|
+
<zn-button
|
|
142
|
+
class="picker__back"
|
|
143
|
+
icon="x@lu"
|
|
144
|
+
icon-button="small"
|
|
145
|
+
plain
|
|
146
|
+
title="Back to conditions"
|
|
147
|
+
@click="${() => (this._picker = null)}"
|
|
148
|
+
></zn-button>` : ''}
|
|
149
|
+
</div>
|
|
150
|
+
<zn-input
|
|
151
|
+
class="picker__search"
|
|
152
|
+
placeholder="Search filters..."
|
|
153
|
+
clearable
|
|
154
|
+
.value="${this._search}"
|
|
155
|
+
@zn-input="${(e: Event) => (this._search = String((e.target as ZnInput).value ?? ''))}"
|
|
156
|
+
@input="${(e: Event) => (this._search = String((e.target as ZnInput).value ?? ''))}"
|
|
157
|
+
>
|
|
158
|
+
<zn-icon slot="suffix" src="search@lu" size="16"></zn-icon>
|
|
159
|
+
</zn-input>
|
|
160
|
+
<div class="picker__list">
|
|
161
|
+
${matches.length === 0
|
|
162
|
+
? html`<p class="picker__empty">No matching filters.</p>`
|
|
163
|
+
: matches.map(f => html`
|
|
164
|
+
<button class="picker__item" @click="${() => this._addCondition(f)}">
|
|
165
|
+
<span class="picker__item-text">
|
|
166
|
+
<span class="picker__item-label">${f.label}</span>
|
|
167
|
+
${f.description ? html`<span class="picker__item-description">${f.description}</span>` : ''}
|
|
168
|
+
</span>
|
|
169
|
+
<zn-icon src="arrow-right@lu" size="16"></zn-icon>
|
|
170
|
+
</button>`)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A select's minimum width, sized so its longest label never truncates
|
|
178
|
+
* (estimated per char, plus padding and the chevron). When the row can't
|
|
179
|
+
* fit it, flex-wrap gives the select its own full-width line instead.
|
|
180
|
+
*/
|
|
181
|
+
private static _selectMinWidth(labels: string[]): number {
|
|
182
|
+
const longest = Math.max(0, ...labels.map(l => l.length));
|
|
183
|
+
return Math.min(260, Math.round(52 + longest * 7.2));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private _renderField(condition: FlowBranchCondition, group: number, index: number, field: FlowFilterField) {
|
|
187
|
+
const entry = condition.values[field.id] ?? {};
|
|
188
|
+
const type = field.type ?? (field.options?.length ? 'select' : 'text');
|
|
189
|
+
const set = (patch: { operator?: string; value?: string | number; unit?: string }) =>
|
|
190
|
+
this._setField(group, index, field.id, patch);
|
|
191
|
+
|
|
192
|
+
return html`
|
|
193
|
+
<div class="condition__row">
|
|
194
|
+
${field.label ? html`<span class="condition__row-label">${field.label}</span>` : ''}
|
|
195
|
+
${field.operators?.length ? html`
|
|
196
|
+
<zn-select
|
|
197
|
+
class="control control--operator"
|
|
198
|
+
size="small"
|
|
199
|
+
style="min-width:${ZnFlowBranchConditions._selectMinWidth(field.operators.map(o => operatorLabel(o)))}px"
|
|
200
|
+
.value="${entry.operator ?? field.operators[0].value}"
|
|
201
|
+
@zn-change="${(e: Event) => set({operator: String((e.target as ZnSelect).value)})}"
|
|
202
|
+
>
|
|
203
|
+
${field.operators.map(o => html`
|
|
204
|
+
<zn-option value="${o.value}">${operatorLabel(o)}</zn-option>`)}
|
|
205
|
+
</zn-select>` : ''}
|
|
206
|
+
${type === 'select' ? html`
|
|
207
|
+
<zn-select
|
|
208
|
+
class="control control--value"
|
|
209
|
+
size="small"
|
|
210
|
+
style="min-width:${ZnFlowBranchConditions._selectMinWidth((field.options ?? []).map(o => o.label ?? o.value))}px"
|
|
211
|
+
placeholder="${field.placeholder ?? ''}"
|
|
212
|
+
.value="${String(entry.value ?? '')}"
|
|
213
|
+
@zn-change="${(e: Event) => set({value: String((e.target as ZnSelect).value)})}"
|
|
214
|
+
>
|
|
215
|
+
${(field.options ?? []).map(o => html`
|
|
216
|
+
<zn-option value="${o.value}">${o.label ?? o.value}</zn-option>`)}
|
|
217
|
+
</zn-select>` : html`
|
|
218
|
+
<zn-input
|
|
219
|
+
class="control ${type === 'number' ? 'control--number' : 'control--value'}"
|
|
220
|
+
size="small"
|
|
221
|
+
type="${type}"
|
|
222
|
+
.value="${String(entry.value ?? '')}"
|
|
223
|
+
placeholder="${field.placeholder ?? ''}"
|
|
224
|
+
@zn-input="${(e: Event) => {
|
|
225
|
+
const raw = String((e.target as ZnInput).value ?? '');
|
|
226
|
+
set({value: type === 'number' ? Number(raw) : raw});
|
|
227
|
+
}}"
|
|
228
|
+
@input="${(e: Event) => {
|
|
229
|
+
const raw = String((e.target as ZnInput).value ?? '');
|
|
230
|
+
set({value: type === 'number' ? Number(raw) : raw});
|
|
231
|
+
}}"
|
|
232
|
+
></zn-input>`}
|
|
233
|
+
${field.units?.length ? html`
|
|
234
|
+
<zn-select
|
|
235
|
+
class="control control--unit"
|
|
236
|
+
size="small"
|
|
237
|
+
style="min-width:${ZnFlowBranchConditions._selectMinWidth(field.units.map(u => u.label ?? u.value))}px"
|
|
238
|
+
.value="${entry.unit ?? field.units[0].value}"
|
|
239
|
+
@zn-change="${(e: Event) => set({unit: String((e.target as ZnSelect).value)})}"
|
|
240
|
+
>
|
|
241
|
+
${field.units.map(u => html`
|
|
242
|
+
<zn-option value="${u.value}">${u.label ?? u.value}</zn-option>`)}
|
|
243
|
+
</zn-select>`
|
|
244
|
+
: field.suffix ? html`<span class="condition__suffix">${field.suffix}</span>` : ''}
|
|
245
|
+
</div>
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private _renderCondition(condition: FlowBranchCondition, group: number, index: number) {
|
|
250
|
+
const filter = this.filters.find(f => f.id === condition.filter);
|
|
251
|
+
return html`
|
|
252
|
+
<div class="condition">
|
|
253
|
+
<div class="condition__head">
|
|
254
|
+
<span class="condition__name">${filter?.label ?? condition.filter}</span>
|
|
255
|
+
<zn-button class="condition__remove" title="Remove filter" icon="x@lu" icon-button="small" plain
|
|
256
|
+
@click="${() => this._removeCondition(group, index)}">
|
|
257
|
+
</zn-button>
|
|
258
|
+
</div>
|
|
259
|
+
${(filter?.fields ?? []).map(f => this._renderField(condition, group, index, f))}
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private _renderConditions() {
|
|
265
|
+
return html`
|
|
266
|
+
<div part="conditions" class="conditions">
|
|
267
|
+
${this._draft.map((group, gi) => html`
|
|
268
|
+
${gi > 0 ? html`
|
|
269
|
+
<div class="or-label">OR</div>` : ''}
|
|
270
|
+
<div class="group">
|
|
271
|
+
${group.map((condition, ci) => html`
|
|
272
|
+
${ci > 0 ? html`
|
|
273
|
+
<div class="and-rule"><span>AND</span></div>` : ''}
|
|
274
|
+
${this._renderCondition(condition, gi, ci)}
|
|
275
|
+
`)}
|
|
276
|
+
<zn-button icon="plus@lu" @click="${() => (this._picker = gi)}" panel-bg>AND
|
|
277
|
+
</zn-button>
|
|
278
|
+
</div>
|
|
279
|
+
`)}
|
|
280
|
+
<zn-button icon="plus@lu" @click="${() => (this._picker = this._draft.length)}" panel-bg>
|
|
281
|
+
OR
|
|
282
|
+
</zn-button>
|
|
283
|
+
</div>
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
render() {
|
|
288
|
+
// With nothing configured yet, the picker is the first step (as designed).
|
|
289
|
+
const picking = this._picker !== null || this._draft.length === 0;
|
|
290
|
+
return html`
|
|
291
|
+
<div part="base" class="editor">
|
|
292
|
+
${picking ? this._renderPicker() : this._renderConditions()}
|
|
293
|
+
<div class="actions">
|
|
294
|
+
<zn-button class="actions__save" icon="check@lu" @click="${this._save}">Save</zn-button>
|
|
295
|
+
<zn-button class="actions__cancel" icon="x@lu" @click="${this._cancel}" panel-bg>Cancel</zn-button>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
}
|
package/src/components/flow-builder/modules/flow-branch-conditions/flow-branch-conditions.scss
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
@use "../../../../wc";
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
font-size: 0.8125rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.editor {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: 14px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- Filter picker (step 1) --------------------------------------------------
|
|
15
|
+
|
|
16
|
+
.picker {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: 10px;
|
|
20
|
+
padding: 12px;
|
|
21
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
background: rgb(var(--zn-panel));
|
|
24
|
+
|
|
25
|
+
&__head {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: space-between;
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&__back {
|
|
33
|
+
flex: 0 0 auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
&__list {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__item {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: space-between;
|
|
45
|
+
gap: 10px;
|
|
46
|
+
width: 100%;
|
|
47
|
+
padding: 10px 12px;
|
|
48
|
+
margin-top: -1px;
|
|
49
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
50
|
+
background: rgb(var(--zn-panel));
|
|
51
|
+
color: rgb(var(--zn-text));
|
|
52
|
+
font-size: 0.8125rem;
|
|
53
|
+
text-align: left;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
|
|
56
|
+
&:first-child {
|
|
57
|
+
margin-top: 0;
|
|
58
|
+
border-top-left-radius: 8px;
|
|
59
|
+
border-top-right-radius: 8px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
&:last-child {
|
|
63
|
+
border-bottom-left-radius: 8px;
|
|
64
|
+
border-bottom-right-radius: 8px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&:hover {
|
|
68
|
+
background: rgb(var(--zn-color-tab));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__item-text {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: 2px;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&__item-label {
|
|
80
|
+
font-weight: 500;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&__item-description {
|
|
84
|
+
font-size: 0.75rem;
|
|
85
|
+
color: rgb(var(--zn-color-muted-text));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&__empty {
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 10px 2px;
|
|
91
|
+
font-size: 0.75rem;
|
|
92
|
+
color: rgb(var(--zn-color-muted-text));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- Condition groups (step 2) -----------------------------------------------
|
|
97
|
+
|
|
98
|
+
.conditions {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
align-items: flex-start;
|
|
102
|
+
gap: 12px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.group {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
gap: 12px;
|
|
110
|
+
width: 100%;
|
|
111
|
+
padding: 14px;
|
|
112
|
+
border: 1px solid rgb(var(--zn-border-color));
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
background: rgb(var(--zn-panel));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The AND joining two conditions in a group: a label on a full-width rule.
|
|
118
|
+
.and-rule {
|
|
119
|
+
@include wc.text-style(paragraph);
|
|
120
|
+
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 10px;
|
|
124
|
+
width: 100%;
|
|
125
|
+
color: rgb(var(--zn-color-success));
|
|
126
|
+
|
|
127
|
+
&::after {
|
|
128
|
+
content: "";
|
|
129
|
+
flex: 1;
|
|
130
|
+
height: 1px;
|
|
131
|
+
background: color-mix(in srgb, rgb(var(--zn-color-success)) 50%, transparent);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// The OR joining two groups.
|
|
136
|
+
.or-label {
|
|
137
|
+
@include wc.text-style(paragraph);
|
|
138
|
+
|
|
139
|
+
margin: 2px 0;
|
|
140
|
+
color: rgb(var(--zn-color-error));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.condition {
|
|
144
|
+
display: flex;
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
gap: 10px;
|
|
147
|
+
width: 100%;
|
|
148
|
+
|
|
149
|
+
&__head {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: space-between;
|
|
153
|
+
gap: 10px;
|
|
154
|
+
width: 100%;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&__name {
|
|
158
|
+
@include wc.text-style(heading-section);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
&__remove {
|
|
162
|
+
flex: 0 0 auto;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&__row {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
flex-wrap: wrap;
|
|
169
|
+
gap: 6px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&__row-label,
|
|
173
|
+
&__suffix {
|
|
174
|
+
font-size: 0.75rem;
|
|
175
|
+
color: rgb(var(--zn-color-muted-text));
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Zinc form controls sized into the row: a zero flex-basis overrides the
|
|
181
|
+
// controls' natural 100% width so a row shares one line. Each select also gets
|
|
182
|
+
// an inline min-width sized to its longest label, so a row that can't show a
|
|
183
|
+
// label in full wraps and gives the select its own line instead of truncating.
|
|
184
|
+
.control {
|
|
185
|
+
min-width: 0;
|
|
186
|
+
|
|
187
|
+
&--operator {
|
|
188
|
+
flex: 1 1 0;
|
|
189
|
+
min-width: 120px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
&--number {
|
|
193
|
+
flex: 0 0 auto;
|
|
194
|
+
width: 80px;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Adjustable unit dropdown after the value (e.g. day(s) / month(s)).
|
|
198
|
+
&--unit {
|
|
199
|
+
flex: 1 1 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The value control soaks up the remaining row width.
|
|
203
|
+
&--value {
|
|
204
|
+
flex: 1 1 0;
|
|
205
|
+
min-width: 100px;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Graceful fallback for labels past the min-width cap.
|
|
210
|
+
zn-select.control::part(display-input) {
|
|
211
|
+
text-overflow: ellipsis;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Save / Cancel sit in their own section, clear of the last group's "+ OR".
|
|
215
|
+
.actions {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px;
|
|
219
|
+
margin-top: 2px;
|
|
220
|
+
padding-top: 16px;
|
|
221
|
+
border-top: 1px solid rgb(var(--zn-border-color));
|
|
222
|
+
}
|
package/src/components/flow-builder/modules/flow-branch-conditions/flow-branch-conditions.test.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import '../../../../../dist/zn.min.js';
|
|
2
|
+
import {expect, fixture, html, oneEvent} from '@open-wc/testing';
|
|
3
|
+
import type {FlowBranchConditions, FlowBranchFilter} from '../../flow.types';
|
|
4
|
+
import type ZnFlowBranchConditions from './flow-branch-conditions.component';
|
|
5
|
+
import type ZnInput from '../../../input';
|
|
6
|
+
|
|
7
|
+
const FILTERS: FlowBranchFilter[] = [
|
|
8
|
+
{
|
|
9
|
+
id: 'engagement',
|
|
10
|
+
label: 'Email engagement',
|
|
11
|
+
fields: [
|
|
12
|
+
{
|
|
13
|
+
id: 'count',
|
|
14
|
+
type: 'number',
|
|
15
|
+
// 'gte' has no label — the editor falls back to its well-known default.
|
|
16
|
+
operators: [{value: 'gte'}, {value: 'at-most', label: 'at most'}],
|
|
17
|
+
units: [{value: 'days', label: 'day(s)'}, {value: 'months', label: 'month(s)'}],
|
|
18
|
+
value: 2,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'lost-reason',
|
|
24
|
+
label: 'Lost reason',
|
|
25
|
+
fields: [
|
|
26
|
+
{
|
|
27
|
+
id: 'reason',
|
|
28
|
+
type: 'select',
|
|
29
|
+
operators: [{value: 'eq', label: 'Is equal to'}, {value: 'neq', label: 'Is not equal to'}],
|
|
30
|
+
options: [{value: 'price', label: 'Price'}, {value: 'competitor', label: 'Competitor'}],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// zn-button overrides click() without dispatching an event, so raise a real one.
|
|
37
|
+
function clickButton(el: Element | null | undefined) {
|
|
38
|
+
el?.dispatchEvent(new MouseEvent('click', {bubbles: true, composed: true}));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function makeEditor(value: FlowBranchConditions = []): Promise<ZnFlowBranchConditions> {
|
|
42
|
+
const el = await fixture<ZnFlowBranchConditions>(html`
|
|
43
|
+
<zn-flow-branch-conditions .filters="${FILTERS}" .value="${value}"></zn-flow-branch-conditions>`);
|
|
44
|
+
await el.updateComplete;
|
|
45
|
+
return el;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('<zn-flow-branch-conditions>', () => {
|
|
49
|
+
it('should open on the filter picker when nothing is configured', async () => {
|
|
50
|
+
const el = await makeEditor();
|
|
51
|
+
const items = el.shadowRoot?.querySelectorAll('.picker__item');
|
|
52
|
+
expect(el.shadowRoot?.querySelector('.picker')).to.exist;
|
|
53
|
+
expect(items?.length).to.equal(2);
|
|
54
|
+
expect(items?.[0].textContent).to.contain('Email engagement');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should add a condition with field defaults when a filter is picked', async () => {
|
|
58
|
+
const el = await makeEditor();
|
|
59
|
+
(el.shadowRoot?.querySelectorAll('.picker__item')[0] as HTMLButtonElement).click();
|
|
60
|
+
await el.updateComplete;
|
|
61
|
+
|
|
62
|
+
const condition = el.shadowRoot?.querySelector('.condition');
|
|
63
|
+
expect(condition?.querySelector('.condition__name')?.textContent).to.contain('Email engagement');
|
|
64
|
+
expect(String(condition!.querySelector<ZnInput>('zn-input.control')!.value)).to.equal('2');
|
|
65
|
+
// A label-less well-known operator key renders its default label.
|
|
66
|
+
expect(condition!.querySelector('zn-select.control--operator zn-option')!.textContent)
|
|
67
|
+
.to.contain('Greater Than or Equal To');
|
|
68
|
+
// The adjustable unit dropdown defaults to the first unit.
|
|
69
|
+
const unit = condition!.querySelector('zn-select.control--unit') as Element & { value: string };
|
|
70
|
+
expect(String(unit.value)).to.equal('days');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should render saved conditions with OR groups and AND rules', async () => {
|
|
74
|
+
const el = await makeEditor([
|
|
75
|
+
[
|
|
76
|
+
{filter: 'engagement', values: {count: {operator: 'gte', value: 2}}},
|
|
77
|
+
{filter: 'lost-reason', values: {reason: {operator: 'eq', value: 'price'}}},
|
|
78
|
+
],
|
|
79
|
+
[{filter: 'engagement', values: {count: {operator: 'at-most', value: 5}}}],
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
expect(el.shadowRoot?.querySelectorAll('.group').length).to.equal(2);
|
|
83
|
+
expect(el.shadowRoot?.querySelectorAll('.condition').length).to.equal(3);
|
|
84
|
+
expect(el.shadowRoot?.querySelectorAll('.and-rule').length).to.equal(1);
|
|
85
|
+
expect(el.shadowRoot?.querySelectorAll('.or-label').length).to.equal(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should emit flow-conditions-save with the draft on Save', async () => {
|
|
89
|
+
const el = await makeEditor();
|
|
90
|
+
(el.shadowRoot?.querySelectorAll('.picker__item')[1] as HTMLButtonElement).click();
|
|
91
|
+
await el.updateComplete;
|
|
92
|
+
|
|
93
|
+
setTimeout(() => clickButton(el.shadowRoot?.querySelector('.actions__save')));
|
|
94
|
+
const event = await oneEvent(el, 'flow-conditions-save') as CustomEvent<{ conditions: FlowBranchConditions }>;
|
|
95
|
+
|
|
96
|
+
expect(event.detail.conditions).to.deep.equal([
|
|
97
|
+
[{filter: 'lost-reason', values: {reason: {operator: 'eq'}}}],
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should emit flow-conditions-cancel without mutating the saved value', async () => {
|
|
102
|
+
const saved: FlowBranchConditions = [[{filter: 'engagement', values: {count: {value: 2}}}]];
|
|
103
|
+
const el = await makeEditor(saved);
|
|
104
|
+
clickButton(el.shadowRoot!.querySelector('.condition__remove'));
|
|
105
|
+
await el.updateComplete;
|
|
106
|
+
|
|
107
|
+
setTimeout(() => clickButton(el.shadowRoot?.querySelector('.actions__cancel')));
|
|
108
|
+
await oneEvent(el, 'flow-conditions-cancel');
|
|
109
|
+
|
|
110
|
+
expect(saved[0]).to.have.length(1);
|
|
111
|
+
expect(el.value).to.deep.equal(saved);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should filter the picker list by the search term', async () => {
|
|
115
|
+
const el = await makeEditor();
|
|
116
|
+
const search = el.shadowRoot!.querySelector<ZnInput>('.picker__search')!;
|
|
117
|
+
search.value = 'lost';
|
|
118
|
+
search.dispatchEvent(new Event('input', {bubbles: true}));
|
|
119
|
+
await el.updateComplete;
|
|
120
|
+
|
|
121
|
+
const items = el.shadowRoot?.querySelectorAll('.picker__item');
|
|
122
|
+
expect(items?.length).to.equal(1);
|
|
123
|
+
expect(items?.[0].textContent).to.contain('Lost reason');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ZnFlowBranchConditions from './flow-branch-conditions.component';
|
|
2
|
+
|
|
3
|
+
export * from './flow-branch-conditions.component';
|
|
4
|
+
export default ZnFlowBranchConditions;
|
|
5
|
+
|
|
6
|
+
ZnFlowBranchConditions.define('zn-flow-branch-conditions');
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
'zn-flow-branch-conditions': ZnFlowBranchConditions;
|
|
11
|
+
}
|
|
12
|
+
}
|