@servicetitan/dte-pdf-editor 1.20.0 → 1.22.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 (121) hide show
  1. package/dist/components/display-conditions/condition-group.d.ts +11 -0
  2. package/dist/components/display-conditions/condition-group.d.ts.map +1 -0
  3. package/dist/components/display-conditions/condition-group.js +52 -0
  4. package/dist/components/display-conditions/condition-group.js.map +1 -0
  5. package/dist/components/display-conditions/condition-groups-section.d.ts +10 -0
  6. package/dist/components/display-conditions/condition-groups-section.d.ts.map +1 -0
  7. package/dist/components/display-conditions/condition-groups-section.js +14 -0
  8. package/dist/components/display-conditions/condition-groups-section.js.map +1 -0
  9. package/dist/components/display-conditions/condition-row.d.ts +10 -0
  10. package/dist/components/display-conditions/condition-row.d.ts.map +1 -0
  11. package/dist/components/display-conditions/condition-row.js +60 -0
  12. package/dist/components/display-conditions/condition-row.js.map +1 -0
  13. package/dist/components/display-conditions/display-condition-modal.d.ts +12 -0
  14. package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -0
  15. package/dist/components/display-conditions/display-condition-modal.js +89 -0
  16. package/dist/components/display-conditions/display-condition-modal.js.map +1 -0
  17. package/dist/components/display-conditions/display-condition.d.ts +9 -0
  18. package/dist/components/display-conditions/display-condition.d.ts.map +1 -0
  19. package/dist/components/display-conditions/display-condition.js +10 -0
  20. package/dist/components/display-conditions/display-condition.js.map +1 -0
  21. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  22. package/dist/components/field-config-panel/advanced-settings.js +10 -8
  23. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  24. package/dist/components/field-config-panel/field-config-panel.d.ts +1 -1
  25. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  26. package/dist/components/field-config-panel/field-config-panel.js +4 -2
  27. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  28. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  29. package/dist/components/field-config-panel/formula-generator.js +3 -20
  30. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  31. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  32. package/dist/components/field-config-panel/formula-modal.js +9 -3
  33. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  34. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  35. package/dist/components/field-config-panel/formula-workspace.js +4 -3
  36. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  37. package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -1
  38. package/dist/components/field-config-panel/result-type-selector.js +2 -2
  39. package/dist/components/field-config-panel/result-type-selector.js.map +1 -1
  40. package/dist/components/pdf-view/pdf-view-field-container.d.ts +2 -1
  41. package/dist/components/pdf-view/pdf-view-field-container.d.ts.map +1 -1
  42. package/dist/components/pdf-view/pdf-view-field-container.js +6 -2
  43. package/dist/components/pdf-view/pdf-view-field-container.js.map +1 -1
  44. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  45. package/dist/components/pdf-view/pdf-view.js +1 -1
  46. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  47. package/dist/constants/calculated.constants.d.ts +2 -0
  48. package/dist/constants/calculated.constants.d.ts.map +1 -0
  49. package/dist/constants/calculated.constants.js +2 -0
  50. package/dist/constants/calculated.constants.js.map +1 -0
  51. package/dist/constants/conditions.constants.d.ts +6 -0
  52. package/dist/constants/conditions.constants.d.ts.map +1 -0
  53. package/dist/constants/conditions.constants.js +17 -0
  54. package/dist/constants/conditions.constants.js.map +1 -0
  55. package/dist/constants/index.d.ts +3 -0
  56. package/dist/constants/index.d.ts.map +1 -1
  57. package/dist/constants/index.js +3 -0
  58. package/dist/constants/index.js.map +1 -1
  59. package/dist/interface/types.d.ts +78 -1
  60. package/dist/interface/types.d.ts.map +1 -1
  61. package/dist/interface/types.js +25 -0
  62. package/dist/interface/types.js.map +1 -1
  63. package/dist/utils/conditions/evaluate.utils.d.ts +6 -0
  64. package/dist/utils/conditions/evaluate.utils.d.ts.map +1 -0
  65. package/dist/utils/conditions/evaluate.utils.js +95 -0
  66. package/dist/utils/conditions/evaluate.utils.js.map +1 -0
  67. package/dist/utils/conditions/index.d.ts +3 -0
  68. package/dist/utils/conditions/index.d.ts.map +1 -0
  69. package/dist/utils/conditions/index.js +3 -0
  70. package/dist/utils/conditions/index.js.map +1 -0
  71. package/dist/utils/conditions/schema-data-points.utils.d.ts +12 -0
  72. package/dist/utils/conditions/schema-data-points.utils.d.ts.map +1 -0
  73. package/dist/utils/conditions/schema-data-points.utils.js +67 -0
  74. package/dist/utils/conditions/schema-data-points.utils.js.map +1 -0
  75. package/dist/utils/data-model/extract-fields.utils.d.ts +1 -0
  76. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  77. package/dist/utils/data-model/extract-fields.utils.js +6 -3
  78. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  79. package/dist/utils/formula/render-formula.utils.d.ts +0 -6
  80. package/dist/utils/formula/render-formula.utils.d.ts.map +1 -1
  81. package/dist/utils/formula/render-formula.utils.js +0 -17
  82. package/dist/utils/formula/render-formula.utils.js.map +1 -1
  83. package/dist/utils/index.d.ts +2 -0
  84. package/dist/utils/index.d.ts.map +1 -1
  85. package/dist/utils/index.js +2 -0
  86. package/dist/utils/index.js.map +1 -1
  87. package/dist/utils/shared/index.d.ts +2 -0
  88. package/dist/utils/shared/index.d.ts.map +1 -0
  89. package/dist/utils/shared/index.js +2 -0
  90. package/dist/utils/shared/index.js.map +1 -0
  91. package/dist/utils/shared/number.utils.d.ts +2 -0
  92. package/dist/utils/shared/number.utils.d.ts.map +1 -0
  93. package/dist/utils/shared/number.utils.js +12 -0
  94. package/dist/utils/shared/number.utils.js.map +1 -0
  95. package/package.json +1 -1
  96. package/src/components/display-conditions/condition-group.tsx +141 -0
  97. package/src/components/display-conditions/condition-groups-section.tsx +63 -0
  98. package/src/components/display-conditions/condition-row.tsx +182 -0
  99. package/src/components/display-conditions/display-condition-modal.tsx +180 -0
  100. package/src/components/display-conditions/display-condition.tsx +41 -0
  101. package/src/components/field-config-panel/advanced-settings.tsx +42 -46
  102. package/src/components/field-config-panel/field-config-panel.tsx +12 -2
  103. package/src/components/field-config-panel/formula-generator.tsx +9 -44
  104. package/src/components/field-config-panel/formula-modal.tsx +72 -82
  105. package/src/components/field-config-panel/formula-workspace.tsx +6 -5
  106. package/src/components/field-config-panel/result-type-selector.tsx +8 -11
  107. package/src/components/pdf-view/pdf-view-field-container.tsx +11 -2
  108. package/src/components/pdf-view/pdf-view.tsx +1 -0
  109. package/src/constants/calculated.constants.ts +1 -0
  110. package/src/constants/conditions.constants.ts +26 -0
  111. package/src/constants/index.ts +3 -0
  112. package/src/interface/types.ts +64 -1
  113. package/src/styles/formula-modal.css +1 -155
  114. package/src/utils/conditions/evaluate.utils.ts +134 -0
  115. package/src/utils/conditions/index.ts +2 -0
  116. package/src/utils/conditions/schema-data-points.utils.ts +93 -0
  117. package/src/utils/data-model/extract-fields.utils.ts +12 -7
  118. package/src/utils/formula/render-formula.utils.ts +0 -19
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/shared/index.ts +1 -0
  121. package/src/utils/shared/number.utils.ts +13 -0
