@byline/admin 4.14.1 → 4.15.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 (34) hide show
  1. package/dist/forms/document-actions.d.ts +16 -1
  2. package/dist/forms/document-actions.js +43 -1
  3. package/dist/forms/form-renderer.d.ts +6 -1
  4. package/dist/forms/form-renderer.js +39 -36
  5. package/dist/forms/form-renderer.module.js +2 -0
  6. package/dist/forms/form-renderer_module.css +7 -1
  7. package/dist/forms/form-status-display.d.ts +10 -1
  8. package/dist/forms/form-status-display.js +4 -2
  9. package/dist/forms/scheduled-publication-control.d.ts +56 -0
  10. package/dist/forms/scheduled-publication-control.js +376 -0
  11. package/dist/forms/scheduled-publication-control.module.js +25 -0
  12. package/dist/forms/scheduled-publication-control_module.css +95 -0
  13. package/dist/forms/scheduled-publication-state.d.ts +83 -0
  14. package/dist/forms/scheduled-publication-state.js +41 -0
  15. package/dist/forms/scheduled-publication-state.test.node.d.ts +8 -0
  16. package/dist/forms/scheduled-publication-time.d.ts +57 -0
  17. package/dist/forms/scheduled-publication-time.js +113 -0
  18. package/dist/forms/scheduled-publication-time.test.node.d.ts +8 -0
  19. package/dist/react.d.ts +1 -0
  20. package/dist/react.js +1 -0
  21. package/package.json +11 -7
  22. package/src/forms/document-actions.tsx +73 -0
  23. package/src/forms/form-renderer.module.css +15 -0
  24. package/src/forms/form-renderer.tsx +59 -64
  25. package/src/forms/form-status-display.tsx +12 -0
  26. package/src/forms/path-widget.test.tsx +20 -8
  27. package/src/forms/scheduled-publication-control.module.css +149 -0
  28. package/src/forms/scheduled-publication-control.tsx +590 -0
  29. package/src/forms/scheduled-publication-datepicker.test.tsx +89 -0
  30. package/src/forms/scheduled-publication-state.test.node.ts +190 -0
  31. package/src/forms/scheduled-publication-state.ts +146 -0
  32. package/src/forms/scheduled-publication-time.test.node.ts +86 -0
  33. package/src/forms/scheduled-publication-time.ts +218 -0
  34. package/src/react.ts +3 -0
