@servicetitan/dte-pdf-editor 1.44.0 → 1.46.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 (50) hide show
  1. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  2. package/dist/components/field-config-panel/field-config-panel.js +4 -2
  3. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  4. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.d.ts.map +1 -1
  5. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js +3 -0
  6. package/dist/components/pdf-fields-overlay/pdf-overlay-field-fillable.js.map +1 -1
  7. package/dist/components/pdf-view/pdf-view-fillable.d.ts +8 -0
  8. package/dist/components/pdf-view/pdf-view-fillable.d.ts.map +1 -1
  9. package/dist/components/pdf-view/pdf-view-fillable.js +17 -8
  10. package/dist/components/pdf-view/pdf-view-fillable.js.map +1 -1
  11. package/dist/interface/types.d.ts +1 -0
  12. package/dist/interface/types.d.ts.map +1 -1
  13. package/dist/interface/types.js.map +1 -1
  14. package/dist/utils/shared/date.utils.d.ts +1 -0
  15. package/dist/utils/shared/date.utils.d.ts.map +1 -1
  16. package/dist/utils/shared/date.utils.js +12 -0
  17. package/dist/utils/shared/date.utils.js.map +1 -1
  18. package/package.json +1 -1
  19. package/src/__tests__/field-types/date-fields.test.ts +212 -0
  20. package/src/__tests__/field-types/display-condition-fields.test.ts +340 -0
  21. package/src/__tests__/field-types/e-sign-fields.test.ts +97 -0
  22. package/src/__tests__/field-types/fillable-fields.test.ts +180 -0
  23. package/src/__tests__/field-types/form-fields.test.ts +193 -0
  24. package/src/__tests__/field-types/formula-fields.test.ts +245 -0
  25. package/src/__tests__/field-types/merge-tag-fields.test.ts +208 -0
  26. package/src/components/field-config-panel/field-config-panel.tsx +11 -0
  27. package/src/components/pdf-fields-overlay/pdf-overlay-field-fillable.tsx +10 -0
  28. package/src/components/pdf-view/pdf-view-fillable.tsx +61 -15
  29. package/src/interface/types.ts +1 -0
  30. package/src/styles/inline-editable.css +1 -0
  31. package/src/utils/conditions/__tests__/evaluate.utils.test.ts +163 -0
  32. package/src/utils/conditions/__tests__/schema-data-points.utils.test.ts +149 -0
  33. package/src/utils/data-model/__tests__/extract-fields.utils.test.ts +154 -0
  34. package/src/utils/data-model/__tests__/resolve-values.utils.test.ts +60 -0
  35. package/src/utils/field/__tests__/field-background-color.utils.test.ts +26 -0
  36. package/src/utils/field/__tests__/field-placeholder-text.utils.test.ts +46 -0
  37. package/src/utils/formula/__tests__/dom.utils.test.ts +33 -0
  38. package/src/utils/formula/__tests__/evaluate-formula.utils.test.ts +202 -0
  39. package/src/utils/formula/__tests__/expression.utils.test.ts +119 -0
  40. package/src/utils/formula/__tests__/form-fields.utils.test.ts +274 -0
  41. package/src/utils/formula/__tests__/format-calculated-result.utils.test.ts +105 -0
  42. package/src/utils/formula/__tests__/render-formula.utils.test.ts +43 -0
  43. package/src/utils/formula/__tests__/validate-formula.utils.test.ts +168 -0
  44. package/src/utils/path/__tests__/generate-e-sign-path.test.ts +26 -0
  45. package/src/utils/path/__tests__/generate-fillable-path.test.ts +17 -0
  46. package/src/utils/path/__tests__/parse-fillable-path.test.ts +25 -0
  47. package/src/utils/recipients/__tests__/map-colors.test.ts +40 -0
  48. package/src/utils/shared/__tests__/date.utils.test.ts +58 -0
  49. package/src/utils/shared/__tests__/number.utils.test.ts +48 -0
  50. package/src/utils/shared/date.utils.ts +16 -0
