@servicetitan/dte-pdf-editor 1.21.0 → 1.23.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/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 +5 -3
  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 +74 -0
  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.map +1 -1
  78. package/dist/utils/formula/render-formula.utils.d.ts +0 -6
  79. package/dist/utils/formula/render-formula.utils.d.ts.map +1 -1
  80. package/dist/utils/formula/render-formula.utils.js +0 -17
  81. package/dist/utils/formula/render-formula.utils.js.map +1 -1
  82. package/dist/utils/index.d.ts +2 -0
  83. package/dist/utils/index.d.ts.map +1 -1
  84. package/dist/utils/index.js +2 -0
  85. package/dist/utils/index.js.map +1 -1
  86. package/dist/utils/shared/index.d.ts +2 -0
  87. package/dist/utils/shared/index.d.ts.map +1 -0
  88. package/dist/utils/shared/index.js +2 -0
  89. package/dist/utils/shared/index.js.map +1 -0
  90. package/dist/utils/shared/number.utils.d.ts +2 -0
  91. package/dist/utils/shared/number.utils.d.ts.map +1 -0
  92. package/dist/utils/shared/number.utils.js +12 -0
  93. package/dist/utils/shared/number.utils.js.map +1 -0
  94. package/package.json +1 -1
  95. package/src/components/display-conditions/condition-group.tsx +141 -0
  96. package/src/components/display-conditions/condition-groups-section.tsx +63 -0
  97. package/src/components/display-conditions/condition-row.tsx +182 -0
  98. package/src/components/display-conditions/display-condition-modal.tsx +180 -0
  99. package/src/components/display-conditions/display-condition.tsx +41 -0
  100. package/src/components/field-config-panel/advanced-settings.tsx +42 -46
  101. package/src/components/field-config-panel/field-config-panel.tsx +13 -3
  102. package/src/components/field-config-panel/formula-generator.tsx +9 -44
  103. package/src/components/field-config-panel/formula-modal.tsx +72 -82
  104. package/src/components/field-config-panel/formula-workspace.tsx +6 -5
  105. package/src/components/field-config-panel/result-type-selector.tsx +8 -11
  106. package/src/components/pdf-view/pdf-view-field-container.tsx +11 -2
  107. package/src/components/pdf-view/pdf-view.tsx +1 -0
  108. package/src/constants/calculated.constants.ts +1 -0
  109. package/src/constants/conditions.constants.ts +26 -0
  110. package/src/constants/index.ts +3 -0
  111. package/src/interface/types.ts +59 -0
  112. package/src/styles/formula-modal.css +1 -155
  113. package/src/utils/conditions/evaluate.utils.ts +134 -0
  114. package/src/utils/conditions/index.ts +2 -0
  115. package/src/utils/conditions/schema-data-points.utils.ts +93 -0
  116. package/src/utils/data-model/extract-fields.utils.ts +1 -0
  117. package/src/utils/formula/render-formula.utils.ts +0 -19
  118. package/src/utils/index.ts +2 -0
  119. package/src/utils/shared/index.ts +1 -0
  120. package/src/utils/shared/number.utils.ts +13 -0
@@ -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;
@@ -79,7 +80,7 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
79
80
  field.type,
