@servicetitan/dte-pdf-editor 1.87.0 → 1.89.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 (81) hide show
  1. package/README.md +37 -30
  2. package/dist/components/display-conditions/condition-row.d.ts.map +1 -1
  3. package/dist/components/display-conditions/condition-row.js +51 -22
  4. package/dist/components/display-conditions/condition-row.js.map +1 -1
  5. package/dist/components/display-conditions/condition-value-control.d.ts.map +1 -1
  6. package/dist/components/display-conditions/condition-value-control.js +1 -2
  7. package/dist/components/display-conditions/condition-value-control.js.map +1 -1
  8. package/dist/components/display-conditions/display-condition-modal.d.ts.map +1 -1
  9. package/dist/components/display-conditions/display-condition-modal.js +6 -49
  10. package/dist/components/display-conditions/display-condition-modal.js.map +1 -1
  11. package/dist/components/field-config-panel/e-sign-date-rules-summary.d.ts +12 -0
  12. package/dist/components/field-config-panel/e-sign-date-rules-summary.d.ts.map +1 -0
  13. package/dist/components/field-config-panel/e-sign-date-rules-summary.js +37 -0
  14. package/dist/components/field-config-panel/e-sign-date-rules-summary.js.map +1 -0
  15. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.d.ts +16 -0
  16. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.d.ts.map +1 -0
  17. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.js +77 -0
  18. package/dist/components/field-config-panel/e-sign-date-signed-offset-modal.js.map +1 -0
  19. package/dist/components/field-config-panel/e-sign-offset-rule-row.d.ts +16 -0
  20. package/dist/components/field-config-panel/e-sign-offset-rule-row.d.ts.map +1 -0
  21. package/dist/components/field-config-panel/e-sign-offset-rule-row.js +25 -0
  22. package/dist/components/field-config-panel/e-sign-offset-rule-row.js.map +1 -0
  23. package/dist/components/field-config-panel/e-sign-options-modal.d.ts +15 -0
  24. package/dist/components/field-config-panel/e-sign-options-modal.d.ts.map +1 -0
  25. package/dist/components/field-config-panel/e-sign-options-modal.js +30 -0
  26. package/dist/components/field-config-panel/e-sign-options-modal.js.map +1 -0
  27. package/dist/components/field-config-panel/field-config-panel.d.ts.map +1 -1
  28. package/dist/components/field-config-panel/field-config-panel.js +30 -28
  29. package/dist/components/field-config-panel/field-config-panel.js.map +1 -1
  30. package/dist/components/field-sidebar/e-sign-field-type-list.d.ts.map +1 -1
  31. package/dist/components/field-sidebar/e-sign-field-type-list.js +1 -6
  32. package/dist/components/field-sidebar/e-sign-field-type-list.js.map +1 -1
  33. package/dist/constants/field.constants.d.ts +1 -5
  34. package/dist/constants/field.constants.d.ts.map +1 -1
  35. package/dist/constants/field.constants.js +21 -7
  36. package/dist/constants/field.constants.js.map +1 -1
  37. package/dist/hooks/usePdfFieldDnD.d.ts.map +1 -1
  38. package/dist/hooks/usePdfFieldDnD.js +13 -5
  39. package/dist/hooks/usePdfFieldDnD.js.map +1 -1
  40. package/dist/interface/types.d.ts +26 -1
  41. package/dist/interface/types.d.ts.map +1 -1
  42. package/dist/interface/types.js +1 -0
  43. package/dist/interface/types.js.map +1 -1
  44. package/dist/styles/field-config-panel.css +11 -0
  45. package/dist/styles/field-type.css +12 -0
  46. package/dist/styles/index.css +1 -0
  47. package/dist/styles/variables.css +2 -0
  48. package/dist/utils/conditions/condition-match-type.utils.d.ts +14 -0
  49. package/dist/utils/conditions/condition-match-type.utils.d.ts.map +1 -0
  50. package/dist/utils/conditions/condition-match-type.utils.js +50 -0
  51. package/dist/utils/conditions/condition-match-type.utils.js.map +1 -0
  52. package/dist/utils/conditions/e-sign-offset-rules.utils.d.ts +17 -0
  53. package/dist/utils/conditions/e-sign-offset-rules.utils.d.ts.map +1 -0
  54. package/dist/utils/conditions/e-sign-offset-rules.utils.js +34 -0
  55. package/dist/utils/conditions/e-sign-offset-rules.utils.js.map +1 -0
  56. package/dist/utils/conditions/index.d.ts +2 -0
  57. package/dist/utils/conditions/index.d.ts.map +1 -1
  58. package/dist/utils/conditions/index.js +2 -0
  59. package/dist/utils/conditions/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/components/display-conditions/condition-row.tsx +114 -131
  62. package/src/components/display-conditions/condition-value-control.tsx +1 -2
  63. package/src/components/display-conditions/display-condition-modal.tsx +13 -63
  64. package/src/components/field-config-panel/e-sign-date-rules-summary.tsx +127 -0
  65. package/src/components/field-config-panel/e-sign-date-signed-offset-modal.tsx +229 -0
  66. package/src/components/field-config-panel/e-sign-offset-rule-row.tsx +91 -0
  67. package/src/components/field-config-panel/e-sign-options-modal.tsx +118 -0
  68. package/src/components/field-config-panel/field-config-panel.tsx +70 -91
  69. package/src/components/field-sidebar/e-sign-field-type-list.tsx +11 -14
  70. package/src/constants/field.constants.ts +21 -8
  71. package/src/hooks/usePdfFieldDnD.ts +17 -2
  72. package/src/interface/types.ts +34 -1
  73. package/src/styles/field-config-panel.css +11 -0
  74. package/src/styles/field-type.css +12 -0
  75. package/src/styles/index.css +1 -0
  76. package/src/styles/variables.css +2 -0
  77. package/src/utils/conditions/__tests__/condition-match-type.utils.test.ts +104 -0
  78. package/src/utils/conditions/__tests__/e-sign-offset-rules.utils.test.ts +109 -0
  79. package/src/utils/conditions/condition-match-type.utils.ts +80 -0
  80. package/src/utils/conditions/e-sign-offset-rules.utils.ts +61 -0
  81. package/src/utils/conditions/index.ts +2 -0
