@byline/ui 2.5.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. package/dist/dnd/draggable-sortable/demo/draggable-list-demo.js +1 -1
  2. package/dist/react.d.ts +18 -54
  3. package/dist/react.js +0 -35
  4. package/dist/uikit.d.ts +1 -0
  5. package/dist/uikit.js +1 -0
  6. package/package.json +2 -8
  7. package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +1 -1
  8. package/src/react.ts +20 -68
  9. package/src/uikit.ts +1 -0
  10. package/dist/admin/group.d.ts +0 -27
  11. package/dist/admin/group.js +0 -14
  12. package/dist/admin/group.module.js +0 -6
  13. package/dist/admin/group_module.css +0 -19
  14. package/dist/admin/row.d.ts +0 -25
  15. package/dist/admin/row.js +0 -8
  16. package/dist/admin/row.module.js +0 -5
  17. package/dist/admin/row_module.css +0 -18
  18. package/dist/admin/tabs.d.ts +0 -25
  19. package/dist/admin/tabs.js +0 -35
  20. package/dist/admin/tabs.module.js +0 -10
  21. package/dist/admin/tabs_module.css +0 -68
  22. package/dist/fields/array/array-field.d.ts +0 -14
  23. package/dist/fields/array/array-field.js +0 -176
  24. package/dist/fields/array/array-field.module.js +0 -11
  25. package/dist/fields/array/array-field_module.css +0 -32
  26. package/dist/fields/blocks/blocks-field.d.ts +0 -13
  27. package/dist/fields/blocks/blocks-field.js +0 -244
  28. package/dist/fields/blocks/blocks-field.module.js +0 -26
  29. package/dist/fields/blocks/blocks-field_module.css +0 -107
  30. package/dist/fields/checkbox/checkbox-field.d.ts +0 -16
  31. package/dist/fields/checkbox/checkbox-field.js +0 -28
  32. package/dist/fields/checkbox/checkbox-field.module.js +0 -6
  33. package/dist/fields/checkbox/checkbox-field_module.css +0 -4
  34. package/dist/fields/column-formatter.d.ts +0 -20
  35. package/dist/fields/column-formatter.js +0 -15
  36. package/dist/fields/date-time-formatter.d.ts +0 -16
  37. package/dist/fields/date-time-formatter.js +0 -8
  38. package/dist/fields/datetime/datetime-field.d.ts +0 -16
  39. package/dist/fields/datetime/datetime-field.js +0 -37
  40. package/dist/fields/datetime/datetime-field.module.js +0 -5
  41. package/dist/fields/datetime/datetime-field_module.css +0 -4
  42. package/dist/fields/draggable-context-menu.d.ts +0 -6
  43. package/dist/fields/draggable-context-menu.js +0 -83
  44. package/dist/fields/draggable-context-menu.module.js +0 -15
  45. package/dist/fields/draggable-context-menu_module.css +0 -91
  46. package/dist/fields/field-helpers.d.ts +0 -26
  47. package/dist/fields/field-helpers.js +0 -50
  48. package/dist/fields/field-renderer.d.ts +0 -37
  49. package/dist/fields/field-renderer.js +0 -206
  50. package/dist/fields/field-renderer.module.js +0 -8
  51. package/dist/fields/field-renderer_module.css +0 -11
  52. package/dist/fields/file/file-field.d.ts +0 -19
  53. package/dist/fields/file/file-field.js +0 -226
  54. package/dist/fields/file/file-field.module.js +0 -18
  55. package/dist/fields/file/file-field_module.css +0 -131
  56. package/dist/fields/file/file-upload-field.d.ts +0 -21
  57. package/dist/fields/file/file-upload-field.js +0 -128
  58. package/dist/fields/file/file-upload-field.module.js +0 -15
  59. package/dist/fields/file/file-upload-field_module.css +0 -74
  60. package/dist/fields/group/group-field.d.ts +0 -15
  61. package/dist/fields/group/group-field.js +0 -59
  62. package/dist/fields/group/group-field.module.js +0 -9
  63. package/dist/fields/group/group-field_module.css +0 -27
  64. package/dist/fields/image/image-field.d.ts +0 -19
  65. package/dist/fields/image/image-field.js +0 -242
  66. package/dist/fields/image/image-field.module.js +0 -22
  67. package/dist/fields/image/image-field_module.css +0 -121
  68. package/dist/fields/image/image-upload-field.d.ts +0 -21
  69. package/dist/fields/image/image-upload-field.js +0 -187
  70. package/dist/fields/image/image-upload-field.module.js +0 -19
  71. package/dist/fields/image/image-upload-field_module.css +0 -92
  72. package/dist/fields/local-date-time.d.ts +0 -27
  73. package/dist/fields/local-date-time.js +0 -49
  74. package/dist/fields/locale-badge.d.ts +0 -18
  75. package/dist/fields/locale-badge.js +0 -10
  76. package/dist/fields/locale-badge.module.js +0 -5
  77. package/dist/fields/locale-badge_module.css +0 -27
  78. package/dist/fields/numerical/numerical-field.d.ts +0 -18
  79. package/dist/fields/numerical/numerical-field.js +0 -74
  80. package/dist/fields/relation/relation-display.d.ts +0 -40
  81. package/dist/fields/relation/relation-display.js +0 -58
  82. package/dist/fields/relation/relation-display.module.js +0 -9
  83. package/dist/fields/relation/relation-display_module.css +0 -21
  84. package/dist/fields/relation/relation-field.d.ts +0 -18
  85. package/dist/fields/relation/relation-field.js +0 -146
  86. package/dist/fields/relation/relation-field.module.js +0 -13
  87. package/dist/fields/relation/relation-field_module.css +0 -62
  88. package/dist/fields/relation/relation-picker.d.ts +0 -49
  89. package/dist/fields/relation/relation-picker.js +0 -233
  90. package/dist/fields/relation/relation-picker.module.js +0 -26
  91. package/dist/fields/relation/relation-picker_module.css +0 -124
  92. package/dist/fields/relation/relation-summary.d.ts +0 -31
  93. package/dist/fields/relation/relation-summary.js +0 -50
  94. package/dist/fields/relation/relation-summary.module.js +0 -11
  95. package/dist/fields/relation/relation-summary_module.css +0 -37
  96. package/dist/fields/select/select-field.d.ts +0 -16
  97. package/dist/fields/select/select-field.js +0 -50
  98. package/dist/fields/select/select-field.module.js +0 -5
  99. package/dist/fields/select/select-field_module.css +0 -4
  100. package/dist/fields/sortable-item.d.ts +0 -15
  101. package/dist/fields/sortable-item.js +0 -80
  102. package/dist/fields/sortable-item.module.js +0 -22
  103. package/dist/fields/sortable-item_module.css +0 -124
  104. package/dist/fields/text/text-field.d.ts +0 -20
  105. package/dist/fields/text/text-field.js +0 -104
  106. package/dist/fields/text/text-field.module.js +0 -6
  107. package/dist/fields/text/text-field_module.css +0 -5
  108. package/dist/fields/text-area/text-area-field.d.ts +0 -20
  109. package/dist/fields/text-area/text-area-field.js +0 -105
  110. package/dist/fields/text-area/text-area-field.module.js +0 -6
  111. package/dist/fields/text-area/text-area-field_module.css +0 -5
  112. package/dist/fields/use-field-change-handler.d.ts +0 -23
  113. package/dist/fields/use-field-change-handler.js +0 -52
  114. package/dist/forms/document-actions.d.ts +0 -48
  115. package/dist/forms/document-actions.js +0 -469
  116. package/dist/forms/document-actions.module.js +0 -34
  117. package/dist/forms/document-actions_module.css +0 -118
  118. package/dist/forms/form-context.d.ts +0 -89
  119. package/dist/forms/form-context.js +0 -466
  120. package/dist/forms/form-renderer.d.ts +0 -98
  121. package/dist/forms/form-renderer.js +0 -591
  122. package/dist/forms/form-renderer.module.js +0 -46
  123. package/dist/forms/form-renderer_module.css +0 -245
  124. package/dist/forms/navigation-guard.d.ts +0 -54
  125. package/dist/forms/navigation-guard.js +0 -22
  126. package/dist/forms/path-widget.d.ts +0 -36
  127. package/dist/forms/path-widget.js +0 -107
  128. package/dist/forms/path-widget.module.js +0 -8
  129. package/dist/forms/path-widget_module.css +0 -29
  130. package/dist/forms/upload-executor.d.ts +0 -57
  131. package/dist/forms/upload-executor.js +0 -92
  132. package/dist/services/field-services-context.d.ts +0 -16
  133. package/dist/services/field-services-context.js +0 -13
  134. package/dist/services/field-services-types.d.ts +0 -63
  135. package/dist/services/field-services-types.js +0 -1
  136. package/dist/widgets/diff-viewer/diff-modal.d.ts +0 -22
  137. package/dist/widgets/diff-viewer/diff-modal.js +0 -146
  138. package/dist/widgets/diff-viewer/diff-modal.module.js +0 -14
  139. package/dist/widgets/diff-viewer/diff-modal_module.css +0 -56
  140. package/dist/widgets/status-badge/status-badge.d.ts +0 -25
  141. package/dist/widgets/status-badge/status-badge.js +0 -35
  142. package/dist/widgets/status-badge/status-badge.module.js +0 -7
  143. package/dist/widgets/status-badge/status-badge_module.css +0 -20
  144. package/src/admin/group.module.css +0 -41
  145. package/src/admin/group.tsx +0 -40
  146. package/src/admin/row.module.css +0 -32
  147. package/src/admin/row.tsx +0 -33
  148. package/src/admin/tabs.module.css +0 -107
  149. package/src/admin/tabs.tsx +0 -82
  150. package/src/fields/array/array-field.module.css +0 -48
  151. package/src/fields/array/array-field.tsx +0 -266
  152. package/src/fields/blocks/blocks-field.module.css +0 -148
  153. package/src/fields/blocks/blocks-field.tsx +0 -312
  154. package/src/fields/checkbox/checkbox-field.module.css +0 -4
  155. package/src/fields/checkbox/checkbox-field.tsx +0 -54
  156. package/src/fields/column-formatter.tsx +0 -31
  157. package/src/fields/date-time-formatter.tsx +0 -22
  158. package/src/fields/datetime/datetime-field.module.css +0 -13
  159. package/src/fields/datetime/datetime-field.tsx +0 -54
  160. package/src/fields/draggable-context-menu.module.css +0 -127
  161. package/src/fields/draggable-context-menu.tsx +0 -85
  162. package/src/fields/field-helpers.ts +0 -69
  163. package/src/fields/field-renderer.module.css +0 -22
  164. package/src/fields/field-renderer.tsx +0 -288
  165. package/src/fields/file/file-field.module.css +0 -153
  166. package/src/fields/file/file-field.tsx +0 -271
  167. package/src/fields/file/file-upload-field.module.css +0 -101
  168. package/src/fields/file/file-upload-field.tsx +0 -183
  169. package/src/fields/group/group-field.module.css +0 -43
  170. package/src/fields/group/group-field.tsx +0 -84
  171. package/src/fields/image/image-field.module.css +0 -155
  172. package/src/fields/image/image-field.tsx +0 -291
  173. package/src/fields/image/image-upload-field.module.css +0 -123
  174. package/src/fields/image/image-upload-field.tsx +0 -270
  175. package/src/fields/local-date-time.tsx +0 -88
  176. package/src/fields/locale-badge.module.css +0 -37
  177. package/src/fields/locale-badge.tsx +0 -32
  178. package/src/fields/numerical/numerical-field.tsx +0 -114
  179. package/src/fields/relation/relation-display.module.css +0 -36
  180. package/src/fields/relation/relation-display.tsx +0 -130
  181. package/src/fields/relation/relation-field.module.css +0 -83
  182. package/src/fields/relation/relation-field.tsx +0 -206
  183. package/src/fields/relation/relation-picker.module.css +0 -168
  184. package/src/fields/relation/relation-picker.tsx +0 -325
  185. package/src/fields/relation/relation-summary.module.css +0 -55
  186. package/src/fields/relation/relation-summary.tsx +0 -123
  187. package/src/fields/select/select-field.module.css +0 -13
  188. package/src/fields/select/select-field.tsx +0 -61
  189. package/src/fields/sortable-item.module.css +0 -167
  190. package/src/fields/sortable-item.tsx +0 -101
  191. package/src/fields/text/text-field.module.css +0 -13
  192. package/src/fields/text/text-field.tsx +0 -146
  193. package/src/fields/text-area/text-area-field.module.css +0 -13
  194. package/src/fields/text-area/text-area-field.tsx +0 -147
  195. package/src/fields/use-field-change-handler.ts +0 -112
  196. package/src/forms/document-actions.module.css +0 -160
  197. package/src/forms/document-actions.tsx +0 -487
  198. package/src/forms/form-context.tsx +0 -704
  199. package/src/forms/form-renderer.module.css +0 -321
  200. package/src/forms/form-renderer.tsx +0 -888
  201. package/src/forms/navigation-guard.tsx +0 -98
  202. package/src/forms/path-widget.module.css +0 -41
  203. package/src/forms/path-widget.test.tsx +0 -217
  204. package/src/forms/path-widget.tsx +0 -181
  205. package/src/forms/upload-executor.ts +0 -190
  206. package/src/services/field-services-context.tsx +0 -35
  207. package/src/services/field-services-types.ts +0 -68
  208. package/src/widgets/diff-viewer/diff-modal.module.css +0 -79
  209. package/src/widgets/diff-viewer/diff-modal.tsx +0 -184
  210. package/src/widgets/status-badge/status-badge.module.css +0 -31
  211. package/src/widgets/status-badge/status-badge.tsx +0 -69