80
81
  ) && (
81
82
  <TextField
82
- required
83
+ required={field.type === FieldTypeEnum.fillable}
83
84
  label="Label"
84
85
  value={field.label}
85
86
  onChange={e =>
@@ -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}
@@ -1,8 +1,8 @@
1
- import { Button, Flex, Text } from '@servicetitan/anvil2';
2
- import IconClose from '@servicetitan/anvil2/assets/icons/material/round/close.svg';
1
+ import { Button, Dialog, Divider, Flex } from '@servicetitan/anvil2';
3
2
  import IconRedo from '@servicetitan/anvil2/assets/icons/material/round/redo.svg';
4
3
  import IconUndo from '@servicetitan/anvil2/assets/icons/material/round/undo.svg';
5
4
  import { FC, useCallback, useMemo, useState } from 'react';
5
+ import { MODAL_CONTENT_MAX_HEIGHT } from '../../constants';
6
6
  import { useFormulaEditor } from '../../hooks';
7
7
  import {
8
8
  CalculatedFieldFormat,
@@ -132,6 +132,10 @@ export const FormulaModal: FC<FormulaModalProps> = ({
132
132
  const canSave =
133
133
  formulaValidation.valid && unknownFieldErrors.length === 0 && parsedTokens.length > 0;
134
134
 
135
+ const handleClose = useCallback(() => {
136
+ onClose();
137
+ }, [onClose]);
138
+
135
139
  const handleSave = useCallback(() => {
136
140
  if (!canSave) {
137
141
  return;
@@ -141,89 +145,75 @@ export const FormulaModal: FC<FormulaModalProps> = ({
141
145
  }, [canSave, format, onClose, onSave, parsedTokens]);
142
146
 
143
147
  return (
144
- <Flex
145
- alignItems="center"
146
- justifyContent="center"
147
- className="dte-formula-modal-overlay"
148
- role="dialog"
149
- aria-modal="true"
150
- aria-label="Formula"
151
- >
152
- <Flex direction="column" className="dte-formula-modal ">
148
+ <Dialog open onClose={handleClose} size="xlarge">
149
+ <Dialog.Header>Formula Builder</Dialog.Header>
150
+ <Divider />
151
+ <Dialog.Content>
153
152
  <Flex
154
- className="dte-formula-modal-header"
155
- alignItems="center"
156
- justifyContent="space-between"
153
+ flex={1}
154
+ alignItems="stretch"
155
+ gap={2}
156
+ style={{
157
+ height: MODAL_CONTENT_MAX_HEIGHT,
158
+ overflowY: 'auto',
159
+ }}
157
160
  >
158
- <Text variant="headline" el="h6" size="small">
159
- Formula Builder
160
- </Text>
161
- <Button
162
- appearance="ghost"
163
- size="small"
164
- onClick={onClose}
165
- aria-label="Close"
166
- icon={IconClose}
161
+ <FieldSidebar
162
+ fillableOptions={fillableFieldsFromDocument}
163
+ mergeTagOptions={allFields}
164
+ highlightElementPath={highlightElementPath}
165
+ onHover={setHighlightElementPath}
166
+ onSelect={formulaEditor.insertField}
167
+ selectedPaths={formulaEditor.selectedFieldPaths}
168
+ />
169
+ <FormulaWorkspace
170
+ editorRef={formulaEditor.editorRef}
171
+ isInvalid={isInvalid && formulaEditor.isDirty}
172
+ validationError={validationError}
173
+ format={format}
174
+ onResultTypeChange={nextType =>
175
+ setFormat(prev => ({ ...prev, resultType: nextType }))
176
+ }
177
+ advancedOpen={advancedOpen}
178
+ onToggleAdvanced={() => setAdvancedOpen(prev => !prev)}
179
+ setFormat={setFormat}
180
+ onInput={formulaEditor.handleEditorInput}
181
+ onKeyDown={formulaEditor.handleKeyDown}
182
+ onClick={() => {}}
183
+ onMouseUp={formulaEditor.handleEditorInput}
184
+ onPaste={formulaEditor.handlePaste}
185
+ onOperatorSelect={formulaEditor.insertOperator}
186
+ onRemoveField={formulaEditor.removeFieldAtIndex}
187
+ actions={
188
+ <Flex gap="1" alignItems="center">
189
+ <Button
190
+ appearance="ghost"
191
+ size="small"
192
+ onClick={formulaEditor.undo}
193
+ disabled={!formulaEditor.canUndo}
194
+ aria-label="Undo"
195
+ icon={IconUndo}
196
+ />
197
+ <Button
198
+ appearance="ghost"
199
+ size="small"
200
+ onClick={formulaEditor.redo}
201
+ disabled={!formulaEditor.canRedo}
202
+ aria-label="Redo"
203
+ icon={IconRedo}
204
+ />
205
+ </Flex>
206
+ }
167
207
  />
168
208
  </Flex>
169
- <div className="dte-formula-modal-body">
170
- <Flex alignItems="stretch" gap={2} className="dte-formula-modal-columns">
171
- <FieldSidebar
172
- fillableOptions={fillableFieldsFromDocument}
173
- mergeTagOptions={allFields}
174
- highlightElementPath={highlightElementPath}
175
- onHover={setHighlightElementPath}
176
- onSelect={formulaEditor.insertField}
177
- selectedPaths={formulaEditor.selectedFieldPaths}
178
- />
179
- <FormulaWorkspace
180
- editorRef={formulaEditor.editorRef}
181
- isInvalid={isInvalid && formulaEditor.isDirty}
182
- validationError={validationError}
183
- format={format}
184
- onResultTypeChange={nextType =>
185
- setFormat(prev => ({ ...prev, resultType: nextType }))
186
- }
187
- advancedOpen={advancedOpen}
188
- onToggleAdvanced={() => setAdvancedOpen(prev => !prev)}
189
- setFormat={setFormat}
190
- onInput={formulaEditor.handleEditorInput}
191
- onKeyDown={formulaEditor.handleKeyDown}
192
- onClick={() => {}}
193
- onMouseUp={formulaEditor.handleEditorInput}
194
- onPaste={formulaEditor.handlePaste}
195
- onOperatorSelect={formulaEditor.insertOperator}
196
- onRemoveField={formulaEditor.removeFieldAtIndex}
197
- actions={
198
- <Flex gap="1" alignItems="center">
199
- <Button
200
- appearance="ghost"
201
- size="small"
202
- onClick={formulaEditor.undo}
203
- disabled={!formulaEditor.canUndo}
204
- aria-label="Undo"
205
- icon={IconUndo}
206
- />
207
- <Button
208
- appearance="ghost"
209
- size="small"
210
- onClick={formulaEditor.redo}
211
- disabled={!formulaEditor.canRedo}
212
- aria-label="Redo"
213
- icon={IconRedo}
214
- />
215
- </Flex>
216
- }
217
- />
218
- </Flex>
219
- </div>
220
- <Flex className="dte-formula-modal-footer" gap="2" justifyContent="flex-end">
221
- <Button onClick={onClose}>Cancel</Button>
222
- <Button onClick={handleSave} appearance="primary" disabled={!canSave}>
223
- Save
224
- </Button>
225
- </Flex>
226
- </Flex>
227
- </Flex>
209
+ </Dialog.Content>
210
+ <Divider />
211
+ <Dialog.Footer sticky>
212
+ <Dialog.CancelButton onClick={handleClose}>Cancel</Dialog.CancelButton>
213
+ <Button onClick={handleSave} appearance="primary" disabled={!canSave}>
214
+ Save
215
+ </Button>
216
+ </Dialog.Footer>
217
+ </Dialog>
228
218
  );
229
219
  };