@byline/admin 3.17.1 → 3.19.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/file/file-field.js +8 -2
- package/dist/fields/image/image-field.js +8 -2
- package/dist/forms/form-context.d.ts +13 -1
- package/dist/forms/form-context.js +2 -1
- package/dist/forms/form-renderer.js +18 -1
- package/dist/forms/path-widget.d.ts +19 -1
- package/dist/forms/path-widget.js +10 -6
- package/dist/forms/upload-executor.d.ts +27 -3
- package/dist/forms/upload-executor.js +83 -8
- package/dist/forms/upload-executor.test.node.d.ts +8 -0
- package/dist/modules/admin-account/errors.d.ts +2 -2
- package/dist/modules/admin-activity/abilities.d.ts +1 -1
- package/dist/modules/admin-permissions/abilities.d.ts +2 -2
- package/dist/modules/admin-permissions/errors.d.ts +2 -2
- package/dist/modules/admin-permissions/schemas.d.ts +4 -4
- package/dist/modules/admin-roles/abilities.d.ts +4 -4
- package/dist/modules/admin-roles/errors.d.ts +4 -4
- package/dist/modules/admin-users/abilities.d.ts +5 -5
- package/dist/modules/admin-users/errors.d.ts +5 -5
- package/dist/modules/admin-users/schemas.d.ts +6 -6
- package/dist/vendor/noble-argon2/blake2.js +17 -17
- package/dist/widgets/source-locale-badge/source-locale-badge.d.ts +4 -18
- package/package.json +16 -16
- package/src/fields/file/file-field.tsx +16 -2
- package/src/fields/image/image-field.tsx +16 -2
- package/src/forms/form-context.tsx +14 -0
- package/src/forms/form-renderer.tsx +27 -0
- package/src/forms/path-widget.test.tsx +33 -0
- package/src/forms/path-widget.tsx +33 -5
- package/src/forms/upload-executor.test.node.ts +176 -0
- package/src/forms/upload-executor.ts +209 -14
|
@@ -25,7 +25,7 @@ const FileField = ({ field, collectionPath, value, defaultValue, onChange: _onCh
|
|
|
25
25
|
const isDirty = useIsDirty(fieldPath);
|
|
26
26
|
const fieldValue = useFieldValue(fieldPath);
|
|
27
27
|
const isUploading = useIsFieldUploading(fieldPath);
|
|
28
|
-
const { removePendingUpload } = useFormContext();
|
|
28
|
+
const { removePendingUpload, documentId } = useFormContext();
|
|
29
29
|
const handleChange = useFieldChangeHandler(field, fieldPath);
|
|
30
30
|
const incomingValue = isDirty ? fieldValue ?? null : value ?? fieldValue ?? defaultValue ?? null;
|
|
31
31
|
const isPending = isPendingStoredFileValue(incomingValue);
|
|
@@ -35,6 +35,7 @@ const FileField = ({ field, collectionPath, value, defaultValue, onChange: _onCh
|
|
|
35
35
|
return 'placeholder' === maybe.storageProvider && 'pending' === maybe.storagePath;
|
|
36
36
|
};
|
|
37
37
|
const showUploadWidget = null == incomingValue || isOldPlaceholder(incomingValue);
|
|
38
|
+
const uploadGated = field.upload?.requireSavedDocument === true && null == documentId;
|
|
38
39
|
const handleRemove = ()=>{
|
|
39
40
|
if (isPending) removePendingUpload(fieldPath);
|
|
40
41
|
handleChange(null);
|
|
@@ -54,7 +55,12 @@ const FileField = ({ field, collectionPath, value, defaultValue, onChange: _onCh
|
|
|
54
55
|
required: !field.optional
|
|
55
56
|
})
|
|
56
57
|
}),
|
|
57
|
-
showUploadWidget ?
|
|
58
|
+
showUploadWidget ? uploadGated ? /*#__PURE__*/ jsx("div", {
|
|
59
|
+
className: classnames('byline-field-file-empty', file_field_module.empty),
|
|
60
|
+
role: "note",
|
|
61
|
+
"data-testid": "upload-require-saved-document",
|
|
62
|
+
children: t('fields.upload.requireSavedDocument')
|
|
63
|
+
}) : collectionPath ? /*#__PURE__*/ jsx(FileUploadField, {
|
|
58
64
|
field: field,
|
|
59
65
|
collectionPath: collectionPath,
|
|
60
66
|
fieldPath: fieldPath,
|
|
@@ -14,7 +14,7 @@ const ImageField = ({ field, collectionPath, value, defaultValue, onChange: _onC
|
|
|
14
14
|
const isDirty = useIsDirty(fieldPath);
|
|
15
15
|
const fieldValue = useFieldValue(fieldPath);
|
|
16
16
|
const isUploading = useIsFieldUploading(fieldPath);
|
|
17
|
-
const { removePendingUpload } = useFormContext();
|
|
17
|
+
const { removePendingUpload, documentId } = useFormContext();
|
|
18
18
|
const { t } = useTranslation('byline-admin');
|
|
19
19
|
const handleChange = useFieldChangeHandler(field, fieldPath);
|
|
20
20
|
const incomingValue = isDirty ? fieldValue ?? null : value ?? fieldValue ?? defaultValue ?? null;
|
|
@@ -25,6 +25,7 @@ const ImageField = ({ field, collectionPath, value, defaultValue, onChange: _onC
|
|
|
25
25
|
return 'placeholder' === maybe.storageProvider && 'pending' === maybe.storagePath;
|
|
26
26
|
};
|
|
27
27
|
const showUploadWidget = null == incomingValue || isOldPlaceholder(incomingValue);
|
|
28
|
+
const uploadGated = field.upload?.requireSavedDocument === true && null == documentId;
|
|
28
29
|
const thumbVariant = incomingValue && !isPendingStoredFileValue(incomingValue) ? incomingValue.variants?.find((v)=>'thumbnail' === v.name) : void 0;
|
|
29
30
|
const previewUrl = thumbVariant?.storageUrl ?? incomingValue?.storageUrl;
|
|
30
31
|
const handleRemove = ()=>{
|
|
@@ -46,7 +47,12 @@ const ImageField = ({ field, collectionPath, value, defaultValue, onChange: _onC
|
|
|
46
47
|
required: !field.optional
|
|
47
48
|
})
|
|
48
49
|
}),
|
|
49
|
-
showUploadWidget ?
|
|
50
|
+
showUploadWidget ? uploadGated ? /*#__PURE__*/ jsx("div", {
|
|
51
|
+
className: classnames('byline-field-image-empty', image_field_module.empty),
|
|
52
|
+
role: "note",
|
|
53
|
+
"data-testid": "upload-require-saved-document",
|
|
54
|
+
children: t('fields.upload.requireSavedDocument')
|
|
55
|
+
}) : collectionPath ? /*#__PURE__*/ jsx(ImageUploadField, {
|
|
50
56
|
field: field,
|
|
51
57
|
collectionPath: collectionPath,
|
|
52
58
|
fieldPath: fieldPath,
|
|
@@ -48,6 +48,13 @@ export interface DirtyBreakdown {
|
|
|
48
48
|
availableLocalesDirty: boolean;
|
|
49
49
|
}
|
|
50
50
|
interface FormContextType {
|
|
51
|
+
/**
|
|
52
|
+
* The persisted document id when the form edits an existing document,
|
|
53
|
+
* `null` while the document is unsaved (create mode). Upload widgets use
|
|
54
|
+
* this to honour `upload.requireSavedDocument` (see `UploadConfig` in
|
|
55
|
+
* `@byline/core`).
|
|
56
|
+
*/
|
|
57
|
+
documentId: string | null;
|
|
51
58
|
setFieldValue: (name: string, value: any) => void;
|
|
52
59
|
setFieldStore: (name: string, value: any) => void;
|
|
53
60
|
getFieldValue: (name: string) => any;
|
|
@@ -90,9 +97,14 @@ interface FormContextType {
|
|
|
90
97
|
subscribeSystemAvailableLocales: (listener: SystemAvailableLocalesListener) => () => void;
|
|
91
98
|
}
|
|
92
99
|
export declare const useFormContext: () => FormContextType;
|
|
93
|
-
export declare const FormProvider: ({ children, initialData, }: {
|
|
100
|
+
export declare const FormProvider: ({ children, initialData, documentId, }: {
|
|
94
101
|
children: React.ReactNode;
|
|
95
102
|
initialData?: Record<string, any>;
|
|
103
|
+
/**
|
|
104
|
+
* The persisted document id (edit mode); `null` while unsaved. Exposed on
|
|
105
|
+
* the context for upload widgets honouring `upload.requireSavedDocument`.
|
|
106
|
+
*/
|
|
107
|
+
documentId?: string | null;
|
|
96
108
|
}) => React.JSX.Element;
|
|
97
109
|
/**
|
|
98
110
|
* Subscribe to the system `path` slot edited by the path widget.
|
|
@@ -22,7 +22,7 @@ const useFormContext = ()=>{
|
|
|
22
22
|
if (null == context) throw new Error('useFormContext must be used within a FormProvider');
|
|
23
23
|
return context;
|
|
24
24
|
};
|
|
25
|
-
const FormProvider = ({ children, initialData = {} })=>{
|
|
25
|
+
const FormProvider = ({ children, initialData = {}, documentId = null })=>{
|
|
26
26
|
const fieldValues = useRef(JSON.parse(JSON.stringify(initialData?.fields ?? initialData)));
|
|
27
27
|
const initialValues = useRef(initialData?.fields ?? initialData);
|
|
28
28
|
const errorsRef = useRef([]);
|
|
@@ -376,6 +376,7 @@ const FormProvider = ({ children, initialData = {} })=>{
|
|
|
376
376
|
]);
|
|
377
377
|
return /*#__PURE__*/ jsx(FormContext.Provider, {
|
|
378
378
|
value: {
|
|
379
|
+
documentId,
|
|
379
380
|
setFieldValue,
|
|
380
381
|
setFieldStore,
|
|
381
382
|
getFieldValue,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { getClientConfig } from "@byline/core";
|
|
4
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
5
6
|
import { Alert, Button, ComboButton } from "@byline/ui/react";
|
|
6
7
|
import classnames from "classnames";
|
|
@@ -32,6 +33,15 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
32
33
|
const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
|
|
33
34
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
34
35
|
const { uploadField } = useBylineFieldServices();
|
|
36
|
+
const pathSlugifier = getClientConfig().slugifier;
|
|
37
|
+
const pathSourceLocked = useMemo(()=>{
|
|
38
|
+
if (!useAsPath) return false;
|
|
39
|
+
const source = fields.find((f)=>f.name === useAsPath);
|
|
40
|
+
return null != source && ('counter' === source.type || true === source.readOnly);
|
|
41
|
+
}, [
|
|
42
|
+
useAsPath,
|
|
43
|
+
fields
|
|
44
|
+
]);
|
|
35
45
|
useEffect(()=>{
|
|
36
46
|
if (initialLocale) setContentLocale(initialLocale);
|
|
37
47
|
}, [
|
|
@@ -112,6 +122,10 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
112
122
|
try {
|
|
113
123
|
const uploadResult = await executeUploadsWithProgress(pendingUploads, uploadField, ({ fieldPath, status })=>{
|
|
114
124
|
setFieldUploading(fieldPath, 'uploading' === status);
|
|
125
|
+
}, {
|
|
126
|
+
documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : void 0,
|
|
127
|
+
fields,
|
|
128
|
+
getFormValues: getFieldValues
|
|
115
129
|
});
|
|
116
130
|
if (!uploadResult.allSucceeded) {
|
|
117
131
|
for (const [fieldPath, errorMessage] of uploadResult.errors.entries())setFieldError(fieldPath, t('forms.uploadFailedFieldError', {
|
|
@@ -345,7 +359,9 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
345
359
|
collectionPath: collectionPath ?? '',
|
|
346
360
|
defaultLocale: defaultLocale,
|
|
347
361
|
activeLocale: contentLocale,
|
|
348
|
-
mode: mode
|
|
362
|
+
mode: mode,
|
|
363
|
+
slugifier: pathSlugifier,
|
|
364
|
+
sourceLocked: pathSourceLocked
|
|
349
365
|
}),
|
|
350
366
|
tree && 'edit' === mode && 'string' == typeof initialData?.id && /*#__PURE__*/ jsx(TreePlacementWidget, {
|
|
351
367
|
collectionPath: collectionPath ?? '',
|
|
@@ -386,6 +402,7 @@ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpu
|
|
|
386
402
|
const savedTabsRef = useRef({});
|
|
387
403
|
return /*#__PURE__*/ jsx(FormProvider, {
|
|
388
404
|
initialData: initialData,
|
|
405
|
+
documentId: 'edit' === mode && 'string' == typeof initialData?.id ? initialData.id : null,
|
|
389
406
|
children: /*#__PURE__*/ jsx(FormContent, {
|
|
390
407
|
mode: mode,
|
|
391
408
|
fields: fields,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SlugifierFn } from '@byline/core';
|
|
1
2
|
export interface PathWidgetProps {
|
|
2
3
|
/** The collection's `useAsPath` source field name, when configured. */
|
|
3
4
|
useAsPath: string | undefined;
|
|
@@ -16,6 +17,23 @@ export interface PathWidgetProps {
|
|
|
16
17
|
activeLocale: string;
|
|
17
18
|
/** `'create'` shows the live derived preview as placeholder text. */
|
|
18
19
|
mode: 'create' | 'edit';
|
|
20
|
+
/**
|
|
21
|
+
* Installation slugifier used to derive the live preview. Must match the
|
|
22
|
+
* server-side `ServerConfig.slugifier` so the preview agrees with what is
|
|
23
|
+
* persisted. Defaults to the built-in `slugify` when omitted — callers
|
|
24
|
+
* that keep the default slugifier need not pass this.
|
|
25
|
+
*/
|
|
26
|
+
slugifier?: SlugifierFn;
|
|
27
|
+
/**
|
|
28
|
+
* When `true`, the `useAsPath` source field's value is not editable through
|
|
29
|
+
* this form (e.g. it is an allocator-assigned `counter`, or an otherwise
|
|
30
|
+
* read-only field). Its value is either server-assigned — and so not
|
|
31
|
+
* reproducible client-side — or simply cannot change, which means the
|
|
32
|
+
* source-derived live preview and the "Regenerate" affordance are
|
|
33
|
+
* meaningless. When set, the widget suppresses both and just shows the
|
|
34
|
+
* persisted path. Defaults to `false`.
|
|
35
|
+
*/
|
|
36
|
+
sourceLocked?: boolean;
|
|
19
37
|
}
|
|
20
38
|
/**
|
|
21
39
|
* System-managed `path` widget.
|
|
@@ -33,4 +51,4 @@ export interface PathWidgetProps {
|
|
|
33
51
|
* Stable override handles: `.byline-form-path`, `.byline-form-path-header`,
|
|
34
52
|
* `.byline-form-path-regenerate`.
|
|
35
53
|
*/
|
|
36
|
-
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, }: PathWidgetProps) => import("react").JSX.Element;
|
|
54
|
+
export declare const PathWidget: ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked, }: PathWidgetProps) => import("react").JSX.Element;
|
|
@@ -12,25 +12,28 @@ function coerceToString(value) {
|
|
|
12
12
|
if (value instanceof Date) return value.toISOString();
|
|
13
13
|
return String(value);
|
|
14
14
|
}
|
|
15
|
-
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode })=>{
|
|
15
|
+
const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mode, slugifier, sourceLocked = false })=>{
|
|
16
16
|
const { setSystemPath } = useFormContext();
|
|
17
17
|
const { t } = useTranslation('byline-admin');
|
|
18
18
|
const systemPath = useSystemPath();
|
|
19
19
|
const sourceValue = useFieldValue(useAsPath ?? '');
|
|
20
|
+
const runSlugify = slugifier ?? slugify;
|
|
20
21
|
const isReadOnly = activeLocale !== defaultLocale;
|
|
21
22
|
const livePreview = useMemo(()=>{
|
|
22
|
-
if (!useAsPath) return '';
|
|
23
|
+
if (!useAsPath || sourceLocked) return '';
|
|
23
24
|
const asString = coerceToString(sourceValue);
|
|
24
25
|
if (0 === asString.length) return '';
|
|
25
|
-
return
|
|
26
|
+
return runSlugify(asString, {
|
|
26
27
|
locale: defaultLocale,
|
|
27
28
|
collectionPath
|
|
28
29
|
});
|
|
29
30
|
}, [
|
|
30
31
|
useAsPath,
|
|
32
|
+
sourceLocked,
|
|
31
33
|
sourceValue,
|
|
32
34
|
defaultLocale,
|
|
33
|
-
collectionPath
|
|
35
|
+
collectionPath,
|
|
36
|
+
runSlugify
|
|
34
37
|
]);
|
|
35
38
|
const inputValue = systemPath ?? '';
|
|
36
39
|
const handleChange = useCallback((next)=>{
|
|
@@ -46,14 +49,15 @@ const PathWidget = ({ useAsPath, collectionPath, defaultLocale, activeLocale, mo
|
|
|
46
49
|
]);
|
|
47
50
|
const formatted = useMemo(()=>{
|
|
48
51
|
if (0 === inputValue.length) return '';
|
|
49
|
-
return
|
|
52
|
+
return runSlugify(inputValue, {
|
|
50
53
|
locale: defaultLocale,
|
|
51
54
|
collectionPath
|
|
52
55
|
});
|
|
53
56
|
}, [
|
|
54
57
|
inputValue,
|
|
55
58
|
defaultLocale,
|
|
56
|
-
collectionPath
|
|
59
|
+
collectionPath,
|
|
60
|
+
runSlugify
|
|
57
61
|
]);
|
|
58
62
|
const validationHint = inputValue.length > 0 && formatted !== inputValue ? t('pathWidget.suggestedHint', {
|
|
59
63
|
formatted
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* This enables "deferred uploads" — files are selected/previewed immediately
|
|
13
13
|
* but only uploaded when the user clicks Save.
|
|
14
14
|
*/
|
|
15
|
-
import type { StoredFileValue } from '@byline/core';
|
|
15
|
+
import type { Field, StoredFileValue } from '@byline/core';
|
|
16
16
|
import type { UploadFieldFn } from '../fields/field-services-types';
|
|
17
17
|
import type { PendingUpload } from './form-context';
|
|
18
18
|
export interface UploadResult {
|
|
@@ -31,6 +31,28 @@ export interface ExecuteUploadsResult {
|
|
|
31
31
|
/** Whether all uploads succeeded */
|
|
32
32
|
allSucceeded: boolean;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Optional document context threaded from the form renderer so upload
|
|
36
|
+
* requests carry the state that server-side `beforeStore` / `afterStore`
|
|
37
|
+
* hooks need (see `UploadConfig.context` in `@byline/core`).
|
|
38
|
+
*/
|
|
39
|
+
export interface UploadExecutionContext {
|
|
40
|
+
/**
|
|
41
|
+
* The persisted document id (edit mode). Posted as `documentId` on every
|
|
42
|
+
* upload request; omitted while the document is unsaved (create mode).
|
|
43
|
+
*/
|
|
44
|
+
documentId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The collection's schema fields — used to locate each upload field's
|
|
47
|
+
* `upload.context` declaration by walking the pending upload's field path.
|
|
48
|
+
*/
|
|
49
|
+
fields?: readonly Field[];
|
|
50
|
+
/**
|
|
51
|
+
* Snapshot accessor for the live form values, resolved lazily per upload
|
|
52
|
+
* so context reflects the state at the moment the request is built.
|
|
53
|
+
*/
|
|
54
|
+
getFormValues?: () => Record<string, any>;
|
|
55
|
+
}
|
|
34
56
|
/**
|
|
35
57
|
* Execute all pending uploads sequentially.
|
|
36
58
|
* Returns a result object with successful uploads and any errors.
|
|
@@ -38,9 +60,11 @@ export interface ExecuteUploadsResult {
|
|
|
38
60
|
* @param pendingUploads - Map of field path to PendingUpload
|
|
39
61
|
* @param uploadField - Host-provided upload transport (resolved via
|
|
40
62
|
* `useBylineFieldServices()` in the calling React tree)
|
|
63
|
+
* @param executionContext - Optional document/form context appended to each
|
|
64
|
+
* upload request (documentId, `upload.context` values)
|
|
41
65
|
* @returns Promise resolving to ExecuteUploadsResult
|
|
42
66
|
*/
|
|
43
|
-
export declare function executeUploads(pendingUploads: Map<string, PendingUpload>, uploadField: UploadFieldFn): Promise<ExecuteUploadsResult>;
|
|
67
|
+
export declare function executeUploads(pendingUploads: Map<string, PendingUpload>, uploadField: UploadFieldFn, executionContext?: UploadExecutionContext): Promise<ExecuteUploadsResult>;
|
|
44
68
|
/**
|
|
45
69
|
* Progress callback type for upload execution with progress tracking.
|
|
46
70
|
*/
|
|
@@ -54,4 +78,4 @@ export type UploadProgressCallback = (info: {
|
|
|
54
78
|
* Execute uploads with progress callbacks.
|
|
55
79
|
* Useful for showing upload progress in the UI.
|
|
56
80
|
*/
|
|
57
|
-
export declare function executeUploadsWithProgress(pendingUploads: Map<string, PendingUpload>, uploadField: UploadFieldFn, onProgress?: UploadProgressCallback): Promise<ExecuteUploadsResult>;
|
|
81
|
+
export declare function executeUploadsWithProgress(pendingUploads: Map<string, PendingUpload>, uploadField: UploadFieldFn, onProgress?: UploadProgressCallback, executionContext?: UploadExecutionContext): Promise<ExecuteUploadsResult>;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { get } from "./nested-path.js";
|
|
2
|
+
async function executeUploads(pendingUploads, uploadField, executionContext) {
|
|
2
3
|
const results = [];
|
|
3
4
|
const successful = new Map();
|
|
4
5
|
const errors = new Map();
|
|
5
6
|
for (const [fieldPath, upload] of pendingUploads.entries()){
|
|
6
|
-
const formData =
|
|
7
|
-
formData.append('file', upload.file);
|
|
8
|
-
formData.append('field', uploadFieldName(fieldPath));
|
|
7
|
+
const formData = buildUploadFormData(fieldPath, upload, executionContext);
|
|
9
8
|
try {
|
|
10
9
|
const result = await uploadField(upload.collectionPath, formData, false);
|
|
11
10
|
results.push({
|
|
@@ -31,11 +30,89 @@ async function executeUploads(pendingUploads, uploadField) {
|
|
|
31
30
|
allSucceeded: 0 === errors.size
|
|
32
31
|
};
|
|
33
32
|
}
|
|
33
|
+
function buildUploadFormData(fieldPath, upload, executionContext) {
|
|
34
|
+
const formData = new FormData();
|
|
35
|
+
formData.append('file', upload.file);
|
|
36
|
+
formData.append('field', uploadFieldName(fieldPath));
|
|
37
|
+
formData.append('fieldPath', fieldPath);
|
|
38
|
+
if (executionContext?.documentId) formData.append('documentId', executionContext.documentId);
|
|
39
|
+
const contextPaths = executionContext?.fields != null ? findUploadFieldByPath(executionContext.fields, fieldPath)?.context : void 0;
|
|
40
|
+
if (contextPaths && contextPaths.length > 0 && executionContext?.getFormValues) {
|
|
41
|
+
const formValues = executionContext.getFormValues();
|
|
42
|
+
for (const contextPath of contextPaths){
|
|
43
|
+
const resolvedPath = resolveContextPath(fieldPath, contextPath);
|
|
44
|
+
if (void 0 === resolvedPath) continue;
|
|
45
|
+
const value = '' === resolvedPath ? formValues : get(formValues, resolvedPath);
|
|
46
|
+
const serialized = serializeContextValue(value);
|
|
47
|
+
if (void 0 !== serialized) formData.append(leafName(contextPath), serialized);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return formData;
|
|
51
|
+
}
|
|
34
52
|
function uploadFieldName(fieldPath) {
|
|
35
53
|
const dot = fieldPath.lastIndexOf('.');
|
|
36
54
|
return -1 === dot ? fieldPath : fieldPath.slice(dot + 1);
|
|
37
55
|
}
|
|
38
|
-
|
|
56
|
+
function leafName(contextPath) {
|
|
57
|
+
const segments = contextPath.split('/').pop() ?? contextPath;
|
|
58
|
+
const dot = segments.lastIndexOf('.');
|
|
59
|
+
return -1 === dot ? segments : segments.slice(dot + 1);
|
|
60
|
+
}
|
|
61
|
+
function resolveContextPath(fieldPath, contextPath) {
|
|
62
|
+
if (contextPath.startsWith('/')) return contextPath.slice(1);
|
|
63
|
+
const scope = fieldPath.split('.');
|
|
64
|
+
scope.pop();
|
|
65
|
+
const parts = contextPath.split('/');
|
|
66
|
+
const leaf = parts.pop() ?? '';
|
|
67
|
+
for (const part of parts)if ('.' !== part && '' !== part) {
|
|
68
|
+
if ('..' === part) {
|
|
69
|
+
if (0 === scope.length) return;
|
|
70
|
+
scope.pop();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
scope.push(part);
|
|
74
|
+
}
|
|
75
|
+
return 0 === scope.length ? leaf : `${scope.join('.')}${leaf ? `.${leaf}` : ''}`;
|
|
76
|
+
}
|
|
77
|
+
function serializeContextValue(value) {
|
|
78
|
+
if (null == value) return;
|
|
79
|
+
if ('string' == typeof value) return value;
|
|
80
|
+
if ('number' == typeof value || 'boolean' == typeof value) return String(value);
|
|
81
|
+
if (isRelationEnvelope(value)) return value.targetDocumentId;
|
|
82
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isRelationEnvelope)) return value.map((v)=>v.targetDocumentId).join(',');
|
|
83
|
+
try {
|
|
84
|
+
return JSON.stringify(value);
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function isRelationEnvelope(value) {
|
|
90
|
+
return 'object' == typeof value && null !== value && 'string' == typeof value.targetDocumentId;
|
|
91
|
+
}
|
|
92
|
+
function findUploadFieldByPath(fields, fieldPath) {
|
|
93
|
+
const segments = fieldPath.split('.').map((s)=>s.replace(/\[\d+\]$/, ''));
|
|
94
|
+
let currentFields = fields;
|
|
95
|
+
for(let i = 0; i < segments.length; i++){
|
|
96
|
+
const name = segments[i];
|
|
97
|
+
const isLeaf = i === segments.length - 1;
|
|
98
|
+
const field = currentFields.find((f)=>f.name === name);
|
|
99
|
+
if (!field) break;
|
|
100
|
+
if (isLeaf) return 'image' === field.type || 'file' === field.type ? field.upload : void 0;
|
|
101
|
+
if ('group' === field.type || 'array' === field.type) {
|
|
102
|
+
currentFields = field.fields;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if ('blocks' === field.type) {
|
|
106
|
+
const remaining = segments[i + 1];
|
|
107
|
+
const block = field.blocks.find((b)=>b.fields.some((f)=>f.name === remaining));
|
|
108
|
+
if (!block) return;
|
|
109
|
+
currentFields = block.fields;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function executeUploadsWithProgress(pendingUploads, uploadField, onProgress, executionContext) {
|
|
39
116
|
const results = [];
|
|
40
117
|
const successful = new Map();
|
|
41
118
|
const errors = new Map();
|
|
@@ -51,9 +128,7 @@ async function executeUploadsWithProgress(pendingUploads, uploadField, onProgres
|
|
|
51
128
|
fieldPath,
|
|
52
129
|
status: 'uploading'
|
|
53
130
|
});
|
|
54
|
-
const formData =
|
|
55
|
-
formData.append('file', upload.file);
|
|
56
|
-
formData.append('field', uploadFieldName(fieldPath));
|
|
131
|
+
const formData = buildUploadFormData(fieldPath, upload, executionContext);
|
|
57
132
|
try {
|
|
58
133
|
const result = await uploadField(upload.collectionPath, formData, false);
|
|
59
134
|
results.push({
|
|
@@ -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 {};
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* code per condition.
|
|
21
21
|
*/
|
|
22
22
|
export declare const AdminAccountErrorCodes: {
|
|
23
|
-
readonly NOT_FOUND:
|
|
24
|
-
readonly INVALID_CURRENT_PASSWORD:
|
|
23
|
+
readonly NOT_FOUND: 'admin.account.notFound';
|
|
24
|
+
readonly INVALID_CURRENT_PASSWORD: 'admin.account.invalidCurrentPassword';
|
|
25
25
|
};
|
|
26
26
|
export type AdminAccountErrorCode = (typeof AdminAccountErrorCodes)[keyof typeof AdminAccountErrorCodes];
|
|
27
27
|
export interface AdminAccountErrorOptions {
|
|
@@ -20,7 +20,7 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
20
20
|
* append-only and is written by the lifecycle write-points, never edited.
|
|
21
21
|
*/
|
|
22
22
|
export declare const ADMIN_ACTIVITY_ABILITIES: {
|
|
23
|
-
readonly read:
|
|
23
|
+
readonly read: 'admin.activity.read';
|
|
24
24
|
};
|
|
25
25
|
export type AdminActivityAbilityKey = (typeof ADMIN_ACTIVITY_ABILITIES)[keyof typeof ADMIN_ACTIVITY_ABILITIES];
|
|
26
26
|
/**
|
|
@@ -19,8 +19,8 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
19
19
|
* every permission-managing role.
|
|
20
20
|
*/
|
|
21
21
|
export declare const ADMIN_PERMISSIONS_ABILITIES: {
|
|
22
|
-
readonly read:
|
|
23
|
-
readonly update:
|
|
22
|
+
readonly read: 'admin.permissions.read';
|
|
23
|
+
readonly update: 'admin.permissions.update';
|
|
24
24
|
};
|
|
25
25
|
export type AdminPermissionsAbilityKey = (typeof ADMIN_PERMISSIONS_ABILITIES)[keyof typeof ADMIN_PERMISSIONS_ABILITIES];
|
|
26
26
|
export declare function registerAdminPermissionsAbilities(registry: AbilityRegistry): void;
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* inspector is read-only and never throws either of these.
|
|
15
15
|
*/
|
|
16
16
|
export declare const AdminPermissionsErrorCodes: {
|
|
17
|
-
readonly ROLE_NOT_FOUND:
|
|
18
|
-
readonly ABILITY_UNREGISTERED:
|
|
17
|
+
readonly ROLE_NOT_FOUND: 'admin.permissions.roleNotFound';
|
|
18
|
+
readonly ABILITY_UNREGISTERED: 'admin.permissions.abilityUnregistered';
|
|
19
19
|
};
|
|
20
20
|
export type AdminPermissionsErrorCode = (typeof AdminPermissionsErrorCodes)[keyof typeof AdminPermissionsErrorCodes];
|
|
21
21
|
export interface AdminPermissionsErrorOptions {
|
|
@@ -29,8 +29,8 @@ export declare const abilityDescriptorResponseSchema: z.ZodObject<{
|
|
|
29
29
|
source: z.ZodNullable<z.ZodEnum<{
|
|
30
30
|
admin: "admin";
|
|
31
31
|
collection: "collection";
|
|
32
|
-
plugin: "plugin";
|
|
33
32
|
core: "core";
|
|
33
|
+
plugin: "plugin";
|
|
34
34
|
}>>;
|
|
35
35
|
}, z.core.$strip>;
|
|
36
36
|
export type AbilityDescriptorResponse = z.infer<typeof abilityDescriptorResponseSchema>;
|
|
@@ -44,8 +44,8 @@ export declare const abilityGroupResponseSchema: z.ZodObject<{
|
|
|
44
44
|
source: z.ZodNullable<z.ZodEnum<{
|
|
45
45
|
admin: "admin";
|
|
46
46
|
collection: "collection";
|
|
47
|
-
plugin: "plugin";
|
|
48
47
|
core: "core";
|
|
48
|
+
plugin: "plugin";
|
|
49
49
|
}>>;
|
|
50
50
|
}, z.core.$strip>>;
|
|
51
51
|
}, z.core.$strip>;
|
|
@@ -63,8 +63,8 @@ export declare const listRegisteredAbilitiesResponseSchema: z.ZodObject<{
|
|
|
63
63
|
source: z.ZodNullable<z.ZodEnum<{
|
|
64
64
|
admin: "admin";
|
|
65
65
|
collection: "collection";
|
|
66
|
-
plugin: "plugin";
|
|
67
66
|
core: "core";
|
|
67
|
+
plugin: "plugin";
|
|
68
68
|
}>>;
|
|
69
69
|
}, z.core.$strip>>;
|
|
70
70
|
groups: z.ZodArray<z.ZodObject<{
|
|
@@ -77,8 +77,8 @@ export declare const listRegisteredAbilitiesResponseSchema: z.ZodObject<{
|
|
|
77
77
|
source: z.ZodNullable<z.ZodEnum<{
|
|
78
78
|
admin: "admin";
|
|
79
79
|
collection: "collection";
|
|
80
|
-
plugin: "plugin";
|
|
81
80
|
core: "core";
|
|
81
|
+
plugin: "plugin";
|
|
82
82
|
}>>;
|
|
83
83
|
}, z.core.$strip>>;
|
|
84
84
|
}, z.core.$strip>>;
|
|
@@ -18,10 +18,10 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
18
18
|
* keys there.
|
|
19
19
|
*/
|
|
20
20
|
export declare const ADMIN_ROLES_ABILITIES: {
|
|
21
|
-
readonly read:
|
|
22
|
-
readonly create:
|
|
23
|
-
readonly update:
|
|
24
|
-
readonly delete:
|
|
21
|
+
readonly read: 'admin.roles.read';
|
|
22
|
+
readonly create: 'admin.roles.create';
|
|
23
|
+
readonly update: 'admin.roles.update';
|
|
24
|
+
readonly delete: 'admin.roles.delete';
|
|
25
25
|
};
|
|
26
26
|
export type AdminRolesAbilityKey = (typeof ADMIN_ROLES_ABILITIES)[keyof typeof ADMIN_ROLES_ABILITIES];
|
|
27
27
|
/**
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* ability keys in logs and admin UI messages.
|
|
14
14
|
*/
|
|
15
15
|
export declare const AdminRolesErrorCodes: {
|
|
16
|
-
readonly NOT_FOUND:
|
|
17
|
-
readonly MACHINE_NAME_IN_USE:
|
|
18
|
-
readonly VERSION_CONFLICT:
|
|
19
|
-
readonly USER_NOT_FOUND:
|
|
16
|
+
readonly NOT_FOUND: 'admin.roles.notFound';
|
|
17
|
+
readonly MACHINE_NAME_IN_USE: 'admin.roles.machineNameInUse';
|
|
18
|
+
readonly VERSION_CONFLICT: 'admin.roles.versionConflict';
|
|
19
|
+
readonly USER_NOT_FOUND: 'admin.roles.userNotFound';
|
|
20
20
|
};
|
|
21
21
|
export type AdminRolesErrorCode = (typeof AdminRolesErrorCodes)[keyof typeof AdminRolesErrorCodes];
|
|
22
22
|
export interface AdminRolesErrorOptions {
|
|
@@ -25,11 +25,11 @@ import type { AbilityRegistry } from '@byline/auth';
|
|
|
25
25
|
* are strictly for administering other admin users.
|
|
26
26
|
*/
|
|
27
27
|
export declare const ADMIN_USERS_ABILITIES: {
|
|
28
|
-
readonly read:
|
|
29
|
-
readonly create:
|
|
30
|
-
readonly update:
|
|
31
|
-
readonly delete:
|
|
32
|
-
readonly changePassword:
|
|
28
|
+
readonly read: 'admin.users.read';
|
|
29
|
+
readonly create: 'admin.users.create';
|
|
30
|
+
readonly update: 'admin.users.update';
|
|
31
|
+
readonly delete: 'admin.users.delete';
|
|
32
|
+
readonly changePassword: 'admin.users.changePassword';
|
|
33
33
|
};
|
|
34
34
|
export type AdminUsersAbilityKey = (typeof ADMIN_USERS_ABILITIES)[keyof typeof ADMIN_USERS_ABILITIES];
|
|
35
35
|
/**
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
* alongside the matching ability keys in logs and admin UI messages.
|
|
19
19
|
*/
|
|
20
20
|
export declare const AdminUsersErrorCodes: {
|
|
21
|
-
readonly NOT_FOUND:
|
|
22
|
-
readonly EMAIL_IN_USE:
|
|
23
|
-
readonly SELF_DELETE_FORBIDDEN:
|
|
24
|
-
readonly SELF_DISABLE_FORBIDDEN:
|
|
25
|
-
readonly VERSION_CONFLICT:
|
|
21
|
+
readonly NOT_FOUND: 'admin.users.notFound';
|
|
22
|
+
readonly EMAIL_IN_USE: 'admin.users.emailInUse';
|
|
23
|
+
readonly SELF_DELETE_FORBIDDEN: 'admin.users.selfDeleteForbidden';
|
|
24
|
+
readonly SELF_DISABLE_FORBIDDEN: 'admin.users.selfDisableForbidden';
|
|
25
|
+
readonly VERSION_CONFLICT: 'admin.users.versionConflict';
|
|
26
26
|
};
|
|
27
27
|
export type AdminUsersErrorCode = (typeof AdminUsersErrorCodes)[keyof typeof AdminUsersErrorCodes];
|
|
28
28
|
export interface AdminUsersErrorOptions {
|
|
@@ -11,12 +11,12 @@ export declare const listAdminUsersRequestSchema: z.ZodObject<{
|
|
|
11
11
|
pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
12
12
|
query: z.ZodOptional<z.ZodString>;
|
|
13
13
|
order: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
14
|
+
created_at: "created_at";
|
|
14
15
|
email: "email";
|
|
15
|
-
given_name: "given_name";
|
|
16
16
|
family_name: "family_name";
|
|
17
|
-
|
|
18
|
-
created_at: "created_at";
|
|
17
|
+
given_name: "given_name";
|
|
19
18
|
updated_at: "updated_at";
|
|
19
|
+
username: "username";
|
|
20
20
|
}>>>;
|
|
21
21
|
desc: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
22
22
|
}, z.core.$strip>;
|
|
@@ -121,12 +121,12 @@ export declare const adminUserListResponseSchema: z.ZodObject<{
|
|
|
121
121
|
page_size: z.ZodNumber;
|
|
122
122
|
query: z.ZodString;
|
|
123
123
|
order: z.ZodEnum<{
|
|
124
|
+
created_at: "created_at";
|
|
124
125
|
email: "email";
|
|
125
|
-
given_name: "given_name";
|
|
126
126
|
family_name: "family_name";
|
|
127
|
-
|
|
128
|
-
created_at: "created_at";
|
|
127
|
+
given_name: "given_name";
|
|
129
128
|
updated_at: "updated_at";
|
|
129
|
+
username: "username";
|
|
130
130
|
}>;
|
|
131
131
|
desc: z.ZodBoolean;
|
|
132
132
|
}, z.core.$strip>;
|