@@ -0,0 +1,105 @@
1
+ import {
2
+ CalculatedFieldFormat,
3
+ CalculatedFormatForDate,
4
+ CalculatedFormatForNumber,
5
+ } from '../../../interface/types';
6
+ import { formatCalculatedResult } from '../format-calculated-result.utils';
7
+
8
+ const baseNumberFormat: CalculatedFormatForNumber = {
9
+ resultType: 'number',
10
+ thousandsSeparator: false,
11
+ decimals: 2,
12
+ roundingMode: 'round',
13
+ decimalSeparatorEnabled: false,
14
+ decimalSeparator: '.',
15
+ prefixText: '',
16
+ postfixText: '',
17
+ };
18
+
19
+ describe('formatCalculatedResult', () => {
20
+ const subject = (value: number, format: CalculatedFieldFormat | undefined) =>
21
+ formatCalculatedResult(value, format);
22
+
23
+ describe('with non-finite values', () => {
24
+ test.each([NaN, Infinity, -Infinity])('returns empty string for %p', value => {
25
+ expect(subject(value, baseNumberFormat)).toBe('');
26
+ });
27
+ });
28
+
29
+ describe('when format is undefined', () => {
30
+ test('returns the raw value as string', () => {
31
+ expect(subject(1234.5, undefined)).toBe('1234.5');
32
+ });
33
+ });
34
+
35
+ describe('with number formatting', () => {
36
+ test('rounds to the specified decimals', () => {
37
+ expect(subject(1.236, { ...baseNumberFormat, decimals: 2 })).toBe('1.24');
38
+ });
39
+
40
+ test('floors when roundingMode is floor', () => {
41
+ expect(subject(1.999, { ...baseNumberFormat, decimals: 0, roundingMode: 'floor' })).toBe(
42
+ '1',
43
+ );
44
+ });
45
+
46
+ test('ceils when roundingMode is ceil', () => {
47
+ expect(subject(1.001, { ...baseNumberFormat, decimals: 0, roundingMode: 'ceil' })).toBe(
48
+ '2',
49
+ );
50
+ });
51
+
52
+ test('adds thousands separator when enabled', () => {
53
+ expect(
54
+ subject(1234567.89, {
55
+ ...baseNumberFormat,
56
+ decimals: 2,
57
+ thousandsSeparator: true,
58
+ }),
59
+ ).toBe('1,234,567.89');
60
+ });
61
+
62
+ test('skips decimal part when decimals is 0', () => {
63
+ expect(subject(1234, { ...baseNumberFormat, decimals: 0 })).toBe('1234');
64
+ });
65
+
66
+ test('applies prefix and postfix text', () => {
67
+ expect(
68
+ subject(50, {
69
+ ...baseNumberFormat,
70
+ decimals: 0,
71
+ prefixText: '$',
72
+ postfixText: ' USD',
73
+ }),
74
+ ).toBe('$50 USD');
75
+ });
76
+ });
77
+
78
+ describe('with date formatting', () => {
79
+ const baseDateFormat: CalculatedFormatForDate = {
80
+ resultType: 'date',
81
+ dateFormat: 'MM/DD/YYYY',
82
+ };
83
+
84
+ // Display range is +/- MAX_DATE_DISPLAY_RANGE_YEARS years from "now",
85
+ // so use today's date to stay within range.
86
+ const today = new Date();
87
+ const todayMs = today.getTime();
88
+
89
+ test('formats epoch milliseconds with default tokens', () => {
90
+ const result = subject(todayMs, baseDateFormat);
91
+ expect(result).toMatch(/^\d{2}\/\d{2}\/\d{4}$/);
92
+ });
93
+
94
+ test('substitutes all supported date tokens', () => {
95
+ const year = String(today.getFullYear());
96
+ const result = subject(todayMs, { resultType: 'date', dateFormat: 'YYYY-YY' });
97
+ expect(result).toBe(`${year}-${year.slice(-2)}`);
98
+ });
99
+
100
+ test('returns empty string when date is far outside display range', () => {
101
+ const futureDate = Date.UTC(2100, 0, 1);
102
+ expect(subject(futureDate, baseDateFormat)).toBe('');
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,43 @@
1
+ import { renderFormulaHtml } from '../render-formula.utils';
2
+
3
+ describe('renderFormulaHtml', () => {
4
+ const subject = (expression: string, labelMap: Map<string, string>) =>
5
+ renderFormulaHtml(expression, labelMap);
6
+
7
+ test('returns empty string when expression is blank', () => {
8
+ expect(subject(' ', new Map())).toBe('');
9
+ });
10
+
11
+ test('renders field tokens as chip spans with label and remove button', () => {
12
+ const html = subject('userName', new Map([['userName', 'User Name']]));
13
+ expect(html).toContain('class="dte-formula-chip"');
14
+ expect(html).toContain('data-field="userName"');
15
+ expect(html).toContain('data-field-index="0"');
16
+ expect(html).toContain('User Name');
17
+ expect(html).toContain('data-field-remove="true"');
18
+ });
19
+
20
+ test('falls back to the field value when label is missing', () => {
21
+ const html = subject('unknown', new Map());
22
+ expect(html).toContain('>unknown<');
23
+ });
24
+
25
+ test('increments data-field-index for each field token', () => {
26
+ const html = subject('a b', new Map());
27
+ expect(html).toContain('data-field-index="0"');
28
+ expect(html).toContain('data-field-index="1"');
29
+ });
30
+
31
+ test('escapes special characters in non-field tokens', () => {
32
+ expect(subject('<', new Map())).toContain('&lt;');
33
+ });
34
+
35
+ test('replaces spaces in non-field tokens with non-breaking spaces', () => {
36
+ const html = subject('a b', new Map());
37
+ expect(html).toContain('&nbsp;&nbsp;&nbsp;');
38
+ });
39
+
40
+ test('returns empty string for whitespace-only expression', () => {
41
+ expect(subject(' ', new Map())).toBe('');
42
+ });
43
+ });
@@ -0,0 +1,168 @@
1
+ import { StructuredFormula } from '../../../interface/types';
2
+ import { validateFormula } from '../validate-formula.utils';
3
+
4
+ const f = (tokens: StructuredFormula['tokens']): StructuredFormula => ({ tokens });
5
+
6
+ describe('validateFormula', () => {
7
+ const subject = (
8
+ formula: StructuredFormula | undefined | null,
9
+ validPaths: Set<string>,
10
+ knownDateFields?: Set<string>,
11
+ pathToLabel?: Map<string, string>,
12
+ ) => validateFormula(formula, validPaths, knownDateFields, pathToLabel);
13
+
14
+ describe('when formula is empty', () => {
15
+ test.each([
16
+ ['undefined', undefined],
17
+ ['null', null],
18
+ ['empty tokens', f([])],
19
+ ])('returns invalid with empty error for %s', (_, formula) => {
20
+ const result = subject(formula, new Set());
21
+ expect(result.valid).toBe(false);
22
+ expect(result.errors).toEqual(['Formula cannot be empty']);
23
+ });
24
+ });
25
+
26
+ describe('when formula is valid', () => {
27
+ test('returns valid for a number-only formula', () => {
28
+ expect(subject(f([{ type: 'number', value: '42' }]), new Set())).toEqual({
29
+ valid: true,
30
+ errors: [],
31
+ });
32
+ });
33
+
34
+ test('returns valid for a known field reference', () => {
35
+ const result = subject(
36
+ f([{ type: 'field', path: 'a.b', fieldType: 'number' }]),
37
+ new Set(['a.b']),
38
+ );
39
+ expect(result.valid).toBe(true);
40
+ });
41
+
42
+ test('returns valid for a balanced expression with operators and parens', () => {
43
+ const result = subject(
44
+ f([
45
+ { type: 'lparen' },
46
+ { type: 'number', value: '1' },
47
+ { type: 'operator', value: '+' },
48
+ { type: 'number', value: '2' },
49
+ { type: 'rparen' },
50
+ { type: 'operator', value: '*' },
51
+ { type: 'number', value: '3' },
52
+ ]),
53
+ new Set(),
54
+ );
55
+ expect(result.valid).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe('when formula is invalid', () => {
60
+ test('reports unknown field', () => {
61
+ const result = subject(
62
+ f([{ type: 'field', path: 'unknown', fieldType: 'number' }]),
63
+ new Set(),
64
+ );
65
+ expect(result.valid).toBe(false);
66
+ expect(result.errors.some(e => e.includes('Unknown field'))).toBe(true);
67
+ });
68
+
69
+ test('uses pathToLabel for unknown field errors', () => {
70
+ const result = subject(
71
+ f([{ type: 'field', path: 'x', fieldType: 'number' }]),
72
+ new Set(),
73
+ undefined,
74
+ new Map([['x', 'My Field']]),
75
+ );
76
+ expect(result.errors.some(e => e.includes('My Field'))).toBe(true);
77
+ });
78
+
79
+ test('reports unbalanced opening parenthesis', () => {
80
+ const result = subject(
81
+ f([{ type: 'lparen' }, { type: 'number', value: '1' }]),
82
+ new Set(),
83
+ );
84
+ expect(result.errors).toContain('Unbalanced parentheses');
85
+ });
86
+
87
+ test('reports unbalanced closing parenthesis', () => {
88
+ const result = subject(
89
+ f([{ type: 'number', value: '1' }, { type: 'rparen' }]),
90
+ new Set(),
91
+ );
92
+ expect(result.errors).toContain('Unbalanced parentheses');
93
+ });
94
+
95
+ test('reports two adjacent operands as missing operator', () => {
96
+ const result = subject(
97
+ f([
98
+ { type: 'number', value: '1' },
99
+ { type: 'number', value: '2' },
100
+ ]),
101
+ new Set(),
102
+ );
103
+ expect(result.errors).toContain('Operator required between two fields or values');
104
+ });
105
+
106
+ test('reports formula starting with operator', () => {
107
+ const result = subject(
108
+ f([
109
+ { type: 'operator', value: '+' },
110
+ { type: 'number', value: '1' },
111
+ ]),
112
+ new Set(),
113
+ );
114
+ expect(
115
+ result.errors.some(e => e.includes('start with an operand')),
116
+ ).toBe(true);
117
+ });
118
+
119
+ test('reports formula ending with operator', () => {
120
+ const result = subject(
121
+ f([
122
+ { type: 'number', value: '1' },
123
+ { type: 'operator', value: '+' },
124
+ ]),
125
+ new Set(),
126
+ );
127
+ expect(
128
+ result.errors.some(e => e.includes('end with an operand')),
129
+ ).toBe(true);
130
+ });
131
+
132
+ test('reports invalid number value', () => {
133
+ const result = subject(f([{ type: 'number', value: 'abc' }]), new Set());
134
+ expect(result.errors.some(e => e.includes('Invalid number'))).toBe(true);
135
+ });
136
+ });
137
+
138
+ describe('with date field constraints', () => {
139
+ const dateFieldFormula = (operator: '*' | '/' | '+') =>
140
+ f([
141
+ { type: 'field', path: 'd', fieldType: 'date' },
142
+ { type: 'operator', value: operator },
143
+ { type: 'number', value: '1' },
144
+ ]);
145
+
146
+ test.each(['*', '/'] as const)('rejects %s operator with date fields', operator => {
147
+ const result = subject(dateFieldFormula(operator), new Set(['d']), new Set(['d']));
148
+ expect(
149
+ result.errors.some(e => e.includes('Multiply') && e.includes('divide')),
150
+ ).toBe(true);
151
+ });
152
+
153
+ test('allows + and - with date fields', () => {
154
+ const result = subject(dateFieldFormula('+'), new Set(['d']), new Set(['d']));
155
+ expect(result.valid).toBe(true);
156
+ });
157
+
158
+ test('rejects numbers larger than MAX_DATE_CALC_DAYS with date fields', () => {
159
+ const formula = f([
160
+ { type: 'field', path: 'd', fieldType: 'date' },
161
+ { type: 'operator', value: '+' },
162
+ { type: 'number', value: '5000' },
163
+ ]);
164
+ const result = subject(formula, new Set(['d']), new Set(['d']));
165
+ expect(result.errors.some(e => e.includes('Number of days cannot exceed'))).toBe(true);
166
+ });
167
+ });
168
+ });
@@ -0,0 +1,26 @@
1
+ import { ESignFieldType } from '../../../interface/types';
2
+ import { generateESignPath } from '../generate-e-sign-path';
3
+
4
+ describe('generateESignPath', () => {
5
+ const subject = (recipient: string, subType: ESignFieldType) =>
6
+ generateESignPath(recipient, subType);
7
+
8
+ test.each([
9
+ [ESignFieldType.signature, 'esign_signer1_signature'],
10
+ [ESignFieldType.initials, 'esign_signer1_initials'],
11
+ [ESignFieldType.dateSigned, 'esign_signer1_dateSigned'],
12
+ [ESignFieldType.fullName, 'esign_signer1_fullName'],
13
+ ])('returns esign_{recipient}_%s path', (subType, expected) => {
14
+ expect(subject('signer1', subType)).toBe(expected);
15
+ });
16
+
17
+ test('builds path with recipient containing special characters', () => {
18
+ expect(subject('recipient-1', ESignFieldType.signature)).toBe(
19
+ 'esign_recipient-1_signature',
20
+ );
21
+ });
22
+
23
+ test('builds path with empty recipient', () => {
24
+ expect(subject('', ESignFieldType.signature)).toBe('esign__signature');
25
+ });
26
+ });
@@ -0,0 +1,17 @@
1
+ import { generateFillablePath } from '../generate-fillable-path';
2
+
3
+ describe('generateFillablePath', () => {
4
+ const subject = (recipient: string, name: string) => generateFillablePath(recipient, name);
5
+
6
+ test('builds fillable_{recipient}_{name} path', () => {
7
+ expect(subject('signer1', 'fieldName')).toBe('fillable_signer1_fieldName');
8
+ });
9
+
10
+ test('builds path with empty recipient', () => {
11
+ expect(subject('', 'fieldName')).toBe('fillable__fieldName');
12
+ });
13
+
14
+ test('builds path with empty name', () => {
15
+ expect(subject('signer1', '')).toBe('fillable_signer1_');
16
+ });
17
+ });
@@ -0,0 +1,25 @@
1
+ import { parseFillablePathName } from '../parse-fillable-path';
2
+
3
+ describe('parseFillablePathName', () => {
4
+ const subject = (path: string | undefined) => parseFillablePathName(path);
5
+
6
+ test.each([
7
+ ['fillable_signer1_fieldName', 'fieldName'],
8
+ ['fillable_signer1_', ''],
9
+ ['fillable_signer1_some_uuid_with_underscores', 'some'],
10
+ ])('parses "%s" to "%s"', (path, expected) => {
11
+ expect(subject(path)).toBe(expected);
12
+ });
13
+
14
+ test('returns empty string for undefined path', () => {
15
+ expect(subject(undefined)).toBe('');
16
+ });
17
+
18
+ test('returns empty string for empty path', () => {
19
+ expect(subject('')).toBe('');
20
+ });
21
+
22
+ test('returns empty string when path has fewer than 3 parts', () => {
23
+ expect(subject('fillable_signer1')).toBe('');
24
+ });
25
+ });
@@ -0,0 +1,40 @@
1
+ import { RecipientInfo } from '../../../interface/types';
2
+ import { mapColorsToRecipients } from '../map-colors';
3
+
4
+ const buildRecipient = (id: number, name: string): RecipientInfo => ({
5
+ id,
6
+ name,
7
+ displayName: name,
8
+ });
9
+
10
+ describe('mapColorsToRecipients', () => {
11
+ const subject = (recipients?: RecipientInfo[]) => mapColorsToRecipients(recipients);
12
+
13
+ test('returns empty object when recipients is undefined', () => {
14
+ expect(subject(undefined)).toEqual({});
15
+ });
16
+
17
+ test('returns empty object when recipients is empty', () => {
18
+ expect(subject([])).toEqual({});
19
+ });
20
+
21
+ test('maps each recipient name to a distinct color', () => {
22
+ const recipients = [
23
+ buildRecipient(1, 'recipient_a'),
24
+ buildRecipient(2, 'recipient_b'),
25
+ buildRecipient(3, 'recipient_c'),
26
+ ];
27
+
28
+ const result = subject(recipients);
29
+
30
+ expect(Object.keys(result)).toEqual(['recipient_a', 'recipient_b', 'recipient_c']);
31
+ expect(new Set(Object.values(result)).size).toBe(3);
32
+ });
33
+
34
+ test('uses last recipient color when names collide', () => {
35
+ const result = subject([buildRecipient(1, 'duplicate'), buildRecipient(2, 'duplicate')]);
36
+
37
+ expect(Object.keys(result)).toEqual(['duplicate']);
38
+ expect(result.duplicate).toBeDefined();
39
+ });
40
+ });
@@ -0,0 +1,58 @@
1
+ import { dateToUtcZero, toDateInputValue } from '../date.utils';
2
+
3
+ describe('dateToUtcZero', () => {
4
+ const subject = (dateString: string) => dateToUtcZero(dateString);
5
+
6
+ test('converts YYYY-MM-DD string to UTC midnight ISO string', () => {
7
+ expect(subject('2024-01-15')).toBe('2024-01-15T00:00:00.000Z');
8
+ });
9
+
10
+ test('handles single-digit month and day', () => {
11
+ expect(subject('2024-03-05')).toBe('2024-03-05T00:00:00.000Z');
12
+ });
13
+
14
+ test('handles end of year dates', () => {
15
+ expect(subject('2024-12-31')).toBe('2024-12-31T00:00:00.000Z');
16
+ });
17
+
18
+ test('handles leap year dates', () => {
19
+ expect(subject('2024-02-29')).toBe('2024-02-29T00:00:00.000Z');
20
+ });
21
+ });
22
+
23
+ describe('toDateInputValue', () => {
24
+ const subject = (value: unknown) => toDateInputValue(value);
25
+
26
+ test.each([
27
+ ['null', null],
28
+ ['undefined', undefined],
29
+ ['empty string', ''],
30
+ ])('returns empty string for %s', (_, value) => {
31
+ expect(subject(value)).toBe('');
32
+ });
33
+
34
+ test('returns ISO date portion for valid Date object', () => {
35
+ const date = new Date(Date.UTC(2024, 0, 15, 12, 0, 0));
36
+ expect(subject(date)).toBe('2024-01-15');
37
+ });
38
+
39
+ test('returns empty string for invalid Date object', () => {
40
+ expect(subject(new Date('not-a-date'))).toBe('');
41
+ });
42
+
43
+ test('returns first 10 characters of an ISO date string', () => {
44
+ expect(subject('2024-01-15T12:30:00.000Z')).toBe('2024-01-15');
45
+ });
46
+
47
+ test('returns first 10 characters of a YYYY-MM-DD string as-is', () => {
48
+ expect(subject('2024-01-15')).toBe('2024-01-15');
49
+ });
50
+
51
+ test.each([
52
+ ['number', 12345],
53
+ ['boolean', true],
54
+ ['object', { foo: 'bar' }],
55
+ ])('returns empty string for unsupported %s value', (_, value) => {
56
+ expect(subject(value)).toBe('');
57
+ });
58
+ });
@@ -0,0 +1,48 @@
1
+ import { isValidNumber } from '../number.utils';
2
+
3
+ describe('isValidNumber', () => {
4
+ const subject = (value: string | number) => isValidNumber(value);
5
+
6
+ describe('with numeric input', () => {
7
+ test.each([
8
+ [0, true],
9
+ [1, true],
10
+ [-1, true],
11
+ [3.14, true],
12
+ [Number.MAX_SAFE_INTEGER, true],
13
+ ])('returns true for finite number %p', (value, expected) => {
14
+ expect(subject(value)).toBe(expected);
15
+ });
16
+
17
+ test.each([
18
+ [NaN, false],
19
+ [Infinity, false],
20
+ [-Infinity, false],
21
+ ])('returns false for non-finite number %p', (value, expected) => {
22
+ expect(subject(value)).toBe(expected);
23
+ });
24
+ });
25
+
26
+ describe('with string input', () => {
27
+ test.each([
28
+ ['0', true],
29
+ ['1', true],
30
+ ['-1', true],
31
+ ['3.14', true],
32
+ [' 42 ', true],
33
+ ['1e3', true],
34
+ ])('returns true for parseable string %p', (value, expected) => {
35
+ expect(subject(value)).toBe(expected);
36
+ });
37
+
38
+ test.each([
39
+ ['', false],
40
+ [' ', false],
41
+ ['abc', false],
42
+ ['1.2.3', false],
43
+ ['NaN', false],
44
+ ])('returns false for non-numeric string %p', (value, expected) => {
45
+ expect(subject(value)).toBe(expected);
46
+ });
47
+ });
48
+ });
@@ -4,3 +4,19 @@ export const dateToUtcZero = (dateString: string): string => {
4
4
 
5
5
  return utcDate.toISOString();
6
6
  };
7
+
8
+ export const toDateInputValue = (value: unknown): string => {
9
+ if (value == null || value === '') {
10
+ return '';
11
+ }
12
+
13
+ if (value instanceof Date) {
14
+ return Number.isNaN(value.getTime()) ? '' : value.toISOString().slice(0, 10);
15
+ }
16
+
17
+ if (typeof value === 'string') {
18
+ return value.slice(0, 10);
19
+ }
20
+
21
+ return '';
22
+ };