@byline/ui 0.9.3
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/LICENSE +373 -0
- package/README.md +17 -0
- package/dist/admin/components/admin-account/change-password.d.ts +9 -0
- package/dist/admin/components/admin-account/change-password.d.ts.map +1 -0
- package/dist/admin/components/admin-account/change-password.js +192 -0
- package/dist/admin/components/admin-account/change-password.module.js +8 -0
- package/dist/admin/components/admin-account/change-password_module.css +27 -0
- package/dist/admin/components/admin-account/container.d.ts +30 -0
- package/dist/admin/components/admin-account/container.d.ts.map +1 -0
- package/dist/admin/components/admin-account/container.js +299 -0
- package/dist/admin/components/admin-account/container.module.js +28 -0
- package/dist/admin/components/admin-account/container_module.css +106 -0
- package/dist/admin/components/admin-account/update.d.ts +9 -0
- package/dist/admin/components/admin-account/update.d.ts.map +1 -0
- package/dist/admin/components/admin-account/update.js +207 -0
- package/dist/admin/components/admin-account/update.module.js +8 -0
- package/dist/admin/components/admin-account/update_module.css +27 -0
- package/dist/admin/components/admin-permissions/inspector.d.ts +5 -0
- package/dist/admin/components/admin-permissions/inspector.d.ts.map +1 -0
- package/dist/admin/components/admin-permissions/inspector.js +284 -0
- package/dist/admin/components/admin-permissions/inspector.module.js +56 -0
- package/dist/admin/components/admin-permissions/inspector_module.css +238 -0
- package/dist/admin/components/admin-roles/create.d.ts +8 -0
- package/dist/admin/components/admin-roles/create.d.ts.map +1 -0
- package/dist/admin/components/admin-roles/create.js +177 -0
- package/dist/admin/components/admin-roles/create.module.js +8 -0
- package/dist/admin/components/admin-roles/create_module.css +27 -0
- package/dist/admin/components/admin-roles/permissions.d.ts +11 -0
- package/dist/admin/components/admin-roles/permissions.d.ts.map +1 -0
- package/dist/admin/components/admin-roles/permissions.js +303 -0
- package/dist/admin/components/admin-roles/permissions.module.js +44 -0
- package/dist/admin/components/admin-roles/permissions_module.css +192 -0
- package/dist/admin/components/admin-roles/update.d.ts +9 -0
- package/dist/admin/components/admin-roles/update.d.ts.map +1 -0
- package/dist/admin/components/admin-roles/update.js +166 -0
- package/dist/admin/components/admin-roles/update.module.js +8 -0
- package/dist/admin/components/admin-roles/update_module.css +27 -0
- package/dist/admin/components/admin-users/create.d.ts +9 -0
- package/dist/admin/components/admin-users/create.d.ts.map +1 -0
- package/dist/admin/components/admin-users/create.js +268 -0
- package/dist/admin/components/admin-users/create.module.js +10 -0
- package/dist/admin/components/admin-users/create_module.css +45 -0
- package/dist/admin/components/admin-users/roles.d.ts +12 -0
- package/dist/admin/components/admin-users/roles.d.ts.map +1 -0
- package/dist/admin/components/admin-users/roles.js +148 -0
- package/dist/admin/components/admin-users/roles.module.js +18 -0
- package/dist/admin/components/admin-users/roles_module.css +75 -0
- package/dist/admin/components/admin-users/set-password.d.ts +9 -0
- package/dist/admin/components/admin-users/set-password.d.ts.map +1 -0
- package/dist/admin/components/admin-users/set-password.js +170 -0
- package/dist/admin/components/admin-users/set-password.module.js +9 -0
- package/dist/admin/components/admin-users/set-password_module.css +31 -0
- package/dist/admin/components/admin-users/update.d.ts +9 -0
- package/dist/admin/components/admin-users/update.d.ts.map +1 -0
- package/dist/admin/components/admin-users/update.js +254 -0
- package/dist/admin/components/admin-users/update.module.js +9 -0
- package/dist/admin/components/admin-users/update_module.css +34 -0
- package/dist/admin/components/auth/sign-in-form.d.ts +14 -0
- package/dist/admin/components/auth/sign-in-form.d.ts.map +1 -0
- package/dist/admin/components/auth/sign-in-form.js +107 -0
- package/dist/admin/components/auth/sign-in-form.module.js +10 -0
- package/dist/admin/components/auth/sign-in-form_module.css +35 -0
- package/dist/admin/components/collections/diff-modal.d.ts +23 -0
- package/dist/admin/components/collections/diff-modal.d.ts.map +1 -0
- package/dist/admin/components/collections/diff-modal.js +147 -0
- package/dist/admin/components/collections/diff-modal.module.js +14 -0
- package/dist/admin/components/collections/diff-modal_module.css +56 -0
- package/dist/admin/components/collections/status-badge.d.ts +26 -0
- package/dist/admin/components/collections/status-badge.d.ts.map +1 -0
- package/dist/admin/components/collections/status-badge.js +35 -0
- package/dist/admin/components/collections/status-badge.module.js +7 -0
- package/dist/admin/components/collections/status-badge_module.css +20 -0
- package/dist/admin/group.d.ts +28 -0
- package/dist/admin/group.d.ts.map +1 -0
- package/dist/admin/group.js +14 -0
- package/dist/admin/group.module.js +6 -0
- package/dist/admin/group_module.css +19 -0
- package/dist/admin/row.d.ts +26 -0
- package/dist/admin/row.d.ts.map +1 -0
- package/dist/admin/row.js +8 -0
- package/dist/admin/row.module.js +5 -0
- package/dist/admin/row_module.css +18 -0
- package/dist/admin/tabs.d.ts +33 -0
- package/dist/admin/tabs.d.ts.map +1 -0
- package/dist/admin/tabs.js +34 -0
- package/dist/admin/tabs.module.js +10 -0
- package/dist/admin/tabs_module.css +68 -0
- package/dist/dnd/draggable-sortable/demo/draggable-list-demo.js +105 -0
- package/dist/dnd/draggable-sortable/demo/draggable-list-demo.module.js +12 -0
- package/dist/dnd/draggable-sortable/demo/draggable-list-demo_module.css +39 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/index.d.ts +19 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/index.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/index.js +27 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/types.d.ts +25 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/types.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/draggable-sortable-item/types.js +1 -0
- package/dist/dnd/draggable-sortable/draggable-sortable.d.ts +17 -0
- package/dist/dnd/draggable-sortable/draggable-sortable.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/draggable-sortable.js +46 -0
- package/dist/dnd/draggable-sortable/index.d.ts +5 -0
- package/dist/dnd/draggable-sortable/index.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/index.js +4 -0
- package/dist/dnd/draggable-sortable/types.d.ts +26 -0
- package/dist/dnd/draggable-sortable/types.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/types.js +1 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/index.d.ts +16 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/index.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/index.js +28 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/types.d.ts +23 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/types.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/use-draggable-sortable/types.js +1 -0
- package/dist/dnd/draggable-sortable/utils.d.ts +14 -0
- package/dist/dnd/draggable-sortable/utils.d.ts.map +1 -0
- package/dist/dnd/draggable-sortable/utils.js +10 -0
- package/dist/fields/array/array-field.d.ts +15 -0
- package/dist/fields/array/array-field.d.ts.map +1 -0
- package/dist/fields/array/array-field.js +176 -0
- package/dist/fields/array/array-field.module.js +11 -0
- package/dist/fields/array/array-field_module.css +32 -0
- package/dist/fields/blocks/blocks-field.d.ts +14 -0
- package/dist/fields/blocks/blocks-field.d.ts.map +1 -0
- package/dist/fields/blocks/blocks-field.js +244 -0
- package/dist/fields/blocks/blocks-field.module.js +26 -0
- package/dist/fields/blocks/blocks-field_module.css +107 -0
- package/dist/fields/checkbox/checkbox-field.d.ts +17 -0
- package/dist/fields/checkbox/checkbox-field.d.ts.map +1 -0
- package/dist/fields/checkbox/checkbox-field.js +27 -0
- package/dist/fields/column-formatter.d.ts +21 -0
- package/dist/fields/column-formatter.d.ts.map +1 -0
- package/dist/fields/column-formatter.js +15 -0
- package/dist/fields/date-time-formatter.d.ts +17 -0
- package/dist/fields/date-time-formatter.d.ts.map +1 -0
- package/dist/fields/date-time-formatter.js +8 -0
- package/dist/fields/datetime/datetime-field.d.ts +17 -0
- package/dist/fields/datetime/datetime-field.d.ts.map +1 -0
- package/dist/fields/datetime/datetime-field.js +37 -0
- package/dist/fields/datetime/datetime-field.module.js +5 -0
- package/dist/fields/datetime/datetime-field_module.css +4 -0
- package/dist/fields/draggable-context-menu.d.ts +7 -0
- package/dist/fields/draggable-context-menu.d.ts.map +1 -0
- package/dist/fields/draggable-context-menu.js +83 -0
- package/dist/fields/draggable-context-menu.module.js +15 -0
- package/dist/fields/draggable-context-menu_module.css +91 -0
- package/dist/fields/field-helpers.d.ts +27 -0
- package/dist/fields/field-helpers.d.ts.map +1 -0
- package/dist/fields/field-helpers.js +48 -0
- package/dist/fields/field-renderer.d.ts +31 -0
- package/dist/fields/field-renderer.d.ts.map +1 -0
- package/dist/fields/field-renderer.js +189 -0
- package/dist/fields/field-renderer.module.js +8 -0
- package/dist/fields/field-renderer_module.css +11 -0
- package/dist/fields/file/file-field.d.ts +18 -0
- package/dist/fields/file/file-field.d.ts.map +1 -0
- package/dist/fields/file/file-field.js +125 -0
- package/dist/fields/file/file-field.module.js +13 -0
- package/dist/fields/file/file-field_module.css +64 -0
- package/dist/fields/group/group-field.d.ts +16 -0
- package/dist/fields/group/group-field.d.ts.map +1 -0
- package/dist/fields/group/group-field.js +59 -0
- package/dist/fields/group/group-field.module.js +9 -0
- package/dist/fields/group/group-field_module.css +27 -0
- package/dist/fields/image/image-field.d.ts +20 -0
- package/dist/fields/image/image-field.d.ts.map +1 -0
- package/dist/fields/image/image-field.js +198 -0
- package/dist/fields/image/image-field.module.js +21 -0
- package/dist/fields/image/image-field_module.css +96 -0
- package/dist/fields/image/image-upload-field.d.ts +22 -0
- package/dist/fields/image/image-upload-field.d.ts.map +1 -0
- package/dist/fields/image/image-upload-field.js +187 -0
- package/dist/fields/image/image-upload-field.module.js +19 -0
- package/dist/fields/image/image-upload-field_module.css +92 -0
- package/dist/fields/local-date-time.d.ts +28 -0
- package/dist/fields/local-date-time.d.ts.map +1 -0
- package/dist/fields/local-date-time.js +49 -0
- package/dist/fields/locale-badge.d.ts +19 -0
- package/dist/fields/locale-badge.d.ts.map +1 -0
- package/dist/fields/locale-badge.js +10 -0
- package/dist/fields/locale-badge.module.js +5 -0
- package/dist/fields/locale-badge_module.css +27 -0
- package/dist/fields/numerical/numerical-field.d.ts +19 -0
- package/dist/fields/numerical/numerical-field.d.ts.map +1 -0
- package/dist/fields/numerical/numerical-field.js +73 -0
- package/dist/fields/relation/relation-display.d.ts +41 -0
- package/dist/fields/relation/relation-display.d.ts.map +1 -0
- package/dist/fields/relation/relation-display.js +58 -0
- package/dist/fields/relation/relation-display.module.js +9 -0
- package/dist/fields/relation/relation-display_module.css +21 -0
- package/dist/fields/relation/relation-field.d.ts +19 -0
- package/dist/fields/relation/relation-field.d.ts.map +1 -0
- package/dist/fields/relation/relation-field.js +133 -0
- package/dist/fields/relation/relation-field.module.js +13 -0
- package/dist/fields/relation/relation-field_module.css +62 -0
- package/dist/fields/relation/relation-picker.d.ts +50 -0
- package/dist/fields/relation/relation-picker.d.ts.map +1 -0
- package/dist/fields/relation/relation-picker.js +233 -0
- package/dist/fields/relation/relation-picker.module.js +26 -0
- package/dist/fields/relation/relation-picker_module.css +124 -0
- package/dist/fields/relation/relation-summary.d.ts +32 -0
- package/dist/fields/relation/relation-summary.d.ts.map +1 -0
- package/dist/fields/relation/relation-summary.js +50 -0
- package/dist/fields/relation/relation-summary.module.js +11 -0
- package/dist/fields/relation/relation-summary_module.css +37 -0
- package/dist/fields/select/select-field.d.ts +17 -0
- package/dist/fields/select/select-field.d.ts.map +1 -0
- package/dist/fields/select/select-field.js +42 -0
- package/dist/fields/select/select-field.module.js +5 -0
- package/dist/fields/select/select-field_module.css +4 -0
- package/dist/fields/sortable-item.d.ts +16 -0
- package/dist/fields/sortable-item.d.ts.map +1 -0
- package/dist/fields/sortable-item.js +80 -0
- package/dist/fields/sortable-item.module.js +22 -0
- package/dist/fields/sortable-item_module.css +124 -0
- package/dist/fields/text/text-field.d.ts +21 -0
- package/dist/fields/text/text-field.d.ts.map +1 -0
- package/dist/fields/text/text-field.js +104 -0
- package/dist/fields/text/text-field.module.js +6 -0
- package/dist/fields/text/text-field_module.css +5 -0
- package/dist/fields/text-area/text-area-field.d.ts +21 -0
- package/dist/fields/text-area/text-area-field.d.ts.map +1 -0
- package/dist/fields/text-area/text-area-field.js +105 -0
- package/dist/fields/text-area/text-area-field.module.js +6 -0
- package/dist/fields/text-area/text-area-field_module.css +5 -0
- package/dist/fields/use-field-change-handler.d.ts +24 -0
- package/dist/fields/use-field-change-handler.d.ts.map +1 -0
- package/dist/fields/use-field-change-handler.js +52 -0
- package/dist/forms/document-actions.d.ts +14 -0
- package/dist/forms/document-actions.d.ts.map +1 -0
- package/dist/forms/document-actions.js +153 -0
- package/dist/forms/document-actions.module.js +18 -0
- package/dist/forms/document-actions_module.css +66 -0
- package/dist/forms/form-context.d.ts +78 -0
- package/dist/forms/form-context.d.ts.map +1 -0
- package/dist/forms/form-context.js +420 -0
- package/dist/forms/form-renderer.d.ts +66 -0
- package/dist/forms/form-renderer.d.ts.map +1 -0
- package/dist/forms/form-renderer.js +555 -0
- package/dist/forms/form-renderer.module.js +46 -0
- package/dist/forms/form-renderer_module.css +242 -0
- package/dist/forms/navigation-guard.d.ts +55 -0
- package/dist/forms/navigation-guard.d.ts.map +1 -0
- package/dist/forms/navigation-guard.js +22 -0
- package/dist/forms/path-widget.d.ts +33 -0
- package/dist/forms/path-widget.d.ts.map +1 -0
- package/dist/forms/path-widget.js +101 -0
- package/dist/forms/path-widget.module.js +8 -0
- package/dist/forms/path-widget_module.css +29 -0
- package/dist/forms/upload-executor.d.ts +58 -0
- package/dist/forms/upload-executor.d.ts.map +1 -0
- package/dist/forms/upload-executor.js +92 -0
- package/dist/react.d.ts +55 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +48 -0
- package/dist/services/admin-services-context.d.ts +17 -0
- package/dist/services/admin-services-context.d.ts.map +1 -0
- package/dist/services/admin-services-context.js +13 -0
- package/dist/services/admin-services-types.d.ts +130 -0
- package/dist/services/admin-services-types.d.ts.map +1 -0
- package/dist/services/admin-services-types.js +1 -0
- package/dist/services/field-services-context.d.ts +17 -0
- package/dist/services/field-services-context.d.ts.map +1 -0
- package/dist/services/field-services-context.js +13 -0
- package/dist/services/field-services-types.d.ts +64 -0
- package/dist/services/field-services-types.d.ts.map +1 -0
- package/dist/services/field-services-types.js +1 -0
- package/package.json +133 -0
- package/src/admin/components/admin-account/change-password.module.css +40 -0
- package/src/admin/components/admin-account/change-password.tsx +232 -0
- package/src/admin/components/admin-account/container.module.css +158 -0
- package/src/admin/components/admin-account/container.tsx +230 -0
- package/src/admin/components/admin-account/update.module.css +40 -0
- package/src/admin/components/admin-account/update.tsx +263 -0
- package/src/admin/components/admin-permissions/inspector.module.css +326 -0
- package/src/admin/components/admin-permissions/inspector.tsx +298 -0
- package/src/admin/components/admin-roles/create.module.css +40 -0
- package/src/admin/components/admin-roles/create.tsx +218 -0
- package/src/admin/components/admin-roles/permissions.module.css +279 -0
- package/src/admin/components/admin-roles/permissions.tsx +396 -0
- package/src/admin/components/admin-roles/update.module.css +40 -0
- package/src/admin/components/admin-roles/update.tsx +218 -0
- package/src/admin/components/admin-users/create.module.css +63 -0
- package/src/admin/components/admin-users/create.tsx +323 -0
- package/src/admin/components/admin-users/roles.module.css +119 -0
- package/src/admin/components/admin-users/roles.tsx +172 -0
- package/src/admin/components/admin-users/set-password.module.css +46 -0
- package/src/admin/components/admin-users/set-password.tsx +199 -0
- package/src/admin/components/admin-users/update.module.css +49 -0
- package/src/admin/components/admin-users/update.tsx +328 -0
- package/src/admin/components/auth/sign-in-form.module.css +53 -0
- package/src/admin/components/auth/sign-in-form.tsx +118 -0
- package/src/admin/components/collections/diff-modal.module.css +79 -0
- package/src/admin/components/collections/diff-modal.tsx +171 -0
- package/src/admin/components/collections/status-badge.module.css +31 -0
- package/src/admin/components/collections/status-badge.tsx +69 -0
- package/src/admin/group.module.css +41 -0
- package/src/admin/group.tsx +40 -0
- package/src/admin/row.module.css +32 -0
- package/src/admin/row.tsx +33 -0
- package/src/admin/tabs.module.css +107 -0
- package/src/admin/tabs.tsx +74 -0
- package/src/declarations.d.ts +4 -0
- package/src/dnd/draggable-sortable/demo/draggable-list-demo.module.css +65 -0
- package/src/dnd/draggable-sortable/demo/draggable-list-demo.tsx +117 -0
- package/src/dnd/draggable-sortable/draggable-sortable-item/index.tsx +54 -0
- package/src/dnd/draggable-sortable/draggable-sortable-item/types.ts +30 -0
- package/src/dnd/draggable-sortable/draggable-sortable.tsx +86 -0
- package/src/dnd/draggable-sortable/index.ts +5 -0
- package/src/dnd/draggable-sortable/types.ts +24 -0
- package/src/dnd/draggable-sortable/use-draggable-sortable/index.tsx +50 -0
- package/src/dnd/draggable-sortable/use-draggable-sortable/types.ts +25 -0
- package/src/dnd/draggable-sortable/utils.ts +29 -0
- package/src/fields/array/array-field.module.css +48 -0
- package/src/fields/array/array-field.tsx +266 -0
- package/src/fields/blocks/blocks-field.module.css +148 -0
- package/src/fields/blocks/blocks-field.tsx +312 -0
- package/src/fields/checkbox/checkbox-field.tsx +53 -0
- package/src/fields/column-formatter.tsx +31 -0
- package/src/fields/date-time-formatter.tsx +22 -0
- package/src/fields/datetime/datetime-field.module.css +13 -0
- package/src/fields/datetime/datetime-field.tsx +54 -0
- package/src/fields/draggable-context-menu.module.css +127 -0
- package/src/fields/draggable-context-menu.tsx +85 -0
- package/src/fields/field-helpers.ts +66 -0
- package/src/fields/field-renderer.module.css +22 -0
- package/src/fields/field-renderer.tsx +255 -0
- package/src/fields/file/file-field.module.css +88 -0
- package/src/fields/file/file-field.tsx +107 -0
- package/src/fields/group/group-field.module.css +43 -0
- package/src/fields/group/group-field.tsx +84 -0
- package/src/fields/image/image-field.module.css +129 -0
- package/src/fields/image/image-field.tsx +212 -0
- package/src/fields/image/image-upload-field.module.css +123 -0
- package/src/fields/image/image-upload-field.tsx +270 -0
- package/src/fields/local-date-time.tsx +88 -0
- package/src/fields/locale-badge.module.css +37 -0
- package/src/fields/locale-badge.tsx +32 -0
- package/src/fields/numerical/numerical-field.tsx +112 -0
- package/src/fields/relation/relation-display.module.css +36 -0
- package/src/fields/relation/relation-display.tsx +130 -0
- package/src/fields/relation/relation-field.module.css +83 -0
- package/src/fields/relation/relation-field.tsx +202 -0
- package/src/fields/relation/relation-picker.module.css +168 -0
- package/src/fields/relation/relation-picker.tsx +325 -0
- package/src/fields/relation/relation-summary.module.css +55 -0
- package/src/fields/relation/relation-summary.tsx +123 -0
- package/src/fields/select/select-field.module.css +13 -0
- package/src/fields/select/select-field.tsx +56 -0
- package/src/fields/sortable-item.module.css +167 -0
- package/src/fields/sortable-item.tsx +101 -0
- package/src/fields/text/text-field.module.css +13 -0
- package/src/fields/text/text-field.tsx +146 -0
- package/src/fields/text-area/text-area-field.module.css +13 -0
- package/src/fields/text-area/text-area-field.tsx +147 -0
- package/src/fields/use-field-change-handler.ts +112 -0
- package/src/forms/document-actions.module.css +94 -0
- package/src/forms/document-actions.tsx +149 -0
- package/src/forms/form-context.tsx +620 -0
- package/src/forms/form-renderer.module.css +318 -0
- package/src/forms/form-renderer.tsx +786 -0
- package/src/forms/navigation-guard.tsx +98 -0
- package/src/forms/path-widget.module.css +41 -0
- package/src/forms/path-widget.test.tsx +217 -0
- package/src/forms/path-widget.tsx +141 -0
- package/src/forms/upload-executor.ts +190 -0
- package/src/react.ts +79 -0
- package/src/services/admin-services-context.tsx +35 -0
- package/src/services/admin-services-types.ts +177 -0
- package/src/services/field-services-context.tsx +35 -0
- package/src/services/field-services-types.ts +68 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Framework-agnostic navigation guard adapter.
|
|
11
|
+
*
|
|
12
|
+
* Different router frameworks (TanStack Router, Next.js, React Router, etc.)
|
|
13
|
+
* each have their own mechanism for blocking navigation when a form has unsaved
|
|
14
|
+
* changes. This module defines a common interface so that `FormRenderer` can
|
|
15
|
+
* remain framework-independent — the consuming application injects the
|
|
16
|
+
* appropriate adapter via a prop or React context.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createContext, useContext, useEffect } from 'react'
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Types
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** The result returned by a `UseNavigationGuard` hook. */
|
|
26
|
+
export interface NavigationGuardResult {
|
|
27
|
+
/** Whether a navigation attempt is currently being blocked (show confirmation UI). */
|
|
28
|
+
isBlocked: boolean
|
|
29
|
+
/** Cancel the pending navigation — stay on the current page. */
|
|
30
|
+
stay: () => void
|
|
31
|
+
/** Confirm the pending navigation — leave the page. */
|
|
32
|
+
proceed: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A hook that blocks in-app navigation and (optionally) browser unload when
|
|
37
|
+
* `shouldBlock` is `true`.
|
|
38
|
+
*
|
|
39
|
+
* Each framework adapter implements this signature.
|
|
40
|
+
*/
|
|
41
|
+
export type UseNavigationGuard = (shouldBlock: boolean) => NavigationGuardResult
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Default (no-op) implementation — browser `beforeunload` only
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fallback navigation guard that only handles the browser's native
|
|
49
|
+
* `beforeunload` event. In-app (client-side) route changes are **not**
|
|
50
|
+
* intercepted — `isBlocked` will never become `true`.
|
|
51
|
+
*
|
|
52
|
+
* This is used when no framework-specific adapter has been provided.
|
|
53
|
+
*/
|
|
54
|
+
export const useBeforeUnloadGuard: UseNavigationGuard = (shouldBlock) => {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!shouldBlock) return
|
|
57
|
+
const handler = (e: BeforeUnloadEvent) => {
|
|
58
|
+
e.preventDefault()
|
|
59
|
+
}
|
|
60
|
+
window.addEventListener('beforeunload', handler)
|
|
61
|
+
return () => window.removeEventListener('beforeunload', handler)
|
|
62
|
+
}, [shouldBlock])
|
|
63
|
+
|
|
64
|
+
return { isBlocked: false, stay: () => {}, proceed: () => {} }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// React context — allows setting the adapter once at the app shell level
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const NavigationGuardContext = createContext<UseNavigationGuard>(useBeforeUnloadGuard)
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Provide a framework-specific `UseNavigationGuard` hook to all descendant
|
|
75
|
+
* `FormRenderer` instances.
|
|
76
|
+
*
|
|
77
|
+
* ```tsx
|
|
78
|
+
* import { NavigationGuardProvider } from './navigation-guard'
|
|
79
|
+
* import { useTanStackNavigationGuard } from './tanstack-navigation-guard'
|
|
80
|
+
*
|
|
81
|
+
* function App() {
|
|
82
|
+
* return (
|
|
83
|
+
* <NavigationGuardProvider value={useTanStackNavigationGuard}>
|
|
84
|
+
* <Outlet />
|
|
85
|
+
* </NavigationGuardProvider>
|
|
86
|
+
* )
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export const NavigationGuardProvider = NavigationGuardContext.Provider
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Consume the current `UseNavigationGuard` hook from context.
|
|
94
|
+
* Falls back to `useBeforeUnloadGuard` when no provider is present.
|
|
95
|
+
*/
|
|
96
|
+
export const useNavigationGuardAdapter = (): UseNavigationGuard => {
|
|
97
|
+
return useContext(NavigationGuardContext)
|
|
98
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PathWidget — system-managed `documentVersions.path` form widget.
|
|
3
|
+
*
|
|
4
|
+
* Override handles:
|
|
5
|
+
* .byline-form-path — wrapper div
|
|
6
|
+
* .byline-form-path-header — label + regenerate-button row
|
|
7
|
+
* .byline-form-path-regenerate — regenerate-link button
|
|
8
|
+
* .byline-form-path-sr-only — visually-hidden screen-reader hint
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.header,
|
|
12
|
+
:global(.byline-form-path-header) {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
gap: var(--spacing-8);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.regenerate,
|
|
20
|
+
:global(.byline-form-path-regenerate) {
|
|
21
|
+
background: none;
|
|
22
|
+
border: none;
|
|
23
|
+
padding: 0;
|
|
24
|
+
color: inherit;
|
|
25
|
+
font-size: 0.8rem;
|
|
26
|
+
text-decoration: underline;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.sr-only,
|
|
31
|
+
:global(.byline-form-path-sr-only) {
|
|
32
|
+
position: absolute;
|
|
33
|
+
width: 1px;
|
|
34
|
+
height: 1px;
|
|
35
|
+
padding: 0;
|
|
36
|
+
margin: -1px;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
clip: rect(0, 0, 0, 0);
|
|
39
|
+
white-space: nowrap;
|
|
40
|
+
border: 0;
|
|
41
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { act } from 'react'
|
|
10
|
+
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
+
|
|
14
|
+
// Lightweight uikit stubs — we don't care about the visual rendering, only
|
|
15
|
+
// that the Input forwards props and the Label renders its htmlFor.
|
|
16
|
+
vi.mock('@infonomic/uikit/react', () => ({
|
|
17
|
+
Label: ({ id, htmlFor, label }: { id?: string; htmlFor?: string; label?: string }) => (
|
|
18
|
+
<label id={id} htmlFor={htmlFor}>
|
|
19
|
+
{label}
|
|
20
|
+
</label>
|
|
21
|
+
),
|
|
22
|
+
Input: ({
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
value,
|
|
26
|
+
placeholder,
|
|
27
|
+
onChange,
|
|
28
|
+
helpText,
|
|
29
|
+
...rest
|
|
30
|
+
}: {
|
|
31
|
+
id?: string
|
|
32
|
+
name?: string
|
|
33
|
+
value?: string
|
|
34
|
+
placeholder?: string
|
|
35
|
+
onChange?: (e: { target: { value: string } }) => void
|
|
36
|
+
helpText?: string
|
|
37
|
+
[key: string]: any
|
|
38
|
+
}) => (
|
|
39
|
+
<>
|
|
40
|
+
<input
|
|
41
|
+
id={id}
|
|
42
|
+
name={name}
|
|
43
|
+
value={value ?? ''}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
onChange={onChange}
|
|
46
|
+
{...rest}
|
|
47
|
+
/>
|
|
48
|
+
{helpText ? <span data-testid="help-text">{helpText}</span> : null}
|
|
49
|
+
</>
|
|
50
|
+
),
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
// Mutable mocks controlled per-test via the setFixture helper below.
|
|
54
|
+
const fixture: {
|
|
55
|
+
systemPath: string | null
|
|
56
|
+
sourceValue: unknown
|
|
57
|
+
setSystemPath: ReturnType<typeof vi.fn>
|
|
58
|
+
} = {
|
|
59
|
+
systemPath: null,
|
|
60
|
+
sourceValue: '',
|
|
61
|
+
setSystemPath: vi.fn(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
vi.mock('./form-context', () => ({
|
|
65
|
+
useFormContext: () => ({ setSystemPath: fixture.setSystemPath }),
|
|
66
|
+
useSystemPath: () => fixture.systemPath,
|
|
67
|
+
useFieldValue: () => fixture.sourceValue,
|
|
68
|
+
}))
|
|
69
|
+
|
|
70
|
+
// Import AFTER the mocks so PathWidget picks them up.
|
|
71
|
+
// biome-ignore lint/correctness/useImportExtensions: webapp TS resolves via tsconfig paths
|
|
72
|
+
import { PathWidget } from './path-widget'
|
|
73
|
+
|
|
74
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
75
|
+
|
|
76
|
+
interface Fixture {
|
|
77
|
+
systemPath?: string | null
|
|
78
|
+
sourceValue?: unknown
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setFixture(next: Fixture = {}) {
|
|
82
|
+
fixture.systemPath = next.systemPath ?? null
|
|
83
|
+
fixture.sourceValue = next.sourceValue ?? ''
|
|
84
|
+
fixture.setSystemPath = vi.fn()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe('PathWidget', () => {
|
|
88
|
+
let container: HTMLDivElement
|
|
89
|
+
let root: Root
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
container = document.createElement('div')
|
|
93
|
+
document.body.appendChild(container)
|
|
94
|
+
root = createRoot(container)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
act(() => {
|
|
99
|
+
root.unmount()
|
|
100
|
+
})
|
|
101
|
+
container.remove()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const render = (
|
|
105
|
+
props: Partial<{
|
|
106
|
+
useAsPath: string | undefined
|
|
107
|
+
mode: 'create' | 'edit'
|
|
108
|
+
}> = {}
|
|
109
|
+
) => {
|
|
110
|
+
act(() => {
|
|
111
|
+
root.render(
|
|
112
|
+
<PathWidget
|
|
113
|
+
useAsPath={props.useAsPath ?? 'title'}
|
|
114
|
+
collectionPath="pages"
|
|
115
|
+
defaultLocale="en"
|
|
116
|
+
mode={props.mode ?? 'create'}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const getInput = () => container.querySelector('#system-path') as HTMLInputElement
|
|
123
|
+
|
|
124
|
+
it('shows the live-derived preview as placeholder when creating with an empty override', () => {
|
|
125
|
+
setFixture({ systemPath: null, sourceValue: 'Hello World' })
|
|
126
|
+
render({ mode: 'create' })
|
|
127
|
+
|
|
128
|
+
const input = getInput()
|
|
129
|
+
expect(input).toBeTruthy()
|
|
130
|
+
expect(input.value).toBe('')
|
|
131
|
+
expect(input.getAttribute('placeholder')).toBe('Will be saved as "hello-world"')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('shows the persisted path in edit mode (no placeholder preview)', () => {
|
|
135
|
+
setFixture({ systemPath: 'existing-path', sourceValue: 'Hello World' })
|
|
136
|
+
render({ mode: 'edit' })
|
|
137
|
+
|
|
138
|
+
const input = getInput()
|
|
139
|
+
expect(input.value).toBe('existing-path')
|
|
140
|
+
// livePreview === 'hello-world' differs from persisted 'existing-path',
|
|
141
|
+
// so the regenerate button is rendered.
|
|
142
|
+
const regenerate = container.querySelector('button')
|
|
143
|
+
expect(regenerate).toBeTruthy()
|
|
144
|
+
expect(regenerate?.textContent).toContain('Regenerate from title')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('"Regenerate" writes the live preview into the systemPath slot', () => {
|
|
148
|
+
setFixture({ systemPath: 'stale-path', sourceValue: 'Brand New Title' })
|
|
149
|
+
render({ mode: 'edit' })
|
|
150
|
+
|
|
151
|
+
const regenerate = container.querySelector('button') as HTMLButtonElement
|
|
152
|
+
expect(regenerate).toBeTruthy()
|
|
153
|
+
|
|
154
|
+
act(() => {
|
|
155
|
+
regenerate.click()
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(fixture.setSystemPath).toHaveBeenCalledWith('brand-new-title')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('clearing the input reverts the slot to null (sticky-from-previous)', () => {
|
|
162
|
+
setFixture({ systemPath: 'my-path', sourceValue: 'My Path' })
|
|
163
|
+
render({ mode: 'edit' })
|
|
164
|
+
|
|
165
|
+
const input = getInput()
|
|
166
|
+
act(() => {
|
|
167
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
168
|
+
window.HTMLInputElement.prototype,
|
|
169
|
+
'value'
|
|
170
|
+
)?.set
|
|
171
|
+
setter?.call(input, '')
|
|
172
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(fixture.setSystemPath).toHaveBeenCalledWith(null)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('typing a non-empty value writes a string override', () => {
|
|
179
|
+
setFixture({ systemPath: null, sourceValue: 'Anything' })
|
|
180
|
+
render({ mode: 'create' })
|
|
181
|
+
|
|
182
|
+
const input = getInput()
|
|
183
|
+
act(() => {
|
|
184
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
185
|
+
window.HTMLInputElement.prototype,
|
|
186
|
+
'value'
|
|
187
|
+
)?.set
|
|
188
|
+
setter?.call(input, 'custom-slug')
|
|
189
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
expect(fixture.setSystemPath).toHaveBeenCalledWith('custom-slug')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('links the input to an sr-only description via aria-describedby', () => {
|
|
196
|
+
setFixture({ systemPath: null, sourceValue: 'Hi' })
|
|
197
|
+
render({ mode: 'create' })
|
|
198
|
+
|
|
199
|
+
const input = getInput()
|
|
200
|
+
expect(input.getAttribute('aria-describedby')).toBe('system-path-description')
|
|
201
|
+
const description = container.querySelector('#system-path-description')
|
|
202
|
+
expect(description).toBeTruthy()
|
|
203
|
+
expect(description?.textContent).toContain('System-managed URL path')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('does not render the Regenerate button when livePreview equals systemPath', () => {
|
|
207
|
+
setFixture({ systemPath: 'hello-world', sourceValue: 'Hello World' })
|
|
208
|
+
render({ mode: 'edit' })
|
|
209
|
+
expect(container.querySelector('button')).toBeNull()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('does not render the Regenerate button when there is no useAsPath', () => {
|
|
213
|
+
setFixture({ systemPath: 'whatever', sourceValue: '' })
|
|
214
|
+
render({ mode: 'edit', useAsPath: undefined })
|
|
215
|
+
expect(container.querySelector('button')).toBeNull()
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useCallback, useMemo } from 'react'
|
|
10
|
+
|
|
11
|
+
import { slugify } from '@byline/core'
|
|
12
|
+
import { Input, Label } from '@infonomic/uikit/react'
|
|
13
|
+
import cx from 'classnames'
|
|
14
|
+
|
|
15
|
+
import { useFieldValue, useFormContext, useSystemPath } from './form-context'
|
|
16
|
+
import styles from './path-widget.module.css'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Coerce an arbitrary source-field value (string, Date, or other) into
|
|
20
|
+
* a string suitable for slugification. Mirrors the lifecycle's coercion
|
|
21
|
+
* so the live preview matches what the server will store.
|
|
22
|
+
*/
|
|
23
|
+
function coerceToString(value: unknown): string {
|
|
24
|
+
if (value == null) return ''
|
|
25
|
+
if (value instanceof Date) return value.toISOString()
|
|
26
|
+
return String(value)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PathWidgetProps {
|
|
30
|
+
/** The collection's `useAsPath` source field name, when configured. */
|
|
31
|
+
useAsPath: string | undefined
|
|
32
|
+
/** Collection path, forwarded to the slugifier as context. */
|
|
33
|
+
collectionPath: string
|
|
34
|
+
/** Default content locale, forwarded to the slugifier as context. */
|
|
35
|
+
defaultLocale: string
|
|
36
|
+
/** `'create'` shows the live derived preview as placeholder text. */
|
|
37
|
+
mode: 'create' | 'edit'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* System-managed `documentVersions.path` widget.
|
|
42
|
+
*
|
|
43
|
+
* Displays the current persisted/overridden path as an editable input.
|
|
44
|
+
* In create mode, when the user hasn't supplied an override, the input
|
|
45
|
+
* shows the live-derived preview (slugified `useAsPath` source field) as
|
|
46
|
+
* a placeholder so the user sees what will be saved. The "Regenerate"
|
|
47
|
+
* action explicitly writes the current live preview into the override
|
|
48
|
+
* slot so the user can re-anchor a path against the source field after
|
|
49
|
+
* editing the title.
|
|
50
|
+
*
|
|
51
|
+
* Stable override handles: `.byline-form-path`, `.byline-form-path-header`,
|
|
52
|
+
* `.byline-form-path-regenerate`.
|
|
53
|
+
*/
|
|
54
|
+
export const PathWidget = ({ useAsPath, collectionPath, defaultLocale, mode }: PathWidgetProps) => {
|
|
55
|
+
const { setSystemPath } = useFormContext()
|
|
56
|
+
const systemPath = useSystemPath()
|
|
57
|
+
const sourceValue = useFieldValue<unknown>(useAsPath ?? '')
|
|
58
|
+
|
|
59
|
+
// Live preview — what the server would derive from the current source
|
|
60
|
+
// field value if no override were set. Used as placeholder in create
|
|
61
|
+
// mode and as the target of the "Regenerate" action.
|
|
62
|
+
const livePreview = useMemo(() => {
|
|
63
|
+
if (!useAsPath) return ''
|
|
64
|
+
const asString = coerceToString(sourceValue)
|
|
65
|
+
if (asString.length === 0) return ''
|
|
66
|
+
return slugify(asString, { locale: defaultLocale, collectionPath })
|
|
67
|
+
}, [useAsPath, sourceValue, defaultLocale, collectionPath])
|
|
68
|
+
|
|
69
|
+
const inputValue = systemPath ?? ''
|
|
70
|
+
|
|
71
|
+
const handleChange = useCallback(
|
|
72
|
+
(next: string) => {
|
|
73
|
+
// Empty string clears the override — server falls back to derive
|
|
74
|
+
// (create) or sticky (update).
|
|
75
|
+
setSystemPath(next.length === 0 ? null : next)
|
|
76
|
+
},
|
|
77
|
+
[setSystemPath]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
const handleRegenerate = useCallback(() => {
|
|
81
|
+
if (livePreview.length > 0) {
|
|
82
|
+
setSystemPath(livePreview)
|
|
83
|
+
}
|
|
84
|
+
}, [livePreview, setSystemPath])
|
|
85
|
+
|
|
86
|
+
// Validate live: if the typed value differs from its slugified form,
|
|
87
|
+
// surface an inline hint without blocking input (mirrors the previous
|
|
88
|
+
// field-hook advisory behaviour).
|
|
89
|
+
const formatted = useMemo(() => {
|
|
90
|
+
if (inputValue.length === 0) return ''
|
|
91
|
+
return slugify(inputValue, { locale: defaultLocale, collectionPath })
|
|
92
|
+
}, [inputValue, defaultLocale, collectionPath])
|
|
93
|
+
|
|
94
|
+
const hint =
|
|
95
|
+
inputValue.length > 0 && formatted !== inputValue ? `Suggested: "${formatted}"` : undefined
|
|
96
|
+
|
|
97
|
+
const placeholder =
|
|
98
|
+
mode === 'create' && livePreview.length > 0 ? `Will be saved as "${livePreview}"` : undefined
|
|
99
|
+
|
|
100
|
+
// Screen-reader description. The input's base purpose ("System-managed
|
|
101
|
+
// URL path") plus whichever of the visible hints (placeholder preview
|
|
102
|
+
// in create mode, "Suggested" validation hint) currently applies. The
|
|
103
|
+
// visible helpText/placeholder cover sighted users; this element makes
|
|
104
|
+
// the same information addressable via aria-describedby for AT.
|
|
105
|
+
const srDescription = ['System-managed URL path for this document.', placeholder, hint]
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.join(' ')
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div className="byline-form-path">
|
|
111
|
+
<div className={cx('byline-form-path-header', styles.header)}>
|
|
112
|
+
<Label id="system-path-label" htmlFor="system-path" label="Path" />
|
|
113
|
+
{useAsPath && livePreview.length > 0 && livePreview !== systemPath && (
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={handleRegenerate}
|
|
117
|
+
className={cx('byline-form-path-regenerate', styles.regenerate)}
|
|
118
|
+
aria-label={`Regenerate path from ${useAsPath} field`}
|
|
119
|
+
>
|
|
120
|
+
Regenerate from {useAsPath}
|
|
121
|
+
</button>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
<Input
|
|
125
|
+
id="system-path"
|
|
126
|
+
name="__systemPath__"
|
|
127
|
+
value={inputValue}
|
|
128
|
+
placeholder={placeholder}
|
|
129
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
130
|
+
helpText={hint}
|
|
131
|
+
aria-describedby="system-path-description"
|
|
132
|
+
/>
|
|
133
|
+
<span
|
|
134
|
+
id="system-path-description"
|
|
135
|
+
className={cx('byline-form-path-sr-only', styles['sr-only'])}
|
|
136
|
+
>
|
|
137
|
+
{srDescription}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upload Executor
|
|
11
|
+
*
|
|
12
|
+
* Handles batch execution of pending file uploads at form submission time.
|
|
13
|
+
* This enables "deferred uploads" — files are selected/previewed immediately
|
|
14
|
+
* but only uploaded when the user clicks Save.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { StoredFileValue } from '@byline/core'
|
|
18
|
+
|
|
19
|
+
import type { UploadFieldFn } from '../services/field-services-types'
|
|
20
|
+
import type { PendingUpload } from './form-context'
|
|
21
|
+
|
|
22
|
+
export interface UploadResult {
|
|
23
|
+
fieldPath: string
|
|
24
|
+
success: boolean
|
|
25
|
+
storedFile?: StoredFileValue
|
|
26
|
+
error?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ExecuteUploadsResult {
|
|
30
|
+
/** All upload results (both successful and failed) */
|
|
31
|
+
results: UploadResult[]
|
|
32
|
+
/** Map of field path to StoredFileValue for successful uploads */
|
|
33
|
+
successful: Map<string, StoredFileValue>
|
|
34
|
+
/** Map of field path to error message for failed uploads */
|
|
35
|
+
errors: Map<string, string>
|
|
36
|
+
/** Whether all uploads succeeded */
|
|
37
|
+
allSucceeded: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Execute all pending uploads sequentially.
|
|
42
|
+
* Returns a result object with successful uploads and any errors.
|
|
43
|
+
*
|
|
44
|
+
* @param pendingUploads - Map of field path to PendingUpload
|
|
45
|
+
* @param uploadField - Host-provided upload transport (resolved via
|
|
46
|
+
* `useBylineFieldServices()` in the calling React tree)
|
|
47
|
+
* @returns Promise resolving to ExecuteUploadsResult
|
|
48
|
+
*/
|
|
49
|
+
export async function executeUploads(
|
|
50
|
+
pendingUploads: Map<string, PendingUpload>,
|
|
51
|
+
uploadField: UploadFieldFn
|
|
52
|
+
): Promise<ExecuteUploadsResult> {
|
|
53
|
+
const results: UploadResult[] = []
|
|
54
|
+
const successful = new Map<string, StoredFileValue>()
|
|
55
|
+
const errors = new Map<string, string>()
|
|
56
|
+
|
|
57
|
+
for (const [fieldPath, upload] of pendingUploads.entries()) {
|
|
58
|
+
const formData = new FormData()
|
|
59
|
+
formData.append('file', upload.file)
|
|
60
|
+
// Tell the server which upload-capable field this file belongs to.
|
|
61
|
+
// With per-field upload config a collection can have multiple
|
|
62
|
+
// image/file fields, each with its own constraints; the server's
|
|
63
|
+
// unique-default fallback covers the single-field case but rejects
|
|
64
|
+
// multi-field collections without an explicit selector.
|
|
65
|
+
formData.append('field', uploadFieldName(fieldPath))
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Pass createDocument=false — we're uploading for an embedded field,
|
|
69
|
+
// the form's save action handles document creation/update.
|
|
70
|
+
const result = await uploadField(upload.collectionPath, formData, false)
|
|
71
|
+
|
|
72
|
+
results.push({
|
|
73
|
+
fieldPath,
|
|
74
|
+
success: true,
|
|
75
|
+
storedFile: result.storedFile,
|
|
76
|
+
})
|
|
77
|
+
successful.set(fieldPath, result.storedFile)
|
|
78
|
+
} catch (err: unknown) {
|
|
79
|
+
const message = err instanceof Error ? err.message : 'Upload failed'
|
|
80
|
+
results.push({
|
|
81
|
+
fieldPath,
|
|
82
|
+
success: false,
|
|
83
|
+
error: message,
|
|
84
|
+
})
|
|
85
|
+
errors.set(fieldPath, message)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
results,
|
|
91
|
+
successful,
|
|
92
|
+
errors,
|
|
93
|
+
allSucceeded: errors.size === 0,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Extract the leaf field name from a `fieldPath`. Top-level upload
|
|
99
|
+
* fields (`'image'`, `'avatar'`) pass through unchanged; nested paths
|
|
100
|
+
* (`'profile.avatar'`) reduce to their last segment, since the
|
|
101
|
+
* server-side resolver matches against top-level field names today.
|
|
102
|
+
* Nested upload fields would need a richer transport selector when
|
|
103
|
+
* they land — the host resolver is the natural place to extend.
|
|
104
|
+
*/
|
|
105
|
+
function uploadFieldName(fieldPath: string): string {
|
|
106
|
+
const dot = fieldPath.lastIndexOf('.')
|
|
107
|
+
return dot === -1 ? fieldPath : fieldPath.slice(dot + 1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Progress callback type for upload execution with progress tracking.
|
|
112
|
+
*/
|
|
113
|
+
export type UploadProgressCallback = (info: {
|
|
114
|
+
current: number
|
|
115
|
+
total: number
|
|
116
|
+
fieldPath: string
|
|
117
|
+
status: 'uploading' | 'done' | 'error'
|
|
118
|
+
}) => void
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Execute uploads with progress callbacks.
|
|
122
|
+
* Useful for showing upload progress in the UI.
|
|
123
|
+
*/
|
|
124
|
+
export async function executeUploadsWithProgress(
|
|
125
|
+
pendingUploads: Map<string, PendingUpload>,
|
|
126
|
+
uploadField: UploadFieldFn,
|
|
127
|
+
onProgress?: UploadProgressCallback
|
|
128
|
+
): Promise<ExecuteUploadsResult> {
|
|
129
|
+
const results: UploadResult[] = []
|
|
130
|
+
const successful = new Map<string, StoredFileValue>()
|
|
131
|
+
const errors = new Map<string, string>()
|
|
132
|
+
|
|
133
|
+
const entries = Array.from(pendingUploads.entries())
|
|
134
|
+
const total = entries.length
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < entries.length; i++) {
|
|
137
|
+
const [fieldPath, upload] = entries[i]
|
|
138
|
+
|
|
139
|
+
onProgress?.({
|
|
140
|
+
current: i + 1,
|
|
141
|
+
total,
|
|
142
|
+
fieldPath,
|
|
143
|
+
status: 'uploading',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const formData = new FormData()
|
|
147
|
+
formData.append('file', upload.file)
|
|
148
|
+
formData.append('field', uploadFieldName(fieldPath))
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const result = await uploadField(upload.collectionPath, formData, false)
|
|
152
|
+
|
|
153
|
+
results.push({
|
|
154
|
+
fieldPath,
|
|
155
|
+
success: true,
|
|
156
|
+
storedFile: result.storedFile,
|
|
157
|
+
})
|
|
158
|
+
successful.set(fieldPath, result.storedFile)
|
|
159
|
+
|
|
160
|
+
onProgress?.({
|
|
161
|
+
current: i + 1,
|
|
162
|
+
total,
|
|
163
|
+
fieldPath,
|
|
164
|
+
status: 'done',
|
|
165
|
+
})
|
|
166
|
+
} catch (err: unknown) {
|
|
167
|
+
const message = err instanceof Error ? err.message : 'Upload failed'
|
|
168
|
+
results.push({
|
|
169
|
+
fieldPath,
|
|
170
|
+
success: false,
|
|
171
|
+
error: message,
|
|
172
|
+
})
|
|
173
|
+
errors.set(fieldPath, message)
|
|
174
|
+
|
|
175
|
+
onProgress?.({
|
|
176
|
+
current: i + 1,
|
|
177
|
+
total,
|
|
178
|
+
fieldPath,
|
|
179
|
+
status: 'error',
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
results,
|
|
186
|
+
successful,
|
|
187
|
+
errors,
|
|
188
|
+
allSucceeded: errors.size === 0,
|
|
189
|
+
}
|
|
190
|
+
}
|