@@ -1,7 +1,8 @@
1
- import { Button, Checkbox, Combobox, Flex, Textarea, TextField } from '@servicetitan/anvil2';
1
+ import { Button, Checkbox, Flex, SelectFieldSync, Textarea, TextField } from '@servicetitan/anvil2';
2
2
  import { FC, useMemo } from 'react';
3
- import { E_SIGN_FIELD_TYPE_OPTIONS, FIELD_FONT_SIZE_OPTIONS } from '../../constants';
3
+ import { FIELD_FONT_SIZE_OPTIONS } from '../../constants';
4
4
  import {
5
+ ESignDateSignedOffsetSettings,
5
6
  ESignFieldType,
6
7
  FieldTypeEnum,
7
8
  type FormFieldsByFormIdI,
@@ -19,6 +20,7 @@ import {
19
20
  supportsFieldFontSize,
20
21
  } from '../../utils';
21
22
  import { DisplayCondition } from '../display-conditions/display-condition';
23
+ import { ESignOptionsModal } from './e-sign-options-modal';
22
24
  import { FormulaGenerator } from './formula-generator';
23
25
  import { TableConfigs } from './table-configs';
24
26
 
@@ -56,39 +58,53 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
56
58
  }) => {
57
59
  const recipientOptions = useMemo(
58
60
  () =>
59
- field.type === FieldTypeEnum.fillable
61
+ (field.type === FieldTypeEnum.fillable
60
62
  ? getFillableFieldsRecipients(recipients)
61
- : recipients,
63
+ : recipients
64
+ ).map(recipient => ({ id: recipient.name, label: recipient.displayName })),
62
65
  [recipients, field.type],
63
66
  );
64
67
 
65
68
  const selectedRecipient = useMemo(
66
- () => recipientOptions.find(recipient => recipient.name === field.recipient) ?? null,
69
+ () => recipientOptions.find(option => option.id === field.recipient) ?? null,
67
70
  [recipientOptions, field.recipient],
68
71
  );
69
72
 
70
73
  const isRadioGroupSibling = (documentField: PdfField) =>
71
74
  documentField.subType === 'radio' && documentField.path === field.path;
72
75
 
73
- const handleRecipientChange = (item: RecipientInfo | null) => {
76
+ const isDateSignedOffsetSibling = (documentField: PdfField) =>
77
+ documentField.type === FieldTypeEnum.eSign &&
78
+ documentField.subType === ESignFieldType.dateSignedOffset;
79
+
80
+ const handleDateSignedOffsetSave = (data: ESignDateSignedOffsetSettings) => {
81
+ if (onMatchingFieldsConfigChange) {
82
+ onMatchingFieldsConfigChange(isDateSignedOffsetSibling, { data });
83
+ return;
84
+ }
85
+
86
+ onFieldConfigChange({ data });
87
+ };
88
+
89
+ const handleRecipientChange = (item: { id: string; label: string } | null) => {
74
90
  if (!item) {
75
91
  return;
76
92
  }
77
93
 
78
94
  let path = field.path;
79
95
  if (field.type === FieldTypeEnum.eSign) {
80
- path = generateESignPath(item.name, field.subType as ESignFieldType);
96
+ path = generateESignPath(item.id, field.subType as ESignFieldType);
81
97
  }
82
98
  if (field.type === FieldTypeEnum.fillable) {
83
99
  const pathName =
84
100
  field.subType === 'radio' && field.path
85
101
  ? parseFillablePathName(field.path)
86
102
  : field.id;
87
- path = generateFillablePath(item.name, pathName);
103
+ path = generateFillablePath(item.id, pathName);
88
104
  }
89
105
 
90
106
  const updates = {
91
- recipient: item.name,
107
+ recipient: item.id,
92
108
  path,
93
109
  };
94
110
 
@@ -105,31 +121,25 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
105
121
  onFieldConfigChange(updates);
106
122
  };
107
123
 
108
- const selectedESignFieldType = useMemo(
109
- () => E_SIGN_FIELD_TYPE_OPTIONS.find(option => option.id === field.subType) ?? null,
110
- [field],
111
- );
124
+ const showFontSizeControl = supportsFieldFontSize(field);
112
125
 
113
- const handleESignFieldTypeChange = (item: { id: ESignFieldType; name: string } | null) => {
114
- if (item) {
115
- onFieldConfigChange({
116
- subType: item.id,
117
- path: generateESignPath(field.recipient!, item.id),
118
- });
119
- }
120
- };
126
+ const fontSizeOptions = useMemo(
127
+ () => FIELD_FONT_SIZE_OPTIONS.map(size => ({ id: size, label: size.toString() })),
128
+ [],
129
+ );
121
130
 
122
- const showFontSizeControl = supportsFieldFontSize(field);
131
+ const selectedFontSize = useMemo(
132
+ () => fontSizeOptions.find(option => option.id === getFieldFontSize(field)) ?? null,
133
+ [fontSizeOptions, field],
134
+ );
123
135
 
124
- const handleFontSizeChange = (item: number | null) => {
125
- if (item) {
126
- onFieldConfigChange({
127
- styles: {
128
- ...field.styles,
129
- fontSize: item,
130
- },
131
- });
132
- }
136
+ const handleFontSizeChange = (item: { id: number; label: string }) => {
137
+ onFieldConfigChange({
138
+ styles: {
139
+ ...field.styles,
140
+ fontSize: item.id,
141
+ },
142
+ });
133
143
  };
134
144
 
135
145
  const handleRadioGroupNameChange = (groupName: string) => {
@@ -184,47 +194,14 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
184
194
  />
185
195
  )}
186
196
  {(field.type === FieldTypeEnum.fillable || field.type === FieldTypeEnum.eSign) && (
187
- <Combobox
188
- items={recipientOptions}
189
- itemToString={item => item?.displayName ?? ''}
190
- onChange={handleRecipientChange}
191
- selectedItem={selectedRecipient}
192
- filterOptions={{ keys: ['displayName'] }}
193
- >
194
- <Combobox.SearchField placeholder="" label="Recipient" />
195
- <Combobox.Content>
196
- {({ items }) => (
197
- <Combobox.List>
198
- {items.map((item, i) => (
199
- <Combobox.Item key={item.name} item={item} index={i}>
200
- {item.displayName}
201
- </Combobox.Item>
202
- ))}
203
- </Combobox.List>
204
- )}
205
- </Combobox.Content>
206
- </Combobox>
207
- )}
208
- {field.type === FieldTypeEnum.eSign && (
209
- <Combobox
210
- items={E_SIGN_FIELD_TYPE_OPTIONS}
211
- itemToString={item => item?.name ?? ''}
212
- onChange={handleESignFieldTypeChange}
213
- selectedItem={selectedESignFieldType}
214
- >
215
- <Combobox.SearchField label="Field Type" />
216
- <Combobox.Content>
217
- {({ items }) => (
218
- <Combobox.List>
219
- {items.map((item, i) => (
220
- <Combobox.Item key={item.id} item={item} index={i}>
221
- {item.name}
222
- </Combobox.Item>
223
- ))}
224
- </Combobox.List>
225
- )}
226
- </Combobox.Content>
227
- </Combobox>
197
+ <SelectFieldSync
198
+ label="Recipient"
199
+ disableClearButton
200
+ options={recipientOptions}
201
+ value={selectedRecipient}
202
+ onSelectedOptionChange={handleRecipientChange}
203
+ filter={{ keys: ['label'] }}
204
+ />
228
205
  )}
229
206
  {field.type !== FieldTypeEnum.generic && (
230
207
  <TextField label="Data Path" value={field.path} disabled />
@@ -271,31 +248,33 @@ export const FieldConfigPanel: FC<FieldConfigPanelProps> = ({
271
248
  )}
272
249
 
273
250
  {showFontSizeControl && (
274
- <Combobox
275
- items={FIELD_FONT_SIZE_OPTIONS}
276
- itemToString={item => item?.toString() ?? ''}
277
- onChange={handleFontSizeChange}
278
- selectedItem={getFieldFontSize(field)}
279
- >
280
- <Combobox.SearchField label="Font Size" />
281
- <Combobox.Content>
282
- {({ items }) => (
283
- <Combobox.List>
284
- {items.map((item, i) => (
285
- <Combobox.Item key={item} item={item} index={i}>
286
- {item}
287
- </Combobox.Item>
288
- ))}
289
- </Combobox.List>
290
- )}
291
- </Combobox.Content>
292
- </Combobox>
251
+ <SelectFieldSync
252
+ label="Font Size"
253
+ disableClearButton
254
+ options={fontSizeOptions}
255
+ value={selectedFontSize}
256
+ onSelectedOptionChange={handleFontSizeChange}
257
+ />
293
258
  )}
294
259
 
295
260
  {field.type === FieldTypeEnum.generic && field.subType === 'table' && (
296
261
  <TableConfigs field={field} onFieldConfigChange={onFieldConfigChange} />
297
262
  )}
298
263
 
264
+ {field.type === FieldTypeEnum.eSign &&
265
+ field.subType === ESignFieldType.dateSignedOffset && (
266
+ <ESignOptionsModal
267
+ initialState={field.data as ESignDateSignedOffsetSettings}
268
+ onSave={handleDateSignedOffsetSave}
269
+ documentFields={documentFields}
270
+ formFieldsByFormId={formFieldsByFormId}
271
+ formFieldsLoadingFormId={formulaLoadingFormId}
272
+ forms={forms}
273
+ onRequestFormFields={onRequestFormFields}
274
+ schema={dataModel}
275
+ />
276
+ )}
277
+
299
278
  {!hideConditionalLogic && (
300
279
  <DisplayCondition
301
280
  onSave={onFieldConfigChange}
@@ -1,4 +1,4 @@
1
- import { FC, Fragment } from 'react';
1
+ import { FC } from 'react';
2
2
  import { E_SIGN_FIELD_TYPES } from '../../constants';
3
3
  import { type FieldTypeOption } from '../../interface/types';
4
4
  import { FieldType } from './field-type';
@@ -10,18 +10,15 @@ interface ESignFieldTypeListProps {
10
10
 
11
11
  export const ESignFieldTypeList: FC<ESignFieldTypeListProps> = ({ onDragEnd, onDragStart }) => {
12
12
  return (
13
- <Fragment>
14
- {E_SIGN_FIELD_TYPES.map(fieldOption => {
15
- const key = `${fieldOption.type}-${fieldOption.path ?? fieldOption.label}`;
16
- return (
17
- <FieldType
18
- key={key}
19
- label={fieldOption.label}
20
- onDragEnd={onDragEnd}
21
- onDragStart={() => onDragStart(fieldOption)}
22
- />
23
- );
24
- })}
25
- </Fragment>
13
+ <div className="dte-field-type-items-wrapper">
14
+ {E_SIGN_FIELD_TYPES.map(fieldOption => (
15
+ <FieldType
16
+ key={fieldOption.subType}
17
+ label={fieldOption.label}
18
+ onDragEnd={onDragEnd}
19
+ onDragStart={() => onDragStart(fieldOption)}
20
+ />
21
+ ))}
22
+ </div>
26
23
  );
27
24
  };
@@ -43,13 +43,6 @@ export const GENERIC_FIELD_DEFAULT_SIZES = {
43
43
  table: { width: BASE_PAGE_WIDTH - 10, height: 2 * GENERIC_FIELD_TABLE_ROW_HEIGHT },
44
44
  } as Record<PdfFieldSubType, { width: number; height: number }>;
45
45
 
46
- export const E_SIGN_FIELD_TYPE_OPTIONS: { name: string; id: ESignFieldType }[] = [
47
- { name: 'Signature', id: ESignFieldType.signature },
48
- { name: 'Initials', id: ESignFieldType.initials },
49
- { name: 'Date Signed', id: ESignFieldType.dateSigned },
50
- { name: 'Full Name', id: ESignFieldType.fullName },
51
- ];
52
-
53
46
  export const FILLABLE_FIELD_TYPES: FieldTypeOption[] = [
54
47
  { label: 'Text Field', type: FieldTypeEnum.fillable, subType: 'text' },
55
48
  { label: 'Number Field', type: FieldTypeEnum.fillable, subType: 'number' },
@@ -60,10 +53,30 @@ export const FILLABLE_FIELD_TYPES: FieldTypeOption[] = [
60
53
 
61
54
  export const E_SIGN_FIELD_TYPES: FieldTypeOption[] = [
62
55
  {
63
- label: 'E-Sign',
56
+ label: 'Signature',
64
57
  type: FieldTypeEnum.eSign,
65
58
  subType: ESignFieldType.signature,
66
59
  },
60
+ {
61
+ label: 'Initials',
62
+ type: FieldTypeEnum.eSign,
63
+ subType: ESignFieldType.initials,
64
+ },
65
+ {
66
+ label: 'Date Signed',
67
+ type: FieldTypeEnum.eSign,
68
+ subType: ESignFieldType.dateSigned,
69
+ },
70
+ {
71
+ label: 'Date Signed Offset',
72
+ type: FieldTypeEnum.eSign,
73
+ subType: ESignFieldType.dateSignedOffset,
74
+ },
75
+ {
76
+ label: 'Full Name',
77
+ type: FieldTypeEnum.eSign,
78
+ subType: ESignFieldType.fullName,
79
+ },
67
80
  ];
68
81
 
69
82
  export const CALCULATED_FIELD_TYPES: FieldTypeOption[] = [
@@ -29,19 +29,33 @@ type InitializeDroppedField = (
29
29
  baseField: PdfField,
30
30
  option: FieldTypeOption,
31
31
  recipients: RecipientInfo[],
32
+ fields: PdfField[],
32
33
  ) => PdfField;
33
34
 
34
35
  const initializeDroppedFieldByType: Record<FieldTypeEnum, InitializeDroppedField> = {
35
- [FieldTypeEnum.eSign]: (base, option, recipients) => {
36
+ [FieldTypeEnum.eSign]: (base, option, recipients, fields) => {
36
37
  const recipientName = recipients[0]?.name ?? '';
37
- return {
38
+ const field = {
38
39
  ...base,
39
40
  recipient: recipientName,
40
41
  path: generateESignPath(recipientName, option.subType as ESignFieldType),
41
42
  };
43
+
44
+ if (field.subType === ESignFieldType.dateSignedOffset) {
45
+ const existingDateSignedOffsetField = fields.find(
46
+ existingField =>
47
+ existingField.type === FieldTypeEnum.eSign &&
48
+ existingField.subType === ESignFieldType.dateSignedOffset,
49
+ );
50
+ field.data = existingDateSignedOffsetField?.data ?? {
51
+ rules: [{ days: 0 }],
52
+ };
53
+ }
54
+ return field;
42
55
  },
43
56
  [FieldTypeEnum.fillable]: (base, option, recipients) => {
44
57
  const recipientName = getFillableFieldsRecipients(recipients)[0]?.name ?? '';
58
+
45
59
  const defaultSize = FILLABLE_FIELD_DEFAULT_SIZES[base.subType!];
46
60
  return {
47
61
  ...base,
@@ -216,6 +230,7 @@ export const usePdfFieldDnD = ({
216
230
  baseField,
217
231
  draggedFieldOption,
218
232
  recipients,
233
+ fields,
219
234
  );
220
235
 
221
236
  onFieldsChange([...fields, newField]);
@@ -11,6 +11,7 @@ export enum ESignFieldType {
11
11
  signature = 'signature',
12
12
  initials = 'initials',
13
13
  dateSigned = 'dateSigned',
14
+ dateSignedOffset = 'dateSignedOffset',
14
15
  fullName = 'fullName',
15
16
  }
16
17
 
@@ -56,7 +57,39 @@ export interface GenericFieldTextDataType {
56
57
  value: string;
57
58
  }
58
59
 
59
- export type FieldDataType = GenericFieldTableDataType | GenericFieldTextDataType;
60
+ export interface ESignDateSignedOffsetSettingsRule {
61
+ days: number;
62
+ condition: DisplayConditionState;
63
+ }
64
+
65
+ export interface ESignDateSignedOffsetSettingsFinalRule {
66
+ days: number;
67
+ condition?: never;
68
+ }
69
+
70
+ /**
71
+ * Ordered "if/else if/.../else" chain of day-offset rules for an e-sign `dateSignedOffset` field.
72
+ * Rules are evaluated in array order; the first rule whose `condition` matches applies its `days`.
73
+ *
74
+ * The tuple shape encodes two invariants the UI and evaluator both rely on:
75
+ * - The last element is always the trailing else case: no `condition`, so it always matches.
76
+ * - Every element before it is a normal, conditioned rule (`ESignDateSignedOffsetSettingsRule`).
77
+ *
78
+ * `rules` therefore always has at least one entry (just the else rule) and never has a
79
+ * condition-less rule anywhere but last. `ESignOptionsModal` is the only place that splits this
80
+ * tuple apart (the else row is edited inline; the conditioned rules are edited in
81
+ * `ESignDateSignedOffsetModal`) and reassembles it before saving.
82
+ */
83
+ export interface ESignDateSignedOffsetSettings {
84
+ rules:
85
+ | [ESignDateSignedOffsetSettingsFinalRule]
86
+ | [...ESignDateSignedOffsetSettingsRule[], ESignDateSignedOffsetSettingsFinalRule];
87
+ }
88
+
89
+ export type FieldDataType =
90
+ | GenericFieldTableDataType
91
+ | GenericFieldTextDataType
92
+ | ESignDateSignedOffsetSettings;
60
93
 
61
94
  export interface DisplayConditionState {
62
95
  behavior: 'show' | 'hide';
@@ -0,0 +1,11 @@
1
+ .dte-rule-card {
2
+ padding: 12px;
3
+ background: var(--color-neutral-30, #f7f7f7);
4
+ border: 1px solid var(--border-color);
5
+ border-radius: var(--radius-2);
6
+
7
+ &.--active {
8
+ border-color: var(--border-color-active, #2270ee);
9
+ background: var(--bg-color-secondary-active, #fafcff);
10
+ }
11
+ }
@@ -15,3 +15,15 @@
15
15
  .dte-field-type-item:hover {
16
16
  box-shadow: 0 0 2px 0 var(--border-color-active);
17
17
  }
18
+
19
+ .dte-field-type-items-wrapper {
20
+ &.--grid-2 {
21
+ display: grid;
22
+ grid-template-columns: 1fr 1fr;
23
+ gap: var(--spacing-1, 8px);
24
+
25
+ .dte-field-type-item {
26
+ margin-top: var(--spacing-0, 0);
27
+ }
28
+ }
29
+ }
@@ -10,3 +10,4 @@
10
10
  @import './field-type.css';
11
11
  @import './pdf-document-renderer.css';
12
12
  @import './inline-editable.css';
13
+ @import './field-config-panel.css';
@@ -28,11 +28,13 @@
28
28
  --color-neutral-70: var(--color-neutral-70, #737475);
29
29
  --color-neutral-200: var(--color-neutral-200, #6c6d70);
30
30
  --color-neutral-300: var(--color-neutral-300, #2d2e31);
31
+ --color-blue-100: var(--color-blue-100, #fafcff);
31
32
 
32
33
  /* Background colors */
33
34
  --bg-color-light: var(--color-neutral-0, #ffffff);
34
35
  --bg-color-main: var(--color-neutral-20, #fafafa);
35
36
  --bg-color-secondary: var(--color-neutral-50, #eeeeee);
37
+ --bg-color-secondary-active: var(--color-blue-100, #fafcff);
36
38
  --bg-color: var(--color-neutral-200, #f5f5f5);
37
39
  --bg-color-hover: #eaeaea;
38
40
 
@@ -0,0 +1,104 @@
1
+ import type { DisplayConditionSingle, DisplayConditionState } from '../../../interface/types';
2
+ import {
3
+ applyConditionMatchType,
4
+ deriveConditionMatchType,
5
+ isValidConditionGroups,
6
+ } from '../condition-match-type.utils';
7
+
8
+ const buildCondition = (overrides: Partial<DisplayConditionSingle> = {}): DisplayConditionSingle => ({
9
+ id: 'c1',
10
+ dataPointKey: '',
11
+ operator: 'is_equal_to',
12
+ value: '',
13
+ ...overrides,
14
+ });
15
+
16
+ describe('deriveConditionMatchType', () => {
17
+ test('defaults to "and" with a single condition', () => {
18
+ const state: DisplayConditionState = {
19
+ behavior: 'show',
20
+ groups: [{ id: 'g1', conditions: [buildCondition()] }],
21
+ };
22
+ expect(deriveConditionMatchType(state)).toBe('and');
23
+ });
24
+
25
+ test('returns "or" when a later condition in the group carries an "or" join', () => {
26
+ const state: DisplayConditionState = {
27
+ behavior: 'show',
28
+ groups: [
29
+ {
30
+ id: 'g1',
31
+ conditions: [buildCondition(), buildCondition({ id: 'c2', logicalOperator: 'or' })],
32
+ },
33
+ ],
34
+ };
35
+ expect(deriveConditionMatchType(state)).toBe('or');
36
+ });
37
+
38
+ test('returns "or" when a later group carries an "or" join', () => {
39
+ const state: DisplayConditionState = {
40
+ behavior: 'show',
41
+ groups: [
42
+ { id: 'g1', conditions: [buildCondition()] },
43
+ { id: 'g2', logicalOperator: 'or', conditions: [buildCondition({ id: 'c2' })] },
44
+ ],
45
+ };
46
+ expect(deriveConditionMatchType(state)).toBe('or');
47
+ });
48
+ });
49
+
50
+ describe('applyConditionMatchType', () => {
51
+ test('stamps the match type onto every condition after the first, and every group after the first', () => {
52
+ const state: DisplayConditionState = {
53
+ behavior: 'show',
54
+ groups: [
55
+ {
56
+ id: 'g1',
57
+ conditions: [buildCondition(), buildCondition({ id: 'c2' })],
58
+ },
59
+ { id: 'g2', conditions: [buildCondition({ id: 'c3' })] },
60
+ ],
61
+ };
62
+ const next = applyConditionMatchType(state, 'or');
63
+ expect(next.groups[0].conditions[0].logicalOperator).toBeUndefined();
64
+ expect(next.groups[0].conditions[1].logicalOperator).toBe('or');
65
+ expect(next.groups[1].logicalOperator).toBe('or');
66
+ });
67
+ });
68
+
69
+ describe('isValidConditionGroups', () => {
70
+ const deps = { fillableOptions: [], formFieldsByFormId: {}, mergeTagOptions: [] };
71
+
72
+ test('is false when there are no groups', () => {
73
+ expect(isValidConditionGroups([], deps)).toBe(false);
74
+ });
75
+
76
+ test('is false when a condition has no data point selected', () => {
77
+ const groups = [{ id: 'g1', conditions: [buildCondition({ dataPointKey: '' })] }];
78
+ expect(isValidConditionGroups(groups, deps)).toBe(false);
79
+ });
80
+
81
+ test('is false when a value-requiring condition has a blank value', () => {
82
+ const groups = [
83
+ { id: 'g1', conditions: [buildCondition({ dataPointKey: 'name', value: ' ' })] },
84
+ ];
85
+ expect(isValidConditionGroups(groups, deps)).toBe(false);
86
+ });
87
+
88
+ test('is true for a value-less operator with no value', () => {
89
+ const groups = [
90
+ {
91
+ id: 'g1',
92
+ conditions: [buildCondition({ dataPointKey: 'name', operator: 'is_empty', value: '' })],
93
+ },
94
+ ];
95
+ expect(isValidConditionGroups(groups, deps)).toBe(true);
96
+ });
97
+
98
+ test('is true when the data point and value are both present', () => {
99
+ const groups = [
100
+ { id: 'g1', conditions: [buildCondition({ dataPointKey: 'name', value: 'Acme' })] },
101
+ ];
102
+ expect(isValidConditionGroups(groups, deps)).toBe(true);
103
+ });
104
+ });
@@ -0,0 +1,109 @@
1
+ import type { ESignDateSignedOffsetSettingsRule } from '../../../interface/types';
2
+ import {
3
+ addESignOffsetRule,
4
+ canAddESignOffsetRule,
5
+ createClearedESignOffsetSettings,
6
+ createESignOffsetRule,
7
+ createInitialESignOffsetRules,
8
+ getESignOffsetRulesForSave,
9
+ MAX_ESIGN_OFFSET_RULES,
10
+ removeESignOffsetRule,
11
+ updateESignOffsetRule,
12
+ } from '../e-sign-offset-rules.utils';
13
+
14
+ describe('createESignOffsetRule', () => {
15
+ test('creates a normal rule with a single default condition group', () => {
16
+ const rule = createESignOffsetRule();
17
+ expect(rule.days).toBe(0);
18
+ expect(rule.condition.behavior).toBe('show');
19
+ expect(rule.condition.groups).toHaveLength(1);
20
+ expect(rule.condition.groups[0].conditions).toHaveLength(1);
21
+ });
22
+ });
23
+
24
+ describe('createInitialESignOffsetRules', () => {
25
+ test('returns the same single blank rule as first opening Set Date Rule', () => {
26
+ const rules = createInitialESignOffsetRules();
27
+ const initialRule = createESignOffsetRule();
28
+ expect(rules).toHaveLength(1);
29
+ expect(rules[0].days).toBe(initialRule.days);
30
+ expect(rules[0].condition.behavior).toBe(initialRule.condition.behavior);
31
+ expect(rules[0].condition.groups).toHaveLength(1);
32
+ expect(rules[0].condition.groups[0].conditions).toHaveLength(1);
33
+ });
34
+ });
35
+
36
+ describe('addESignOffsetRule', () => {
37
+ test('appends a new rule after any existing rules, preserving their order', () => {
38
+ const first = createESignOffsetRule();
39
+ const withSecond = addESignOffsetRule([first]);
40
+ expect(withSecond).toHaveLength(2);
41
+ expect(withSecond[0]).toBe(first);
42
+
43
+ const withThird = addESignOffsetRule(withSecond);
44
+ expect(withThird).toHaveLength(3);
45
+ expect(withThird[0]).toBe(first);
46
+ expect(withThird[1]).toBe(withSecond[1]);
47
+ });
48
+
49
+ test('is a no-op once the max rule count is reached', () => {
50
+ let rules: ESignDateSignedOffsetSettingsRule[] = [];
51
+ for (let i = 0; i < MAX_ESIGN_OFFSET_RULES + 3; i++) {
52
+ rules = addESignOffsetRule(rules);
53
+ }
54
+ expect(rules).toHaveLength(MAX_ESIGN_OFFSET_RULES - 1);
55
+ expect(canAddESignOffsetRule(rules)).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('updateESignOffsetRule', () => {
60
+ test('replaces the rule at the given index without mutating the input array', () => {
61
+ const rules = [createESignOffsetRule(), createESignOffsetRule()];
62
+ const updated: ESignDateSignedOffsetSettingsRule = { ...rules[1], days: 7 };
63
+ const next = updateESignOffsetRule(rules, 1, updated);
64
+ expect(next[1].days).toBe(7);
65
+ expect(rules[1].days).toBe(0);
66
+ });
67
+ });
68
+
69
+ describe('removeESignOffsetRule', () => {
70
+ test('removes the rule at the given index without mutating the input array', () => {
71
+ const first = createESignOffsetRule();
72
+ const second = createESignOffsetRule();
73
+ const rules = [first, second];
74
+ const next = removeESignOffsetRule(rules, 0);
75
+ expect(next).toEqual([second]);
76
+ expect(rules).toHaveLength(2);
77
+ });
78
+
79
+ test('returns an empty array when the last conditioned rule is removed', () => {
80
+ const next = removeESignOffsetRule([createESignOffsetRule()], 0);
81
+ expect(next).toEqual([]);
82
+ });
83
+ });
84
+
85
+ describe('getESignOffsetRulesForSave', () => {
86
+ test('saves an empty conditioned-rule list after clear when no rule is configured', () => {
87
+ const blankRules = createInitialESignOffsetRules();
88
+ const subject = () => getESignOffsetRulesForSave(blankRules, true, false);
89
+
90
+ expect(subject()).toEqual([]);
91
+ });
92
+
93
+ test('saves the configured rules when a rule is added after clear', () => {
94
+ const configured = [{ ...createESignOffsetRule(), days: 3 }];
95
+ const subject = () => getESignOffsetRulesForSave(configured, true, true);
96
+
97
+ expect(subject()).toBe(configured);
98
+ });
99
+ });
100
+
101
+ describe('createClearedESignOffsetSettings', () => {
102
+ test('keeps only a zero-day else rule so a cleared date rule can be saved', () => {
103
+ const subject = () => createClearedESignOffsetSettings();
104
+
105
+ expect(subject()).toEqual({
106
+ rules: [{ days: 0 }],
107
+ });
108
+ });
109
+ });