@pilotiq/pilotiq 0.7.2 → 0.8.1
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/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +208 -0
- package/CLAUDE.md +59 -3
- package/dist/Pilotiq.d.ts +83 -0
- package/dist/Pilotiq.d.ts.map +1 -1
- package/dist/Pilotiq.js +39 -0
- package/dist/Pilotiq.js.map +1 -1
- package/dist/actions/Action.d.ts +27 -99
- package/dist/actions/Action.d.ts.map +1 -1
- package/dist/actions/Action.js +52 -754
- package/dist/actions/Action.js.map +1 -1
- package/dist/actions/bulkFactories.d.ts +46 -0
- package/dist/actions/bulkFactories.d.ts.map +1 -0
- package/dist/actions/bulkFactories.js +144 -0
- package/dist/actions/bulkFactories.js.map +1 -0
- package/dist/actions/crudFactories.d.ts +94 -0
- package/dist/actions/crudFactories.d.ts.map +1 -0
- package/dist/actions/crudFactories.js +209 -0
- package/dist/actions/crudFactories.js.map +1 -0
- package/dist/actions/factoryHelpers.d.ts +108 -0
- package/dist/actions/factoryHelpers.d.ts.map +1 -0
- package/dist/actions/factoryHelpers.js +138 -0
- package/dist/actions/factoryHelpers.js.map +1 -0
- package/dist/actions/m2mFactories.d.ts +47 -0
- package/dist/actions/m2mFactories.d.ts.map +1 -0
- package/dist/actions/m2mFactories.js +173 -0
- package/dist/actions/m2mFactories.js.map +1 -0
- package/dist/actions/relationFactories.d.ts +93 -0
- package/dist/actions/relationFactories.d.ts.map +1 -0
- package/dist/actions/relationFactories.js +321 -0
- package/dist/actions/relationFactories.js.map +1 -0
- package/dist/elements/dispatchForm.js +1 -1
- package/dist/elements/dispatchForm.js.map +1 -1
- package/dist/elements/dispatchTable.js +1 -1
- package/dist/elements/dispatchTable.js.map +1 -1
- package/dist/fields/Field.d.ts +31 -0
- package/dist/fields/Field.d.ts.map +1 -1
- package/dist/fields/Field.js +25 -0
- package/dist/fields/Field.js.map +1 -1
- package/dist/pageData/breadcrumbs.d.ts +42 -0
- package/dist/pageData/breadcrumbs.d.ts.map +1 -0
- package/dist/pageData/breadcrumbs.js +172 -0
- package/dist/pageData/breadcrumbs.js.map +1 -0
- package/dist/pageData/forms.d.ts +137 -0
- package/dist/pageData/forms.d.ts.map +1 -0
- package/dist/pageData/forms.js +427 -0
- package/dist/pageData/forms.js.map +1 -0
- package/dist/pageData/helpers.d.ts +239 -0
- package/dist/pageData/helpers.d.ts.map +1 -0
- package/dist/pageData/helpers.js +703 -0
- package/dist/pageData/helpers.js.map +1 -0
- package/dist/pageData/misc.d.ts +76 -0
- package/dist/pageData/misc.d.ts.map +1 -0
- package/dist/pageData/misc.js +263 -0
- package/dist/pageData/misc.js.map +1 -0
- package/dist/pageData/navigation.d.ts +292 -0
- package/dist/pageData/navigation.d.ts.map +1 -0
- package/dist/pageData/navigation.js +591 -0
- package/dist/pageData/navigation.js.map +1 -0
- package/dist/pageData/relationPages.d.ts +172 -0
- package/dist/pageData/relationPages.d.ts.map +1 -0
- package/dist/pageData/relationPages.js +867 -0
- package/dist/pageData/relationPages.js.map +1 -0
- package/dist/pageData/relationTabs.d.ts +65 -0
- package/dist/pageData/relationTabs.d.ts.map +1 -0
- package/dist/pageData/relationTabs.js +258 -0
- package/dist/pageData/relationTabs.js.map +1 -0
- package/dist/pageData/resourcePages.d.ts +48 -0
- package/dist/pageData/resourcePages.d.ts.map +1 -0
- package/dist/pageData/resourcePages.js +504 -0
- package/dist/pageData/resourcePages.js.map +1 -0
- package/dist/pageData.d.ts +12 -792
- package/dist/pageData.d.ts.map +1 -1
- package/dist/pageData.js +24 -3797
- package/dist/pageData.js.map +1 -1
- package/dist/react/AppShell.d.ts +8 -0
- package/dist/react/AppShell.d.ts.map +1 -1
- package/dist/react/AppShell.js +11 -1
- package/dist/react/AppShell.js.map +1 -1
- package/dist/react/CollabExtensionFactoryRegistry.d.ts +47 -0
- package/dist/react/CollabExtensionFactoryRegistry.d.ts.map +1 -0
- package/dist/react/CollabExtensionFactoryRegistry.js +14 -0
- package/dist/react/CollabExtensionFactoryRegistry.js.map +1 -0
- package/dist/react/CollabRoomContext.d.ts +37 -0
- package/dist/react/CollabRoomContext.d.ts.map +1 -0
- package/dist/react/CollabRoomContext.js +12 -0
- package/dist/react/CollabRoomContext.js.map +1 -0
- package/dist/react/FormCollabBindingRegistry.d.ts +62 -0
- package/dist/react/FormCollabBindingRegistry.d.ts.map +1 -0
- package/dist/react/FormCollabBindingRegistry.js +14 -0
- package/dist/react/FormCollabBindingRegistry.js.map +1 -0
- package/dist/react/FormStateContext.d.ts.map +1 -1
- package/dist/react/FormStateContext.js +87 -0
- package/dist/react/FormStateContext.js.map +1 -1
- package/dist/react/RecordWrapperGate.d.ts +25 -0
- package/dist/react/RecordWrapperGate.d.ts.map +1 -0
- package/dist/react/RecordWrapperGate.js +30 -0
- package/dist/react/RecordWrapperGate.js.map +1 -0
- package/dist/react/RecordWrapperRegistry.d.ts +31 -0
- package/dist/react/RecordWrapperRegistry.d.ts.map +1 -0
- package/dist/react/RecordWrapperRegistry.js +15 -0
- package/dist/react/RecordWrapperRegistry.js.map +1 -0
- package/dist/react/SchemaRenderer.d.ts +17 -23
- package/dist/react/SchemaRenderer.d.ts.map +1 -1
- package/dist/react/SchemaRenderer.js +71 -3647
- package/dist/react/SchemaRenderer.js.map +1 -1
- package/dist/react/component-slots.d.ts +103 -0
- package/dist/react/component-slots.d.ts.map +1 -0
- package/dist/react/component-slots.js +18 -0
- package/dist/react/component-slots.js.map +1 -0
- package/dist/react/fields/BuilderInput.d.ts.map +1 -1
- package/dist/react/fields/BuilderInput.js +21 -117
- package/dist/react/fields/BuilderInput.js.map +1 -1
- package/dist/react/fields/MarkdownInput.d.ts.map +1 -1
- package/dist/react/fields/MarkdownInput.js +1 -3
- package/dist/react/fields/MarkdownInput.js.map +1 -1
- package/dist/react/fields/RepeaterInput.d.ts.map +1 -1
- package/dist/react/fields/RepeaterInput.js +22 -127
- package/dist/react/fields/RepeaterInput.js.map +1 -1
- package/dist/react/fields/rowState.d.ts +40 -0
- package/dist/react/fields/rowState.d.ts.map +1 -0
- package/dist/react/fields/rowState.js +60 -0
- package/dist/react/fields/rowState.js.map +1 -0
- package/dist/react/fields/useRowReorderDnd.d.ts +28 -0
- package/dist/react/fields/useRowReorderDnd.d.ts.map +1 -0
- package/dist/react/fields/useRowReorderDnd.js +51 -0
- package/dist/react/fields/useRowReorderDnd.js.map +1 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/layouts/SidebarLayout.d.ts +1 -1
- package/dist/react/layouts/SidebarLayout.d.ts.map +1 -1
- package/dist/react/layouts/SidebarLayout.js +10 -2
- package/dist/react/layouts/SidebarLayout.js.map +1 -1
- package/dist/react/layouts/TopbarLayout.d.ts +1 -1
- package/dist/react/layouts/TopbarLayout.d.ts.map +1 -1
- package/dist/react/layouts/TopbarLayout.js +19 -11
- package/dist/react/layouts/TopbarLayout.js.map +1 -1
- package/dist/react/parseRecordEditUrl.d.ts +29 -0
- package/dist/react/parseRecordEditUrl.d.ts.map +1 -0
- package/dist/react/parseRecordEditUrl.js +25 -0
- package/dist/react/parseRecordEditUrl.js.map +1 -0
- package/dist/react/persistedState.d.ts +19 -0
- package/dist/react/persistedState.d.ts.map +1 -0
- package/dist/react/persistedState.js +51 -0
- package/dist/react/persistedState.js.map +1 -0
- package/dist/react/schemaRenderer/AlertRenderer.d.ts +12 -0
- package/dist/react/schemaRenderer/AlertRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/AlertRenderer.js +61 -0
- package/dist/react/schemaRenderer/AlertRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/EntryRenderer.d.ts +13 -0
- package/dist/react/schemaRenderer/EntryRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/EntryRenderer.js +277 -0
- package/dist/react/schemaRenderer/EntryRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/SectionRenderer.d.ts +16 -0
- package/dist/react/schemaRenderer/SectionRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/SectionRenderer.js +62 -0
- package/dist/react/schemaRenderer/SectionRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/SimpleElements.d.ts +25 -0
- package/dist/react/schemaRenderer/SimpleElements.d.ts.map +1 -0
- package/dist/react/schemaRenderer/SimpleElements.js +147 -0
- package/dist/react/schemaRenderer/SimpleElements.js.map +1 -0
- package/dist/react/schemaRenderer/TabsRenderer.d.ts +17 -0
- package/dist/react/schemaRenderer/TabsRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/TabsRenderer.js +31 -0
- package/dist/react/schemaRenderer/TabsRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/WizardRenderer.d.ts +34 -0
- package/dist/react/schemaRenderer/WizardRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/WizardRenderer.js +208 -0
- package/dist/react/schemaRenderer/WizardRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts +21 -0
- package/dist/react/schemaRenderer/action/ActionGroupTrigger.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/ActionGroupTrigger.js +82 -0
- package/dist/react/schemaRenderer/action/ActionGroupTrigger.js.map +1 -0
- package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts +30 -0
- package/dist/react/schemaRenderer/action/ActionModalDialog.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/ActionModalDialog.js +182 -0
- package/dist/react/schemaRenderer/action/ActionModalDialog.js.map +1 -0
- package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts +17 -0
- package/dist/react/schemaRenderer/action/ConfirmActionDialog.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/ConfirmActionDialog.js +19 -0
- package/dist/react/schemaRenderer/action/ConfirmActionDialog.js.map +1 -0
- package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts +16 -0
- package/dist/react/schemaRenderer/action/HandlerActionButton.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/HandlerActionButton.js +16 -0
- package/dist/react/schemaRenderer/action/HandlerActionButton.js.map +1 -0
- package/dist/react/schemaRenderer/action/MethodActionButton.d.ts +22 -0
- package/dist/react/schemaRenderer/action/MethodActionButton.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/MethodActionButton.js +26 -0
- package/dist/react/schemaRenderer/action/MethodActionButton.js.map +1 -0
- package/dist/react/schemaRenderer/action/buttons.d.ts +18 -0
- package/dist/react/schemaRenderer/action/buttons.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/buttons.js +74 -0
- package/dist/react/schemaRenderer/action/buttons.js.map +1 -0
- package/dist/react/schemaRenderer/action/helpers.d.ts +26 -0
- package/dist/react/schemaRenderer/action/helpers.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/helpers.js +126 -0
- package/dist/react/schemaRenderer/action/helpers.js.map +1 -0
- package/dist/react/schemaRenderer/action/renderAction.d.ts +21 -0
- package/dist/react/schemaRenderer/action/renderAction.d.ts.map +1 -0
- package/dist/react/schemaRenderer/action/renderAction.js +102 -0
- package/dist/react/schemaRenderer/action/renderAction.js.map +1 -0
- package/dist/react/schemaRenderer/columnFormat.d.ts +10 -0
- package/dist/react/schemaRenderer/columnFormat.d.ts.map +1 -0
- package/dist/react/schemaRenderer/columnFormat.js +76 -0
- package/dist/react/schemaRenderer/columnFormat.js.map +1 -0
- package/dist/react/schemaRenderer/constants.d.ts +8 -0
- package/dist/react/schemaRenderer/constants.d.ts.map +1 -0
- package/dist/react/schemaRenderer/constants.js +45 -0
- package/dist/react/schemaRenderer/constants.js.map +1 -0
- package/dist/react/schemaRenderer/form/FormRenderer.d.ts +29 -0
- package/dist/react/schemaRenderer/form/FormRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/form/FormRenderer.js +163 -0
- package/dist/react/schemaRenderer/form/FormRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/form/renderField.d.ts +6 -0
- package/dist/react/schemaRenderer/form/renderField.d.ts.map +1 -0
- package/dist/react/schemaRenderer/form/renderField.js +239 -0
- package/dist/react/schemaRenderer/form/renderField.js.map +1 -0
- package/dist/react/schemaRenderer/helpers.d.ts +32 -0
- package/dist/react/schemaRenderer/helpers.d.ts.map +1 -0
- package/dist/react/schemaRenderer/helpers.js +52 -0
- package/dist/react/schemaRenderer/helpers.js.map +1 -0
- package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts +60 -0
- package/dist/react/schemaRenderer/table/CardsLayoutBody.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/CardsLayoutBody.js +189 -0
- package/dist/react/schemaRenderer/table/CardsLayoutBody.js.map +1 -0
- package/dist/react/schemaRenderer/table/TableRenderer.d.ts +29 -0
- package/dist/react/schemaRenderer/table/TableRenderer.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/TableRenderer.js +85 -0
- package/dist/react/schemaRenderer/table/TableRenderer.js.map +1 -0
- package/dist/react/schemaRenderer/table/TableRendererBody.d.ts +18 -0
- package/dist/react/schemaRenderer/table/TableRendererBody.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/TableRendererBody.js +555 -0
- package/dist/react/schemaRenderer/table/TableRendererBody.js.map +1 -0
- package/dist/react/schemaRenderer/table/filters.d.ts +263 -0
- package/dist/react/schemaRenderer/table/filters.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/filters.js +497 -0
- package/dist/react/schemaRenderer/table/filters.js.map +1 -0
- package/dist/react/schemaRenderer/table/formatCell.d.ts +11 -0
- package/dist/react/schemaRenderer/table/formatCell.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/formatCell.js +172 -0
- package/dist/react/schemaRenderer/table/formatCell.js.map +1 -0
- package/dist/react/schemaRenderer/table/links.d.ts +42 -0
- package/dist/react/schemaRenderer/table/links.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/links.js +55 -0
- package/dist/react/schemaRenderer/table/links.js.map +1 -0
- package/dist/react/schemaRenderer/table/renderRowActions.d.ts +13 -0
- package/dist/react/schemaRenderer/table/renderRowActions.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/renderRowActions.js +25 -0
- package/dist/react/schemaRenderer/table/renderRowActions.js.map +1 -0
- package/dist/react/schemaRenderer/table/url.d.ts +41 -0
- package/dist/react/schemaRenderer/table/url.d.ts.map +1 -0
- package/dist/react/schemaRenderer/table/url.js +114 -0
- package/dist/react/schemaRenderer/table/url.js.map +1 -0
- package/dist/routes/globals.d.ts +13 -0
- package/dist/routes/globals.d.ts.map +1 -0
- package/dist/routes/globals.js +131 -0
- package/dist/routes/globals.js.map +1 -0
- package/dist/routes/helpers.d.ts +217 -0
- package/dist/routes/helpers.d.ts.map +1 -0
- package/dist/routes/helpers.js +498 -0
- package/dist/routes/helpers.js.map +1 -0
- package/dist/routes/pages.d.ts +15 -0
- package/dist/routes/pages.d.ts.map +1 -0
- package/dist/routes/pages.js +145 -0
- package/dist/routes/pages.js.map +1 -0
- package/dist/routes/panel.d.ts +19 -0
- package/dist/routes/panel.d.ts.map +1 -0
- package/dist/routes/panel.js +191 -0
- package/dist/routes/panel.js.map +1 -0
- package/dist/routes/relations.d.ts +21 -0
- package/dist/routes/relations.d.ts.map +1 -0
- package/dist/routes/relations.js +1239 -0
- package/dist/routes/relations.js.map +1 -0
- package/dist/routes/resources.d.ts +28 -0
- package/dist/routes/resources.d.ts.map +1 -0
- package/dist/routes/resources.js +741 -0
- package/dist/routes/resources.js.map +1 -0
- package/dist/routes/theme.d.ts +12 -0
- package/dist/routes/theme.d.ts.map +1 -0
- package/dist/routes/theme.js +82 -0
- package/dist/routes/theme.js.map +1 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +64 -3078
- package/dist/routes.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +26 -5
- package/dist/vite.js.map +1 -1
- package/package.json +2 -1
- package/src/Pilotiq.ts +95 -0
- package/src/actions/Action.ts +79 -723
- package/src/actions/bulkFactories.ts +168 -0
- package/src/actions/crudFactories.ts +220 -0
- package/src/actions/factoryHelpers.ts +177 -0
- package/src/actions/m2mFactories.ts +193 -0
- package/src/actions/relationFactories.ts +372 -0
- package/src/elements/dispatchForm.ts +1 -1
- package/src/elements/dispatchTable.ts +1 -1
- package/src/fields/Field.ts +39 -0
- package/src/pageData/breadcrumbs.ts +288 -0
- package/src/pageData/forms.ts +578 -0
- package/src/pageData/helpers.ts +764 -0
- package/src/pageData/misc.ts +347 -0
- package/src/pageData/navigation.ts +779 -0
- package/src/pageData/relationPages.ts +1246 -0
- package/src/pageData/relationTabs.ts +286 -0
- package/src/pageData/resourcePages.ts +593 -0
- package/src/pageData.ts +122 -4731
- package/src/react/AppShell.tsx +27 -1
- package/src/react/CollabExtensionFactoryRegistry.ts +55 -0
- package/src/react/CollabRoomContext.ts +42 -0
- package/src/react/FormCollabBindingRegistry.ts +72 -0
- package/src/react/FormStateContext.tsx +91 -0
- package/src/react/RecordWrapperGate.tsx +40 -0
- package/src/react/RecordWrapperRegistry.ts +39 -0
- package/src/react/SchemaRenderer.tsx +230 -6479
- package/src/react/component-slots.test.ts +103 -0
- package/src/react/component-slots.ts +116 -0
- package/src/react/fields/BuilderInput.tsx +29 -117
- package/src/react/fields/MarkdownInput.tsx +0 -1
- package/src/react/fields/RepeaterInput.tsx +29 -130
- package/src/react/fields/rowState.ts +106 -0
- package/src/react/fields/useRowReorderDnd.ts +78 -0
- package/src/react/index.ts +38 -0
- package/src/react/layouts/SidebarLayout.tsx +39 -28
- package/src/react/layouts/TopbarLayout.tsx +70 -57
- package/src/react/parseRecordEditUrl.test.ts +75 -0
- package/src/react/parseRecordEditUrl.ts +55 -0
- package/src/react/persistedState.ts +40 -0
- package/src/react/schemaRenderer/AlertRenderer.tsx +112 -0
- package/src/react/schemaRenderer/EntryRenderer.tsx +501 -0
- package/src/react/schemaRenderer/SectionRenderer.tsx +120 -0
- package/src/react/schemaRenderer/SimpleElements.tsx +306 -0
- package/src/react/schemaRenderer/TabsRenderer.tsx +62 -0
- package/src/react/schemaRenderer/WizardRenderer.tsx +338 -0
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +177 -0
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +273 -0
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +61 -0
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +43 -0
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +64 -0
- package/src/react/schemaRenderer/action/buttons.tsx +99 -0
- package/src/react/schemaRenderer/action/helpers.ts +140 -0
- package/src/react/schemaRenderer/action/renderAction.tsx +245 -0
- package/src/react/schemaRenderer/columnFormat.ts +65 -0
- package/src/react/schemaRenderer/constants.ts +50 -0
- package/src/react/schemaRenderer/form/FormRenderer.tsx +245 -0
- package/src/react/schemaRenderer/form/renderField.tsx +511 -0
- package/src/react/schemaRenderer/helpers.tsx +81 -0
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +308 -0
- package/src/react/schemaRenderer/table/TableRenderer.tsx +123 -0
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +974 -0
- package/src/react/schemaRenderer/table/filters.tsx +1233 -0
- package/src/react/schemaRenderer/table/formatCell.tsx +264 -0
- package/src/react/schemaRenderer/table/links.tsx +112 -0
- package/src/react/schemaRenderer/table/renderRowActions.tsx +52 -0
- package/src/react/schemaRenderer/table/url.tsx +143 -0
- package/src/routes/globals.ts +154 -0
- package/src/routes/helpers.ts +668 -0
- package/src/routes/pages.ts +173 -0
- package/src/routes/panel.ts +204 -0
- package/src/routes/relations.ts +1219 -0
- package/src/routes/resources.ts +786 -0
- package/src/routes/theme.ts +109 -0
- package/src/routes.test.ts +1 -1
- package/src/routes.ts +64 -3176
- package/src/schema/TableWidget.test.ts +2 -2
- package/src/theme/migrate.test.ts +178 -0
- package/src/vite.test.ts +184 -0
- package/src/vite.ts +26 -4
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M2M relation Action factories — `relationAttach` (header, modal-form
|
|
3
|
+
* picker → POST `_action/relationAttach`), `relationDetach` (row, direct
|
|
4
|
+
* POST to `_detach/:childId`), `relationBulkDetach` (bulk, handler-
|
|
5
|
+
* dispatched).
|
|
6
|
+
*
|
|
7
|
+
* Sibling of `relationCreate / Edit / Delete` for every M2M mode
|
|
8
|
+
* (`belongsToMany`, `morphToMany` (owning polymorphic side),
|
|
9
|
+
* `morphedByMany` (inverse polymorphic side)). All three modes share
|
|
10
|
+
* the same `attach` / `detach` / `sync` accessor surface — the rudder
|
|
11
|
+
* ORM stamps + filters the polymorphic discriminator on the morph
|
|
12
|
+
* variants automatically, so pilotiq's pivot factories are mode-agnostic
|
|
13
|
+
* beyond the visibility gate.
|
|
14
|
+
*
|
|
15
|
+
* All three auto-hide outside any M2M mode so dropping a factory into
|
|
16
|
+
* a non-M2M manager is a no-op (visible=false) instead of a confusing
|
|
17
|
+
* 404.
|
|
18
|
+
*
|
|
19
|
+
* The first and third route through the manager-scoped
|
|
20
|
+
* `_action/:actionName` endpoint (added in routes.ts) so handlers
|
|
21
|
+
* see `ctx.relation = { parent, parentId, relationship }`.
|
|
22
|
+
*
|
|
23
|
+
* See `docs/plans/action-split.md` for the split plan.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Action, type ActionResult } from './Action.js'
|
|
27
|
+
import {
|
|
28
|
+
safeManagerPolicy,
|
|
29
|
+
type RelationManager,
|
|
30
|
+
type RelationManagerContext,
|
|
31
|
+
} from '../RelationManager.js'
|
|
32
|
+
import { resolveM2MAccessor } from '../orm/m2mAccessor.js'
|
|
33
|
+
import { buildAttachModalSchema } from './attachFactory.js'
|
|
34
|
+
import { isM2MMode, relationUrlPrefix } from './factoryHelpers.js'
|
|
35
|
+
|
|
36
|
+
/** Resolve the M2M accessor on `rel.parent`, null-check the requested
|
|
37
|
+
* method, run it, and shape the per-failure-mode error envelope.
|
|
38
|
+
* Used by `relationAttachAction` and `relationBulkDetachAction` —
|
|
39
|
+
* both follow the same pattern (resolve → null-check method →
|
|
40
|
+
* try/catch). Keeps the "Pivot accessor missing on …" error string
|
|
41
|
+
* consistent across both call sites. */
|
|
42
|
+
async function callM2MAccessor(
|
|
43
|
+
rel: { parent: unknown; relationship: string },
|
|
44
|
+
method: 'attach' | 'detach',
|
|
45
|
+
ids: string[],
|
|
46
|
+
failureLabel: string,
|
|
47
|
+
): Promise<{ ok: true } | { ok: false; result: ActionResult }> {
|
|
48
|
+
const accessor = resolveM2MAccessor(rel.parent, rel.relationship) as
|
|
49
|
+
| { attach?: (ids: string[]) => Promise<unknown>; detach?: (ids: string[]) => Promise<unknown> }
|
|
50
|
+
| null
|
|
51
|
+
const fn = accessor?.[method]
|
|
52
|
+
if (typeof fn !== 'function') {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
result: { notify: { title: `Pivot accessor missing on ${rel.relationship} — wrong relation type or ORM version?`, type: 'error' } as never },
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await fn(ids)
|
|
60
|
+
return { ok: true }
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
result: { notify: { title: `${failureLabel}: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never },
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Header-placement attach factory — opens a modal with a SelectField
|
|
70
|
+
* listing related records that aren't already attached, and POSTs the
|
|
71
|
+
* selected id to the manager's `_action/relationAttach` endpoint.
|
|
72
|
+
*
|
|
73
|
+
* Visibility delegates to `M.canAttach(user, parentRecord)` AND
|
|
74
|
+
* guards against being dropped into a non-M2M manager. */
|
|
75
|
+
export function relationAttachAction(
|
|
76
|
+
M: typeof RelationManager,
|
|
77
|
+
ctx: RelationManagerContext,
|
|
78
|
+
): Action {
|
|
79
|
+
const labelSingular = M.getLabelSingular()
|
|
80
|
+
const a = Action.make('relationAttach')
|
|
81
|
+
.label(`Attach ${labelSingular}`)
|
|
82
|
+
.header()
|
|
83
|
+
.modalHeading(`Attach ${labelSingular}`)
|
|
84
|
+
.modalSubmitLabel('Attach')
|
|
85
|
+
.modalCancelLabel('Cancel')
|
|
86
|
+
.handler(async (hctx) => {
|
|
87
|
+
const rel = hctx.relation
|
|
88
|
+
if (!rel) {
|
|
89
|
+
return { notify: { title: 'Attach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
|
|
90
|
+
}
|
|
91
|
+
const Related = ctx.related
|
|
92
|
+
if (!Related?.model) {
|
|
93
|
+
return { notify: { title: 'Cannot attach: related Resource has no model', type: 'error' } as never }
|
|
94
|
+
}
|
|
95
|
+
const idStr = String((hctx.values?.['_attachId'] as unknown) ?? '')
|
|
96
|
+
if (idStr.length === 0) {
|
|
97
|
+
return { notify: { title: 'Pick a record to attach', type: 'error' } as never }
|
|
98
|
+
}
|
|
99
|
+
const call = await callM2MAccessor(rel, 'attach', [idStr], 'Attach failed')
|
|
100
|
+
if (!call.ok) return call.result
|
|
101
|
+
return { notify: { title: `${labelSingular} attached`, type: 'success' } as never }
|
|
102
|
+
})
|
|
103
|
+
.visible(({ user }) => {
|
|
104
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
105
|
+
return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Build the modal-form schema only when this is actually an M2M
|
|
109
|
+
// manager — non-M2M drops keep the action hidden via the visibility
|
|
110
|
+
// predicate, but still need a schema-less Action so the meta walker
|
|
111
|
+
// doesn't blow up. Static import is fine: `attachFactory` only
|
|
112
|
+
// depends on `SelectField` + ORM helpers, no cycle back to Action.
|
|
113
|
+
if (isM2MMode(ctx.mode) && ctx.related?.model) {
|
|
114
|
+
a.schema(buildAttachModalSchema({
|
|
115
|
+
Related: ctx.related,
|
|
116
|
+
relationship: ctx.relationship,
|
|
117
|
+
recordTitleAttr: M.getRecordTitleAttribute() ?? ctx.related.recordTitleAttribute,
|
|
118
|
+
labelSingular,
|
|
119
|
+
}))
|
|
120
|
+
}
|
|
121
|
+
return a
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Row-placement detach factory — POSTs to
|
|
125
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/_detach`,
|
|
126
|
+
* destructive style with a confirmation prompt that says "Detach"
|
|
127
|
+
* (not "Delete") so users understand the target record stays.
|
|
128
|
+
* Visibility delegates to `M.canDetach`. */
|
|
129
|
+
export function relationDetachAction(
|
|
130
|
+
M: typeof RelationManager,
|
|
131
|
+
ctx: RelationManagerContext,
|
|
132
|
+
recordId?: string,
|
|
133
|
+
): Action {
|
|
134
|
+
const id = recordId ?? ':id'
|
|
135
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
136
|
+
return Action.make('relationDetach')
|
|
137
|
+
.label('Detach')
|
|
138
|
+
.destructive()
|
|
139
|
+
.method('post')
|
|
140
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/_detach`)
|
|
141
|
+
.confirm(`Detach this ${singular}? The ${singular} record stays in place; only the link is removed.`)
|
|
142
|
+
.visible(async ({ user, record }) => {
|
|
143
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
144
|
+
return safeManagerPolicy(M, 'canDetach', ctx.related, user, ctx.parentRecord, record)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Bulk-placement bulk-detach factory — handler-dispatched. Calls
|
|
149
|
+
* `parent.related(rel).detach(ids)` for the selected rows. Visibility
|
|
150
|
+
* delegates to `M.canAttach` (acts like a "manager admin" gate; we
|
|
151
|
+
* intentionally don't enforce per-row `canDetach` on the visibility
|
|
152
|
+
* side because the bulk button needs to be visible before the user
|
|
153
|
+
* has selected anything — per-row gating happens inside the handler). */
|
|
154
|
+
export function relationBulkDetachAction(
|
|
155
|
+
M: typeof RelationManager,
|
|
156
|
+
ctx: RelationManagerContext,
|
|
157
|
+
): Action {
|
|
158
|
+
const labelPlural = M.getLabel().toLowerCase()
|
|
159
|
+
return Action.make('relationBulkDetach')
|
|
160
|
+
.label('Detach selected')
|
|
161
|
+
.destructive()
|
|
162
|
+
.bulk()
|
|
163
|
+
.confirm(`Detach the selected ${labelPlural}? The records stay in place; only the links are removed.`)
|
|
164
|
+
.handler(async (hctx) => {
|
|
165
|
+
const rel = hctx.relation
|
|
166
|
+
if (!rel) {
|
|
167
|
+
return { notify: { title: 'Bulk-detach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
|
|
168
|
+
}
|
|
169
|
+
const records = hctx.records ?? []
|
|
170
|
+
// Parallelize the per-row policy probes; the accessor call itself stays a single batched op.
|
|
171
|
+
const allowedFlags = await Promise.all(
|
|
172
|
+
records.map(r => safeManagerPolicy(M, 'canDetach', ctx.related, hctx.user, ctx.parentRecord, r)),
|
|
173
|
+
)
|
|
174
|
+
const ids: string[] = []
|
|
175
|
+
for (let i = 0; i < records.length; i++) {
|
|
176
|
+
if (!allowedFlags[i]) continue
|
|
177
|
+
const id = String((records[i] as { id?: unknown }).id ?? '')
|
|
178
|
+
if (id) ids.push(id)
|
|
179
|
+
}
|
|
180
|
+
if (ids.length === 0) {
|
|
181
|
+
return { notify: { title: 'Nothing to detach (no permitted rows)', type: 'warning' } as never }
|
|
182
|
+
}
|
|
183
|
+
const call = await callM2MAccessor(rel, 'detach', ids, 'Bulk detach failed')
|
|
184
|
+
if (!call.ok) return call.result
|
|
185
|
+
return { notify: { title: `${ids.length} ${labelPlural} detached`, type: 'success' } as never }
|
|
186
|
+
})
|
|
187
|
+
.visible(({ user }) => {
|
|
188
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
189
|
+
// Bulk gate uses canAttach as a stand-in for "manager admin" —
|
|
190
|
+
// per-row canDetach is enforced inside the handler.
|
|
191
|
+
return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation-manager Action factories — `relationCreate / relationEdit /
|
|
3
|
+
* relationDelete / relationRestore / relationForceDelete` (hasMany +
|
|
4
|
+
* morphMany) and `relationReplicate / relationBulkReplicate` (clone
|
|
5
|
+
* with force-pinned parent attachment).
|
|
6
|
+
*
|
|
7
|
+
* Designed to be called inside `RelationManager.static table()` — the
|
|
8
|
+
* page-data builder pipes `RelationManagerContext` into that
|
|
9
|
+
* configurator so users get `basePath`, `parentId`, and the discovered
|
|
10
|
+
* Related resource without threading them by hand.
|
|
11
|
+
*
|
|
12
|
+
* Visibility predicates use `safeManagerPolicy` so the manager's
|
|
13
|
+
* `canX` (when overridden) wins, otherwise falls through to the
|
|
14
|
+
* related Resource's `canX`. Throws absorb as `false`.
|
|
15
|
+
*
|
|
16
|
+
* M2M factories (`relationAttach / relationDetach / relationBulkDetach`)
|
|
17
|
+
* live in `m2mFactories.ts`.
|
|
18
|
+
*
|
|
19
|
+
* See `docs/plans/action-split.md` for the split plan.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
Action,
|
|
24
|
+
type ActionContext,
|
|
25
|
+
type ActionResult,
|
|
26
|
+
type ReplicateOptions,
|
|
27
|
+
type ResourceLike,
|
|
28
|
+
} from './Action.js'
|
|
29
|
+
import {
|
|
30
|
+
safeManagerPolicy,
|
|
31
|
+
type RelationManager,
|
|
32
|
+
type RelationManagerContext,
|
|
33
|
+
} from '../RelationManager.js'
|
|
34
|
+
import {
|
|
35
|
+
computeMorphPayload,
|
|
36
|
+
getMorphRelationDescriptor,
|
|
37
|
+
getParentRelationDescriptor,
|
|
38
|
+
type ModelLike,
|
|
39
|
+
} from '../orm/modelDefaults.js'
|
|
40
|
+
import { buildReplica, forEachAllowed, isM2MMode, isTrashed, relationUrlPrefix } from './factoryHelpers.js'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute the parent-attachment payload to force-pin onto a relation
|
|
44
|
+
* replica. For `hasMany`, returns `{ [foreignKey]: parentId }` from the
|
|
45
|
+
* parent's `static relations[name]` descriptor. For `morphMany` /
|
|
46
|
+
* `morphOne`, returns `{ <morphName>Id, <morphName>Type }` via
|
|
47
|
+
* `computeMorphPayload(parentRecord)`. Returns `{}` when no descriptor
|
|
48
|
+
* matches — the route dispatcher already auto-hides under M2M / morphTo,
|
|
49
|
+
* so missing descriptors there are a no-op rather than an error. Pure;
|
|
50
|
+
* exported for tests and re-used by both factories.
|
|
51
|
+
*/
|
|
52
|
+
function computeRelationPin(
|
|
53
|
+
ctx: RelationManagerContext,
|
|
54
|
+
): Record<string, unknown> {
|
|
55
|
+
const parentModel = (ctx.parentRecord as { constructor?: ModelLike } | null | undefined)?.constructor
|
|
56
|
+
if (!parentModel) return {}
|
|
57
|
+
const rel = ctx.relationship
|
|
58
|
+
// Polymorphic owner side first — `morphMany` carries no foreignKey
|
|
59
|
+
// and would fail the hasMany descriptor's gate.
|
|
60
|
+
if (ctx.mode === 'morphMany') {
|
|
61
|
+
const morph = getMorphRelationDescriptor(parentModel, rel)
|
|
62
|
+
if (!morph) return {}
|
|
63
|
+
try { return computeMorphPayload(ctx.parentRecord, morph) }
|
|
64
|
+
catch { return {} }
|
|
65
|
+
}
|
|
66
|
+
const desc = getParentRelationDescriptor(parentModel, rel)
|
|
67
|
+
if (!desc) return {}
|
|
68
|
+
return { [desc.foreignKey]: ctx.parentId }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build + persist a single relation replica. Runs the strip set
|
|
73
|
+
* (PK + soft-delete column on the **related** Resource +
|
|
74
|
+
* `opts.excludeAttributes`), force-pins the parent attachment columns,
|
|
75
|
+
* runs the optional `beforeReplicaSaved` hook, and calls
|
|
76
|
+
* `Related.model.create(...)`. Returns the model's create result so
|
|
77
|
+
* callers can read its primary key for redirect targeting.
|
|
78
|
+
*
|
|
79
|
+
* Throws when the related Resource has no model — caller (single-row
|
|
80
|
+
* factory) catches and surfaces an error notification; bulk caller
|
|
81
|
+
* checks the model presence ahead of the loop.
|
|
82
|
+
*/
|
|
83
|
+
async function persistRelationReplica(
|
|
84
|
+
_M: typeof RelationManager,
|
|
85
|
+
ctx: RelationManagerContext,
|
|
86
|
+
source: unknown,
|
|
87
|
+
opts: ReplicateOptions,
|
|
88
|
+
): Promise<unknown> {
|
|
89
|
+
const Related = ctx.related
|
|
90
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
91
|
+
throw new Error('Related Resource has no model.create')
|
|
92
|
+
}
|
|
93
|
+
const M2 = Related.model as ModelLike
|
|
94
|
+
// Force-pin the parent attachment via `pin` so `beforeReplicaSaved` can
|
|
95
|
+
// still read / override the FK if it really wants to (rare); tampered
|
|
96
|
+
// source rows can't slip a different parent in by riding their own FK
|
|
97
|
+
// column — the pin overwrites whatever value was there.
|
|
98
|
+
const { replica } = await buildReplica(source, M2, {
|
|
99
|
+
excludeAttributes: opts.excludeAttributes,
|
|
100
|
+
deletedAtColumn: Related.deletedAtColumn,
|
|
101
|
+
pin: computeRelationPin(ctx),
|
|
102
|
+
beforeReplicaSaved: opts.beforeReplicaSaved,
|
|
103
|
+
})
|
|
104
|
+
return M2.create(replica)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Single-row dispatch for `relationReplicateAction`. Resolves
|
|
109
|
+
* `ctx.record` (loaded by the route's resolveRecord hook), validates,
|
|
110
|
+
* persists the replica, and shapes the success notification. Errors
|
|
111
|
+
* are caught and surfaced as error toasts.
|
|
112
|
+
*/
|
|
113
|
+
async function runRelationReplicateRow(
|
|
114
|
+
M: typeof RelationManager,
|
|
115
|
+
ctx: RelationManagerContext,
|
|
116
|
+
hctx: ActionContext,
|
|
117
|
+
opts: ReplicateOptions,
|
|
118
|
+
): Promise<ActionResult> {
|
|
119
|
+
const source = hctx.record
|
|
120
|
+
if (!source || typeof source !== 'object') {
|
|
121
|
+
return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
|
|
122
|
+
}
|
|
123
|
+
const Related = ctx.related
|
|
124
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
125
|
+
return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
|
|
126
|
+
}
|
|
127
|
+
let created: unknown
|
|
128
|
+
try {
|
|
129
|
+
created = await persistRelationReplica(M, ctx, source, opts)
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
132
|
+
}
|
|
133
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
134
|
+
? await opts.getCreatedNotificationTitle({ replica: created, source })
|
|
135
|
+
: undefined
|
|
136
|
+
const title = overrideTitle !== undefined ? overrideTitle : `${M.getLabelSingular()} replicated`
|
|
137
|
+
// The manager-scoped `_action/:actionName` route falls back to the
|
|
138
|
+
// manager list URL when `result.redirect` is undefined, so we only
|
|
139
|
+
// emit `redirect` when the user override returned a string. That
|
|
140
|
+
// way default behavior (route owns the fallback) is unchanged.
|
|
141
|
+
const overrideRedirect = opts.getRedirectUrl
|
|
142
|
+
? await opts.getRedirectUrl({ replica: created, source })
|
|
143
|
+
: undefined
|
|
144
|
+
return {
|
|
145
|
+
...(overrideRedirect !== undefined ? { redirect: overrideRedirect } : {}),
|
|
146
|
+
notify: { title, type: 'success' } as never,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Relation create-action factory — link to
|
|
151
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/create`.
|
|
152
|
+
*
|
|
153
|
+
* Visibility delegates to `M.canCreate(user, parentRecord)` (or the
|
|
154
|
+
* related Resource's `canCreate(user)` when the manager hasn't
|
|
155
|
+
* overridden). Drop into `headerActions([...])` from inside
|
|
156
|
+
* `RelationManager.table(table, ctx)`.
|
|
157
|
+
*/
|
|
158
|
+
export function relationCreateAction(
|
|
159
|
+
M: typeof RelationManager,
|
|
160
|
+
ctx: RelationManagerContext,
|
|
161
|
+
): Action {
|
|
162
|
+
const labelSingular = M.getLabelSingular()
|
|
163
|
+
return Action.make('create')
|
|
164
|
+
.label(`New ${labelSingular}`)
|
|
165
|
+
.href(`${relationUrlPrefix(ctx)}/create`)
|
|
166
|
+
.visible(({ user }) => {
|
|
167
|
+
// M2M managers don't have a per-pivot-row create surface — the
|
|
168
|
+
// related record is created via its own Resource, then attached
|
|
169
|
+
// via `relationAttach`. Auto-hide so dropping this factory into
|
|
170
|
+
// any M2M manager (belongsToMany / morphToMany / morphedByMany)
|
|
171
|
+
// is a no-op (visible=false) instead of a 404-on-click foot-gun.
|
|
172
|
+
if (isM2MMode(ctx.mode)) return false
|
|
173
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Relation edit-action factory — link to
|
|
178
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/edit`.
|
|
179
|
+
*
|
|
180
|
+
* Same `recordId` semantics as `editAction`: omit for row context
|
|
181
|
+
* so the renderer substitutes `:id` per row; pass explicitly when
|
|
182
|
+
* building actions for a single-record context. Visibility delegates
|
|
183
|
+
* to `M.canEdit(user, child, parentRecord)` with fall-through to the
|
|
184
|
+
* related Resource's `canEdit(user, record)`.
|
|
185
|
+
*/
|
|
186
|
+
export function relationEditAction(
|
|
187
|
+
M: typeof RelationManager,
|
|
188
|
+
ctx: RelationManagerContext,
|
|
189
|
+
recordId?: string,
|
|
190
|
+
): Action {
|
|
191
|
+
const id = recordId ?? ':id'
|
|
192
|
+
return Action.make('edit')
|
|
193
|
+
.label('Edit')
|
|
194
|
+
.href(`${relationUrlPrefix(ctx)}/${id}/edit`)
|
|
195
|
+
.visible(({ user, record }) => {
|
|
196
|
+
// M2M: per-pivot-row "edit" doesn't exist; users edit the
|
|
197
|
+
// related record via its own Resource. Auto-hide for every M2M
|
|
198
|
+
// mode (belongsToMany / morphToMany / morphedByMany).
|
|
199
|
+
if (isM2MMode(ctx.mode)) return false
|
|
200
|
+
return safeManagerPolicy(M, 'canEdit', ctx.related, user, ctx.parentRecord, record)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Relation delete-action factory — POST to
|
|
205
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/delete`,
|
|
206
|
+
* destructive style with a labeled confirmation. Visibility delegates
|
|
207
|
+
* to `M.canDelete(user, child, parentRecord)` with fall-through to the
|
|
208
|
+
* related Resource's `canDelete(user, record)`.
|
|
209
|
+
*/
|
|
210
|
+
export function relationDeleteAction(
|
|
211
|
+
M: typeof RelationManager,
|
|
212
|
+
ctx: RelationManagerContext,
|
|
213
|
+
recordId?: string,
|
|
214
|
+
): Action {
|
|
215
|
+
const id = recordId ?? ':id'
|
|
216
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
217
|
+
return Action.make('delete')
|
|
218
|
+
.label('Delete')
|
|
219
|
+
.destructive()
|
|
220
|
+
.method('post')
|
|
221
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/delete`)
|
|
222
|
+
.confirm(`Delete this ${singular}?`)
|
|
223
|
+
.visible(async ({ user, record }) => {
|
|
224
|
+
// M2M: "delete" of the related record is destructive in a way
|
|
225
|
+
// that "detach" isn't — surface only `relationDetach` on every
|
|
226
|
+
// M2M manager (belongsToMany / morphToMany / morphedByMany).
|
|
227
|
+
// Users who genuinely want to delete the related record reach
|
|
228
|
+
// for `Action.delete(R)` on the related Resource instead.
|
|
229
|
+
if (isM2MMode(ctx.mode)) return false
|
|
230
|
+
if (ctx.related?.softDeletes && isTrashed(record, ctx.related as ResourceLike)) return false
|
|
231
|
+
return safeManagerPolicy(M, 'canDelete', ctx.related, user, ctx.parentRecord, record)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Plan #13 polish — Restore factory for relation managers. POSTs to
|
|
237
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/restore`,
|
|
238
|
+
* success-styled, no confirm prompt. Auto-hides on live (non-trashed)
|
|
239
|
+
* rows AND when `M.canRestore` (or related Resource fall-through)
|
|
240
|
+
* denies. Drop into `recordActions([...])` from `RelationManager.table(table, ctx)`.
|
|
241
|
+
*/
|
|
242
|
+
export function relationRestoreAction(
|
|
243
|
+
M: typeof RelationManager,
|
|
244
|
+
ctx: RelationManagerContext,
|
|
245
|
+
recordId?: string,
|
|
246
|
+
): Action {
|
|
247
|
+
const id = recordId ?? ':id'
|
|
248
|
+
return Action.make('restore')
|
|
249
|
+
.label('Restore')
|
|
250
|
+
.color('success')
|
|
251
|
+
.method('post')
|
|
252
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/restore`)
|
|
253
|
+
.visible(async ({ user, record }) => {
|
|
254
|
+
if (!ctx.related?.softDeletes) return false
|
|
255
|
+
if (!isTrashed(record, ctx.related as ResourceLike)) return false
|
|
256
|
+
return safeManagerPolicy(M, 'canRestore', ctx.related, user, ctx.parentRecord, record)
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Plan #13 polish — Force-delete factory for relation managers. POSTs
|
|
262
|
+
* to `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/force-delete`,
|
|
263
|
+
* destructive style with a permanence-aware confirmation. Auto-hides on
|
|
264
|
+
* live (non-trashed) rows and when policy denies.
|
|
265
|
+
*/
|
|
266
|
+
export function relationForceDeleteAction(
|
|
267
|
+
M: typeof RelationManager,
|
|
268
|
+
ctx: RelationManagerContext,
|
|
269
|
+
recordId?: string,
|
|
270
|
+
): Action {
|
|
271
|
+
const id = recordId ?? ':id'
|
|
272
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
273
|
+
return Action.make('forceDelete')
|
|
274
|
+
.label('Delete forever')
|
|
275
|
+
.destructive()
|
|
276
|
+
.method('post')
|
|
277
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/force-delete`)
|
|
278
|
+
.confirm(`Permanently delete this ${singular}? This cannot be undone.`)
|
|
279
|
+
.visible(async ({ user, record }) => {
|
|
280
|
+
if (!ctx.related?.softDeletes) return false
|
|
281
|
+
if (!isTrashed(record, ctx.related as ResourceLike)) return false
|
|
282
|
+
return safeManagerPolicy(M, 'canForceDelete', ctx.related, user, ctx.parentRecord, record)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Relation row-replicate factory. Clones the row's child record
|
|
288
|
+
* inside the manager's parent scope.
|
|
289
|
+
*
|
|
290
|
+
* Strips the related model's primary key, soft-delete column, and
|
|
291
|
+
* `opts.excludeAttributes`. Re-applies the parent attachment columns
|
|
292
|
+
* after the strip + before the optional `beforeReplicaSaved` hook,
|
|
293
|
+
* so user code can still mutate non-FK fields without accidentally
|
|
294
|
+
* unlinking the replica.
|
|
295
|
+
*
|
|
296
|
+
* On success the manager-scoped route falls back to the manager
|
|
297
|
+
* list URL (`${base}/${parentSlug}/${parentId}/${relationship}`)
|
|
298
|
+
* because no explicit `redirect` is returned — same default as the
|
|
299
|
+
* other handler-style relation factories.
|
|
300
|
+
*
|
|
301
|
+
* `recordId` kept in the signature for parity with the rest of the
|
|
302
|
+
* relation factory family. The dispatcher resolves the source row
|
|
303
|
+
* from the request body, so it isn't referenced here.
|
|
304
|
+
*/
|
|
305
|
+
export function relationReplicateAction(
|
|
306
|
+
M: typeof RelationManager,
|
|
307
|
+
ctx: RelationManagerContext,
|
|
308
|
+
recordId?: string,
|
|
309
|
+
opts: ReplicateOptions = {},
|
|
310
|
+
): Action {
|
|
311
|
+
void recordId
|
|
312
|
+
return Action.make('relationReplicate')
|
|
313
|
+
.label('Replicate')
|
|
314
|
+
.row()
|
|
315
|
+
.handler(async (hctx) => {
|
|
316
|
+
const result = await runRelationReplicateRow(M, ctx, hctx, opts)
|
|
317
|
+
return result
|
|
318
|
+
})
|
|
319
|
+
.visible(({ user }) => {
|
|
320
|
+
if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
|
|
321
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Bulk sibling — replicates every selected child row inside the
|
|
327
|
+
* manager's parent scope. Same strip + force-pin pipeline applied
|
|
328
|
+
* per row. Per-row `safeManagerPolicy(M, 'canCreate', …)` runs
|
|
329
|
+
* inside the loop so a partially-permitted selection still proceeds
|
|
330
|
+
* for the rows that pass. Rows that throw are skipped silently —
|
|
331
|
+
* the toast count reflects only successful creates.
|
|
332
|
+
*/
|
|
333
|
+
export function relationBulkReplicateAction(
|
|
334
|
+
M: typeof RelationManager,
|
|
335
|
+
ctx: RelationManagerContext,
|
|
336
|
+
opts: ReplicateOptions = {},
|
|
337
|
+
): Action {
|
|
338
|
+
const labelPlural = M.getLabel().toLowerCase()
|
|
339
|
+
const labelSingular = M.getLabelSingular().toLowerCase()
|
|
340
|
+
return Action.make('relationBulkReplicate')
|
|
341
|
+
.label('Replicate selected')
|
|
342
|
+
.bulk()
|
|
343
|
+
.confirm(`Replicate the selected ${labelPlural}?`)
|
|
344
|
+
.handler(async (hctx) => {
|
|
345
|
+
const Related = ctx.related
|
|
346
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
347
|
+
return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
|
|
348
|
+
}
|
|
349
|
+
const records = hctx.records ?? []
|
|
350
|
+
// Per-row predicate eval preserved — users may write stateful predicates that gate per-attempt.
|
|
351
|
+
const n = await forEachAllowed(
|
|
352
|
+
records,
|
|
353
|
+
() => safeManagerPolicy(M, 'canCreate', Related, hctx.user, ctx.parentRecord),
|
|
354
|
+
async (_id, source) => { await persistRelationReplica(M, ctx, source, opts) },
|
|
355
|
+
)
|
|
356
|
+
if (n === 0) {
|
|
357
|
+
return { notify: { title: `Nothing to replicate (no permitted rows)`, type: 'warning' } as never }
|
|
358
|
+
}
|
|
359
|
+
const defaultTitle = `${n} ${n === 1 ? labelSingular : labelPlural} replicated`
|
|
360
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
361
|
+
? await opts.getCreatedNotificationTitle({ count: n, records })
|
|
362
|
+
: undefined
|
|
363
|
+
const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
|
|
364
|
+
return {
|
|
365
|
+
notify: { title, type: 'success' } as never,
|
|
366
|
+
}
|
|
367
|
+
})
|
|
368
|
+
.visible(({ user }) => {
|
|
369
|
+
if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
|
|
370
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
371
|
+
})
|
|
372
|
+
}
|
|
@@ -1646,7 +1646,7 @@ async function persistRelationshipRows(
|
|
|
1646
1646
|
keptPks.add(submittedId!)
|
|
1647
1647
|
if (afterUpdate) await afterUpdate(updatedRecord, buildRowCtx(idx))
|
|
1648
1648
|
} else {
|
|
1649
|
-
let createdRecord: unknown
|
|
1649
|
+
let createdRecord: unknown
|
|
1650
1650
|
if (attachment.kind === 'hasMany') {
|
|
1651
1651
|
payload[attachment.foreignKey] = parentPk
|
|
1652
1652
|
createdRecord = await model.create(payload)
|
|
@@ -568,7 +568,7 @@ export async function loadTableRecords(
|
|
|
568
568
|
// ONE auth call per row regardless of editable column count
|
|
569
569
|
// — same record, same answer. Failures or false → no edit
|
|
570
570
|
// affordance for any column.
|
|
571
|
-
let allowed
|
|
571
|
+
let allowed: boolean
|
|
572
572
|
try { allowed = await hooks!.canEdit!(user, recordObj) }
|
|
573
573
|
catch { allowed = false }
|
|
574
574
|
if (allowed) {
|
package/src/fields/Field.ts
CHANGED
|
@@ -111,6 +111,18 @@ export interface FieldMeta extends ElementMeta {
|
|
|
111
111
|
* `Column.formatStateUsing` from Plan #2).
|
|
112
112
|
*/
|
|
113
113
|
formattedValue?: string
|
|
114
|
+
/**
|
|
115
|
+
* Per-field collab override. Absent = inherit the panel-wide default
|
|
116
|
+
* (collab on every record-edit page when `@pilotiq-pro/collab` is
|
|
117
|
+
* registered). Explicit `false` opts THIS field out of the collab
|
|
118
|
+
* layer entirely — no value sync AND no presence chip — so the field
|
|
119
|
+
* stays device-local inside an otherwise collab-on form. Useful for
|
|
120
|
+
* sensitive scratch space or fields whose LWW semantics would
|
|
121
|
+
* surprise users (rapid typing in plain-text inputs). Forward-compat
|
|
122
|
+
* for `true` (opt in even when panel default is off) once
|
|
123
|
+
* panel-level disable lands.
|
|
124
|
+
*/
|
|
125
|
+
collab?: boolean
|
|
114
126
|
}
|
|
115
127
|
|
|
116
128
|
/**
|
|
@@ -278,6 +290,10 @@ export abstract class Field extends Element {
|
|
|
278
290
|
protected _hiddenOn?: ReadonlyArray<'table' | 'create' | 'edit' | 'view'>
|
|
279
291
|
protected _visibleOn?: ReadonlyArray<'table' | 'create' | 'edit' | 'view'>
|
|
280
292
|
|
|
293
|
+
// Per-field collab override. `undefined` = inherit panel default;
|
|
294
|
+
// explicit `false` = opt out of value sync AND presence entirely.
|
|
295
|
+
protected _collab?: boolean
|
|
296
|
+
|
|
281
297
|
constructor(name: string, type: FieldType) {
|
|
282
298
|
super()
|
|
283
299
|
this.name = name
|
|
@@ -477,6 +493,28 @@ export abstract class Field extends Element {
|
|
|
477
493
|
*/
|
|
478
494
|
dehydrated(value: boolean = true): this { this._dehydrated = value; return this }
|
|
479
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Per-field realtime-collab override.
|
|
498
|
+
*
|
|
499
|
+
* - `.collab(false)` — opts THIS field out of the collab layer
|
|
500
|
+
* entirely (no value sync via the form-level CRDT, no presence
|
|
501
|
+
* chip rendered next to the label). The field stays device-local
|
|
502
|
+
* inside an otherwise collab-on form. Useful for private scratch
|
|
503
|
+
* space or fields whose LWW semantics would surprise users
|
|
504
|
+
* (rapid typing in plain-text inputs hit the v1 last-writer-wins
|
|
505
|
+
* footgun — `.collab(false)` is the v1 escape hatch).
|
|
506
|
+
* - `.collab(true)` (default of the bare call) — explicit opt-in.
|
|
507
|
+
* Forward-compat for a future panel-level disable; today it
|
|
508
|
+
* behaves the same as not calling the setter when collab is
|
|
509
|
+
* already on at the panel level.
|
|
510
|
+
* - Not calling the setter — inherits the panel-wide default
|
|
511
|
+
* (collab on whenever `.plugins([collab()])` is registered).
|
|
512
|
+
*/
|
|
513
|
+
collab(enabled = true): this {
|
|
514
|
+
this._collab = enabled
|
|
515
|
+
return this
|
|
516
|
+
}
|
|
517
|
+
|
|
480
518
|
/**
|
|
481
519
|
* Display-time transform — receives `(value, { record })` and returns
|
|
482
520
|
* a string. Result lands on `FieldMeta.formattedValue`; renderers
|
|
@@ -769,6 +807,7 @@ export abstract class Field extends Element {
|
|
|
769
807
|
...(this._extraAttributes !== undefined ? { extraAttributes: this._extraAttributes } : {}),
|
|
770
808
|
...(this._extraInputAttributes !== undefined ? { extraInputAttributes: this._extraInputAttributes } : {}),
|
|
771
809
|
...(this._extraFieldWrapperAttributes !== undefined ? { extraFieldWrapperAttributes: this._extraFieldWrapperAttributes } : {}),
|
|
810
|
+
...(this._collab !== undefined ? { collab: this._collab } : {}),
|
|
772
811
|
}
|
|
773
812
|
}
|
|
774
813
|
|