@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
@@ -1,4 +1,4 @@
1
- import { Alert, Button, Flex, Text } from '@servicetitan/anvil2';
1
+ import { Alert, Button, Flex, LinkButton, Text } from '@servicetitan/anvil2';
2
2
  import {
3
3
  Dispatch,
4
4
  FC,
@@ -9,6 +9,7 @@ import {
9
9
  SetStateAction,
10
10
  useCallback,
11
11
  } from 'react';
12
+ import { CALCULATED_OPERATIONS } from '../../constants';
12
13
  import { CalculatedFieldFormat } from '../../interface/types';
13
14
  import { AdvancedSettings } from './advanced-settings';
14
15
  import { ResultTypeSelector } from './result-type-selector';
@@ -70,7 +71,7 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
70
71
  );
71
72
 
72
73
  return (
73
- <Flex direction="column" flex={1} gap={2}>
74
+ <Flex direction="column" flex={1} gap={2} style={{ padding: '12px' }}>
74
75
  <Text variant="body" size="small">
75
76
  Click a field on the left to add it. Use +, -, *, /, and parentheses to build
76
77
  formulas.
@@ -100,16 +101,16 @@ export const FormulaWorkspace: FC<FormulaWorkspaceProps> = ({
100
101
  />
101
102
  {isInvalid && validationError && <Alert status="danger" title={validationError} />}
102
103
  <Flex gap={1}>
103
- {['+', '-', '*', '/', '(', ')'].map(op => (
104
+ {CALCULATED_OPERATIONS.map(op => (
104
105
  <Button key={op} onClick={() => onOperatorSelect(op)} size="small">
105
106
  {op}
106
107
  </Button>
107
108
  ))}
108
109
  </Flex>
109
110
  <ResultTypeSelector format={format} onChange={onResultTypeChange} />
110
- <Button appearance="ghost" onClick={onToggleAdvanced} size="small">
111
+ <LinkButton appearance="primary" onClick={onToggleAdvanced}>
111
112
  {advancedOpen ? 'Hide advanced settings' : 'Show advanced settings'}
112
- </Button>
113
+ </LinkButton>
113
114
  {advancedOpen && <AdvancedSettings format={format} setFormat={setFormat} />}
114
115
  </Flex>
115
116
  );
@@ -1,4 +1,4 @@
1
- import { Button, Flex, Text } from '@servicetitan/anvil2';
1
+ import { Flex, SegmentedControl, Text } from '@servicetitan/anvil2';
2
2
  import { FC, Fragment } from 'react';
3
3
  import { CalculatedFieldFormat } from '../../interface/types';
4
4
 
@@ -19,16 +19,13 @@ export const ResultTypeSelector: FC<ResultTypeSelectorProps> = ({ format, onChan
19
19
  Result type
20
20
  </Text>
21
21
  <Flex gap={2}>
22
- {RESULT_TYPE_OPTIONS.map(({ label, value }) => (
23
- <Button
24
- key={value}
25
- appearance={format.resultType === value ? 'primary' : 'secondary'}
26
- size="small"
27
- onClick={() => onChange(value)}
28
- >
29
- {label}
30
- </Button>
31
- ))}
22
+ <SegmentedControl size="medium" selected={format.resultType} onChange={onChange} fill>
23
+ {RESULT_TYPE_OPTIONS.map(opt => (
24
+ <SegmentedControl.Segment key={opt.value} value={opt.value}>
25
+ {opt.label}
26
+ </SegmentedControl.Segment>
27
+ ))}
28
+ </SegmentedControl>
32
29
  </Flex>
33
30
  </Fragment>
34
31
  );
@@ -1,9 +1,10 @@
1
1
  import { FC, PropsWithChildren, RefObject } from 'react';
2
- import { PdfField } from '../../interface/types';
3
- import { getFieldBackgroundColor, getPagePosition } from '../../utils';
2
+ import { DataModelValues, PdfField } from '../../interface/types';
3
+ import { evaluateDisplayCondition, getFieldBackgroundColor, getPagePosition } from '../../utils';
4
4
 
5
5
  interface PdfViewFieldContainer {
6
6
  pdfWrapperRef: RefObject<HTMLDivElement>;
7
+ data?: DataModelValues;
7
8
  error?: string;
8
9
  field: PdfField;
9
10
  recipientsColors: Record<string, string>;
@@ -11,11 +12,19 @@ interface PdfViewFieldContainer {
11
12
 
12
13
  export const PdfViewFieldContainer: FC<PropsWithChildren<PdfViewFieldContainer>> = ({
13
14
  children,
15
+ data,
14
16
  error,
15
17
  field,
16
18
  pdfWrapperRef,
17
19
  recipientsColors,
18
20
  }) => {
21
+ const visible =
22
+ !field.displayCondition || evaluateDisplayCondition(field.displayCondition, data);
23
+
24
+ if (!visible) {
25
+ return null;
26
+ }
27
+
19
28
  const pagePos = getPagePosition(field.page, pdfWrapperRef);
20
29
  const bgColor = getFieldBackgroundColor(field.recipient, recipientsColors);
21
30
  return (
@@ -90,6 +90,7 @@ export const PdfView: FC<PdfViewProps> = ({
90
90
  <PdfViewFieldContainer
91
91
  key={field.id}
92
92
  field={field}
93
+ data={previewMode === 'fillable' ? previewData : data}
93
94
  error={errors[field.path!] || ''}
94
95
  recipientsColors={recipientsColors}
95
96
  pdfWrapperRef={pdfWrapperRef}
@@ -0,0 +1 @@
1
+ export const CALCULATED_OPERATIONS = ['+', '-', '*', '/', '(', ')'];
@@ -0,0 +1,26 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ import type {
4
+ DisplayConditionGroup,
5
+ DisplayConditionSingle,
6
+ DisplayConditionState,
7
+ } from '../interface/types';
8
+
9
+ export const defaultCondition = (): DisplayConditionSingle => ({
10
+ dataPointKey: '',
11
+ id: uuidv4(),
12
+ operator: 'is_equal_to',
13
+ value: '',
14
+ });
15
+
16
+ export const defaultGroup = (): DisplayConditionGroup => ({
17
+ conditions: [defaultCondition()],
18
+ id: uuidv4(),
19
+ });
20
+
21
+ export const defaultState = (): DisplayConditionState => ({
22
+ behavior: 'show',
23
+ groups: [defaultGroup()],
24
+ });
25
+
26
+ export const MODAL_CONTENT_MAX_HEIGHT = '70vh';
@@ -1,2 +1,5 @@
1
+ export * from './calculated.constants';
2
+ export * from './conditions.constants';
1
3
  export * from './field.constants';
4
+ export * from './menu-group';
2
5
  export * from './pdf-editor.constants';
@@ -16,6 +16,25 @@ export type FillableFieldType = 'text' | 'date' | 'checkbox' | 'radio' | 'number
16
16
 
17
17
  export type PdfFieldSubType = FillableFieldType | ESignFieldType;
18
18
 
19
+ export interface DisplayConditionState {
20
+ behavior: 'show' | 'hide';
21
+ groups: DisplayConditionGroup[];
22
+ }
23
+
24
+ export interface DisplayConditionGroup {
25
+ id: string;
26
+ logicalOperator?: 'and' | 'or';
27
+ conditions: DisplayConditionSingle[];
28
+ }
29
+
30
+ export interface DisplayConditionSingle {
31
+ id: string;
32
+ dataPointKey: string;
33
+ logicalOperator?: 'and' | 'or';
34
+ operator: string;
35
+ value: string;
36
+ }
37
+
19
38
  export interface PdfField {
20
39
  id: string;
21
40
  type: FieldTypeEnum;
@@ -32,6 +51,7 @@ export interface PdfField {
32
51
  description?: string;
33
52
  formula?: StructuredFormula;
34
53
  formulaFormat?: CalculatedFieldFormat;
54
+ displayCondition?: DisplayConditionState | null;
35
55
  }
36
56
 
37
57
  export interface FieldTypeOption {
@@ -144,3 +164,42 @@ export interface DataChangePayload {
144
164
  * it is displayed.
145
165
  */
146
166
  export type PreviewMode = 'fillable' | 'view';
167
+
168
+ /**
169
+ * Display condition types for the Rules and Conditions modal and evaluation.
170
+ */
171
+
172
+ /** Operators for string-type fields. */
173
+ export const STRING_OPERATORS = [
174
+ { label: 'Contains', value: 'contains' },
175
+ { label: 'Does not contain', value: 'does_not_contain' },
176
+ { label: 'Is equal to', value: 'is_equal_to' },
177
+ { label: 'Is not equal to', value: 'is_not_equal_to' },
178
+ { label: 'Starts with', value: 'starts_with' },
179
+ { label: 'Ends with', value: 'ends_with' },
180
+ { label: 'Is empty', value: 'is_empty' },
181
+ { label: 'Is not empty', value: 'is_not_empty' },
182
+ ] as const;
183
+
184
+ /** Operators for number-type fields. */
185
+ export const NUMBER_OPERATORS = [
186
+ { label: '== (equal to)', value: 'num_eq' },
187
+ { label: '!= (not equal to)', value: 'num_neq' },
188
+ { label: '> (greater than)', value: 'num_gt' },
189
+ { label: '< (less than)', value: 'num_lt' },
190
+ { label: '>= (greater than or equal to)', value: 'num_gte' },
191
+ { label: '<= (less than or equal to)', value: 'num_lte' },
192
+ ] as const;
193
+
194
+ export type ConditionOperator =
195
+ | (typeof STRING_OPERATORS)[number]['value']
196
+ | (typeof NUMBER_OPERATORS)[number]['value'];
197
+
198
+ /** Operators that do not require a value (empty/is not empty) */
199
+ export const VALUE_LESS_OPERATORS: ConditionOperator[] = ['is_empty', 'is_not_empty'];
200
+
201
+ export interface DataPointOption {
202
+ fieldType: 'number' | 'string';
203
+ fullKey: string;
204
+ title: string;
205
+ }
@@ -15,19 +15,9 @@
15
15
  max-height: 85vh;
16
16
  }
17
17
 
18
- .dte-formula-modal-body {
19
- padding: var(--spacing-2);
20
- overflow: auto;
21
- }
22
-
23
- .dte-formula-modal-columns {
24
- min-height: 280px;
25
- max-height: 60vh;
26
- }
27
-
28
18
  /* Field sidebar (left) */
29
19
  .dte-formula-sidebar {
30
- flex: 0 0 220px;
20
+ flex: 0 0 250px;
31
21
  border-right: 1px solid var(--border-color);
32
22
  padding-right: var(--spacing-2);
33
23
  gap: var(--spacing-2);
@@ -115,20 +105,6 @@
115
105
  opacity: 1;
116
106
  }
117
107
 
118
- .dte-formula-advanced-toggle {
119
- background: none;
120
- border: none;
121
- padding: var(--spacing-0);
122
- font-size: var(--typescale-2);
123
- color: var(--menu-active-color, #0265dc);
124
- cursor: pointer;
125
- text-decoration: none;
126
- }
127
-
128
- .dte-formula-advanced-toggle:hover {
129
- text-decoration: underline;
130
- }
131
-
132
108
  .dte-formula-advanced {
133
109
  margin-top: var(--spacing-2);
134
110
  padding-top: var(--spacing-2);
@@ -138,30 +114,10 @@
138
114
  gap: var(--spacing-2);
139
115
  }
140
116
 
141
- .dte-formula-advanced-row {
142
- margin-bottom: var(--spacing-0);
143
- }
144
-
145
117
  .dte-formula-advanced-value {
146
118
  color: var(--color-neutral-300);
147
119
  }
148
120
 
149
- .dte-formula-advanced-range {
150
- width: 100%;
151
- margin: var(--spacing-0);
152
- }
153
-
154
- .dte-formula-advanced-checkbox {
155
- display: flex;
156
- align-items: center;
157
- gap: var(--spacing-1);
158
- cursor: pointer;
159
- }
160
-
161
- .dte-formula-advanced-checkbox input {
162
- margin: var(--spacing-0);
163
- }
164
-
165
121
  .dte-formula-advanced-input {
166
122
  flex: 1;
167
123
  min-width: 0;
@@ -182,70 +138,6 @@
182
138
  border-top: 1px solid var(--border-color);
183
139
  }
184
140
 
185
- /* Token input */
186
- .dte-formula-token-input-wrapper {
187
- display: flex;
188
- flex-direction: row;
189
- align-items: stretch;
190
- gap: var(--spacing-2);
191
- }
192
-
193
- .dte-formula-token-input {
194
- flex: 1;
195
- padding: var(--spacing-1) var(--spacing-2);
196
- border: 1px solid var(--border-color);
197
- border-radius: 4px;
198
- outline: none;
199
- flex-wrap: wrap;
200
- gap: 2px;
201
- cursor: text;
202
- }
203
-
204
- .dte-formula-token-input:focus {
205
- border-color: var(--border-color-active);
206
- box-shadow: 0 0 0 1px var(--border-color-active);
207
- }
208
-
209
- .dte-formula-placeholder {
210
- color: var(--color-neutral-200);
211
- }
212
-
213
- .dte-formula-token {
214
- margin: var(--spacing-half);
215
- user-select: none;
216
- font-size: var(--typescale-2);
217
- }
218
-
219
- .dte-formula-token-field {
220
- padding: 2px var(--spacing-half);
221
- background: var(--menu-active-color, #0265dc);
222
- border-radius: var(--spacing-half, 4px);
223
- color: var(--white);
224
- flex-shrink: 0;
225
- }
226
-
227
- .dte-formula-token-delete {
228
- display: inline-flex;
229
- align-items: center;
230
- justify-content: center;
231
- padding: var(--spacing-0);
232
- margin: var(--spacing-0);
233
- margin-left: 2px;
234
- border: none;
235
- background: transparent;
236
- color: var(--color-neutral-200);
237
- cursor: pointer;
238
- border-radius: 2px;
239
- }
240
-
241
- .dte-formula-token-delete:hover {
242
- color: var(--danger, #dc3545);
243
- }
244
-
245
- .dte-formula-token-number {
246
- font-variant-numeric: tabular-nums;
247
- }
248
-
249
141
  .dte-formula-caret {
250
142
  width: 0;
251
143
  height: 1em;
@@ -259,49 +151,3 @@
259
151
  opacity: 0;
260
152
  }
261
153
  }
262
-
263
- /* Autosuggest - left side, always visible */
264
- .dte-formula-autosuggest {
265
- margin: var(--spacing-0);
266
- padding: var(--spacing-0);
267
- list-style: none;
268
- width: 200px;
269
- min-width: 200px;
270
- max-height: 200px;
271
- overflow-y: auto;
272
- background: var(--white);
273
- border: 1px solid var(--border-color);
274
- border-radius: 4px;
275
- flex-shrink: 0;
276
- }
277
-
278
- .dte-formula-autosuggest-item {
279
- padding: var(--spacing-1) var(--spacing-2);
280
- cursor: pointer;
281
- }
282
-
283
- .dte-formula-autosuggest-item:hover,
284
- .dte-formula-autosuggest-item.--highlight {
285
- color: var(--menu-active-color);
286
- }
287
-
288
- .dte-formula-autosuggest-item.empty {
289
- cursor: default;
290
- color: var(--color-neutral-200);
291
- }
292
-
293
- .dte-formula-box {
294
- padding: var(--spacing-1) var(--spacing-2);
295
- border: 1px solid var(--border-color);
296
- border-radius: 4px;
297
- background: var(--color-neutral-0);
298
- cursor: pointer;
299
- display: flex;
300
- overflow-x: auto;
301
- align-items: center;
302
- gap: 2px;
303
- }
304
-
305
- .dte-formula-box:hover {
306
- border-color: var(--border-color-hover);
307
- }
@@ -0,0 +1,134 @@
1
+ import {
2
+ ConditionOperator,
3
+ DataModelValues,
4
+ DisplayConditionGroup,
5
+ DisplayConditionState,
6
+ VALUE_LESS_OPERATORS,
7
+ } from '../../interface/types';
8
+
9
+ function getValueAtPath(
10
+ data: DataModelValues | undefined,
11
+ path: string,
12
+ ): string | number | undefined {
13
+ if (!data || !path) {
14
+ return undefined;
15
+ }
16
+ const keys = path.split('.');
17
+ let value: unknown = data;
18
+ for (const key of keys) {
19
+ if (value === null || value === undefined || typeof value !== 'object') {
20
+ return undefined;
21
+ }
22
+ value = (value as Record<string, unknown>)[key];
23
+ }
24
+ if (value === null || value === undefined) {
25
+ return undefined;
26
+ }
27
+ if (typeof value === 'number' || typeof value === 'string') {
28
+ return value;
29
+ }
30
+ return String(value);
31
+ }
32
+
33
+ function evaluateSingleCondition(
34
+ dataPointKey: string,
35
+ operator: string,
36
+ value: string,
37
+ data: DataModelValues | undefined,
38
+ ): boolean {
39
+ const raw = getValueAtPath(data, dataPointKey);
40
+ const strVal = raw === undefined ? '' : String(raw);
41
+ const numVal = typeof raw === 'number' ? raw : Number(strVal);
42
+ const trimmedInput = value.trim();
43
+ const parsed = Number(trimmedInput);
44
+ const numInput = trimmedInput !== '' && Number.isFinite(parsed) ? parsed : 0;
45
+
46
+ switch (operator as ConditionOperator) {
47
+ case 'is_equal_to':
48
+ return strVal === trimmedInput;
49
+ case 'is_not_equal_to':
50
+ return strVal !== trimmedInput;
51
+ case 'contains':
52
+ return strVal.includes(trimmedInput);
53
+ case 'does_not_contain':
54
+ return !strVal.includes(trimmedInput);
55
+ case 'starts_with':
56
+ return strVal.startsWith(trimmedInput);
57
+ case 'ends_with':
58
+ return strVal.endsWith(trimmedInput);
59
+ case 'is_empty':
60
+ return strVal === '' || strVal.length === 0;
61
+ case 'is_not_empty':
62
+ return strVal !== '' && strVal.length > 0;
63
+ case 'num_eq':
64
+ return numVal === numInput;
65
+ case 'num_neq':
66
+ return numVal !== numInput;
67
+ case 'num_gt':
68
+ return numVal > numInput;
69
+ case 'num_lt':
70
+ return numVal < numInput;
71
+ case 'num_gte':
72
+ return numVal >= numInput;
73
+ case 'num_lte':
74
+ return numVal <= numInput;
75
+ default:
76
+ return strVal === trimmedInput;
77
+ }
78
+ }
79
+
80
+ function evaluateGroup(group: DisplayConditionGroup, data: DataModelValues | undefined): boolean {
81
+ const valid = group.conditions.filter(
82
+ c =>
83
+ c.dataPointKey &&
84
+ (VALUE_LESS_OPERATORS.includes(c.operator as ConditionOperator) ||
85
+ c.value.trim() !== ''),
86
+ );
87
+ if (valid.length === 0) {
88
+ return false;
89
+ }
90
+ let result = evaluateSingleCondition(
91
+ valid[0].dataPointKey,
92
+ valid[0].operator,
93
+ valid[0].value,
94
+ data,
95
+ );
96
+ for (let i = 1; i < valid.length; i++) {
97
+ const op = valid[i].logicalOperator === 'or' ? 'or' : 'and';
98
+ const next = evaluateSingleCondition(
99
+ valid[i].dataPointKey,
100
+ valid[i].operator,
101
+ valid[i].value,
102
+ data,
103
+ );
104
+ result = op === 'or' ? result || next : result && next;
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Evaluate display condition state against data. Returns true if the field should be visible.
111
+ */
112
+ export function evaluateDisplayCondition(
113
+ state: DisplayConditionState,
114
+ data: DataModelValues | undefined,
115
+ ): boolean {
116
+ const validGroups = state.groups.filter(g =>
117
+ g.conditions.some(
118
+ c =>
119
+ c.dataPointKey &&
120
+ (VALUE_LESS_OPERATORS.includes(c.operator as ConditionOperator) ||
121
+ c.value.trim() !== ''),
122
+ ),
123
+ );
124
+ if (validGroups.length === 0) {
125
+ return true;
126
+ }
127
+ let result = evaluateGroup(validGroups[0], data);
128
+ for (let i = 1; i < validGroups.length; i++) {
129
+ const op = validGroups[i].logicalOperator === 'or' ? 'or' : 'and';
130
+ const next = evaluateGroup(validGroups[i], data);
131
+ result = op === 'or' ? result || next : result && next;
132
+ }
133
+ return state.behavior === 'hide' ? !result : result;
134
+ }
@@ -0,0 +1,2 @@
1
+ export * from './evaluate.utils';
2
+ export * from './schema-data-points.utils';
@@ -0,0 +1,93 @@
1
+ import {
2
+ DataPointOption,
3
+ FieldTypeEnum,
4
+ type PdfField,
5
+ type SchemaNode,
6
+ type SchemaObject,
7
+ } from '../../interface/types';
8
+
9
+ function isUseInConditionals(
10
+ node:
11
+ | { options?: { useInConditionals?: boolean; useInCalculatedFields?: boolean } }
12
+ | undefined,
13
+ ): boolean {
14
+ return (
15
+ node?.options?.useInConditionals === true || node?.options?.useInCalculatedFields === true
16
+ );
17
+ }
18
+
19
+ function isValueNode(
20
+ node: SchemaNode,
21
+ ): node is { type: 'string' | 'number'; title?: string; options?: object } {
22
+ return node?.type === 'string' || node?.type === 'number';
23
+ }
24
+
25
+ function walkSchema(
26
+ schema: SchemaObject | undefined,
27
+ prefix: string,
28
+ parentTitles: string[],
29
+ ): DataPointOption[] {
30
+ if (!schema?.properties) {
31
+ return [];
32
+ }
33
+ const result: DataPointOption[] = [];
34
+ for (const [key, node] of Object.entries(schema.properties)) {
35
+ const fullKey = prefix ? `${prefix}.${key}` : key;
36
+ const title = (node as { title?: string }).title ?? key;
37
+ const fullTitle = [...parentTitles, title].join(' - ');
38
+
39
+ if (node?.type === 'object') {
40
+ result.push(...walkSchema(node as SchemaObject, fullKey, [...parentTitles, title]));
41
+ } else if (isValueNode(node) && isUseInConditionals(node)) {
42
+ result.push({
43
+ fieldType: node.type === 'number' ? 'number' : 'string',
44
+ fullKey,
45
+ title: fullTitle || fullKey,
46
+ });
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ export function getSchemaDataPointOptions(schema: SchemaObject | undefined): DataPointOption[] {
53
+ const options = walkSchema(schema, '', []);
54
+ return options.sort((a, b) => a.title.localeCompare(b.title));
55
+ }
56
+
57
+ /**
58
+ * Data point options from document fields: fillable only (no calculated fields).
59
+ * Uses field.path as fullKey so condition evaluation can read from the same data object.
60
+ */
61
+ export function getDocumentFieldsDataPointOptions(
62
+ fields: PdfField[] | undefined,
63
+ ): DataPointOption[] {
64
+ if (!fields?.length) {
65
+ return [];
66
+ }
67
+ const result: DataPointOption[] = [];
68
+ for (const f of fields) {
69
+ if (!f.path || f.type !== FieldTypeEnum.fillable) {
70
+ continue;
71
+ }
72
+ const fieldType = f.subType === 'number' ? 'number' : 'string';
73
+ result.push({
74
+ fieldType,
75
+ fullKey: f.path,
76
+ title: f.label || f.path,
77
+ });
78
+ }
79
+ return result.sort((a, b) => a.title.localeCompare(b.title));
80
+ }
81
+
82
+ /**
83
+ * Combined data point options: schema (data model) + fillable fields.
84
+ */
85
+ export function getDataPointOptions(
86
+ schema: SchemaObject | undefined,
87
+ documentFields: PdfField[] | undefined,
88
+ ): DataPointOption[] {
89
+ const schemaOptions = getSchemaDataPointOptions(schema);
90
+ const documentOptions = getDocumentFieldsDataPointOptions(documentFields);
91
+ const combined = [...schemaOptions, ...documentOptions];
92
+ return combined.sort((a, b) => a.title.localeCompare(b.title));
93
+ }
@@ -23,6 +23,7 @@ function useInCalculatedFields(node: SchemaNode): boolean {
23
23
  export interface ExtractGroupedFieldsOptions {
24
24
  /** When true, only include fields with SchemaFieldBaseOptions.useInCalculatedFields === true */
25
25
  onlyUseInCalculatedFields?: boolean;
26
+ onlyUseInConditionals?: boolean;
26
27
  }
27
28
 
28
29
  /**
@@ -1,25 +1,6 @@
1
- import { FormulaToken } from '../../interface/types';
2
1
  import { escapeHtml } from './dom.utils';
3
2
  import { tokenizeExpression } from './expression.utils';
4
3
 
5
- /**
6
- * Renders formula tokens to HTML for the read-only preview (e.g. formula box).
7
- * Uses the same token classes as the modal (dte-formula-token, dte-formula-token-field, etc.).
8
- */
9
- export function renderFormulaPreviewHtml(tokens: FormulaToken[]): string {
10
- if (!tokens?.length) {
11
- return '';
12
- }
13
- return tokens
14
- .map(token => {
15
- const type = token.type;
16
- const text = type === 'field' ? token.label : (token as { value: string }).value;
17
- const escaped = escapeHtml(text).replace(/ /g, '&nbsp;');
18
- return `<span class="dte-formula-token dte-formula-token-${type}">${escaped}</span>`;
19
- })
20
- .join('');
21
- }
22
-
23
4
  export function renderFormulaHtml(expression: string, labelMap: Map<string, string>): string {
24
5
  if (!expression.trim()) {
25
6
  return '';