@@ -0,0 +1,182 @@
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 {
5
+ ConditionOperator,
6
+ DataPointOption,
7
+ DisplayConditionSingle,
8
+ NUMBER_OPERATORS,
9
+ STRING_OPERATORS,
10
+ VALUE_LESS_OPERATORS,
11
+ } from '../../interface/types';
12
+
13
+ export interface ConditionRowProps {
14
+ canRemove: boolean;
15
+ condition: DisplayConditionSingle;
16
+ dataPointOptions: DataPointOption[];
17
+ onChange: (c: DisplayConditionSingle) => void;
18
+ onRemove: () => void;
19
+ }
20
+
21
+ interface OperatorOption {
22
+ label: string;
23
+ value: string;
24
+ }
25
+
26
+ function sanitizeNumericInput(raw: string): string {
27
+ let value = raw.replaceAll(/[^0-9.-]/g, '');
28
+ const isNegative = value.startsWith('-');
29
+ value = value.replaceAll('-', '');
30
+ if (isNegative) {
31
+ value = `-${value}`;
32
+ }
33
+ const firstDot = value.indexOf('.');
34
+ if (firstDot >= 0) {
35
+ value = `${value.slice(0, firstDot + 1)}${value.slice(firstDot + 1).replaceAll('.', '')}`;
36
+ }
37
+ return value;
38
+ }
39
+
40
+ export function ConditionRow({
41
+ canRemove,
42
+ condition,
43
+ dataPointOptions,
44
+ onChange,
45
+ onRemove,
46
+ }: Readonly<ConditionRowProps>) {
47
+ const selectedDataPoint = useMemo(
48
+ () => dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey) ?? null,
49
+ [dataPointOptions, condition.dataPointKey],
50
+ );
51
+
52
+ const fieldType = selectedDataPoint?.fieldType ?? 'string';
53
+
54
+ const operatorItems: OperatorOption[] = useMemo(
55
+ () =>
56
+ fieldType === 'number'
57
+ ? ([...NUMBER_OPERATORS] as OperatorOption[])
58
+ : ([...STRING_OPERATORS] as OperatorOption[]),
59
+ [fieldType],
60
+ );
61
+
62
+ const selectedOperator = useMemo(
63
+ () => operatorItems.find(op => op.value === condition.operator) ?? null,
64
+ [operatorItems, condition.operator],
65
+ );
66
+
67
+ const isValueLess = VALUE_LESS_OPERATORS.includes(condition.operator as ConditionOperator);
68
+
69
+ const handleValueChange = (raw: string) => {
70
+ const value = fieldType === 'number' ? sanitizeNumericInput(raw) : raw;
71
+ onChange({ ...condition, value });
72
+ };
73
+
74
+ const handleDataPointChange = (item: DataPointOption | null) => {
75
+ 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);
79
+ onChange({
80
+ ...condition,
81
+ dataPointKey: item?.fullKey ?? '',
82
+ operator: operatorReset
83
+ ? nextIsNumber
84
+ ? 'num_eq'
85
+ : 'is_equal_to'
86
+ : condition.operator,
87
+ });
88
+ };
89
+
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>
122
+ </Flex>
123
+ <Flex flex={1}>
124
+ <Combobox
125
+ 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
+ }
137
+ >
138
+ <Combobox.SelectTrigger label="Condition" placeholder="Select..." />
139
+ <Combobox.Content>
140
+ {({ items }: { items: OperatorOption[] }) => (
141
+ <Combobox.List>
142
+ {items.map((item, i) => (
143
+ <Combobox.Item index={i} item={item} key={item.value}>
144
+ {item.label}
145
+ </Combobox.Item>
146
+ ))}
147
+ </Combobox.List>
148
+ )}
149
+ </Combobox.Content>
150
+ </Combobox>
151
+ </Flex>
152
+ {!isValueLess && (
153
+ <Flex flex={1}>
154
+ <TextField
155
+ label="Value"
156
+ 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 && (
166
+ <Flex
167
+ alignItems="flex-end"
168
+ style={{
169
+ padding: '6px 12px',
170
+ }}
171
+ >
172
+ <Button
173
+ appearance="secondary"
174
+ aria-label="Remove condition"
175
+ icon={{ before: TrashIcon }}
176
+ onClick={onRemove}
177
+ />
178
+ </Flex>
179
+ )}
180
+ </Flex>
181
+ );
182
+ }
@@ -0,0 +1,180 @@
1
+ import { Button, Dialog, Divider, Flex, SegmentedControl, Text } from '@servicetitan/anvil2';
2
+ import { useCallback, useMemo, useState } from 'react';
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';
6
+ import { ConditionGroupsSection } from './condition-groups-section';
7
+
8
+ const BEHAVIOR_OPTIONS = [
9
+ { label: 'Show', value: 'show' },
10
+ { label: 'Hide', value: 'hide' },
11
+ ] as const;
12
+
13
+ export interface DisplayConditionModalProps {
14
+ onClose: () => void;
15
+ onSave: (state: DisplayConditionState | null) => void;
16
+ initialState: DisplayConditionState | null | undefined;
17
+ /** Data model schema (for schema-backed data points). */
18
+ schema?: SchemaObject;
19
+ /** All document fields: fillable fields are included as data points. */
20
+ documentFields?: PdfField[];
21
+ }
22
+
23
+ export function DisplayConditionModal({
24
+ documentFields,
25
+ initialState,
26
+ onClose,
27
+ onSave,
28
+ schema,
29
+ }: DisplayConditionModalProps) {
30
+ const [state, setState] = useState<DisplayConditionState>(() =>
31
+ initialState?.groups?.length ? initialState : defaultState(),
32
+ );
33
+
34
+ const dataPointOptions = useMemo(
35
+ () => getDataPointOptions(schema, documentFields),
36
+ [schema, documentFields],
37
+ );
38
+
39
+ const handleClose = useCallback(() => {
40
+ onClose();
41
+ }, [onClose]);
42
+
43
+ const handleSave = useCallback(() => {
44
+ const hasValidGroup = state.groups.some(group =>
45
+ group.conditions.some(
46
+ c =>
47
+ c.dataPointKey &&
48
+ (c.operator === 'is_empty' ||
49
+ c.operator === 'is_not_empty' ||
50
+ c.value.trim() !== ''),
51
+ ),
52
+ );
53
+ if (hasValidGroup) {
54
+ onSave(state);
55
+ } else {
56
+ onSave(null);
57
+ }
58
+ onClose();
59
+ }, [onSave, state, onClose]);
60
+
61
+ const updateGroup = useCallback((index: number, group: DisplayConditionState['groups'][0]) => {
62
+ setState(prev => {
63
+ const next = [...prev.groups];
64
+ next[index] = group;
65
+ return { ...prev, groups: next };
66
+ });
67
+ }, []);
68
+
69
+ const removeGroup = useCallback((index: number) => {
70
+ setState(prev => ({
71
+ ...prev,
72
+ groups: (() => {
73
+ const nextGroups = prev.groups.filter((_, i) => i !== index);
74
+ if (nextGroups.length === 0) {
75
+ return [defaultGroup()];
76
+ }
77
+ if (index === 0 && nextGroups[0]) {
78
+ const { logicalOperator: omit, ...firstGroup } = nextGroups[0];
79
+ return [firstGroup, ...nextGroups.slice(1)];
80
+ }
81
+ return nextGroups;
82
+ })(),
83
+ }));
84
+ }, []);
85
+
86
+ const addGroup = useCallback(() => {
87
+ setState(prev => ({
88
+ ...prev,
89
+ groups: [...prev.groups, { ...defaultGroup(), logicalOperator: 'and' }],
90
+ }));
91
+ }, []);
92
+
93
+ const handleBehaviorChange = useCallback((behavior: DisplayConditionState['behavior']) => {
94
+ setState(prev => ({ ...prev, behavior }));
95
+ }, []);
96
+
97
+ const canSave = useMemo(() => {
98
+ for (const group of state.groups) {
99
+ for (const condition of group.conditions) {
100
+ if (!condition.dataPointKey) {
101
+ return false;
102
+ }
103
+ if (condition.operator === 'is_empty' || condition.operator === 'is_not_empty') {
104
+ continue;
105
+ }
106
+ const trimmedValue = condition.value.trim();
107
+ if (!trimmedValue) {
108
+ return false;
109
+ }
110
+ const fieldType =
111
+ dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey)
112
+ ?.fieldType ?? 'string';
113
+ if (fieldType === 'number' && !isValidNumber(trimmedValue)) {
114
+ return false;
115
+ }
116
+ }
117
+ }
118
+ return state.groups.length > 0;
119
+ }, [dataPointOptions, state.groups]);
120
+
121
+ return (
122
+ <Dialog open onClose={handleClose} size="xlarge">
123
+ <Dialog.Header>Rules and Conditions</Dialog.Header>
124
+ <Dialog.Content>
125
+ <Flex
126
+ direction="column"
127
+ gap="6"
128
+ flex={1}
129
+ style={{
130
+ maxHeight: MODAL_CONTENT_MAX_HEIGHT,
131
+ overflowY: 'auto',
132
+ }}
133
+ >
134
+ <Flex direction="column" gap="3">
135
+ <Text size="medium" variant="body" style={{ fontWeight: 'bold' }}>
136
+ When conditions are met:
137
+ </Text>
138
+ <Flex direction="row" alignItems="center" gap="3">
139
+ <Text size="small" subdued variant="body">
140
+ Select to show or hide this field.
141
+ </Text>
142
+
143
+ <SegmentedControl
144
+ size="small"
145
+ selected={state.behavior}
146
+ onChange={handleBehaviorChange}
147
+ >
148
+ {BEHAVIOR_OPTIONS.map(opt => (
149
+ <SegmentedControl.Segment key={opt.value} value={opt.value}>
150
+ {opt.label}
151
+ </SegmentedControl.Segment>
152
+ ))}
153
+ </SegmentedControl>
154
+ </Flex>
155
+ </Flex>
156
+ <Divider />
157
+ <ConditionGroupsSection
158
+ dataPointOptions={dataPointOptions}
159
+ groups={state.groups}
160
+ onAddGroup={addGroup}
161
+ onRemoveGroup={removeGroup}
162
+ onUpdateGroup={updateGroup}
163
+ />
164
+ {!canSave && (
165
+ <Text size="small" subdued variant="body">
166
+ Complete each condition before saving. Data point and value are
167
+ required, and numeric fields must contain a valid number.
168
+ </Text>
169
+ )}
170
+ </Flex>
171
+ </Dialog.Content>
172
+ <Dialog.Footer sticky>
173
+ <Dialog.CancelButton onClick={handleClose}>Cancel</Dialog.CancelButton>
174
+ <Button appearance="primary" disabled={!canSave} onClick={handleSave}>
175
+ Save Changes
176
+ </Button>
177
+ </Dialog.Footer>
178
+ </Dialog>
179
+ );
180
+ }
@@ -0,0 +1,41 @@
1
+ import { Button } from '@servicetitan/anvil2';
2
+ import PlusIcon from '@servicetitan/anvil2/assets/icons/material/round/add.svg';
3
+ import { FC, Fragment, useState } from 'react';
4
+ import { PdfField } from '../../interface/types';
5
+ import { DisplayConditionModal, DisplayConditionModalProps } from './display-condition-modal';
6
+
7
+ interface DisplayConditionProps
8
+ extends Pick<DisplayConditionModalProps, 'initialState' | 'schema' | 'documentFields'> {
9
+ onSave: (state: Partial<PdfField>) => void;
10
+ }
11
+
12
+ export const DisplayCondition: FC<DisplayConditionProps> = ({
13
+ documentFields,
14
+ initialState,
15
+ onSave,
16
+ schema,
17
+ }) => {
18
+ const [open, setOpen] = useState(false);
19
+
20
+ return (
21
+ <Fragment>
22
+ <Button
23
+ appearance="secondary"
24
+ className="full-width"
25
+ icon={PlusIcon}
26
+ onClick={() => setOpen(true)}
27
+ >
28
+ Add Display Condition
29
+ </Button>
30
+ {open && (
31
+ <DisplayConditionModal
32
+ onClose={() => setOpen(false)}
33
+ onSave={state => onSave({ displayCondition: state ?? undefined })}
34
+ initialState={initialState}
35
+ schema={schema}
36
+ documentFields={documentFields}
37
+ />
38
+ )}
39
+ </Fragment>
40
+ );
41
+ };
@@ -1,4 +1,4 @@
1
- import { Button, Flex, Text } from '@servicetitan/anvil2';
1
+ import { Flex, SegmentedControl, Switch, Text } from '@servicetitan/anvil2';
2
2
  import { Dispatch, FC, SetStateAction } from 'react';
