@servicetitan/dte-unlayer 0.157.0 → 0.159.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 (166) hide show
  1. package/dist/api-core.js +22 -29
  2. package/dist/api-core.js.map +1 -1
  3. package/dist/api-custom-tools.js +23 -29
  4. package/dist/api-custom-tools.js.map +1 -1
  5. package/dist/business-calendar/BusinessCalendarDrawer.d.ts +19 -0
  6. package/dist/business-calendar/BusinessCalendarDrawer.d.ts.map +1 -0
  7. package/dist/business-calendar/BusinessCalendarDrawer.js +41 -0
  8. package/dist/business-calendar/BusinessCalendarDrawer.js.map +1 -0
  9. package/dist/business-calendar/business-calendar-requests.d.ts +25 -0
  10. package/dist/business-calendar/business-calendar-requests.d.ts.map +1 -0
  11. package/dist/business-calendar/business-calendar-requests.js +46 -0
  12. package/dist/business-calendar/business-calendar-requests.js.map +1 -0
  13. package/dist/business-calendar/business-calendar-state.d.ts +22 -0
  14. package/dist/business-calendar/business-calendar-state.d.ts.map +1 -0
  15. package/dist/business-calendar/business-calendar-state.js +74 -0
  16. package/dist/business-calendar/business-calendar-state.js.map +1 -0
  17. package/dist/business-calendar/useBusinessCalendarRequests.d.ts +15 -0
  18. package/dist/business-calendar/useBusinessCalendarRequests.d.ts.map +1 -0
  19. package/dist/business-calendar/useBusinessCalendarRequests.js +30 -0
  20. package/dist/business-calendar/useBusinessCalendarRequests.js.map +1 -0
  21. package/dist/display-conditions/ConditionGroup.js +15 -89
  22. package/dist/display-conditions/ConditionGroup.js.map +1 -1
  23. package/dist/display-conditions/ConditionGroupsSection.js +2 -70
  24. package/dist/display-conditions/ConditionGroupsSection.js.map +1 -1
  25. package/dist/display-conditions/ConditionRow.d.ts.map +1 -1
  26. package/dist/display-conditions/ConditionRow.js +71 -296
  27. package/dist/display-conditions/ConditionRow.js.map +1 -1
  28. package/dist/display-conditions/DisplayConditionModal.js +40 -190
  29. package/dist/display-conditions/DisplayConditionModal.js.map +1 -1
  30. package/dist/display-conditions/LookupValueControl.d.ts.map +1 -1
  31. package/dist/display-conditions/LookupValueControl.js +42 -109
  32. package/dist/display-conditions/LookupValueControl.js.map +1 -1
  33. package/dist/display-conditions/SeparatorWithChip.js +1 -10
  34. package/dist/display-conditions/SeparatorWithChip.js.map +1 -1
  35. package/dist/display-conditions/constants.js +15 -20
  36. package/dist/display-conditions/constants.js.map +1 -1
  37. package/dist/display-conditions/displayConditionController.js +5 -8
  38. package/dist/display-conditions/displayConditionController.js.map +1 -1
  39. package/dist/display-conditions/lookupOptionsController.js +16 -16
  40. package/dist/display-conditions/lookupOptionsController.js.map +1 -1
  41. package/dist/display-conditions/nunjucks.js +98 -116
  42. package/dist/display-conditions/nunjucks.js.map +1 -1
  43. package/dist/display-conditions/schemaDataPoints.js +13 -11
  44. package/dist/display-conditions/schemaDataPoints.js.map +1 -1
  45. package/dist/display-conditions/types.js +23 -67
  46. package/dist/display-conditions/types.js.map +1 -1
  47. package/dist/display-conditions/useConditionForms.js +38 -56
  48. package/dist/display-conditions/useConditionForms.js.map +1 -1
  49. package/dist/display-conditions/useLookupOptions.js +24 -44
  50. package/dist/display-conditions/useLookupOptions.js.map +1 -1
  51. package/dist/e-sign-offset/ESignDateSignedOffsetModal.d.ts.map +1 -1
  52. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js +48 -156
  53. package/dist/e-sign-offset/ESignDateSignedOffsetModal.js.map +1 -1
  54. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts +3 -1
  55. package/dist/e-sign-offset/ESignOffsetRuleRow.d.ts.map +1 -1
  56. package/dist/e-sign-offset/ESignOffsetRuleRow.js +12 -71
  57. package/dist/e-sign-offset/ESignOffsetRuleRow.js.map +1 -1
  58. package/dist/e-sign-offset/eSignOffsetController.js +5 -8
  59. package/dist/e-sign-offset/eSignOffsetController.js.map +1 -1
  60. package/dist/editor-core-source.d.ts +2 -2
  61. package/dist/editor-core-source.d.ts.map +1 -1
  62. package/dist/editor-core-source.js +3 -3
  63. package/dist/editor-core-source.js.map +1 -1
  64. package/dist/editor-core.js +2 -2
  65. package/dist/editor-core.js.map +1 -1
  66. package/dist/editor.d.ts.map +1 -1
  67. package/dist/editor.js +56 -118
  68. package/dist/editor.js.map +1 -1
  69. package/dist/image-upload-error.js +3 -3
  70. package/dist/image-upload-error.js.map +1 -1
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +0 -1
  74. package/dist/index.js.map +1 -1
  75. package/dist/loadScript.js +7 -8
  76. package/dist/loadScript.js.map +1 -1
  77. package/dist/log.js +2 -3
  78. package/dist/log.js.map +1 -1
  79. package/dist/merge-tags.js +18 -17
  80. package/dist/merge-tags.js.map +1 -1
  81. package/dist/shared/configs.js +1 -2
  82. package/dist/shared/configs.js.map +1 -1
  83. package/dist/shared/const.d.ts +72 -0
  84. package/dist/shared/const.d.ts.map +1 -1
  85. package/dist/shared/const.js +13 -4
  86. package/dist/shared/const.js.map +1 -1
  87. package/dist/shared/date-calc.d.ts +32 -13
  88. package/dist/shared/date-calc.d.ts.map +1 -1
  89. package/dist/shared/date-calc.js +130 -76
  90. package/dist/shared/date-calc.js.map +1 -1
  91. package/dist/shared/e-sign-offset.js +46 -63
  92. package/dist/shared/e-sign-offset.js.map +1 -1
  93. package/dist/shared/edit-icon.js +0 -1
  94. package/dist/shared/edit-icon.js.map +1 -1
  95. package/dist/shared/fonts.js +16 -17
  96. package/dist/shared/fonts.js.map +1 -1
  97. package/dist/shared/forms.js +16 -38
  98. package/dist/shared/forms.js.map +1 -1
  99. package/dist/shared/instance-id.d.ts +3 -0
  100. package/dist/shared/instance-id.d.ts.map +1 -1
  101. package/dist/shared/instance-id.js +10 -7
  102. package/dist/shared/instance-id.js.map +1 -1
  103. package/dist/shared/nunjucks-helpers.js +2 -5
  104. package/dist/shared/nunjucks-helpers.js.map +1 -1
  105. package/dist/shared/schema.js +52 -96
  106. package/dist/shared/schema.js.map +1 -1
  107. package/dist/shared/tools.js +6 -13
  108. package/dist/shared/tools.js.map +1 -1
  109. package/dist/store.d.ts +15 -1
  110. package/dist/store.d.ts.map +1 -1
  111. package/dist/store.js +851 -547
  112. package/dist/store.js.map +1 -1
  113. package/dist/tools.js +133 -143
  114. package/dist/tools.js.map +1 -1
  115. package/dist/unlayer-interface.d.ts +25 -1
  116. package/dist/unlayer-interface.d.ts.map +1 -1
  117. package/dist/unlayer-interface.js +1 -2
  118. package/dist/unlayer-interface.js.map +1 -1
  119. package/dist/unlayer.js +86 -113
  120. package/dist/unlayer.js.map +1 -1
  121. package/dist/vendor/dte-shared/business-calendar-validation.utils.d.ts +25 -0
  122. package/dist/vendor/dte-shared/business-calendar-validation.utils.js +80 -0
  123. package/dist/vendor/dte-shared/components/business-calendar-section.d.ts +15 -0
  124. package/dist/vendor/dte-shared/components/business-calendar-section.js +14 -0
  125. package/dist/vendor/dte-shared/components/schedule-drawer.d.ts +18 -0
  126. package/dist/vendor/dte-shared/components/schedule-drawer.js +119 -0
  127. package/dist/vendor/dte-shared/components/schedule-picker.d.ts +8 -0
  128. package/dist/vendor/dte-shared/components/schedule-picker.js +88 -0
  129. package/dist/vendor/dte-shared/constants.d.ts +31 -0
  130. package/dist/vendor/dte-shared/constants.js +34 -0
  131. package/dist/vendor/dte-shared/field-schedule-editor-context.d.ts +31 -0
  132. package/dist/vendor/dte-shared/field-schedule-editor-context.js +110 -0
  133. package/dist/vendor/dte-shared/index.d.ts +8 -0
  134. package/dist/vendor/dte-shared/index.js +7 -0
  135. package/dist/vendor/dte-shared/schedule-bindings.utils.d.ts +22 -0
  136. package/dist/vendor/dte-shared/schedule-bindings.utils.js +31 -0
  137. package/dist/vendor/dte-shared/styles/schedule.css +19 -0
  138. package/dist/vendor/dte-shared/types.d.ts +129 -0
  139. package/dist/vendor/dte-shared/types.js +1 -0
  140. package/package.json +7 -1
  141. package/src/__tests__/business-calendar-drawer.test.tsx +183 -0
  142. package/src/__tests__/business-calendar-requests.test.ts +166 -0
  143. package/src/__tests__/business-calendar-state.test.tsx +142 -0
  144. package/src/__tests__/condition-row.test.tsx +131 -0
  145. package/src/__tests__/date-calc.test.ts +145 -0
  146. package/src/__tests__/e-sign-offset-modal.test.tsx +124 -0
  147. package/src/__tests__/instance-id.test.ts +49 -0
  148. package/src/__tests__/store.business-calendar.test.ts +185 -0
  149. package/src/__tests__/unlayer.e-sign-types.test.ts +30 -0
  150. package/src/business-calendar/BusinessCalendarDrawer.tsx +76 -0
  151. package/src/business-calendar/business-calendar-requests.ts +80 -0
  152. package/src/business-calendar/business-calendar-state.ts +113 -0
  153. package/src/business-calendar/useBusinessCalendarRequests.ts +49 -0
  154. package/src/display-conditions/ConditionRow.tsx +102 -175
  155. package/src/display-conditions/LookupValueControl.tsx +1 -12
  156. package/src/e-sign-offset/ESignDateSignedOffsetModal.tsx +20 -12
  157. package/src/e-sign-offset/ESignOffsetRuleRow.tsx +7 -2
  158. package/src/editor-core-source.ts +2 -2
  159. package/src/editor.tsx +15 -0
  160. package/src/index.ts +10 -0
  161. package/src/shared/const.ts +61 -0
  162. package/src/shared/date-calc.ts +155 -78
  163. package/src/shared/instance-id.ts +6 -1
  164. package/src/store.ts +68 -5
  165. package/src/unlayer-interface.tsx +25 -1
  166. package/src/unlayer.tsx +5 -5
