@servicetitan/dte-pdf-editor 1.34.0 → 1.36.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 (114) hide show
  1. package/README.md +28 -12
  2. package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
  3. package/dist/components/display-conditions/condition-row.js +1 -1
  4. package/dist/components/display-conditions/condition-row.js.map +1 -1
  5. package/dist/components/field-config-panel/advanced-settings.d.ts.map +1 -1
  6. package/dist/components/field-config-panel/advanced-settings.js +20 -10
  7. package/dist/components/field-config-panel/advanced-settings.js.map +1 -1
  8. package/dist/components/field-config-panel/formula-generator.d.ts.map +1 -1
  9. package/dist/components/field-config-panel/formula-generator.js +2 -1
  10. package/dist/components/field-config-panel/formula-generator.js.map +1 -1
  11. package/dist/components/field-config-panel/formula-modal.d.ts.map +1 -1
  12. package/dist/components/field-config-panel/formula-modal.js +47 -6
  13. package/dist/components/field-config-panel/formula-modal.js.map +1 -1
  14. package/dist/components/field-config-panel/formula-workspace.d.ts +1 -0
  15. package/dist/components/field-config-panel/formula-workspace.d.ts.map +1 -1
  16. package/dist/components/field-config-panel/formula-workspace.js +10 -4
  17. package/dist/components/field-config-panel/formula-workspace.js.map +1 -1
  18. package/dist/components/field-config-panel/result-type-selector.d.ts.map +1 -1
  19. package/dist/components/field-config-panel/result-type-selector.js +1 -0
  20. package/dist/components/field-config-panel/result-type-selector.js.map +1 -1
  21. package/dist/components/pdf-view/pdf-view-calculated.d.ts +1 -0
  22. package/dist/components/pdf-view/pdf-view-calculated.d.ts.map +1 -1
  23. package/dist/components/pdf-view/pdf-view-calculated.js +20 -3
  24. package/dist/components/pdf-view/pdf-view-calculated.js.map +1 -1
  25. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  26. package/dist/components/pdf-view/pdf-view-fillable.js +5 -2
  27. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  28. package/dist/components/pdf-view/pdf-view.d.ts +6 -0
  29. package/dist/components/pdf-view/pdf-view.d.ts.map +1 -1
  30. package/dist/components/pdf-view/pdf-view.js +2 -2
  31. package/dist/components/pdf-view/pdf-view.js.map +1 -1
  32. package/dist/constants/calculated.constants.d.ts +2 -0
  33. package/dist/constants/calculated.constants.d.ts.map +1 -1
  34. package/dist/constants/calculated.constants.js +2 -0
  35. package/dist/constants/calculated.constants.js.map +1 -1
  36. package/dist/hooks/useFormulaEditor.d.ts +3 -0
  37. package/dist/hooks/useFormulaEditor.d.ts.map +1 -1
  38. package/dist/hooks/useFormulaEditor.js +43 -35
  39. package/dist/hooks/useFormulaEditor.js.map +1 -1
  40. package/dist/interface/types.d.ts +7 -1
  41. package/dist/interface/types.d.ts.map +1 -1
  42. package/dist/interface/types.js.map +1 -1
  43. package/dist/utils/conditions/schema-data-points.utils.d.ts.map +1 -1
  44. package/dist/utils/conditions/schema-data-points.utils.js +4 -1
  45. package/dist/utils/conditions/schema-data-points.utils.js.map +1 -1
  46. package/dist/utils/data-model/extract-fields.utils.d.ts.map +1 -1
  47. package/dist/utils/data-model/extract-fields.utils.js +11 -1
  48. package/dist/utils/data-model/extract-fields.utils.js.map +1 -1
  49. package/dist/utils/formula/caret.utils.js +4 -4
  50. package/dist/utils/formula/caret.utils.js.map +1 -1
  51. package/dist/utils/formula/dom.utils.js +2 -2
  52. package/dist/utils/formula/dom.utils.js.map +1 -1
  53. package/dist/utils/formula/evaluate-formula.utils.d.ts +10 -2
  54. package/dist/utils/formula/evaluate-formula.utils.d.ts.map +1 -1
  55. package/dist/utils/formula/evaluate-formula.utils.js +205 -4
  56. package/dist/utils/formula/evaluate-formula.utils.js.map +1 -1
  57. package/dist/utils/formula/expression.utils.d.ts +10 -9
  58. package/dist/utils/formula/expression.utils.d.ts.map +1 -1
  59. package/dist/utils/formula/expression.utils.js +39 -41
  60. package/dist/utils/formula/expression.utils.js.map +1 -1
  61. package/dist/utils/formula/format-calculated-result.utils.d.ts +1 -1
  62. package/dist/utils/formula/format-calculated-result.utils.d.ts.map +1 -1
  63. package/dist/utils/formula/format-calculated-result.utils.js +29 -8
  64. package/dist/utils/formula/format-calculated-result.utils.js.map +1 -1
  65. package/dist/utils/formula/index.d.ts +0 -2
  66. package/dist/utils/formula/index.d.ts.map +1 -1
  67. package/dist/utils/formula/index.js +0 -2
  68. package/dist/utils/formula/index.js.map +1 -1
  69. package/dist/utils/formula/validate-formula.utils.d.ts +1 -1
  70. package/dist/utils/formula/validate-formula.utils.d.ts.map +1 -1
  71. package/dist/utils/formula/validate-formula.utils.js +15 -1
  72. package/dist/utils/formula/validate-formula.utils.js.map +1 -1
  73. package/dist/utils/shared/date.utils.d.ts +2 -0
  74. package/dist/utils/shared/date.utils.d.ts.map +1 -0
  75. package/dist/utils/shared/date.utils.js +6 -0
  76. package/dist/utils/shared/date.utils.js.map +1 -0
  77. package/dist/utils/shared/index.d.ts +1 -0
  78. package/dist/utils/shared/index.d.ts.map +1 -1
  79. package/dist/utils/shared/index.js +1 -0
  80. package/dist/utils/shared/index.js.map +1 -1
  81. package/package.json +1 -1
  82. package/src/components/display-conditions/condition-row.tsx +1 -0
  83. package/src/components/field-config-panel/advanced-settings.tsx +103 -77
  84. package/src/components/field-config-panel/formula-generator.tsx +2 -1
  85. package/src/components/field-config-panel/formula-modal.tsx +46 -6
  86. package/src/components/field-config-panel/formula-workspace.tsx +21 -7
  87. package/src/components/field-config-panel/result-type-selector.tsx +1 -0
  88. package/src/components/pdf-view/pdf-view-calculated.tsx +20 -3
  89. package/src/components/pdf-view/pdf-view-fillable.tsx +6 -3
  90. package/src/components/pdf-view/pdf-view.tsx +8 -0
  91. package/src/constants/calculated.constants.ts +4 -0
  92. package/src/hooks/useFormulaEditor.ts +108 -97
  93. package/src/interface/types.ts +8 -1
  94. package/src/utils/conditions/schema-data-points.utils.ts +4 -1
  95. package/src/utils/data-model/extract-fields.utils.ts +13 -1
  96. package/src/utils/formula/caret.utils.ts +1 -1
  97. package/src/utils/formula/dom.utils.ts +1 -1
  98. package/src/utils/formula/evaluate-formula.utils.ts +271 -5
  99. package/src/utils/formula/expression.utils.ts +44 -47
  100. package/src/utils/formula/format-calculated-result.utils.ts +32 -10
  101. package/src/utils/formula/index.ts +0 -2
  102. package/src/utils/formula/validate-formula.utils.ts +24 -0
  103. package/src/utils/shared/date.utils.ts +6 -0
  104. package/src/utils/shared/index.ts +1 -0
  105. package/dist/utils/formula/referenced-paths.utils.d.ts +0 -7
  106. package/dist/utils/formula/referenced-paths.utils.d.ts.map +0 -1
  107. package/dist/utils/formula/referenced-paths.utils.js +0 -18
  108. package/dist/utils/formula/referenced-paths.utils.js.map +0 -1
  109. package/dist/utils/formula/serialize-formula.utils.d.ts +0 -14
  110. package/dist/utils/formula/serialize-formula.utils.d.ts.map +0 -1
  111. package/dist/utils/formula/serialize-formula.utils.js +0 -33
  112. package/dist/utils/formula/serialize-formula.utils.js.map +0 -1
  113. package/src/utils/formula/referenced-paths.utils.ts +0 -18
  114. package/src/utils/formula/serialize-formula.utils.ts +0 -40
