import {type CSSResultGroup, html, type PropertyValues, unsafeCSS} from 'lit'; import {property, state} from 'lit/decorators.js'; import ZincElement from '../../../../internal/zinc-element'; import ZnButton from '../../../button'; import ZnIcon from '../../../icon'; import ZnInput from '../../../input'; import ZnOption from '../../../option'; import ZnSelect from '../../../select'; import { type FlowBranchCondition, type FlowBranchConditions, type FlowBranchFilter, type FlowFilterField, operatorLabel, } from '../../flow.types'; import styles from './flow-branch-conditions.scss'; /** * @summary The built-in branch conditions editor: pick filters from a searchable list, then * combine them into AND groups joined by OR. Rendered by ``'s branch editor * for node types that declare `branchFilters`; edits are drafted locally and only applied * on Save. * @documentation https://zinc.style/components/flow-branch-conditions * @status experimental * @since 1.0 * * @dependency zn-button * @dependency zn-icon * @dependency zn-input * @dependency zn-option * @dependency zn-select * * @event flow-conditions-save - Emitted on Save with `detail.conditions` (OR groups of AND-ed conditions). * @event flow-conditions-cancel - Emitted on Cancel, with the draft discarded. * * @csspart base - The editor wrapper. * @csspart picker - The filter picker (search + list). * @csspart conditions - The configured condition groups. */ export default class ZnFlowBranchConditions extends ZincElement { static styles: CSSResultGroup = unsafeCSS(styles); static dependencies = { 'zn-button': ZnButton, 'zn-icon': ZnIcon, 'zn-input': ZnInput, 'zn-option': ZnOption, 'zn-select': ZnSelect, }; /** The filters available to build conditions from. */ @property({attribute: false}) filters: FlowBranchFilter[] = []; /** The saved conditions being edited; changes stay in a local draft until Save. */ @property({attribute: false}) value: FlowBranchConditions = []; @state() private _draft: FlowBranchConditions = []; /** Group index the picker adds into (`_draft.length` starts a new OR group), or null when closed. */ @state() private _picker: number | null = null; @state() private _search = ''; /** JSON of the last `value` the draft was built from, so re-renders that pass an * equivalent value (fresh array refs) don't wipe in-progress edits. */ private _valueJson = ''; protected willUpdate(changed: PropertyValues) { super.willUpdate(changed); if (changed.has('value')) { const json = JSON.stringify(this.value ?? []); if (json !== this._valueJson) { this._valueJson = json; this._draft = JSON.parse(json) as FlowBranchConditions; this._picker = null; this._search = ''; } } } private _emit(name: string, detail?: Record) { this.dispatchEvent(new CustomEvent(name, {bubbles: true, composed: true, detail})); } private _save = () => { this._emit('flow-conditions-save', {conditions: JSON.parse(JSON.stringify(this._draft)) as FlowBranchConditions}); }; private _cancel = () => { this._emit('flow-conditions-cancel'); }; // --- Draft edits ------------------------------------------------------------ private _cloneDraft(): FlowBranchConditions { return this._draft.map(group => group.map(c => ({...c, values: {...c.values}}))); } private _addCondition(filter: FlowBranchFilter) { const values: FlowBranchCondition['values'] = {}; filter.fields.forEach(f => { values[f.id] = { ...(f.operators?.length ? {operator: f.operators[0].value} : {}), ...(f.value !== undefined ? {value: f.value} : {}), ...(f.units?.length ? {unit: f.units[0].value} : {}), }; }); const draft = this._cloneDraft(); const group = Math.min(this._picker ?? 0, draft.length); if (group === draft.length) draft.push([]); draft[group].push({filter: filter.id, values}); this._draft = draft; this._picker = null; this._search = ''; } private _removeCondition(group: number, index: number) { const draft = this._cloneDraft(); draft[group].splice(index, 1); this._draft = draft.filter(g => g.length); } private _setField(group: number, index: number, fieldId: string, patch: { operator?: string; value?: string | number; unit?: string }) { const draft = this._cloneDraft(); const condition = draft[group][index]; condition.values[fieldId] = {...condition.values[fieldId], ...patch}; this._draft = draft; } // --- Rendering -------------------------------------------------------------- private _renderPicker() { const term = this._search.trim().toLowerCase(); const matches = this.filters.filter(f => !term || f.label.toLowerCase().includes(term)); return html`
Add Filters ${this._draft.length ? html` ` : ''}
${matches.length === 0 ? html`

No matching filters.

` : matches.map(f => html` `)}
`; } /** * A select's minimum width, sized so its longest label never truncates * (estimated per char, plus padding and the chevron). When the row can't * fit it, flex-wrap gives the select its own full-width line instead. */ private static _selectMinWidth(labels: string[]): number { const longest = Math.max(0, ...labels.map(l => l.length)); return Math.min(260, Math.round(52 + longest * 7.2)); } private _renderField(condition: FlowBranchCondition, group: number, index: number, field: FlowFilterField) { const entry = condition.values[field.id] ?? {}; const type = field.type ?? (field.options?.length ? 'select' : 'text'); const set = (patch: { operator?: string; value?: string | number; unit?: string }) => this._setField(group, index, field.id, patch); return html`
${field.label ? html`${field.label}` : ''} ${field.operators?.length ? html` ${field.operators.map(o => html` ${operatorLabel(o)}`)} ` : ''} ${type === 'select' ? html` ${(field.options ?? []).map(o => html` ${o.label ?? o.value}`)} ` : html` `} ${field.units?.length ? html` ${field.units.map(u => html` ${u.label ?? u.value}`)} ` : field.suffix ? html`${field.suffix}` : ''}
`; } private _renderCondition(condition: FlowBranchCondition, group: number, index: number) { const filter = this.filters.find(f => f.id === condition.filter); return html`
${filter?.label ?? condition.filter}
${(filter?.fields ?? []).map(f => this._renderField(condition, group, index, f))}
`; } private _renderConditions() { return html`
${this._draft.map((group, gi) => html` ${gi > 0 ? html`
OR
` : ''}
${group.map((condition, ci) => html` ${ci > 0 ? html`
AND
` : ''} ${this._renderCondition(condition, gi, ci)} `)} AND
`)} OR
`; } render() { // With nothing configured yet, the picker is the first step (as designed). const picking = this._picker !== null || this._draft.length === 0; return html`
${picking ? this._renderPicker() : this._renderConditions()}
Save Cancel
`; } }