@byline/admin 6.3.0 → 6.5.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/fields/array/array-field.js +29 -12
- package/dist/fields/blocks/blocks-field.js +20 -8
- package/dist/fields/checkbox/checkbox-field.js +1 -0
- package/dist/fields/datetime/datetime-field.js +1 -0
- package/dist/fields/file/file-field.js +3 -2
- package/dist/fields/group/group-field.js +5 -2
- package/dist/fields/image/image-field.js +3 -2
- package/dist/fields/relation/relation-field.js +5 -0
- package/dist/fields/relation/relation-many-field.js +9 -2
- package/dist/fields/select/select-field.js +1 -0
- package/dist/fields/text/text-field.js +4 -2
- package/dist/fields/text-area/text-area-field.js +4 -2
- package/dist/fields/use-field-change-handler.js +24 -6
- package/dist/fields/use-field-condition.d.ts +0 -15
- package/dist/fields/use-field-condition.js +10 -1
- package/dist/forms/form-context.d.ts +3 -12
- package/dist/forms/form-context.js +111 -129
- package/dist/forms/form-layout.js +7 -2
- package/dist/forms/form-page-chrome.js +14 -24
- package/dist/forms/form-renderer.d.ts +2 -1
- package/dist/forms/form-renderer.js +12 -56
- package/dist/forms/nested-path.d.ts +2 -0
- package/dist/forms/nested-path.js +26 -1
- package/dist/forms/use-document-reload-recovery.d.ts +22 -0
- package/dist/forms/use-document-reload-recovery.js +67 -0
- package/dist/forms/use-form-submission.d.ts +1 -1
- package/dist/forms/use-form-submission.js +45 -27
- package/dist/forms/use-form-tabs.d.ts +2 -2
- package/dist/forms/use-form-tabs.js +18 -11
- package/dist/modules/admin-account/components/change-password.js +6 -0
- package/dist/modules/admin-users/components/create.js +4 -3
- package/dist/modules/admin-users/components/set-password.js +4 -0
- package/dist/modules/auth/components/sign-in-form.js +4 -3
- package/package.json +16 -16
- package/src/fields/array/array-field.tsx +24 -12
- package/src/fields/blocks/blocks-field.tsx +18 -8
- package/src/fields/checkbox/checkbox-field.tsx +1 -0
- package/src/fields/datetime/datetime-field.tsx +1 -0
- package/src/fields/file/file-field.tsx +3 -2
- package/src/fields/group/group-field.tsx +2 -2
- package/src/fields/image/image-field.tsx +3 -2
- package/src/fields/relation/relation-field.tsx +5 -0
- package/src/fields/relation/relation-many-field.tsx +16 -2
- package/src/fields/select/select-field.tsx +1 -0
- package/src/fields/text/text-field.tsx +3 -2
- package/src/fields/text-area/text-area-field.tsx +3 -2
- package/src/fields/use-field-change-handler.ts +43 -9
- package/src/fields/use-field-condition.ts +17 -1
- package/src/forms/form-context.tsx +178 -233
- package/src/forms/form-correctness.test.tsx +560 -0
- package/src/forms/form-layout.tsx +3 -2
- package/src/forms/form-page-chrome.tsx +17 -34
- package/src/forms/form-renderer-submit.test.tsx +36 -0
- package/src/forms/form-renderer.tsx +15 -64
- package/src/forms/nested-path.ts +35 -0
- package/src/forms/use-document-reload-recovery.ts +92 -0
- package/src/forms/use-form-submission.ts +56 -27
- package/src/forms/use-form-tabs.ts +25 -17
- package/src/modules/admin-account/components/change-password.tsx +6 -0
- package/src/modules/admin-users/components/create.tsx +4 -3
- package/src/modules/admin-users/components/set-password.tsx +4 -0
- package/src/modules/auth/components/sign-in-form.tsx +4 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useRef,
|
|
4
|
-
import { normalizeHooks } from "@byline/core";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import { normalizeHooks, validateDocumentFields } from "@byline/core";
|
|
5
5
|
import { FormDomScopeProvider } from "./form-dom-scope.js";
|
|
6
|
-
import { get, hasExistingIdTargets,
|
|
6
|
+
import { get, hasExistingIdTargets, withValue } from "./nested-path.js";
|
|
7
7
|
import { deletePendingUploadsUnderPath } from "./pending-uploads.js";
|
|
8
8
|
import { useTrackedSlot } from "./use-tracked-slot.js";
|
|
9
9
|
const sameLocaleSet = (a, b)=>{
|
|
@@ -26,7 +26,6 @@ const useFormContext = ()=>{
|
|
|
26
26
|
};
|
|
27
27
|
const FormProvider = ({ children, initialData = {}, documentId = null, collectionPath = null })=>{
|
|
28
28
|
const fieldValues = useRef(JSON.parse(JSON.stringify(initialData?.fields ?? initialData)));
|
|
29
|
-
const initialValues = useRef(initialData?.fields ?? initialData);
|
|
30
29
|
const errorsRef = useRef([]);
|
|
31
30
|
const dirtyFields = useRef(new Set());
|
|
32
31
|
const patchesRef = useRef([]);
|
|
@@ -94,10 +93,8 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
94
93
|
]
|
|
95
94
|
});
|
|
96
95
|
const updateFieldStoreInternal = useCallback((name, value)=>{
|
|
97
|
-
const newFieldValues =
|
|
98
|
-
|
|
99
|
-
};
|
|
100
|
-
if (!setWithResult(newFieldValues, name, value)) return false;
|
|
96
|
+
const newFieldValues = withValue(fieldValues.current, name, value);
|
|
97
|
+
if (!newFieldValues) return false;
|
|
101
98
|
fieldValues.current = newFieldValues;
|
|
102
99
|
dirtyFields.current.add(name);
|
|
103
100
|
notifyFieldListeners(name, value);
|
|
@@ -150,12 +147,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
150
147
|
}, [
|
|
151
148
|
notifyMetaListeners
|
|
152
149
|
]);
|
|
153
|
-
const getFieldValue = useCallback((name)=>
|
|
154
|
-
const dirty = dirtyFields.current.has(name);
|
|
155
|
-
const currentValue = get(fieldValues.current, name);
|
|
156
|
-
if (void 0 !== currentValue) return currentValue;
|
|
157
|
-
if (!dirty) return get(initialValues.current, name);
|
|
158
|
-
}, []);
|
|
150
|
+
const getFieldValue = useCallback((name)=>get(fieldValues.current, name), []);
|
|
159
151
|
const hasChanges = useCallback(()=>dirtyFields.current.size > 0, []);
|
|
160
152
|
const resetHasChanges = useCallback(()=>{
|
|
161
153
|
dirtyFields.current.clear();
|
|
@@ -254,58 +246,35 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
254
246
|
useEffect(()=>()=>{
|
|
255
247
|
for (const upload of pendingUploadsRef.current.values())URL.revokeObjectURL(upload.previewUrl);
|
|
256
248
|
}, []);
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
});
|
|
267
|
-
if (null != value && '' !== value) switch(field.type){
|
|
268
|
-
case 'text':
|
|
269
|
-
if ('string' != typeof value) formErrors.push({
|
|
270
|
-
field: field.name,
|
|
271
|
-
message: `${field.label} must be text`
|
|
272
|
-
});
|
|
273
|
-
break;
|
|
274
|
-
case 'checkbox':
|
|
275
|
-
if ('boolean' != typeof value) formErrors.push({
|
|
276
|
-
field: field.name,
|
|
277
|
-
message: `${field.label} must be true or false`
|
|
278
|
-
});
|
|
279
|
-
break;
|
|
280
|
-
case 'select':
|
|
281
|
-
if ('options' in field && field.options) {
|
|
282
|
-
const validValues = field.options.map((opt)=>opt.value);
|
|
283
|
-
if (!validValues.includes(value)) formErrors.push({
|
|
284
|
-
field: field.name,
|
|
285
|
-
message: `${field.label} must be one of: ${validValues.join(', ')}`
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
break;
|
|
289
|
-
case 'datetime':
|
|
290
|
-
if (value instanceof Date === false && 'string' != typeof value) formErrors.push({
|
|
291
|
-
field: field.name,
|
|
292
|
-
message: `${field.label} must be a valid date`
|
|
293
|
-
});
|
|
294
|
-
break;
|
|
295
|
-
}
|
|
296
|
-
if (field.validate) {
|
|
297
|
-
const error = field.validate(value, data);
|
|
298
|
-
if (error) formErrors.push({
|
|
299
|
-
field: field.name,
|
|
300
|
-
message: error
|
|
301
|
-
});
|
|
249
|
+
const fieldChanges = useRef(new Set());
|
|
250
|
+
const fieldChangeWaiters = useRef(new Set());
|
|
251
|
+
const trackFieldChange = useCallback((pending)=>{
|
|
252
|
+
fieldChanges.current.add(pending);
|
|
253
|
+
const release = ()=>{
|
|
254
|
+
fieldChanges.current.delete(pending);
|
|
255
|
+
if (0 === fieldChanges.current.size) {
|
|
256
|
+
for (const resolve of fieldChangeWaiters.current)resolve();
|
|
257
|
+
fieldChangeWaiters.current.clear();
|
|
302
258
|
}
|
|
303
|
-
}
|
|
304
|
-
|
|
259
|
+
};
|
|
260
|
+
pending.then(release, release);
|
|
261
|
+
return release;
|
|
262
|
+
}, []);
|
|
263
|
+
const waitForFieldChanges = useCallback(async ()=>{
|
|
264
|
+
while(fieldChanges.current.size)await new Promise((resolve)=>fieldChangeWaiters.current.add(resolve));
|
|
265
|
+
}, []);
|
|
266
|
+
const validateForm = useCallback((fields, additionalErrors = [])=>{
|
|
267
|
+
const formErrors = validateDocumentFields(fields, getFieldValues(), {
|
|
268
|
+
respectConditions: true,
|
|
269
|
+
skip: (path)=>pendingUploadsRef.current.has(path)
|
|
270
|
+
});
|
|
271
|
+
errorsRef.current = [
|
|
272
|
+
...additionalErrors,
|
|
273
|
+
...formErrors
|
|
274
|
+
];
|
|
305
275
|
notifyErrorListeners();
|
|
306
|
-
return
|
|
276
|
+
return errorsRef.current;
|
|
307
277
|
}, [
|
|
308
|
-
getFieldValue,
|
|
309
278
|
getFieldValues,
|
|
310
279
|
notifyErrorListeners
|
|
311
280
|
]);
|
|
@@ -342,7 +311,13 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
342
311
|
for (const field of fields){
|
|
343
312
|
const fns = normalizeHooks(field.hooks?.beforeValidate);
|
|
344
313
|
if (0 === fns.length) continue;
|
|
345
|
-
if (field.condition
|
|
314
|
+
if (field.condition) {
|
|
315
|
+
let visible = true;
|
|
316
|
+
try {
|
|
317
|
+
visible = Boolean(field.condition(data, data));
|
|
318
|
+
} catch {}
|
|
319
|
+
if (!visible) continue;
|
|
320
|
+
}
|
|
346
321
|
const path = field.name;
|
|
347
322
|
const value = getFieldValue(path);
|
|
348
323
|
const ctx = {
|
|
@@ -388,8 +363,11 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
388
363
|
setFieldValue,
|
|
389
364
|
notifyErrorListeners
|
|
390
365
|
]);
|
|
391
|
-
|
|
392
|
-
|
|
366
|
+
const resetPatches = useCallback(()=>{
|
|
367
|
+
patchesRef.current = [];
|
|
368
|
+
}, []);
|
|
369
|
+
const getErrors = useCallback(()=>errorsRef.current, []);
|
|
370
|
+
const context = useMemo(()=>({
|
|
393
371
|
documentId,
|
|
394
372
|
collectionPath,
|
|
395
373
|
setFieldValue,
|
|
@@ -398,15 +376,17 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
398
376
|
getFieldValues,
|
|
399
377
|
getPatches,
|
|
400
378
|
appendPatch,
|
|
401
|
-
resetPatches
|
|
402
|
-
patchesRef.current = [];
|
|
403
|
-
},
|
|
379
|
+
resetPatches,
|
|
404
380
|
hasChanges,
|
|
405
381
|
resetHasChanges,
|
|
406
382
|
runFieldHooks,
|
|
407
383
|
validateForm,
|
|
408
|
-
errors
|
|
409
|
-
|
|
384
|
+
get errors () {
|
|
385
|
+
return errorsRef.current;
|
|
386
|
+
},
|
|
387
|
+
getErrors,
|
|
388
|
+
trackFieldChange,
|
|
389
|
+
waitForFieldChanges,
|
|
410
390
|
clearErrors,
|
|
411
391
|
setFieldError,
|
|
412
392
|
clearFieldError,
|
|
@@ -430,7 +410,49 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
430
410
|
getSystemAvailableLocales: availableLocalesSlot.get,
|
|
431
411
|
setSystemAvailableLocales: availableLocalesSlot.set,
|
|
432
412
|
subscribeSystemAvailableLocales: availableLocalesSlot.subscribe
|
|
433
|
-
},
|
|
413
|
+
}), [
|
|
414
|
+
documentId,
|
|
415
|
+
collectionPath,
|
|
416
|
+
setFieldValue,
|
|
417
|
+
setFieldStore,
|
|
418
|
+
getFieldValue,
|
|
419
|
+
getFieldValues,
|
|
420
|
+
getPatches,
|
|
421
|
+
appendPatch,
|
|
422
|
+
resetPatches,
|
|
423
|
+
hasChanges,
|
|
424
|
+
resetHasChanges,
|
|
425
|
+
runFieldHooks,
|
|
426
|
+
validateForm,
|
|
427
|
+
getErrors,
|
|
428
|
+
trackFieldChange,
|
|
429
|
+
waitForFieldChanges,
|
|
430
|
+
clearErrors,
|
|
431
|
+
setFieldError,
|
|
432
|
+
clearFieldError,
|
|
433
|
+
isDirty,
|
|
434
|
+
getDirtyBreakdown,
|
|
435
|
+
subscribeField,
|
|
436
|
+
subscribeErrors,
|
|
437
|
+
subscribeMeta,
|
|
438
|
+
addPendingUpload,
|
|
439
|
+
removePendingUpload,
|
|
440
|
+
removePendingUploadsUnder,
|
|
441
|
+
getPendingUploads,
|
|
442
|
+
hasPendingUploads,
|
|
443
|
+
clearPendingUploads,
|
|
444
|
+
setFieldUploading,
|
|
445
|
+
getIsFieldUploading,
|
|
446
|
+
subscribeFieldUploading,
|
|
447
|
+
pathSlot.get,
|
|
448
|
+
pathSlot.set,
|
|
449
|
+
pathSlot.subscribe,
|
|
450
|
+
availableLocalesSlot.get,
|
|
451
|
+
availableLocalesSlot.set,
|
|
452
|
+
availableLocalesSlot.subscribe
|
|
453
|
+
]);
|
|
454
|
+
return /*#__PURE__*/ jsx(FormContext.Provider, {
|
|
455
|
+
value: context,
|
|
434
456
|
children: /*#__PURE__*/ jsx(FormDomScopeProvider, {
|
|
435
457
|
children: children
|
|
436
458
|
})
|
|
@@ -438,90 +460,50 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
|
|
|
438
460
|
};
|
|
439
461
|
const useSystemPath = ()=>{
|
|
440
462
|
const { getSystemPath, subscribeSystemPath } = useFormContext();
|
|
441
|
-
|
|
442
|
-
useEffect(()=>subscribeSystemPath((next)=>setValue(next)), [
|
|
443
|
-
subscribeSystemPath
|
|
444
|
-
]);
|
|
445
|
-
return value;
|
|
463
|
+
return useSyncExternalStore(subscribeSystemPath, getSystemPath, getSystemPath);
|
|
446
464
|
};
|
|
447
465
|
const useSystemAvailableLocales = ()=>{
|
|
448
466
|
const { getSystemAvailableLocales, subscribeSystemAvailableLocales } = useFormContext();
|
|
449
|
-
|
|
450
|
-
useEffect(()=>subscribeSystemAvailableLocales((next)=>setValue(next)), [
|
|
451
|
-
subscribeSystemAvailableLocales
|
|
452
|
-
]);
|
|
453
|
-
return value;
|
|
467
|
+
return useSyncExternalStore(subscribeSystemAvailableLocales, getSystemAvailableLocales, getSystemAvailableLocales);
|
|
454
468
|
};
|
|
455
469
|
const useFormStore = ()=>useFormContext();
|
|
456
470
|
const useFieldError = (name)=>{
|
|
457
471
|
const { getErrors, subscribeErrors } = useFormContext();
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
const unsubscribe = subscribeErrors((currentErrors)=>{
|
|
461
|
-
const fieldError = currentErrors.find((e)=>e.field === name);
|
|
462
|
-
setError(fieldError?.message);
|
|
463
|
-
});
|
|
464
|
-
return unsubscribe;
|
|
465
|
-
}, [
|
|
466
|
-
subscribeErrors,
|
|
467
|
-
name
|
|
468
|
-
]);
|
|
469
|
-
return error;
|
|
472
|
+
const errors = useSyncExternalStore(subscribeErrors, getErrors, getErrors);
|
|
473
|
+
return errors.find((error)=>error.field === name)?.message;
|
|
470
474
|
};
|
|
471
475
|
const useFormMeta = ()=>{
|
|
472
476
|
const { hasChanges, subscribeMeta } = useFormContext();
|
|
473
|
-
const [hasChangesValue, setHasChangesValue] = useState(hasChanges());
|
|
474
|
-
useEffect(()=>{
|
|
475
|
-
const unsubscribe = subscribeMeta(()=>{
|
|
476
|
-
setHasChangesValue(hasChanges());
|
|
477
|
-
});
|
|
478
|
-
return unsubscribe;
|
|
479
|
-
}, [
|
|
480
|
-
subscribeMeta,
|
|
481
|
-
hasChanges
|
|
482
|
-
]);
|
|
483
477
|
return {
|
|
484
|
-
hasChanges:
|
|
478
|
+
hasChanges: useSyncExternalStore(subscribeMeta, hasChanges, hasChanges)
|
|
485
479
|
};
|
|
486
480
|
};
|
|
487
481
|
const useIsDirty = (name)=>{
|
|
488
482
|
const { isDirty, subscribeMeta } = useFormContext();
|
|
489
|
-
const
|
|
490
|
-
useEffect(()=>{
|
|
491
|
-
const unsubscribe = subscribeMeta(()=>{
|
|
492
|
-
setDirty(isDirty(name));
|
|
493
|
-
});
|
|
494
|
-
return unsubscribe;
|
|
495
|
-
}, [
|
|
496
|
-
subscribeMeta,
|
|
483
|
+
const snapshot = useCallback(()=>isDirty(name), [
|
|
497
484
|
isDirty,
|
|
498
485
|
name
|
|
499
486
|
]);
|
|
500
|
-
return
|
|
487
|
+
return useSyncExternalStore(subscribeMeta, snapshot, snapshot);
|
|
501
488
|
};
|
|
502
489
|
const useFieldValue = (name)=>{
|
|
503
|
-
const { getFieldValue,
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
const unsubscribe = subscribeField(name, (nextValue)=>{
|
|
507
|
-
setValue(nextValue);
|
|
508
|
-
});
|
|
509
|
-
return unsubscribe;
|
|
510
|
-
}, [
|
|
511
|
-
subscribeField,
|
|
490
|
+
const { getFieldValue, subscribeMeta } = useFormContext();
|
|
491
|
+
const snapshot = useCallback(()=>getFieldValue(name), [
|
|
492
|
+
getFieldValue,
|
|
512
493
|
name
|
|
513
494
|
]);
|
|
514
|
-
return
|
|
495
|
+
return useSyncExternalStore(subscribeMeta, snapshot, snapshot);
|
|
515
496
|
};
|
|
516
497
|
const useIsFieldUploading = (fieldPath)=>{
|
|
517
498
|
const { getIsFieldUploading, subscribeFieldUploading } = useFormContext();
|
|
518
|
-
const
|
|
519
|
-
useEffect(()=>subscribeFieldUploading(fieldPath, (next)=>{
|
|
520
|
-
setUploading(next);
|
|
521
|
-
}), [
|
|
499
|
+
const subscribe = useCallback((notify)=>subscribeFieldUploading(fieldPath, notify), [
|
|
522
500
|
subscribeFieldUploading,
|
|
523
501
|
fieldPath
|
|
524
502
|
]);
|
|
525
|
-
|
|
503
|
+
const snapshot = useCallback(()=>getIsFieldUploading(fieldPath), [
|
|
504
|
+
getIsFieldUploading,
|
|
505
|
+
fieldPath
|
|
506
|
+
]);
|
|
507
|
+
return useSyncExternalStore(subscribe, snapshot, snapshot);
|
|
526
508
|
};
|
|
527
509
|
export { FormProvider, useFieldError, useFieldValue, useFormContext, useFormMeta, useFormStore, useIsDirty, useIsFieldUploading, useSystemAvailableLocales, useSystemPath };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment as external_react_Fragment } from "react";
|
|
3
|
+
import { Fragment as external_react_Fragment, useMemo } from "react";
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import { sliceFieldAdmin } from "../fields/field-admin.js";
|
|
6
6
|
import { FieldRenderer } from "../fields/field-renderer.js";
|
|
@@ -17,7 +17,12 @@ const BOTH_REGIONS = [
|
|
|
17
17
|
];
|
|
18
18
|
const FormLayout = ({ fields, adminConfig, regions = BOTH_REGIONS, variant = 'split', activeLocale, initialData, activeTabBySet, onTabChange, sidebarSlot })=>{
|
|
19
19
|
const { fieldByName, tabSetByName, rowByName, groupByName, layout, fieldToTabPath } = useFormLayout(adminConfig, fields);
|
|
20
|
-
const
|
|
20
|
+
const tabSets = useMemo(()=>[
|
|
21
|
+
...tabSetByName.values()
|
|
22
|
+
], [
|
|
23
|
+
tabSetByName
|
|
24
|
+
]);
|
|
25
|
+
const tabs = useFormTabs(fieldToTabPath, activeTabBySet, tabSets);
|
|
21
26
|
const domScope = useFormDomScope();
|
|
22
27
|
const renderField = (fieldName)=>{
|
|
23
28
|
const field = fieldByName.get(fieldName);
|
|
@@ -23,6 +23,18 @@ const FormHeadingRow = ({ heading, headerSlot })=>/*#__PURE__*/ jsxs("div", {
|
|
|
23
23
|
const FormStatusBar = ({ disabled, initialData, workflowStatuses, publishedVersion, scheduling, hasChanges, isUploading, isSubmitting, onCancel, transitions, statusBusy, onStatusBusyChange, onStatusChange, onMutationError, isBlocked, onUnsavedChanges, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, useAsTitle, contentLocale, contentLocales, defaultLocale })=>{
|
|
24
24
|
const { t } = useTranslation('byline-admin');
|
|
25
25
|
const { primaryStatus, secondaryStatuses, isTerminal } = transitions;
|
|
26
|
+
const runTransition = async (status)=>{
|
|
27
|
+
if (isBlocked() || !onStatusChange) return;
|
|
28
|
+
if (hasChanges) return void onUnsavedChanges();
|
|
29
|
+
onStatusBusyChange(true);
|
|
30
|
+
try {
|
|
31
|
+
await onStatusChange(status);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
onMutationError?.(error);
|
|
34
|
+
} finally{
|
|
35
|
+
onStatusBusyChange(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
26
38
|
return /*#__PURE__*/ jsxs("div", {
|
|
27
39
|
className: clsx('byline-form-status-bar', form_renderer_module["status-bar"]),
|
|
28
40
|
children: [
|
|
@@ -90,30 +102,8 @@ const FormStatusBar = ({ disabled, initialData, workflowStatuses, publishedVersi
|
|
|
90
102
|
type: "button",
|
|
91
103
|
intent: isTerminal ? 'info' : 'success',
|
|
92
104
|
disabled: disabled || statusBusy,
|
|
93
|
-
onOptionSelect:
|
|
94
|
-
|
|
95
|
-
if (hasChanges) return void onUnsavedChanges();
|
|
96
|
-
onStatusBusyChange(true);
|
|
97
|
-
try {
|
|
98
|
-
await onStatusChange(value);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
onMutationError?.(error);
|
|
101
|
-
} finally{
|
|
102
|
-
onStatusBusyChange(false);
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
onButtonClick: isTerminal ? void 0 : async ()=>{
|
|
106
|
-
if (isBlocked()) return;
|
|
107
|
-
if (hasChanges) return void onUnsavedChanges();
|
|
108
|
-
onStatusBusyChange(true);
|
|
109
|
-
try {
|
|
110
|
-
await onStatusChange(primaryStatus.name);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
onMutationError?.(error);
|
|
113
|
-
} finally{
|
|
114
|
-
onStatusBusyChange(false);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
105
|
+
onOptionSelect: runTransition,
|
|
106
|
+
onButtonClick: isTerminal ? void 0 : ()=>runTransition(primaryStatus.name),
|
|
117
107
|
children: statusBusy ? '...' : isTerminal ? primaryStatus.label ?? primaryStatus.name : primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name
|
|
118
108
|
})
|
|
119
109
|
}),
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { type ReactNode } from 'react';
|
|
9
9
|
import type { AdminResourceConfig, Field, WorkflowStatus } from '@byline/core';
|
|
10
10
|
import { type ScheduledPublicationInfo, type SchedulePublicationInput } from './scheduled-publication-control.js';
|
|
11
|
+
import { type SystemFieldsSubmitPayload } from './use-form-submission.js';
|
|
11
12
|
import type { DocumentActionsLocaleOption } from './document-actions.js';
|
|
12
13
|
import type { UseNavigationGuard } from './navigation-guard.js';
|
|
13
14
|
export type { SystemFieldsSubmitPayload } from './use-form-submission.js';
|
|
@@ -32,7 +33,7 @@ export interface FormRendererProps {
|
|
|
32
33
|
onReloadDocument?: () => void | Promise<void>;
|
|
33
34
|
mode: 'create' | 'edit';
|
|
34
35
|
fields: Field[];
|
|
35
|
-
onSubmit: (data:
|
|
36
|
+
onSubmit: (data: SystemFieldsSubmitPayload) => void | Promise<void>;
|
|
36
37
|
onCancel: () => void;
|
|
37
38
|
onStatusChange?: (nextStatus: string) => Promise<void>;
|
|
38
39
|
onUnpublish?: () => Promise<void>;
|
|
@@ -13,15 +13,13 @@ import form_renderer_module from "./form-renderer.module.js";
|
|
|
13
13
|
import { useNavigationGuardAdapter } from "./navigation-guard.js";
|
|
14
14
|
import { useScheduledPublication } from "./scheduled-publication-control.js";
|
|
15
15
|
import { computeStatusTransitions } from "./status-transitions.js";
|
|
16
|
+
import { useDocumentReloadRecovery } from "./use-document-reload-recovery.js";
|
|
16
17
|
import { useFormSubmission } from "./use-form-submission.js";
|
|
17
18
|
const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRevision, onMutationError, onTreeMutationCommitted, scheduledPublicationsNeedReconfirmation = false, scheduledPublicationsHref, onReloadDocument, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, showPath = true, heading, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
18
19
|
const { hasChanges: hasChangesFn, subscribeMeta } = useFormContext();
|
|
19
20
|
const { t } = useTranslation('byline-admin');
|
|
20
21
|
const [hasChanges, setHasChanges] = useState(hasChangesFn());
|
|
21
22
|
const [statusBusy, setStatusBusy] = useState(false);
|
|
22
|
-
const formRef = useRef(null);
|
|
23
|
-
const focusBeforeBusyRef = useRef(null);
|
|
24
|
-
const restoreFocusAfterBusyRef = useRef(false);
|
|
25
23
|
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
|
|
26
24
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
27
25
|
const scheduling = useScheduledPublication({
|
|
@@ -79,72 +77,33 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
|
|
|
79
77
|
}) : 'create' === mode ? t('forms.heading.create') : t('forms.heading.edit'));
|
|
80
78
|
const guardFromContext = useNavigationGuardAdapter();
|
|
81
79
|
const useGuard = useNavigationGuardProp ?? guardFromContext;
|
|
82
|
-
const [discarding, setDiscarding] = useState(false);
|
|
83
|
-
const [reloadFailed, setReloadFailed] = useState(false);
|
|
84
|
-
const warningRef = useRef(null);
|
|
85
|
-
const mutationBlockedRef = useRef(mutationsBlocked);
|
|
86
|
-
mutationBlockedRef.current = mutationsBlocked || discarding;
|
|
87
|
-
const guard = useGuard(hasChanges && !discarding);
|
|
88
|
-
useEffect(()=>{
|
|
89
|
-
if (mutationIssue) warningRef.current?.focus();
|
|
90
|
-
}, [
|
|
91
|
-
mutationIssue
|
|
92
|
-
]);
|
|
93
|
-
useEffect(()=>{
|
|
94
|
-
if (!discarding) return;
|
|
95
|
-
Promise.resolve().then(()=>onReloadDocument ? onReloadDocument() : window.location.reload()).catch(()=>{
|
|
96
|
-
setDiscarding(false);
|
|
97
|
-
setReloadFailed(true);
|
|
98
|
-
});
|
|
99
|
-
}, [
|
|
100
|
-
discarding,
|
|
101
|
-
onReloadDocument
|
|
102
|
-
]);
|
|
103
80
|
const currentStatus = initialData?.status;
|
|
104
81
|
const { primaryStatus, secondaryStatuses, isTerminal } = computeStatusTransitions(currentStatus, workflowStatuses, nextStatus);
|
|
105
82
|
useEffect(()=>subscribeMeta(()=>setHasChanges(hasChangesFn())), [
|
|
106
83
|
subscribeMeta,
|
|
107
84
|
hasChangesFn
|
|
108
85
|
]);
|
|
109
|
-
const captureFocusBeforeBusy = useCallback(()=>{
|
|
110
|
-
if (null != focusBeforeBusyRef.current) return;
|
|
111
|
-
const activeElement = document.activeElement;
|
|
112
|
-
if (!(activeElement instanceof HTMLElement) || !formRef.current?.contains(activeElement)) return;
|
|
113
|
-
focusBeforeBusyRef.current = activeElement;
|
|
114
|
-
restoreFocusAfterBusyRef.current = true;
|
|
115
|
-
}, []);
|
|
116
86
|
const submission = useFormSubmission({
|
|
117
87
|
mode,
|
|
118
88
|
fields,
|
|
119
89
|
documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : void 0,
|
|
120
90
|
advertiseLocales,
|
|
121
91
|
onSubmit,
|
|
122
|
-
isBlocked: ()=>mutationBlockedRef.current,
|
|
123
|
-
onBeforeBusy: captureFocusBeforeBusy
|
|
92
|
+
isBlocked: ()=>recovery.mutationBlockedRef.current,
|
|
93
|
+
onBeforeBusy: ()=>recovery.captureFocusBeforeBusy()
|
|
124
94
|
});
|
|
95
|
+
const recovery = useDocumentReloadRecovery({
|
|
96
|
+
mutationsBlocked,
|
|
97
|
+
mutationIssue,
|
|
98
|
+
onReloadDocument,
|
|
99
|
+
isBusy: submission.isBusy
|
|
100
|
+
});
|
|
101
|
+
const { formRef, warningRef, mutationBlockedRef, discarding, reloadFailed, requestDiscard } = recovery;
|
|
102
|
+
const guard = useGuard(hasChanges && !discarding);
|
|
125
103
|
const isUploading = 'uploading' === submission.phase.kind;
|
|
126
104
|
const isSubmitting = 'submitting' === submission.phase.kind;
|
|
127
105
|
const isBusy = submission.isBusy;
|
|
128
106
|
const pendingSystemFieldsSubmit = 'confirmingSystemFields' === submission.phase.kind ? submission.phase.payload : null;
|
|
129
|
-
useEffect(()=>{
|
|
130
|
-
if (isBusy || !restoreFocusAfterBusyRef.current) return;
|
|
131
|
-
restoreFocusAfterBusyRef.current = false;
|
|
132
|
-
if (mutationIssue) {
|
|
133
|
-
warningRef.current?.focus();
|
|
134
|
-
focusBeforeBusyRef.current = null;
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const original = focusBeforeBusyRef.current;
|
|
138
|
-
focusBeforeBusyRef.current = null;
|
|
139
|
-
const originalCanReceiveFocus = original?.isConnected === true && !original.matches(':disabled, [aria-disabled="true"]');
|
|
140
|
-
const target = originalCanReceiveFocus ? original : formRef.current?.querySelector('input:not(:disabled), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])');
|
|
141
|
-
target?.focus({
|
|
142
|
-
preventScroll: true
|
|
143
|
-
});
|
|
144
|
-
}, [
|
|
145
|
-
isBusy,
|
|
146
|
-
mutationIssue
|
|
147
|
-
]);
|
|
148
107
|
const handleCancel = ()=>{
|
|
149
108
|
if (onCancel && 'function' == typeof onCancel) onCancel();
|
|
150
109
|
};
|
|
@@ -216,10 +175,7 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
|
|
|
216
175
|
warningRef: warningRef,
|
|
217
176
|
discarding: discarding,
|
|
218
177
|
reloadFailed: reloadFailed,
|
|
219
|
-
onDiscardRequested:
|
|
220
|
-
setReloadFailed(false);
|
|
221
|
-
setDiscarding(true);
|
|
222
|
-
},
|
|
178
|
+
onDiscardRequested: requestDiscard,
|
|
223
179
|
scheduling: scheduling,
|
|
224
180
|
restoreWarnings: restoreWarnings
|
|
225
181
|
}),
|
|
@@ -26,3 +26,5 @@ export declare function hasExistingIdTargets(object: unknown, path: string): boo
|
|
|
26
26
|
/** Set a path and report whether all stable-id selectors resolved. */
|
|
27
27
|
export declare function setWithResult<T extends object>(object: T, path: string, value: unknown): boolean;
|
|
28
28
|
export declare function set<T extends object>(object: T, path: string, value: unknown): T;
|
|
29
|
+
/** Clone only the containers along a write path, preserving other snapshots. */
|
|
30
|
+
export declare function withValue<T extends object>(object: T, path: string, value: unknown): T | undefined;
|
|
@@ -95,4 +95,29 @@ function set(object, path, value) {
|
|
|
95
95
|
setWithResult(object, path, value);
|
|
96
96
|
return object;
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
function withValue(object, path, value) {
|
|
99
|
+
const parsed = parseInstancePath(path);
|
|
100
|
+
if (!parsed.ok || !hasExistingIdTargets(object, path)) return;
|
|
101
|
+
const result = Array.isArray(object) ? [
|
|
102
|
+
...object
|
|
103
|
+
] : {
|
|
104
|
+
...object
|
|
105
|
+
};
|
|
106
|
+
let current = result;
|
|
107
|
+
for (const [index, segment] of parsed.segments.entries()){
|
|
108
|
+
const key = 'field' === segment.kind ? segment.name : 'index' === segment.kind ? segment.index : 'id' === segment.kind ? selectId(current, segment.id) : void 0;
|
|
109
|
+
if (void 0 === key || -1 === key) break;
|
|
110
|
+
if (index === parsed.segments.length - 1) {
|
|
111
|
+
current[key] = value;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
const existing = current[key];
|
|
115
|
+
current[key] = Array.isArray(existing) ? [
|
|
116
|
+
...existing
|
|
117
|
+
] : null != existing && 'object' == typeof existing ? {
|
|
118
|
+
...existing
|
|
119
|
+
} : newContainer(parsed.segments[index + 1]);
|
|
120
|
+
current = current[key];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export { get, hasExistingIdTargets, set, setWithResult, toPath, withValue };
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
/** Reload admission and focus recovery remain synchronous at event boundaries. */
|
|
9
|
+
export declare function useDocumentReloadRecovery({ mutationsBlocked, mutationIssue, onReloadDocument, isBusy, }: {
|
|
10
|
+
mutationsBlocked: boolean;
|
|
11
|
+
mutationIssue?: string | null;
|
|
12
|
+
onReloadDocument?: () => void | Promise<void>;
|
|
13
|
+
isBusy: boolean;
|
|
14
|
+
}): {
|
|
15
|
+
formRef: import("react").RefObject<HTMLFormElement | null>;
|
|
16
|
+
warningRef: import("react").RefObject<HTMLDivElement | null>;
|
|
17
|
+
mutationBlockedRef: import("react").RefObject<boolean>;
|
|
18
|
+
discarding: boolean;
|
|
19
|
+
reloadFailed: boolean;
|
|
20
|
+
requestDiscard: () => void;
|
|
21
|
+
captureFocusBeforeBusy: () => void;
|
|
22
|
+
};
|