@servicetitan/dte-pdf-editor 1.41.0 → 1.43.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.
Files changed (120) hide show
  1. package/README.md +89 -45
  2. package/dist/components/display-conditions/condition-group.d.ts +8 -3
  3. package/dist/components/display-conditions/condition-group.d.ts.map +1 -1
  4. package/dist/components/display-conditions/condition-group.js +7 -2
  5. package/dist/components/display-conditions/condition-group.js.map +1 -1
  6. package/dist/components/display-conditions/condition-groups-section.d.ts +8 -3
  7. package/dist/components/display-conditions/condition-groups-section.d.ts.map +1 -1
  8. package/dist/components/display-conditions/condition-groups-section.js +2 -2
  9. package/dist/components/display-conditions/condition-groups-section.js.map +1 -1
  10. package/dist/components/display-conditions/condition-row.d.ts +8 -3
  11. package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
  12. package/dist/components/display-conditions/condition-row.js +154 -22
  13. package/dist/components/display-conditions/condition-row.js.map +1 -1
  14. package/dist/components/display-conditions/display-condition-modal.d.ts +6 -2
  15. package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -1
  16. package/dist/components/display-conditions/display-condition-modal.js +7 -7
  17. package/dist/components/display-conditions/display-condition-modal.js.map +1 -1
  18. package/dist/components/display-conditions/display-condition.d.ts +1 -1
  19. package/dist/components/display-conditions/display-condition.d.ts.map +1 -1
  20. package/dist/components/display-conditions/display-condition.js +2 -2
  21. package/dist/components/display-conditions/display-condition.js.map +1 -1
  22. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts +6 -2
  23. package/dist/components/field-config-panel/field-config-panel-overlay.d.ts.map +1 -1
  24. package/dist/components/field-config-panel/field-config-panel-overlay.js +2 -2
  25. package/dist/components/field-config-panel/field-config-panel-overlay.js.map +1 -1
  26. package/dist/components/field-config-panel/field-config-panel.d.ts +6 -2
  27. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  28. package/dist/components/field-config-panel/field-config-panel.js +9 -4
  29. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  30. package/dist/components/field-config-panel/field-sidebar.d.ts +5 -1
  31. package/dist/components/field-config-panel/field-sidebar.d.ts.map +1 -1
  32. package/dist/components/field-config-panel/field-sidebar.js +42 -8
  33. package/dist/components/field-config-panel/field-sidebar.js.map +1 -1
  34. package/dist/components/field-config-panel/formula-generator.d.ts +5 -1
  35. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  36. package/dist/components/field-config-panel/formula-generator.js +2 -2
  37. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  38. package/dist/components/field-config-panel/formula-modal.d.ts +5 -1
  39. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  40. package/dist/components/field-config-panel/formula-modal.js +38 -8
  41. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  42. package/dist/components/field-sidebar/field-sidebar.d.ts +6 -1
  43. package/dist/components/field-sidebar/field-sidebar.d.ts.map +1 -1
  44. package/dist/components/field-sidebar/field-sidebar.js +11 -6
  45. package/dist/components/field-sidebar/field-sidebar.js.map +1 -1
  46. package/dist/components/field-sidebar/form-fields-type-list.d.ts +13 -0
  47. package/dist/components/field-sidebar/form-fields-type-list.d.ts.map +1 -0
  48. package/dist/components/field-sidebar/form-fields-type-list.js +14 -0
  49. package/dist/components/field-sidebar/form-fields-type-list.js.map +1 -0
  50. package/dist/components/pdf-editor/pdf-editor.d.ts +4 -1
  51. package/dist/components/pdf-editor/pdf-editor.d.ts.map +1 -1
  52. package/dist/components/pdf-editor/pdf-editor.js +6 -5
  53. package/dist/components/pdf-editor/pdf-editor.js.map +1 -1
  54. package/dist/components/pdf-fields-overlay/pdf-overlay-field.d.ts.map +1 -1
  55. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js +1 -1
  56. package/dist/components/pdf-fields-overlay/pdf-overlay-field.js.map +1 -1
  57. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  58. package/dist/components/pdf-view/pdf-view.js +2 -1
  59. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  60. package/dist/constants/menu-group.d.ts.map +1 -1
  61. package/dist/constants/menu-group.js +2 -0
  62. package/dist/constants/menu-group.js.map +1 -1
  63. package/dist/hooks/index.d.ts +1 -0
  64. package/dist/hooks/index.d.ts.map +1 -1
  65. package/dist/hooks/index.js +1 -0
  66. package/dist/hooks/index.js.map +1 -1
  67. package/dist/hooks/useFormFields.d.ts +13 -0
  68. package/dist/hooks/useFormFields.d.ts.map +1 -0
  69. package/dist/hooks/useFormFields.js +121 -0
  70. package/dist/hooks/useFormFields.js.map +1 -0
  71. package/dist/hooks/useFormulaEditor.d.ts.map +1 -1
  72. package/dist/hooks/useFormulaEditor.js +7 -1
  73. package/dist/hooks/useFormulaEditor.js.map +1 -1
  74. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  75. package/dist/hooks/usePdfFieldDnD.js +4 -0
  76. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  77. package/dist/interface/types.d.ts +35 -3
  78. package/dist/interface/types.d.ts.map +1 -1
  79. package/dist/interface/types.js +1 -0
  80. package/dist/interface/types.js.map +1 -1
  81. package/dist/utils/formula/expression.utils.d.ts +5 -2
  82. package/dist/utils/formula/expression.utils.d.ts.map +1 -1
  83. package/dist/utils/formula/expression.utils.js +8 -6
  84. package/dist/utils/formula/expression.utils.js.map +1 -1
  85. package/dist/utils/formula/form-fields.utils.d.ts +21 -0
  86. package/dist/utils/formula/form-fields.utils.d.ts.map +1 -0
  87. package/dist/utils/formula/form-fields.utils.js +101 -0
  88. package/dist/utils/formula/form-fields.utils.js.map +1 -0
  89. package/dist/utils/formula/index.d.ts +1 -0
  90. package/dist/utils/formula/index.d.ts.map +1 -1
  91. package/dist/utils/formula/index.js +1 -0
  92. package/dist/utils/formula/index.js.map +1 -1
  93. package/package.json +1 -1
  94. package/src/components/display-conditions/condition-group.tsx +32 -5
  95. package/src/components/display-conditions/condition-groups-section.tsx +24 -4
  96. package/src/components/display-conditions/condition-row.tsx +359 -80
  97. package/src/components/display-conditions/display-condition-modal.tsx +39 -10
  98. package/src/components/display-conditions/display-condition.tsx +19 -2
  99. package/src/components/field-config-panel/field-config-panel-overlay.tsx +22 -2
  100. package/src/components/field-config-panel/field-config-panel.tsx +34 -10
  101. package/src/components/field-config-panel/field-sidebar.tsx +187 -41
  102. package/src/components/field-config-panel/formula-generator.tsx +14 -0
  103. package/src/components/field-config-panel/formula-modal.tsx +62 -5
  104. package/src/components/field-sidebar/field-sidebar.tsx +35 -4
  105. package/src/components/field-sidebar/form-fields-type-list.tsx +113 -0
  106. package/src/components/pdf-editor/pdf-editor.tsx +42 -25
  107. package/src/components/pdf-fields-overlay/pdf-overlay-field.tsx +3 -1
  108. package/src/components/pdf-view/pdf-view.tsx +2 -1
  109. package/src/constants/menu-group.ts +2 -0
  110. package/src/hooks/index.ts +1 -0
  111. package/src/hooks/useFormFields.ts +157 -0
  112. package/src/hooks/useFormulaEditor.ts +7 -1
  113. package/src/hooks/usePdfFieldDnD.ts +4 -0
  114. package/src/interface/types.ts +43 -4
  115. package/src/styles/field-type-list.css +1 -0
  116. package/src/styles/formula-modal.css +66 -8
  117. package/src/styles/variables.css +4 -0
  118. package/src/utils/formula/expression.utils.ts +24 -6
  119. package/src/utils/formula/form-fields.utils.ts +165 -0
  120. package/src/utils/formula/index.ts +1 -0