3
3
  import { CalculatedFieldFormat } from '../../interface/types';
4
4
 
@@ -18,23 +18,21 @@ export const AdvancedSettings: FC<AdvancedSettingsProps> = ({ format, setFormat
18
18
  <Text variant="body" size="small">
19
19
  Rounding mode
20
20
  </Text>
21
- <Flex gap={1}>
22
- {ROUNDING_OPTIONS.map(({ label, value }) => (
23
- <Button
24
- key={value}
25
- appearance={format.roundingMode === value ? 'primary' : 'secondary'}
26
- size="small"
27
- onClick={() => setFormat(prev => ({ ...prev, roundingMode: value }))}
28
- >
29
- {label}
30
- </Button>
31
- ))}
32
- </Flex>
33
- <Flex
34
- alignItems="center"
35
- justifyContent="space-between"
36
- className="dte-formula-advanced-row"
21
+ <SegmentedControl
22
+ size="medium"
23
+ selected={format.roundingMode}
24
+ onChange={(roundingMode: CalculatedFieldFormat['roundingMode']) =>
25
+ setFormat(prev => ({ ...prev, roundingMode }))
26
+ }
27
+ fill
37
28
  >
29
+ {ROUNDING_OPTIONS.map(opt => (
30
+ <SegmentedControl.Segment key={opt.value} value={opt.value}>
31
+ {opt.label}
32
+ </SegmentedControl.Segment>
33
+ ))}
34
+ </SegmentedControl>
35
+ <Flex alignItems="center" justifyContent="space-between">
38
36
  <Text variant="body" size="small">
39
37
  Decimal places
40
38
  </Text>
@@ -44,7 +42,6 @@ export const AdvancedSettings: FC<AdvancedSettingsProps> = ({ format, setFormat
44
42
  </Flex>
45
43
  <input
46
44
  type="range"
47
- className="dte-formula-advanced-range"
48
45
  min={0}
49
46
  max={10}
50
47
  value={format.decimals}
@@ -55,36 +52,35 @@ export const AdvancedSettings: FC<AdvancedSettingsProps> = ({ format, setFormat
55
52
  }))
56
53
  }
57
54
  />
58
- <label className="dte-formula-advanced-checkbox">
59
- <input
60
- type="checkbox"
61
- checked={format.thousandsSeparator}
62
- onChange={e =>
63
- setFormat(prev => ({
64
- ...prev,
65
- thousandsSeparator: e.target.checked,
66
- decimalSeparatorEnabled: e.target.checked,
67
- decimalSeparator: e.target.checked ? prev.decimalSeparator : '.',
68
- }))
69
- }
70
- />
71
- <Text variant="body" size="small">
72
- Thousands separator
73
- </Text>
74
- </label>
55
+ <Switch
56
+ checked={format.thousandsSeparator}
57
+ onChange={(_e, { checked }) => {
58
+ setFormat(prev => ({
59
+ ...prev,
60
+ thousandsSeparator: checked,
61
+ decimalSeparatorEnabled: checked,
62
+ decimalSeparator: checked ? prev.decimalSeparator : '.',
63
+ }));
64
+ }}
65
+ label="Thousands separator"
66
+ value="switch-example"
67
+ />
68
+
75
69
  {format.thousandsSeparator && (
76
- <Flex gap={2}>
77
- {(['.', ','] as const).map(sep => (
78
- <Button
79
- key={sep}
80
- appearance={format.decimalSeparator === sep ? 'primary' : 'secondary'}
81
- size="small"
82
- onClick={() => setFormat(prev => ({ ...prev, decimalSeparator: sep }))}
83
- >
84
- {sep}
85
- </Button>
70
+ <SegmentedControl
71
+ size="medium"
72
+ fill
73
+ selected={format.decimalSeparator}
74
+ onChange={(decimalSeparator: CalculatedFieldFormat['decimalSeparator']) =>
75
+ setFormat(prev => ({ ...prev, decimalSeparator }))
76
+ }
77
+ >
78
+ {['.', ','].map(decimalSeparator => (
79
+ <SegmentedControl.Segment key={decimalSeparator} value={decimalSeparator}>
80
+ {decimalSeparator}
81
+ </SegmentedControl.Segment>
86
82
  ))}
87
- </Flex>
83
+ </SegmentedControl>
88
84
  )}
89
85
  <Text variant="body" size="small">
90
86
  Result affixes
@@ -9,12 +9,13 @@ import {
9
9
  SchemaObject,
10
10
  } from '../../interface/types';
11
11
  import { generateESignPath, generateFillablePath } from '../../utils';
12
+ import { DisplayCondition } from '../display-conditions/display-condition';
12
13
  import { FormulaGenerator } from './formula-generator';
13
14
 
14
15
  interface FieldConfigPanelProps {
15
16
  field: PdfField;
16
17
  dataModel?: SchemaObject;
17
- /** All fields on the document (e.g. for formula builder fillable section). */
18
+ /** All fields on the document (e.g., for a formula builder fillable section). */
18
19
  documentFields?: PdfField[];
19
20
  recipients?: RecipientInfo[];
20
21
  onDeleteField(): void;
@@ -158,6 +159,14 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
158
159
  }
159
160
  />
160
161
  )}
162
+
163
+ <DisplayCondition
164
+ onSave={onFieldConfigChange}
165
+ documentFields={documentFields}
166
+ schema={dataModel}
167
+ initialState={field.displayCondition ?? undefined}
168
+ />
169
+
161
170
  <Textarea
162
171
  label="Description"
163
172
  value={field.description}
@@ -167,7 +176,8 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
167
176
  })
168
177
  }
