@servicetitan/dte-unlayer 0.94.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/display-conditions/ConditionGroup.d.ts +12 -0
- package/dist/display-conditions/ConditionGroup.d.ts.map +1 -0
- package/dist/display-conditions/ConditionGroup.js +181 -0
- package/dist/display-conditions/ConditionGroup.js.map +1 -0
- package/dist/display-conditions/ConditionGroupsSection.d.ts +11 -0
- package/dist/display-conditions/ConditionGroupsSection.d.ts.map +1 -0
- package/dist/display-conditions/ConditionGroupsSection.js +71 -0
- package/dist/display-conditions/ConditionGroupsSection.js.map +1 -0
- package/dist/display-conditions/ConditionRow.d.ts +11 -0
- package/dist/display-conditions/ConditionRow.d.ts.map +1 -0
- package/dist/display-conditions/ConditionRow.js +206 -0
- package/dist/display-conditions/ConditionRow.js.map +1 -0
- package/dist/display-conditions/DisplayConditionModal.d.ts +5 -0
- package/dist/display-conditions/DisplayConditionModal.d.ts.map +1 -0
- package/dist/display-conditions/DisplayConditionModal.js +282 -0
- package/dist/display-conditions/DisplayConditionModal.js.map +1 -0
- package/dist/display-conditions/SeparatorWithChip.d.ts +6 -0
- package/dist/display-conditions/SeparatorWithChip.d.ts.map +1 -0
- package/dist/display-conditions/SeparatorWithChip.js +15 -0
- package/dist/display-conditions/SeparatorWithChip.js.map +1 -0
- package/dist/display-conditions/constants.d.ts +7 -0
- package/dist/display-conditions/constants.d.ts.map +1 -0
- package/dist/display-conditions/constants.js +22 -0
- package/dist/display-conditions/constants.js.map +1 -0
- package/dist/display-conditions/displayConditionController.d.ts +9 -0
- package/dist/display-conditions/displayConditionController.d.ts.map +1 -0
- package/dist/display-conditions/displayConditionController.js +29 -0
- package/dist/display-conditions/displayConditionController.js.map +1 -0
- package/dist/display-conditions/nunjucks.d.ts +8 -0
- package/dist/display-conditions/nunjucks.d.ts.map +1 -0
- package/dist/display-conditions/nunjucks.js +448 -0
- package/dist/display-conditions/nunjucks.js.map +1 -0
- package/dist/display-conditions/schemaDataPoints.d.ts +4 -0
- package/dist/display-conditions/schemaDataPoints.d.ts.map +1 -0
- package/dist/display-conditions/schemaDataPoints.js +18 -0
- package/dist/display-conditions/schemaDataPoints.js.map +1 -0
- package/dist/display-conditions/types.d.ts +130 -0
- package/dist/display-conditions/types.d.ts.map +1 -0
- package/dist/display-conditions/types.js +72 -0
- package/dist/display-conditions/types.js.map +1 -0
- package/dist/editor-core-source.d.ts +1 -1
- package/dist/editor-core-source.d.ts.map +1 -1
- package/dist/editor-core-source.js +1 -1
- package/dist/editor-core-source.js.map +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +4 -0
- package/dist/editor.js.map +1 -1
- package/dist/shared/schema.d.ts +2 -0
- package/dist/shared/schema.d.ts.map +1 -1
- package/dist/shared/schema.js.map +1 -1
- package/dist/unlayer.d.ts.map +1 -1
- package/dist/unlayer.js +7 -0
- package/dist/unlayer.js.map +1 -1
- package/package.json +4 -2
- package/src/display-conditions/ConditionGroup.tsx +145 -0
- package/src/display-conditions/ConditionGroupsSection.tsx +64 -0
- package/src/display-conditions/ConditionRow.tsx +185 -0
- package/src/display-conditions/DisplayConditionModal.tsx +231 -0
- package/src/display-conditions/SeparatorWithChip.tsx +14 -0
- package/src/display-conditions/constants.ts +22 -0
- package/src/display-conditions/displayConditionController.ts +42 -0
- package/src/display-conditions/nunjucks.ts +503 -0
- package/src/display-conditions/schemaDataPoints.ts +33 -0
- package/src/display-conditions/types.ts +75 -0
- package/src/editor-core-source.ts +1 -1
- package/src/editor.tsx +2 -0
- package/src/shared/schema.ts +2 -0
- package/src/unlayer.tsx +9 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Button, Chip, Combobox, Flex, TextField } from '@servicetitan/anvil2';
|
|
2
|
+
import TrashIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { NUMBER_OPERATORS, SingleCondition, STRING_OPERATORS, VALUE_LESS_OPERATORS } from './types';
|
|
5
|
+
import type { DataPointOption } from './types';
|
|
6
|
+
|
|
7
|
+
export interface ConditionRowProps {
|
|
8
|
+
canRemove: boolean;
|
|
9
|
+
condition: SingleCondition;
|
|
10
|
+
dataPointOptions: DataPointOption[];
|
|
11
|
+
onChange: (c: SingleCondition) => void;
|
|
12
|
+
onRemove: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface OperatorOption {
|
|
16
|
+
label: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sanitizeNumericInput(raw: string): string {
|
|
21
|
+
// Allow only a single leading minus and one decimal separator.
|
|
22
|
+
let value = raw.replaceAll(/[^0-9.-]/g, '');
|
|
23
|
+
const isNegative = value.startsWith('-');
|
|
24
|
+
value = value.replaceAll('-', '');
|
|
25
|
+
if (isNegative) {
|
|
26
|
+
value = `-${value}`;
|
|
27
|
+
}
|
|
28
|
+
const firstDot = value.indexOf('.');
|
|
29
|
+
if (firstDot >= 0) {
|
|
30
|
+
value = `${value.slice(0, firstDot + 1)}${value.slice(firstDot + 1).replaceAll('.', '')}`;
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ConditionRow({
|
|
36
|
+
canRemove,
|
|
37
|
+
condition,
|
|
38
|
+
dataPointOptions,
|
|
39
|
+
onChange,
|
|
40
|
+
onRemove,
|
|
41
|
+
}: Readonly<ConditionRowProps>) {
|
|
42
|
+
const selectedDataPoint = useMemo(
|
|
43
|
+
() => dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey) ?? null,
|
|
44
|
+
[dataPointOptions, condition.dataPointKey],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const fieldType = selectedDataPoint?.fieldType ?? 'string';
|
|
48
|
+
|
|
49
|
+
const operatorItems: OperatorOption[] = useMemo(
|
|
50
|
+
() =>
|
|
51
|
+
fieldType === 'number'
|
|
52
|
+
? ([...NUMBER_OPERATORS] as OperatorOption[])
|
|
53
|
+
: ([...STRING_OPERATORS] as OperatorOption[]),
|
|
54
|
+
[fieldType],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const selectedOperator = useMemo(
|
|
58
|
+
() => operatorItems.find(op => op.value === condition.operator) ?? null,
|
|
59
|
+
[operatorItems, condition.operator],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const isValueLess = VALUE_LESS_OPERATORS.includes(condition.operator);
|
|
63
|
+
|
|
64
|
+
const handleValueChange = (raw: string) => {
|
|
65
|
+
const value = fieldType === 'number' ? sanitizeNumericInput(raw) : raw;
|
|
66
|
+
onChange({ ...condition, value });
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleDataPointChange = (item: DataPointOption | null) => {
|
|
70
|
+
const nextFieldType = item?.fieldType ?? 'string';
|
|
71
|
+
const nextIsNumber = nextFieldType === 'number';
|
|
72
|
+
const nextOperatorItems = nextIsNumber ? NUMBER_OPERATORS : STRING_OPERATORS;
|
|
73
|
+
const operatorReset = !nextOperatorItems.some(op => op.value === condition.operator);
|
|
74
|
+
onChange({
|
|
75
|
+
...condition,
|
|
76
|
+
dataPointKey: item?.fullKey ?? '',
|
|
77
|
+
operator: operatorReset
|
|
78
|
+
? nextIsNumber
|
|
79
|
+
? 'num_eq'
|
|
80
|
+
: 'is_equal_to'
|
|
81
|
+
: condition.operator,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Flex
|
|
87
|
+
direction="row"
|
|
88
|
+
alignItems="flex-end"
|
|
89
|
+
gap="2"
|
|
90
|
+
style={{ flexWrap: 'wrap', rowGap: 8, width: '100%' }}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
style={{
|
|
94
|
+
display: 'flex',
|
|
95
|
+
alignItems: 'flex-end',
|
|
96
|
+
flexShrink: 0,
|
|
97
|
+
padding: '6px 12px',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<Chip label="IF" size="medium" />
|
|
101
|
+
</div>
|
|
102
|
+
<div style={{ flex: '2 1 280px', minWidth: 240 }}>
|
|
103
|
+
<Combobox
|
|
104
|
+
{...({ disableClearSelection: true } as object)}
|
|
105
|
+
itemToKey={(item: DataPointOption | null) => item?.fullKey ?? ''}
|
|
106
|
+
itemToString={(item: DataPointOption | null) => item?.title ?? ''}
|
|
107
|
+
items={dataPointOptions}
|
|
108
|
+
selectedItem={selectedDataPoint}
|
|
109
|
+
onChange={handleDataPointChange}
|
|
110
|
+
>
|
|
111
|
+
<Combobox.SelectTrigger label="Data point" placeholder="Select data point..." />
|
|
112
|
+
<Combobox.Content>
|
|
113
|
+
{({ items }: { items: DataPointOption[] }) => (
|
|
114
|
+
<Combobox.List>
|
|
115
|
+
{items.map((item, i) => (
|
|
116
|
+
<Combobox.Item index={i} item={item} key={item.fullKey}>
|
|
117
|
+
{item.title}
|
|
118
|
+
</Combobox.Item>
|
|
119
|
+
))}
|
|
120
|
+
</Combobox.List>
|
|
121
|
+
)}
|
|
122
|
+
</Combobox.Content>
|
|
123
|
+
</Combobox>
|
|
124
|
+
</div>
|
|
125
|
+
<div style={{ flex: '1 1 220px', minWidth: 180 }}>
|
|
126
|
+
<Combobox
|
|
127
|
+
{...({ disableClearSelection: true } as object)}
|
|
128
|
+
itemToKey={(item: OperatorOption | null) => item?.value ?? ''}
|
|
129
|
+
itemToString={(item: OperatorOption | null) => item?.label ?? ''}
|
|
130
|
+
items={operatorItems}
|
|
131
|
+
selectedItem={selectedOperator}
|
|
132
|
+
onChange={(item: OperatorOption | null) =>
|
|
133
|
+
onChange({
|
|
134
|
+
...condition,
|
|
135
|
+
operator: (item?.value ?? 'is_equal_to') as SingleCondition['operator'],
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
>
|
|
139
|
+
<Combobox.SelectTrigger label="Condition" placeholder="Select..." />
|
|
140
|
+
<Combobox.Content>
|
|
141
|
+
{({ items }: { items: OperatorOption[] }) => (
|
|
142
|
+
<Combobox.List>
|
|
143
|
+
{items.map((item, i) => (
|
|
144
|
+
<Combobox.Item index={i} item={item} key={item.value}>
|
|
145
|
+
{item.label}
|
|
146
|
+
</Combobox.Item>
|
|
147
|
+
))}
|
|
148
|
+
</Combobox.List>
|
|
149
|
+
)}
|
|
150
|
+
</Combobox.Content>
|
|
151
|
+
</Combobox>
|
|
152
|
+
</div>
|
|
153
|
+
{!isValueLess && (
|
|
154
|
+
<div style={{ flex: '1 1 220px', minWidth: 180 }}>
|
|
155
|
+
<TextField
|
|
156
|
+
label="Value"
|
|
157
|
+
value={condition.value}
|
|
158
|
+
onChange={e => handleValueChange(e.target.value)}
|
|
159
|
+
placeholder={fieldType === 'number' ? 'e.g. 1.5' : 'Enter value...'}
|
|
160
|
+
style={{ width: '100%' }}
|
|
161
|
+
aria-label="Value"
|
|
162
|
+
{...(fieldType === 'number' && { inputMode: 'decimal' })}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
{canRemove && (
|
|
167
|
+
<div
|
|
168
|
+
style={{
|
|
169
|
+
display: 'flex',
|
|
170
|
+
alignItems: 'flex-end',
|
|
171
|
+
flexShrink: 0,
|
|
172
|
+
padding: '6px 12px',
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<Button
|
|
176
|
+
appearance="secondary"
|
|
177
|
+
aria-label="Remove condition"
|
|
178
|
+
icon={{ before: TrashIcon }}
|
|
179
|
+
onClick={onRemove}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</Flex>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Button, Combobox, Dialog, Flex, Text } from '@servicetitan/anvil2';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { ConditionGroupsSection } from './ConditionGroupsSection';
|
|
5
|
+
import { defaultGroup, defaultState, MODAL_CONTENT_MAX_HEIGHT } from './constants';
|
|
6
|
+
import { DisplayConditionRequest, onDisplayCondition } from './displayConditionController';
|
|
7
|
+
import { buildUnlayerDisplayCondition, parseUnlayerDisplayCondition } from './nunjucks';
|
|
8
|
+
import { getSchemaDataPointOptions } from './schemaDataPoints';
|
|
9
|
+
import { ConditionGroup as ConditionGroupType, DisplayBehavior } from './types';
|
|
10
|
+
|
|
11
|
+
const BEHAVIOR_OPTIONS = [
|
|
12
|
+
{ label: 'Show', value: 'show' },
|
|
13
|
+
{ label: 'Hide', value: 'hide' },
|
|
14
|
+
] as const;
|
|
15
|
+
const NUMERIC_VALUE_RE = /^-?(?:\d+\.?\d*|\.\d+)$/;
|
|
16
|
+
|
|
17
|
+
type BehaviorOption = (typeof BEHAVIOR_OPTIONS)[number];
|
|
18
|
+
|
|
19
|
+
export interface DisplayConditionModalProps {
|
|
20
|
+
schema?: import('../shared/schema').SchemaObject;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DisplayConditionModal = (props: DisplayConditionModalProps) => {
|
|
24
|
+
const [request, setRequest] = useState<DisplayConditionRequest | null>(null);
|
|
25
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
+
const [state, setState] = useState(defaultState);
|
|
27
|
+
|
|
28
|
+
const dataPointOptions = useMemo(() => getSchemaDataPointOptions(props.schema), [props.schema]);
|
|
29
|
+
|
|
30
|
+
const portalTarget = useMemo(() => {
|
|
31
|
+
if (typeof document === 'undefined') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return document.body;
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
return onDisplayCondition(nextRequest => {
|
|
39
|
+
setRequest(nextRequest);
|
|
40
|
+
const existing = nextRequest.data;
|
|
41
|
+
const parsed = parseUnlayerDisplayCondition(existing);
|
|
42
|
+
setState(parsed ?? defaultState());
|
|
43
|
+
setIsOpen(true);
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleClose = useCallback(() => {
|
|
48
|
+
if (request) {
|
|
49
|
+
request.done(request.data ?? null);
|
|
50
|
+
setRequest(null);
|
|
51
|
+
}
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
}, [request]);
|
|
54
|
+
|
|
55
|
+
const handleSave = useCallback(() => {
|
|
56
|
+
if (!request) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const condition = buildUnlayerDisplayCondition(state);
|
|
60
|
+
if (condition) {
|
|
61
|
+
request.done(condition);
|
|
62
|
+
} else {
|
|
63
|
+
request.done(null);
|
|
64
|
+
}
|
|
65
|
+
setRequest(null);
|
|
66
|
+
setIsOpen(false);
|
|
67
|
+
}, [request, state]);
|
|
68
|
+
|
|
69
|
+
const updateGroup = useCallback((index: number, group: ConditionGroupType) => {
|
|
70
|
+
setState(prev => {
|
|
71
|
+
const next = [...prev.groups];
|
|
72
|
+
next[index] = group;
|
|
73
|
+
return { ...prev, groups: next };
|
|
74
|
+
});
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const removeGroup = useCallback((index: number) => {
|
|
78
|
+
setState(prev => ({
|
|
79
|
+
...prev,
|
|
80
|
+
groups: (() => {
|
|
81
|
+
const nextGroups = prev.groups.filter((_, i) => i !== index);
|
|
82
|
+
if (nextGroups.length === 0) {
|
|
83
|
+
return [defaultGroup()];
|
|
84
|
+
}
|
|
85
|
+
if (index === 0) {
|
|
86
|
+
const { logicalOperator: unusedOp, ...firstGroup } = nextGroups[0];
|
|
87
|
+
return [firstGroup, ...nextGroups.slice(1)];
|
|
88
|
+
}
|
|
89
|
+
return nextGroups;
|
|
90
|
+
})(),
|
|
91
|
+
}));
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const addGroup = useCallback(() => {
|
|
95
|
+
setState(prev => ({
|
|
96
|
+
...prev,
|
|
97
|
+
groups: [...prev.groups, { ...defaultGroup(), logicalOperator: 'and' }],
|
|
98
|
+
}));
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const selectedBehavior = useMemo(
|
|
102
|
+
() => BEHAVIOR_OPTIONS.find(opt => opt.value === state.behavior) ?? BEHAVIOR_OPTIONS[0],
|
|
103
|
+
[state.behavior],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const handleBehaviorChange = useCallback((item: BehaviorOption | null) => {
|
|
107
|
+
if (item) {
|
|
108
|
+
setState(prev => ({ ...prev, behavior: item.value as DisplayBehavior }));
|
|
109
|
+
}
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const canSave = useMemo(() => {
|
|
113
|
+
for (const group of state.groups) {
|
|
114
|
+
for (const condition of group.conditions) {
|
|
115
|
+
if (!condition.dataPointKey) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (condition.operator === 'is_empty' || condition.operator === 'is_not_empty') {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const trimmedValue = condition.value.trim();
|
|
122
|
+
if (!trimmedValue) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const fieldType =
|
|
126
|
+
dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey)
|
|
127
|
+
?.fieldType ?? 'string';
|
|
128
|
+
if (fieldType === 'number' && !NUMERIC_VALUE_RE.test(trimmedValue)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return state.groups.length > 0;
|
|
134
|
+
}, [dataPointOptions, state.groups]);
|
|
135
|
+
|
|
136
|
+
if (!portalTarget || !isOpen) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return createPortal(
|
|
141
|
+
<Dialog open={isOpen} onClose={handleClose} size="xlarge">
|
|
142
|
+
<Dialog.Header>Rules and Conditions</Dialog.Header>
|
|
143
|
+
<Dialog.Content>
|
|
144
|
+
<Flex
|
|
145
|
+
direction="column"
|
|
146
|
+
gap="6"
|
|
147
|
+
style={{
|
|
148
|
+
maxHeight: MODAL_CONTENT_MAX_HEIGHT,
|
|
149
|
+
overflowY: 'auto',
|
|
150
|
+
width: '100%',
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<Flex
|
|
154
|
+
direction="column"
|
|
155
|
+
gap="3"
|
|
156
|
+
style={{
|
|
157
|
+
borderBottom: '1px solid #e0e0e0',
|
|
158
|
+
paddingBottom: 16,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<Text size="medium" variant="body" style={{ fontWeight: 'bold' }}>
|
|
162
|
+
When conditions are met:
|
|
163
|
+
</Text>
|
|
164
|
+
<Flex
|
|
165
|
+
direction="row"
|
|
166
|
+
alignItems="center"
|
|
167
|
+
gap="3"
|
|
168
|
+
style={{ flexWrap: 'wrap' }}
|
|
169
|
+
>
|
|
170
|
+
<Text size="small" subdued variant="body">
|
|
171
|
+
Select to show or hide selected component.
|
|
172
|
+
</Text>
|
|
173
|
+
<div style={{ width: 160 }}>
|
|
174
|
+
<Combobox
|
|
175
|
+
{...({ disableClearSelection: true } as object)}
|
|
176
|
+
itemToKey={(item: BehaviorOption | null) => item?.value ?? ''}
|
|
177
|
+
itemToString={(item: BehaviorOption | null) =>
|
|
178
|
+
item?.label ?? ''
|
|
179
|
+
}
|
|
180
|
+
items={[...BEHAVIOR_OPTIONS]}
|
|
181
|
+
selectedItem={selectedBehavior}
|
|
182
|
+
onChange={handleBehaviorChange}
|
|
183
|
+
>
|
|
184
|
+
<Combobox.SelectTrigger
|
|
185
|
+
label="Behavior"
|
|
186
|
+
placeholder="Select..."
|
|
187
|
+
/>
|
|
188
|
+
<Combobox.Content>
|
|
189
|
+
{({ items }: { items: BehaviorOption[] }) => (
|
|
190
|
+
<Combobox.List>
|
|
191
|
+
{items.map((item, i) => (
|
|
192
|
+
<Combobox.Item
|
|
193
|
+
index={i}
|
|
194
|
+
item={item}
|
|
195
|
+
key={item.value}
|
|
196
|
+
>
|
|
197
|
+
{item.label}
|
|
198
|
+
</Combobox.Item>
|
|
199
|
+
))}
|
|
200
|
+
</Combobox.List>
|
|
201
|
+
)}
|
|
202
|
+
</Combobox.Content>
|
|
203
|
+
</Combobox>
|
|
204
|
+
</div>
|
|
205
|
+
</Flex>
|
|
206
|
+
</Flex>
|
|
207
|
+
<ConditionGroupsSection
|
|
208
|
+
dataPointOptions={dataPointOptions}
|
|
209
|
+
groups={state.groups}
|
|
210
|
+
onAddGroup={addGroup}
|
|
211
|
+
onRemoveGroup={removeGroup}
|
|
212
|
+
onUpdateGroup={updateGroup}
|
|
213
|
+
/>
|
|
214
|
+
{!canSave && (
|
|
215
|
+
<Text size="small" subdued variant="body">
|
|
216
|
+
Complete each condition before saving. Data point and value are
|
|
217
|
+
required, and numeric fields must contain a valid number.
|
|
218
|
+
</Text>
|
|
219
|
+
)}
|
|
220
|
+
</Flex>
|
|
221
|
+
</Dialog.Content>
|
|
222
|
+
<Dialog.Footer sticky>
|
|
223
|
+
<Dialog.CancelButton onClick={handleClose}>Cancel</Dialog.CancelButton>
|
|
224
|
+
<Button appearance="primary" disabled={!canSave} onClick={handleSave}>
|
|
225
|
+
Save Changes
|
|
226
|
+
</Button>
|
|
227
|
+
</Dialog.Footer>
|
|
228
|
+
</Dialog>,
|
|
229
|
+
portalTarget,
|
|
230
|
+
);
|
|
231
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Chip, Divider } from '@servicetitan/anvil2';
|
|
2
|
+
|
|
3
|
+
export interface SeparatorWithChipProps {
|
|
4
|
+
label: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function SeparatorWithChip({ color = '#94a3b8', label }: Readonly<SeparatorWithChipProps>) {
|
|
9
|
+
return (
|
|
10
|
+
<Divider alignContent="center" spacing="2">
|
|
11
|
+
<Chip label={label} size="small" color={color} />
|
|
12
|
+
</Divider>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ConditionGroup, DisplayConditionState, SingleCondition } from './types';
|
|
2
|
+
|
|
3
|
+
export const generateId = () => Math.random().toString(36).slice(2, 11);
|
|
4
|
+
|
|
5
|
+
export const defaultCondition = (): SingleCondition => ({
|
|
6
|
+
dataPointKey: '',
|
|
7
|
+
id: generateId(),
|
|
8
|
+
operator: 'is_equal_to',
|
|
9
|
+
value: '',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const defaultGroup = (): ConditionGroup => ({
|
|
13
|
+
conditions: [defaultCondition()],
|
|
14
|
+
id: generateId(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const defaultState = (): DisplayConditionState => ({
|
|
18
|
+
behavior: 'show',
|
|
19
|
+
groups: [defaultGroup()],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const MODAL_CONTENT_MAX_HEIGHT = '70vh';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface DisplayConditionRequest {
|
|
2
|
+
data: any;
|
|
3
|
+
done: (condition: any) => void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Listener = (request: DisplayConditionRequest) => void;
|
|
7
|
+
|
|
8
|
+
const DISPLAY_CONDITION_EVENT = 'dte:display-condition';
|
|
9
|
+
const displayConditionEvents = new EventTarget();
|
|
10
|
+
let activeListeners = 0;
|
|
11
|
+
|
|
12
|
+
export const onDisplayCondition = (listener: Listener) => {
|
|
13
|
+
const handler = (event: Event) => {
|
|
14
|
+
const detail = (event as CustomEvent<DisplayConditionRequest>).detail;
|
|
15
|
+
if (!detail) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
listener(detail);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
activeListeners += 1;
|
|
22
|
+
displayConditionEvents.addEventListener(DISPLAY_CONDITION_EVENT, handler as EventListener);
|
|
23
|
+
|
|
24
|
+
return () => {
|
|
25
|
+
activeListeners = Math.max(0, activeListeners - 1);
|
|
26
|
+
displayConditionEvents.removeEventListener(
|
|
27
|
+
DISPLAY_CONDITION_EVENT,
|
|
28
|
+
handler as EventListener,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const emitDisplayCondition = (request: DisplayConditionRequest) => {
|
|
34
|
+
if (activeListeners === 0) {
|
|
35
|
+
request.done(request.data);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
displayConditionEvents.dispatchEvent(
|
|
40
|
+
new CustomEvent<DisplayConditionRequest>(DISPLAY_CONDITION_EVENT, { detail: request }),
|
|
41
|
+
);
|
|
42
|
+
};
|