@@ -0,0 +1,76 @@
1
+ import { ThemeProvider } from '@servicetitan/anvil2';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
3
+ import {
4
+ type BusinessCalendarHostProps,
5
+ type BusinessDaySchedule,
6
+ findSelectedSchedule,
7
+ ScheduleDrawer,
8
+ useFieldScheduleEditorContext,
9
+ } from '@servicetitan/dte-shared';
10
+ import { useEffect } from 'react';
11
+ import { createPortal } from 'react-dom';
12
+ import type { BusinessCalendarDrawerRequest } from './business-calendar-requests';
13
+
14
+ export interface BusinessCalendarDrawerProps {
15
+ /** The field and the schedule to edit (none: a new schedule), asked for from the iframe. */
16
+ request: BusinessCalendarDrawerRequest;
17
+ /** The editor's schedule props (`CreateUnlayerEditorProps`). */
18
+ host: BusinessCalendarHostProps;
19
+ /** Same contract as `DisplayConditionModalProps.portalContainer`. */
20
+ portalContainer?: Element | DocumentFragment | null;
21
+ onClose(): void;
22
+ }
23
+
24
+ /**
25
+ * The shared "Create New Schedule" / "Edit Schedule" drawer, opened in the parent window from a
26
+ * date field's inline schedule picker (a calculated field's, in the Formula Builder, or an e-sign
27
+ * "Date Signed Offset" field's, in its property panel), which runs inside the Unlayer iframe
28
+ * without the design system. As BYOD's picker does, a saved schedule is selected for the field.
29
+ */
30
+ export function BusinessCalendarDrawer({
31
+ host,
32
+ onClose,
33
+ portalContainer,
34
+ request,
35
+ }: Readonly<BusinessCalendarDrawerProps>) {
36
+ const businessCalendar = useFieldScheduleEditorContext(request.fieldKey, host);
37
+ const editingSchedule =
38
+ request.scheduleId === undefined
39
+ ? undefined
40
+ : findSelectedSchedule(host.schedules, request.scheduleId);
41
+
42
+ /*
43
+ * The schedule to edit was deleted meanwhile: close the request instead of turning the edit
44
+ * into a create, or of leaving it pending to open the drawer on its own later.
45
+ */
46
+ const scheduleMissing = request.scheduleId !== undefined && !editingSchedule;
47
+ useEffect(() => {
48
+ if (scheduleMissing) {
49
+ onClose();
50
+ }
51
+ }, [onClose, scheduleMissing]);
52
+
53
+ if (scheduleMissing) {
54
+ return null;
55
+ }
56
+
57
+ const handleSaved = (schedule: BusinessDaySchedule) => {
58
+ businessCalendar.onSelectSchedule(schedule.id);
59
+ onClose();
60
+ };
61
+
62
+ const drawer = (
63
+ <ScheduleDrawer
64
+ businessCalendar={businessCalendar}
65
+ editingSchedule={editingSchedule}
66
+ onClose={onClose}
67
+ onSaved={handleSaved}
68
+ />
69
+ );
70
+
71
+ if (portalContainer) {
72
+ return createPortal(<ThemeProvider>{drawer}</ThemeProvider>, portalContainer);
73
+ }
74
+
75
+ return drawer;
76
+ }
@@ -0,0 +1,80 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
2
+ import {
3
+ buildFieldScheduleEditorContext,
4
+ type BusinessCalendarHostProps,
5
+ type FieldScheduleBindingsAccess,
6
+ SCHEDULE_PICKER_TEXT,
7
+ } from '@servicetitan/dte-shared';
8
+ import type {
9
+ UnlayerEventBusinessCalendarRequest,
10
+ UnlayerEventBusinessCalendarResult,
11
+ } from '../shared/const';
12
+
13
+ /** The schedule drawer to show for a field: a new schedule, or `scheduleId` to edit. */
14
+ export interface BusinessCalendarDrawerRequest {
15
+ fieldKey: string;
16
+ scheduleId?: number;
17
+ }
18
+
19
+ export interface BusinessCalendarRequestHandlerDeps {
20
+ bindings: FieldScheduleBindingsAccess;
21
+ /** The editor's current schedule props. */
22
+ getHost(): BusinessCalendarHostProps;
23
+ openDrawer(request: BusinessCalendarDrawerRequest): void;
24
+ sendResult(result: UnlayerEventBusinessCalendarResult): void;
25
+ }
26
+
27
+ /**
28
+ * Carries out the iframe's schedule requests (`--business-calendar-request`) through the field's
29
+ * `ScheduleEditorContext`, the one BYOD's toggle and picker use, so a toggle, a pick or a delete
30
+ * changes the bindings exactly as there. A delete then reports back. Unlike `SchedulePicker` it
31
+ * doesn't select the default schedule for the field afterwards: the context's delete has already
32
+ * moved every field still on that schedule to the default one, and selecting it again would turn
33
+ * the field's toggle back on if it was switched off while the delete was running. A pick or a
34
+ * delete of a schedule the host doesn't list as one of the tenant's own is ignored or refused.
35
+ */
36
+ export const createBusinessCalendarRequestHandler =
37
+ ({ bindings, getHost, openDrawer, sendResult }: BusinessCalendarRequestHandlerDeps) =>
38
+ (request: UnlayerEventBusinessCalendarRequest) => {
39
+ const host = getHost();
40
+ const context = buildFieldScheduleEditorContext(request.fieldKey, host, bindings);
41
+ const isListedTenantSchedule = (scheduleId: number) =>
42
+ !!host.schedules?.some(schedule => schedule.id === scheduleId && !schedule.isDefault);
43
+
44
+ switch (request.action) {
45
+ case 'toggle':
46
+ context.onToggle(request.enabled);
47
+ return;
48
+ case 'select':
49
+ // No id is the default schedule.
50
+ if (
51
+ request.scheduleId === undefined ||
52
+ isListedTenantSchedule(request.scheduleId)
53
+ ) {
54
+ context.onSelectSchedule(request.scheduleId);
55
+ }
56
+ return;
57
+ case 'create':
58
+ openDrawer({ fieldKey: request.fieldKey });
59
+ return;
60
+ case 'edit':
61
+ openDrawer({ fieldKey: request.fieldKey, scheduleId: request.scheduleId });
62
+ return;
63
+ case 'delete': {
64
+ const { requestId, scheduleId } = request;
65
+ if (!context.onDeleteSchedule || !isListedTenantSchedule(scheduleId)) {
66
+ sendResult({ requestId, error: SCHEDULE_PICKER_TEXT.deleteFailed });
67
+ return;
68
+ }
69
+ context.onDeleteSchedule(scheduleId).then(
70
+ () => sendResult({ requestId }),
71
+ (e: unknown) =>
72
+ sendResult({
73
+ requestId,
74
+ error:
75
+ e instanceof Error ? e.message : SCHEDULE_PICKER_TEXT.deleteFailed,
76
+ }),
77
+ );
78
+ }
79
+ }
80
+ };
@@ -0,0 +1,113 @@
1
+ import { useEffect, useMemo } from 'react';
2
+ import type { UnlayerEventBusinessCalendarState } from '../shared/const';
3
+ import type { BusinessCalendar } from '../shared/date-calc';
4
+ import type { UnlayerStore } from '../store';
5
+ import type { CreateUnlayerEditorProps } from '../unlayer-interface';
6
+
7
+ type BusinessCalendarStateSource = Pick<
8
+ CreateUnlayerEditorProps,
9
+ | 'fieldCalendars'
10
+ | 'holidayCatalog'
11
+ | 'holidayCatalogError'
12
+ | 'holidayCatalogLoading'
13
+ | 'holidays'
14
+ | 'scheduleBindings'
15
+ | 'schedules'
16
+ | 'schedulesError'
17
+ | 'schedulesLoading'
18
+ > &
19
+ Pick<UnlayerEventBusinessCalendarState, 'canDeleteSchedule' | 'enabled'>;
20
+
21
+ /**
22
+ * The host's schedule props as the iframe needs them (`--business-calendar-state`): whether the
23
+ * schedule UI is on, each bound field's schedule, the schedules and holidays the inline picker
24
+ * shows, and the calendars the previews use.
25
+ *
26
+ * While the host edits schedules, only bound fields keep their calendar: a field whose toggle was
27
+ * just turned off uses the default calendar right away, without waiting for the host to drop its
28
+ * resolved calendar.
29
+ */
30
+ export const buildBusinessCalendarState = (
31
+ source: BusinessCalendarStateSource,
32
+ ): UnlayerEventBusinessCalendarState => {
33
+ const bindings: UnlayerEventBusinessCalendarState['bindings'] = {};
34
+ const boundCalendars: Record<string, BusinessCalendar> = {};
35
+
36
+ for (const { fieldKey, scheduleId } of source.scheduleBindings ?? []) {
37
+ bindings[fieldKey] = { scheduleId };
38
+
39
+ const calendar = source.fieldCalendars?.[fieldKey];
40
+ if (calendar) {
41
+ boundCalendars[fieldKey] = calendar;
42
+ }
43
+ }
44
+
45
+ return {
46
+ enabled: source.enabled,
47
+ bindings,
48
+ schedules: source.schedules ?? [],
49
+ schedulesLoading: source.schedulesLoading,
50
+ schedulesError: source.schedulesError,
51
+ holidayCatalog: source.holidayCatalog,
52
+ holidayCatalogLoading: source.holidayCatalogLoading,
53
+ holidayCatalogError: source.holidayCatalogError,
54
+ canDeleteSchedule: source.canDeleteSchedule,
55
+ fieldCalendars: source.enabled ? boundCalendars : (source.fieldCalendars ?? {}),
56
+ holidays: source.holidays,
57
+ };
58
+ };
59
+
60
+ /**
61
+ * Keeps the iframe's schedule state in step with the editor's props: rebuilt only when one of
62
+ * them changes (the callbacks count only for whether they are passed), and sent by the store only
63
+ * when the result differs from what the iframe has.
64
+ */
65
+ export function useBusinessCalendarState(store: UnlayerStore, opts: CreateUnlayerEditorProps) {
66
+ const {
67
+ fieldCalendars,
68
+ holidayCatalog,
69
+ holidayCatalogError,
70
+ holidayCatalogLoading,
71
+ holidays,
72
+ scheduleBindings,
73
+ schedules,
74
+ schedulesError,
75
+ schedulesLoading,
76
+ } = opts;
77
+ const enabled = !!opts.onScheduleBindingsChange;
78
+ const canDeleteSchedule = !!opts.onDeleteSchedule;
79
+
80
+ const state = useMemo(
81
+ () =>
82
+ buildBusinessCalendarState({
83
+ canDeleteSchedule,
84
+ enabled,
85
+ fieldCalendars,
86
+ holidayCatalog,
87
+ holidayCatalogError,
88
+ holidayCatalogLoading,
89
+ holidays,
90
+ scheduleBindings,
91
+ schedules,
92
+ schedulesError,
93
+ schedulesLoading,
94
+ }),
95
+ [
96
+ canDeleteSchedule,
97
+ enabled,
98
+ fieldCalendars,
99
+ holidayCatalog,
100
+ holidayCatalogError,
101
+ holidayCatalogLoading,
102
+ holidays,
103
+ scheduleBindings,
104
+ schedules,
105
+ schedulesError,
106
+ schedulesLoading,
107
+ ],
108
+ );
109
+
110
+ useEffect(() => {
111
+ store.setBusinessCalendarState(state);
112
+ }, [state, store]);
113
+ }
@@ -0,0 +1,49 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies -- bundled, see bundle-shared.mjs
2
+ import {
3
+ type BusinessCalendarHostProps,
4
+ useFieldScheduleBindingsAccess,
5
+ } from '@servicetitan/dte-shared';
6
+ import { useCallback, useEffect, useRef, useState } from 'react';
7
+ import type { UnlayerStore } from '../store';
8
+ import {
9
+ type BusinessCalendarDrawerRequest,
10
+ createBusinessCalendarRequestHandler,
11
+ } from './business-calendar-requests';
12
+
13
+ /**
14
+ * Handles the iframe's schedule requests for `UnlayerEditor`: changes the bindings through the
15
+ * host's callbacks, and returns the schedule drawer to show. Each drawer request carries a new
16
+ * `id`, to remount the drawer with its own starting values.
17
+ */
18
+ export function useBusinessCalendarRequests(store: UnlayerStore, host: BusinessCalendarHostProps) {
19
+ const [drawer, setDrawer] = useState<(BusinessCalendarDrawerRequest & { id: number }) | null>(
20
+ null,
21
+ );
22
+ const bindings = useFieldScheduleBindingsAccess(
23
+ host.scheduleBindings,
24
+ host.onScheduleBindingsChange,
25
+ );
26
+ const latestHost = useRef(host);
27
+ useEffect(() => {
28
+ latestHost.current = host;
29
+ });
30
+
31
+ useEffect(() => {
32
+ let drawerId = 0;
33
+
34
+ store.setOnBusinessCalendarRequest(
35
+ createBusinessCalendarRequestHandler({
36
+ bindings,
37
+ getHost: () => latestHost.current,
38
+ openDrawer: request => setDrawer({ ...request, id: ++drawerId }),
39
+ sendResult: store.sendBusinessCalendarResult,
40
+ }),
41
+ );
42
+
43
+ return () => store.setOnBusinessCalendarRequest();
44
+ }, [bindings, store]);
45
+
46
+ const closeDrawer = useCallback(() => setDrawer(null), []);
47
+
48
+ return { closeDrawer, drawer };
49
+ }
@@ -1,4 +1,4 @@
1
- import { Button, Chip, Combobox, Flex } from '@servicetitan/anvil2';
1
+ import { Button, Chip, Flex, SelectFieldSync } from '@servicetitan/anvil2';
2
2
  import TrashIcon from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