169
178
  />
170
- <Button className="full-width" onClick={onDeleteField}>
179
+
180
+ <Button className="full-width" onClick={onDeleteField} appearance="danger">
171
181
  Delete Field
172
182
  </Button>
173
183
  </Flex>
@@ -1,10 +1,10 @@
1
- import { Text } from '@servicetitan/anvil2';
1
+ import { Button } from '@servicetitan/anvil2';
2
+ import EditIcon from '@servicetitan/anvil2/assets/icons/material/round/edit.svg';
2
3
  import { FC, Fragment, useMemo, useState } from 'react';
3
4
  import {
4
5
  CalculatedFieldFormat,
5
6
  FieldTypeEnum,
6
7
  FieldTypeOption,
7
- FormulaToken,
8
8
  PdfField,
9
9
  PdfFieldSubType,
10
10
  SchemaObject,
@@ -57,56 +57,21 @@ export const FormulaGenerator: FC<FormulaGeneratorProps> = ({
57
57
  [documentFields],
58
58
  );
59
59
 
60
- const hasFormula = formula?.tokens && formula.tokens.length > 0;
61
-
62
60
  const handleSave = (newFormula: StructuredFormula, format: CalculatedFieldFormat) => {
63
61
  onFormulaChange?.(newFormula, format);
64
62
  };
65
63
 
66
64
  return (
67
65
  <Fragment>
68
- <Text variant="body" size="small">
69
- Formula
70
- </Text>
71
- <div
72
- className="dte-formula-box"
73
- role="button"
74
- tabIndex={0}
66
+ <Button
67
+ appearance="secondary"
68
+ className="full-width"
69
+ icon={EditIcon}
75
70
  onClick={() => setModalOpen(true)}
76
- onKeyDown={e => {
77
- if (e.key === 'Enter' || e.key === ' ') {
78
- e.preventDefault();
79
- setModalOpen(true);
80
- }
81
- }}
82
- aria-label="Configure formula"
83
71
  >
84
- {hasFormula ? (
85
- (() => {
86
- let offset = 0;
87
- return formula!.tokens.map((token: FormulaToken) => {
88
- const value =
89
- token.type === 'field'
90
- ? token.path
91
- : (token as { value: string }).value;
92
- const key = `formula-${offset}-${token.type}-${value}`;
93
- offset += value.length + 1;
94
- return (
95
- <span
96
- key={key}
97
- className={`dte-formula-token dte-formula-token-${token.type}`}
98
- >
99
- {token.type === 'field'
100
- ? token.label
101
- : (token as { value: string }).value}
102
- </span>
103
- );
104
- });
105
- })()
106
- ) : (
107
- <span>Configure formula</span>
108
- )}
109
- </div>
72
+ Configure formula
73
+ </Button>
74
+
110
75
  {modalOpen && (
111
76
  <FormulaModal
112
77
  initialFormula={formula}