@pilotiq/pilotiq 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +142 -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/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 +152 -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/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 +233 -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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bulk-placement Action factories — `bulkDelete / bulkRestore /
|
|
3
|
+
* bulkForceDelete / bulkReplicate`. Handler-style: iterate
|
|
4
|
+
* `ctx.records`, run policy per-row (parallelized via
|
|
5
|
+
* `forEachAllowed`), call the matching Resource / Model method. No new
|
|
6
|
+
* routes — the existing `/_action/:actionName` dispatcher already
|
|
7
|
+
* handles bulk via `ctx.records`.
|
|
8
|
+
*
|
|
9
|
+
* Drop into `bulkActions([...])` from inside `Resource.table()`. Each
|
|
10
|
+
* returns a notification with the count succeeded; rows whose policy
|
|
11
|
+
* denied (or whose call threw) are silently skipped — surface them
|
|
12
|
+
* via your own logging if needed. When no rows succeed (empty
|
|
13
|
+
* selection, all denied, all threw) the handler emits a `'warning'`
|
|
14
|
+
* toast instead of misleading "0 X deleted" success.
|
|
15
|
+
*
|
|
16
|
+
* See `docs/plans/action-split.md` for the split plan.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Action, type ReplicateOptions, type ResourceLike } from './Action.js'
|
|
20
|
+
import { buildReplica, callPredicate, forEachAllowed } from './factoryHelpers.js'
|
|
21
|
+
|
|
22
|
+
/** Pick the right label form for a count — `labelSingular` for 1,
|
|
23
|
+
* `label` (plural, lowercased) for any other count. Fall back to a
|
|
24
|
+
* naive `${labelSingular}s` when no plural label is set. Used by bulk
|
|
25
|
+
* notification copy so we don't ship "1 posts moved to trash". */
|
|
26
|
+
function labelForCount(R: ResourceLike, n: number): string {
|
|
27
|
+
if (n === 1) return R.labelSingular.toLowerCase()
|
|
28
|
+
const plural = R.label?.toLowerCase()
|
|
29
|
+
return plural ?? `${R.labelSingular.toLowerCase()}s`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Bulk delete — calls `R.deleteRecord(id)` per row. On a
|
|
33
|
+
* soft-delete resource that hits `Model.delete()` which writes
|
|
34
|
+
* `deletedAt`. Notification: "N posts moved to trash" / "N posts
|
|
35
|
+
* deleted" depending on `R.softDeletes`. */
|
|
36
|
+
export function bulkDeleteAction(R: ResourceLike, _basePath: string): Action {
|
|
37
|
+
return Action.make('bulkDelete')
|
|
38
|
+
.label('Delete selected')
|
|
39
|
+
.destructive()
|
|
40
|
+
.bulk()
|
|
41
|
+
.confirm(`Delete the selected ${labelForCount(R, 0)}?`)
|
|
42
|
+
.handler(async (ctx) => {
|
|
43
|
+
const records = ctx.records ?? []
|
|
44
|
+
const Rfull = R as ResourceLike & { deleteRecord(id: string): Promise<void> }
|
|
45
|
+
const verb = R.softDeletes ? 'moved to trash' : 'deleted'
|
|
46
|
+
const n = await forEachAllowed(
|
|
47
|
+
records,
|
|
48
|
+
(record) => callPredicate(R.canDelete, ctx.user, record),
|
|
49
|
+
async (id) => { await Rfull.deleteRecord(id) },
|
|
50
|
+
)
|
|
51
|
+
if (n === 0) {
|
|
52
|
+
return { notify: { title: `Nothing to delete (no permitted rows)`, type: 'warning' } as never }
|
|
53
|
+
}
|
|
54
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} ${verb}`, type: 'success' } as never }
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Bulk restore — calls `R.model.restore(id)` per row. Visible only
|
|
59
|
+
* on soft-delete resources (the entire bulk-restore concept is
|
|
60
|
+
* specific to them). */
|
|
61
|
+
export function bulkRestoreAction(R: ResourceLike, _basePath: string): Action {
|
|
62
|
+
return Action.make('bulkRestore')
|
|
63
|
+
.label('Restore selected')
|
|
64
|
+
.color('success')
|
|
65
|
+
.bulk()
|
|
66
|
+
.confirm(`Restore the selected ${labelForCount(R, 0)}?`)
|
|
67
|
+
.handler(async (ctx) => {
|
|
68
|
+
const Rfull = R as ResourceLike & { model?: { restore?(id: string | number): Promise<unknown> } }
|
|
69
|
+
const restore = Rfull.model?.restore
|
|
70
|
+
if (!restore) {
|
|
71
|
+
return { notify: { title: 'Restore not configured', type: 'error' } as never }
|
|
72
|
+
}
|
|
73
|
+
const records = ctx.records ?? []
|
|
74
|
+
const n = await forEachAllowed(
|
|
75
|
+
records,
|
|
76
|
+
(record) => callPredicate(R.canRestore, ctx.user, record),
|
|
77
|
+
async (id) => { await restore(id) },
|
|
78
|
+
)
|
|
79
|
+
if (n === 0) {
|
|
80
|
+
return { notify: { title: `Nothing to restore (no permitted rows)`, type: 'warning' } as never }
|
|
81
|
+
}
|
|
82
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} restored`, type: 'success' } as never }
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Bulk force-delete — calls `R.model.forceDelete(id)` per row. Same
|
|
87
|
+
* destructive confirm as the per-row variant. Visible only on
|
|
88
|
+
* soft-delete resources. */
|
|
89
|
+
export function bulkForceDeleteAction(R: ResourceLike, _basePath: string): Action {
|
|
90
|
+
return Action.make('bulkForceDelete')
|
|
91
|
+
.label('Delete forever')
|
|
92
|
+
.destructive()
|
|
93
|
+
.bulk()
|
|
94
|
+
.confirm(`Permanently delete the selected ${labelForCount(R, 0)}? This cannot be undone.`)
|
|
95
|
+
.handler(async (ctx) => {
|
|
96
|
+
const Rfull = R as ResourceLike & { model?: { forceDelete?(id: string | number): Promise<void> } }
|
|
97
|
+
const forceDelete = Rfull.model?.forceDelete
|
|
98
|
+
if (!forceDelete) {
|
|
99
|
+
return { notify: { title: 'Force-delete not configured', type: 'error' } as never }
|
|
100
|
+
}
|
|
101
|
+
const records = ctx.records ?? []
|
|
102
|
+
const n = await forEachAllowed(
|
|
103
|
+
records,
|
|
104
|
+
(record) => callPredicate(R.canForceDelete, ctx.user, record),
|
|
105
|
+
async (id) => { await forceDelete(id) },
|
|
106
|
+
)
|
|
107
|
+
if (n === 0) {
|
|
108
|
+
return { notify: { title: `Nothing to delete (no permitted rows)`, type: 'warning' } as never }
|
|
109
|
+
}
|
|
110
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} permanently deleted`, type: 'success' } as never }
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Bulk replicate — calls `R.model.create(...)` once per selected row
|
|
116
|
+
* with the source row's attributes minus PK / soft-delete column /
|
|
117
|
+
* `opts.excludeAttributes`. Optional `opts.beforeReplicaSaved(replica,
|
|
118
|
+
* source)` runs per-row. Rows that throw during create are skipped
|
|
119
|
+
* silently so a single bad row doesn't abort the batch (the user sees
|
|
120
|
+
* the success count on the toast). Visibility delegates to
|
|
121
|
+
* `R.canCreate(user)`.
|
|
122
|
+
*
|
|
123
|
+
* Sibling of `replicateAction` — same options bag, same strip set,
|
|
124
|
+
* same authorization gate. Stays on the list page (no per-row
|
|
125
|
+
* redirect possible for N rows).
|
|
126
|
+
*/
|
|
127
|
+
export function bulkReplicateAction(
|
|
128
|
+
R: ResourceLike,
|
|
129
|
+
_basePath: string,
|
|
130
|
+
opts: ReplicateOptions = {},
|
|
131
|
+
): Action {
|
|
132
|
+
return Action.make('bulkReplicate')
|
|
133
|
+
.label('Replicate selected')
|
|
134
|
+
.bulk()
|
|
135
|
+
.confirm(`Replicate the selected ${labelForCount(R, 0)}?`)
|
|
136
|
+
.handler(async (ctx) => {
|
|
137
|
+
const M = R.model
|
|
138
|
+
if (!M || typeof M.create !== 'function') {
|
|
139
|
+
return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
|
|
140
|
+
}
|
|
141
|
+
const records = ctx.records ?? []
|
|
142
|
+
// Per-row predicate eval preserved — `canCreate` ignores the record
|
|
143
|
+
// in the common case, but users may write stateful predicates that
|
|
144
|
+
// gate per-attempt.
|
|
145
|
+
const n = await forEachAllowed(
|
|
146
|
+
records,
|
|
147
|
+
() => callPredicate(R.canCreate, ctx.user),
|
|
148
|
+
async (_id, source) => {
|
|
149
|
+
const { replica } = await buildReplica(source, M, {
|
|
150
|
+
excludeAttributes: opts.excludeAttributes,
|
|
151
|
+
deletedAtColumn: R.deletedAtColumn,
|
|
152
|
+
beforeReplicaSaved: opts.beforeReplicaSaved,
|
|
153
|
+
})
|
|
154
|
+
await M.create(replica)
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
if (n === 0) {
|
|
158
|
+
return { notify: { title: `Nothing to replicate (no permitted rows)`, type: 'warning' } as never }
|
|
159
|
+
}
|
|
160
|
+
const defaultTitle = `${n} ${labelForCount(R, n)} replicated`
|
|
161
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
162
|
+
? await opts.getCreatedNotificationTitle({ count: n, records })
|
|
163
|
+
: undefined
|
|
164
|
+
const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
|
|
165
|
+
return { notify: { title, type: 'success' } as never }
|
|
166
|
+
})
|
|
167
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
168
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRUD single-row Action factories — `create / edit / view / delete /
|
|
3
|
+
* replicate / restore / forceDelete`, plus the notification `markAsRead`
|
|
4
|
+
* factory.
|
|
5
|
+
*
|
|
6
|
+
* Each factory builds and returns a configured `Action`. The matching
|
|
7
|
+
* `static` methods on the `Action` class are thin delegators that call
|
|
8
|
+
* into these functions, preserving the public `Action.create(R, base)`
|
|
9
|
+
* call shape.
|
|
10
|
+
*
|
|
11
|
+
* See `docs/plans/action-split.md` for the split plan.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Action, type ReplicateOptions, type ResourceLike } from './Action.js'
|
|
15
|
+
import { buildReplica, callPredicate, isTrashed, resourceBase } from './factoryHelpers.js'
|
|
16
|
+
|
|
17
|
+
/** Create-action factory — link to `${basePath}/${R.slug}/create`.
|
|
18
|
+
* Auto-hides when `R.canCreate(user)` returns false. */
|
|
19
|
+
export function createAction(R: ResourceLike, basePath: string): Action {
|
|
20
|
+
return Action.make('create')
|
|
21
|
+
.label(`New ${R.labelSingular}`)
|
|
22
|
+
.href(`${resourceBase(basePath, R)}/create`)
|
|
23
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Edit-action factory — link to the resource's edit page.
|
|
28
|
+
*
|
|
29
|
+
* Pass `recordId` when building actions for a single-record context
|
|
30
|
+
* (e.g. `ViewPage.getActions()`); the URL is baked at config time.
|
|
31
|
+
* Omit `recordId` for row context (`Table.recordActions(...)`); the
|
|
32
|
+
* URL keeps the `:id` template and the renderer substitutes per-row.
|
|
33
|
+
*
|
|
34
|
+
* Auto-hides when `R.canEdit(user, record)` returns false. For row
|
|
35
|
+
* context the per-row record threads in via `loadTableRecords`'s
|
|
36
|
+
* per-row eval; for view-page context, `resolveSchema` provides the
|
|
37
|
+
* resolved record on the eval context.
|
|
38
|
+
*/
|
|
39
|
+
export function editAction(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
40
|
+
const id = recordId ?? ':id'
|
|
41
|
+
return Action.make('edit')
|
|
42
|
+
.label('Edit')
|
|
43
|
+
.href(`${resourceBase(basePath, R)}/${id}/edit`)
|
|
44
|
+
.visible(({ user, record }) => callPredicate(R.canEdit, user, record))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** View-action factory — link to the resource's view page. See `editAction` for the `recordId` semantics.
|
|
48
|
+
* Auto-hides when `R.canView(user, record)` returns false. */
|
|
49
|
+
export function viewAction(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
50
|
+
const id = recordId ?? ':id'
|
|
51
|
+
return Action.make('view')
|
|
52
|
+
.label('View')
|
|
53
|
+
.href(`${resourceBase(basePath, R)}/${id}`)
|
|
54
|
+
.visible(({ user, record }) => callPredicate(R.canView, user, record))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Delete-action factory — POSTs to the resource's delete route,
|
|
59
|
+
* destructive style, with a confirmation prompt referencing the
|
|
60
|
+
* resource label. Same `recordId` semantics as `editAction`.
|
|
61
|
+
* Auto-hides when `R.canDelete(user, record)` returns false.
|
|
62
|
+
*
|
|
63
|
+
* Plan #13 — when `R.softDeletes = true`, additionally hides on
|
|
64
|
+
* rows whose `deletedAtColumn` is set (already-trashed rows get the
|
|
65
|
+
* Restore + ForceDelete pair instead, surfaced via the matching
|
|
66
|
+
* factories below).
|
|
67
|
+
*/
|
|
68
|
+
export function deleteAction(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
69
|
+
const id = recordId ?? ':id'
|
|
70
|
+
return Action.make('delete')
|
|
71
|
+
.label('Delete')
|
|
72
|
+
.destructive()
|
|
73
|
+
.method('post')
|
|
74
|
+
.action(`${resourceBase(basePath, R)}/${id}/delete`)
|
|
75
|
+
.confirm(`Delete this ${R.labelSingular.toLowerCase()}?`)
|
|
76
|
+
.visible(async ({ user, record }) => {
|
|
77
|
+
if (R.softDeletes && isTrashed(record, R)) return false
|
|
78
|
+
return callPredicate(R.canDelete, user, record)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Replicate-action factory — handler-style. Loads the source record
|
|
84
|
+
* from `ctx.record` (the `_action/:actionName` route already resolves
|
|
85
|
+
* it through `R.query(ctx)` for row + single-target placements),
|
|
86
|
+
* strips PK + soft-delete column + any `opts.excludeAttributes`,
|
|
87
|
+
* optionally runs `opts.beforeReplicaSaved`, and creates a new row
|
|
88
|
+
* via `R.model.create(...)`. Redirects to the new record's edit page
|
|
89
|
+
* on success so the user can review + tweak before saving again.
|
|
90
|
+
*
|
|
91
|
+
* `recordId` kept in the signature for parity with `delete / edit /
|
|
92
|
+
* view` so users can swap factories without rewriting call sites; the
|
|
93
|
+
* dispatcher resolves the source record from the URL and hands it to
|
|
94
|
+
* the handler as `ctx.record`, so we don't reference `recordId` here.
|
|
95
|
+
*
|
|
96
|
+
* Auto-hides when `R.canCreate(user)` returns false — replicating
|
|
97
|
+
* writes a new row, so the gate is `canCreate`, not `canView`.
|
|
98
|
+
*/
|
|
99
|
+
export function replicateAction(
|
|
100
|
+
R: ResourceLike,
|
|
101
|
+
basePath: string,
|
|
102
|
+
recordId?: string,
|
|
103
|
+
opts: ReplicateOptions = {},
|
|
104
|
+
): Action {
|
|
105
|
+
void recordId
|
|
106
|
+
return Action.make('replicate')
|
|
107
|
+
.label('Replicate')
|
|
108
|
+
.handler(async (ctx) => {
|
|
109
|
+
const source = ctx.record
|
|
110
|
+
if (!source || typeof source !== 'object') {
|
|
111
|
+
return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
|
|
112
|
+
}
|
|
113
|
+
const M = R.model
|
|
114
|
+
if (!M || typeof M.create !== 'function') {
|
|
115
|
+
return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let replica: Record<string, unknown>
|
|
119
|
+
let pkCol: string
|
|
120
|
+
try {
|
|
121
|
+
({ replica, pkCol } = await buildReplica(source, M, {
|
|
122
|
+
excludeAttributes: opts.excludeAttributes,
|
|
123
|
+
deletedAtColumn: R.deletedAtColumn,
|
|
124
|
+
beforeReplicaSaved: opts.beforeReplicaSaved,
|
|
125
|
+
}))
|
|
126
|
+
} catch (err) {
|
|
127
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let created: unknown
|
|
131
|
+
try {
|
|
132
|
+
created = await M.create(replica)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const newId = (created as Record<string, unknown> | null | undefined)?.[pkCol]
|
|
138
|
+
const defaultRedirect = newId !== undefined && newId !== null
|
|
139
|
+
? `${resourceBase(basePath, R)}/${String(newId)}/edit`
|
|
140
|
+
: `${resourceBase(basePath, R)}`
|
|
141
|
+
// `!== undefined` rather than `??` so an override returning
|
|
142
|
+
// `null`/empty-string isn't silently swallowed (see
|
|
143
|
+
// feedback_nullish_swallows_explicit_null).
|
|
144
|
+
const overrideRedirect = opts.getRedirectUrl
|
|
145
|
+
? await opts.getRedirectUrl({ replica: created, source })
|
|
146
|
+
: undefined
|
|
147
|
+
const redirect = overrideRedirect !== undefined ? overrideRedirect : defaultRedirect
|
|
148
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
149
|
+
? await opts.getCreatedNotificationTitle({ replica: created, source })
|
|
150
|
+
: undefined
|
|
151
|
+
const title = overrideTitle !== undefined ? overrideTitle : `${R.labelSingular} replicated`
|
|
152
|
+
return {
|
|
153
|
+
redirect,
|
|
154
|
+
notify: { title, type: 'success' } as never,
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Plan #13 — Restore factory. POSTs to the resource's restore route,
|
|
162
|
+
* success-styled, no confirm prompt (restoration is reversible).
|
|
163
|
+
* Auto-hides on live (non-trashed) rows AND when `R.canRestore(user,
|
|
164
|
+
* record)` returns false. Same `recordId` semantics as `editAction`.
|
|
165
|
+
*/
|
|
166
|
+
export function restoreAction(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
167
|
+
const id = recordId ?? ':id'
|
|
168
|
+
return Action.make('restore')
|
|
169
|
+
.label('Restore')
|
|
170
|
+
.color('success')
|
|
171
|
+
.method('post')
|
|
172
|
+
.action(`${resourceBase(basePath, R)}/${id}/restore`)
|
|
173
|
+
.visible(async ({ user, record }) => {
|
|
174
|
+
if (!isTrashed(record, R)) return false
|
|
175
|
+
return callPredicate(R.canRestore, user, record)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Plan #13 — Force-delete factory. POSTs to the resource's
|
|
181
|
+
* force-delete route, destructive-styled, with a stricter confirm
|
|
182
|
+
* prompt referencing permanence. Auto-hides on live (non-trashed)
|
|
183
|
+
* rows AND when `R.canForceDelete(user, record)` returns false.
|
|
184
|
+
*/
|
|
185
|
+
export function forceDeleteAction(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
186
|
+
const id = recordId ?? ':id'
|
|
187
|
+
return Action.make('forceDelete')
|
|
188
|
+
.label('Delete forever')
|
|
189
|
+
.destructive()
|
|
190
|
+
.method('post')
|
|
191
|
+
.action(`${resourceBase(basePath, R)}/${id}/force-delete`)
|
|
192
|
+
.confirm(`Permanently delete this ${R.labelSingular.toLowerCase()}? This cannot be undone.`)
|
|
193
|
+
.visible(async ({ user, record }) => {
|
|
194
|
+
if (!isTrashed(record, R)) return false
|
|
195
|
+
return callPredicate(R.canForceDelete, user, record)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Mark-as-read factory — POSTs to the panel's notification read
|
|
201
|
+
* endpoint for the given notification id. The endpoint
|
|
202
|
+
* (`${base}/_notifications/:id/read`) is mounted by
|
|
203
|
+
* `Pilotiq.databaseNotifications()`, so calling this without
|
|
204
|
+
* opting into the bell surface produces an Action whose POST 404s.
|
|
205
|
+
*
|
|
206
|
+
* `notificationId` is baked at config time. For row context where
|
|
207
|
+
* the id varies per row, omit it and the URL keeps the `:id`
|
|
208
|
+
* template; the renderer substitutes per-row at render time
|
|
209
|
+
* (parallel to `editAction`'s row form).
|
|
210
|
+
*
|
|
211
|
+
* No auto-visibility. Wrap in `.visible(({ record }) => !record.readAt)`
|
|
212
|
+
* to hide on already-read rows.
|
|
213
|
+
*/
|
|
214
|
+
export function markAsReadAction(basePath: string, notificationId?: string): Action {
|
|
215
|
+
const id = notificationId ?? ':id'
|
|
216
|
+
return Action.make('markAsRead')
|
|
217
|
+
.label('Mark as read')
|
|
218
|
+
.method('post')
|
|
219
|
+
.action(`${basePath}/_notifications/${id}/read`)
|
|
220
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Action factory modules — `crudFactories.ts`,
|
|
3
|
+
* `bulkFactories.ts`, `relationFactories.ts`, `m2mFactories.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Lives in its own file so the per-phase factory modules stay focused
|
|
6
|
+
* on their own factory bodies. Anything consumed by 2+ phase files
|
|
7
|
+
* lands here; phase-local helpers stay alongside their phase's
|
|
8
|
+
* factories.
|
|
9
|
+
*
|
|
10
|
+
* See `docs/plans/action-split.md` for the split plan.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ResourceLike } from './Action.js'
|
|
14
|
+
import type { RelationManagerContext } from '../RelationManager.js'
|
|
15
|
+
|
|
16
|
+
/** Cluster-aware resource base path. Mirrors `clusterPaths.resourceBasePath`
|
|
17
|
+
* but uses the structural `ResourceLike` shape so `Action.ts` stays
|
|
18
|
+
* cycle-free against `Resource.ts`. */
|
|
19
|
+
export function resourceBase(basePath: string, R: ResourceLike): string {
|
|
20
|
+
if (R.cluster) return `${basePath}/${R.cluster.getSlug()}/${R.getSlug()}`
|
|
21
|
+
return `${basePath}/${R.getSlug()}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Call a (possibly undefined) Resource predicate. When unset, the
|
|
25
|
+
* predicate is treated as "allowed" (returns true) so the factory
|
|
26
|
+
* doesn't hide actions on Resources that haven't opted into Plan #10. */
|
|
27
|
+
export function callPredicate(
|
|
28
|
+
fn: ((user: unknown, record?: unknown) => boolean | Promise<boolean>) | undefined,
|
|
29
|
+
user: unknown,
|
|
30
|
+
record?: unknown,
|
|
31
|
+
): boolean | Promise<boolean> {
|
|
32
|
+
if (!fn) return true
|
|
33
|
+
return fn(user, record)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read `record[R.deletedAtColumn ?? 'deletedAt']` and return true when
|
|
37
|
+
* the row is currently trashed (soft-deleted). Permissive on shape —
|
|
38
|
+
* bare `null` / `undefined` count as live; any other truthy value is
|
|
39
|
+
* trashed. */
|
|
40
|
+
export function isTrashed(record: unknown, R: ResourceLike): boolean {
|
|
41
|
+
if (!record || typeof record !== 'object') return false
|
|
42
|
+
const col = R.deletedAtColumn ?? 'deletedAt'
|
|
43
|
+
const v = (record as Record<string, unknown>)[col]
|
|
44
|
+
return v !== null && v !== undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True when a `RelationManagerContext.mode` denotes a pivot-mutation
|
|
48
|
+
* shape — i.e. a many-to-many relation. All three modes share the
|
|
49
|
+
* `attach` / `detach` / `sync` accessor surface (the rudder ORM stamps
|
|
50
|
+
* + filters the polymorphic discriminator transparently for the morph
|
|
51
|
+
* variants). The `relationCreate / Edit / Delete` factories auto-hide
|
|
52
|
+
* under any of these modes because per-pivot-row create / edit / delete
|
|
53
|
+
* is meaningless — users create the related record via its own Resource,
|
|
54
|
+
* then attach via `relationAttach`. */
|
|
55
|
+
export function isM2MMode(mode: RelationManagerContext['mode']): boolean {
|
|
56
|
+
return mode === 'belongsToMany' || mode === 'morphToMany' || mode === 'morphedByMany'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the URL prefix for a relation factory action. Without
|
|
61
|
+
* a `chain` (depth-1 manager), this is the familiar
|
|
62
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}`. With a chain
|
|
63
|
+
* (depth-2 nested manager), it threads the outer record + relationship
|
|
64
|
+
* between the parent slug and the leaf parent id:
|
|
65
|
+
*
|
|
66
|
+
* `${base}/${parentSlug}/${chain[0].recordId}/${chain[0].relationship}/${parentId}/${relationship}`
|
|
67
|
+
*
|
|
68
|
+
* Pure; takes a `RelationManagerContext` and emits a string. The leaf
|
|
69
|
+
* record id (and trailing `/edit`, `/delete`, etc.) gets appended by
|
|
70
|
+
* the caller.
|
|
71
|
+
*/
|
|
72
|
+
export function relationUrlPrefix(ctx: RelationManagerContext): string {
|
|
73
|
+
const head = `${ctx.basePath}/${ctx.parentSlug}`
|
|
74
|
+
const chain = ctx.chain ?? []
|
|
75
|
+
let mid = ''
|
|
76
|
+
for (const step of chain) {
|
|
77
|
+
mid += `/${step.recordId}/${step.relationship}`
|
|
78
|
+
}
|
|
79
|
+
return `${head}${mid}/${ctx.parentId}/${ctx.relationship}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Options bag for `buildReplica`. Optional fields explicitly accept
|
|
83
|
+
* `undefined` so call sites can pass `opts.excludeAttributes` through
|
|
84
|
+
* unconditionally under `exactOptionalPropertyTypes: true`. */
|
|
85
|
+
export interface BuildReplicaOptions {
|
|
86
|
+
/** Attribute keys to drop from the replicated payload IN ADDITION TO
|
|
87
|
+
* the model's primary key and soft-delete column. */
|
|
88
|
+
excludeAttributes?: readonly string[] | undefined
|
|
89
|
+
/** Soft-delete column name on the source Resource. Defaults to
|
|
90
|
+
* `'deletedAt'`. Read separately from the model because the column
|
|
91
|
+
* lives on the Resource shape (`R.deletedAtColumn`) rather than on
|
|
92
|
+
* the model itself. */
|
|
93
|
+
deletedAtColumn?: string | undefined
|
|
94
|
+
/** Force-pinned columns applied AFTER the strip and BEFORE the user
|
|
95
|
+
* `beforeReplicaSaved` mutator. Used by the relation replicate
|
|
96
|
+
* factories to re-stamp the parent attachment FK / morph columns
|
|
97
|
+
* so a tampered source row can't slip a different parent in. */
|
|
98
|
+
pin?: Record<string, unknown> | undefined
|
|
99
|
+
/** Optional user mutator. Runs after the strip + pin. */
|
|
100
|
+
beforeReplicaSaved?: ((
|
|
101
|
+
replica: Record<string, unknown>,
|
|
102
|
+
source: unknown,
|
|
103
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>) | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Build a replica payload from a source record. Used by every replicate
|
|
108
|
+
* factory (`replicateAction / bulkReplicateAction / relationReplicateAction
|
|
109
|
+
* / relationBulkReplicateAction`).
|
|
110
|
+
*
|
|
111
|
+
* Strips the model's primary key (`model.primaryKey`, defaulting to
|
|
112
|
+
* `'id'`), the soft-delete column (defaulting to `'deletedAt'`), and any
|
|
113
|
+
* `excludeAttributes` keys. Applies `pin` columns (parent attachment
|
|
114
|
+
* for relation factories), then runs the optional `beforeReplicaSaved`
|
|
115
|
+
* user mutator. Returns the replica AND the resolved primary-key column
|
|
116
|
+
* name (callers need it to read `created[pkCol]` for redirect URLs).
|
|
117
|
+
*
|
|
118
|
+
* Does NOT call `model.create` — callers wrap their own create + error
|
|
119
|
+
* handling around the returned replica.
|
|
120
|
+
*/
|
|
121
|
+
export async function buildReplica(
|
|
122
|
+
source: unknown,
|
|
123
|
+
model: { primaryKey?: string },
|
|
124
|
+
opts: BuildReplicaOptions = {},
|
|
125
|
+
): Promise<{ replica: Record<string, unknown>; pkCol: string }> {
|
|
126
|
+
const pkCol = model.primaryKey ?? 'id'
|
|
127
|
+
const trashedCol = opts.deletedAtColumn ?? 'deletedAt'
|
|
128
|
+
const skip = new Set<string>([pkCol, trashedCol, ...(opts.excludeAttributes ?? [])])
|
|
129
|
+
let replica: Record<string, unknown> = {}
|
|
130
|
+
for (const [k, v] of Object.entries(source as Record<string, unknown>)) {
|
|
131
|
+
if (skip.has(k)) continue
|
|
132
|
+
replica[k] = v
|
|
133
|
+
}
|
|
134
|
+
if (opts.pin) Object.assign(replica, opts.pin)
|
|
135
|
+
if (opts.beforeReplicaSaved) {
|
|
136
|
+
replica = await opts.beforeReplicaSaved(replica, source)
|
|
137
|
+
}
|
|
138
|
+
return { replica, pkCol }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Iterate `records`, run the policy probe in parallel up-front (only
|
|
143
|
+
* allowed rows enter the serial work loop), call `op(id, record)` per
|
|
144
|
+
* allowed row, swallow per-row throws (the aggregate notification shows
|
|
145
|
+
* the count succeeded). Returns the success count.
|
|
146
|
+
*
|
|
147
|
+
* Used by every bulk handler (`bulkDeleteAction / bulkRestoreAction /
|
|
148
|
+
* bulkForceDeleteAction / bulkReplicateAction / relationBulkReplicateAction
|
|
149
|
+
* / relationBulkDetachAction`-style pattern).
|
|
150
|
+
*
|
|
151
|
+
* Rows whose `record.id` coerces to an empty string are skipped without
|
|
152
|
+
* counting them as an attempt. The policy probe runs via `Promise.all`
|
|
153
|
+
* so backend round-trips parallelize, but the write loop stays serial
|
|
154
|
+
* (no transaction in v1 — concurrent writes would muddy failure
|
|
155
|
+
* semantics).
|
|
156
|
+
*/
|
|
157
|
+
export async function forEachAllowed(
|
|
158
|
+
records: readonly unknown[],
|
|
159
|
+
isAllowed: (record: unknown, index: number) => boolean | Promise<boolean>,
|
|
160
|
+
op: (id: string, record: unknown, index: number) => Promise<void>,
|
|
161
|
+
): Promise<number> {
|
|
162
|
+
const allowedFlags = await Promise.all(
|
|
163
|
+
records.map((r, i) => isAllowed(r, i)),
|
|
164
|
+
)
|
|
165
|
+
let n = 0
|
|
166
|
+
for (let i = 0; i < records.length; i++) {
|
|
167
|
+
if (!allowedFlags[i]) continue
|
|
168
|
+
const record = records[i]
|
|
169
|
+
const id = String((record as { id?: unknown } | null | undefined)?.id ?? '')
|
|
170
|
+
if (!id) continue
|
|
171
|
+
try {
|
|
172
|
+
await op(id, record, i)
|
|
173
|
+
n++
|
|
174
|
+
} catch { /* skip — agg notify shows total */ }
|
|
175
|
+
}
|
|
176
|
+
return n
|
|
177
|
+
}
|