3
3
  import { useEffect, useMemo, useState } from 'react';
4
4
  import { FormInfo, parseFormFieldKey } from '../shared/forms';
@@ -48,7 +48,7 @@ export function ConditionRow({
48
48
 
49
49
  const sourceOptions = useMemo<SourceOption[]>(
50
50
  () => [
51
- { label: 'Data point', value: 'schema' },
51
+ { label: 'Merge tags', value: 'schema' },
52
52
  ...forms.map(form => ({
53
53
  formId: form.id,
54
54
  label: form.name,
@@ -129,194 +129,121 @@ export function ConditionRow({
129
129
  return;
130
130
  }
131
131
  setSelectedSourceValue(item.value);
132
+ // As in BYOD, a new source starts the condition over instead of picking a data point for it.
133
+ onChange({ ...condition, dataPointKey: '', operator: 'is_equal_to', value: '' });
132
134
 
133
- if (!item.formId) {
134
- const schemaField =
135
- dataPointOptions.find(opt => opt.fullKey === condition.dataPointKey) ?? null;
136
- const nextFieldType = schemaField?.fieldType ?? 'string';
137
- const nextIsNumber = nextFieldType === 'number';
138
- const nextOperatorItems = nextIsNumber ? NUMBER_OPERATORS : STRING_OPERATORS;
139
- const operatorReset = !nextOperatorItems.some(op => op.value === condition.operator);
140
-
141
- onChange({
142
- ...condition,
143
- dataPointKey: schemaField?.fullKey ?? '',
144
- operator: operatorReset
145
- ? nextIsNumber
146
- ? 'num_eq'
147
- : 'is_equal_to'
148
- : condition.operator,
149
- });
150
- return;
135
+ if (item.formId) {
136
+ onFormSelect(item.formId);
151
137
  }
152
-
153
- onFormSelect(item.formId);
154
- const nextFormFieldOptions = formFieldOptions.filter(
155
- option => option.formId === item.formId,
156
- );
157
- const currentSelected = nextFormFieldOptions.find(
158
- opt => opt.fullKey === condition.dataPointKey,
159
- );
160
- const nextSelected = currentSelected ?? nextFormFieldOptions[0] ?? null;
161
- const nextFieldType = nextSelected?.fieldType ?? 'string';
162
- const nextIsNumber = nextFieldType === 'number';
163
- const nextOperatorItems = nextIsNumber ? NUMBER_OPERATORS : STRING_OPERATORS;
164
- const operatorReset = !nextOperatorItems.some(op => op.value === condition.operator);
165
-
166
- onChange({
167
- ...condition,
168
- dataPointKey: nextSelected?.fullKey ?? '',
169
- operator: operatorReset
170
- ? nextIsNumber
171
- ? 'num_eq'
172
- : 'is_equal_to'
173
- : condition.operator,
174
- });
175
138
  };
176
139
 
140
+ // Same controls and layout as BYOD's condition row (`dte-pdf-editor`'s `ConditionRow`).
141
+ const sourceSelectOptions = sourceOptions.map(option => ({
142
+ id: option.value,
143
+ label: option.label,
144
+ }));
145
+ const dataPointSelectOptions = activeDataPointOptions.map(option => ({
146
+ id: option.fullKey,
147
+ label: option.title,
148
+ }));
149
+ const operatorSelectOptions = operatorItems.map(option => ({
150
+ id: option.value,
151
+ label: option.label,
152
+ }));
153
+
177
154
  return (
178
- <Flex direction="column" gap="3" style={{ width: '100%' }}>
179
- <div style={{ width: '100%' }}>
180
- <Combobox.Select
155
+ <Flex direction="column" gap="4" flex={1}>
156
+ <Flex flex={1} gap="2">
157
+ <SelectFieldSync
181
158
  flex={1}
182
- style={{ width: '100%' }}
183
- disableClearSelection
184
- itemToKey={(item: SourceOption | null) => item?.value ?? ''}
185
- itemToString={(item: SourceOption | null) => item?.label ?? ''}
186
- items={sourceOptions}
187
- selectedItem={selectedSource}
188
- onChange={handleSourceChange}
189
- >
190
- <Combobox.SelectTrigger
191
- label="Data Type"
192
- placeholder="Select source..."
193
- size="small"
159
+ disableSearch
160
+ disableClearButton
161
+ label="Source"
162
+ placeholder="Select source..."
163
+ options={sourceSelectOptions}
164
+ value={
165
+ selectedSource
166
+ ? { id: selectedSource.value, label: selectedSource.label }
167
+ : null
168
+ }
169
+ onSelectedOptionChange={(option: { id: string; label: string } | null) =>
170
+ handleSourceChange(sourceOptions.find(o => o.value === option?.id) ?? null)
171
+ }
172
+ />
173
+ </Flex>
174
+ <Flex
175
+ direction="row"
176
+ alignItems="flex-end"
177
+ gap="2"
178
+ flex={1}
179
+ style={{ flexWrap: 'wrap' }}
180
+ >
181
+ <Flex alignItems="flex-end" style={{ padding: '8px 12px' }}>
182
+ <Chip label="IF" size="medium" />
183
+ </Flex>
184
+ <Flex flex={1} style={{ minWidth: 160 }}>
185
+ <SelectFieldSync
186
+ flex={1}
187
+ disableSearch
188
+ disableClearButton
189
+ label="Data point"
190
+ placeholder={
191
+ currentFormId ? 'Select form field...' : 'Select data point...'
192
+ }
193
+ options={dataPointSelectOptions}
194
+ value={
195
+ selectedDataPoint
196
+ ? { id: selectedDataPoint.fullKey, label: selectedDataPoint.title }
197
+ : null
198
+ }
199
+ onSelectedOptionChange={(option: { id: string; label: string } | null) =>
200
+ handleDataPointChange(
201
+ activeDataPointOptions.find(o => o.fullKey === option?.id) ?? null,
202
+ )
203
+ }
204
+ />
205
+ </Flex>
206
+ <Flex flex={1} style={{ minWidth: 160 }}>
207
+ <SelectFieldSync
208
+ flex={1}
209
+ disableSearch
210
+ disableClearButton
211
+ label="Condition"
212
+ placeholder="Select..."
213
+ options={operatorSelectOptions}
214
+ value={
215
+ selectedOperator
216
+ ? { id: selectedOperator.value, label: selectedOperator.label }
217
+ : null
218
+ }
219
+ onSelectedOptionChange={(option: { id: string; label: string } | null) =>
220
+ onChange({
221
+ ...condition,
222
+ operator: (option?.id ??
223
+ 'is_equal_to') as SingleCondition['operator'],
224
+ })
225
+ }
194
226
  />
195
- <Combobox.Content>
196
- {({ items }: { items: SourceOption[] }) => (
197
- <Combobox.List>
198
- {items.map((item, i) => (
199
- <Combobox.Item index={i} item={item} key={item.value}>
200
- {item.label}
201
- </Combobox.Item>
202
- ))}
203
- </Combobox.List>
204
- )}
205
- </Combobox.Content>
206
- </Combobox.Select>
207
- </div>
208
- <Flex direction="row" alignItems="flex-end" gap="2" style={{ width: '100%' }}>
209
- <div
210
- style={{
211
- display: 'flex',
212
- alignItems: 'flex-end',
213
- flexShrink: 0,
214
- padding: '4px 8px',
215
- }}
216
- >
217
- <Chip label="IF" size="small" />
218
- </div>
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
- })
272
- }
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>
291
- </Flex>
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
227
  </Flex>
228
+ {!isValueLess && (
229
+ <Flex flex={1} style={{ minWidth: 160 }}>
230
+ <LookupValueControl
231
+ value={condition.value}
232
+ fieldType={fieldType}
233
+ lookupSource={selectedDataPoint?.lookupSource}
234
+ onChange={value => onChange({ ...condition, value })}
235
+ />
236
+ </Flex>
237
+ )}
303
238
  {canRemove && (
304
- <div
305
- style={{
306
- display: 'flex',
307
- alignItems: 'flex-end',
308
- flexShrink: 0,
309
- paddingBottom: 4,
310
- }}
311
- >
239
+ <Flex alignItems="flex-end" style={{ padding: '6px 12px' }}>
312
240
  <Button
313
241
  appearance="secondary"
314
242
  aria-label="Remove condition"
315
243
  icon={{ before: TrashIcon }}
316
- size="small"
317
244
  onClick={onRemove}
318
245
  />
319
- </div>
246
+ </Flex>
320
247
  )}
321
248
  </Flex>
322
249
  </Flex>
@@ -149,15 +149,7 @@ export function LookupValueControl({
149
149
 
150
150
  if (status === 'loading') {
151
151
  return (
152
- <TextField
153
- label="Value"
154
- size="small"
155
- flex={1}
156
- disabled
157
- value=""
158
- placeholder=""
159
- aria-label="Value"
160
- />
152
+ <TextField label="Value" flex={1} disabled value="" placeholder="" aria-label="Value" />
161
153
  );
162
154
  }
163
155
 
@@ -165,7 +157,6 @@ export function LookupValueControl({
165
157
  return (
166
158
  <SelectFieldSync
167
159
  flex={1}
168
- size="small"
169
160
  label="Value"
170
161
  placeholder="Search value..."
171
162
  options={staticOptionsWithSelected}
@@ -179,7 +170,6 @@ export function LookupValueControl({
179
170
  return (
180
171
  <SelectField
181
172
  flex={1}
182
- size="small"
183
173
  label="Value"
184
174
  placeholder="Type to search..."
185
175
  initialLoad="open"
@@ -202,7 +192,6 @@ export function LookupValueControl({
202
192
  return (
203
193
  <TextField
204
194
  label="Value"
205
- size="small"
206
195
  flex={1}
207
196
  value={value}
208
197
  onChange={e => handleTextChange(e.target.value)}