@@ -7,8 +7,8 @@ import {
7
7
  useState,
8
8
  } from 'react';
9
9
  import {
10
- backspaceBetweenTwoFields,
11
10
  getCaretExpressionIndex,
11
+ normalizeMergeTags,
12
12
  placeCaretAtEnd,
13
13
  readExpressionFromEditor,
14
14
  renderFormulaHtml,
@@ -18,10 +18,11 @@ import {
18
18
 
19
19
  export const useFormulaEditor = (params: {
20
20
  currentExpression: string;
21
+ knownDateFields?: Set<string>;
21
22
  opened: boolean;
22
23
  pathToLabel: Map<string, string>;
23
24
  }) => {
24
- const { currentExpression, opened, pathToLabel } = params;
25
+ const { currentExpression, knownDateFields, opened, pathToLabel } = params;
25
26
  const [draftExpression, setDraftExpression] = useState(currentExpression);
26
27
  const [history, setHistory] = useState<string[]>([currentExpression]);
27
28
  const [historyIndex, setHistoryIndex] = useState(0);
@@ -32,6 +33,7 @@ export const useFormulaEditor = (params: {
32
33
  const lastCaretRef = useRef(0);
33
34
  const historyIndexRef = useRef(0);
34
35
  const historyRef = useRef<string[]>([currentExpression]);
36
+ const knownDateFieldsRef = useRef(knownDateFields);
35
37
 
36
38
  useEffect(() => {
37
39
  if (opened) {
@@ -79,26 +81,36 @@ export const useFormulaEditor = (params: {
79
81
  }
80
82
  const nextValue = readExpressionFromEditor(editor);
81
83
  const caretIndex = getCaretExpressionIndex(editor);
82
- pendingCaretRef.current = caretIndex;
83
- lastCaretRef.current = caretIndex;
84
- updateSourceRef.current = 'editor';
85
- setDraftExpression(nextValue);
84
+ const validPaths = new Set(pathToLabel.keys());
85
+ const { normalized, removed } = normalizeMergeTags(nextValue, validPaths);
86
+ let nextCaret = caretIndex;
87
+ if (removed) {
88
+ if (caretIndex >= removed.end) {
89
+ nextCaret = caretIndex - (removed.end - removed.start);
90
+ } else if (caretIndex > removed.start) {
91
+ nextCaret = removed.start;
92
+ }
93
+ }
94
+ pendingCaretRef.current = nextCaret;
95
+ lastCaretRef.current = nextCaret;
96
+ updateSourceRef.current = removed ? 'state' : 'editor';
97
+ setDraftExpression(normalized);
86
98
  setIsDirty(true);
87
99
  const cur = historyIndexRef.current;
88
100
  const hist = historyRef.current;
89
101
  const base = hist.slice(0, cur + 1);
90
- if (base[base.length - 1] !== nextValue) {
91
- setHistory([...base, nextValue]);
102
+ if (base[base.length - 1] !== normalized) {
103
+ setHistory([...base, normalized]);
92
104
  setHistoryIndex(base.length);
93
105
  }
94
- }, []);
106
+ }, [pathToLabel]);
95
107
 
96
108
  const getCaretIndexForInsert = useCallback((): number => {
97
109
  const editor = editorRef.current;
98
110
  if (!editor) {
99
111
  return draftExpression.length;
100
112
  }
101
- const sel = editor.ownerDocument?.defaultView?.getSelection() ?? null;
113
+ const sel = window.getSelection();
102
114
  if (sel?.rangeCount && editor.contains(sel.getRangeAt(0).startContainer)) {
103
115
  const idx = getCaretExpressionIndex(editor);
104
116
  lastCaretRef.current = idx;
@@ -200,99 +212,83 @@ export const useFormulaEditor = (params: {
200
212
  [draftExpression],
201
213
  );
202
214
 
203
- const handleKeyDown = useCallback(
204
- (e: ReactKeyboardEvent<HTMLDivElement>) => {
205
- if (e.ctrlKey || e.metaKey) {
206
- const key = e.key.toLowerCase();
207
- if (key === 'z') {
208
- e.preventDefault();
209
- if (historyIndexRef.current > 0) {
210
- const next = historyRef.current[historyIndexRef.current - 1];
211
- updateSourceRef.current = 'state';
212
- pendingCaretRef.current = next.length;
213
- setDraftExpression(next);
214
- setHistoryIndex(historyIndexRef.current - 1);
215
- setIsDirty(true);
216
- }
217
- return;
218
- }
219
- if (key === 'y' || (e.shiftKey && key === 'z')) {
220
- e.preventDefault();
221
- const cur = historyIndexRef.current;
222
- const hist = historyRef.current;
223
- if (cur < hist.length - 1) {
224
- const next = hist[cur + 1];
225
- updateSourceRef.current = 'state';
226
- pendingCaretRef.current = next.length;
227
- setDraftExpression(next);
228
- setHistoryIndex(cur + 1);
229
- setIsDirty(true);
230
- }
231
- return;
215
+ useEffect(() => {
216
+ knownDateFieldsRef.current = knownDateFields;
217
+ }, [knownDateFields]);
218
+
219
+ const handleKeyDown = useCallback((e: ReactKeyboardEvent<HTMLDivElement>) => {
220
+ if (e.ctrlKey || e.metaKey) {
221
+ const key = e.key.toLowerCase();
222
+ if (key === 'z') {
223
+ e.preventDefault();
224
+ if (historyIndexRef.current > 0) {
225
+ const next = historyRef.current[historyIndexRef.current - 1];
226
+ updateSourceRef.current = 'state';
227
+ pendingCaretRef.current = next.length;
228
+ setDraftExpression(next);
229
+ setHistoryIndex(historyIndexRef.current - 1);
230
+ setIsDirty(true);
232
231
  }
232
+ return;
233
233
  }
234
- if (e.key === 'Backspace') {
235
- const editor = editorRef.current;
236
- if (editor) {
237
- const currentExpression = readExpressionFromEditor(editor);
238
- let caretIndex = getCaretExpressionIndex(editor);
239
- /*
240
- * Selection can collapse to (element, childCount) after DOM updates, yielding
241
- * expression.length (e.g. 33) instead of the real position (e.g. 16). Use last
242
- * known valid position when reported position is at end but we had a mid-string index.
243
- */
244
- if (
245
- caretIndex === currentExpression.length &&
246
- lastCaretRef.current >= 0 &&
247
- lastCaretRef.current < currentExpression.length
248
- ) {
249
- caretIndex = lastCaretRef.current;
250
- }
251
- lastCaretRef.current = caretIndex;
252
- const result = backspaceBetweenTwoFields(currentExpression, caretIndex);
253
- if (result) {
254
- e.preventDefault();
255
- updateSourceRef.current = 'state';
256
- pushHistory(result.newExpression, result.newCaret);
257
- return;
258
- }
234
+ if (key === 'y' || (e.shiftKey && key === 'z')) {
235
+ e.preventDefault();
236
+ const cur = historyIndexRef.current;
237
+ const hist = historyRef.current;
238
+ if (cur < hist.length - 1) {
239
+ const next = hist[cur + 1];
240
+ updateSourceRef.current = 'state';
241
+ pendingCaretRef.current = next.length;
242
+ setDraftExpression(next);
243
+ setHistoryIndex(cur + 1);
244
+ setIsDirty(true);
259
245
  }
246
+ return;
260
247
  }
261
- const allowed = new Set([
262
- '+',
263
- '-',
264
- '*',
265
- '/',
266
- '(',
267
- ')',
268
- '0',
269
- '1',
270
- '2',
271
- '3',
272
- '4',
273
- '5',
274
- '6',
275
- '7',
276
- '8',
277
- '9',
278
- '.',
279
- 'Backspace',
280
- 'Delete',
281
- 'ArrowLeft',
282
- 'ArrowRight',
283
- 'ArrowUp',
284
- 'ArrowDown',
285
- 'Home',
286
- 'End',
287
- 'Tab',
288
- ' ',
289
- ]);
290
- if (!allowed.has(e.key)) {
248
+ }
249
+ if (knownDateFieldsRef.current?.size && (e.key === '*' || e.key === '/')) {
250
+ const parts = tokenizeExpression(readExpressionFromEditor(editorRef.current!));
251
+ const hasDate = parts.some(
252
+ p => p.type === 'field' && knownDateFieldsRef.current!.has(p.value),
253
+ );
254
+ if (hasDate) {
291
255
  e.preventDefault();
256
+ return;
292
257
  }
293
- },
294
- [pushHistory],
295
- );
258
+ }
259
+ const allowed = new Set([
260
+ '+',
261
+ '-',
262
+ '*',
263
+ '/',
264
+ '(',
265
+ ')',
266
+ '0',
267
+ '1',
268
+ '2',
269
+ '3',
270
+ '4',
271
+ '5',
272
+ '6',
273
+ '7',
274
+ '8',
275
+ '9',
276
+ '.',
277
+ 'Backspace',
278
+ 'Delete',
279
+ 'ArrowLeft',
280
+ 'ArrowRight',
281
+ 'ArrowUp',
282
+ 'ArrowDown',
283
+ 'Home',
284
+ 'End',
285
+ 'Tab',
286
+ ' ',
287
+ ]);
288
+ if (!allowed.has(e.key)) {
289
+ e.preventDefault();
290
+ }
291
+ }, []);
296
292
 
297
293
  const handlePaste = useCallback(
298
294
  (text: string) => {
@@ -351,9 +347,24 @@ export const useFormulaEditor = (params: {
351
347
  return new Set(parts.filter(p => p.type === 'field').map(p => p.value));
352
348
  }, [draftExpression]);
353
349
 
350
+ const expressionHasDateFields = useMemo(() => {
351
+ if (!knownDateFields?.size) {
352
+ return false;
353
+ }
354
+ const parts = tokenizeExpression(draftExpression);
355
+ return parts.some(p => p.type === 'field' && knownDateFields.has(p.value));
356
+ }, [draftExpression, knownDateFields]);
357
+
358
+ const disabledOperators = useMemo(
359
+ () => (expressionHasDateFields ? new Set(['*', '/']) : undefined),
360
+ [expressionHasDateFields],
361
+ );
362
+
354
363
  return {
355
364
  draftExpression,
365
+ disabledOperators,
356
366
  editorRef,
367
+ expressionHasDateFields,
357
368
  isDirty,
358
369
  canUndo: historyIndex > 0,
359
370
  canRedo: historyIndex < history.length - 1,
@@ -80,11 +80,14 @@ export interface PdfField {
80
80
  displayCondition?: DisplayConditionState | null;
81
81
  }
82
82
 
83
+ export type SchemaFieldType = 'number' | 'date';
84
+
83
85
  export interface FieldTypeOption {
84
86
  label: string;
85
87
  type: FieldTypeEnum;
86
88
  subType?: PdfFieldSubType;
87
89
  path?: string;
90
+ fieldType?: SchemaFieldType;
88
91
  }
89
92
 
90
93
  export interface DataModelFieldGroup {
@@ -111,7 +114,7 @@ export interface StructuredFormula {
111
114
 
112
115
  /** Format options for a calculated field result (display and rounding) */
113
116
  export interface CalculatedFieldFormat {
114
- resultType: 'number' | 'currency' | 'percent';
117
+ resultType: 'number' | 'currency' | 'percent' | 'date';
115
118
  thousandsSeparator: boolean;
116
119
  decimals: number;
117
120
  roundingMode: 'round' | 'floor' | 'ceil';
@@ -119,6 +122,9 @@ export interface CalculatedFieldFormat {
119
122
  decimalSeparator: '.' | ',';
120
123
  prefixText: string;
121
124
  postfixText: string;
125
+ dateFormat?: string;
126
+ /** Paths of date-typed fields used in the formula, persisted for date-aware evaluation */
127
+ dateFieldPaths?: string[];
122
128
  }
123
129
 
124
130
  export interface SchemaFieldBaseOptions {
@@ -227,5 +233,6 @@ export const VALUE_LESS_OPERATORS: ConditionOperator[] = ['is_empty', 'is_not_em
227
233
  export interface DataPointOption {
228
234
  fieldType: 'number' | 'string';
229
235
  fullKey: string;
236
+ group: string;
230
237
  title: string;
231
238
  }
@@ -39,9 +39,11 @@ function walkSchema(
39
39
  if (node?.type === 'object') {
40
40
  result.push(...walkSchema(node as SchemaObject, fullKey, [...parentTitles, title]));
41
41
  } else if (isValueNode(node) && isUseInConditionals(node)) {
42
+ const fieldType = node.type === 'number' ? 'number' : 'string';
42
43
  result.push({
43
- fieldType: node.type === 'number' ? 'number' : 'string',
44
+ fieldType,
44
45
  fullKey,
46
+ group: fieldType === 'number' ? 'Numeric Fields' : 'Data Model',
45
47
  title: fullTitle || fullKey,
46
48
  });
47
49
  }
@@ -73,6 +75,7 @@ export function getDocumentFieldsDataPointOptions(
73
75
  result.push({
74
76
  fieldType,
75
77
  fullKey: f.path,
78
+ group: 'Document Fields',
76
79
  title: f.label || f.path,
77
80
  });
78
81
  }
@@ -2,6 +2,7 @@ import {
2
2
  DataModelFieldGroup,
3
3
  FieldTypeEnum,
4
4
  FieldTypeOption,
5
+ SchemaFieldType,
5
6
  SchemaNode,
6
7
  SchemaObject,
7
8
  SchemaSimpleDate,
@@ -16,6 +17,16 @@ function isSchemaSimpleDate(node: SchemaNode): node is SchemaSimpleDate {
16
17
  return node.type === 'date';
17
18
  }
18
19
 
20
+ function getSchemaFieldType(node: SchemaNode): SchemaFieldType | undefined {
21
+ if (isSchemaSimpleNumber(node)) {
22
+ return 'number';
23
+ }
24
+ if (isSchemaSimpleDate(node)) {
25
+ return 'date';
26
+ }
27
+ return undefined;
28
+ }
29
+
19
30
  function useInCalculatedFields(node: SchemaNode): boolean {
20
31
  return node.options?.useInCalculatedFields === true;
21
32
  }
@@ -79,6 +90,7 @@ export const extractGroupedFieldsFromDataModel = (
79
90
  label: property.title ?? key,
80
91
  type: FieldTypeEnum.dataModel,
81
92
  path: key,
93
+ fieldType: getSchemaFieldType(property),
82
94
  });
83
95
  }
84
96
  }
@@ -114,12 +126,12 @@ const extractFieldsRecursive = (
114
126
  rawType === 'boolean';
115
127
  const includeField = !onlyUseInCalculatedFields || useInCalculatedFields(fieldProperty);
116
128
  if (isLeaf && includeField) {
117
- // Leaf property - add as a field
118
129
  const label = fieldProperty.title ?? fieldKey;
119
130
  fields.push({
120
131
  label,
121
132
  type: FieldTypeEnum.dataModel,
122
133
  path: currentPath,
134
+ fieldType: getSchemaFieldType(fieldProperty),
123
135
  });
124
136
  } else if (fieldProperty.type === 'object' && fieldProperty.properties) {
125
137
  extractFieldsRecursive(
@@ -1,7 +1,7 @@
1
1
  import { placeCaretAtEnd, readExpressionFromEditor } from './dom.utils';
2
2
 
3
3
  function getExpressionBeforeCaret(element: HTMLElement): string {
4
- const selection = element.ownerDocument?.defaultView?.getSelection() ?? null;
4
+ const selection = window.getSelection() ?? null;
5
5
  if (!selection || selection.rangeCount === 0) {
6
6
  return readExpressionFromEditor(element);
7
7
  }
@@ -23,7 +23,7 @@ export function readExpressionFromEditor(node: HTMLElement): string {
23
23
  }
24
24
 
25
25
  export function placeCaretAtEnd(element: HTMLElement): void {
26
- const selection = element.ownerDocument?.defaultView?.getSelection() ?? null;
26
+ const selection = window.getSelection() ?? null;
27
27
  if (!selection) {
28
28
  return;
29
29
  }