@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,867 @@
|
|
|
1
|
+
import { resourceBasePath } from '../clusterPaths.js';
|
|
2
|
+
import { resolveSchema } from '../schema/resolveSchema.js';
|
|
3
|
+
import { Form } from '../elements/Form.js';
|
|
4
|
+
import { Table } from '../elements/Table.js';
|
|
5
|
+
import { Filter } from '../filters/Filter.js';
|
|
6
|
+
import { TrashedFilter } from '../filters/TrashedFilter.js';
|
|
7
|
+
import { loadTableRecords } from '../elements/dispatchTable.js';
|
|
8
|
+
import { consumeFlashedNotifications } from '../notifications/flash.js';
|
|
9
|
+
import { serializeIcon } from '../icons/types.js';
|
|
10
|
+
import { safeManagerPolicy as safeManagerPolicyImpl, } from '../RelationManager.js';
|
|
11
|
+
import { findRecord, getPrimaryKey, getRelationType, modelLoadRecord, modelRelationTableRecords, modelSave, } from '../orm/modelDefaults.js';
|
|
12
|
+
import { normalizeRelationMode } from '../RelationManager.js';
|
|
13
|
+
import { nestedRelationCreateBreadcrumbs, nestedRelationEditBreadcrumbs, nestedRelationListBreadcrumbs, nestedRelationViewBreadcrumbs, relationCreateBreadcrumbs, relationEditBreadcrumbs, relationListBreadcrumbs, relationViewBreadcrumbs, } from './breadcrumbs.js';
|
|
14
|
+
import { applyFillPipeline, tagActionDispatch, tagFormActions, uploadCtx, userCtx, } from './helpers.js';
|
|
15
|
+
import { applyRoleHooks, panelInfo, } from './navigation.js';
|
|
16
|
+
import { buildNestedRelationTabs, buildRelationTabs, deriveParentTitle, safeBool, } from './relationTabs.js';
|
|
17
|
+
/**
|
|
18
|
+
* Discover the related Resource for a manager. Order:
|
|
19
|
+
* 1. `M.relatedResource` explicit override (skip discovery).
|
|
20
|
+
* 2. Rudder ORM convention: walk
|
|
21
|
+
* `R.model.relations[manager.relationship].model()` and find
|
|
22
|
+
* `cfg.resources[i].model === relatedModel`.
|
|
23
|
+
* 3. Otherwise undefined — caller must error or fall back.
|
|
24
|
+
*
|
|
25
|
+
* A returned Resource is the one whose `model` backs the related
|
|
26
|
+
* table. Callers use it for `Related.model.find(childId)`,
|
|
27
|
+
* `Related.canEdit(user, child)`, and the auto-wired form save handler.
|
|
28
|
+
*/
|
|
29
|
+
export function findRelatedResource(M, R, cfg) {
|
|
30
|
+
if (M.relatedResource)
|
|
31
|
+
return M.relatedResource;
|
|
32
|
+
const ParentModel = R.model;
|
|
33
|
+
if (!ParentModel)
|
|
34
|
+
return undefined;
|
|
35
|
+
const def = ParentModel.relations?.[M.getRelationship()];
|
|
36
|
+
const RelatedModel = typeof def?.model === 'function' ? def.model() : undefined;
|
|
37
|
+
if (!RelatedModel)
|
|
38
|
+
return undefined;
|
|
39
|
+
return cfg.resources.find(r => r.model === RelatedModel);
|
|
40
|
+
}
|
|
41
|
+
/** Find a registered manager on a Resource by its relationship key.
|
|
42
|
+
* Throws on unknown manager — so the route can 404 cleanly. */
|
|
43
|
+
function findManager(R, relationship) {
|
|
44
|
+
return R.relations().find(M => {
|
|
45
|
+
try {
|
|
46
|
+
return M.getRelationship() === relationship;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify a child record actually belongs to the given parent under the
|
|
55
|
+
* declared relationship. Anti-IDOR — without this an attacker can swap
|
|
56
|
+
* the `:childId` segment to load any related-model row regardless of
|
|
57
|
+
* whether it's actually owned by the parent.
|
|
58
|
+
*
|
|
59
|
+
* Strategy: re-resolve the parent's relation query and check whether
|
|
60
|
+
* the child's primary key shows up in `where(pk, '=', childId).paginate(1, 1)`.
|
|
61
|
+
* Yes, it's a second round-trip — but it's the single point of trust
|
|
62
|
+
* for IDOR safety, and it fits naturally into the same query path
|
|
63
|
+
* `modelRelationTableRecords` uses.
|
|
64
|
+
*/
|
|
65
|
+
async function childBelongsToParent(parentModel, parent, relationship, childPk, childId) {
|
|
66
|
+
try {
|
|
67
|
+
const q = (parentModel.relatedQuery
|
|
68
|
+
? parentModel.relatedQuery(parent, relationship)
|
|
69
|
+
: parent.related(relationship));
|
|
70
|
+
const result = await q.where(childPk, '=', childId).paginate(1, 1);
|
|
71
|
+
return result.total > 0;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Auto-wire the manager's table records loader against the parent's
|
|
79
|
+
* relation query when the user didn't set `Table.records()` themselves.
|
|
80
|
+
* Mirrors `defaultPages`'s wiring of `Table.records()` from `R.model`
|
|
81
|
+
* for the resource list page.
|
|
82
|
+
*/
|
|
83
|
+
function autoWireManagerTable(table, parentModel, parent, relationship) {
|
|
84
|
+
if (table.getRecords())
|
|
85
|
+
return; // user wired it explicitly
|
|
86
|
+
table.records(modelRelationTableRecords(parentModel, parent, relationship, table));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Plan #13 polish — auto-inject `TrashedFilter` on a relation manager's
|
|
90
|
+
* table when the **related** Resource opts into soft deletes. Mirrors the
|
|
91
|
+
* resource-list pattern in `defaultPages.applyTableDefaults`. The check
|
|
92
|
+
* is on the related Resource (not the manager), because soft-delete is a
|
|
93
|
+
* model-level capability — if the child model supports trashing, the
|
|
94
|
+
* manager's table should expose the toggle.
|
|
95
|
+
*
|
|
96
|
+
* No-op when:
|
|
97
|
+
* - the related Resource hasn't set `softDeletes = true`
|
|
98
|
+
* - the user already attached a `TrashedFilter` in `M.table()`
|
|
99
|
+
*/
|
|
100
|
+
function injectManagerTrashedFilter(table, Related) {
|
|
101
|
+
if (!Related?.softDeletes)
|
|
102
|
+
return;
|
|
103
|
+
const children = table.getChildren() ?? [];
|
|
104
|
+
const hasTrashed = children.some(c => c instanceof TrashedFilter);
|
|
105
|
+
if (hasTrashed)
|
|
106
|
+
return;
|
|
107
|
+
const existing = children.filter(c => c instanceof Filter);
|
|
108
|
+
table.filters([...existing, TrashedFilter.make()]);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Auto-wire the manager's form save + loadRecord handlers against the
|
|
112
|
+
* **related** Resource's `model` when the user didn't set them. The
|
|
113
|
+
* route handler is responsible for stamping the parent context
|
|
114
|
+
* (parent, parentRecord, parentId, relationship) onto the
|
|
115
|
+
* `FormContext` so user-supplied `mutateDataBeforeCreate` etc. can
|
|
116
|
+
* read them.
|
|
117
|
+
*/
|
|
118
|
+
function autoWireManagerForm(form, Related) {
|
|
119
|
+
const RelatedModel = Related.model;
|
|
120
|
+
if (!RelatedModel)
|
|
121
|
+
return;
|
|
122
|
+
if (!form.getSave())
|
|
123
|
+
form.save(modelSave(RelatedModel));
|
|
124
|
+
if (!form.getLoadRecord())
|
|
125
|
+
form.loadRecord(modelLoadRecord(Related));
|
|
126
|
+
}
|
|
127
|
+
/** Plan #11 — authorize a relation-manager action with sensible defaults.
|
|
128
|
+
* Re-exported from `RelationManager.ts` so external callers (route
|
|
129
|
+
* handlers, third-party plugins) keep their existing import path. */
|
|
130
|
+
export const safeManagerPolicy = safeManagerPolicyImpl;
|
|
131
|
+
/**
|
|
132
|
+
* Plan #11 — render data for the three relation-manager URL scopes.
|
|
133
|
+
* Mirrors the resource* builders' shape so routes and Vike +data hooks
|
|
134
|
+
* consume identical props. Authorization runs inline (parent
|
|
135
|
+
* `canAccess + canEdit(parent)` then manager-scoped predicate); IDOR
|
|
136
|
+
* check on `relation-edit` runs against the parent's relation query.
|
|
137
|
+
*
|
|
138
|
+
* Returns:
|
|
139
|
+
* - `null` when panel / parent / manager / child don't exist.
|
|
140
|
+
* - `{ ok: false, status: 403 }` when authorization denies.
|
|
141
|
+
* - the props record on success (route picks SSR view / SPA prop
|
|
142
|
+
* downstream).
|
|
143
|
+
*/
|
|
144
|
+
export async function relationManagerData(pilotiq, scope, req) {
|
|
145
|
+
// Phase B nested-relation-* scopes split out into their own pipeline
|
|
146
|
+
// — the chain walking + per-layer auth differs enough from the
|
|
147
|
+
// depth-1 path that interleaving them would mostly hurt readability.
|
|
148
|
+
if (scope.kind === 'nested-relation-list'
|
|
149
|
+
|| scope.kind === 'nested-relation-create'
|
|
150
|
+
|| scope.kind === 'nested-relation-view'
|
|
151
|
+
|| scope.kind === 'nested-relation-edit') {
|
|
152
|
+
return nestedRelationManagerData(pilotiq, scope, req);
|
|
153
|
+
}
|
|
154
|
+
const cfg = pilotiq.getConfig();
|
|
155
|
+
const R = cfg.resources.find(r => r.getSlug() === scope.slug);
|
|
156
|
+
if (!R)
|
|
157
|
+
return null;
|
|
158
|
+
const M = findManager(R, scope.relationship);
|
|
159
|
+
if (!M)
|
|
160
|
+
return null;
|
|
161
|
+
const user = await pilotiq.resolveUser(req);
|
|
162
|
+
// Layer 1: parent access. canAccess gates the resource entirely;
|
|
163
|
+
// canEdit gates managing its relations (managers are read-write
|
|
164
|
+
// surfaces — read-only inline views opt in by overriding the
|
|
165
|
+
// manager's can*). Cluster gate composes with R.canAccess — both
|
|
166
|
+
// must pass when the parent resource is inside a cluster.
|
|
167
|
+
const [clusterOk, accessOk] = await Promise.all([
|
|
168
|
+
R.cluster ? safeBool(() => R.cluster.canAccess(user)) : Promise.resolve(true),
|
|
169
|
+
safeBool(() => R.canAccess(user)),
|
|
170
|
+
]);
|
|
171
|
+
if (!clusterOk || !accessOk)
|
|
172
|
+
return { ok: false, status: 403 };
|
|
173
|
+
if (!R.model) {
|
|
174
|
+
// Without a model on the parent we can't load the parent record,
|
|
175
|
+
// and without that we can't IDOR-check children. Point users at
|
|
176
|
+
// the missing wiring rather than silent 500s.
|
|
177
|
+
throw new Error(`[Pilotiq] Resource "${R.name}" has relations(${M.name}) but no static model. ` +
|
|
178
|
+
`Set Resource.model = … to enable relation managers, or remove the manager.`);
|
|
179
|
+
}
|
|
180
|
+
const parentRecord = await findRecord(R, scope.recordId, { user }).catch(() => undefined);
|
|
181
|
+
if (!parentRecord)
|
|
182
|
+
return null;
|
|
183
|
+
if (!await safeBool(() => R.canEdit(user, parentRecord)))
|
|
184
|
+
return { ok: false, status: 403 };
|
|
185
|
+
// Read the relation type off the parent's relations map once,
|
|
186
|
+
// normalize to the six-way `RelationMode` the manager-side logic
|
|
187
|
+
// uses. `belongsToMany` / `morphToMany` (owning polymorphic) /
|
|
188
|
+
// `morphedByMany` (inverse polymorphic) all flip into pivot-mutation
|
|
189
|
+
// mode (attach / detach / sync — same accessor surface), `morphMany|
|
|
190
|
+
// morphOne` collapses to `'morphMany'` (parent-side polymorphic —
|
|
191
|
+
// auto-fills morph columns on create), `morphTo` is the child-side
|
|
192
|
+
// polymorphic (no auto-actions; requires explicit `M.relatedResource`).
|
|
193
|
+
// Everything else collapses to `'hasMany'`.
|
|
194
|
+
const relationType = getRelationType(R.model, scope.relationship);
|
|
195
|
+
const mode = normalizeRelationMode(relationType);
|
|
196
|
+
const Related = findRelatedResource(M, R, cfg);
|
|
197
|
+
// Related Resource is required for: edit/create form auto-wire,
|
|
198
|
+
// child loading on edit, related URL generation. Throw when missing
|
|
199
|
+
// *only* if we'd otherwise need it — for `relation-list` it's
|
|
200
|
+
// optional (the table can be hand-wired by the user).
|
|
201
|
+
const needRelated = scope.kind !== 'relation-list';
|
|
202
|
+
if (needRelated && !Related) {
|
|
203
|
+
throw new Error(`[Pilotiq] RelationManager ${M.name} on ${R.name} could not resolve its related Resource. ` +
|
|
204
|
+
`Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M.getRelationship())}].`);
|
|
205
|
+
}
|
|
206
|
+
switch (scope.kind) {
|
|
207
|
+
case 'relation-list':
|
|
208
|
+
return buildRelationListData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
|
|
209
|
+
case 'relation-create':
|
|
210
|
+
return buildRelationCreateData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
|
|
211
|
+
case 'relation-view':
|
|
212
|
+
return buildRelationViewData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
|
|
213
|
+
case 'relation-edit':
|
|
214
|
+
return buildRelationEditData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function buildRelationListData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
|
|
218
|
+
if (!await safeManagerPolicy(M, 'canViewAny', Related, user, parentRecord))
|
|
219
|
+
return { ok: false, status: 403 };
|
|
220
|
+
const cfg = pilotiq.getConfig();
|
|
221
|
+
const base = cfg.path;
|
|
222
|
+
const resourceBase = resourceBasePath(base, R);
|
|
223
|
+
const listUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}`;
|
|
224
|
+
// Build a single Table by piping a fresh Table through M.table(table, ctx).
|
|
225
|
+
// Context lets the user wire `Action.relationCreate / relationEdit /
|
|
226
|
+
// relationDelete(M, ctx)` factories inside `static table()` to template
|
|
227
|
+
// URLs without threading basePath / parentId by hand.
|
|
228
|
+
const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode);
|
|
229
|
+
const table = M.table(Table.make(), managerCtx);
|
|
230
|
+
autoWireManagerTable(table, R.model, parentRecord, scope.relationship);
|
|
231
|
+
injectManagerTrashedFilter(table, Related);
|
|
232
|
+
const ctx = uploadCtx(userCtx({
|
|
233
|
+
mode: 'table',
|
|
234
|
+
basePath: base,
|
|
235
|
+
record: parentRecord,
|
|
236
|
+
}, user), cfg);
|
|
237
|
+
const elements = [table];
|
|
238
|
+
tagActionDispatch(elements, listUrl);
|
|
239
|
+
// Independent work — records load against the parent's relation
|
|
240
|
+
// accessor; tabs probe per-tab `safeManagerPolicy` predicates that
|
|
241
|
+
// don't touch the records pipeline.
|
|
242
|
+
const [, tabs] = await Promise.all([
|
|
243
|
+
loadTableRecords(elements, scope.query ?? {}, listUrl, user),
|
|
244
|
+
buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord),
|
|
245
|
+
]);
|
|
246
|
+
if (tabs)
|
|
247
|
+
elements.unshift(tabs);
|
|
248
|
+
const breadcrumbs = relationListBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord));
|
|
249
|
+
if (breadcrumbs)
|
|
250
|
+
elements.unshift(breadcrumbs);
|
|
251
|
+
const relationListRoute = { resource: R, recordId: scope.recordId };
|
|
252
|
+
const [panel, schemaData] = await Promise.all([
|
|
253
|
+
panelInfo(pilotiq, req, relationListRoute),
|
|
254
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-list', metas, relationListRoute)),
|
|
255
|
+
]);
|
|
256
|
+
return {
|
|
257
|
+
pageType: 'relation-list',
|
|
258
|
+
panel,
|
|
259
|
+
resource: { name: R.name, label: R.label, labelSingular: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
|
|
260
|
+
relation: {
|
|
261
|
+
name: M.name,
|
|
262
|
+
label: M.getLabel(),
|
|
263
|
+
labelSingular: M.getLabelSingular(),
|
|
264
|
+
relationship: scope.relationship,
|
|
265
|
+
icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
|
|
266
|
+
relatedSlug: Related?.getSlug(),
|
|
267
|
+
},
|
|
268
|
+
parent: {
|
|
269
|
+
id: scope.recordId,
|
|
270
|
+
title: deriveParentTitle(R, parentRecord),
|
|
271
|
+
},
|
|
272
|
+
basePath: base,
|
|
273
|
+
layout: cfg.layout,
|
|
274
|
+
schemaData,
|
|
275
|
+
notifications: consumeFlashedNotifications(req),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function buildRelationCreateData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
|
|
279
|
+
if (!await safeManagerPolicy(M, 'canCreate', Related, user, parentRecord))
|
|
280
|
+
return { ok: false, status: 403 };
|
|
281
|
+
const cfg = pilotiq.getConfig();
|
|
282
|
+
const base = cfg.path;
|
|
283
|
+
const resourceBase = resourceBasePath(base, R);
|
|
284
|
+
const createUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/create`;
|
|
285
|
+
const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode);
|
|
286
|
+
const form = M.form(Form.make(), managerCtx);
|
|
287
|
+
if (Related.model)
|
|
288
|
+
autoWireManagerForm(form, Related);
|
|
289
|
+
const elements = [form];
|
|
290
|
+
tagFormActions(elements, createUrl);
|
|
291
|
+
if (scope.prefill) {
|
|
292
|
+
if (scope.prefill.values)
|
|
293
|
+
form.withValues(scope.prefill.values);
|
|
294
|
+
if (scope.prefill.errors)
|
|
295
|
+
form.withErrors(scope.prefill.errors);
|
|
296
|
+
}
|
|
297
|
+
const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord);
|
|
298
|
+
if (tabs)
|
|
299
|
+
elements.unshift(tabs);
|
|
300
|
+
const breadcrumbs = relationCreateBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord));
|
|
301
|
+
if (breadcrumbs)
|
|
302
|
+
elements.unshift(breadcrumbs);
|
|
303
|
+
const ctx = uploadCtx(userCtx({
|
|
304
|
+
mode: 'create',
|
|
305
|
+
basePath: base,
|
|
306
|
+
record: parentRecord,
|
|
307
|
+
}, user), cfg);
|
|
308
|
+
const relationCreateRoute = { resource: R, recordId: scope.recordId };
|
|
309
|
+
const [panel, schemaData] = await Promise.all([
|
|
310
|
+
panelInfo(pilotiq, req, relationCreateRoute),
|
|
311
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-create', metas, relationCreateRoute)),
|
|
312
|
+
]);
|
|
313
|
+
return {
|
|
314
|
+
pageType: 'relation-create',
|
|
315
|
+
panel,
|
|
316
|
+
resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
|
|
317
|
+
relation: {
|
|
318
|
+
name: M.name,
|
|
319
|
+
label: M.getLabel(),
|
|
320
|
+
labelSingular: M.getLabelSingular(),
|
|
321
|
+
relationship: scope.relationship,
|
|
322
|
+
icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
|
|
323
|
+
relatedSlug: Related.getSlug(),
|
|
324
|
+
},
|
|
325
|
+
parent: {
|
|
326
|
+
id: scope.recordId,
|
|
327
|
+
title: deriveParentTitle(R, parentRecord),
|
|
328
|
+
},
|
|
329
|
+
mode: 'create',
|
|
330
|
+
basePath: base,
|
|
331
|
+
layout: cfg.layout,
|
|
332
|
+
schemaData,
|
|
333
|
+
notifications: consumeFlashedNotifications(req),
|
|
334
|
+
...(scope.prefill?.errors ? { hasErrors: true } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Phase A — read-only view page for a related record at depth-2:
|
|
339
|
+
* `${base}/${slug}/:id/${rel}/:childId`. Mirrors `buildRelationEditData`'s
|
|
340
|
+
* IDOR + auth posture but resolves the manager's `static detail(child,
|
|
341
|
+
* parent)` instead of its form. The default `detail()` returns `[]` —
|
|
342
|
+
* managers opt in by overriding it; the chrome (RelationTabs strip)
|
|
343
|
+
* still renders so users can sideways-nav between sibling managers.
|
|
344
|
+
*/
|
|
345
|
+
async function buildRelationViewData(pilotiq, R, M, Related, parentRecord, scope, req, user, _mode) {
|
|
346
|
+
if (!Related.model) {
|
|
347
|
+
throw new Error(`[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`);
|
|
348
|
+
}
|
|
349
|
+
const childPk = getPrimaryKey(Related.model);
|
|
350
|
+
const [belongs, child] = await Promise.all([
|
|
351
|
+
childBelongsToParent(R.model, parentRecord, scope.relationship, childPk, scope.childId),
|
|
352
|
+
findRecord(Related, scope.childId, { user }).catch(() => undefined),
|
|
353
|
+
]);
|
|
354
|
+
if (!belongs || !child)
|
|
355
|
+
return null;
|
|
356
|
+
if (!await safeManagerPolicy(M, 'canView', Related, user, parentRecord, child))
|
|
357
|
+
return { ok: false, status: 403 };
|
|
358
|
+
const cfg = pilotiq.getConfig();
|
|
359
|
+
const base = cfg.path;
|
|
360
|
+
const elements = M.detail(child, parentRecord);
|
|
361
|
+
// Phase B polish — when M declares nested managers, surface them on
|
|
362
|
+
// this page too. The strip lists the leaf parent's view tab plus one
|
|
363
|
+
// tab per sibling nested manager so users can jump from the Phase A
|
|
364
|
+
// view straight into a grandchild list / create / view / edit page.
|
|
365
|
+
// Active key `'__view'` because the user is currently viewing the
|
|
366
|
+
// leaf parent record itself, not any nested manager.
|
|
367
|
+
const nestedTabs = await buildNestedRelationTabs(R, M, base, { recordId: scope.recordId, relationship: scope.relationship }, scope.childId, '__view', user, child);
|
|
368
|
+
if (nestedTabs)
|
|
369
|
+
elements.unshift(nestedTabs);
|
|
370
|
+
const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord);
|
|
371
|
+
if (tabs)
|
|
372
|
+
elements.unshift(tabs);
|
|
373
|
+
const breadcrumbs = relationViewBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord), deriveParentTitle(Related, child, M));
|
|
374
|
+
if (breadcrumbs)
|
|
375
|
+
elements.unshift(breadcrumbs);
|
|
376
|
+
const ctx = uploadCtx(userCtx({
|
|
377
|
+
mode: 'view',
|
|
378
|
+
basePath: base,
|
|
379
|
+
record: child,
|
|
380
|
+
recordId: scope.childId,
|
|
381
|
+
}, user), cfg);
|
|
382
|
+
const relationViewRoute = { resource: R, recordId: scope.childId };
|
|
383
|
+
const [panel, schemaData] = await Promise.all([
|
|
384
|
+
panelInfo(pilotiq, req, relationViewRoute),
|
|
385
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-view', metas, relationViewRoute)),
|
|
386
|
+
]);
|
|
387
|
+
return {
|
|
388
|
+
pageType: 'relation-view',
|
|
389
|
+
panel,
|
|
390
|
+
resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
|
|
391
|
+
relation: {
|
|
392
|
+
name: M.name,
|
|
393
|
+
label: M.getLabel(),
|
|
394
|
+
labelSingular: M.getLabelSingular(),
|
|
395
|
+
relationship: scope.relationship,
|
|
396
|
+
icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
|
|
397
|
+
relatedSlug: Related.getSlug(),
|
|
398
|
+
},
|
|
399
|
+
parent: {
|
|
400
|
+
id: scope.recordId,
|
|
401
|
+
title: deriveParentTitle(R, parentRecord),
|
|
402
|
+
},
|
|
403
|
+
mode: 'view',
|
|
404
|
+
childId: scope.childId,
|
|
405
|
+
basePath: base,
|
|
406
|
+
layout: cfg.layout,
|
|
407
|
+
schemaData,
|
|
408
|
+
notifications: consumeFlashedNotifications(req),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function buildRelationEditData(pilotiq, R, M, Related, parentRecord, scope, req, user, mode) {
|
|
412
|
+
if (!Related.model) {
|
|
413
|
+
throw new Error(`[Pilotiq] Cannot load child record for ${M.name}: Related Resource ${Related.name} has no static model.`);
|
|
414
|
+
}
|
|
415
|
+
const childPk = getPrimaryKey(Related.model);
|
|
416
|
+
// IDOR check + child load in parallel. Both fail-paths collapse to
|
|
417
|
+
// the same `return null` so behavior matches the serial version; the
|
|
418
|
+
// independent queries (parent's relation accessor vs related model's
|
|
419
|
+
// find) save one RTT on every relation-edit request.
|
|
420
|
+
const [belongs, child] = await Promise.all([
|
|
421
|
+
childBelongsToParent(R.model, parentRecord, scope.relationship, childPk, scope.childId),
|
|
422
|
+
findRecord(Related, scope.childId, { user }).catch(() => undefined),
|
|
423
|
+
]);
|
|
424
|
+
if (!belongs || !child)
|
|
425
|
+
return null;
|
|
426
|
+
if (!await safeManagerPolicy(M, 'canEdit', Related, user, parentRecord, child))
|
|
427
|
+
return { ok: false, status: 403 };
|
|
428
|
+
const cfg = pilotiq.getConfig();
|
|
429
|
+
const base = cfg.path;
|
|
430
|
+
const resourceBase = resourceBasePath(base, R);
|
|
431
|
+
const editUrl = `${resourceBase}/${scope.recordId}/${scope.relationship}/${scope.childId}/edit`;
|
|
432
|
+
const managerCtx = buildRelationManagerCtx(base, scope, parentRecord, Related, mode);
|
|
433
|
+
const form = M.form(Form.make(), managerCtx);
|
|
434
|
+
autoWireManagerForm(form, Related);
|
|
435
|
+
const elements = [form];
|
|
436
|
+
tagFormActions(elements, editUrl);
|
|
437
|
+
// Prefill values: explicit prefill (re-render after 422) wins,
|
|
438
|
+
// otherwise pipe the loaded child through Form's fill pipeline.
|
|
439
|
+
if (scope.prefill?.values) {
|
|
440
|
+
form.withValues(scope.prefill.values);
|
|
441
|
+
if (scope.prefill.errors)
|
|
442
|
+
form.withErrors(scope.prefill.errors);
|
|
443
|
+
}
|
|
444
|
+
else if (child != null) {
|
|
445
|
+
const values = await applyFillPipeline(form, child);
|
|
446
|
+
form.withValues(values);
|
|
447
|
+
}
|
|
448
|
+
const tabs = await buildRelationTabs(R, scope.recordId, base, scope.relationship, user, parentRecord);
|
|
449
|
+
if (tabs)
|
|
450
|
+
elements.unshift(tabs);
|
|
451
|
+
const breadcrumbs = relationEditBreadcrumbs(cfg, R, M, scope.recordId, deriveParentTitle(R, parentRecord), scope.childId, deriveParentTitle(Related, child, M));
|
|
452
|
+
if (breadcrumbs)
|
|
453
|
+
elements.unshift(breadcrumbs);
|
|
454
|
+
const ctx = uploadCtx(userCtx({
|
|
455
|
+
mode: 'edit',
|
|
456
|
+
basePath: base,
|
|
457
|
+
record: child,
|
|
458
|
+
recordId: scope.childId,
|
|
459
|
+
}, user), cfg);
|
|
460
|
+
const relationEditRoute = { resource: R, recordId: scope.childId };
|
|
461
|
+
const [panel, schemaData] = await Promise.all([
|
|
462
|
+
panelInfo(pilotiq, req, relationEditRoute),
|
|
463
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-edit', metas, relationEditRoute)),
|
|
464
|
+
]);
|
|
465
|
+
return {
|
|
466
|
+
pageType: 'relation-edit',
|
|
467
|
+
panel,
|
|
468
|
+
resource: { name: R.name, label: R.labelSingular, slug: scope.slug, icon: serializeIcon(R.icon, R.name) },
|
|
469
|
+
relation: {
|
|
470
|
+
name: M.name,
|
|
471
|
+
label: M.getLabel(),
|
|
472
|
+
labelSingular: M.getLabelSingular(),
|
|
473
|
+
relationship: scope.relationship,
|
|
474
|
+
icon: M.getIcon() ? serializeIcon(M.getIcon(), M.name) : undefined,
|
|
475
|
+
relatedSlug: Related.getSlug(),
|
|
476
|
+
},
|
|
477
|
+
parent: {
|
|
478
|
+
id: scope.recordId,
|
|
479
|
+
title: deriveParentTitle(R, parentRecord),
|
|
480
|
+
},
|
|
481
|
+
mode: 'edit',
|
|
482
|
+
childId: scope.childId,
|
|
483
|
+
basePath: base,
|
|
484
|
+
layout: cfg.layout,
|
|
485
|
+
schemaData,
|
|
486
|
+
notifications: consumeFlashedNotifications(req),
|
|
487
|
+
...(scope.prefill?.errors ? { hasErrors: true } : {}),
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Phase B — resolve a depth-2 chain, running every auth + IDOR layer:
|
|
492
|
+
* Layer 0 — top-level Resource: cluster gate, R.canAccess.
|
|
493
|
+
* Layer 1 — parent record: R.canEdit(parent) (Phase A gate to manage relations).
|
|
494
|
+
* Layer 2 — first manager M1: relationship discovered, related resource discovered.
|
|
495
|
+
* IDOR #1 — child1 (the leaf parent) must belong to parentRecord under chain[0].relationship.
|
|
496
|
+
* Layer 3 — M1.canView(child1, parent) (Filament-style: must be allowed
|
|
497
|
+
* to view the child to drill into its sub-relations).
|
|
498
|
+
* Layer 4 — second manager M2 lookup; relation type read off Related1.model.
|
|
499
|
+
*
|
|
500
|
+
* The leaf manager's per-scope predicate (canViewAny / canCreate /
|
|
501
|
+
* canView / canEdit) runs inside the per-scope builders below, since
|
|
502
|
+
* each predicate has different arguments.
|
|
503
|
+
*/
|
|
504
|
+
export async function resolveRelationChain(pilotiq, scope, user) {
|
|
505
|
+
const cfg = pilotiq.getConfig();
|
|
506
|
+
const R = cfg.resources.find(r => r.getSlug() === scope.slug);
|
|
507
|
+
if (!R)
|
|
508
|
+
return null;
|
|
509
|
+
// Layer 0 — same gates as the depth-1 pipeline.
|
|
510
|
+
const [clusterOk, accessOk] = await Promise.all([
|
|
511
|
+
R.cluster ? safeBool(() => R.cluster.canAccess(user)) : Promise.resolve(true),
|
|
512
|
+
safeBool(() => R.canAccess(user)),
|
|
513
|
+
]);
|
|
514
|
+
if (!clusterOk || !accessOk)
|
|
515
|
+
return { ok: false, status: 403 };
|
|
516
|
+
if (!R.model) {
|
|
517
|
+
throw new Error(`[Pilotiq] Resource "${R.name}" has nested relations but no static model. ` +
|
|
518
|
+
`Set Resource.model = … or remove the manager.`);
|
|
519
|
+
}
|
|
520
|
+
const [step0, step1] = scope.chain;
|
|
521
|
+
const parentRecord = await findRecord(R, step0.recordId, { user }).catch(() => undefined);
|
|
522
|
+
if (!parentRecord)
|
|
523
|
+
return null;
|
|
524
|
+
// Layer 1 — parent record gate.
|
|
525
|
+
if (!await safeBool(() => R.canEdit(user, parentRecord)))
|
|
526
|
+
return { ok: false, status: 403 };
|
|
527
|
+
// Layer 2 — first manager M1.
|
|
528
|
+
const M1 = findManager(R, step0.relationship);
|
|
529
|
+
if (!M1)
|
|
530
|
+
return null;
|
|
531
|
+
const Related1 = findRelatedResource(M1, R, cfg);
|
|
532
|
+
if (!Related1) {
|
|
533
|
+
throw new Error(`[Pilotiq] RelationManager ${M1.name} on ${R.name} could not resolve its related Resource. ` +
|
|
534
|
+
`Set static relatedResource on the manager, or ensure the parent's model declares relations[${JSON.stringify(M1.getRelationship())}].`);
|
|
535
|
+
}
|
|
536
|
+
if (!Related1.model) {
|
|
537
|
+
throw new Error(`[Pilotiq] Related Resource ${Related1.name} has no static model — ` +
|
|
538
|
+
`cannot resolve nested manager chain through it.`);
|
|
539
|
+
}
|
|
540
|
+
const child1Mode = normalizeRelationMode(getRelationType(R.model, step0.relationship));
|
|
541
|
+
// IDOR #1 + child1 load in parallel — confirm the leaf parent
|
|
542
|
+
// (`step1.recordId`) actually belongs to the top parent under the
|
|
543
|
+
// first relationship key. Independent queries.
|
|
544
|
+
const child1Pk = getPrimaryKey(Related1.model);
|
|
545
|
+
const [belongs1, child1] = await Promise.all([
|
|
546
|
+
childBelongsToParent(R.model, parentRecord, step0.relationship, child1Pk, step1.recordId),
|
|
547
|
+
findRecord(Related1, step1.recordId, { user }).catch(() => undefined),
|
|
548
|
+
]);
|
|
549
|
+
if (!belongs1 || !child1)
|
|
550
|
+
return null;
|
|
551
|
+
// Layer 3 — M1.canView(child1, parent) gate. Filament-style: viewing
|
|
552
|
+
// the child is the prerequisite for entering its nested manager strip.
|
|
553
|
+
if (!await safeManagerPolicy(M1, 'canView', Related1, user, parentRecord, child1))
|
|
554
|
+
return { ok: false, status: 403 };
|
|
555
|
+
// Layer 4 — second manager M2 declared under M1.relations().
|
|
556
|
+
const M2 = M1.relations().find(N => {
|
|
557
|
+
try {
|
|
558
|
+
return N.getRelationship() === step1.relationship;
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
if (!M2)
|
|
565
|
+
return null;
|
|
566
|
+
const Related2 = findRelatedResource(M2, Related1, cfg);
|
|
567
|
+
const child2Mode = normalizeRelationMode(getRelationType(Related1.model, step1.relationship));
|
|
568
|
+
return { R, parentRecord, M1, Related1, child1, child1Mode, M2, Related2, child2Mode };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Phase B dispatcher — splits the four nested scopes onto their builders
|
|
572
|
+
* after the shared chain walk. Mirrors the depth-1 `relationManagerData`
|
|
573
|
+
* function shape.
|
|
574
|
+
*/
|
|
575
|
+
async function nestedRelationManagerData(pilotiq, scope, req) {
|
|
576
|
+
const user = await pilotiq.resolveUser(req);
|
|
577
|
+
const resolved = await resolveRelationChain(pilotiq, scope, user);
|
|
578
|
+
if (resolved === null)
|
|
579
|
+
return null;
|
|
580
|
+
if ('ok' in resolved)
|
|
581
|
+
return resolved;
|
|
582
|
+
// For create / view / edit we strictly need a registered Related2 so
|
|
583
|
+
// we can load the leaf record + auto-wire the form save.
|
|
584
|
+
const needRelated2 = scope.kind !== 'nested-relation-list';
|
|
585
|
+
if (needRelated2 && !resolved.Related2) {
|
|
586
|
+
throw new Error(`[Pilotiq] Nested RelationManager ${resolved.M2.name} under ${resolved.M1.name} ` +
|
|
587
|
+
`on ${resolved.R.name} could not resolve its related Resource. ` +
|
|
588
|
+
`Set static relatedResource on the manager, or ensure the parent's model declares ` +
|
|
589
|
+
`relations[${JSON.stringify(resolved.M2.getRelationship())}].`);
|
|
590
|
+
}
|
|
591
|
+
switch (scope.kind) {
|
|
592
|
+
case 'nested-relation-list':
|
|
593
|
+
return buildNestedRelationListData(pilotiq, scope, resolved, req, user);
|
|
594
|
+
case 'nested-relation-create':
|
|
595
|
+
return buildNestedRelationCreateData(pilotiq, scope, resolved, req, user);
|
|
596
|
+
case 'nested-relation-view':
|
|
597
|
+
return buildNestedRelationViewData(pilotiq, scope, resolved, req, user);
|
|
598
|
+
case 'nested-relation-edit':
|
|
599
|
+
return buildNestedRelationEditData(pilotiq, scope, resolved, req, user);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Phase B — build the manager context for a nested leaf manager. The
|
|
603
|
+
* parent here is `child1` (the chain's leaf parent record); the URL
|
|
604
|
+
* prefix comes from `scope.chain[0]` via `Action.relation*` factories
|
|
605
|
+
* reading `ctx.chain`. */
|
|
606
|
+
/** Depth-1 manager context constructor — mirror of `nestedManagerCtx` for
|
|
607
|
+
* the four `relation-*` page roles. Three call sites (list / create / edit)
|
|
608
|
+
* build identical shapes; the helper keeps them in lock-step. */
|
|
609
|
+
function buildRelationManagerCtx(base, scope, parentRecord, Related, mode) {
|
|
610
|
+
return {
|
|
611
|
+
basePath: base,
|
|
612
|
+
parentSlug: scope.slug,
|
|
613
|
+
parentId: scope.recordId,
|
|
614
|
+
relationship: scope.relationship,
|
|
615
|
+
parentRecord,
|
|
616
|
+
related: Related,
|
|
617
|
+
mode,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function nestedManagerCtx(base, scope, resolved) {
|
|
621
|
+
const [step0, step1] = scope.chain;
|
|
622
|
+
return {
|
|
623
|
+
basePath: base,
|
|
624
|
+
parentSlug: resolved.R.getSlug(),
|
|
625
|
+
parentId: step1.recordId, // immediate parent = child1's id
|
|
626
|
+
relationship: step1.relationship, // leaf manager's relationship
|
|
627
|
+
parentRecord: resolved.child1, // immediate parent record = child1
|
|
628
|
+
related: resolved.Related2,
|
|
629
|
+
mode: resolved.child2Mode,
|
|
630
|
+
chain: [{
|
|
631
|
+
slug: resolved.R.getSlug(),
|
|
632
|
+
recordId: step0.recordId,
|
|
633
|
+
relationship: step0.relationship,
|
|
634
|
+
}],
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
/** Phase B — assemble the response shape that mirrors the depth-1
|
|
638
|
+
* builders but adds a `chain` array so renderers can build breadcrumbs
|
|
639
|
+
* and back-links without re-deriving them. */
|
|
640
|
+
function nestedResponseEnvelope(pageType, pilotiq, base, scope, resolved, req) {
|
|
641
|
+
const { R, M1, Related1, child1, M2, Related2 } = resolved;
|
|
642
|
+
const [step0, step1] = scope.chain;
|
|
643
|
+
const parentChildTitle = deriveParentTitle(Related1, child1, M1);
|
|
644
|
+
return {
|
|
645
|
+
pageType,
|
|
646
|
+
resource: { name: R.name, label: R.labelSingular, slug: R.getSlug(), icon: serializeIcon(R.icon, R.name) },
|
|
647
|
+
parentRelation: {
|
|
648
|
+
name: M1.name,
|
|
649
|
+
relationship: step0.relationship,
|
|
650
|
+
label: M1.getLabel(),
|
|
651
|
+
relatedSlug: Related1.getSlug(),
|
|
652
|
+
},
|
|
653
|
+
parentChild: {
|
|
654
|
+
id: step1.recordId,
|
|
655
|
+
title: parentChildTitle,
|
|
656
|
+
},
|
|
657
|
+
relation: {
|
|
658
|
+
name: M2.name,
|
|
659
|
+
relationship: step1.relationship,
|
|
660
|
+
label: M2.getLabel(),
|
|
661
|
+
labelSingular: M2.getLabelSingular(),
|
|
662
|
+
icon: M2.getIcon() ? serializeIcon(M2.getIcon(), M2.name) : undefined,
|
|
663
|
+
relatedSlug: Related2?.getSlug(),
|
|
664
|
+
},
|
|
665
|
+
parent: {
|
|
666
|
+
// Top-of-chain record — same shape the depth-1 builders ship as
|
|
667
|
+
// `parent` so renderers can reuse the back-to-resource link.
|
|
668
|
+
id: step0.recordId,
|
|
669
|
+
title: deriveParentTitle(R, resolved.parentRecord),
|
|
670
|
+
},
|
|
671
|
+
basePath: base,
|
|
672
|
+
layout: pilotiq.getConfig().layout,
|
|
673
|
+
notifications: consumeFlashedNotifications(req),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function buildNestedRelationListData(pilotiq, scope, resolved, req, user) {
|
|
677
|
+
const { Related1, child1, M2, Related2 } = resolved;
|
|
678
|
+
if (!await safeManagerPolicy(M2, 'canViewAny', Related2, user, child1))
|
|
679
|
+
return { ok: false, status: 403 };
|
|
680
|
+
const cfg = pilotiq.getConfig();
|
|
681
|
+
const base = cfg.path;
|
|
682
|
+
const [step0, step1] = scope.chain;
|
|
683
|
+
const resourceBase = resourceBasePath(base, resolved.R);
|
|
684
|
+
const listUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}`;
|
|
685
|
+
const managerCtx = nestedManagerCtx(base, scope, resolved);
|
|
686
|
+
const table = M2.table(Table.make(), managerCtx);
|
|
687
|
+
if (Related1.model) {
|
|
688
|
+
autoWireManagerTable(table, Related1.model, child1, step1.relationship);
|
|
689
|
+
}
|
|
690
|
+
injectManagerTrashedFilter(table, Related2);
|
|
691
|
+
const ctx = uploadCtx(userCtx({
|
|
692
|
+
mode: 'table',
|
|
693
|
+
basePath: base,
|
|
694
|
+
record: child1,
|
|
695
|
+
}, user), cfg);
|
|
696
|
+
const elements = [table];
|
|
697
|
+
tagActionDispatch(elements, listUrl);
|
|
698
|
+
const [, tabs] = await Promise.all([
|
|
699
|
+
loadTableRecords(elements, scope.query ?? {}, listUrl, user),
|
|
700
|
+
buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1),
|
|
701
|
+
]);
|
|
702
|
+
if (tabs)
|
|
703
|
+
elements.unshift(tabs);
|
|
704
|
+
const breadcrumbs = nestedRelationListBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1));
|
|
705
|
+
if (breadcrumbs)
|
|
706
|
+
elements.unshift(breadcrumbs);
|
|
707
|
+
const nestedListRoute = { resource: resolved.R, recordId: scope.chain[1].recordId };
|
|
708
|
+
const [panel, schemaData] = await Promise.all([
|
|
709
|
+
panelInfo(pilotiq, req, nestedListRoute),
|
|
710
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-list', metas, nestedListRoute)),
|
|
711
|
+
]);
|
|
712
|
+
return {
|
|
713
|
+
...nestedResponseEnvelope('nested-relation-list', pilotiq, base, scope, resolved, req),
|
|
714
|
+
panel,
|
|
715
|
+
schemaData,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async function buildNestedRelationCreateData(pilotiq, scope, resolved, req, user) {
|
|
719
|
+
const { child1, M2, Related2 } = resolved;
|
|
720
|
+
if (!await safeManagerPolicy(M2, 'canCreate', Related2, user, child1))
|
|
721
|
+
return { ok: false, status: 403 };
|
|
722
|
+
const cfg = pilotiq.getConfig();
|
|
723
|
+
const base = cfg.path;
|
|
724
|
+
const [step0, step1] = scope.chain;
|
|
725
|
+
const resourceBase = resourceBasePath(base, resolved.R);
|
|
726
|
+
const createUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/create`;
|
|
727
|
+
const managerCtx = nestedManagerCtx(base, scope, resolved);
|
|
728
|
+
const form = M2.form(Form.make(), managerCtx);
|
|
729
|
+
if (Related2?.model)
|
|
730
|
+
autoWireManagerForm(form, Related2);
|
|
731
|
+
const elements = [form];
|
|
732
|
+
tagFormActions(elements, createUrl);
|
|
733
|
+
if (scope.prefill) {
|
|
734
|
+
if (scope.prefill.values)
|
|
735
|
+
form.withValues(scope.prefill.values);
|
|
736
|
+
if (scope.prefill.errors)
|
|
737
|
+
form.withErrors(scope.prefill.errors);
|
|
738
|
+
}
|
|
739
|
+
const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1);
|
|
740
|
+
if (tabs)
|
|
741
|
+
elements.unshift(tabs);
|
|
742
|
+
const breadcrumbs = nestedRelationCreateBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(resolved.Related1, child1, resolved.M1));
|
|
743
|
+
if (breadcrumbs)
|
|
744
|
+
elements.unshift(breadcrumbs);
|
|
745
|
+
const ctx = uploadCtx(userCtx({
|
|
746
|
+
mode: 'create',
|
|
747
|
+
basePath: base,
|
|
748
|
+
record: child1,
|
|
749
|
+
}, user), cfg);
|
|
750
|
+
const nestedCreateRoute = { resource: resolved.R, recordId: scope.chain[1].recordId };
|
|
751
|
+
const [panel, schemaData] = await Promise.all([
|
|
752
|
+
panelInfo(pilotiq, req, nestedCreateRoute),
|
|
753
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-create', metas, nestedCreateRoute)),
|
|
754
|
+
]);
|
|
755
|
+
return {
|
|
756
|
+
...nestedResponseEnvelope('nested-relation-create', pilotiq, base, scope, resolved, req),
|
|
757
|
+
panel,
|
|
758
|
+
mode: 'create',
|
|
759
|
+
schemaData,
|
|
760
|
+
...(scope.prefill?.errors ? { hasErrors: true } : {}),
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
async function buildNestedRelationViewData(pilotiq, scope, resolved, req, user) {
|
|
764
|
+
const { Related1, child1, M2, Related2 } = resolved;
|
|
765
|
+
if (!Related2?.model) {
|
|
766
|
+
throw new Error(`[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
|
|
767
|
+
`Related Resource ${Related2?.name ?? '(none)'} has no static model.`);
|
|
768
|
+
}
|
|
769
|
+
const [, step1] = scope.chain;
|
|
770
|
+
const child2Pk = getPrimaryKey(Related2.model);
|
|
771
|
+
const [belongs2, child2] = await Promise.all([
|
|
772
|
+
childBelongsToParent(Related1.model, child1, step1.relationship, child2Pk, scope.childId),
|
|
773
|
+
findRecord(Related2, scope.childId, { user }).catch(() => undefined),
|
|
774
|
+
]);
|
|
775
|
+
if (!belongs2 || !child2)
|
|
776
|
+
return null;
|
|
777
|
+
if (!await safeManagerPolicy(M2, 'canView', Related2, user, child1, child2))
|
|
778
|
+
return { ok: false, status: 403 };
|
|
779
|
+
const cfg = pilotiq.getConfig();
|
|
780
|
+
const base = cfg.path;
|
|
781
|
+
const elements = M2.detail(child2, child1);
|
|
782
|
+
const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1);
|
|
783
|
+
if (tabs)
|
|
784
|
+
elements.unshift(tabs);
|
|
785
|
+
const breadcrumbs = nestedRelationViewBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1), deriveParentTitle(Related2, child2, M2));
|
|
786
|
+
if (breadcrumbs)
|
|
787
|
+
elements.unshift(breadcrumbs);
|
|
788
|
+
const ctx = uploadCtx(userCtx({
|
|
789
|
+
mode: 'view',
|
|
790
|
+
basePath: base,
|
|
791
|
+
record: child2,
|
|
792
|
+
recordId: scope.childId,
|
|
793
|
+
}, user), cfg);
|
|
794
|
+
const nestedViewRoute = { resource: resolved.R, recordId: scope.childId };
|
|
795
|
+
const [panel, schemaData] = await Promise.all([
|
|
796
|
+
panelInfo(pilotiq, req, nestedViewRoute),
|
|
797
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-view', metas, nestedViewRoute)),
|
|
798
|
+
]);
|
|
799
|
+
return {
|
|
800
|
+
...nestedResponseEnvelope('nested-relation-view', pilotiq, base, scope, resolved, req),
|
|
801
|
+
panel,
|
|
802
|
+
mode: 'view',
|
|
803
|
+
childId: scope.childId,
|
|
804
|
+
schemaData,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
async function buildNestedRelationEditData(pilotiq, scope, resolved, req, user) {
|
|
808
|
+
const { Related1, child1, M2, Related2 } = resolved;
|
|
809
|
+
if (!Related2?.model) {
|
|
810
|
+
throw new Error(`[Pilotiq] Cannot load child record for nested manager ${M2.name}: ` +
|
|
811
|
+
`Related Resource ${Related2?.name ?? '(none)'} has no static model.`);
|
|
812
|
+
}
|
|
813
|
+
const [step0, step1] = scope.chain;
|
|
814
|
+
const child2Pk = getPrimaryKey(Related2.model);
|
|
815
|
+
const [belongs2, child2] = await Promise.all([
|
|
816
|
+
childBelongsToParent(Related1.model, child1, step1.relationship, child2Pk, scope.childId),
|
|
817
|
+
findRecord(Related2, scope.childId, { user }).catch(() => undefined),
|
|
818
|
+
]);
|
|
819
|
+
if (!belongs2 || !child2)
|
|
820
|
+
return null;
|
|
821
|
+
if (!await safeManagerPolicy(M2, 'canEdit', Related2, user, child1, child2))
|
|
822
|
+
return { ok: false, status: 403 };
|
|
823
|
+
const cfg = pilotiq.getConfig();
|
|
824
|
+
const base = cfg.path;
|
|
825
|
+
const resourceBase = resourceBasePath(base, resolved.R);
|
|
826
|
+
const editUrl = `${resourceBase}/${step0.recordId}/${step0.relationship}/${step1.recordId}/${step1.relationship}/${scope.childId}/edit`;
|
|
827
|
+
const managerCtx = nestedManagerCtx(base, scope, resolved);
|
|
828
|
+
const form = M2.form(Form.make(), managerCtx);
|
|
829
|
+
autoWireManagerForm(form, Related2);
|
|
830
|
+
const elements = [form];
|
|
831
|
+
tagFormActions(elements, editUrl);
|
|
832
|
+
if (scope.prefill?.values) {
|
|
833
|
+
form.withValues(scope.prefill.values);
|
|
834
|
+
if (scope.prefill.errors)
|
|
835
|
+
form.withErrors(scope.prefill.errors);
|
|
836
|
+
}
|
|
837
|
+
else if (child2 != null) {
|
|
838
|
+
const values = await applyFillPipeline(form, child2);
|
|
839
|
+
form.withValues(values);
|
|
840
|
+
}
|
|
841
|
+
const tabs = await buildNestedRelationTabs(resolved.R, resolved.M1, base, scope.chain[0], scope.chain[1].recordId, scope.chain[1].relationship, user, resolved.child1);
|
|
842
|
+
if (tabs)
|
|
843
|
+
elements.unshift(tabs);
|
|
844
|
+
const breadcrumbs = nestedRelationEditBreadcrumbs(cfg, resolved.R, resolved.M1, M2, scope.chain[0], deriveParentTitle(resolved.R, resolved.parentRecord), scope.chain[1].recordId, deriveParentTitle(Related1, child1, resolved.M1), scope.childId, deriveParentTitle(Related2, child2, M2));
|
|
845
|
+
if (breadcrumbs)
|
|
846
|
+
elements.unshift(breadcrumbs);
|
|
847
|
+
const ctx = uploadCtx(userCtx({
|
|
848
|
+
mode: 'edit',
|
|
849
|
+
basePath: base,
|
|
850
|
+
record: child2,
|
|
851
|
+
recordId: scope.childId,
|
|
852
|
+
}, user), cfg);
|
|
853
|
+
const nestedEditRoute = { resource: resolved.R, recordId: scope.childId };
|
|
854
|
+
const [panel, schemaData] = await Promise.all([
|
|
855
|
+
panelInfo(pilotiq, req, nestedEditRoute),
|
|
856
|
+
resolveSchema(elements, ctx).then(metas => applyRoleHooks(pilotiq, user, 'relation-edit', metas, nestedEditRoute)),
|
|
857
|
+
]);
|
|
858
|
+
return {
|
|
859
|
+
...nestedResponseEnvelope('nested-relation-edit', pilotiq, base, scope, resolved, req),
|
|
860
|
+
panel,
|
|
861
|
+
mode: 'edit',
|
|
862
|
+
childId: scope.childId,
|
|
863
|
+
schemaData,
|
|
864
|
+
...(scope.prefill?.errors ? { hasErrors: true } : {}),
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
//# sourceMappingURL=relationPages.js.map
|