@@ -1,19 +1,34 @@
1
- import { Button, Chip, Combobox, Flex, TextField } from '@servicetitan/anvil2';
1
+ import { Button, Chip, Combobox, Flex, Text, TextField } from '@servicetitan/anvil2';
2
2
  import TrashIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
3
- import { useMemo } from 'react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import {
5
5
  ConditionOperator,
6
6
  DataPointOption,
7
7
  DisplayConditionSingle,
8
+ FieldTypeEnum,
9
+ FormFieldsByFormIdI,
10
+ FormInfo,
8
11
  NUMBER_OPERATORS,
9
12
  STRING_OPERATORS,
10
13
  VALUE_LESS_OPERATORS,
11
14
  } from '../../interface/types';
15
+ import {
16
+ DisplayConditionSourceKind,
17
+ formFieldToDisplayConditionDataPointOption,
18
+ getDisplayConditionFieldTypeForKey,
19
+ inferDisplayConditionSourceKind,
20
+ parseFormFieldKey,
21
+ } from '../../utils';
12
22
 
13
23
  export interface ConditionRowProps {
14
24
  canRemove: boolean;
15
25
  condition: DisplayConditionSingle;
16
- dataPointOptions: DataPointOption[];
26
+ fillableOptions: DataPointOption[];
27
+ mergeTagOptions: DataPointOption[];
28
+ forms?: FormInfo[];
29
+ formFieldsByFormId?: FormFieldsByFormIdI;
30
+ onRequestFormFields?: (formId: number) => void;
31
+ formFieldsLoadingFormId?: number | null;
17
32
  onChange: (c: DisplayConditionSingle) => void;
18
33
  onRemove: () => void;
19
34
  }
