@flamingo-stack/openframe-frontend-core 0.0.739 → 0.0.741
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.
- package/dist/build/export-map.json +1 -1
- package/dist/{chunk-ENCAIAMY.cjs → chunk-4XL4YEZM.cjs} +3 -3
- package/dist/{chunk-FL6UO7IR.cjs → chunk-5SVOGLNK.cjs} +2 -2
- package/dist/{chunk-WFPCCBOM.cjs → chunk-7AM3AFM4.cjs} +10 -10
- package/dist/{chunk-S2CA5WE4.cjs → chunk-CWRVTQUO.cjs} +9 -1
- package/dist/{chunk-KHXZFLIN.cjs → chunk-DKOLQPE7.cjs} +8 -2
- package/dist/{chunk-UXFJXIUT.cjs → chunk-EKGL5K5K.cjs} +2 -2
- package/dist/{chunk-DTLHY2ZF.cjs → chunk-FQN6STIM.cjs} +12 -155
- package/dist/{chunk-V2ZFH42N.cjs → chunk-HYBMADYO.cjs} +3 -3
- package/dist/{chunk-NX6JQT25.cjs → chunk-I6UE6YVM.cjs} +4 -4
- package/dist/{chunk-SB4VWA5V.cjs → chunk-IK2RJUEA.cjs} +2118 -268
- package/dist/{chunk-CUDTMCXK.cjs → chunk-ITPEFCQ2.cjs} +13 -13
- package/dist/{chunk-WMKNZI4G.cjs → chunk-JFS6TZDX.cjs} +22 -22
- package/dist/{chunk-RGFT3P3L.cjs → chunk-LZ6NOVXX.cjs} +55 -55
- package/dist/{chunk-TF7J6VXG.cjs → chunk-QEUGYHFB.cjs} +9 -9
- package/dist/{chunk-II4QMH26.cjs → chunk-STEJZPIO.cjs} +3 -3
- package/dist/{chunk-UI3HROSI.cjs → chunk-XJ36TEGE.cjs} +33 -33
- package/dist/{chunk-UYP3G6PB.cjs → chunk-YQ37ZCD2.cjs} +2 -2
- package/dist/{chunk-Q7GG3WC5.cjs → chunk-ZJPDGVA6.cjs} +5 -5
- package/dist/{chunk-EGN4I5Q7.cjs → chunk-ZYOS7LVQ.cjs} +134 -46
- package/dist/{cloud-tenants-QSFSO3PA.cjs → cloud-tenants-MVJK52I2.cjs} +13 -13
- package/dist/components/case-studies/index.cjs +11 -11
- package/dist/components/chat/index.cjs +2 -2
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/features/capability-explorer.d.ts +9 -2
- package/dist/components/features/capability-explorer.d.ts.map +1 -1
- package/dist/components/features/capability-explorer.js +113 -6
- package/dist/components/features/index.cjs +141 -3
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +1 -0
- package/dist/components/features/scripts/execution-cells.d.ts +65 -0
- package/dist/components/features/scripts/execution-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-cells.js +75 -0
- package/dist/components/features/scripts/execution-status.d.ts +48 -0
- package/dist/components/features/scripts/execution-status.d.ts.map +1 -0
- package/dist/components/features/scripts/execution-status.js +15 -0
- package/dist/components/features/scripts/index.d.ts +20 -0
- package/dist/components/features/scripts/index.d.ts.map +1 -0
- package/dist/components/features/scripts/index.js +59 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts +18 -0
- package/dist/components/features/scripts/schedule-form/index.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/index.js +84 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts +24 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-enums.js +18 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts +57 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-fields.js +169 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts +138 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-form-types.js +72 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +54 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +60 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts +29 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +124 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts +52 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-picker-card.js +244 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts +35 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-script-search-field.js +58 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts +25 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-scripts-field.js +66 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts +77 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing-fields.js +262 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts +288 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.d.ts.map +1 -0
- package/dist/components/features/scripts/schedule-form/schedule-timing.js +165 -0
- package/dist/components/features/scripts/script-cells.d.ts +11 -0
- package/dist/components/features/scripts/script-cells.d.ts.map +1 -0
- package/dist/components/features/scripts/script-cells.js +19 -0
- package/dist/components/features/scripts/script-executions-table.d.ts +58 -0
- package/dist/components/features/scripts/script-executions-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-executions-table.js +88 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts +38 -0
- package/dist/components/features/scripts/script-schedules-table.d.ts.map +1 -0
- package/dist/components/features/scripts/script-schedules-table.js +71 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts +9 -0
- package/dist/components/features/scripts/script-shell-badge.d.ts.map +1 -0
- package/dist/components/features/scripts/script-shell-badge.js +15 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts +162 -0
- package/dist/components/features/scripts/scripts-table-columns.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-columns.js +69 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts +19 -0
- package/dist/components/features/scripts/scripts-table-shared.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table-shared.js +35 -0
- package/dist/components/features/scripts/scripts-table.d.ts +40 -0
- package/dist/components/features/scripts/scripts-table.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-table.js +84 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts +5 -0
- package/dist/components/features/scripts/scripts-tabs.d.ts.map +1 -0
- package/dist/components/features/scripts/scripts-tabs.js +10 -0
- package/dist/components/features/scripts/shell-types.d.ts +13 -0
- package/dist/components/features/scripts/shell-types.d.ts.map +1 -0
- package/dist/components/features/scripts/shell-types.js +21 -0
- package/dist/components/help-center-pages/index.cjs +53 -53
- package/dist/components/index.cjs +178 -34
- package/dist/components/meeting-scheduler/index.cjs +17 -17
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.cjs +5 -5
- package/dist/components/product-demo/fixtures/scripts.d.ts +34 -0
- package/dist/components/product-demo/fixtures/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/fixtures/scripts.js +123 -0
- package/dist/components/product-demo/fixtures/shared.d.ts +8 -0
- package/dist/components/product-demo/fixtures/shared.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/shared.js +7 -1
- package/dist/components/product-demo/fixtures/software-update.d.ts.map +1 -1
- package/dist/components/product-demo/fixtures/software-update.js +1 -5
- package/dist/components/product-demo/index.cjs +16 -13
- package/dist/components/product-demo/product-screen.d.ts.map +1 -1
- package/dist/components/product-demo/product-screen.js +2 -1
- package/dist/components/product-demo/registry.d.ts.map +1 -1
- package/dist/components/product-demo/registry.js +2 -1
- package/dist/components/product-demo/screen-keys.cjs +2 -1
- package/dist/components/product-demo/screen-keys.d.ts +1 -0
- package/dist/components/product-demo/screen-keys.d.ts.map +1 -1
- package/dist/components/product-demo/screen-keys.js +2 -1
- package/dist/components/product-demo/screens/scripts.d.ts +11 -0
- package/dist/components/product-demo/screens/scripts.d.ts.map +1 -0
- package/dist/components/product-demo/screens/scripts.js +130 -0
- package/dist/components/shared/legal-document/index.d.ts +2 -1
- package/dist/components/shared/legal-document/index.d.ts.map +1 -1
- package/dist/components/shared/legal-document/index.js +10 -2
- package/dist/components/shared/legal-document/legal-document-page.d.ts +35 -23
- package/dist/components/shared/legal-document/legal-document-page.d.ts.map +1 -1
- package/dist/components/shared/legal-document/legal-document-page.js +27 -10
- package/dist/components/shared/legal-document/use-legal-docs.d.ts +7 -3
- package/dist/components/shared/legal-document/use-legal-docs.d.ts.map +1 -1
- package/dist/components/shared/legal-document/use-legal-docs.js +7 -0
- package/dist/components/shared/legal-document/use-legal-documents.d.ts +31 -0
- package/dist/components/shared/legal-document/use-legal-documents.d.ts.map +1 -0
- package/dist/components/shared/legal-document/use-legal-documents.js +67 -0
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/ui/index.cjs +3 -3
- package/dist/{devices-C7XLWJFC.cjs → devices-W3S7Q2PG.cjs} +9 -9
- package/dist/index.cjs +150 -8
- package/dist/{knowledge-3QKWTOE7.cjs → knowledge-AIYQ4FC3.cjs} +7 -7
- package/dist/{logs-JXD75NR4.cjs → logs-7SHJ2ARE.cjs} +25 -25
- package/dist/{policies-ZPN53PFQ.cjs → policies-EIZJ5SUP.cjs} +10 -10
- package/dist/{remote-session-DNFZ5OLY.cjs → remote-session-BZMNE6D2.cjs} +5 -5
- package/dist/scripts-J2FWC7E4.cjs +2112 -0
- package/dist/{software-update-CRSIBS3K.cjs → software-update-QAMNASVF.cjs} +14 -18
- package/dist/{tickets-board-CKZRB6YV.cjs → tickets-board-CEDHD3KB.cjs} +17 -17
- package/dist/utils/index.cjs +8 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/legal-documents.d.ts +18 -0
- package/dist/utils/legal-documents.d.ts.map +1 -0
- package/dist/utils/legal-documents.js +8 -0
- package/package.json +1 -1
- package/src/components/features/capability-explorer.tsx +134 -7
- package/src/components/features/index.ts +1 -0
- package/src/components/features/scripts/execution-cells.tsx +149 -0
- package/src/components/features/scripts/execution-status.ts +42 -0
- package/src/components/features/scripts/index.ts +42 -0
- package/src/components/features/scripts/schedule-form/index.ts +59 -0
- package/src/components/features/scripts/schedule-form/schedule-enums.ts +26 -0
- package/src/components/features/scripts/schedule-form/schedule-form-fields.tsx +249 -0
- package/src/components/features/scripts/schedule-form/schedule-form-types.ts +189 -0
- package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +121 -0
- package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +218 -0
- package/src/components/features/scripts/schedule-form/schedule-script-picker-card.tsx +321 -0
- package/src/components/features/scripts/schedule-form/schedule-script-search-field.tsx +115 -0
- package/src/components/features/scripts/schedule-form/schedule-scripts-field.tsx +97 -0
- package/src/components/features/scripts/schedule-form/schedule-timing-fields.tsx +482 -0
- package/src/components/features/scripts/schedule-form/schedule-timing.ts +484 -0
- package/src/components/features/scripts/script-cells.tsx +29 -0
- package/src/components/features/scripts/script-executions-table.tsx +145 -0
- package/src/components/features/scripts/script-schedules-table.tsx +113 -0
- package/src/components/features/scripts/script-shell-badge.tsx +25 -0
- package/src/components/features/scripts/scripts-table-columns.ts +95 -0
- package/src/components/features/scripts/scripts-table-shared.tsx +43 -0
- package/src/components/features/scripts/scripts-table.tsx +124 -0
- package/src/components/features/scripts/scripts-tabs.ts +11 -0
- package/src/components/features/scripts/shell-types.ts +26 -0
- package/src/components/product-demo/fixtures/scripts.ts +186 -0
- package/src/components/product-demo/fixtures/shared.ts +7 -0
- package/src/components/product-demo/fixtures/software-update.ts +1 -7
- package/src/components/product-demo/product-screen.tsx +3 -1
- package/src/components/product-demo/registry.ts +1 -0
- package/src/components/product-demo/screen-keys.ts +1 -0
- package/src/components/product-demo/screens/scripts.tsx +192 -0
- package/src/components/shared/legal-document/index.ts +8 -0
- package/src/components/shared/legal-document/legal-document-page.tsx +83 -44
- package/src/components/shared/legal-document/use-legal-docs.ts +18 -3
- package/src/components/shared/legal-document/use-legal-documents.ts +113 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/legal-documents.ts +21 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
'use no memo';
|
|
3
|
+
|
|
4
|
+
import { useCallback, useMemo } from 'react';
|
|
5
|
+
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
|
6
|
+
import { cn } from '../../../../utils/cn';
|
|
7
|
+
import { XmarkCircleIcon } from '../../../icons-v2-generated/signs-and-symbols/xmark-circle-icon';
|
|
8
|
+
import { CheckboxBlock } from '../../../ui/checkbox-block';
|
|
9
|
+
import { DatePickerInputSimple } from '../../../ui/date-picker';
|
|
10
|
+
import { Label } from '../../../ui/label';
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../ui/select';
|
|
12
|
+
import type { ScheduleTimeReference } from './schedule-enums';
|
|
13
|
+
import {
|
|
14
|
+
SCHEDULE_TIME_REFERENCE_OPTIONS,
|
|
15
|
+
type ScheduleFormData,
|
|
16
|
+
type ScheduleFormGate,
|
|
17
|
+
type ScheduleTimeSlot,
|
|
18
|
+
} from './schedule-form-types';
|
|
19
|
+
import { ScheduleIntervalInput } from './schedule-interval-input';
|
|
20
|
+
import {
|
|
21
|
+
DURATION_UNIT_OPTIONS,
|
|
22
|
+
type DurationUnit,
|
|
23
|
+
earliestScheduleDay,
|
|
24
|
+
getTimeSlotOptions,
|
|
25
|
+
isEventTrigger,
|
|
26
|
+
isScheduleStartInPast,
|
|
27
|
+
isSlotOnGrid,
|
|
28
|
+
isStartInPastAndChanged,
|
|
29
|
+
MIN_REPEAT_MINUTES,
|
|
30
|
+
NO_SLOTS_TODAY_MESSAGE,
|
|
31
|
+
PAST_START_MESSAGE,
|
|
32
|
+
slotToLabel,
|
|
33
|
+
snapRepeatInterval,
|
|
34
|
+
} from './schedule-timing';
|
|
35
|
+
|
|
36
|
+
export interface ScheduleTimingFieldsProps {
|
|
37
|
+
showErrors: boolean;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Whether the Timezone picker is offered. It is a layout switch (it turns one
|
|
41
|
+
* row into two), so a host that reads it from a flag passes the flag's GATE.
|
|
42
|
+
* Default `off`.
|
|
43
|
+
*/
|
|
44
|
+
timeReferenceGate?: ScheduleFormGate;
|
|
45
|
+
/**
|
|
46
|
+
* The Time dropdown's options for a picked day and reading. Default
|
|
47
|
+
* `getTimeSlotOptions`: the viewer's own clock and locale. A picture of the
|
|
48
|
+
* form passes fixed readings, so a server and a browser draw the same one.
|
|
49
|
+
*/
|
|
50
|
+
getTimeSlots?: (date: Date | null, timeReference: ScheduleTimeReference) => ScheduleTimeSlot[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Date | Time | Timezone | Repeat | Repeat in — the timing a DATE_TIME schedule
|
|
55
|
+
* fires on.
|
|
56
|
+
*
|
|
57
|
+
* `scheduledDate` + `scheduledTime` combine into the backend's `startAt` (30-min
|
|
58
|
+
* boundary, enforced by the Time slots) and are both required; the repeat toggle
|
|
59
|
+
* + interval + unit become `repeat` seconds.
|
|
60
|
+
*
|
|
61
|
+
* **Timezone** (`timeReference`, design node 793:61340) says what that pair
|
|
62
|
+
* MEANS — the account's clock (SERVER, one instant worldwide) or each device's
|
|
63
|
+
* (DEVICE_LOCAL, one reading re-based per device). It is behind the
|
|
64
|
+
* host's `timeReferenceGate` (the product's `script-schedule-device-time`
|
|
65
|
+
* flag), and it is the field that makes this a
|
|
66
|
+
* five-control row: with the picker on, the design wraps Repeat onto a second
|
|
67
|
+
* line, which is why the row is a 4-column GRID rather than the flex row it
|
|
68
|
+
* used to be — the wrap is then a `col-start-1` on one cell instead of a second
|
|
69
|
+
* layout.
|
|
70
|
+
*
|
|
71
|
+
* Recurrence stays OFFERED for DEVICE_LOCAL — "every day at 9 AM on each
|
|
72
|
+
* device's own clock" is the reason the reading exists — even though the API
|
|
73
|
+
* refuses it today ("A DEVICE_LOCAL schedule does not support repeat **yet**",
|
|
74
|
+
* `ScheduleScriptService.validateTiming`). Saving one is answered by that
|
|
75
|
+
* message in the error toast until the backend drops the check; nothing here
|
|
76
|
+
* has to change when it does. Its offline setting is refused the same way and
|
|
77
|
+
* IS hidden, because that one is not merely unimplemented: the device-local
|
|
78
|
+
* runner already retries on reconnect within its catch-up window, so the
|
|
79
|
+
* setting would have nothing to add — see `ScheduleOfflineFields`.
|
|
80
|
+
*
|
|
81
|
+
* What it does NOT constrain is the clock. The picked pair is a wall clock every
|
|
82
|
+
* device reads on its own timezone, so the viewer's "already gone by" says
|
|
83
|
+
* nothing about the fleet's: all 48 slots stay offered, the past-start rule is
|
|
84
|
+
* off, and the only bound left is the day one (`earliestScheduleDay` — today as
|
|
85
|
+
* read in UTC−12). The API validates no start against the past either, for
|
|
86
|
+
* either reading.
|
|
87
|
+
*
|
|
88
|
+
* Only DATE_TIME schedules have timing, so the row collapses when the event
|
|
89
|
+
* trigger is picked. It stays MOUNTED (a toggle back restores what was typed)
|
|
90
|
+
* and animates with the `0fr → 1fr` grid-rows technique — the portable way to
|
|
91
|
+
* transition to an intrinsic height, since `interpolate-size` / `calc-size()`
|
|
92
|
+
* still aren't cross-browser. `inert` drops the collapsed fields out of the tab
|
|
93
|
+
* order and the accessibility tree.
|
|
94
|
+
*
|
|
95
|
+
* The `overflow-hidden` the technique requires also clips anything hanging below
|
|
96
|
+
* the fields — and field errors are absolutely positioned there (out of flow, so
|
|
97
|
+
* showing one never reflows the form). The bottom padding is the room they render
|
|
98
|
+
* into; the constant negative margin cancels it again, so the spacing below the
|
|
99
|
+
* row is unchanged and a COLLAPSED row leaves no 24px hole.
|
|
100
|
+
*
|
|
101
|
+
* That padding sits on the field row INSIDE the clipping box, not on the box
|
|
102
|
+
* itself, and the difference is load-bearing: a box is never shorter than its own
|
|
103
|
+
* padding — `border-box` does not change that, it only decides what a specified
|
|
104
|
+
* height includes — so padding on the grid item survives the 0fr track as 24px of
|
|
105
|
+
* residue. The negative margin then spends itself cancelling that instead of
|
|
106
|
+
* closing the section gap, and the collapsed row leaves the hole this was written
|
|
107
|
+
* to avoid. Nested one level down it is clipped like everything else, and the row
|
|
108
|
+
* measures 0.
|
|
109
|
+
*/
|
|
110
|
+
export function ScheduleTimingFields({
|
|
111
|
+
showErrors,
|
|
112
|
+
disabled = false,
|
|
113
|
+
timeReferenceGate = 'off',
|
|
114
|
+
getTimeSlots = getTimeSlotOptions,
|
|
115
|
+
}: ScheduleTimingFieldsProps) {
|
|
116
|
+
const { control, getValues, setValue, trigger: triggerValidation } = useFormContext<ScheduleFormData>();
|
|
117
|
+
|
|
118
|
+
// The Timezone picker is a layout switch — it turns one row into two — so the
|
|
119
|
+
// flag is read as a GATE and the unanswered window is not guessed at. There is
|
|
120
|
+
// nothing to draw for it (a picker shown and then withdrawn is exactly what a
|
|
121
|
+
// both-directions gate must not do), so the window is spent with the row
|
|
122
|
+
// LOCKED instead: the same treatment the page already gives fields whose
|
|
123
|
+
// record has not landed, and it means the control cannot appear beside a
|
|
124
|
+
// choice the user has already made.
|
|
125
|
+
const showTimeReference = timeReferenceGate === 'on';
|
|
126
|
+
const fieldsDisabled = disabled || timeReferenceGate === 'loading';
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Re-checks the reconnect window after the cadence moves.
|
|
130
|
+
*
|
|
131
|
+
* The rule "the window must be shorter than the cadence" is attached to
|
|
132
|
+
* `reconnectInterval`, and react-hook-form only refreshes the error of the
|
|
133
|
+
* field that just fired an event — so fixing the CADENCE left the complaint
|
|
134
|
+
* sitting under the offline block until the next Save, pointing at a value
|
|
135
|
+
* that was already legal.
|
|
136
|
+
*
|
|
137
|
+
* Called on the cadence's blur and on its two discrete controls, never
|
|
138
|
+
* per-keystroke: the half-typed "6" of "60" is briefly shorter than the
|
|
139
|
+
* window, and grading it would flash an error at a number still being
|
|
140
|
+
* written. Silent until the first Save, like every other rule here.
|
|
141
|
+
*/
|
|
142
|
+
const recheckReconnectWindow = useCallback(() => {
|
|
143
|
+
if (showErrors) void triggerValidation('reconnectInterval');
|
|
144
|
+
}, [showErrors, triggerValidation]);
|
|
145
|
+
const trigger = useWatch({ control, name: 'trigger' });
|
|
146
|
+
const repeatEnabled = useWatch({ control, name: 'repeatEnabled' });
|
|
147
|
+
const repeatUnit = useWatch({ control, name: 'repeatUnit' });
|
|
148
|
+
const scheduledDate = useWatch({ control, name: 'scheduledDate' });
|
|
149
|
+
const scheduledTime = useWatch({ control, name: 'scheduledTime' });
|
|
150
|
+
const timeReference = useWatch({ control, name: 'timeReference' });
|
|
151
|
+
const eventDriven = isEventTrigger(trigger);
|
|
152
|
+
// Minutes are the one unit that can express a cadence finer than the runner's
|
|
153
|
+
// 30-minute grid, so they are the one unit the stepper has to constrain — it
|
|
154
|
+
// then produces only legal values, and the schema rule behind it is left to
|
|
155
|
+
// catch typed-in ones.
|
|
156
|
+
const intervalStep = repeatUnit === 'minute' ? MIN_REPEAT_MINUTES : 1;
|
|
157
|
+
// Local slots, so the grid depends on the viewer's timezone — built per mount
|
|
158
|
+
// rather than at import time, which would happen on the server, and rebuilt
|
|
159
|
+
// per picked DAY because today offers only the slots still ahead.
|
|
160
|
+
//
|
|
161
|
+
// A stored start that has already gone by today keeps its option: it is the
|
|
162
|
+
// value the form holds and will save if nothing else changes, and a Select
|
|
163
|
+
// whose value is missing from its list renders as the placeholder — the field
|
|
164
|
+
// would read empty on a schedule that has a perfectly good start time.
|
|
165
|
+
//
|
|
166
|
+
// The grid itself also depends on the READING: a DEVICE_LOCAL start stores its
|
|
167
|
+
// wall clock verbatim, so the boundary applies to the picked digits and the
|
|
168
|
+
// slots are the plain xx:00 / xx:30 — which differs from the SERVER grid in
|
|
169
|
+
// the 45-minute zones, and only there.
|
|
170
|
+
const timeSlots = useMemo(() => {
|
|
171
|
+
const slots = getTimeSlots(scheduledDate, timeReference);
|
|
172
|
+
if (!scheduledTime || slots.some(slot => slot.value === scheduledTime)) return slots;
|
|
173
|
+
return [{ value: scheduledTime, label: slotToLabel(scheduledTime) }, ...slots];
|
|
174
|
+
}, [getTimeSlots, scheduledDate, scheduledTime, timeReference]);
|
|
175
|
+
// Today, recomputed per mount so a tab left open overnight cannot still treat
|
|
176
|
+
// yesterday as selectable. Handed to the picker as `fromDate`, which greys out
|
|
177
|
+
// every earlier day and stops the calendar paging past it.
|
|
178
|
+
//
|
|
179
|
+
// ⚠ Requires a core library that TRANSLATES that prop. Up to and including
|
|
180
|
+
// 0.0.514 the calendar forwarded it straight to react-day-picker v9, where
|
|
181
|
+
// `fromDate` was removed — the bound was silently dropped and past days stayed
|
|
182
|
+
// clickable. Fixed in core (`DatePickerCalendar` maps it to a `disabled`
|
|
183
|
+
// matcher); if this app is ever pinned back below that release, the two rules
|
|
184
|
+
// below are what still hold the line.
|
|
185
|
+
//
|
|
186
|
+
// They are not a fallback in any case: a disabled day cannot stop the clock
|
|
187
|
+
// from passing the SLOT this form already holds, and the seeded value never
|
|
188
|
+
// went through the calendar at all.
|
|
189
|
+
//
|
|
190
|
+
// DEVICE_LOCAL moves the bound west instead of dropping it: a wall clock is
|
|
191
|
+
// gone only once it is gone on every clock on Earth, so the floor is today as
|
|
192
|
+
// read in UTC−12 (`earliestScheduleDay`). Depends on the reading, so it is
|
|
193
|
+
// rebuilt when that changes — and on nothing else, so a tab left open still
|
|
194
|
+
// cannot walk the bound backwards past its own mount.
|
|
195
|
+
const minDate = useMemo(() => earliestScheduleDay(timeReference), [timeReference]);
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Moving the DAY can invalidate the time already chosen: 8:00 AM is a fine
|
|
199
|
+
* slot for tomorrow and a gone one for today. The dropdown stops offering it,
|
|
200
|
+
* so a stale value would sit in the form as an empty-looking Select that fails
|
|
201
|
+
* validation on Save — clear it instead, and let the user re-pick from what
|
|
202
|
+
* the new day actually has. A day that keeps the slot keeps the value.
|
|
203
|
+
*
|
|
204
|
+
* Only for a day that is itself selectable: on a PAST day every slot is in the
|
|
205
|
+
* past, and clearing the time there would replace the real complaint ("that
|
|
206
|
+
* day has gone") with a second, misleading one ("pick a time").
|
|
207
|
+
*
|
|
208
|
+
* Never for DEVICE_LOCAL — no slot of a selectable day is withheld there, so
|
|
209
|
+
* moving the day cannot invalidate the time (`isScheduleStartInPast` answers
|
|
210
|
+
* false, which is what makes this a no-op rather than a second condition).
|
|
211
|
+
*/
|
|
212
|
+
const handleDateChange = useCallback(
|
|
213
|
+
(onChange: (date: Date | null) => void, date: Date | null) => {
|
|
214
|
+
onChange(date);
|
|
215
|
+
const time = getValues('scheduledTime');
|
|
216
|
+
if (date && time && date >= minDate && isScheduleStartInPast(date, time, getValues('timeReference')))
|
|
217
|
+
setValue('scheduledTime', '');
|
|
218
|
+
},
|
|
219
|
+
[getValues, setValue, minDate],
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Switching the reading can invalidate the time already picked: the two do not
|
|
224
|
+
* share a slot grid in the 45-minute zones (see `getTimeSlotOptions`), so a
|
|
225
|
+
* value the dropdown no longer lists would sit in the form as an empty-looking
|
|
226
|
+
* Select that fails on Save. Cleared for the same reason a day change clears
|
|
227
|
+
* it — and asked of the GRID, not of the option list, so a stored slot that
|
|
228
|
+
* has merely gone by is left alone.
|
|
229
|
+
*
|
|
230
|
+
* Nothing else is reset. Recurrence and the offline rule survive the switch:
|
|
231
|
+
* "every day, on each device's own clock" is a setting the user meant, not a
|
|
232
|
+
* leftover, and the API refusing it today is a message on Save rather than a
|
|
233
|
+
* reason to silently undo a choice.
|
|
234
|
+
*/
|
|
235
|
+
const handleTimeReferenceChange = useCallback(
|
|
236
|
+
(onChange: (next: ScheduleTimeReference) => void, next: ScheduleTimeReference) => {
|
|
237
|
+
onChange(next);
|
|
238
|
+
const time = getValues('scheduledTime');
|
|
239
|
+
if (time && !isSlotOnGrid(time, next)) setValue('scheduledTime', '');
|
|
240
|
+
},
|
|
241
|
+
[getValues, setValue],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Shown IMMEDIATELY, unlike every other rule on this form, which waits for
|
|
245
|
+
// Save: the calendar cannot withhold a past day itself (see `minDate`), so a
|
|
246
|
+
// picked one has to say so at once rather than look accepted until Save
|
|
247
|
+
// refuses it. Same predicate the schema uses — stored past starts exempt — so
|
|
248
|
+
// the field and the save can never disagree.
|
|
249
|
+
const startAtStored = useWatch({ control, name: 'startAtStored' });
|
|
250
|
+
const startsInPast =
|
|
251
|
+
!eventDriven && isStartInPastAndChanged(scheduledDate, scheduledTime, startAtStored, timeReference);
|
|
252
|
+
// On the field the user can act on: a past DAY is the date's problem, a past
|
|
253
|
+
// slot of today is the time's.
|
|
254
|
+
const pastDateError = startsInPast && scheduledDate && scheduledDate < minDate ? PAST_START_MESSAGE : undefined;
|
|
255
|
+
const pastTimeError = startsInPast && !pastDateError ? PAST_START_MESSAGE : undefined;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Today, picked after its last slot has gone by: `timeSlots` is empty and the
|
|
259
|
+
* Time dropdown has nothing to give. Reported on the DATE — that is the field
|
|
260
|
+
* the user has to move — and shown immediately, for the same reason
|
|
261
|
+
* `pastDateError` is: the calendar accepted the day, so nothing else says it
|
|
262
|
+
* cannot work until Save refuses it for a missing time.
|
|
263
|
+
*
|
|
264
|
+
* `timeSlots` is only ever empty in this exact case. A future day keeps all 48
|
|
265
|
+
* slots, a PAST day keeps them too, and a stored slot that has gone by is
|
|
266
|
+
* re-added above — so an empty list means today, and no way to finish.
|
|
267
|
+
*/
|
|
268
|
+
const noSlotsLeftToday = !eventDriven && timeSlots.length === 0;
|
|
269
|
+
const dateError = pastDateError ?? (noSlotsLeftToday ? NO_SLOTS_TODAY_MESSAGE : undefined);
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
inert={eventDriven}
|
|
274
|
+
style={{
|
|
275
|
+
gridTemplateRows: eventDriven ? '0fr' : '1fr',
|
|
276
|
+
opacity: eventDriven ? 0 : 1,
|
|
277
|
+
}}
|
|
278
|
+
className="mb-[calc(-1*var(--spacing-system-lf))] grid transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none"
|
|
279
|
+
>
|
|
280
|
+
<div className="min-h-0 overflow-hidden">
|
|
281
|
+
{/* Four equal columns, as the design lays them out. A grid rather than a
|
|
282
|
+
flex row because the Timezone picker makes it five cells: the wrap is
|
|
283
|
+
then the one `col-start-1` below, and both layouts are the same
|
|
284
|
+
markup. */}
|
|
285
|
+
<div className="grid grid-cols-1 gap-[var(--spacing-system-lf)] pb-[var(--spacing-system-lf)] content-md:grid-cols-4 content-md:items-end">
|
|
286
|
+
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
287
|
+
<Label className="text-h4">Date</Label>
|
|
288
|
+
<Controller
|
|
289
|
+
name="scheduledDate"
|
|
290
|
+
control={control}
|
|
291
|
+
render={({ field, fieldState }) => (
|
|
292
|
+
// The core picker has no clear affordance, so the design's xmark
|
|
293
|
+
// rides on top of the trigger's free right edge.
|
|
294
|
+
<div className="relative">
|
|
295
|
+
<DatePickerInputSimple
|
|
296
|
+
placeholder="Select date"
|
|
297
|
+
value={field.value ?? undefined}
|
|
298
|
+
onChange={date => handleDateChange(field.onChange, date ?? null)}
|
|
299
|
+
// No day before today: a schedule cannot start in the past.
|
|
300
|
+
fromDate={minDate}
|
|
301
|
+
disabled={fieldsDisabled}
|
|
302
|
+
className="w-full"
|
|
303
|
+
error={dateError ?? (showErrors ? fieldState.error?.message : undefined)}
|
|
304
|
+
invalid={!!dateError || (showErrors && !!fieldState.error)}
|
|
305
|
+
/>
|
|
306
|
+
{field.value && !fieldsDisabled && (
|
|
307
|
+
<button
|
|
308
|
+
type="button"
|
|
309
|
+
onClick={() => field.onChange(null)}
|
|
310
|
+
aria-label="Clear date"
|
|
311
|
+
className="absolute right-3 top-6 -translate-y-1/2 text-ods-text-secondary hover:text-ods-text-primary"
|
|
312
|
+
>
|
|
313
|
+
<XmarkCircleIcon size={24} />
|
|
314
|
+
</button>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
322
|
+
<Label className="text-h4">Time</Label>
|
|
323
|
+
<Controller
|
|
324
|
+
name="scheduledTime"
|
|
325
|
+
control={control}
|
|
326
|
+
render={({ field, fieldState }) => (
|
|
327
|
+
// `''` reads as "no selection" to Radix, so the placeholder shows
|
|
328
|
+
// until a slot is picked — a date at midnight no longer
|
|
329
|
+
// masquerades as a chosen 12:00 AM.
|
|
330
|
+
<Select value={field.value} onValueChange={field.onChange} disabled={fieldsDisabled}>
|
|
331
|
+
{/* No `border-ods-border` here: the trigger already sets it, and
|
|
332
|
+
a repeat of it in `className` lands AFTER the invalid branch
|
|
333
|
+
in the component's `cn()`, so tailwind-merge would drop the
|
|
334
|
+
error border. */}
|
|
335
|
+
<SelectTrigger
|
|
336
|
+
className="w-full"
|
|
337
|
+
error={pastTimeError ?? (showErrors ? fieldState.error?.message : undefined)}
|
|
338
|
+
invalid={!!pastTimeError || (showErrors && !!fieldState.error)}
|
|
339
|
+
>
|
|
340
|
+
<SelectValue placeholder="Select time" />
|
|
341
|
+
</SelectTrigger>
|
|
342
|
+
<SelectContent>
|
|
343
|
+
{timeSlots.length === 0 ? (
|
|
344
|
+
// Deliberately NOT a `SelectItem` — there is nothing here
|
|
345
|
+
// to select, and a selectable row would put a bogus value
|
|
346
|
+
// in the form. It replaces a popup that opened blank; the
|
|
347
|
+
// Date field carries the same sentence for whoever never
|
|
348
|
+
// opens this one.
|
|
349
|
+
<p className="px-4 py-[var(--spacing-system-sf)] text-ods-text-secondary text-h6">
|
|
350
|
+
{NO_SLOTS_TODAY_MESSAGE}
|
|
351
|
+
</p>
|
|
352
|
+
) : (
|
|
353
|
+
timeSlots.map(opt => (
|
|
354
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
355
|
+
{opt.label}
|
|
356
|
+
</SelectItem>
|
|
357
|
+
))
|
|
358
|
+
)}
|
|
359
|
+
</SelectContent>
|
|
360
|
+
</Select>
|
|
361
|
+
)}
|
|
362
|
+
/>
|
|
363
|
+
</div>
|
|
364
|
+
|
|
365
|
+
{showTimeReference && (
|
|
366
|
+
<div className="flex min-w-0 flex-col gap-[var(--spacing-system-xxs)]">
|
|
367
|
+
<Label className="text-h4">Timezone</Label>
|
|
368
|
+
<Controller
|
|
369
|
+
name="timeReference"
|
|
370
|
+
control={control}
|
|
371
|
+
render={({ field }) => (
|
|
372
|
+
<Select
|
|
373
|
+
value={field.value}
|
|
374
|
+
onValueChange={(next: ScheduleTimeReference) => handleTimeReferenceChange(field.onChange, next)}
|
|
375
|
+
disabled={fieldsDisabled}
|
|
376
|
+
>
|
|
377
|
+
{/* No placeholder: the field always holds one of the two
|
|
378
|
+
readings — SERVER is the default a schedule carries when
|
|
379
|
+
nothing was picked, not an empty state. */}
|
|
380
|
+
<SelectTrigger className="w-full" aria-label="Timezone">
|
|
381
|
+
<SelectValue />
|
|
382
|
+
</SelectTrigger>
|
|
383
|
+
<SelectContent>
|
|
384
|
+
{SCHEDULE_TIME_REFERENCE_OPTIONS.map(opt => (
|
|
385
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
386
|
+
{opt.label}
|
|
387
|
+
</SelectItem>
|
|
388
|
+
))}
|
|
389
|
+
</SelectContent>
|
|
390
|
+
</Select>
|
|
391
|
+
)}
|
|
392
|
+
/>
|
|
393
|
+
</div>
|
|
394
|
+
)}
|
|
395
|
+
|
|
396
|
+
{/* Back to column 1 when the Timezone picker took the third: the
|
|
397
|
+
design wraps Repeat onto its own line rather than squeezing five
|
|
398
|
+
controls into four columns. */}
|
|
399
|
+
<div className={cn('min-w-0', showTimeReference && 'content-md:col-start-1')}>
|
|
400
|
+
<Controller
|
|
401
|
+
name="repeatEnabled"
|
|
402
|
+
control={control}
|
|
403
|
+
render={({ field }) => (
|
|
404
|
+
<CheckboxBlock
|
|
405
|
+
label="Repeat Script Run"
|
|
406
|
+
checked={field.value}
|
|
407
|
+
onCheckedChange={next => {
|
|
408
|
+
field.onChange(next);
|
|
409
|
+
// Turning recurrence off removes the cadence the window is
|
|
410
|
+
// measured against, so the rule stops applying entirely.
|
|
411
|
+
recheckReconnectWindow();
|
|
412
|
+
}}
|
|
413
|
+
disabled={fieldsDisabled}
|
|
414
|
+
className="w-full"
|
|
415
|
+
/>
|
|
416
|
+
)}
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
<div className="flex min-w-0 items-end gap-[var(--spacing-system-xs)]">
|
|
421
|
+
<div className="flex min-w-0 flex-1 flex-col gap-[var(--spacing-system-xxs)]">
|
|
422
|
+
<Label className="text-h4">Repeat in</Label>
|
|
423
|
+
<Controller
|
|
424
|
+
name="repeatInterval"
|
|
425
|
+
control={control}
|
|
426
|
+
render={({ field, fieldState }) => (
|
|
427
|
+
<ScheduleIntervalInput
|
|
428
|
+
min={intervalStep}
|
|
429
|
+
className="w-full"
|
|
430
|
+
value={field.value}
|
|
431
|
+
onChange={field.onChange}
|
|
432
|
+
onBlur={() => {
|
|
433
|
+
field.onBlur();
|
|
434
|
+
recheckReconnectWindow();
|
|
435
|
+
}}
|
|
436
|
+
disabled={fieldsDisabled || !repeatEnabled}
|
|
437
|
+
error={showErrors ? fieldState.error?.message : undefined}
|
|
438
|
+
invalid={showErrors && !!fieldState.error}
|
|
439
|
+
/>
|
|
440
|
+
)}
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="min-w-0 flex-1">
|
|
444
|
+
<Controller
|
|
445
|
+
name="repeatUnit"
|
|
446
|
+
control={control}
|
|
447
|
+
render={({ field }) => (
|
|
448
|
+
<Select
|
|
449
|
+
value={field.value}
|
|
450
|
+
// Switching TO minutes drags the interval up onto the grid
|
|
451
|
+
// with it: "1 Day" would otherwise become "1 Minute", a
|
|
452
|
+
// value the user never typed and the form cannot save. The
|
|
453
|
+
// other direction needs nothing — every coarser unit is
|
|
454
|
+
// already a whole number of slots at any interval.
|
|
455
|
+
onValueChange={(next: DurationUnit) => {
|
|
456
|
+
field.onChange(next);
|
|
457
|
+
const snapped = snapRepeatInterval(getValues('repeatInterval'), next);
|
|
458
|
+
if (snapped !== getValues('repeatInterval')) setValue('repeatInterval', snapped);
|
|
459
|
+
recheckReconnectWindow();
|
|
460
|
+
}}
|
|
461
|
+
disabled={fieldsDisabled || !repeatEnabled}
|
|
462
|
+
>
|
|
463
|
+
<SelectTrigger className="w-full">
|
|
464
|
+
<SelectValue />
|
|
465
|
+
</SelectTrigger>
|
|
466
|
+
<SelectContent>
|
|
467
|
+
{DURATION_UNIT_OPTIONS.map(opt => (
|
|
468
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
469
|
+
{opt.label}
|
|
470
|
+
</SelectItem>
|
|
471
|
+
))}
|
|
472
|
+
</SelectContent>
|
|
473
|
+
</Select>
|
|
474
|
+
)}
|
|
475
|
+
/>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|