@servicetitan/dte-unlayer 0.154.0 → 0.156.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 (71) hide show
  1. package/dist/display-conditions/ConditionRow.d.ts.map +1 -1
  2. package/dist/display-conditions/ConditionRow.js +111 -95
  3. package/dist/display-conditions/ConditionRow.js.map +1 -1
  4. package/dist/display-conditions/DisplayConditionModal.d.ts +3 -3
  5. package/dist/display-conditions/DisplayConditionModal.d.ts.map +1 -1
  6. package/dist/display-conditions/DisplayConditionModal.js +8 -95
  7. package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
  8. package/dist/display-conditions/useConditionForms.d.ts +22 -0
  9. package/dist/display-conditions/useConditionForms.d.ts.map +1 -0
  10. package/dist/display-conditions/useConditionForms.js +110 -0
  11. package/dist/display-conditions/useConditionForms.js.map +1 -0
  12. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts +11 -0
  13. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts.map +1 -0
  14. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +192 -0
  15. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -0
  16. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts +20 -0
  17. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts.map +1 -0
  18. package/dist/e-sign-offset/ESignOffsetRuleRow.js +87 -0
  19. package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -0
  20. package/dist/e-sign-offset/eSignOffsetController.d.ts +10 -0
  21. package/dist/e-sign-offset/eSignOffsetController.d.ts.map +1 -0
  22. package/dist/e-sign-offset/eSignOffsetController.js +27 -0
  23. package/dist/e-sign-offset/eSignOffsetController.js.map +1 -0
  24. package/dist/editor-core-source.d.ts +2 -2
  25. package/dist/editor-core-source.d.ts.map +1 -1
  26. package/dist/editor-core-source.js +2 -2
  27. package/dist/editor-core-source.js.map +1 -1
  28. package/dist/editor.d.ts.map +1 -1
  29. package/dist/editor.js +8 -0
  30. package/dist/editor.js.map +1 -1
  31. package/dist/image-upload-error.d.ts +5 -0
  32. package/dist/image-upload-error.d.ts.map +1 -0
  33. package/dist/image-upload-error.js +8 -0
  34. package/dist/image-upload-error.js.map +1 -0
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/shared/const.d.ts +30 -0
  40. package/dist/shared/const.d.ts.map +1 -1
  41. package/dist/shared/const.js +5 -0
  42. package/dist/shared/const.js.map +1 -1
  43. package/dist/shared/e-sign-offset.d.ts +21 -0
  44. package/dist/shared/e-sign-offset.d.ts.map +1 -0
  45. package/dist/shared/e-sign-offset.js +130 -0
  46. package/dist/shared/e-sign-offset.js.map +1 -0
  47. package/dist/store.d.ts +1 -0
  48. package/dist/store.d.ts.map +1 -1
  49. package/dist/store.js +36 -11
  50. package/dist/store.js.map +1 -1
  51. package/dist/tools.d.ts +6 -0
  52. package/dist/tools.d.ts.map +1 -1
  53. package/dist/tools.js +21 -1
  54. package/dist/tools.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/__tests__/e-sign-offset.test.ts +281 -0
  57. package/src/__tests__/store.image-upload.test.ts +132 -0
  58. package/src/display-conditions/ConditionRow.tsx +86 -81
  59. package/src/display-conditions/DisplayConditionModal.tsx +12 -116
  60. package/src/display-conditions/useConditionForms.ts +146 -0
  61. package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +197 -0
  62. package/src/e-sign-offset/ESignOffsetRuleRow.tsx +101 -0
  63. package/src/e-sign-offset/eSignOffsetController.ts +39 -0
  64. package/src/editor-core-source.ts +2 -2
  65. package/src/editor.tsx +9 -0
  66. package/src/image-upload-error.ts +7 -0
  67. package/src/index.ts +1 -0
  68. package/src/shared/const.ts +37 -0
  69. package/src/shared/e-sign-offset.ts +173 -0
  70. package/src/store.ts +53 -8
  71. package/src/tools.ts +36 -1