@@ -23,6 +38,23 @@ interface OperatorOption {
23
38
  value: string;
24
39
  }
25
40
 
41
+ interface SourceKindOption {
42
+ value: DisplayConditionSourceKind;
43
+ label: string;
44
+ }
45
+
46
+ const SOURCE_KIND_ORDER: DisplayConditionSourceKind[] = [
47
+ FieldTypeEnum.dataModel,
48
+ FieldTypeEnum.fillable,
49
+ FieldTypeEnum.forms,
50
+ ];
51
+
52
+ const SOURCE_KIND_LABELS: Record<DisplayConditionSourceKind, string> = {
53
+ [FieldTypeEnum.dataModel]: 'Merge tags',
54
+ [FieldTypeEnum.fillable]: 'Fillable field',
55
+ [FieldTypeEnum.forms]: 'Form',
56
+ };
57
+
26
58
  function sanitizeNumericInput(raw: string): string {
27
59
  let value = raw.replaceAll(/[^0-9.-]/g, '');
28
60
  const isNegative = value.startsWith('-');
@@ -37,19 +69,135 @@ function sanitizeNumericInput(raw: string): string {
37
69
  return value;
38
70
  }
39
71
 
72
+ const pickDefaultSourceKind = (options: SourceKindOption[]) => {
73
+ return options[0]?.value ?? FieldTypeEnum.dataModel;
74
+ };
75
+
40
76
  export function ConditionRow({
41
77
  canRemove,
42
78
  condition,
43
- dataPointOptions,
79
+ fillableOptions,
80
+ formFieldsByFormId,
81
+ formFieldsLoadingFormId,
82
+ forms,
83
+ mergeTagOptions,
44
84
  onChange,
45
85
  onRemove,
86
+ onRequestFormFields,
46
87
  }: Readonly<ConditionRowProps>) {
47
- const selectedDataPoint = useMemo(
48
- () => dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey) ?? null,
49
- [dataPointOptions, condition.dataPointKey],
88
+ const inferredKind = inferDisplayConditionSourceKind(
89
+ condition.dataPointKey,
90
+ mergeTagOptions,
91
+ fillableOptions,
92
+ );
93
+
94
+ const [pendingSourceKind, setPendingSourceKind] = useState<DisplayConditionSourceKind | null>(
95
+ null,
96
+ );
97
+
98
+ const [pickerForm, setPickerForm] = useState<FormInfo | null>(() => {
99
+ const parsed = parseFormFieldKey(condition.dataPointKey);
100
+ if (!parsed || !forms?.length) {
101
+ return null;
102
+ }
103
+ return forms.find(f => f.id === parsed.formId) ?? null;
104
+ });
105
+
106
+ const sourceKindOptions = useMemo((): SourceKindOption[] => {
107
+ const allowed = new Set<DisplayConditionSourceKind>();
108
+ if (mergeTagOptions.length) {
109
+ allowed.add(FieldTypeEnum.dataModel);
110
+ }
111
+ if (fillableOptions.length) {
112
+ allowed.add(FieldTypeEnum.fillable);
113
+ }
114
+ if (forms?.length) {
115
+ allowed.add(FieldTypeEnum.forms);
116
+ }
117
+ const inferred = inferDisplayConditionSourceKind(
118
+ condition.dataPointKey,
119
+ mergeTagOptions,
120
+ fillableOptions,
121
+ );
122
+ if (inferred) {
123
+ allowed.add(inferred);
124
+ }
125
+ if (parseFormFieldKey(condition.dataPointKey)) {
126
+ allowed.add(FieldTypeEnum.forms);
127
+ }
128
+ return SOURCE_KIND_ORDER.filter(k => allowed.has(k)).map(value => ({
129
+ value,
130
+ label: SOURCE_KIND_LABELS[value],
131
+ }));
132
+ }, [condition.dataPointKey, fillableOptions, forms, mergeTagOptions]);
133
+
134
+ const sourceKind: DisplayConditionSourceKind =
135
+ inferredKind ?? pendingSourceKind ?? pickDefaultSourceKind(sourceKindOptions);
136
+
137
+ useEffect(() => {
138
+ if (inferredKind) {
139
+ setPendingSourceKind(null);
140
+ }
141
+ }, [inferredKind]);
142
+
143
+ const parsedKey = useMemo(
144
+ () => parseFormFieldKey(condition.dataPointKey),
145
+ [condition.dataPointKey],
50
146
  );
51
147
 
52
- const fieldType = selectedDataPoint?.fieldType ?? 'string';
148
+ useEffect(() => {
149
+ if (parsedKey && forms?.length) {
150
+ setPickerForm(forms.find(f => f.id === parsedKey.formId) ?? null);
151
+ return;
152
+ }
153
+ if (sourceKind !== FieldTypeEnum.forms) {
154
+ setPickerForm(null);
155
+ }
156
+ }, [parsedKey, forms, sourceKind]);
157
+
158
+ useEffect(() => {
159
+ if (!parsedKey || !onRequestFormFields) {
160
+ return;
161
+ }
162
+ const list = formFieldsByFormId?.[parsedKey.formId];
163
+ if (!list?.length) {
164
+ onRequestFormFields(parsedKey.formId);
165
+ }
166
+ }, [parsedKey, onRequestFormFields, formFieldsByFormId]);
167
+
168
+ const sortedForms = useMemo(
169
+ () => (forms?.length ? [...forms].sort((a, b) => a.name.localeCompare(b.name)) : []),
170
+ [forms],
171
+ );
172
+
173
+ const dataPointItems: DataPointOption[] = useMemo(() => {
174
+ if (sourceKind === FieldTypeEnum.dataModel) {
175
+ return mergeTagOptions;
176
+ }
177
+ if (sourceKind === FieldTypeEnum.fillable) {
178
+ return fillableOptions;
179
+ }
180
+ if (!pickerForm) {
181
+ return [];
182
+ }
183
+ const list = formFieldsByFormId?.[pickerForm.id] ?? [];
184
+ return list.map(f => formFieldToDisplayConditionDataPointOption(pickerForm, f));
185
+ }, [sourceKind, mergeTagOptions, fillableOptions, pickerForm, formFieldsByFormId]);
186
+
187
+ const fieldType = getDisplayConditionFieldTypeForKey(
188
+ condition.dataPointKey,
189
+ mergeTagOptions,
190
+ fillableOptions,
191
+ formFieldsByFormId ?? {},
192
+ );
193
+
194
+ const selectedSourceKindOption =
195
+ sourceKindOptions.find(o => o.value === sourceKind) ?? sourceKindOptions[0] ?? null;
196
+
197
+ const selectedDataPoint = useMemo(
198
+ () => dataPointItems.find(opt => opt.fullKey === condition.dataPointKey) ?? null,
199
+ [dataPointItems, condition.dataPointKey],
200
+ );
53
201
 
54
202
  const operatorItems: OperatorOption[] = useMemo(
55
203
  () =>
@@ -71,73 +219,95 @@ export function ConditionRow({
71
219
  onChange({ ...condition, value });
72
220
  };
73
221
 
222
+ const resetOperatorForFieldType = (nextFieldType: 'number' | 'string') => {
223
+ const nextOperatorItems = nextFieldType === 'number' ? NUMBER_OPERATORS : STRING_OPERATORS;
224
+ return !nextOperatorItems.some(op => op.value === condition.operator);
225
+ };
226
+
74
227
  const handleDataPointChange = (item: DataPointOption | null) => {
75
228
  const nextFieldType = item?.fieldType ?? 'string';
76
- const nextIsNumber = nextFieldType === 'number';
77
- const nextOperatorItems = nextIsNumber ? NUMBER_OPERATORS : STRING_OPERATORS;
78
- const operatorReset = !nextOperatorItems.some(op => op.value === condition.operator);
229
+ const operatorReset = resetOperatorForFieldType(nextFieldType);
79
230
  onChange({
80
231
  ...condition,
81
232
  dataPointKey: item?.fullKey ?? '',
233
+ formSnapshot: item?.formSnapshot,
82
234
  operator: operatorReset
83
- ? nextIsNumber
235
+ ? nextFieldType === 'number'
84
236
  ? 'num_eq'
85
237
  : 'is_equal_to'
86
238
  : condition.operator,
87
239
  });
88
240
  };
89
241
 
90
- return (
91
- <Flex direction="row" alignItems="flex-end" gap="2" flex={1}>
92
- <Flex
93
- alignItems="flex-end"
94
- style={{
95
- padding: '8px 12px',
96
- }}
97
- >
98
- <Chip label="IF" size="medium" />
99
- </Flex>
100
- <Flex flex={1}>
101
- <Combobox
102
- flex={1}
103
- itemToKey={(item: DataPointOption | null) => item?.fullKey ?? ''}
104
- itemToString={(item: DataPointOption | null) => item?.title ?? ''}
105
- items={dataPointOptions}
106
- selectedItem={selectedDataPoint}
107
- onChange={handleDataPointChange}
108
- >
109
- <Combobox.SelectTrigger label="Data point" placeholder="Select data point..." />
110
- <Combobox.Content>
111
- {({ items }: { items: DataPointOption[] }) => (
112
- <Combobox.List>
113
- {items.map((item, i) => (
114
- <Combobox.Item index={i} item={item} key={item.fullKey}>
115
- {item.title}
116
- </Combobox.Item>
117
- ))}
118
- </Combobox.List>
119
- )}
120
- </Combobox.Content>
121
- </Combobox>
242
+ const handleSourceKindChange = (item: SourceKindOption | null) => {
243
+ const next = item?.value;
244
+ if (!next) {
245
+ return;
246
+ }
247
+ setPendingSourceKind(next);
248
+ setPickerForm(null);
249
+ onChange({
250
+ ...condition,
251
+ dataPointKey: '',
252
+ formSnapshot: undefined,
253
+ operator: 'is_equal_to',
254
+ value: '',
255
+ });
256
+ };
257
+
258
+ const handlePickerFormChange = (form: FormInfo | null) => {
259
+ setPickerForm(form);
260
+ onChange({
261
+ ...condition,
262
+ dataPointKey: '',
263
+ formSnapshot: undefined,
264
+ operator: 'is_equal_to',
265
+ value: '',
266
+ });
267
+ if (form) {
268
+ onRequestFormFields?.(form.id);
269
+ }
270
+ };
271
+
272
+ const isLoadingFormFields =
273
+ pickerForm != null &&
274
+ formFieldsLoadingFormId != null &&
275
+ formFieldsLoadingFormId === pickerForm.id;
276
+
277
+ if (!sourceKindOptions.length) {
278
+ return (
279
+ <Flex direction="row" alignItems="center" gap="3" flex={1} style={{ flexWrap: 'wrap' }}>
280
+ <Text flex={1} size="small" subdued variant="body">
281
+ Add merge tags (data model), fillable fields on the document, or job forms to
282
+ build conditions.
283
+ </Text>
284
+ {canRemove && (
285
+ <Button
286
+ appearance="secondary"
287
+ aria-label="Remove condition"
288
+ icon={{ before: TrashIcon }}
289
+ onClick={onRemove}
290
+ />
291
+ )}
122
292
  </Flex>
123
- <Flex flex={1}>
293
+ );
294
+ }
295
+
296
+ return (
297
+ <Flex direction="column" gap="4" flex={1}>
298
+ <Flex flex={1} gap="2">
124
299
  <Combobox
125
300
  flex={1}
126
- itemToKey={(item: OperatorOption | null) => item?.value ?? ''}
127
- itemToString={(item: OperatorOption | null) => item?.label ?? ''}
128
- items={operatorItems}
129
- selectedItem={selectedOperator}
130
- onChange={(item: OperatorOption | null) =>
131
- onChange({
132
- ...condition,
133
- operator: (item?.value ??
134
- 'is_equal_to') as DisplayConditionSingle['operator'],
135
- })
136
- }
301
+ itemToKey={(item: SourceKindOption | null) => item?.value ?? ''}
302
+ itemToString={(item: SourceKindOption | null) => item?.label ?? ''}
303
+ items={sourceKindOptions}
304
+ selectedItem={selectedSourceKindOption}
305
+ onChange={handleSourceKindChange}
306
+ filterOptions={{ keys: ['label'] }}
137
307
  >
138
- <Combobox.SelectTrigger label="Condition" placeholder="Select..." />
308
+ <Combobox.SelectTrigger label="Source" placeholder="Select source..." />
139
309
  <Combobox.Content>
140
- {({ items }: { items: OperatorOption[] }) => (
310
+ {({ items }: { items: SourceKindOption[] }) => (
141
311
  <Combobox.List>
142
312
  {items.map((item, i) => (
143
313
  <Combobox.Item index={i} item={item} key={item.value}>
@@ -148,35 +318,144 @@ export function ConditionRow({
148
318
  )}
149
319
  </Combobox.Content>
150
320
  </Combobox>
151
- </Flex>
152
- {!isValueLess && (
153
- <Flex flex={1}>
154
- <TextField
155
- label="Value"
321
+ {sourceKind === FieldTypeEnum.forms && (
322
+ <Combobox
156
323
  flex={1}
157
- value={condition.value}
158
- onChange={e => handleValueChange(e.target.value)}
159
- placeholder={fieldType === 'number' ? 'e.g. 1.5' : 'Enter value...'}
160
- aria-label="Value"
161
- {...(fieldType === 'number' && { inputMode: 'decimal' })}
162
- />
163
- </Flex>
164
- )}
165
- {canRemove && (
324
+ itemToKey={(item: FormInfo | null) => String(item?.id ?? '')}
325
+ itemToString={(item: FormInfo | null) => item?.name ?? ''}
326
+ items={sortedForms}
327
+ selectedItem={pickerForm}
328
+ onChange={handlePickerFormChange}
329
+ filterOptions={{ keys: ['name'] }}
330
+ >
331
+ <Combobox.SelectTrigger label="Select Form" placeholder="Select form..." />
332
+ <Combobox.Content>
333
+ {({ items }: { items: FormInfo[] }) => (
334
+ <Combobox.List>
335
+ {items.map((item, i) => (
336
+ <Combobox.Item index={i} item={item} key={item.id}>
337
+ {item.name}
338
+ </Combobox.Item>
339
+ ))}
340
+ </Combobox.List>
341
+ )}
342
+ </Combobox.Content>
343
+ </Combobox>
344
+ )}
345
+ </Flex>
346
+
347
+ <Flex
348
+ direction="row"
349
+ alignItems="flex-end"
350
+ gap="2"
351
+ flex={1}
352
+ style={{ flexWrap: 'wrap' }}
353
+ >
166
354
  <Flex
167
355
  alignItems="flex-end"
168
356
  style={{
169
- padding: '6px 12px',
357
+ padding: '8px 12px',
170
358
  }}
171
359
  >
172
- <Button
173
- appearance="secondary"
174
- aria-label="Remove condition"
175
- icon={{ before: TrashIcon }}
176
- onClick={onRemove}
177
- />
360
+ <Chip label="IF" size="medium" />
361
+ </Flex>
362
+
363
+ <Flex flex={1} style={{ minWidth: 160 }}>
364
+ <Combobox
365
+ flex={1}
366
+ disabled={
367
+ sourceKind === FieldTypeEnum.forms &&
368
+ (!pickerForm || isLoadingFormFields)
369
+ }
370
+ itemToKey={(item: DataPointOption | null) => item?.fullKey ?? ''}
371
+ itemToString={(item: DataPointOption | null) => item?.title ?? ''}
372
+ items={dataPointItems}
373
+ selectedItem={selectedDataPoint}
374
+ onChange={handleDataPointChange}
375
+ filterOptions={{ keys: ['title'] }}
376
+ >
377
+ <Combobox.SelectTrigger
378
+ label="Data point"
379
+ placeholder={
380
+ sourceKind === FieldTypeEnum.forms && isLoadingFormFields
381
+ ? 'Loading fields...'
382
+ : sourceKind === FieldTypeEnum.forms && !pickerForm
383
+ ? 'Select a form first...'
384
+ : 'Select data point...'
385
+ }
386
+ />
387
+ <Combobox.Content>
388
+ {({ items }: { items: DataPointOption[] }) => (
389
+ <Combobox.List>
390
+ {items.map((item, i) => (
391
+ <Combobox.Item index={i} item={item} key={item.fullKey}>
392
+ {item.title}
393
+ </Combobox.Item>
394
+ ))}
395
+ </Combobox.List>
396
+ )}
397
+ </Combobox.Content>
398
+ </Combobox>
178
399
  </Flex>
179
- )}
400
+ <Flex flex={1} style={{ minWidth: 140 }}>
401
+ <Combobox
402
+ flex={1}
403
+ itemToKey={(item: OperatorOption | null) => item?.value ?? ''}
404
+ itemToString={(item: OperatorOption | null) => item?.label ?? ''}
405
+ items={operatorItems}
406
+ selectedItem={selectedOperator}
407
+ onChange={(item: OperatorOption | null) =>
408
+ onChange({
409
+ ...condition,
410
+ operator: (item?.value ??
411
+ 'is_equal_to') as DisplayConditionSingle['operator'],
412
+ })
413
+ }
414
+ filterOptions={{ keys: ['label'] }}
415
+ >
416
+ <Combobox.SelectTrigger label="Condition" placeholder="Select..." />
417
+ <Combobox.Content>
418
+ {({ items }: { items: OperatorOption[] }) => (
419
+ <Combobox.List>
420
+ {items.map((item, i) => (
421
+ <Combobox.Item index={i} item={item} key={item.value}>
422
+ {item.label}
423
+ </Combobox.Item>
424
+ ))}
425
+ </Combobox.List>
426
+ )}
427
+ </Combobox.Content>
428
+ </Combobox>
429
+ </Flex>
430
+ {!isValueLess && (
431
+ <Flex flex={1} style={{ minWidth: 120 }}>
432
+ <TextField
433
+ label="Value"
434
+ flex={1}
435
+ value={condition.value}
436
+ onChange={e => handleValueChange(e.target.value)}
437
+ placeholder={fieldType === 'number' ? 'e.g. 1.5' : 'Enter value...'}
438
+ aria-label="Value"
439
+ {...(fieldType === 'number' && { inputMode: 'decimal' })}
440
+ />
441
+ </Flex>
442
+ )}
443
+ {canRemove && (
444
+ <Flex
445
+ alignItems="flex-end"
446
+ style={{
447
+ padding: '6px 12px',
448
+ }}
449
+ >
450
+ <Button
451
+ appearance="secondary"
452
+ aria-label="Remove condition"
453
+ icon={{ before: TrashIcon }}
454
+ onClick={onRemove}
455
+ />
456
+ </Flex>
457
+ )}
458
+ </Flex>
180
459
  </Flex>
181
460
  );
182
461
  }
@@ -1,8 +1,19 @@
1
1
  import { Button, Chip, Dialog, Flex, SegmentedControl, Text } from '@servicetitan/anvil2';
2
2
  import { useCallback, useMemo, useState } from 'react';
3
3
  import { defaultGroup, defaultState, MODAL_CONTENT_MAX_HEIGHT } from '../../constants';
4
- import type { DisplayConditionState, PdfField, SchemaObject } from '../../interface/types';
5
- import { getDataPointOptions, isValidNumber } from '../../utils';
4
+ import type {
5
+ DisplayConditionState,
6
+ FormFieldsByFormIdI,
7
+ FormInfo,
8
+ PdfField,
9
+ SchemaObject,
10
+ } from '../../interface/types';
11
+ import {
12
+ getDisplayConditionFieldTypeForKey,
13
+ getDocumentFieldsDataPointOptions,
14
+ getSchemaDataPointOptions,
15
+ isValidNumber,
16
+ } from '../../utils';
6
17
  import { ConditionGroupsSection } from './condition-groups-section';
7
18
 
8
19
  type ConditionMatchType = 'and' | 'or';
@@ -53,12 +64,20 @@ export interface DisplayConditionModalProps {
53
64
  schema?: SchemaObject;
54
65
  /** All document fields: fillable fields are included as data points. */
55
66
  documentFields?: PdfField[];
67
+ forms?: FormInfo[];
68
+ formFieldsByFormId?: FormFieldsByFormIdI;
69
+ onRequestFormFields?: (formId: number) => void;
70
+ formFieldsLoadingFormId?: number | null;
56
71
  }
57
72
 
58
73
  export function DisplayConditionModal({
59
74
  documentFields,
75
+ formFieldsByFormId,
76
+ formFieldsLoadingFormId,
77
+ forms,
60
78
  initialState,
61
79
  onClose,
80
+ onRequestFormFields,
62
81
  onSave,
63
82
  schema,
64
83
  }: DisplayConditionModalProps) {
@@ -69,9 +88,11 @@ export function DisplayConditionModal({
69
88
  deriveConditionMatchType(initialState?.groups?.length ? initialState : defaultState()),
70
89
  );
71
90
 
72
- const dataPointOptions = useMemo(
73
- () => getDataPointOptions(schema, documentFields),
74
- [schema, documentFields],
91
+ const mergeTagOptions = useMemo(() => getSchemaDataPointOptions(schema), [schema]);
92
+
93
+ const fillableOptions = useMemo(
94
+ () => getDocumentFieldsDataPointOptions(documentFields),
95
+ [documentFields],
75
96
  );
76
97
 
77
98
  const handleClose = useCallback(() => {
@@ -151,16 +172,19 @@ export function DisplayConditionModal({
151
172
  if (!trimmedValue) {
152
173
  return false;
153
174
  }
154
- const fieldType =
155
- dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey)
156
- ?.fieldType ?? 'string';
175
+ const fieldType = getDisplayConditionFieldTypeForKey(
176
+ condition.dataPointKey,
177
+ mergeTagOptions,
178
+ fillableOptions,
179
+ formFieldsByFormId ?? {},
180
+ );
157
181
  if (fieldType === 'number' && !isValidNumber(trimmedValue)) {
158
182
  return false;
159
183
  }
160
184
  }
161
185
  }
162
186
  return state.groups.length > 0;
163
- }, [dataPointOptions, state.groups]);
187
+ }, [fillableOptions, formFieldsByFormId, mergeTagOptions, state.groups]);
164
188
 
165
189
  return (
166
190
  <Dialog open onClose={handleClose} size="xlarge">
@@ -221,10 +245,15 @@ export function DisplayConditionModal({
221
245
  </Flex>
222
246
  <ConditionGroupsSection
223
247
  conditionJoinOperator={matchType}
224
- dataPointOptions={dataPointOptions}
248
+ fillableOptions={fillableOptions}
249
+ formFieldsByFormId={formFieldsByFormId}
250
+ formFieldsLoadingFormId={formFieldsLoadingFormId}
251
+ forms={forms}
225
252
  groups={state.groups}
253
+ mergeTagOptions={mergeTagOptions}
226
254
  onAddGroup={addGroup}
227
255
  onRemoveGroup={removeGroup}
256
+ onRequestFormFields={onRequestFormFields}
228
257
  onUpdateGroup={updateGroup}
229
258
  />
230
259
  </Flex>
@@ -5,13 +5,26 @@ import { PdfField } from '../../interface/types';
5
5
  import { DisplayConditionModal, DisplayConditionModalProps } from './display-condition-modal';
6
6
 
7
7
  interface DisplayConditionProps
8
- extends Pick<DisplayConditionModalProps, 'initialState' | 'schema' | 'documentFields'> {
8
+ extends Pick<
9
+ DisplayConditionModalProps,
10
+ | 'documentFields'
11
+ | 'formFieldsByFormId'
12
+ | 'formFieldsLoadingFormId'
13
+ | 'forms'
14
+ | 'initialState'
15
+ | 'onRequestFormFields'
16
+ | 'schema'
17
+ > {
9
18
  onSave: (state: Partial<PdfField>) => void;
10
19
  }
11
20
 
12
21
  export const DisplayCondition: FC<DisplayConditionProps> = ({
13
22
  documentFields,
23
+ formFieldsByFormId,
24
+ formFieldsLoadingFormId,
25
+ forms,
14
26
  initialState,
27
+ onRequestFormFields,
15
28
  onSave,
16
29
  schema,
17
30
  }) => {
@@ -31,9 +44,13 @@ export const DisplayCondition: FC<DisplayConditionProps> = ({
31
44
  <DisplayConditionModal
32
45
  onClose={() => setOpen(false)}
33
46
  onSave={state => onSave({ displayCondition: state ?? undefined })}
47
+ documentFields={documentFields}
48
+ formFieldsByFormId={formFieldsByFormId}
49
+ formFieldsLoadingFormId={formFieldsLoadingFormId}
50
+ forms={forms}
34
51
  initialState={initialState}
52
+ onRequestFormFields={onRequestFormFields}
35
53
  schema={schema}
36
- documentFields={documentFields}
37
54
  />
38
55
  )}
39
56
  </Fragment>