@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.
- package/dist/forms/document-actions.d.ts +16 -1
- package/dist/forms/document-actions.js +43 -1
- package/dist/forms/form-renderer.d.ts +6 -1
- package/dist/forms/form-renderer.js +39 -36
- package/dist/forms/form-renderer.module.js +2 -0
- package/dist/forms/form-renderer_module.css +7 -1
- package/dist/forms/form-status-display.d.ts +10 -1
- package/dist/forms/form-status-display.js +4 -2
- package/dist/forms/scheduled-publication-control.d.ts +56 -0
- package/dist/forms/scheduled-publication-control.js +376 -0
- package/dist/forms/scheduled-publication-control.module.js +25 -0
- package/dist/forms/scheduled-publication-control_module.css +95 -0
- package/dist/forms/scheduled-publication-state.d.ts +83 -0
- package/dist/forms/scheduled-publication-state.js +41 -0
- package/dist/forms/scheduled-publication-state.test.node.d.ts +8 -0
- package/dist/forms/scheduled-publication-time.d.ts +57 -0
- package/dist/forms/scheduled-publication-time.js +113 -0
- package/dist/forms/scheduled-publication-time.test.node.d.ts +8 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -0
- package/package.json +11 -7
- package/src/forms/document-actions.tsx +73 -0
- package/src/forms/form-renderer.module.css +15 -0
- package/src/forms/form-renderer.tsx +59 -64
- package/src/forms/form-status-display.tsx +12 -0
- package/src/forms/path-widget.test.tsx +20 -8
- package/src/forms/scheduled-publication-control.module.css +149 -0
- package/src/forms/scheduled-publication-control.tsx +590 -0
- package/src/forms/scheduled-publication-datepicker.test.tsx +89 -0
- package/src/forms/scheduled-publication-state.test.node.ts +190 -0
- package/src/forms/scheduled-publication-state.ts +146 -0
- package/src/forms/scheduled-publication-time.test.node.ts +86 -0
- package/src/forms/scheduled-publication-time.ts +218 -0
- 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;
|