@@ -0,0 +1,281 @@
1
+ import {
2
+ emitESignOffsetRequest,
3
+ onESignOffsetRequest,
4
+ } from '../e-sign-offset/eSignOffsetController';
5
+ import type { SingleCondition } from '../display-conditions/types';
6
+ import {
7
+ type ESignDateSignedOffsetRule,
8
+ type ESignDateSignedOffsetSettings,
9
+ MAX_E_SIGN_OFFSET_RULES,
10
+ } from '../shared/const';
11
+ import {
12
+ addESignOffsetRule,
13
+ buildESignOffsetSettings,
14
+ canAddESignOffsetRule,
15
+ clampESignOffsetDays,
16
+ createESignOffsetRule,
17
+ createESignOffsetSettings,
18
+ getESignOffsetConditionText,
19
+ getESignOffsetRuleCondition,
20
+ isCompleteESignOffsetCondition,
21
+ isESignOffsetSettings,
22
+ normalizeESignOffsetSettings,
23
+ removeESignOffsetRule,
24
+ splitESignOffsetSettings,
25
+ updateESignOffsetRule,
26
+ } from '../shared/e-sign-offset';
27
+ import { unlayerToolsBuildDesign, unlayerToolsNormalizeESignOffsets } from '../tools';
28
+ import type { UnlayerDesignCustomTool } from '../unlayer-interface';
29
+
30
+ const createCondition = (overrides: Partial<SingleCondition> = {}): SingleCondition => ({
31
+ dataPointKey: 'job.status',
32
+ id: 'c1',
33
+ operator: 'is_equal_to',
34
+ value: 'Completed',
35
+ ...overrides,
36
+ });
37
+
38
+ const createRule = (days: number, condition = createCondition()): ESignDateSignedOffsetRule => ({
39
+ condition: { behavior: 'show', groups: [{ conditions: [condition], id: `g-${days}` }] },
40
+ days,
41
+ });
42
+
43
+ describe('clampESignOffsetDays', () => {
44
+ test.each([
45
+ [14, 14],
46
+ [0, 0],
47
+ [-5, 0],
48
+ [400, 365],
49
+ [3.7, 3],
50
+ [Number.NaN, 0],
51
+ [null, 0],
52
+ ['7', 0],
53
+ ])('clamps %p to %p', (input, expected) => {
54
+ expect(clampESignOffsetDays(input)).toBe(expected);
55
+ });
56
+ });
57
+
58
+ describe('createESignOffsetSettings', () => {
59
+ test('defaults to a single unconditional zero-day rule', () => {
60
+ expect(createESignOffsetSettings()).toEqual({ rules: [{ days: 0 }] });
61
+ });
62
+
63
+ test('clamps the provided days', () => {
64
+ expect(createESignOffsetSettings(500)).toEqual({ rules: [{ days: 365 }] });
65
+ });
66
+ });
67
+
68
+ describe('createESignOffsetRule', () => {
69
+ test('creates a show rule with a single blank condition', () => {
70
+ const rule = createESignOffsetRule();
71
+
72
+ expect(rule.days).toBe(0);
73
+ expect(rule.condition.behavior).toBe('show');
74
+ expect(rule.condition.groups).toHaveLength(1);
75
+ expect(rule.condition.groups[0].logicalOperator).toBeUndefined();
76
+ expect(rule.condition.groups[0].conditions).toHaveLength(1);
77
+ expect(rule.condition.groups[0].conditions[0].logicalOperator).toBeUndefined();
78
+ });
79
+ });
80
+
81
+ describe('buildESignOffsetSettings / splitESignOffsetSettings', () => {
82
+ test('appends the unconditional final rule after the conditioned rules', () => {
83
+ const settings = buildESignOffsetSettings([createRule(3), createRule(7)], 14);
84
+
85
+ expect(settings.rules).toHaveLength(3);
86
+ expect(settings.rules[2]).toEqual({ days: 14 });
87
+ expect(splitESignOffsetSettings(settings)).toEqual({
88
+ conditionedRules: [createRule(3), createRule(7)],
89
+ finalDays: 14,
90
+ });
91
+ });
92
+
93
+ test('clamps days of every rule', () => {
94
+ const settings = buildESignOffsetSettings([createRule(900)], -2);
95
+
96
+ expect(settings.rules.map(rule => rule.days)).toEqual([365, 0]);
97
+ });
98
+ });
99
+
100
+ describe('isESignOffsetSettings', () => {
101
+ test('accepts a lone final rule and a conditioned chain', () => {
102
+ expect(isESignOffsetSettings({ rules: [{ days: 5 }] })).toBe(true);
103
+ expect(isESignOffsetSettings({ rules: [createRule(3), { days: 5 }] })).toBe(true);
104
+ });
105
+
106
+ test.each([
107
+ ['missing value', undefined],
108
+ ['missing rules', {}],
109
+ ['empty rules', { rules: [] }],
110
+ ['condition on the last rule', { rules: [createRule(3)] }],
111
+ ['unconditioned rule before the last', { rules: [{ days: 1 }, { days: 5 }] }],
112
+ ['days out of range', { rules: [{ days: 366 }] }],
113
+ ['negative days', { rules: [{ days: -1 }] }],
114
+ [
115
+ 'too many rules',
116
+ {
117
+ rules: [
118
+ ...Array.from({ length: MAX_E_SIGN_OFFSET_RULES }, (_, i) => createRule(i)),
119
+ { days: 0 },
120
+ ],
121
+ },
122
+ ],
123
+ ])('rejects %s', (_, value) => {
124
+ expect(isESignOffsetSettings(value)).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe('normalizeESignOffsetSettings', () => {
129
+ test('returns valid settings unchanged', () => {
130
+ const settings: ESignDateSignedOffsetSettings = { rules: [createRule(3), { days: 5 }] };
131
+
132
+ expect(normalizeESignOffsetSettings(settings)).toBe(settings);
133
+ });
134
+
135
+ test.each([undefined, null, 'x', {}, { rules: 'x' }])('defaults %p', value => {
136
+ expect(normalizeESignOffsetSettings(value)).toEqual({ rules: [{ days: 0 }] });
137
+ });
138
+
139
+ test('repairs malformed chains', () => {
140
+ const normalized = normalizeESignOffsetSettings({
141
+ rules: [createRule(500), { days: 2 }, createRule(20)],
142
+ });
143
+
144
+ expect(normalized).toEqual({ rules: [{ ...createRule(500), days: 365 }, { days: 20 }] });
145
+ expect(isESignOffsetSettings(normalized)).toBe(true);
146
+ });
147
+
148
+ test('drops conditioned rules beyond the maximum', () => {
149
+ const normalized = normalizeESignOffsetSettings({
150
+ rules: [
151
+ ...Array.from({ length: MAX_E_SIGN_OFFSET_RULES + 2 }, (_, i) => createRule(i)),
152
+ { days: 9 },
153
+ ],
154
+ });
155
+
156
+ expect(normalized.rules).toHaveLength(MAX_E_SIGN_OFFSET_RULES);
157
+ expect(normalized.rules[normalized.rules.length - 1]).toEqual({ days: 9 });
158
+ });
159
+ });
160
+
161
+ describe('rule list operations', () => {
162
+ test('adds rules up to the maximum, counting the final rule', () => {
163
+ let rules: ESignDateSignedOffsetRule[] = [];
164
+
165
+ for (let i = 0; i < MAX_E_SIGN_OFFSET_RULES + 2; i++) {
166
+ rules = addESignOffsetRule(rules);
167
+ }
168
+
169
+ expect(rules).toHaveLength(MAX_E_SIGN_OFFSET_RULES - 1);
170
+ expect(canAddESignOffsetRule(rules)).toBe(false);
171
+ });
172
+
173
+ test('updates and removes by index without mutating the input', () => {
174
+ const rules = [createRule(1), createRule(2)];
175
+ const updated = updateESignOffsetRule(rules, 1, createRule(9));
176
+ const removed = removeESignOffsetRule(rules, 0);
177
+
178
+ expect(updated.map(rule => rule.days)).toEqual([1, 9]);
179
+ expect(removed.map(rule => rule.days)).toEqual([2]);
180
+ expect(rules.map(rule => rule.days)).toEqual([1, 2]);
181
+ });
182
+
183
+ test('reads the single condition of a rule', () => {
184
+ expect(getESignOffsetRuleCondition(createRule(1))).toEqual(createCondition());
185
+ });
186
+ });
187
+
188
+ describe('isCompleteESignOffsetCondition', () => {
189
+ test.each([
190
+ ['complete string condition', createCondition(), 'string', true],
191
+ ['missing condition', undefined, 'string', false],
192
+ ['missing data point', createCondition({ dataPointKey: '' }), 'string', false],
193
+ ['blank value', createCondition({ value: ' ' }), 'string', false],
194
+ [
195
+ 'value-less operator',
196
+ createCondition({ operator: 'is_empty', value: '' }),
197
+ 'string',
198
+ true,
199
+ ],
200
+ ['numeric value', createCondition({ operator: 'num_gt', value: '64' }), 'number', true],
201
+ [
202
+ 'non-numeric value',
203
+ createCondition({ operator: 'num_gt', value: 'abc' }),
204
+ 'number',
205
+ false,
206
+ ],
207
+ ] as const)('%s', (_, condition, fieldType, expected) => {
208
+ expect(isCompleteESignOffsetCondition(condition, fieldType)).toBe(expected);
209
+ });
210
+ });
211
+
212
+ describe('getESignOffsetConditionText', () => {
213
+ test('formats string, number and value-less operators', () => {
214
+ expect(getESignOffsetConditionText(createCondition(), 'Job Status')).toBe(
215
+ 'Job Status is equal to Completed',
216
+ );
217
+ expect(
218
+ getESignOffsetConditionText(
219
+ createCondition({ operator: 'num_eq', value: '3' }),
220
+ 'Visits',
221
+ ),
222
+ ).toBe('Visits = 3');
223
+ expect(
224
+ getESignOffsetConditionText(createCondition({ operator: 'is_empty' }), 'Notes'),
225
+ ).toBe('Notes is empty');
226
+ });
227
+ });
228
+
229
+ describe('unlayerToolsNormalizeESignOffsets', () => {
230
+ const createESignTool = (values: Record<string, unknown>): UnlayerDesignCustomTool => ({
231
+ type: 'custom',
232
+ slug: 'e-sign',
233
+ values: { eSignRecipient: 'customer', ...values },
234
+ });
235
+
236
+ test('fills settings on offset placements saved without them and leaves the rest alone', () => {
237
+ const valid = { rules: [createRule(3), { days: 5 }] };
238
+ const legacyOffset = createESignTool({ eSignFieldType: 'dateSignedOffset' });
239
+ const configuredOffset = createESignTool({
240
+ eSignDateSignedOffset: valid,
241
+ eSignFieldType: 'dateSignedOffset',
242
+ });
243
+ const signature = createESignTool({ eSignFieldType: 'signature' });
244
+ const design = unlayerToolsBuildDesign([legacyOffset, configuredOffset, signature]);
245
+
246
+ expect(unlayerToolsNormalizeESignOffsets(design)).toBe(true);
247
+ expect(legacyOffset.values.eSignDateSignedOffset).toEqual({ rules: [{ days: 0 }] });
248
+ expect(configuredOffset.values.eSignDateSignedOffset).toBe(valid);
249
+ expect(signature.values).not.toHaveProperty('eSignDateSignedOffset');
250
+ expect(unlayerToolsNormalizeESignOffsets(design)).toBe(false);
251
+ });
252
+
253
+ test('tolerates a missing design', () => {
254
+ expect(unlayerToolsNormalizeESignOffsets(undefined as never)).toBe(false);
255
+ });
256
+ });
257
+
258
+ describe('eSignOffsetController', () => {
259
+ test('resolves with the original value when no modal is listening', () => {
260
+ const value = createESignOffsetSettings(4);
261
+ const done = jest.fn();
262
+
263
+ emitESignOffsetRequest({ done, value });
264
+
265
+ expect(done).toHaveBeenCalledWith(value);
266
+ });
267
+
268
+ test('delivers requests to the listener until it unsubscribes', () => {
269
+ const listener = jest.fn();
270
+ const unsubscribe = onESignOffsetRequest(listener);
271
+ const request = { done: jest.fn(), value: createESignOffsetSettings(4) };
272
+
273
+ emitESignOffsetRequest(request);
274
+ unsubscribe();
275
+ emitESignOffsetRequest(request);
276
+
277
+ expect(listener).toHaveBeenCalledTimes(1);
278
+ expect(listener).toHaveBeenCalledWith(request);
279
+ expect(request.done).toHaveBeenCalledTimes(1);
280
+ });
281
+ });
@@ -0,0 +1,132 @@
1
+ import { ImageUploadError } from '../image-upload-error';
2
+ import { UnlayerStore } from '../store';
3
+
4
+ const editorMock = {
5
+ addEventListener: jest.fn(),
6
+ removeEventListener: jest.fn(),
7
+ registerCallback: jest.fn(),
8
+ unregisterCallback: jest.fn(),
9
+ };
10
+
11
+ jest.mock('../loadScript', () => ({ loadScript: jest.fn().mockResolvedValue(undefined) }));
12
+ jest.mock('../unlayer', () => ({
13
+ ...jest.requireActual('../unlayer'),
14
+ createUnlayerEditor: jest.fn(() => editorMock),
15
+ }));
16
+
17
+ class LoadedImageMock {
18
+ width = 100;
19
+ height = 100;
20
+ onload?: () => void;
21
+ onerror?: () => void;
22
+
23
+ set src(_: string) {
24
+ setTimeout(() => this.onload?.());
25
+ }
26
+ }
27
+
28
+ const GENERIC_DESCRIPTION =
29
+ 'Something went wrong while uploading the image. Please try again or select another image.';
30
+
31
+ describe('UnlayerStore image upload', () => {
32
+ const originalImage = window.Image;
33
+ const originalCreateObjectURL = URL.createObjectURL;
34
+ const originalRevokeObjectURL = URL.revokeObjectURL;
35
+
36
+ let store: UnlayerStore;
37
+ let onError: jest.Mock;
38
+ let done: jest.Mock;
39
+
40
+ const uploadImage = () => {
41
+ const [, handler] = editorMock.registerCallback.mock.calls.find(
42
+ ([name]) => name === 'image',
43
+ );
44
+ const file = new File(['img'], 'logo.png', { type: 'image/png' });
45
+
46
+ return handler({ attachments: [file] }, done);
47
+ };
48
+
49
+ beforeEach(async () => {
50
+ jest.clearAllMocks();
51
+ jest.spyOn(console, 'error').mockImplementation(() => undefined);
52
+ window.Image = LoadedImageMock as unknown as typeof Image;
53
+ URL.createObjectURL = jest.fn(() => 'blob:logo');
54
+ URL.revokeObjectURL = jest.fn();
55
+ (window as any).unlayer = {};
56
+
57
+ onError = jest.fn();
58
+ done = jest.fn();
59
+ store = new UnlayerStore({ tools: [] });
60
+ store.setOnError(onError);
61
+
62
+ const container = document.createElement('div');
63
+ container.appendChild(document.createElement('iframe'));
64
+ await store.init(container);
65
+ });
66
+
67
+ afterEach(() => {
68
+ store.destroy();
69
+ window.Image = originalImage;
70
+ URL.createObjectURL = originalCreateObjectURL;
71
+ URL.revokeObjectURL = originalRevokeObjectURL;
72
+ jest.restoreAllMocks();
73
+ });
74
+
75
+ describe('when the host resolves with a url', () => {
76
+ beforeEach(async () => {
77
+ store.setOnImage(jest.fn().mockResolvedValue({ url: 'https://cdn/logo.png' }));
78
+ await uploadImage();
79
+ });
80
+
81
+ test('completes the upload with the url only', () => {
82
+ expect(done.mock.calls).toEqual([[{ progress: 100, url: 'https://cdn/logo.png' }]]);
83
+ });
84
+
85
+ test('does not report an error', () => {
86
+ expect(onError).not.toHaveBeenCalled();
87
+ });
88
+ });
89
+
90
+ describe('when the host rejects with an ImageUploadError', () => {
91
+ beforeEach(async () => {
92
+ store.setOnImage(
93
+ jest
94
+ .fn()
95
+ .mockRejectedValue(
96
+ new ImageUploadError(
97
+ 'Image dimensions 4320x2144 exceed maximum allowed 4000px',
98
+ ),
99
+ ),
100
+ );
101
+ await uploadImage();
102
+ });
103
+
104
+ test('reports the host message', () => {
105
+ expect(onError).toHaveBeenCalledWith(
106
+ 'Image upload failed',
107
+ 'Image dimensions 4320x2144 exceed maximum allowed 4000px',
108
+ );
109
+ });
110
+
111
+ test('does not report upload progress', () => {
112
+ expect(done).not.toHaveBeenCalled();
113
+ });
114
+ });
115
+
116
+ describe('when the host rejects with any other error', () => {
117
+ beforeEach(async () => {
118
+ store.setOnImage(
119
+ jest.fn().mockRejectedValue(new Error('Request failed with status code 500')),
120
+ );
121
+ await uploadImage();
122
+ });
123
+
124
+ test('reports the generic message', () => {
125
+ expect(onError).toHaveBeenCalledWith('Image upload failed', GENERIC_DESCRIPTION);
126
+ });
127
+
128
+ test('does not report upload progress', () => {
129
+ expect(done).not.toHaveBeenCalled();
130
+ });
131
+ });
132
+ });
@@ -178,6 +178,8 @@ export function ConditionRow({
178
178
  <Flex direction="column" gap="3" style={{ width: '100%' }}>
179
179
  <div style={{ width: '100%' }}>
180
180
  <Combobox.Select
181
+ flex={1}
182
+ style={{ width: '100%' }}
181
183
  disableClearSelection
182
184
  itemToKey={(item: SourceOption | null) => item?.value ?? ''}
183
185
  itemToString={(item: SourceOption | null) => item?.label ?? ''}
@@ -203,12 +205,7 @@ export function ConditionRow({
203
205
  </Combobox.Content>
204
206
  </Combobox.Select>
205
207
  </div>
206
- <Flex
207
- direction="row"
208
- alignItems="flex-end"
209
- gap="2"
210
- style={{ flexWrap: 'wrap', rowGap: 8, width: '100%' }}
211
- >
208
+ <Flex direction="row" alignItems="flex-end" gap="2" style={{ width: '100%' }}>
212
209
  <div
213
210
  style={{
214
211
  display: 'flex',
@@ -219,89 +216,97 @@ export function ConditionRow({
219
216
  >
220
217
  <Chip label="IF" size="small" />
221
218
  </div>
222
- <Flex flex={1} style={{ minWidth: 160 }}>
223
- <Combobox.Select
224
- flex={1}
225
- style={{ width: '100%' }}
226
- disableClearSelection
227
- itemToKey={(item: DataPointOption | null) => item?.fullKey ?? ''}
228
- itemToString={(item: DataPointOption | null) => item?.title ?? ''}
229
- items={activeDataPointOptions}
230
- selectedItem={selectedDataPoint}
231
- onChange={handleDataPointChange}
232
- >
233
- <Combobox.SelectTrigger
234
- label="Data Point"
235
- placeholder={
236
- currentFormId ? 'Select form field...' : 'Select data point...'
219
+ <Flex
220
+ flex={1}
221
+ direction="row"
222
+ alignItems="flex-end"
223
+ gap="2"
224
+ style={{ minWidth: 0, width: '100%' }}
225
+ >
226
+ <Flex flex={1} style={{ minWidth: 0, width: '100%' }}>
227
+ <Combobox.Select
228
+ flex={1}
229
+ style={{ width: '100%' }}
230
+ disableClearSelection
231
+ itemToKey={(item: DataPointOption | null) => item?.fullKey ?? ''}
232
+ itemToString={(item: DataPointOption | null) => item?.title ?? ''}
233
+ items={activeDataPointOptions}
234
+ selectedItem={selectedDataPoint}
235
+ onChange={handleDataPointChange}
236
+ >
237
+ <Combobox.SelectTrigger
238
+ label="Data Point"
239
+ placeholder={
240
+ currentFormId ? 'Select form field...' : 'Select data point...'
241
+ }
242
+ size="small"
243
+ />
244
+ <Combobox.Content>
245
+ {({ items }: { items: DataPointOption[] }) => (
246
+ <Combobox.List>
247
+ {items.map((item, i) => (
248
+ <Combobox.Item index={i} item={item} key={item.fullKey}>
249
+ {item.title}
250
+ </Combobox.Item>
251
+ ))}
252
+ </Combobox.List>
253
+ )}
254
+ </Combobox.Content>
255
+ </Combobox.Select>
256
+ </Flex>
257
+ <Flex flex={1} style={{ minWidth: 0, width: '100%' }}>
258
+ <Combobox.Select
259
+ flex={1}
260
+ style={{ width: '100%' }}
261
+ disableClearSelection
262
+ itemToKey={(item: OperatorOption | null) => item?.value ?? ''}
263
+ itemToString={(item: OperatorOption | null) => item?.label ?? ''}
264
+ items={operatorItems}
265
+ selectedItem={selectedOperator}
266
+ onChange={(item: OperatorOption | null) =>
267
+ onChange({
268
+ ...condition,
269
+ operator: (item?.value ??
270
+ 'is_equal_to') as SingleCondition['operator'],
271
+ })
237
272
  }
238
- size="small"
239
- />
240
- <Combobox.Content>
241
- {({ items }: { items: DataPointOption[] }) => (
242
- <Combobox.List>
243
- {items.map((item, i) => (
244
- <Combobox.Item index={i} item={item} key={item.fullKey}>
245
- {item.title}
246
- </Combobox.Item>
247
- ))}
248
- </Combobox.List>
249
- )}
250
- </Combobox.Content>
251
- </Combobox.Select>
252
- </Flex>
253
- <Flex flex={1} style={{ minWidth: 160 }}>
254
- <Combobox.Select
255
- flex={1}
256
- style={{ width: '100%' }}
257
- disableClearSelection
258
- itemToKey={(item: OperatorOption | null) => item?.value ?? ''}
259
- itemToString={(item: OperatorOption | null) => item?.label ?? ''}
260
- items={operatorItems}
261
- selectedItem={selectedOperator}
262
- onChange={(item: OperatorOption | null) =>
263
- onChange({
264
- ...condition,
265
- operator: (item?.value ??
266
- 'is_equal_to') as SingleCondition['operator'],
267
- })
268
- }
269
- >
270
- <Combobox.SelectTrigger
271
- label="Condition"
272
- placeholder="Select..."
273
- size="small"
274
- />
275
- <Combobox.Content>
276
- {({ items }: { items: OperatorOption[] }) => (
277
- <Combobox.List>
278
- {items.map((item, i) => (
279
- <Combobox.Item index={i} item={item} key={item.value}>
280
- {item.label}
281
- </Combobox.Item>
282
- ))}
283
- </Combobox.List>
284
- )}
285
- </Combobox.Content>
286
- </Combobox.Select>
287
- </Flex>
288
- {!isValueLess && (
289
- <Flex flex={1} style={{ minWidth: 160 }}>
290
- <LookupValueControl
291
- value={condition.value}
292
- fieldType={fieldType}
293
- lookupSource={selectedDataPoint?.lookupSource}
294
- onChange={value => onChange({ ...condition, value })}
295
- />
273
+ >
274
+ <Combobox.SelectTrigger
275
+ label="Condition"
276
+ placeholder="Select..."
277
+ size="small"
278
+ />
279
+ <Combobox.Content>
280
+ {({ items }: { items: OperatorOption[] }) => (
281
+ <Combobox.List>
282
+ {items.map((item, i) => (
283
+ <Combobox.Item index={i} item={item} key={item.value}>
284
+ {item.label}
285
+ </Combobox.Item>
286
+ ))}
287
+ </Combobox.List>
288
+ )}
289
+ </Combobox.Content>
290
+ </Combobox.Select>
296
291
  </Flex>
297
- )}
292
+ {!isValueLess && (
293
+ <Flex flex="0 0 186px" style={{ minWidth: 0 }}>
294
+ <LookupValueControl
295
+ value={condition.value}
296
+ fieldType={fieldType}
297
+ lookupSource={selectedDataPoint?.lookupSource}
298
+ onChange={value => onChange({ ...condition, value })}
299
+ />
300
+ </Flex>
301
+ )}
302
+ </Flex>
298
303
  {canRemove && (
299
304
  <div
300
305
  style={{
301
306
  display: 'flex',
302
307
  alignItems: 'flex-end',
303
308
  flexShrink: 0,
304
- padding: '4px 8px',
309
+ paddingBottom: 4,
305
310
  }}
306
311
  >
307
312
  <Button