@@ -0,0 +1,376 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { useTranslation } from "@byline/i18n/react";
5
+ import { Alert, Button, CloseIcon, DatePicker, IconButton, Label, Modal, Select } from "@byline/ui/react";
6
+ import clsx from "clsx";
7
+ import scheduled_publication_control_module from "./scheduled-publication-control.module.js";
8
+ import { deriveScheduledPublicationState } from "./scheduled-publication-state.js";
9
+ import { joinWallTime, resolveScheduledPublicationWallTime, wallTimeInZone } from "./scheduled-publication-time.js";
10
+ const DUE_POLL_INTERVAL_MS = 30000;
11
+ const DEFAULT_LEAD_MS = 900000;
12
+ function browserTimeZone() {
13
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
14
+ }
15
+ function formatWallTime(wall, locale) {
16
+ const [year, month, day] = wall.date.split('-').map(Number);
17
+ if (null == year || null == month || null == day) return `${wall.date} ${wall.time}`;
18
+ if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day)) return `${wall.date} ${wall.time}`;
19
+ const dayLabel = new Intl.DateTimeFormat(locale, {
20
+ dateStyle: 'medium',
21
+ timeZone: 'UTC'
22
+ }).format(new Date(Date.UTC(year, month - 1, day, 12)));
23
+ return `${dayLabel}, ${wall.time}`;
24
+ }
25
+ function seedScheduleInstant(schedule) {
26
+ if (null != schedule) return new Date(schedule.publishAt);
27
+ const seed = new Date(Date.now() + DEFAULT_LEAD_MS);
28
+ seed.setSeconds(0, 0);
29
+ return seed;
30
+ }
31
+ function useScheduledPublication({ schedule, onSchedule, onConfirm, onCancel, hasUnsavedChanges, onUnsavedChanges }) {
32
+ const timeZone = useMemo(browserTimeZone, []);
33
+ const [now, setNow] = useState(()=>Date.now());
34
+ const [showSchedule, setShowSchedule] = useState(false);
35
+ const [busy, setBusy] = useState(false);
36
+ useEffect(()=>{
37
+ if (null == schedule) return;
38
+ const timer = setInterval(()=>setNow(Date.now()), DUE_POLL_INTERVAL_MS);
39
+ return ()=>clearInterval(timer);
40
+ }, [
41
+ schedule
42
+ ]);
43
+ const capabilities = useMemo(()=>({
44
+ canSchedule: null != onSchedule,
45
+ canConfirm: null != onConfirm,
46
+ canCancel: null != onCancel
47
+ }), [
48
+ onSchedule,
49
+ onConfirm,
50
+ onCancel
51
+ ]);
52
+ const state = useMemo(()=>deriveScheduledPublicationState(schedule, capabilities, now), [
53
+ schedule,
54
+ capabilities,
55
+ now
56
+ ]);
57
+ const openSchedule = useCallback(()=>{
58
+ if (hasUnsavedChanges) return void onUnsavedChanges();
59
+ setShowSchedule(true);
60
+ }, [
61
+ hasUnsavedChanges,
62
+ onUnsavedChanges
63
+ ]);
64
+ const confirm = useCallback(async ()=>{
65
+ if (null == onConfirm) return;
66
+ if (hasUnsavedChanges) return void onUnsavedChanges();
67
+ setBusy(true);
68
+ try {
69
+ await onConfirm();
70
+ } finally{
71
+ setBusy(false);
72
+ }
73
+ }, [
74
+ onConfirm,
75
+ hasUnsavedChanges,
76
+ onUnsavedChanges
77
+ ]);
78
+ const cancel = useCallback(async ()=>{
79
+ if (null == onCancel) return;
80
+ setBusy(true);
81
+ try {
82
+ await onCancel();
83
+ } finally{
84
+ setBusy(false);
85
+ }
86
+ }, [
87
+ onCancel
88
+ ]);
89
+ const modal = showSchedule ? /*#__PURE__*/ jsx(ScheduleModal, {
90
+ schedule: schedule,
91
+ timeZone: timeZone,
92
+ onSubmit: async (input)=>{
93
+ if (null == onSchedule) return;
94
+ setBusy(true);
95
+ try {
96
+ await onSchedule(input);
97
+ setShowSchedule(false);
98
+ } finally{
99
+ setBusy(false);
100
+ }
101
+ },
102
+ onDismiss: ()=>setShowSchedule(false),
103
+ busy: busy
104
+ }) : null;
105
+ return {
106
+ state,
107
+ timeZone,
108
+ busy,
109
+ isActive: 'none' !== state.kind || state.actions.schedule,
110
+ openSchedule,
111
+ confirm,
112
+ cancel,
113
+ modal
114
+ };
115
+ }
116
+ function useInstantFormatter(timeZone) {
117
+ const { locale } = useTranslation('byline-admin');
118
+ return useMemo(()=>new Intl.DateTimeFormat(locale, {
119
+ dateStyle: 'medium',
120
+ timeStyle: 'short',
121
+ timeZone
122
+ }), [
123
+ locale,
124
+ timeZone
125
+ ]);
126
+ }
127
+ function ScheduledPublicationCell({ state, timeZone }) {
128
+ const { t } = useTranslation('byline-admin');
129
+ const format = useInstantFormatter(timeZone);
130
+ if ('armed' !== state.kind || null == state.publishAt) return null;
131
+ return /*#__PURE__*/ jsx("time", {
132
+ className: clsx('byline-scheduled-publication-cell', scheduled_publication_control_module.cell),
133
+ dateTime: state.publishAt.toISOString(),
134
+ children: t('scheduledPublication.status.cellText', {
135
+ dateTime: `${format.format(state.publishAt)} (${timeZone})`
136
+ })
137
+ });
138
+ }
139
+ function ScheduledPublicationNotice({ state, timeZone, busy, onConfirm, onReschedule, onCancel }) {
140
+ const { t } = useTranslation('byline-admin');
141
+ const format = useInstantFormatter(timeZone);
142
+ if (!state.isExceptional || null == state.publishAt) return null;
143
+ const instant = `${format.format(state.publishAt)} (${timeZone})`;
144
+ const title = 'needs_reconfirm' === state.kind ? t('scheduledPublication.status.needsReconfirm') : t('scheduledPublication.status.overdue');
145
+ return /*#__PURE__*/ jsx("div", {
146
+ role: "region",
147
+ "aria-label": title,
148
+ children: /*#__PURE__*/ jsxs(Alert, {
149
+ className: clsx('byline-scheduled-publication-notice', scheduled_publication_control_module.notice),
150
+ intent: 'danger' === state.tone ? 'danger' : 'warning',
151
+ icon: true,
152
+ close: false,
153
+ title: title,
154
+ children: [
155
+ /*#__PURE__*/ jsx("p", {
156
+ className: scheduled_publication_control_module["notice-body"],
157
+ children: 'needs_reconfirm' === state.kind ? t('scheduledPublication.status.contentChanged') : t('scheduledPublication.status.overdueBody')
158
+ }),
159
+ /*#__PURE__*/ jsxs("p", {
160
+ className: scheduled_publication_control_module["notice-instant"],
161
+ children: [
162
+ t('scheduledPublication.status.authorizedFor', {
163
+ dateTime: instant
164
+ }),
165
+ 'needs_reconfirm' === state.kind && state.isPastDue && /*#__PURE__*/ jsxs(Fragment, {
166
+ children: [
167
+ " ",
168
+ t('scheduledPublication.status.pastDueNote')
169
+ ]
170
+ })
171
+ ]
172
+ }),
173
+ state.attemptCount > 0 && /*#__PURE__*/ jsx("p", {
174
+ className: scheduled_publication_control_module["notice-attempts"],
175
+ children: t('scheduledPublication.status.attempts', {
176
+ count: state.attemptCount
177
+ })
178
+ }),
179
+ null != state.lastError && /*#__PURE__*/ jsx("p", {
180
+ className: scheduled_publication_control_module["notice-error"],
181
+ children: state.lastError
182
+ }),
183
+ /*#__PURE__*/ jsxs("div", {
184
+ className: clsx('byline-scheduled-publication-notice-actions', scheduled_publication_control_module["notice-actions"]),
185
+ children: [
186
+ state.actions.confirm && /*#__PURE__*/ jsx(Button, {
187
+ size: "sm",
188
+ type: "button",
189
+ intent: "success",
190
+ disabled: busy,
191
+ onClick: onConfirm,
192
+ children: t('scheduledPublication.actions.confirm')
193
+ }),
194
+ state.actions.reschedule && /*#__PURE__*/ jsx(Button, {
195
+ size: "sm",
196
+ type: "button",
197
+ intent: "info",
198
+ disabled: busy,
199
+ onClick: onReschedule,
200
+ children: t('scheduledPublication.actions.reschedule')
201
+ }),
202
+ state.actions.cancel && /*#__PURE__*/ jsx(Button, {
203
+ size: "sm",
204
+ type: "button",
205
+ variant: "text",
206
+ disabled: busy,
207
+ onClick: onCancel,
208
+ children: t('scheduledPublication.actions.cancel')
209
+ })
210
+ ]
211
+ })
212
+ ]
213
+ })
214
+ });
215
+ }
216
+ function ScheduleModal({ schedule, timeZone, onSubmit, onDismiss, busy }) {
217
+ const { t, locale } = useTranslation('byline-admin');
218
+ const [seedInstant] = useState(()=>seedScheduleInstant(schedule));
219
+ const [today] = useState(()=>new Date());
220
+ const [wall, setWall] = useState(()=>wallTimeInZone(seedInstant, timeZone));
221
+ const [instantChoices, setInstantChoices] = useState([]);
222
+ const [selectedInstant, setSelectedInstant] = useState('');
223
+ const [validationError, setValidationError] = useState(null);
224
+ const resetResolution = ()=>{
225
+ setInstantChoices([]);
226
+ setSelectedInstant('');
227
+ setValidationError(null);
228
+ };
229
+ const submit = async ()=>{
230
+ const value = joinWallTime(wall);
231
+ if (null == value) return void setValidationError(t('scheduledPublication.form.invalid'));
232
+ const resolution = resolveScheduledPublicationWallTime(value, timeZone);
233
+ if ('invalid' === resolution.status) return void setValidationError(t('scheduledPublication.form.invalid'));
234
+ const offending = formatWallTime(wall, locale);
235
+ if ('nonexistent' === resolution.status) return void setValidationError(t('scheduledPublication.form.nonexistent', {
236
+ wallTime: offending
237
+ }));
238
+ if (resolution.choices.length > 1 && !resolution.choices.some((c)=>c.iso === selectedInstant)) {
239
+ setInstantChoices(resolution.choices);
240
+ setValidationError(t('scheduledPublication.form.ambiguous', {
241
+ wallTime: offending
242
+ }));
243
+ return;
244
+ }
245
+ const publishAtIso = selectedInstant || resolution.choices[0]?.iso;
246
+ if (null == publishAtIso) return;
247
+ if (Date.parse(publishAtIso) <= Date.now()) return void setValidationError(t('scheduledPublication.form.notFuture', {
248
+ wallTime: offending
249
+ }));
250
+ await onSubmit({
251
+ publishAt: publishAtIso
252
+ });
253
+ };
254
+ const title = null == schedule ? t('scheduledPublication.form.scheduleTitle') : t('scheduledPublication.form.rescheduleTitle');
255
+ return /*#__PURE__*/ jsx(Modal, {
256
+ isOpen: true,
257
+ closeOnOverlayClick: !busy,
258
+ onDismiss: ()=>{
259
+ if (!busy) onDismiss();
260
+ },
261
+ children: /*#__PURE__*/ jsxs(Modal.Container, {
262
+ style: {
263
+ maxWidth: '560px'
264
+ },
265
+ children: [
266
+ /*#__PURE__*/ jsxs(Modal.Header, {
267
+ className: scheduled_publication_control_module.modalHead,
268
+ children: [
269
+ /*#__PURE__*/ jsx("h3", {
270
+ className: scheduled_publication_control_module.modalTitle,
271
+ children: title
272
+ }),
273
+ /*#__PURE__*/ jsx(IconButton, {
274
+ "aria-label": t('common.actions.close'),
275
+ size: "xs",
276
+ disabled: busy,
277
+ onClick: onDismiss,
278
+ children: /*#__PURE__*/ jsx(CloseIcon, {
279
+ width: "16px",
280
+ height: "16px",
281
+ svgClassName: "white-icon"
282
+ })
283
+ })
284
+ ]
285
+ }),
286
+ /*#__PURE__*/ jsxs(Modal.Content, {
287
+ children: [
288
+ /*#__PURE__*/ jsx("div", {
289
+ className: scheduled_publication_control_module.field,
290
+ children: /*#__PURE__*/ jsx(DatePicker, {
291
+ id: "scheduled-publication-at",
292
+ name: "scheduled-publication-at",
293
+ label: t('scheduledPublication.form.dateTime'),
294
+ mode: "datetime",
295
+ inputSize: "sm",
296
+ minDate: today,
297
+ yearsInPast: 0,
298
+ yearsInFuture: 5,
299
+ initialValue: seedInstant,
300
+ onWallTimeChange: (value)=>{
301
+ if (null == value) return;
302
+ setWall(value);
303
+ resetResolution();
304
+ }
305
+ })
306
+ }),
307
+ /*#__PURE__*/ jsx("p", {
308
+ className: scheduled_publication_control_module.help,
309
+ children: t('scheduledPublication.form.timeZone', {
310
+ timeZone
311
+ })
312
+ }),
313
+ instantChoices.length > 1 && /*#__PURE__*/ jsxs("div", {
314
+ className: scheduled_publication_control_module.choice,
315
+ children: [
316
+ /*#__PURE__*/ jsx(Label, {
317
+ id: "scheduled-publication-offset-label",
318
+ htmlFor: "scheduled-publication-offset",
319
+ label: t('scheduledPublication.form.offset')
320
+ }),
321
+ /*#__PURE__*/ jsx(Select, {
322
+ id: "scheduled-publication-offset",
323
+ name: "scheduled-publication-offset",
324
+ ariaLabel: t('scheduledPublication.form.offset'),
325
+ containerClassName: scheduled_publication_control_module["choice-select"],
326
+ placeholder: t('scheduledPublication.form.offsetPlaceholder'),
327
+ size: "sm",
328
+ value: selectedInstant,
329
+ items: instantChoices.map((choice, index)=>({
330
+ value: choice.iso,
331
+ label: t(0 === index ? 'scheduledPublication.form.offsetEarlier' : 'scheduledPublication.form.offsetLater', {
332
+ offset: choice.offsetLabel
333
+ })
334
+ })),
335
+ onValueChange: (value)=>{
336
+ setSelectedInstant(value ?? '');
337
+ setValidationError(null);
338
+ },
339
+ disabled: busy
340
+ })
341
+ ]
342
+ }),
343
+ null != validationError && /*#__PURE__*/ jsx("p", {
344
+ className: scheduled_publication_control_module.validation,
345
+ role: "alert",
346
+ children: validationError
347
+ }),
348
+ /*#__PURE__*/ jsx("p", {
349
+ className: scheduled_publication_control_module.warningText,
350
+ children: t('scheduledPublication.form.editWarning')
351
+ })
352
+ ]
353
+ }),
354
+ /*#__PURE__*/ jsxs(Modal.Actions, {
355
+ children: [
356
+ /*#__PURE__*/ jsx(Button, {
357
+ size: "sm",
358
+ intent: "noeffect",
359
+ disabled: busy,
360
+ onClick: onDismiss,
361
+ children: t('common.actions.cancel')
362
+ }),
363
+ /*#__PURE__*/ jsx(Button, {
364
+ size: "sm",
365
+ intent: "primary",
366
+ disabled: busy || 0 === wall.date.length || 0 === wall.time.length,
367
+ onClick: submit,
368
+ children: busy ? t('scheduledPublication.form.saving') : null == schedule ? t('scheduledPublication.actions.schedule') : t('scheduledPublication.actions.reschedule')
369
+ })
370
+ ]
371
+ })
372
+ ]
373
+ })
374
+ });
375
+ }
376
+ export { ScheduledPublicationCell, ScheduledPublicationNotice, useScheduledPublication };
@@ -0,0 +1,25 @@
1
+ import "./scheduled-publication-control_module.css";
2
+ const scheduled_publication_control_module = {
3
+ cell: "cell-PLfZ8N",
4
+ notice: "notice-a85M8d",
5
+ "notice-body": "notice-body-I7ZQZ6",
6
+ noticeBody: "notice-body-I7ZQZ6",
7
+ "notice-instant": "notice-instant-vUk7Iz",
8
+ noticeInstant: "notice-instant-vUk7Iz",
9
+ "notice-attempts": "notice-attempts-lqDFQZ",
10
+ noticeAttempts: "notice-attempts-lqDFQZ",
11
+ "notice-error": "notice-error-b8EmB9",
12
+ noticeError: "notice-error-b8EmB9",
13
+ "notice-actions": "notice-actions-dDHhl_",
14
+ noticeActions: "notice-actions-dDHhl_",
15
+ modalHead: "modalHead-sE__5Y",
16
+ modalTitle: "modalTitle-NXtMjb",
17
+ field: "field-Z0Zwaq",
18
+ help: "help-UVZDXJ",
19
+ validation: "validation-ThnwlW",
20
+ warningText: "warningText-zXrWei",
21
+ choice: "choice-aTHmEn",
22
+ "choice-select": "choice-select-fA1Bvp",
23
+ choiceSelect: "choice-select-fA1Bvp"
24
+ };
25
+ export default scheduled_publication_control_module;
@@ -0,0 +1,95 @@
1
+ .cell-PLfZ8N {
2
+ color: var(--text-success);
3
+ overflow-wrap: anywhere;
4
+ white-space: normal;
5
+ flex-basis: 100%;
6
+ font-weight: 600;
7
+ }
8
+
9
+ @media (min-width: 66rem) {
10
+ .cell-PLfZ8N {
11
+ margin-left: var(--spacing-4);
12
+ flex-basis: auto;
13
+ }
14
+
15
+ .byline-form-status-cell:has( > .cell-PLfZ8N) {
16
+ align-items: flex-start;
17
+ }
18
+ }
19
+
20
+ .notice-a85M8d {
21
+ margin-top: var(--spacing-8);
22
+ margin-bottom: var(--spacing-8);
23
+ }
24
+
25
+ .notice-body-I7ZQZ6, .notice-instant-vUk7Iz, .notice-attempts-lqDFQZ, .notice-error-b8EmB9 {
26
+ margin: 0 0 var(--spacing-4);
27
+ font-size: var(--font-size-sm);
28
+ }
29
+
30
+ .notice-instant-vUk7Iz {
31
+ font-weight: 600;
32
+ }
33
+
34
+ .notice-attempts-lqDFQZ {
35
+ color: var(--gray-500);
36
+ }
37
+
38
+ .notice-error-b8EmB9 {
39
+ font-family: var(--font-family-mono, monospace);
40
+ overflow-wrap: anywhere;
41
+ }
42
+
43
+ .notice-actions-dDHhl_ {
44
+ align-items: center;
45
+ gap: var(--spacing-8);
46
+ margin-top: var(--spacing-8);
47
+ flex-wrap: wrap;
48
+ display: flex;
49
+ }
50
+
51
+ .modalHead-sE__5Y {
52
+ margin-bottom: var(--spacing-8);
53
+ padding-top: 1rem;
54
+ }
55
+
56
+ .modalTitle-NXtMjb {
57
+ margin: 0 0 var(--spacing-8) 0;
58
+ font-size: var(--font-size-2xl);
59
+ }
60
+
61
+ .field-Z0Zwaq {
62
+ display: block;
63
+ }
64
+
65
+ .help-UVZDXJ, .validation-ThnwlW, .warningText-zXrWei {
66
+ font-size: var(--font-size-sm);
67
+ }
68
+
69
+ .help-UVZDXJ {
70
+ margin: var(--spacing-4) 0 0;
71
+ }
72
+
73
+ .warningText-zXrWei {
74
+ margin: var(--spacing-12) 0 0;
75
+ color: var(--gray-500);
76
+ }
77
+
78
+ .validation-ThnwlW {
79
+ margin: var(--spacing-8) 0 0;
80
+ color: var(--red-600);
81
+ }
82
+
83
+ .choice-aTHmEn {
84
+ margin-top: var(--spacing-12);
85
+ }
86
+
87
+ .choice-select-fA1Bvp {
88
+ min-width: 16rem;
89
+ max-width: 100%;
90
+ }
91
+
92
+ :is([data-theme="dark"], .dark) .validation-ThnwlW {
93
+ color: var(--red-400, #f87171);
94
+ }
95
+
@@ -0,0 +1,83 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ /**
9
+ * Presentation state for a document's pending publication schedule.
10
+ *
11
+ * The editor surfaces four situations, and they differ in how loudly they
12
+ * should be presented, not just in wording:
13
+ *
14
+ * - `none` — nothing scheduled; only the "schedule" action applies.
15
+ * - `armed` — a future instant is authorized against a reviewed
16
+ * version. Quiet: a metadata cell in the status bar.
17
+ * - `needs_reconfirm` — content was saved after the time was authorized, so
18
+ * the schedule is suspended until an editor reviews and
19
+ * confirms the current version. Loud, and durable: this
20
+ * has to stay discoverable long after the toast is gone.
21
+ * - `overdue` — armed, due, and not yet finalized. The sweep may still
22
+ * be retrying, and `lastError` may carry a bounded
23
+ * reason. Loud.
24
+ *
25
+ * Deriving this in one pure place keeps the branching out of the components and
26
+ * makes the awkward combinations (a suspended schedule whose instant has also
27
+ * passed; an overdue schedule the actor may not cancel) testable without a DOM.
28
+ */
29
+ export type ScheduledPublicationStateKind = 'none' | 'armed' | 'needs_reconfirm' | 'overdue';
30
+ /** How prominently the state should be rendered. */
31
+ export type ScheduledPublicationTone = 'neutral' | 'info' | 'warning' | 'danger';
32
+ export interface ScheduledPublicationInfo {
33
+ publishAt: string | Date;
34
+ targetVersionId: string;
35
+ state: 'armed' | 'needs_reconfirm';
36
+ lastAuthorizedBy: string | null;
37
+ lastError: string | null;
38
+ nextAttemptAt: string | Date;
39
+ attemptCount: number;
40
+ }
41
+ /**
42
+ * Which operations the host has wired up for this document and actor. A
43
+ * missing handler means the actor cannot perform the operation — the server
44
+ * decides, and the editor never renders an action it cannot complete.
45
+ */
46
+ export interface ScheduledPublicationCapabilities {
47
+ canSchedule: boolean;
48
+ canConfirm: boolean;
49
+ canCancel: boolean;
50
+ }
51
+ export interface ScheduledPublicationState {
52
+ kind: ScheduledPublicationStateKind;
53
+ tone: ScheduledPublicationTone;
54
+ /** The authorized instant, or null when nothing is scheduled. */
55
+ publishAt: Date | null;
56
+ /**
57
+ * True when the authorized instant has passed. Tracked separately from
58
+ * `kind` so a suspended schedule that is also past due still reads as
59
+ * "needs re-confirmation" — the reason it did not publish — while the
60
+ * elapsed time remains available to the summary.
61
+ */
62
+ isPastDue: boolean;
63
+ /** Bounded failure reason from the last sweep attempt, when the server sent one. */
64
+ lastError: string | null;
65
+ /** Sweep attempts so far. Only meaningful once the instant has passed. */
66
+ attemptCount: number;
67
+ /** True when the state warrants an escalated, dismissible-proof notice rather than a metadata cell. */
68
+ isExceptional: boolean;
69
+ actions: {
70
+ schedule: boolean;
71
+ reschedule: boolean;
72
+ confirm: boolean;
73
+ cancel: boolean;
74
+ };
75
+ }
76
+ /**
77
+ * Derive the editor's presentation state from the server's schedule record.
78
+ *
79
+ * `now` is injected rather than read from the clock so the boundary between
80
+ * armed and overdue is testable, and so a single render pass cannot disagree
81
+ * with itself.
82
+ */
83
+ export declare function deriveScheduledPublicationState(schedule: ScheduledPublicationInfo | null, capabilities: ScheduledPublicationCapabilities, now: number): ScheduledPublicationState;
@@ -0,0 +1,41 @@
1
+ function toDate(value) {
2
+ return value instanceof Date ? value : new Date(value);
3
+ }
4
+ function deriveScheduledPublicationState(schedule, capabilities, now) {
5
+ if (null == schedule) return {
6
+ kind: 'none',
7
+ tone: 'neutral',
8
+ publishAt: null,
9
+ isPastDue: false,
10
+ lastError: null,
11
+ attemptCount: 0,
12
+ isExceptional: false,
13
+ actions: {
14
+ schedule: capabilities.canSchedule,
15
+ reschedule: false,
16
+ confirm: false,
17
+ cancel: false
18
+ }
19
+ };
20
+ const publishAt = toDate(schedule.publishAt);
21
+ const isPastDue = Number.isFinite(publishAt.getTime()) && publishAt.getTime() <= now;
22
+ const needsReconfirm = 'needs_reconfirm' === schedule.state;
23
+ const kind = needsReconfirm ? 'needs_reconfirm' : isPastDue ? 'overdue' : 'armed';
24
+ const tone = null != schedule.lastError ? 'danger' : 'armed' === kind ? 'info' : 'warning';
25
+ return {
26
+ kind,
27
+ tone,
28
+ publishAt,
29
+ isPastDue,
30
+ lastError: schedule.lastError,
31
+ attemptCount: schedule.attemptCount,
32
+ isExceptional: 'armed' !== kind,
33
+ actions: {
34
+ schedule: false,
35
+ reschedule: capabilities.canSchedule,
36
+ confirm: needsReconfirm && capabilities.canConfirm,
37
+ cancel: capabilities.canCancel
38
+ }
39
+ };
40
+ }
41
+ export { deriveScheduledPublicationState };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -0,0 +1,57 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export interface ScheduledPublicationInstantChoice {
9
+ iso: string;
10
+ offsetLabel: string;
11
+ }
12
+ /**
13
+ * A wall time as the editor sees it: a calendar day and a clock reading, with
14
+ * no instant attached yet.
15
+ *
16
+ * The schedule modal takes these two halves from the date picker's
17
+ * `onWallTimeChange` and keeps them as strings all the way to
18
+ * `resolveScheduledPublicationWallTime`, never routing them through a `Date`.
19
+ * That is deliberate: `setHours` silently normalizes a nonexistent wall time (a
20
+ * spring-forward 02:30 becomes 03:30) and silently picks the first of two
21
+ * ambiguous instants at a fall-back overlap. Both are exactly the cases the
22
+ * editor has to be asked about, so the only safe carrier between the picker and
23
+ * the resolver is text.
24
+ */
25
+ export interface ScheduledPublicationWallTime {
26
+ /** Calendar day as `YYYY-MM-DD`. */
27
+ date: string;
28
+ /** Clock reading as `HH:mm`, 24-hour. */
29
+ time: string;
30
+ }
31
+ export type ScheduledPublicationWallTimeResolution = {
32
+ status: 'invalid';
33
+ } | {
34
+ status: 'nonexistent';
35
+ } | {
36
+ status: 'valid';
37
+ choices: ScheduledPublicationInstantChoice[];
38
+ };
39
+ /**
40
+ * Resolve a browser-entered wall time in an explicit IANA zone. Returns no
41
+ * choice for a daylight-saving gap and two choices for an overlap, forcing
42
+ * the editor to choose an actual instant rather than accepting JS Date's
43
+ * silent normalization.
44
+ */
45
+ export declare function resolveScheduledPublicationWallTime(value: string, timeZone: string): ScheduledPublicationWallTimeResolution;
46
+ /**
47
+ * Split an instant into the calendar day and clock reading it shows in
48
+ * `timeZone`. Used to seed the modal when rescheduling, so the editor starts
49
+ * from the time they authorized rather than from its UTC spelling.
50
+ */
51
+ export declare function wallTimeInZone(instant: Date, timeZone: string): ScheduledPublicationWallTime;
52
+ /**
53
+ * Join a wall time back into the `YYYY-MM-DDTHH:mm` string that
54
+ * `resolveScheduledPublicationWallTime` parses. Returns null when either half
55
+ * is still blank, which is the modal's "nothing to validate yet" signal.
56
+ */
57
+ export declare function joinWallTime(wall: ScheduledPublicationWallTime): string | null;