@@ -1,89 +0,0 @@
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
- import type React from 'react';
9
- import type { Field } from '@byline/core';
10
- import type { DocumentPatch } from '@byline/core/patches';
11
- interface FormError {
12
- field: string;
13
- message: string;
14
- }
15
- /**
16
- * Represents a file that has been selected but not yet uploaded.
17
- * The file is held locally until form submission.
18
- */
19
- export interface PendingUpload {
20
- /** The actual File object to upload */
21
- file: File;
22
- /** Blob URL for local preview (must be revoked on cleanup) */
23
- previewUrl: string;
24
- /** The collection path for the upload endpoint */
25
- collectionPath: string;
26
- }
27
- type FieldListener = (value: any) => void;
28
- type ErrorsListener = (errors: FormError[]) => void;
29
- type MetaListener = () => void;
30
- type SystemPathListener = (value: string | null) => void;
31
- type FieldUploadingListener = (uploading: boolean) => void;
32
- interface FormContextType {
33
- setFieldValue: (name: string, value: any) => void;
34
- setFieldStore: (name: string, value: any) => void;
35
- getFieldValue: (name: string) => any;
36
- getFieldValues: () => Record<string, any>;
37
- getPatches: () => DocumentPatch[];
38
- appendPatch: (patch: DocumentPatch) => void;
39
- resetPatches: () => void;
40
- hasChanges: () => boolean;
41
- resetHasChanges: () => void;
42
- runFieldHooks: (fields: Field[]) => Promise<FormError[]>;
43
- validateForm: (fields: Field[]) => FormError[];
44
- errors: FormError[];
45
- getErrors: () => FormError[];
46
- clearErrors: () => void;
47
- setFieldError: (field: string, message: string) => void;
48
- clearFieldError: (field: string) => void;
49
- isDirty: (fieldName: string) => boolean;
50
- subscribeField: (name: string, listener: FieldListener) => () => void;
51
- subscribeErrors: (listener: ErrorsListener) => () => void;
52
- subscribeMeta: (listener: MetaListener) => () => void;
53
- addPendingUpload: (fieldPath: string, upload: PendingUpload) => void;
54
- removePendingUpload: (fieldPath: string) => void;
55
- getPendingUploads: () => Map<string, PendingUpload>;
56
- hasPendingUploads: () => boolean;
57
- clearPendingUploads: () => void;
58
- setFieldUploading: (fieldPath: string, uploading: boolean) => void;
59
- getIsFieldUploading: (fieldPath: string) => boolean;
60
- subscribeFieldUploading: (fieldPath: string, listener: FieldUploadingListener) => () => void;
61
- getSystemPath: () => string | null;
62
- setSystemPath: (value: string | null) => void;
63
- subscribeSystemPath: (listener: SystemPathListener) => () => void;
64
- }
65
- export declare const useFormContext: () => FormContextType;
66
- export declare const FormProvider: ({ children, initialData, }: {
67
- children: React.ReactNode;
68
- initialData?: Record<string, any>;
69
- }) => React.JSX.Element;
70
- /**
71
- * Subscribe to the system `path` slot edited by the path widget.
72
- * Returns the current value (or `null` when no override is set).
73
- */
74
- export declare const useSystemPath: () => string | null;
75
- export declare const useFormStore: () => FormContextType;
76
- export declare const useFieldError: (name: string) => string | undefined;
77
- export declare const useFormMeta: () => {
78
- hasChanges: boolean;
79
- };
80
- export declare const useIsDirty: (name: string) => boolean;
81
- export declare const useFieldValue: <T = any>(name: string) => T | undefined;
82
- /**
83
- * Subscribe to a single field's upload-in-flight state. Returns `true` while
84
- * the form orchestrator is actively transporting this field's pending upload
85
- * (between the `setFieldUploading(path, true)` and the matching `false`
86
- * emitted by the upload executor's progress callback).
87
- */
88
- export declare const useIsFieldUploading: (fieldPath: string) => boolean;
89
- export {};
@@ -1,466 +0,0 @@
1
- "use client";
2
- import { jsx } from "react/jsx-runtime";
3
- import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
4
- import { normalizeHooks } from "@byline/core";
5
- import { get, set as external_lodash_es_set } from "lodash-es";
6
- const FormContext = /*#__PURE__*/ createContext(null);
7
- const useFormContext = ()=>{
8
- const context = useContext(FormContext);
9
- if (null == context) throw new Error('useFormContext must be used within a FormProvider');
10
- return context;
11
- };
12
- const FormProvider = ({ children, initialData = {} })=>{
13
- const fieldValues = useRef(JSON.parse(JSON.stringify(initialData?.fields ?? initialData)));
14
- const initialValues = useRef(initialData?.fields ?? initialData);
15
- const errorsRef = useRef([]);
16
- const dirtyFields = useRef(new Set());
17
- const patchesRef = useRef([]);
18
- const pendingUploadsRef = useRef(new Map());
19
- const uploadingFieldsRef = useRef(new Set());
20
- const uploadingListenersRef = useRef(new Map());
21
- const fieldListeners = useRef(new Map());
22
- const errorListeners = useRef(new Set());
23
- const metaListeners = useRef(new Set());
24
- const systemPathRef = useRef('string' == typeof initialData?.path && initialData.path.length > 0 ? initialData.path : null);
25
- const initialSystemPath = useRef(systemPathRef.current);
26
- const systemPathListeners = useRef(new Set());
27
- const subscribeField = useCallback((name, listener)=>{
28
- if (!fieldListeners.current.has(name)) fieldListeners.current.set(name, new Set());
29
- fieldListeners.current.get(name)?.add(listener);
30
- return ()=>{
31
- const listeners = fieldListeners.current.get(name);
32
- if (listeners) {
33
- listeners.delete(listener);
34
- if (0 === listeners.size) fieldListeners.current.delete(name);
35
- }
36
- };
37
- }, []);
38
- const subscribeErrors = useCallback((listener)=>{
39
- errorListeners.current.add(listener);
40
- return ()=>{
41
- errorListeners.current.delete(listener);
42
- };
43
- }, []);
44
- const subscribeMeta = useCallback((listener)=>{
45
- metaListeners.current.add(listener);
46
- return ()=>{
47
- metaListeners.current.delete(listener);
48
- };
49
- }, []);
50
- const notifyFieldListeners = useCallback((name, value)=>{
51
- const listeners = fieldListeners.current.get(name);
52
- if (listeners) listeners.forEach((listener)=>{
53
- listener(value);
54
- });
55
- }, []);
56
- const notifyErrorListeners = useCallback(()=>{
57
- errorListeners.current.forEach((listener)=>{
58
- listener(errorsRef.current);
59
- });
60
- }, []);
61
- const notifyMetaListeners = useCallback(()=>{
62
- metaListeners.current.forEach((listener)=>{
63
- listener();
64
- });
65
- }, []);
66
- const updateFieldStoreInternal = useCallback((name, value)=>{
67
- const newFieldValues = {
68
- ...fieldValues.current
69
- };
70
- external_lodash_es_set(newFieldValues, name, value);
71
- fieldValues.current = newFieldValues;
72
- dirtyFields.current.add(name);
73
- notifyFieldListeners(name, value);
74
- notifyMetaListeners();
75
- }, [
76
- notifyFieldListeners,
77
- notifyMetaListeners
78
- ]);
79
- const setFieldStore = useCallback((name, value)=>{
80
- updateFieldStoreInternal(name, value);
81
- }, [
82
- updateFieldStoreInternal
83
- ]);
84
- const setFieldValue = useCallback((name, value)=>{
85
- updateFieldStoreInternal(name, value);
86
- const patch = {
87
- kind: 'field.set',
88
- path: name,
89
- value
90
- };
91
- const lastPatch = patchesRef.current[patchesRef.current.length - 1];
92
- if (lastPatch && 'field.set' === lastPatch.kind && lastPatch.path === name) {
93
- const newPatches = [
94
- ...patchesRef.current
95
- ];
96
- newPatches[newPatches.length - 1] = patch;
97
- patchesRef.current = newPatches;
98
- } else patchesRef.current = [
99
- ...patchesRef.current,
100
- patch
101
- ];
102
- if (errorsRef.current.some((error)=>error.field === name)) {
103
- errorsRef.current = errorsRef.current.filter((error)=>error.field !== name);
104
- notifyErrorListeners();
105
- }
106
- }, [
107
- updateFieldStoreInternal,
108
- notifyErrorListeners
109
- ]);
110
- const getFieldValues = useCallback(()=>fieldValues.current, []);
111
- const getPatches = useCallback(()=>patchesRef.current, []);
112
- const appendPatch = useCallback((patch)=>{
113
- patchesRef.current = [
114
- ...patchesRef.current,
115
- patch
116
- ];
117
- dirtyFields.current.add('__patch__');
118
- notifyMetaListeners();
119
- if ('production' !== process.env.NODE_ENV) console.debug('FormContext.appendPatch', {
120
- patch,
121
- dirtyCount: dirtyFields.current.size
122
- });
123
- }, [
124
- notifyMetaListeners
125
- ]);
126
- const getFieldValue = useCallback((name)=>{
127
- const dirty = dirtyFields.current.has(name);
128
- const currentValue = get(fieldValues.current, name);
129
- if (void 0 !== currentValue) return currentValue;
130
- if (!dirty) return get(initialValues.current, name);
131
- }, []);
132
- const hasChanges = useCallback(()=>dirtyFields.current.size > 0, []);
133
- const resetHasChanges = useCallback(()=>{
134
- dirtyFields.current.clear();
135
- patchesRef.current = [];
136
- initialSystemPath.current = systemPathRef.current;
137
- notifyMetaListeners();
138
- }, [
139
- notifyMetaListeners
140
- ]);
141
- const isDirty = useCallback((fieldName)=>dirtyFields.current.has(fieldName), []);
142
- const getSystemPath = useCallback(()=>systemPathRef.current, []);
143
- const setSystemPath = useCallback((value)=>{
144
- systemPathRef.current = value;
145
- if (value !== initialSystemPath.current) dirtyFields.current.add('__systemPath__');
146
- else dirtyFields.current.delete('__systemPath__');
147
- systemPathListeners.current.forEach((listener)=>{
148
- listener(value);
149
- });
150
- notifyMetaListeners();
151
- }, [
152
- notifyMetaListeners
153
- ]);
154
- const subscribeSystemPath = useCallback((listener)=>{
155
- systemPathListeners.current.add(listener);
156
- return ()=>{
157
- systemPathListeners.current.delete(listener);
158
- };
159
- }, []);
160
- const addPendingUpload = useCallback((fieldPath, upload)=>{
161
- const existing = pendingUploadsRef.current.get(fieldPath);
162
- if (existing) URL.revokeObjectURL(existing.previewUrl);
163
- pendingUploadsRef.current.set(fieldPath, upload);
164
- dirtyFields.current.add(fieldPath);
165
- notifyMetaListeners();
166
- }, [
167
- notifyMetaListeners
168
- ]);
169
- const removePendingUpload = useCallback((fieldPath)=>{
170
- const existing = pendingUploadsRef.current.get(fieldPath);
171
- if (existing) {
172
- URL.revokeObjectURL(existing.previewUrl);
173
- pendingUploadsRef.current.delete(fieldPath);
174
- notifyMetaListeners();
175
- }
176
- }, [
177
- notifyMetaListeners
178
- ]);
179
- const getPendingUploads = useCallback(()=>new Map(pendingUploadsRef.current), []);
180
- const hasPendingUploads = useCallback(()=>pendingUploadsRef.current.size > 0, []);
181
- const clearPendingUploads = useCallback(()=>{
182
- for (const upload of pendingUploadsRef.current.values())URL.revokeObjectURL(upload.previewUrl);
183
- pendingUploadsRef.current.clear();
184
- }, []);
185
- const setFieldUploading = useCallback((fieldPath, uploading)=>{
186
- if (uploading) {
187
- if (uploadingFieldsRef.current.has(fieldPath)) return;
188
- uploadingFieldsRef.current.add(fieldPath);
189
- } else {
190
- if (!uploadingFieldsRef.current.has(fieldPath)) return;
191
- uploadingFieldsRef.current.delete(fieldPath);
192
- }
193
- uploadingListenersRef.current.get(fieldPath)?.forEach((listener)=>{
194
- listener(uploading);
195
- });
196
- }, []);
197
- const getIsFieldUploading = useCallback((fieldPath)=>uploadingFieldsRef.current.has(fieldPath), []);
198
- const subscribeFieldUploading = useCallback((fieldPath, listener)=>{
199
- let listeners = uploadingListenersRef.current.get(fieldPath);
200
- if (!listeners) {
201
- listeners = new Set();
202
- uploadingListenersRef.current.set(fieldPath, listeners);
203
- }
204
- listeners.add(listener);
205
- return ()=>{
206
- const set = uploadingListenersRef.current.get(fieldPath);
207
- if (set) {
208
- set.delete(listener);
209
- if (0 === set.size) uploadingListenersRef.current.delete(fieldPath);
210
- }
211
- };
212
- }, []);
213
- useEffect(()=>()=>{
214
- for (const upload of pendingUploadsRef.current.values())URL.revokeObjectURL(upload.previewUrl);
215
- }, []);
216
- const validateForm = useCallback((fields)=>{
217
- const formErrors = [];
218
- const data = getFieldValues();
219
- for (const field of fields){
220
- const value = getFieldValue(field.name);
221
- if (!field.optional && (null == value || '' === value)) formErrors.push({
222
- field: field.name,
223
- message: `${field.label} is required`
224
- });
225
- if (null != value && '' !== value) switch(field.type){
226
- case 'text':
227
- if ('string' != typeof value) formErrors.push({
228
- field: field.name,
229
- message: `${field.label} must be text`
230
- });
231
- break;
232
- case 'checkbox':
233
- if ('boolean' != typeof value) formErrors.push({
234
- field: field.name,
235
- message: `${field.label} must be true or false`
236
- });
237
- break;
238
- case 'select':
239
- if ('options' in field && field.options) {
240
- const validValues = field.options.map((opt)=>opt.value);
241
- if (!validValues.includes(value)) formErrors.push({
242
- field: field.name,
243
- message: `${field.label} must be one of: ${validValues.join(', ')}`
244
- });
245
- }
246
- break;
247
- case 'datetime':
248
- if (value instanceof Date === false && 'string' != typeof value) formErrors.push({
249
- field: field.name,
250
- message: `${field.label} must be a valid date`
251
- });
252
- break;
253
- }
254
- if (field.validate) {
255
- const error = field.validate(value, data);
256
- if (error) formErrors.push({
257
- field: field.name,
258
- message: error
259
- });
260
- }
261
- }
262
- errorsRef.current = formErrors;
263
- notifyErrorListeners();
264
- return formErrors;
265
- }, [
266
- getFieldValue,
267
- getFieldValues,
268
- notifyErrorListeners
269
- ]);
270
- const clearErrors = useCallback(()=>{
271
- errorsRef.current = [];
272
- notifyErrorListeners();
273
- }, [
274
- notifyErrorListeners
275
- ]);
276
- const setFieldError = useCallback((field, message)=>{
277
- const filtered = errorsRef.current.filter((e)=>e.field !== field);
278
- filtered.push({
279
- field,
280
- message
281
- });
282
- errorsRef.current = filtered;
283
- notifyErrorListeners();
284
- }, [
285
- notifyErrorListeners
286
- ]);
287
- const clearFieldError = useCallback((field)=>{
288
- if (errorsRef.current.some((e)=>e.field === field)) {
289
- errorsRef.current = errorsRef.current.filter((e)=>e.field !== field);
290
- notifyErrorListeners();
291
- }
292
- }, [
293
- notifyErrorListeners
294
- ]);
295
- const runFieldHooks = useCallback(async (fields)=>{
296
- const hookErrors = [];
297
- const data = {
298
- ...fieldValues.current
299
- };
300
- for (const field of fields){
301
- const fns = normalizeHooks(field.hooks?.beforeValidate);
302
- if (0 === fns.length) continue;
303
- const path = field.name;
304
- const value = getFieldValue(path);
305
- const ctx = {
306
- value,
307
- previousValue: value,
308
- data,
309
- path,
310
- field,
311
- operation: 'submit'
312
- };
313
- try {
314
- for (const fn of fns){
315
- const result = await fn(ctx);
316
- if (result?.error) hookErrors.push({
317
- field: path,
318
- message: result.error
319
- });
320
- if (result?.value !== void 0) {
321
- setFieldValue(path, result.value);
322
- ctx.value = result.value;
323
- data[path] = result.value;
324
- }
325
- }
326
- } catch (err) {
327
- const message = err instanceof Error ? err.message : 'Unexpected hook error';
328
- hookErrors.push({
329
- field: path,
330
- message
331
- });
332
- }
333
- }
334
- if (hookErrors.length > 0) {
335
- errorsRef.current = [
336
- ...errorsRef.current,
337
- ...hookErrors
338
- ];
339
- notifyErrorListeners();
340
- }
341
- return hookErrors;
342
- }, [
343
- getFieldValue,
344
- setFieldValue,
345
- notifyErrorListeners
346
- ]);
347
- return /*#__PURE__*/ jsx(FormContext.Provider, {
348
- value: {
349
- setFieldValue,
350
- setFieldStore,
351
- getFieldValue,
352
- getFieldValues,
353
- getPatches,
354
- appendPatch,
355
- resetPatches: ()=>{
356
- patchesRef.current = [];
357
- },
358
- hasChanges,
359
- resetHasChanges,
360
- runFieldHooks,
361
- validateForm,
362
- errors: errorsRef.current,
363
- getErrors: ()=>errorsRef.current,
364
- clearErrors,
365
- setFieldError,
366
- clearFieldError,
367
- isDirty,
368
- subscribeField,
369
- subscribeErrors,
370
- subscribeMeta,
371
- addPendingUpload,
372
- removePendingUpload,
373
- getPendingUploads,
374
- hasPendingUploads,
375
- clearPendingUploads,
376
- setFieldUploading,
377
- getIsFieldUploading,
378
- subscribeFieldUploading,
379
- getSystemPath,
380
- setSystemPath,
381
- subscribeSystemPath
382
- },
383
- children: children
384
- });
385
- };
386
- const useSystemPath = ()=>{
387
- const { getSystemPath, subscribeSystemPath } = useFormContext();
388
- const [value, setValue] = useState(()=>getSystemPath());
389
- useEffect(()=>subscribeSystemPath((next)=>setValue(next)), [
390
- subscribeSystemPath
391
- ]);
392
- return value;
393
- };
394
- const useFormStore = ()=>useFormContext();
395
- const useFieldError = (name)=>{
396
- const { getErrors, subscribeErrors } = useFormContext();
397
- const [error, setError] = useState(()=>getErrors().find((e)=>e.field === name)?.message);
398
- useEffect(()=>{
399
- const unsubscribe = subscribeErrors((currentErrors)=>{
400
- const fieldError = currentErrors.find((e)=>e.field === name);
401
- setError(fieldError?.message);
402
- });
403
- return unsubscribe;
404
- }, [
405
- subscribeErrors,
406
- name
407
- ]);
408
- return error;
409
- };
410
- const useFormMeta = ()=>{
411
- const { hasChanges, subscribeMeta } = useFormContext();
412
- const [hasChangesValue, setHasChangesValue] = useState(hasChanges());
413
- useEffect(()=>{
414
- const unsubscribe = subscribeMeta(()=>{
415
- setHasChangesValue(hasChanges());
416
- });
417
- return unsubscribe;
418
- }, [
419
- subscribeMeta,
420
- hasChanges
421
- ]);
422
- return {
423
- hasChanges: hasChangesValue
424
- };
425
- };
426
- const useIsDirty = (name)=>{
427
- const { isDirty, subscribeMeta } = useFormContext();
428
- const [dirty, setDirty] = useState(isDirty(name));
429
- useEffect(()=>{
430
- const unsubscribe = subscribeMeta(()=>{
431
- setDirty(isDirty(name));
432
- });
433
- return unsubscribe;
434
- }, [
435
- subscribeMeta,
436
- isDirty,
437
- name
438
- ]);
439
- return dirty;
440
- };
441
- const useFieldValue = (name)=>{
442
- const { getFieldValue, subscribeField } = useFormContext();
443
- const [value, setValue] = useState(()=>getFieldValue(name));
444
- useEffect(()=>{
445
- const unsubscribe = subscribeField(name, (nextValue)=>{
446
- setValue(nextValue);
447
- });
448
- return unsubscribe;
449
- }, [
450
- subscribeField,
451
- name
452
- ]);
453
- return value;
454
- };
455
- const useIsFieldUploading = (fieldPath)=>{
456
- const { getIsFieldUploading, subscribeFieldUploading } = useFormContext();
457
- const [uploading, setUploading] = useState(()=>getIsFieldUploading(fieldPath));
458
- useEffect(()=>subscribeFieldUploading(fieldPath, (next)=>{
459
- setUploading(next);
460
- }), [
461
- subscribeFieldUploading,
462
- fieldPath
463
- ]);
464
- return uploading;
465
- };
466
- export { FormProvider, useFieldError, useFieldValue, useFormContext, useFormMeta, useFormStore, useIsDirty, useIsFieldUploading, useSystemPath };
@@ -1,98 +0,0 @@
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
- import { type ReactNode } from 'react';
9
- import type { CollectionAdminConfig, Field, WorkflowStatus } from '@byline/core';
10
- import { type DocumentActionsLocaleOption } from './document-actions';
11
- import type { UseNavigationGuard } from './navigation-guard';
12
- /** Metadata about a previously published version that is still live. */
13
- export interface PublishedVersionInfo {
14
- id: string;
15
- versionId: string;
16
- status: string;
17
- createdAt: string | Date;
18
- updatedAt: string | Date;
19
- }
20
- /** Props shared by both the public FormRenderer and its internal FormContent component. */
21
- export interface FormRendererProps {
22
- mode: 'create' | 'edit';
23
- fields: Field[];
24
- onSubmit: (data: any) => void;
25
- onCancel: () => void;
26
- onStatusChange?: (nextStatus: string) => Promise<void>;
27
- onUnpublish?: () => Promise<void>;
28
- onDelete?: () => Promise<void>;
29
- /**
30
- * Called when the editor confirms the duplicate modal in
31
- * `DocumentActions`. Edit views provide a handler that invokes the
32
- * `duplicateCollectionDocument` server fn and navigates to the new doc.
33
- * When omitted, the Duplicate menu item is hidden.
34
- */
35
- onDuplicate?: () => Promise<void>;
36
- /**
37
- * Called when the editor confirms the Copy-to-Locale modal in
38
- * `DocumentActions`. Edit views provide a handler that invokes the
39
- * `copyDocumentToLocale` server fn and navigates to the target-locale
40
- * view. When omitted (or when fewer than two `contentLocales` are
41
- * configured), the Copy-to-Locale menu item is hidden.
42
- */
43
- onCopyToLocale?: (args: {
44
- targetLocale: string;
45
- overwrite: boolean;
46
- }) => Promise<void>;
47
- /**
48
- * All configured content locales (code + display label) — required for
49
- * the Copy-to-Locale modal's target Select. Threaded as an opaque list
50
- * through to `DocumentActions`.
51
- */
52
- contentLocales?: ReadonlyArray<DocumentActionsLocaleOption>;
53
- nextStatus?: WorkflowStatus;
54
- workflowStatuses?: WorkflowStatus[];
55
- publishedVersion?: PublishedVersionInfo | null;
56
- initialData?: Record<string, any>;
57
- adminConfig?: CollectionAdminConfig;
58
- /**
59
- * Name of the schema field to render as the live form heading.
60
- * Sourced from `CollectionDefinition.useAsTitle` by the caller.
61
- */
62
- useAsTitle?: string;
63
- /**
64
- * Name of the schema field that initialises the system path.
65
- * Sourced from `CollectionDefinition.useAsPath` by the caller. When
66
- * present the path widget renders in the sidebar.
67
- */
68
- useAsPath?: string;
69
- headingLabel?: string;
70
- headerSlot?: ReactNode;
71
- /** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
72
- collectionPath?: string;
73
- /** The active content locale — initialised from the route query string. */
74
- initialLocale?: string;
75
- /** Called when the user picks a different content locale. */
76
- onLocaleChange?: (locale: string) => void;
77
- /**
78
- * Schema-mismatch warnings produced by a "best-effort" reconstruction
79
- * of the document (`findById({ lenient: true })`). When present, the
80
- * form renders an inline Alert telling the editor that fields from a
81
- * previous schema have been dropped — saving the form will overwrite
82
- * them with the new shape.
83
- */
84
- restoreWarnings?: string[];
85
- /**
86
- * Default content locale used when no `initialLocale` is supplied and as the
87
- * fallback inside `PathWidget`. Hosts typically pass their app-wide
88
- * `i18n.content.defaultLocale`. Defaults to `'en'`.
89
- */
90
- defaultLocale?: string;
91
- /**
92
- * Framework-specific navigation guard hook.
93
- * When provided, this overrides the adapter from `NavigationGuardProvider` context.
94
- * If neither is set, a no-op `beforeunload`-only guard is used.
95
- */
96
- useNavigationGuard?: UseNavigationGuard;
97
- }
98
- export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;