@pilotiq/pilotiq 0.1.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 +4 -0
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +207 -0
- package/LICENSE +21 -0
- package/dist/Cluster.d.ts +56 -0
- package/dist/Cluster.d.ts.map +1 -0
- package/dist/Cluster.js +62 -0
- package/dist/Cluster.js.map +1 -0
- package/dist/Column.d.ts +378 -0
- package/dist/Column.d.ts.map +1 -0
- package/dist/Column.js +434 -0
- package/dist/Column.js.map +1 -0
- package/dist/Global.d.ts +123 -0
- package/dist/Global.d.ts.map +1 -0
- package/dist/Global.js +124 -0
- package/dist/Global.js.map +1 -0
- package/dist/Page.d.ts +90 -0
- package/dist/Page.d.ts.map +1 -0
- package/dist/Page.js +107 -0
- package/dist/Page.js.map +1 -0
- package/dist/Pilotiq.d.ts +505 -0
- package/dist/Pilotiq.d.ts.map +1 -0
- package/dist/Pilotiq.js +463 -0
- package/dist/Pilotiq.js.map +1 -0
- package/dist/PilotiqRegistry.d.ts +10 -0
- package/dist/PilotiqRegistry.d.ts.map +1 -0
- package/dist/PilotiqRegistry.js +32 -0
- package/dist/PilotiqRegistry.js.map +1 -0
- package/dist/PilotiqServiceProvider.d.ts +16 -0
- package/dist/PilotiqServiceProvider.d.ts.map +1 -0
- package/dist/PilotiqServiceProvider.js +57 -0
- package/dist/PilotiqServiceProvider.js.map +1 -0
- package/dist/RelationManager.d.ts +372 -0
- package/dist/RelationManager.d.ts.map +1 -0
- package/dist/RelationManager.js +342 -0
- package/dist/RelationManager.js.map +1 -0
- package/dist/RenderHook.d.ts +86 -0
- package/dist/RenderHook.d.ts.map +1 -0
- package/dist/RenderHook.js +116 -0
- package/dist/RenderHook.js.map +1 -0
- package/dist/Resource.d.ts +290 -0
- package/dist/Resource.d.ts.map +1 -0
- package/dist/Resource.js +362 -0
- package/dist/Resource.js.map +1 -0
- package/dist/RightPanel.d.ts +92 -0
- package/dist/RightPanel.d.ts.map +1 -0
- package/dist/RightPanel.js +61 -0
- package/dist/RightPanel.js.map +1 -0
- package/dist/Tab.d.ts +92 -0
- package/dist/Tab.d.ts.map +1 -0
- package/dist/Tab.js +93 -0
- package/dist/Tab.js.map +1 -0
- package/dist/UserMenuItem.d.ts +76 -0
- package/dist/UserMenuItem.d.ts.map +1 -0
- package/dist/UserMenuItem.js +87 -0
- package/dist/UserMenuItem.js.map +1 -0
- package/dist/actions/Action.d.ts +888 -0
- package/dist/actions/Action.d.ts.map +1 -0
- package/dist/actions/Action.js +1652 -0
- package/dist/actions/Action.js.map +1 -0
- package/dist/actions/ActionGroup.d.ts +85 -0
- package/dist/actions/ActionGroup.d.ts.map +1 -0
- package/dist/actions/ActionGroup.js +132 -0
- package/dist/actions/ActionGroup.js.map +1 -0
- package/dist/actions/attachFactory.d.ts +67 -0
- package/dist/actions/attachFactory.d.ts.map +1 -0
- package/dist/actions/attachFactory.js +115 -0
- package/dist/actions/attachFactory.js.map +1 -0
- package/dist/actions/exportFactory.d.ts +88 -0
- package/dist/actions/exportFactory.d.ts.map +1 -0
- package/dist/actions/exportFactory.js +144 -0
- package/dist/actions/exportFactory.js.map +1 -0
- package/dist/actions/importFactory.d.ts +97 -0
- package/dist/actions/importFactory.d.ts.map +1 -0
- package/dist/actions/importFactory.js +143 -0
- package/dist/actions/importFactory.js.map +1 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/applyPageHooks.d.ts +54 -0
- package/dist/applyPageHooks.d.ts.map +1 -0
- package/dist/applyPageHooks.js +149 -0
- package/dist/applyPageHooks.js.map +1 -0
- package/dist/cells/coerce.d.ts +25 -0
- package/dist/cells/coerce.d.ts.map +1 -0
- package/dist/cells/coerce.js +87 -0
- package/dist/cells/coerce.js.map +1 -0
- package/dist/clusterPaths.d.ts +9 -0
- package/dist/clusterPaths.d.ts.map +1 -0
- package/dist/clusterPaths.js +19 -0
- package/dist/clusterPaths.js.map +1 -0
- package/dist/columns/BadgeColumn.d.ts +16 -0
- package/dist/columns/BadgeColumn.d.ts.map +1 -0
- package/dist/columns/BadgeColumn.js +25 -0
- package/dist/columns/BadgeColumn.js.map +1 -0
- package/dist/columns/BooleanColumn.d.ts +10 -0
- package/dist/columns/BooleanColumn.d.ts.map +1 -0
- package/dist/columns/BooleanColumn.js +18 -0
- package/dist/columns/BooleanColumn.js.map +1 -0
- package/dist/columns/ColorColumn.d.ts +27 -0
- package/dist/columns/ColorColumn.d.ts.map +1 -0
- package/dist/columns/ColorColumn.js +35 -0
- package/dist/columns/ColorColumn.js.map +1 -0
- package/dist/columns/IconColumn.d.ts +22 -0
- package/dist/columns/IconColumn.d.ts.map +1 -0
- package/dist/columns/IconColumn.js +28 -0
- package/dist/columns/IconColumn.js.map +1 -0
- package/dist/columns/ImageColumn.d.ts +17 -0
- package/dist/columns/ImageColumn.d.ts.map +1 -0
- package/dist/columns/ImageColumn.js +24 -0
- package/dist/columns/ImageColumn.js.map +1 -0
- package/dist/columns/SelectColumn.d.ts +36 -0
- package/dist/columns/SelectColumn.d.ts.map +1 -0
- package/dist/columns/SelectColumn.js +52 -0
- package/dist/columns/SelectColumn.js.map +1 -0
- package/dist/columns/TextColumn.d.ts +18 -0
- package/dist/columns/TextColumn.d.ts.map +1 -0
- package/dist/columns/TextColumn.js +20 -0
- package/dist/columns/TextColumn.js.map +1 -0
- package/dist/columns/TextInputColumn.d.ts +47 -0
- package/dist/columns/TextInputColumn.d.ts.map +1 -0
- package/dist/columns/TextInputColumn.js +60 -0
- package/dist/columns/TextInputColumn.js.map +1 -0
- package/dist/columns/ToggleColumn.d.ts +32 -0
- package/dist/columns/ToggleColumn.d.ts.map +1 -0
- package/dist/columns/ToggleColumn.js +45 -0
- package/dist/columns/ToggleColumn.js.map +1 -0
- package/dist/columns/index.d.ts +10 -0
- package/dist/columns/index.d.ts.map +1 -0
- package/dist/columns/index.js +10 -0
- package/dist/columns/index.js.map +1 -0
- package/dist/defaultGlobalPages.d.ts +11 -0
- package/dist/defaultGlobalPages.d.ts.map +1 -0
- package/dist/defaultGlobalPages.js +87 -0
- package/dist/defaultGlobalPages.js.map +1 -0
- package/dist/defaultPages.d.ts +247 -0
- package/dist/defaultPages.d.ts.map +1 -0
- package/dist/defaultPages.js +558 -0
- package/dist/defaultPages.js.map +1 -0
- package/dist/elements/Form.d.ts +219 -0
- package/dist/elements/Form.d.ts.map +1 -0
- package/dist/elements/Form.js +259 -0
- package/dist/elements/Form.js.map +1 -0
- package/dist/elements/ListTabs.d.ts +17 -0
- package/dist/elements/ListTabs.d.ts.map +1 -0
- package/dist/elements/ListTabs.js +23 -0
- package/dist/elements/ListTabs.js.map +1 -0
- package/dist/elements/Table.d.ts +535 -0
- package/dist/elements/Table.d.ts.map +1 -0
- package/dist/elements/Table.js +481 -0
- package/dist/elements/Table.js.map +1 -0
- package/dist/elements/TableGroup.d.ts +121 -0
- package/dist/elements/TableGroup.d.ts.map +1 -0
- package/dist/elements/TableGroup.js +162 -0
- package/dist/elements/TableGroup.js.map +1 -0
- package/dist/elements/dispatchAction.d.ts +127 -0
- package/dist/elements/dispatchAction.d.ts.map +1 -0
- package/dist/elements/dispatchAction.js +254 -0
- package/dist/elements/dispatchAction.js.map +1 -0
- package/dist/elements/dispatchForm.d.ts +220 -0
- package/dist/elements/dispatchForm.d.ts.map +1 -0
- package/dist/elements/dispatchForm.js +1645 -0
- package/dist/elements/dispatchForm.js.map +1 -0
- package/dist/elements/dispatchTable.d.ts +69 -0
- package/dist/elements/dispatchTable.d.ts.map +1 -0
- package/dist/elements/dispatchTable.js +606 -0
- package/dist/elements/dispatchTable.js.map +1 -0
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.d.ts.map +1 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/index.js.map +1 -0
- package/dist/entries/BadgeEntry.d.ts +21 -0
- package/dist/entries/BadgeEntry.d.ts.map +1 -0
- package/dist/entries/BadgeEntry.js +32 -0
- package/dist/entries/BadgeEntry.js.map +1 -0
- package/dist/entries/CodeEntry.d.ts +38 -0
- package/dist/entries/CodeEntry.d.ts.map +1 -0
- package/dist/entries/CodeEntry.js +44 -0
- package/dist/entries/CodeEntry.js.map +1 -0
- package/dist/entries/ColorEntry.d.ts +32 -0
- package/dist/entries/ColorEntry.d.ts.map +1 -0
- package/dist/entries/ColorEntry.js +48 -0
- package/dist/entries/ColorEntry.js.map +1 -0
- package/dist/entries/ComponentEntry.d.ts +66 -0
- package/dist/entries/ComponentEntry.d.ts.map +1 -0
- package/dist/entries/ComponentEntry.js +86 -0
- package/dist/entries/ComponentEntry.js.map +1 -0
- package/dist/entries/Entry.d.ts +175 -0
- package/dist/entries/Entry.d.ts.map +1 -0
- package/dist/entries/Entry.js +233 -0
- package/dist/entries/Entry.js.map +1 -0
- package/dist/entries/IconEntry.d.ts +30 -0
- package/dist/entries/IconEntry.d.ts.map +1 -0
- package/dist/entries/IconEntry.js +34 -0
- package/dist/entries/IconEntry.js.map +1 -0
- package/dist/entries/ImageEntry.d.ts +33 -0
- package/dist/entries/ImageEntry.d.ts.map +1 -0
- package/dist/entries/ImageEntry.js +47 -0
- package/dist/entries/ImageEntry.js.map +1 -0
- package/dist/entries/KeyValueEntry.d.ts +30 -0
- package/dist/entries/KeyValueEntry.d.ts.map +1 -0
- package/dist/entries/KeyValueEntry.js +38 -0
- package/dist/entries/KeyValueEntry.js.map +1 -0
- package/dist/entries/RepeatableEntry.d.ts +122 -0
- package/dist/entries/RepeatableEntry.d.ts.map +1 -0
- package/dist/entries/RepeatableEntry.js +121 -0
- package/dist/entries/RepeatableEntry.js.map +1 -0
- package/dist/entries/TextEntry.d.ts +38 -0
- package/dist/entries/TextEntry.d.ts.map +1 -0
- package/dist/entries/TextEntry.js +49 -0
- package/dist/entries/TextEntry.js.map +1 -0
- package/dist/entries/index.d.ts +2 -0
- package/dist/entries/index.d.ts.map +1 -0
- package/dist/entries/index.js +8 -0
- package/dist/entries/index.js.map +1 -0
- package/dist/entries/registry.d.ts +41 -0
- package/dist/entries/registry.d.ts.map +1 -0
- package/dist/entries/registry.js +17 -0
- package/dist/entries/registry.js.map +1 -0
- package/dist/fields/BuilderField.d.ts +420 -0
- package/dist/fields/BuilderField.d.ts.map +1 -0
- package/dist/fields/BuilderField.js +359 -0
- package/dist/fields/BuilderField.js.map +1 -0
- package/dist/fields/CheckboxField.d.ts +18 -0
- package/dist/fields/CheckboxField.d.ts.map +1 -0
- package/dist/fields/CheckboxField.js +23 -0
- package/dist/fields/CheckboxField.js.map +1 -0
- package/dist/fields/CheckboxListField.d.ts +25 -0
- package/dist/fields/CheckboxListField.d.ts.map +1 -0
- package/dist/fields/CheckboxListField.js +46 -0
- package/dist/fields/CheckboxListField.js.map +1 -0
- package/dist/fields/ColorPickerField.d.ts +16 -0
- package/dist/fields/ColorPickerField.d.ts.map +1 -0
- package/dist/fields/ColorPickerField.js +21 -0
- package/dist/fields/ColorPickerField.js.map +1 -0
- package/dist/fields/DateField.d.ts +29 -0
- package/dist/fields/DateField.d.ts.map +1 -0
- package/dist/fields/DateField.js +45 -0
- package/dist/fields/DateField.js.map +1 -0
- package/dist/fields/EmailField.d.ts +8 -0
- package/dist/fields/EmailField.d.ts.map +1 -0
- package/dist/fields/EmailField.js +13 -0
- package/dist/fields/EmailField.js.map +1 -0
- package/dist/fields/Field.d.ts +485 -0
- package/dist/fields/Field.d.ts.map +1 -0
- package/dist/fields/Field.js +539 -0
- package/dist/fields/Field.js.map +1 -0
- package/dist/fields/FileUploadField.d.ts +43 -0
- package/dist/fields/FileUploadField.d.ts.map +1 -0
- package/dist/fields/FileUploadField.js +60 -0
- package/dist/fields/FileUploadField.js.map +1 -0
- package/dist/fields/HiddenField.d.ts +19 -0
- package/dist/fields/HiddenField.d.ts.map +1 -0
- package/dist/fields/HiddenField.js +24 -0
- package/dist/fields/HiddenField.js.map +1 -0
- package/dist/fields/KeyValueField.d.ts +36 -0
- package/dist/fields/KeyValueField.d.ts.map +1 -0
- package/dist/fields/KeyValueField.js +47 -0
- package/dist/fields/KeyValueField.js.map +1 -0
- package/dist/fields/MarkdownField.d.ts +79 -0
- package/dist/fields/MarkdownField.d.ts.map +1 -0
- package/dist/fields/MarkdownField.js +117 -0
- package/dist/fields/MarkdownField.js.map +1 -0
- package/dist/fields/NumberField.d.ts +17 -0
- package/dist/fields/NumberField.d.ts.map +1 -0
- package/dist/fields/NumberField.js +27 -0
- package/dist/fields/NumberField.js.map +1 -0
- package/dist/fields/RadioField.d.ts +26 -0
- package/dist/fields/RadioField.d.ts.map +1 -0
- package/dist/fields/RadioField.js +47 -0
- package/dist/fields/RadioField.js.map +1 -0
- package/dist/fields/RepeaterField.d.ts +594 -0
- package/dist/fields/RepeaterField.d.ts.map +1 -0
- package/dist/fields/RepeaterField.js +504 -0
- package/dist/fields/RepeaterField.js.map +1 -0
- package/dist/fields/RowButton.d.ts +86 -0
- package/dist/fields/RowButton.d.ts.map +1 -0
- package/dist/fields/RowButton.js +85 -0
- package/dist/fields/RowButton.js.map +1 -0
- package/dist/fields/SelectField.d.ts +127 -0
- package/dist/fields/SelectField.d.ts.map +1 -0
- package/dist/fields/SelectField.js +160 -0
- package/dist/fields/SelectField.js.map +1 -0
- package/dist/fields/SliderField.d.ts +31 -0
- package/dist/fields/SliderField.d.ts.map +1 -0
- package/dist/fields/SliderField.js +45 -0
- package/dist/fields/SliderField.js.map +1 -0
- package/dist/fields/SlugField.d.ts +11 -0
- package/dist/fields/SlugField.d.ts.map +1 -0
- package/dist/fields/SlugField.js +19 -0
- package/dist/fields/SlugField.js.map +1 -0
- package/dist/fields/TagsInputField.d.ts +65 -0
- package/dist/fields/TagsInputField.d.ts.map +1 -0
- package/dist/fields/TagsInputField.js +104 -0
- package/dist/fields/TagsInputField.js.map +1 -0
- package/dist/fields/TextField.d.ts +11 -0
- package/dist/fields/TextField.d.ts.map +1 -0
- package/dist/fields/TextField.js +19 -0
- package/dist/fields/TextField.js.map +1 -0
- package/dist/fields/TextareaField.d.ts +40 -0
- package/dist/fields/TextareaField.d.ts.map +1 -0
- package/dist/fields/TextareaField.js +51 -0
- package/dist/fields/TextareaField.js.map +1 -0
- package/dist/fields/ToggleButtonsField.d.ts +24 -0
- package/dist/fields/ToggleButtonsField.d.ts.map +1 -0
- package/dist/fields/ToggleButtonsField.js +41 -0
- package/dist/fields/ToggleButtonsField.js.map +1 -0
- package/dist/fields/ToggleField.d.ts +8 -0
- package/dist/fields/ToggleField.d.ts.map +1 -0
- package/dist/fields/ToggleField.js +13 -0
- package/dist/fields/ToggleField.js.map +1 -0
- package/dist/fields/optionsResolver.d.ts +54 -0
- package/dist/fields/optionsResolver.d.ts.map +1 -0
- package/dist/fields/optionsResolver.js +62 -0
- package/dist/fields/optionsResolver.js.map +1 -0
- package/dist/fields/resolveField.d.ts +21 -0
- package/dist/fields/resolveField.d.ts.map +1 -0
- package/dist/fields/resolveField.js +26 -0
- package/dist/fields/resolveField.js.map +1 -0
- package/dist/filters/BooleanFilter.d.ts +20 -0
- package/dist/filters/BooleanFilter.d.ts.map +1 -0
- package/dist/filters/BooleanFilter.js +31 -0
- package/dist/filters/BooleanFilter.js.map +1 -0
- package/dist/filters/DateRangeFilter.d.ts +68 -0
- package/dist/filters/DateRangeFilter.d.ts.map +1 -0
- package/dist/filters/DateRangeFilter.js +137 -0
- package/dist/filters/DateRangeFilter.js.map +1 -0
- package/dist/filters/Filter.d.ts +140 -0
- package/dist/filters/Filter.d.ts.map +1 -0
- package/dist/filters/Filter.js +99 -0
- package/dist/filters/Filter.js.map +1 -0
- package/dist/filters/FormFilter.d.ts +103 -0
- package/dist/filters/FormFilter.d.ts.map +1 -0
- package/dist/filters/FormFilter.js +180 -0
- package/dist/filters/FormFilter.js.map +1 -0
- package/dist/filters/MultiSelectFilter.d.ts +41 -0
- package/dist/filters/MultiSelectFilter.d.ts.map +1 -0
- package/dist/filters/MultiSelectFilter.js +67 -0
- package/dist/filters/MultiSelectFilter.js.map +1 -0
- package/dist/filters/QueryBuilderFilter.d.ts +145 -0
- package/dist/filters/QueryBuilderFilter.d.ts.map +1 -0
- package/dist/filters/QueryBuilderFilter.js +323 -0
- package/dist/filters/QueryBuilderFilter.js.map +1 -0
- package/dist/filters/SelectFilter.d.ts +26 -0
- package/dist/filters/SelectFilter.d.ts.map +1 -0
- package/dist/filters/SelectFilter.js +35 -0
- package/dist/filters/SelectFilter.js.map +1 -0
- package/dist/filters/TernaryFilter.d.ts +35 -0
- package/dist/filters/TernaryFilter.d.ts.map +1 -0
- package/dist/filters/TernaryFilter.js +71 -0
- package/dist/filters/TernaryFilter.js.map +1 -0
- package/dist/filters/TrashedFilter.d.ts +28 -0
- package/dist/filters/TrashedFilter.d.ts.map +1 -0
- package/dist/filters/TrashedFilter.js +52 -0
- package/dist/filters/TrashedFilter.js.map +1 -0
- package/dist/filters/queryBuilder/BooleanConstraint.d.ts +13 -0
- package/dist/filters/queryBuilder/BooleanConstraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/BooleanConstraint.js +27 -0
- package/dist/filters/queryBuilder/BooleanConstraint.js.map +1 -0
- package/dist/filters/queryBuilder/Constraint.d.ts +74 -0
- package/dist/filters/queryBuilder/Constraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/Constraint.js +45 -0
- package/dist/filters/queryBuilder/Constraint.js.map +1 -0
- package/dist/filters/queryBuilder/DateConstraint.d.ts +18 -0
- package/dist/filters/queryBuilder/DateConstraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/DateConstraint.js +63 -0
- package/dist/filters/queryBuilder/DateConstraint.js.map +1 -0
- package/dist/filters/queryBuilder/NumberConstraint.d.ts +12 -0
- package/dist/filters/queryBuilder/NumberConstraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/NumberConstraint.js +61 -0
- package/dist/filters/queryBuilder/NumberConstraint.js.map +1 -0
- package/dist/filters/queryBuilder/SelectConstraint.d.ts +22 -0
- package/dist/filters/queryBuilder/SelectConstraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/SelectConstraint.js +66 -0
- package/dist/filters/queryBuilder/SelectConstraint.js.map +1 -0
- package/dist/filters/queryBuilder/TextConstraint.d.ts +18 -0
- package/dist/filters/queryBuilder/TextConstraint.d.ts.map +1 -0
- package/dist/filters/queryBuilder/TextConstraint.js +58 -0
- package/dist/filters/queryBuilder/TextConstraint.js.map +1 -0
- package/dist/filters/queryBuilder/index.d.ts +7 -0
- package/dist/filters/queryBuilder/index.d.ts.map +1 -0
- package/dist/filters/queryBuilder/index.js +7 -0
- package/dist/filters/queryBuilder/index.js.map +1 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.d.ts.map +1 -0
- package/dist/icons/index.js +3 -0
- package/dist/icons/index.js.map +1 -0
- package/dist/icons/lucide.d.ts +16 -0
- package/dist/icons/lucide.d.ts.map +1 -0
- package/dist/icons/lucide.js +173 -0
- package/dist/icons/lucide.js.map +1 -0
- package/dist/icons/registry.d.ts +27 -0
- package/dist/icons/registry.d.ts.map +1 -0
- package/dist/icons/registry.js +35 -0
- package/dist/icons/registry.js.map +1 -0
- package/dist/icons/types.d.ts +38 -0
- package/dist/icons/types.d.ts.map +1 -0
- package/dist/icons/types.js +23 -0
- package/dist/icons/types.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/index.js.map +1 -0
- package/dist/io/csv.d.ts +51 -0
- package/dist/io/csv.d.ts.map +1 -0
- package/dist/io/csv.js +168 -0
- package/dist/io/csv.js.map +1 -0
- package/dist/notifications/Notification.d.ts +181 -0
- package/dist/notifications/Notification.d.ts.map +1 -0
- package/dist/notifications/Notification.js +290 -0
- package/dist/notifications/Notification.js.map +1 -0
- package/dist/notifications/broadcast.d.ts +58 -0
- package/dist/notifications/broadcast.d.ts.map +1 -0
- package/dist/notifications/broadcast.js +72 -0
- package/dist/notifications/broadcast.js.map +1 -0
- package/dist/notifications/database.d.ts +164 -0
- package/dist/notifications/database.d.ts.map +1 -0
- package/dist/notifications/database.js +321 -0
- package/dist/notifications/database.js.map +1 -0
- package/dist/notifications/dispatchNotificationAction.d.ts +48 -0
- package/dist/notifications/dispatchNotificationAction.d.ts.map +1 -0
- package/dist/notifications/dispatchNotificationAction.js +100 -0
- package/dist/notifications/dispatchNotificationAction.js.map +1 -0
- package/dist/notifications/flash.d.ts +34 -0
- package/dist/notifications/flash.d.ts.map +1 -0
- package/dist/notifications/flash.js +51 -0
- package/dist/notifications/flash.js.map +1 -0
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +6 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/registerBroadcastAuth.d.ts +45 -0
- package/dist/notifications/registerBroadcastAuth.d.ts.map +1 -0
- package/dist/notifications/registerBroadcastAuth.js +86 -0
- package/dist/notifications/registerBroadcastAuth.js.map +1 -0
- package/dist/notifications/resolveSavedNotification.d.ts +21 -0
- package/dist/notifications/resolveSavedNotification.d.ts.map +1 -0
- package/dist/notifications/resolveSavedNotification.js +43 -0
- package/dist/notifications/resolveSavedNotification.js.map +1 -0
- package/dist/notifications/types.d.ts +87 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +2 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/orm/m2mAccessor.d.ts +49 -0
- package/dist/orm/m2mAccessor.d.ts.map +1 -0
- package/dist/orm/m2mAccessor.js +45 -0
- package/dist/orm/m2mAccessor.js.map +1 -0
- package/dist/orm/modelDefaults.d.ts +347 -0
- package/dist/orm/modelDefaults.d.ts.map +1 -0
- package/dist/orm/modelDefaults.js +375 -0
- package/dist/orm/modelDefaults.js.map +1 -0
- package/dist/pageData.d.ts +778 -0
- package/dist/pageData.d.ts.map +1 -0
- package/dist/pageData.js +3725 -0
- package/dist/pageData.js.map +1 -0
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +2 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/themeEditor.d.ts +17 -0
- package/dist/plugins/themeEditor.d.ts.map +1 -0
- package/dist/plugins/themeEditor.js +23 -0
- package/dist/plugins/themeEditor.js.map +1 -0
- package/dist/react/AppShell.d.ts +58 -0
- package/dist/react/AppShell.d.ts.map +1 -0
- package/dist/react/AppShell.js +58 -0
- package/dist/react/AppShell.js.map +1 -0
- package/dist/react/CommandPalette.d.ts +21 -0
- package/dist/react/CommandPalette.d.ts.map +1 -0
- package/dist/react/CommandPalette.js +236 -0
- package/dist/react/CommandPalette.js.map +1 -0
- package/dist/react/FormStateContext.d.ts +83 -0
- package/dist/react/FormStateContext.d.ts.map +1 -0
- package/dist/react/FormStateContext.js +284 -0
- package/dist/react/FormStateContext.js.map +1 -0
- package/dist/react/HeadHooks.d.ts +26 -0
- package/dist/react/HeadHooks.d.ts.map +1 -0
- package/dist/react/HeadHooks.js +141 -0
- package/dist/react/HeadHooks.js.map +1 -0
- package/dist/react/NotificationActionStrip.d.ts +39 -0
- package/dist/react/NotificationActionStrip.d.ts.map +1 -0
- package/dist/react/NotificationActionStrip.js +129 -0
- package/dist/react/NotificationActionStrip.js.map +1 -0
- package/dist/react/NotificationBell.d.ts +20 -0
- package/dist/react/NotificationBell.d.ts.map +1 -0
- package/dist/react/NotificationBell.js +273 -0
- package/dist/react/NotificationBell.js.map +1 -0
- package/dist/react/RenderHookSlot.d.ts +20 -0
- package/dist/react/RenderHookSlot.d.ts.map +1 -0
- package/dist/react/RenderHookSlot.js +24 -0
- package/dist/react/RenderHookSlot.js.map +1 -0
- package/dist/react/RightSidebar.d.ts +33 -0
- package/dist/react/RightSidebar.d.ts.map +1 -0
- package/dist/react/RightSidebar.js +82 -0
- package/dist/react/RightSidebar.js.map +1 -0
- package/dist/react/RightSidebarContext.d.ts +62 -0
- package/dist/react/RightSidebarContext.d.ts.map +1 -0
- package/dist/react/RightSidebarContext.js +178 -0
- package/dist/react/RightSidebarContext.js.map +1 -0
- package/dist/react/RightSidebarTrigger.d.ts +16 -0
- package/dist/react/RightSidebarTrigger.d.ts.map +1 -0
- package/dist/react/RightSidebarTrigger.js +24 -0
- package/dist/react/RightSidebarTrigger.js.map +1 -0
- package/dist/react/SchemaRenderer.d.ts +63 -0
- package/dist/react/SchemaRenderer.d.ts.map +1 -0
- package/dist/react/SchemaRenderer.js +3458 -0
- package/dist/react/SchemaRenderer.js.map +1 -0
- package/dist/react/SearchTrigger.d.ts +13 -0
- package/dist/react/SearchTrigger.d.ts.map +1 -0
- package/dist/react/SearchTrigger.js +30 -0
- package/dist/react/SearchTrigger.js.map +1 -0
- package/dist/react/ThemeProvider.d.ts +18 -0
- package/dist/react/ThemeProvider.d.ts.map +1 -0
- package/dist/react/ThemeProvider.js +66 -0
- package/dist/react/ThemeProvider.js.map +1 -0
- package/dist/react/ThemeSettingsPage.d.ts +10 -0
- package/dist/react/ThemeSettingsPage.d.ts.map +1 -0
- package/dist/react/ThemeSettingsPage.js +293 -0
- package/dist/react/ThemeSettingsPage.js.map +1 -0
- package/dist/react/ThemeToggle.d.ts +2 -0
- package/dist/react/ThemeToggle.d.ts.map +1 -0
- package/dist/react/ThemeToggle.js +8 -0
- package/dist/react/ThemeToggle.js.map +1 -0
- package/dist/react/Toaster.d.ts +25 -0
- package/dist/react/Toaster.d.ts.map +1 -0
- package/dist/react/Toaster.js +89 -0
- package/dist/react/Toaster.js.map +1 -0
- package/dist/react/UserMenu.d.ts +23 -0
- package/dist/react/UserMenu.d.ts.map +1 -0
- package/dist/react/UserMenu.js +78 -0
- package/dist/react/UserMenu.js.map +1 -0
- package/dist/react/WidgetDataContext.d.ts +64 -0
- package/dist/react/WidgetDataContext.d.ts.map +1 -0
- package/dist/react/WidgetDataContext.js +89 -0
- package/dist/react/WidgetDataContext.js.map +1 -0
- package/dist/react/cells/EditableCell.d.ts +20 -0
- package/dist/react/cells/EditableCell.d.ts.map +1 -0
- package/dist/react/cells/EditableCell.js +251 -0
- package/dist/react/cells/EditableCell.js.map +1 -0
- package/dist/react/fieldJsHandler.d.ts +33 -0
- package/dist/react/fieldJsHandler.d.ts.map +1 -0
- package/dist/react/fieldJsHandler.js +61 -0
- package/dist/react/fieldJsHandler.js.map +1 -0
- package/dist/react/fields/BuilderInput.d.ts +21 -0
- package/dist/react/fields/BuilderInput.d.ts.map +1 -0
- package/dist/react/fields/BuilderInput.js +553 -0
- package/dist/react/fields/BuilderInput.js.map +1 -0
- package/dist/react/fields/CheckboxInput.d.ts +9 -0
- package/dist/react/fields/CheckboxInput.d.ts.map +1 -0
- package/dist/react/fields/CheckboxInput.js +23 -0
- package/dist/react/fields/CheckboxInput.js.map +1 -0
- package/dist/react/fields/CheckboxListInput.d.ts +19 -0
- package/dist/react/fields/CheckboxListInput.d.ts.map +1 -0
- package/dist/react/fields/CheckboxListInput.js +53 -0
- package/dist/react/fields/CheckboxListInput.js.map +1 -0
- package/dist/react/fields/ColorInput.d.ts +12 -0
- package/dist/react/fields/ColorInput.d.ts.map +1 -0
- package/dist/react/fields/ColorInput.js +29 -0
- package/dist/react/fields/ColorInput.js.map +1 -0
- package/dist/react/fields/DateFieldInput.d.ts +8 -0
- package/dist/react/fields/DateFieldInput.d.ts.map +1 -0
- package/dist/react/fields/DateFieldInput.js +39 -0
- package/dist/react/fields/DateFieldInput.js.map +1 -0
- package/dist/react/fields/DateTimeInput.d.ts +13 -0
- package/dist/react/fields/DateTimeInput.d.ts.map +1 -0
- package/dist/react/fields/DateTimeInput.js +29 -0
- package/dist/react/fields/DateTimeInput.js.map +1 -0
- package/dist/react/fields/FieldShell.d.ts +23 -0
- package/dist/react/fields/FieldShell.d.ts.map +1 -0
- package/dist/react/fields/FieldShell.js +46 -0
- package/dist/react/fields/FieldShell.js.map +1 -0
- package/dist/react/fields/FileUploadInput.d.ts +21 -0
- package/dist/react/fields/FileUploadInput.d.ts.map +1 -0
- package/dist/react/fields/FileUploadInput.js +120 -0
- package/dist/react/fields/FileUploadInput.js.map +1 -0
- package/dist/react/fields/HiddenInput.d.ts +11 -0
- package/dist/react/fields/HiddenInput.d.ts.map +1 -0
- package/dist/react/fields/HiddenInput.js +14 -0
- package/dist/react/fields/HiddenInput.js.map +1 -0
- package/dist/react/fields/KeyValueInput.d.ts +18 -0
- package/dist/react/fields/KeyValueInput.d.ts.map +1 -0
- package/dist/react/fields/KeyValueInput.js +122 -0
- package/dist/react/fields/KeyValueInput.js.map +1 -0
- package/dist/react/fields/MarkdownInput.d.ts +29 -0
- package/dist/react/fields/MarkdownInput.d.ts.map +1 -0
- package/dist/react/fields/MarkdownInput.js +250 -0
- package/dist/react/fields/MarkdownInput.js.map +1 -0
- package/dist/react/fields/RadioInput.d.ts +18 -0
- package/dist/react/fields/RadioInput.d.ts.map +1 -0
- package/dist/react/fields/RadioInput.js +34 -0
- package/dist/react/fields/RadioInput.js.map +1 -0
- package/dist/react/fields/RepeaterInput.d.ts +92 -0
- package/dist/react/fields/RepeaterInput.d.ts.map +1 -0
- package/dist/react/fields/RepeaterInput.js +705 -0
- package/dist/react/fields/RepeaterInput.js.map +1 -0
- package/dist/react/fields/SelectFieldInput.d.ts +23 -0
- package/dist/react/fields/SelectFieldInput.d.ts.map +1 -0
- package/dist/react/fields/SelectFieldInput.js +146 -0
- package/dist/react/fields/SelectFieldInput.js.map +1 -0
- package/dist/react/fields/SliderInput.d.ts +16 -0
- package/dist/react/fields/SliderInput.d.ts.map +1 -0
- package/dist/react/fields/SliderInput.js +37 -0
- package/dist/react/fields/SliderInput.js.map +1 -0
- package/dist/react/fields/TagsInput.d.ts +27 -0
- package/dist/react/fields/TagsInput.d.ts.map +1 -0
- package/dist/react/fields/TagsInput.js +189 -0
- package/dist/react/fields/TagsInput.js.map +1 -0
- package/dist/react/fields/TextLikeInput.d.ts +18 -0
- package/dist/react/fields/TextLikeInput.d.ts.map +1 -0
- package/dist/react/fields/TextLikeInput.js +46 -0
- package/dist/react/fields/TextLikeInput.js.map +1 -0
- package/dist/react/fields/ToggleButtonsInput.d.ts +20 -0
- package/dist/react/fields/ToggleButtonsInput.d.ts.map +1 -0
- package/dist/react/fields/ToggleButtonsInput.js +42 -0
- package/dist/react/fields/ToggleButtonsInput.js.map +1 -0
- package/dist/react/fields/ToggleFieldInput.d.ts +7 -0
- package/dist/react/fields/ToggleFieldInput.d.ts.map +1 -0
- package/dist/react/fields/ToggleFieldInput.js +30 -0
- package/dist/react/fields/ToggleFieldInput.js.map +1 -0
- package/dist/react/fields/rowChromeButton.d.ts +84 -0
- package/dist/react/fields/rowChromeButton.d.ts.map +1 -0
- package/dist/react/fields/rowChromeButton.js +111 -0
- package/dist/react/fields/rowChromeButton.js.map +1 -0
- package/dist/react/fields/syncRowGates.d.ts +11 -0
- package/dist/react/fields/syncRowGates.d.ts.map +1 -0
- package/dist/react/fields/syncRowGates.js +55 -0
- package/dist/react/fields/syncRowGates.js.map +1 -0
- package/dist/react/formStateHelpers.d.ts +44 -0
- package/dist/react/formStateHelpers.d.ts.map +1 -0
- package/dist/react/formStateHelpers.js +230 -0
- package/dist/react/formStateHelpers.js.map +1 -0
- package/dist/react/hooks/use-mobile.d.ts +2 -0
- package/dist/react/hooks/use-mobile.d.ts.map +1 -0
- package/dist/react/hooks/use-mobile.js +16 -0
- package/dist/react/hooks/use-mobile.js.map +1 -0
- package/dist/react/icon-context.d.ts +35 -0
- package/dist/react/icon-context.d.ts.map +1 -0
- package/dist/react/icon-context.js +45 -0
- package/dist/react/icon-context.js.map +1 -0
- package/dist/react/index.d.ts +26 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +28 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/layouts/SidebarLayout.d.ts +3 -0
- package/dist/react/layouts/SidebarLayout.d.ts.map +1 -0
- package/dist/react/layouts/SidebarLayout.js +85 -0
- package/dist/react/layouts/SidebarLayout.js.map +1 -0
- package/dist/react/layouts/TopbarLayout.d.ts +3 -0
- package/dist/react/layouts/TopbarLayout.d.ts.map +1 -0
- package/dist/react/layouts/TopbarLayout.js +103 -0
- package/dist/react/layouts/TopbarLayout.js.map +1 -0
- package/dist/react/navigate.d.ts +22 -0
- package/dist/react/navigate.d.ts.map +1 -0
- package/dist/react/navigate.js +30 -0
- package/dist/react/navigate.js.map +1 -0
- package/dist/react/registry.d.ts +35 -0
- package/dist/react/registry.d.ts.map +1 -0
- package/dist/react/registry.js +22 -0
- package/dist/react/registry.js.map +1 -0
- package/dist/react/right-panel-registry.d.ts +32 -0
- package/dist/react/right-panel-registry.d.ts.map +1 -0
- package/dist/react/right-panel-registry.js +20 -0
- package/dist/react/right-panel-registry.js.map +1 -0
- package/dist/react/theme-preview/apply.d.ts +11 -0
- package/dist/react/theme-preview/apply.d.ts.map +1 -0
- package/dist/react/theme-preview/apply.js +93 -0
- package/dist/react/theme-preview/apply.js.map +1 -0
- package/dist/react/theme-preview/build-html.d.ts +3 -0
- package/dist/react/theme-preview/build-html.d.ts.map +1 -0
- package/dist/react/theme-preview/build-html.js +437 -0
- package/dist/react/theme-preview/build-html.js.map +1 -0
- package/dist/react/ui/button.d.ts +9 -0
- package/dist/react/ui/button.d.ts.map +1 -0
- package/dist/react/ui/button.js +35 -0
- package/dist/react/ui/button.js.map +1 -0
- package/dist/react/ui/calendar.d.ts +5 -0
- package/dist/react/ui/calendar.d.ts.map +1 -0
- package/dist/react/ui/calendar.js +34 -0
- package/dist/react/ui/calendar.js.map +1 -0
- package/dist/react/ui/checkbox.d.ts +4 -0
- package/dist/react/ui/checkbox.d.ts.map +1 -0
- package/dist/react/ui/checkbox.js +9 -0
- package/dist/react/ui/checkbox.js.map +1 -0
- package/dist/react/ui/dialog.d.ts +12 -0
- package/dist/react/ui/dialog.d.ts.map +1 -0
- package/dist/react/ui/dialog.js +34 -0
- package/dist/react/ui/dialog.js.map +1 -0
- package/dist/react/ui/dropdown-menu.d.ts +12 -0
- package/dist/react/ui/dropdown-menu.d.ts.map +1 -0
- package/dist/react/ui/dropdown-menu.js +23 -0
- package/dist/react/ui/dropdown-menu.js.map +1 -0
- package/dist/react/ui/input.d.ts +4 -0
- package/dist/react/ui/input.d.ts.map +1 -0
- package/dist/react/ui/input.js +8 -0
- package/dist/react/ui/input.js.map +1 -0
- package/dist/react/ui/label.d.ts +4 -0
- package/dist/react/ui/label.d.ts.map +1 -0
- package/dist/react/ui/label.js +7 -0
- package/dist/react/ui/label.js.map +1 -0
- package/dist/react/ui/popover.d.ts +6 -0
- package/dist/react/ui/popover.d.ts.map +1 -0
- package/dist/react/ui/popover.js +14 -0
- package/dist/react/ui/popover.js.map +1 -0
- package/dist/react/ui/select.d.ts +17 -0
- package/dist/react/ui/select.d.ts.map +1 -0
- package/dist/react/ui/select.js +39 -0
- package/dist/react/ui/select.js.map +1 -0
- package/dist/react/ui/separator.d.ts +4 -0
- package/dist/react/ui/separator.d.ts.map +1 -0
- package/dist/react/ui/separator.js +9 -0
- package/dist/react/ui/separator.js.map +1 -0
- package/dist/react/ui/sheet.d.ts +15 -0
- package/dist/react/ui/sheet.d.ts.map +1 -0
- package/dist/react/ui/sheet.js +37 -0
- package/dist/react/ui/sheet.js.map +1 -0
- package/dist/react/ui/sidebar.d.ts +64 -0
- package/dist/react/ui/sidebar.d.ts.map +1 -0
- package/dist/react/ui/sidebar.js +257 -0
- package/dist/react/ui/sidebar.js.map +1 -0
- package/dist/react/ui/skeleton.d.ts +3 -0
- package/dist/react/ui/skeleton.d.ts.map +1 -0
- package/dist/react/ui/skeleton.js +7 -0
- package/dist/react/ui/skeleton.js.map +1 -0
- package/dist/react/ui/slider.d.ts +4 -0
- package/dist/react/ui/slider.d.ts.map +1 -0
- package/dist/react/ui/slider.js +8 -0
- package/dist/react/ui/slider.js.map +1 -0
- package/dist/react/ui/switch.d.ts +4 -0
- package/dist/react/ui/switch.d.ts.map +1 -0
- package/dist/react/ui/switch.js +8 -0
- package/dist/react/ui/switch.js.map +1 -0
- package/dist/react/ui/table.d.ts +11 -0
- package/dist/react/ui/table.d.ts.map +1 -0
- package/dist/react/ui/table.js +28 -0
- package/dist/react/ui/table.js.map +1 -0
- package/dist/react/ui/tabs.d.ts +7 -0
- package/dist/react/ui/tabs.d.ts.map +1 -0
- package/dist/react/ui/tabs.js +17 -0
- package/dist/react/ui/tabs.js.map +1 -0
- package/dist/react/ui/textarea.d.ts +4 -0
- package/dist/react/ui/textarea.d.ts.map +1 -0
- package/dist/react/ui/textarea.js +7 -0
- package/dist/react/ui/textarea.js.map +1 -0
- package/dist/react/ui/tooltip.d.ts +7 -0
- package/dist/react/ui/tooltip.d.ts.map +1 -0
- package/dist/react/ui/tooltip.js +17 -0
- package/dist/react/ui/tooltip.js.map +1 -0
- package/dist/react/useResizableWidth.d.ts +47 -0
- package/dist/react/useResizableWidth.d.ts.map +1 -0
- package/dist/react/useResizableWidth.js +99 -0
- package/dist/react/useResizableWidth.js.map +1 -0
- package/dist/react/utils.d.ts +3 -0
- package/dist/react/utils.d.ts.map +1 -0
- package/dist/react/utils.js +6 -0
- package/dist/react/utils.js.map +1 -0
- package/dist/react/widgetRegistry.d.ts +33 -0
- package/dist/react/widgetRegistry.d.ts.map +1 -0
- package/dist/react/widgetRegistry.js +15 -0
- package/dist/react/widgetRegistry.js.map +1 -0
- package/dist/react/widgets/StatsOverviewRenderer.d.ts +6 -0
- package/dist/react/widgets/StatsOverviewRenderer.d.ts.map +1 -0
- package/dist/react/widgets/StatsOverviewRenderer.js +124 -0
- package/dist/react/widgets/StatsOverviewRenderer.js.map +1 -0
- package/dist/react/widgets/TableWidgetRenderer.d.ts +6 -0
- package/dist/react/widgets/TableWidgetRenderer.d.ts.map +1 -0
- package/dist/react/widgets/TableWidgetRenderer.js +123 -0
- package/dist/react/widgets/TableWidgetRenderer.js.map +1 -0
- package/dist/react/widgets/ViewRenderer.d.ts +16 -0
- package/dist/react/widgets/ViewRenderer.d.ts.map +1 -0
- package/dist/react/widgets/ViewRenderer.js +26 -0
- package/dist/react/widgets/ViewRenderer.js.map +1 -0
- package/dist/richtext/index.d.ts +2 -0
- package/dist/richtext/index.d.ts.map +1 -0
- package/dist/richtext/index.js +2 -0
- package/dist/richtext/index.js.map +1 -0
- package/dist/richtext/registry.d.ts +55 -0
- package/dist/richtext/registry.d.ts.map +1 -0
- package/dist/richtext/registry.js +66 -0
- package/dist/richtext/registry.js.map +1 -0
- package/dist/routes.d.ts +9 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +3116 -0
- package/dist/routes.js.map +1 -0
- package/dist/schema/Alert.d.ts +33 -0
- package/dist/schema/Alert.d.ts.map +1 -0
- package/dist/schema/Alert.js +41 -0
- package/dist/schema/Alert.js.map +1 -0
- package/dist/schema/Block.d.ts +112 -0
- package/dist/schema/Block.d.ts.map +1 -0
- package/dist/schema/Block.js +136 -0
- package/dist/schema/Block.js.map +1 -0
- package/dist/schema/Breadcrumbs.d.ts +31 -0
- package/dist/schema/Breadcrumbs.d.ts.map +1 -0
- package/dist/schema/Breadcrumbs.js +30 -0
- package/dist/schema/Breadcrumbs.js.map +1 -0
- package/dist/schema/Card.d.ts +17 -0
- package/dist/schema/Card.d.ts.map +1 -0
- package/dist/schema/Card.js +31 -0
- package/dist/schema/Card.js.map +1 -0
- package/dist/schema/Divider.d.ts +12 -0
- package/dist/schema/Divider.d.ts.map +1 -0
- package/dist/schema/Divider.js +19 -0
- package/dist/schema/Divider.js.map +1 -0
- package/dist/schema/Element.d.ts +150 -0
- package/dist/schema/Element.d.ts.map +1 -0
- package/dist/schema/Element.js +124 -0
- package/dist/schema/Element.js.map +1 -0
- package/dist/schema/EmptyState.d.ts +48 -0
- package/dist/schema/EmptyState.d.ts.map +1 -0
- package/dist/schema/EmptyState.js +57 -0
- package/dist/schema/EmptyState.js.map +1 -0
- package/dist/schema/Fieldset.d.ts +25 -0
- package/dist/schema/Fieldset.d.ts.map +1 -0
- package/dist/schema/Fieldset.js +39 -0
- package/dist/schema/Fieldset.js.map +1 -0
- package/dist/schema/Grid.d.ts +23 -0
- package/dist/schema/Grid.d.ts.map +1 -0
- package/dist/schema/Grid.js +36 -0
- package/dist/schema/Grid.js.map +1 -0
- package/dist/schema/Group.d.ts +19 -0
- package/dist/schema/Group.d.ts.map +1 -0
- package/dist/schema/Group.js +26 -0
- package/dist/schema/Group.js.map +1 -0
- package/dist/schema/Heading.d.ts +25 -0
- package/dist/schema/Heading.d.ts.map +1 -0
- package/dist/schema/Heading.js +34 -0
- package/dist/schema/Heading.js.map +1 -0
- package/dist/schema/Html.d.ts +48 -0
- package/dist/schema/Html.d.ts.map +1 -0
- package/dist/schema/Html.js +60 -0
- package/dist/schema/Html.js.map +1 -0
- package/dist/schema/Icon.d.ts +34 -0
- package/dist/schema/Icon.d.ts.map +1 -0
- package/dist/schema/Icon.js +40 -0
- package/dist/schema/Icon.js.map +1 -0
- package/dist/schema/Image.d.ts +38 -0
- package/dist/schema/Image.d.ts.map +1 -0
- package/dist/schema/Image.js +48 -0
- package/dist/schema/Image.js.map +1 -0
- package/dist/schema/LinkTag.d.ts +48 -0
- package/dist/schema/LinkTag.d.ts.map +1 -0
- package/dist/schema/LinkTag.js +16 -0
- package/dist/schema/LinkTag.js.map +1 -0
- package/dist/schema/Markdown.d.ts +57 -0
- package/dist/schema/Markdown.d.ts.map +1 -0
- package/dist/schema/Markdown.js +75 -0
- package/dist/schema/Markdown.js.map +1 -0
- package/dist/schema/MetaTag.d.ts +41 -0
- package/dist/schema/MetaTag.d.ts.map +1 -0
- package/dist/schema/MetaTag.js +16 -0
- package/dist/schema/MetaTag.js.map +1 -0
- package/dist/schema/RelationTabs.d.ts +50 -0
- package/dist/schema/RelationTabs.d.ts.map +1 -0
- package/dist/schema/RelationTabs.js +48 -0
- package/dist/schema/RelationTabs.js.map +1 -0
- package/dist/schema/ScriptTag.d.ts +63 -0
- package/dist/schema/ScriptTag.d.ts.map +1 -0
- package/dist/schema/ScriptTag.js +16 -0
- package/dist/schema/ScriptTag.js.map +1 -0
- package/dist/schema/Section.d.ts +93 -0
- package/dist/schema/Section.d.ts.map +1 -0
- package/dist/schema/Section.js +127 -0
- package/dist/schema/Section.js.map +1 -0
- package/dist/schema/ServerDataElement.d.ts +101 -0
- package/dist/schema/ServerDataElement.d.ts.map +1 -0
- package/dist/schema/ServerDataElement.js +135 -0
- package/dist/schema/ServerDataElement.js.map +1 -0
- package/dist/schema/Split.d.ts +31 -0
- package/dist/schema/Split.d.ts.map +1 -0
- package/dist/schema/Split.js +41 -0
- package/dist/schema/Split.js.map +1 -0
- package/dist/schema/Stat.d.ts +92 -0
- package/dist/schema/Stat.d.ts.map +1 -0
- package/dist/schema/Stat.js +116 -0
- package/dist/schema/Stat.js.map +1 -0
- package/dist/schema/StatsOverview.d.ts +76 -0
- package/dist/schema/StatsOverview.d.ts.map +1 -0
- package/dist/schema/StatsOverview.js +71 -0
- package/dist/schema/StatsOverview.js.map +1 -0
- package/dist/schema/StyleTag.d.ts +32 -0
- package/dist/schema/StyleTag.d.ts.map +1 -0
- package/dist/schema/StyleTag.js +38 -0
- package/dist/schema/StyleTag.js.map +1 -0
- package/dist/schema/TableWidget.d.ts +148 -0
- package/dist/schema/TableWidget.d.ts.map +1 -0
- package/dist/schema/TableWidget.js +190 -0
- package/dist/schema/TableWidget.js.map +1 -0
- package/dist/schema/Tabs.d.ts +40 -0
- package/dist/schema/Tabs.d.ts.map +1 -0
- package/dist/schema/Tabs.js +66 -0
- package/dist/schema/Tabs.js.map +1 -0
- package/dist/schema/Text.d.ts +33 -0
- package/dist/schema/Text.d.ts.map +1 -0
- package/dist/schema/Text.js +40 -0
- package/dist/schema/Text.js.map +1 -0
- package/dist/schema/UnorderedList.d.ts +36 -0
- package/dist/schema/UnorderedList.d.ts.map +1 -0
- package/dist/schema/UnorderedList.js +42 -0
- package/dist/schema/UnorderedList.js.map +1 -0
- package/dist/schema/View.d.ts +81 -0
- package/dist/schema/View.d.ts.map +1 -0
- package/dist/schema/View.js +81 -0
- package/dist/schema/View.js.map +1 -0
- package/dist/schema/Wizard.d.ts +67 -0
- package/dist/schema/Wizard.d.ts.map +1 -0
- package/dist/schema/Wizard.js +94 -0
- package/dist/schema/Wizard.js.map +1 -0
- package/dist/schema/index.d.ts +26 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +26 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/resolveSchema.d.ts +122 -0
- package/dist/schema/resolveSchema.d.ts.map +1 -0
- package/dist/schema/resolveSchema.js +648 -0
- package/dist/schema/resolveSchema.js.map +1 -0
- package/dist/schema/sanitize.d.ts +21 -0
- package/dist/schema/sanitize.d.ts.map +1 -0
- package/dist/schema/sanitize.js +46 -0
- package/dist/schema/sanitize.js.map +1 -0
- package/dist/search.d.ts +53 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +114 -0
- package/dist/search.js.map +1 -0
- package/dist/sessionFilters.d.ts +8 -0
- package/dist/sessionFilters.d.ts.map +1 -0
- package/dist/sessionFilters.js +115 -0
- package/dist/sessionFilters.js.map +1 -0
- package/dist/summarizers/Summarizer.d.ts +65 -0
- package/dist/summarizers/Summarizer.d.ts.map +1 -0
- package/dist/summarizers/Summarizer.js +98 -0
- package/dist/summarizers/Summarizer.js.map +1 -0
- package/dist/summarizers/index.d.ts +2 -0
- package/dist/summarizers/index.d.ts.map +1 -0
- package/dist/summarizers/index.js +2 -0
- package/dist/summarizers/index.js.map +1 -0
- package/dist/theme/base-colors.d.ts +3 -0
- package/dist/theme/base-colors.d.ts.map +1 -0
- package/dist/theme/base-colors.js +64 -0
- package/dist/theme/base-colors.js.map +1 -0
- package/dist/theme/chart-colors.d.ts +3 -0
- package/dist/theme/chart-colors.d.ts.map +1 -0
- package/dist/theme/chart-colors.js +46 -0
- package/dist/theme/chart-colors.js.map +1 -0
- package/dist/theme/colors.d.ts +56 -0
- package/dist/theme/colors.d.ts.map +1 -0
- package/dist/theme/colors.js +410 -0
- package/dist/theme/colors.js.map +1 -0
- package/dist/theme/generate-css.d.ts +9 -0
- package/dist/theme/generate-css.d.ts.map +1 -0
- package/dist/theme/generate-css.js +36 -0
- package/dist/theme/generate-css.js.map +1 -0
- package/dist/theme/generate-scale.d.ts +3 -0
- package/dist/theme/generate-scale.d.ts.map +1 -0
- package/dist/theme/generate-scale.js +89 -0
- package/dist/theme/generate-scale.js.map +1 -0
- package/dist/theme/icon-map.d.ts +9 -0
- package/dist/theme/icon-map.d.ts.map +1 -0
- package/dist/theme/icon-map.js +40 -0
- package/dist/theme/icon-map.js.map +1 -0
- package/dist/theme/index.d.ts +15 -0
- package/dist/theme/index.d.ts.map +1 -0
- package/dist/theme/index.js +13 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/migrate.d.ts +14 -0
- package/dist/theme/migrate.d.ts.map +1 -0
- package/dist/theme/migrate.js +79 -0
- package/dist/theme/migrate.js.map +1 -0
- package/dist/theme/presets.d.ts +30 -0
- package/dist/theme/presets.d.ts.map +1 -0
- package/dist/theme/presets.js +128 -0
- package/dist/theme/presets.js.map +1 -0
- package/dist/theme/radius.d.ts +11 -0
- package/dist/theme/radius.d.ts.map +1 -0
- package/dist/theme/radius.js +17 -0
- package/dist/theme/radius.js.map +1 -0
- package/dist/theme/resolve.d.ts +13 -0
- package/dist/theme/resolve.d.ts.map +1 -0
- package/dist/theme/resolve.js +91 -0
- package/dist/theme/resolve.js.map +1 -0
- package/dist/theme/spacing.d.ts +14 -0
- package/dist/theme/spacing.d.ts.map +1 -0
- package/dist/theme/spacing.js +17 -0
- package/dist/theme/spacing.js.map +1 -0
- package/dist/theme/theme-colors.d.ts +9 -0
- package/dist/theme/theme-colors.d.ts.map +1 -0
- package/dist/theme/theme-colors.js +84 -0
- package/dist/theme/theme-colors.js.map +1 -0
- package/dist/theme/types.d.ts +94 -0
- package/dist/theme/types.d.ts.map +1 -0
- package/dist/theme/types.js +2 -0
- package/dist/theme/types.js.map +1 -0
- package/dist/uploads/UploadAdapter.d.ts +34 -0
- package/dist/uploads/UploadAdapter.d.ts.map +1 -0
- package/dist/uploads/UploadAdapter.js +2 -0
- package/dist/uploads/UploadAdapter.js.map +1 -0
- package/dist/uploads/index.d.ts +3 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +2 -0
- package/dist/uploads/index.js.map +1 -0
- package/dist/uploads/localUpload.d.ts +25 -0
- package/dist/uploads/localUpload.d.ts.map +1 -0
- package/dist/uploads/localUpload.js +65 -0
- package/dist/uploads/localUpload.js.map +1 -0
- package/dist/validation/Validator.d.ts +40 -0
- package/dist/validation/Validator.d.ts.map +1 -0
- package/dist/validation/Validator.js +25 -0
- package/dist/validation/Validator.js.map +1 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/rules.d.ts +9 -0
- package/dist/validation/rules.d.ts.map +1 -0
- package/dist/validation/rules.js +61 -0
- package/dist/validation/rules.js.map +1 -0
- package/dist/validation/runValidators.d.ts +30 -0
- package/dist/validation/runValidators.d.ts.map +1 -0
- package/dist/validation/runValidators.js +438 -0
- package/dist/validation/runValidators.js.map +1 -0
- package/dist/validation/uniqueValidator.d.ts +61 -0
- package/dist/validation/uniqueValidator.d.ts.map +1 -0
- package/dist/validation/uniqueValidator.js +80 -0
- package/dist/validation/uniqueValidator.js.map +1 -0
- package/dist/vite.d.ts +19 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +696 -0
- package/dist/vite.js.map +1 -0
- package/dist/widgets/index.d.ts +2 -0
- package/dist/widgets/index.d.ts.map +1 -0
- package/dist/widgets/index.js +7 -0
- package/dist/widgets/index.js.map +1 -0
- package/dist/widgets/registry.d.ts +32 -0
- package/dist/widgets/registry.d.ts.map +1 -0
- package/dist/widgets/registry.js +17 -0
- package/dist/widgets/registry.js.map +1 -0
- package/package.json +101 -0
- package/src/Cluster.test.ts +283 -0
- package/src/Cluster.ts +83 -0
- package/src/Column.test.ts +140 -0
- package/src/Column.ts +612 -0
- package/src/Global.test.ts +367 -0
- package/src/Global.ts +169 -0
- package/src/Page.test.ts +50 -0
- package/src/Page.ts +139 -0
- package/src/Pilotiq.test.ts +47 -0
- package/src/Pilotiq.ts +705 -0
- package/src/PilotiqRegistry.ts +36 -0
- package/src/PilotiqServiceProvider.ts +69 -0
- package/src/RelationManager.test.ts +400 -0
- package/src/RelationManager.ts +527 -0
- package/src/RenderHook.test.ts +252 -0
- package/src/RenderHook.ts +226 -0
- package/src/Resource.test.ts +240 -0
- package/src/Resource.ts +439 -0
- package/src/RightPanel.test.ts +202 -0
- package/src/RightPanel.ts +132 -0
- package/src/Tab.test.ts +91 -0
- package/src/Tab.ts +156 -0
- package/src/UserMenuItem.ts +145 -0
- package/src/actions/Action.test.ts +2479 -0
- package/src/actions/Action.ts +2124 -0
- package/src/actions/ActionGroup.test.ts +112 -0
- package/src/actions/ActionGroup.ts +173 -0
- package/src/actions/attachFactory.ts +172 -0
- package/src/actions/exportFactory.ts +215 -0
- package/src/actions/importFactory.ts +222 -0
- package/src/actions/index.ts +17 -0
- package/src/applyPageHooks.test.ts +298 -0
- package/src/applyPageHooks.ts +242 -0
- package/src/authorization.test.ts +483 -0
- package/src/breadcrumbs.test.ts +238 -0
- package/src/cells/coerce.test.ts +85 -0
- package/src/cells/coerce.ts +84 -0
- package/src/clusterPaths.ts +35 -0
- package/src/columns/BadgeColumn.test.ts +54 -0
- package/src/columns/BadgeColumn.ts +32 -0
- package/src/columns/BooleanColumn.test.ts +41 -0
- package/src/columns/BooleanColumn.ts +18 -0
- package/src/columns/ColorColumn.test.ts +37 -0
- package/src/columns/ColorColumn.ts +38 -0
- package/src/columns/IconColumn.test.ts +54 -0
- package/src/columns/IconColumn.ts +37 -0
- package/src/columns/ImageColumn.test.ts +41 -0
- package/src/columns/ImageColumn.ts +28 -0
- package/src/columns/SelectColumn.ts +60 -0
- package/src/columns/TextColumn.test.ts +190 -0
- package/src/columns/TextColumn.ts +20 -0
- package/src/columns/TextInputColumn.ts +68 -0
- package/src/columns/ToggleColumn.ts +46 -0
- package/src/columns/editableColumns.test.ts +193 -0
- package/src/columns/index.ts +9 -0
- package/src/defaultGlobalPages.ts +95 -0
- package/src/defaultPages.test.ts +634 -0
- package/src/defaultPages.ts +614 -0
- package/src/defaultViewPage.test.ts +147 -0
- package/src/elements/Form.test.ts +223 -0
- package/src/elements/Form.ts +397 -0
- package/src/elements/ListTabs.ts +28 -0
- package/src/elements/Table.test.ts +422 -0
- package/src/elements/Table.ts +816 -0
- package/src/elements/TableGroup.test.ts +149 -0
- package/src/elements/TableGroup.ts +199 -0
- package/src/elements/dispatchAction.test.ts +463 -0
- package/src/elements/dispatchAction.ts +355 -0
- package/src/elements/dispatchForm.test.ts +455 -0
- package/src/elements/dispatchForm.ts +1855 -0
- package/src/elements/dispatchTable.test.ts +1247 -0
- package/src/elements/dispatchTable.ts +666 -0
- package/src/elements/index.ts +21 -0
- package/src/entries/BadgeEntry.ts +39 -0
- package/src/entries/CodeEntry.test.ts +40 -0
- package/src/entries/CodeEntry.ts +52 -0
- package/src/entries/ColorEntry.ts +63 -0
- package/src/entries/ComponentEntry.test.ts +173 -0
- package/src/entries/ComponentEntry.ts +95 -0
- package/src/entries/Entry.ts +304 -0
- package/src/entries/IconEntry.ts +49 -0
- package/src/entries/ImageEntry.ts +61 -0
- package/src/entries/KeyValueEntry.ts +47 -0
- package/src/entries/RepeatableEntry.test.ts +239 -0
- package/src/entries/RepeatableEntry.ts +173 -0
- package/src/entries/TextEntry.test.ts +394 -0
- package/src/entries/TextEntry.ts +60 -0
- package/src/entries/index.ts +12 -0
- package/src/entries/leaves.test.ts +306 -0
- package/src/entries/registry.ts +54 -0
- package/src/fields/BuilderField.test.ts +1188 -0
- package/src/fields/BuilderField.ts +568 -0
- package/src/fields/BuilderRelationship.test.ts +811 -0
- package/src/fields/CheckboxField.test.ts +44 -0
- package/src/fields/CheckboxField.ts +27 -0
- package/src/fields/CheckboxListField.test.ts +99 -0
- package/src/fields/CheckboxListField.ts +66 -0
- package/src/fields/ColorPickerField.test.ts +33 -0
- package/src/fields/ColorPickerField.ts +25 -0
- package/src/fields/DateField.ts +54 -0
- package/src/fields/DateTimeField.test.ts +55 -0
- package/src/fields/EmailField.ts +16 -0
- package/src/fields/Field.test.ts +639 -0
- package/src/fields/Field.ts +773 -0
- package/src/fields/FileUploadField.test.ts +97 -0
- package/src/fields/FileUploadField.ts +71 -0
- package/src/fields/HiddenField.test.ts +27 -0
- package/src/fields/HiddenField.ts +28 -0
- package/src/fields/KeyValueField.test.ts +105 -0
- package/src/fields/KeyValueField.ts +55 -0
- package/src/fields/MarkdownField.test.ts +167 -0
- package/src/fields/MarkdownField.ts +151 -0
- package/src/fields/NumberField.ts +33 -0
- package/src/fields/RadioField.test.ts +94 -0
- package/src/fields/RadioField.ts +67 -0
- package/src/fields/RepeaterField.test.ts +1806 -0
- package/src/fields/RepeaterField.ts +791 -0
- package/src/fields/RepeaterRelationship.test.ts +1630 -0
- package/src/fields/RepeaterSimple.test.ts +248 -0
- package/src/fields/RowButton.test.ts +149 -0
- package/src/fields/RowButton.ts +125 -0
- package/src/fields/SelectField.test.ts +192 -0
- package/src/fields/SelectField.ts +235 -0
- package/src/fields/SliderField.test.ts +50 -0
- package/src/fields/SliderField.ts +53 -0
- package/src/fields/SlugField.ts +24 -0
- package/src/fields/TagsInputField.test.ts +154 -0
- package/src/fields/TagsInputField.ts +133 -0
- package/src/fields/TextField.ts +24 -0
- package/src/fields/TextareaField.test.ts +58 -0
- package/src/fields/TextareaField.ts +59 -0
- package/src/fields/ToggleButtonsField.test.ts +106 -0
- package/src/fields/ToggleButtonsField.ts +59 -0
- package/src/fields/ToggleField.ts +16 -0
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +319 -0
- package/src/fields/optionsResolver.ts +95 -0
- package/src/fields/resolveField.ts +28 -0
- package/src/filters/BooleanFilter.ts +35 -0
- package/src/filters/DateRangeFilter.test.ts +194 -0
- package/src/filters/DateRangeFilter.ts +148 -0
- package/src/filters/Filter.test.ts +268 -0
- package/src/filters/Filter.ts +184 -0
- package/src/filters/FormFilter.test.ts +238 -0
- package/src/filters/FormFilter.ts +215 -0
- package/src/filters/MultiSelectFilter.test.ts +119 -0
- package/src/filters/MultiSelectFilter.ts +78 -0
- package/src/filters/QueryBuilderFilter.test.ts +644 -0
- package/src/filters/QueryBuilderFilter.ts +398 -0
- package/src/filters/SelectFilter.ts +46 -0
- package/src/filters/TernaryFilter.test.ts +160 -0
- package/src/filters/TernaryFilter.ts +72 -0
- package/src/filters/TrashedFilter.test.ts +149 -0
- package/src/filters/TrashedFilter.ts +55 -0
- package/src/filters/queryBuilder/BooleanConstraint.ts +31 -0
- package/src/filters/queryBuilder/Constraint.ts +115 -0
- package/src/filters/queryBuilder/DateConstraint.ts +69 -0
- package/src/filters/queryBuilder/NumberConstraint.ts +66 -0
- package/src/filters/queryBuilder/SelectConstraint.ts +72 -0
- package/src/filters/queryBuilder/TextConstraint.ts +65 -0
- package/src/filters/queryBuilder/index.ts +12 -0
- package/src/icons/index.ts +2 -0
- package/src/icons/lucide.ts +204 -0
- package/src/icons/registry.test.ts +56 -0
- package/src/icons/registry.ts +41 -0
- package/src/icons/types.ts +47 -0
- package/src/index.ts +521 -0
- package/src/io/csv.test.ts +142 -0
- package/src/io/csv.ts +170 -0
- package/src/nestedRelationManagerData.test.ts +526 -0
- package/src/notifications/Notification.test.ts +210 -0
- package/src/notifications/Notification.ts +354 -0
- package/src/notifications/broadcast.test.ts +110 -0
- package/src/notifications/broadcast.ts +95 -0
- package/src/notifications/database.test.ts +383 -0
- package/src/notifications/database.ts +398 -0
- package/src/notifications/databaseNotifications.test.ts +187 -0
- package/src/notifications/dispatchNotificationAction.test.ts +341 -0
- package/src/notifications/dispatchNotificationAction.ts +142 -0
- package/src/notifications/flash.test.ts +89 -0
- package/src/notifications/flash.ts +71 -0
- package/src/notifications/index.ts +45 -0
- package/src/notifications/registerBroadcastAuth.test.ts +134 -0
- package/src/notifications/registerBroadcastAuth.ts +100 -0
- package/src/notifications/resolveSavedNotification.test.ts +82 -0
- package/src/notifications/resolveSavedNotification.ts +59 -0
- package/src/notifications/types.ts +93 -0
- package/src/orm/m2mAccessor.ts +66 -0
- package/src/orm/modelDefaults.test.ts +633 -0
- package/src/orm/modelDefaults.ts +632 -0
- package/src/pageData.test.ts +1121 -0
- package/src/pageData.ts +4662 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/themeEditor.ts +24 -0
- package/src/react/AppShell.tsx +148 -0
- package/src/react/CommandPalette.tsx +375 -0
- package/src/react/FormStateContext.tsx +398 -0
- package/src/react/HeadHooks.tsx +126 -0
- package/src/react/NotificationActionStrip.tsx +263 -0
- package/src/react/NotificationBell.tsx +426 -0
- package/src/react/RenderHookSlot.tsx +32 -0
- package/src/react/RightSidebar.tsx +257 -0
- package/src/react/RightSidebarContext.tsx +211 -0
- package/src/react/RightSidebarTrigger.tsx +53 -0
- package/src/react/SchemaRenderer.tsx +6128 -0
- package/src/react/SearchTrigger.tsx +46 -0
- package/src/react/ThemeProvider.tsx +93 -0
- package/src/react/ThemeSettingsPage.tsx +579 -0
- package/src/react/ThemeToggle.tsx +20 -0
- package/src/react/Toaster.tsx +158 -0
- package/src/react/UserMenu.tsx +196 -0
- package/src/react/WidgetDataContext.tsx +157 -0
- package/src/react/cells/EditableCell.tsx +376 -0
- package/src/react/fieldJsHandler.test.ts +166 -0
- package/src/react/fieldJsHandler.ts +79 -0
- package/src/react/fields/BuilderInput.tsx +995 -0
- package/src/react/fields/CheckboxInput.tsx +39 -0
- package/src/react/fields/CheckboxListInput.tsx +81 -0
- package/src/react/fields/ColorInput.tsx +51 -0
- package/src/react/fields/DateFieldInput.tsx +70 -0
- package/src/react/fields/DateTimeInput.tsx +42 -0
- package/src/react/fields/FieldShell.tsx +107 -0
- package/src/react/fields/FileUploadInput.tsx +189 -0
- package/src/react/fields/HiddenInput.tsx +17 -0
- package/src/react/fields/KeyValueInput.tsx +200 -0
- package/src/react/fields/MarkdownInput.tsx +333 -0
- package/src/react/fields/RadioInput.tsx +60 -0
- package/src/react/fields/RepeaterInput.test.ts +116 -0
- package/src/react/fields/RepeaterInput.tsx +1313 -0
- package/src/react/fields/SelectFieldInput.tsx +257 -0
- package/src/react/fields/SliderInput.tsx +63 -0
- package/src/react/fields/TagsInput.tsx +265 -0
- package/src/react/fields/TextLikeInput.tsx +54 -0
- package/src/react/fields/ToggleButtonsInput.tsx +60 -0
- package/src/react/fields/ToggleFieldInput.tsx +35 -0
- package/src/react/fields/rowChromeButton.tsx +225 -0
- package/src/react/fields/syncRowGates.test.ts +202 -0
- package/src/react/fields/syncRowGates.ts +66 -0
- package/src/react/formStateHelpers.test.ts +295 -0
- package/src/react/formStateHelpers.ts +218 -0
- package/src/react/hooks/use-mobile.ts +19 -0
- package/src/react/icon-context.tsx +60 -0
- package/src/react/index.ts +85 -0
- package/src/react/layouts/SidebarLayout.tsx +239 -0
- package/src/react/layouts/TopbarLayout.tsx +245 -0
- package/src/react/navigate.tsx +37 -0
- package/src/react/registry.ts +48 -0
- package/src/react/right-panel-registry.tsx +47 -0
- package/src/react/theme-preview/apply.ts +99 -0
- package/src/react/theme-preview/build-html.ts +436 -0
- package/src/react/ui/button.tsx +51 -0
- package/src/react/ui/calendar.tsx +67 -0
- package/src/react/ui/checkbox.tsx +29 -0
- package/src/react/ui/dialog.tsx +108 -0
- package/src/react/ui/dropdown-menu.tsx +97 -0
- package/src/react/ui/input.tsx +20 -0
- package/src/react/ui/label.tsx +21 -0
- package/src/react/ui/popover.tsx +50 -0
- package/src/react/ui/select.tsx +169 -0
- package/src/react/ui/separator.tsx +25 -0
- package/src/react/ui/sheet.tsx +136 -0
- package/src/react/ui/sidebar.tsx +723 -0
- package/src/react/ui/skeleton.tsx +13 -0
- package/src/react/ui/slider.tsx +34 -0
- package/src/react/ui/switch.tsx +28 -0
- package/src/react/ui/table.tsx +105 -0
- package/src/react/ui/tabs.tsx +63 -0
- package/src/react/ui/textarea.tsx +18 -0
- package/src/react/ui/tooltip.tsx +64 -0
- package/src/react/useResizableWidth.ts +139 -0
- package/src/react/utils.ts +6 -0
- package/src/react/widgetRegistry.test.ts +43 -0
- package/src/react/widgetRegistry.ts +50 -0
- package/src/react/widgets/StatsOverviewRenderer.tsx +232 -0
- package/src/react/widgets/TableWidgetRenderer.tsx +231 -0
- package/src/react/widgets/ViewRenderer.tsx +71 -0
- package/src/relationManagerData.test.ts +1146 -0
- package/src/richtext/index.ts +8 -0
- package/src/richtext/registry.ts +89 -0
- package/src/routes-nested-relations.test.ts +676 -0
- package/src/routes-relations.test.ts +972 -0
- package/src/routes.test.ts +1886 -0
- package/src/routes.ts +3262 -0
- package/src/schema/Alert.test.ts +63 -0
- package/src/schema/Alert.ts +49 -0
- package/src/schema/Block.ts +169 -0
- package/src/schema/Breadcrumbs.ts +40 -0
- package/src/schema/Card.ts +35 -0
- package/src/schema/Divider.ts +20 -0
- package/src/schema/Element.ts +219 -0
- package/src/schema/EmptyState.test.ts +37 -0
- package/src/schema/EmptyState.ts +63 -0
- package/src/schema/Fieldset.ts +43 -0
- package/src/schema/Grid.ts +43 -0
- package/src/schema/Group.ts +30 -0
- package/src/schema/Heading.ts +39 -0
- package/src/schema/Html.ts +67 -0
- package/src/schema/Icon.ts +54 -0
- package/src/schema/Image.ts +57 -0
- package/src/schema/LinkTag.ts +41 -0
- package/src/schema/Markdown.ts +85 -0
- package/src/schema/MetaTag.ts +41 -0
- package/src/schema/RelationTabs.ts +71 -0
- package/src/schema/ScriptTag.ts +55 -0
- package/src/schema/Section.ts +143 -0
- package/src/schema/ServerDataElement.test.ts +140 -0
- package/src/schema/ServerDataElement.ts +156 -0
- package/src/schema/Split.ts +50 -0
- package/src/schema/Stat.test.ts +118 -0
- package/src/schema/Stat.ts +154 -0
- package/src/schema/StatsOverview.test.ts +141 -0
- package/src/schema/StatsOverview.ts +119 -0
- package/src/schema/StyleTag.ts +35 -0
- package/src/schema/TableWidget.test.ts +297 -0
- package/src/schema/TableWidget.ts +289 -0
- package/src/schema/Tabs.ts +79 -0
- package/src/schema/Text.ts +58 -0
- package/src/schema/UnorderedList.ts +49 -0
- package/src/schema/View.test.ts +111 -0
- package/src/schema/View.ts +127 -0
- package/src/schema/Wizard.ts +108 -0
- package/src/schema/containers.test.ts +446 -0
- package/src/schema/headTags.test.ts +134 -0
- package/src/schema/index.ts +39 -0
- package/src/schema/primes.test.ts +269 -0
- package/src/schema/resolveSchema.test.ts +329 -0
- package/src/schema/resolveSchema.ts +807 -0
- package/src/schema/sanitize.ts +49 -0
- package/src/search.test.ts +446 -0
- package/src/search.ts +178 -0
- package/src/sessionFilters.test.ts +352 -0
- package/src/sessionFilters.ts +133 -0
- package/src/summarizers/Summarizer.test.ts +84 -0
- package/src/summarizers/Summarizer.ts +123 -0
- package/src/summarizers/index.ts +11 -0
- package/src/theme/base-colors.ts +68 -0
- package/src/theme/chart-colors.ts +50 -0
- package/src/theme/colors.ts +447 -0
- package/src/theme/generate-css.test.ts +139 -0
- package/src/theme/generate-css.ts +44 -0
- package/src/theme/generate-scale.test.ts +106 -0
- package/src/theme/generate-scale.ts +97 -0
- package/src/theme/icon-map.ts +42 -0
- package/src/theme/index.ts +28 -0
- package/src/theme/migrate.ts +81 -0
- package/src/theme/presets.ts +135 -0
- package/src/theme/radius.ts +18 -0
- package/src/theme/resolve.test.ts +238 -0
- package/src/theme/resolve.ts +96 -0
- package/src/theme/spacing.ts +18 -0
- package/src/theme/theme-colors.ts +88 -0
- package/src/theme/types.ts +125 -0
- package/src/uploads/UploadAdapter.ts +35 -0
- package/src/uploads/index.ts +2 -0
- package/src/uploads/localUpload.test.ts +70 -0
- package/src/uploads/localUpload.ts +84 -0
- package/src/validation/Validator.ts +49 -0
- package/src/validation/index.ts +28 -0
- package/src/validation/rules.ts +78 -0
- package/src/validation/runValidators.ts +435 -0
- package/src/validation/uniqueValidator.test.ts +196 -0
- package/src/validation/uniqueValidator.ts +133 -0
- package/src/validation/validators.test.ts +268 -0
- package/src/vite.ts +758 -0
- package/src/widgets/index.ts +10 -0
- package/src/widgets/registry.ts +45 -0
- package/src/widgets.test.ts +592 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +4 -0
- package/tsconfig.test.json +10 -0
- package/views/react/Dashboard.tsx +27 -0
- package/views/react/Resources/Form.tsx +102 -0
- package/views/react/Resources/Index.tsx +49 -0
|
@@ -0,0 +1,2124 @@
|
|
|
1
|
+
import { Element, type ElementMeta } from '../schema/Element.js'
|
|
2
|
+
import type { ValidationErrors } from '../validation/index.js'
|
|
3
|
+
import type { Notification, NotificationMeta } from '../notifications/Notification.js'
|
|
4
|
+
import {
|
|
5
|
+
safeManagerPolicy,
|
|
6
|
+
type RelationManager,
|
|
7
|
+
type RelationManagerContext,
|
|
8
|
+
} from '../RelationManager.js'
|
|
9
|
+
import {
|
|
10
|
+
computeMorphPayload,
|
|
11
|
+
getMorphRelationDescriptor,
|
|
12
|
+
getParentRelationDescriptor,
|
|
13
|
+
type ModelLike,
|
|
14
|
+
} from '../orm/modelDefaults.js'
|
|
15
|
+
import { resolveM2MAccessor } from '../orm/m2mAccessor.js'
|
|
16
|
+
import { buildImportSchema as buildImportModalSchema } from './importFactory.js'
|
|
17
|
+
import { buildAttachModalSchema } from './attachFactory.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Where an Action renders. `inline` is the default — appears wherever the
|
|
21
|
+
* Action sits in the schema tree (e.g. a button inside a Card). The other
|
|
22
|
+
* three are list-page patterns:
|
|
23
|
+
* - `header` — top-right of a resource list (e.g. "Create new")
|
|
24
|
+
* - `bulk` — appears in the action bar when rows are selected
|
|
25
|
+
* - `row` — per-row dropdown menu entry
|
|
26
|
+
*/
|
|
27
|
+
export type ActionPlacement = 'inline' | 'bulk' | 'row' | 'header'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Context handed to an Action's handler at dispatch time. `record` is set
|
|
31
|
+
* for row/inline actions that operate on a single entity; `records` is set
|
|
32
|
+
* for bulk actions. `values` carries any additional payload submitted with
|
|
33
|
+
* the action (useful when an action has its own confirmation dialog form).
|
|
34
|
+
* `request` is the raw `AppRequest` for handlers that need direct access
|
|
35
|
+
* (auth, headers, etc).
|
|
36
|
+
*/
|
|
37
|
+
export interface ActionContext {
|
|
38
|
+
record?: unknown
|
|
39
|
+
records?: unknown[]
|
|
40
|
+
user?: unknown
|
|
41
|
+
values?: Record<string, unknown>
|
|
42
|
+
request?: unknown
|
|
43
|
+
/**
|
|
44
|
+
* Row-scoped context populated when this action was dispatched as a
|
|
45
|
+
* Repeater / Builder `extraItemActions` button. `index` is the row's
|
|
46
|
+
* 0-based position; `id` is the row's stable `__id`; `values` is the
|
|
47
|
+
* row's submitted fields (the row's `data` body inside Builder); for
|
|
48
|
+
* Builder, `blockType` carries the matched block name. `fieldName`
|
|
49
|
+
* is the parent Repeater/Builder field's name — useful when a single
|
|
50
|
+
* handler is shared across multiple repeater fields.
|
|
51
|
+
*
|
|
52
|
+
* Always undefined for non-row actions. Handlers that don't care about
|
|
53
|
+
* row context just ignore this field.
|
|
54
|
+
*/
|
|
55
|
+
row?: {
|
|
56
|
+
index: number
|
|
57
|
+
id: string
|
|
58
|
+
values: Record<string, unknown>
|
|
59
|
+
fieldName: string
|
|
60
|
+
blockType?: string
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Stamped by the manager-scoped `_action` route when an action is
|
|
64
|
+
* dispatched from a `RelationManager`'s table. Carries the freshly-
|
|
65
|
+
* loaded parent record + relationship key so handlers can call
|
|
66
|
+
* `parent.related(name).attach(...) / detach(...) / sync(...)`
|
|
67
|
+
* (rudder ORM accessor) without re-loading the parent themselves.
|
|
68
|
+
*
|
|
69
|
+
* Always undefined for resource-level / page-level / dashboard
|
|
70
|
+
* dispatch. M2M-aware factories (`relationAttach / relationDetach /
|
|
71
|
+
* relationBulkDetach`) consult this and `notify` an error when it's
|
|
72
|
+
* missing — that means the action got dispatched outside the manager
|
|
73
|
+
* scope (misconfiguration).
|
|
74
|
+
*/
|
|
75
|
+
relation?: {
|
|
76
|
+
parent: unknown
|
|
77
|
+
parentId: string
|
|
78
|
+
relationship: string
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convenience type: handlers can return either a built `Notification`
|
|
83
|
+
* instance, its serialized meta, or arrays of either. */
|
|
84
|
+
export type NotificationLike =
|
|
85
|
+
| Notification
|
|
86
|
+
| NotificationMeta
|
|
87
|
+
| ReadonlyArray<Notification | NotificationMeta>
|
|
88
|
+
|
|
89
|
+
/** Download envelope a handler can return to ask the route layer to
|
|
90
|
+
* stream a file back instead of issuing the standard JSON / 303
|
|
91
|
+
* response. Used by `Action.export / Action.bulkExport`; any handler
|
|
92
|
+
* is free to return one. The route writes `Content-Type` +
|
|
93
|
+
* `Content-Disposition: attachment; filename="…"` and ends with
|
|
94
|
+
* `body`; the client renderer detects the disposition header and
|
|
95
|
+
* triggers a browser download via a synthesized `<a download>`. */
|
|
96
|
+
export interface DownloadEnvelope {
|
|
97
|
+
filename: string
|
|
98
|
+
contentType: string
|
|
99
|
+
body: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Result a handler may return to influence the response. `void` is the
|
|
104
|
+
* default — the dispatcher 303-redirects to the page the action was
|
|
105
|
+
* triggered from. Returning `{ redirect }` overrides that with an
|
|
106
|
+
* explicit URL. Returning `{ notify }` flashes one or more toast
|
|
107
|
+
* notifications on the next render. Returning `{ download }` triggers
|
|
108
|
+
* a file download (mutually exclusive with `redirect` — the route
|
|
109
|
+
* prefers the download branch when both are set). Throw an Error to
|
|
110
|
+
* surface as a 500 with the message.
|
|
111
|
+
*/
|
|
112
|
+
export type ActionResult =
|
|
113
|
+
| void
|
|
114
|
+
| { redirect?: string; notify?: NotificationLike; download?: DownloadEnvelope }
|
|
115
|
+
|
|
116
|
+
export type ActionHandler = (ctx: ActionContext) => ActionResult | Promise<ActionResult>
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A confirmation prompt shown before the handler runs. A bare string is
|
|
120
|
+
* shorthand for `{ message: string }`; the object form lets callers
|
|
121
|
+
* override the dialog title and confirm-button label.
|
|
122
|
+
*/
|
|
123
|
+
export interface ActionConfirm {
|
|
124
|
+
title?: string
|
|
125
|
+
message: string
|
|
126
|
+
confirmLabel?: string
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** HTTP method for form-style actions. `'get'` is implied by `.href()`; the
|
|
130
|
+
* others spawn a `<form>`-wrapped submit button at render time. */
|
|
131
|
+
export type ActionMethod = 'post' | 'put' | 'patch' | 'delete'
|
|
132
|
+
|
|
133
|
+
/** Visual color preset. Maps to a tailwind class group at render time.
|
|
134
|
+
* `destructive` is what `Action.destructive()` sugar sets; the other
|
|
135
|
+
* presets exist so users can opt-in explicitly. */
|
|
136
|
+
export type ActionColor = 'primary' | 'destructive' | 'success' | 'warning' | 'info' | 'ghost'
|
|
137
|
+
|
|
138
|
+
/** Visual size preset. Maps to button height + padding + text size. */
|
|
139
|
+
export type ActionSize = 'sm' | 'md' | 'lg'
|
|
140
|
+
|
|
141
|
+
/** Context passed to visibility / disabled callbacks. `record` is set
|
|
142
|
+
* for single-target evaluation (row actions, edit-page header actions);
|
|
143
|
+
* `records` for bulk evaluations; `user` from the request when wired.
|
|
144
|
+
*
|
|
145
|
+
* `values` is populated only when this action is being evaluated inside
|
|
146
|
+
* a Repeater / Builder row at meta-build time (via `extraItemActions`).
|
|
147
|
+
* It mirrors the resolver's row-scoped values map — predicates branch
|
|
148
|
+
* on the row's submitted fields (e.g. `({ values }) => values.status !==
|
|
149
|
+
* 'archived'`). */
|
|
150
|
+
export interface ActionVisibilityContext {
|
|
151
|
+
record?: unknown
|
|
152
|
+
records?: unknown[]
|
|
153
|
+
user?: unknown
|
|
154
|
+
values?: Record<string, unknown>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Boolean-or-callback rule used by `.visible()` / `.hidden()` /
|
|
158
|
+
* `.disabled()`. Boolean values short-circuit; functions receive the
|
|
159
|
+
* evaluation context and return the result (sync or async).
|
|
160
|
+
*
|
|
161
|
+
* Async support landed with Plan #10 authorization — `Resource.canEdit`
|
|
162
|
+
* etc. return Promise<boolean>, and the `Action.create/edit/view/delete`
|
|
163
|
+
* factories install those predicates as visibility rules. Sync rules
|
|
164
|
+
* keep working unchanged; the awaiter coerces both. */
|
|
165
|
+
export type VisibilityRule =
|
|
166
|
+
| boolean
|
|
167
|
+
| ((ctx: ActionVisibilityContext) => boolean | Promise<boolean>)
|
|
168
|
+
|
|
169
|
+
/** Modal width preset — maps to a max-width class on the Dialog popup. */
|
|
170
|
+
export type ActionModalWidth = 'sm' | 'md' | 'lg' | 'xl'
|
|
171
|
+
|
|
172
|
+
/** Horizontal alignment applied to the modal's header / body / footer
|
|
173
|
+
* text. Matches Filament v5's `modalAlignment()` (start / center / end). */
|
|
174
|
+
export type ActionModalAlignment = 'start' | 'center' | 'end'
|
|
175
|
+
|
|
176
|
+
/** Color preset for the icon shown next to the modal heading. Mirrors
|
|
177
|
+
* `BadgeColor` so userspace doesn't have to learn another scale. */
|
|
178
|
+
export type ActionModalIconColor =
|
|
179
|
+
| 'gray' | 'primary' | 'success' | 'warning' | 'destructive' | 'info'
|
|
180
|
+
|
|
181
|
+
/** Context shape passed to `ReplicateOptions.getCreatedNotificationTitle`.
|
|
182
|
+
* Single-row factories (`replicate`, `relationReplicate`) populate
|
|
183
|
+
* `replica` (the just-created record returned by `M.create`) and
|
|
184
|
+
* `source` (the original row). Bulk factories (`bulkReplicate`,
|
|
185
|
+
* `relationBulkReplicate`) populate `count` (number of successful
|
|
186
|
+
* creates) and `records` (the original selected rows). The opposite
|
|
187
|
+
* fields are always undefined for the other call site so consumers
|
|
188
|
+
* can branch on whichever is set. */
|
|
189
|
+
export interface ReplicateNotificationContext {
|
|
190
|
+
replica?: unknown
|
|
191
|
+
source?: unknown
|
|
192
|
+
count?: number
|
|
193
|
+
records?: unknown[]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Context shape passed to `ReplicateOptions.getRedirectUrl`. Single-row
|
|
197
|
+
* factories only — bulk variants stay on the list / manager URL
|
|
198
|
+
* regardless. `replica` is the freshly-created record; `source` is
|
|
199
|
+
* the original. */
|
|
200
|
+
export interface ReplicateRedirectContext {
|
|
201
|
+
replica: unknown
|
|
202
|
+
source: unknown
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Options bag for `Action.replicate` and its three siblings
|
|
206
|
+
* (`bulkReplicate`, `relationReplicate`, `relationBulkReplicate`).
|
|
207
|
+
* Every field optional. */
|
|
208
|
+
export interface ReplicateOptions {
|
|
209
|
+
/** Attribute names to drop from the replicated payload IN ADDITION TO
|
|
210
|
+
* the always-stripped primary key + soft-delete column. Useful for
|
|
211
|
+
* unique columns the source row holds (e.g. `slug`, `email`) so the
|
|
212
|
+
* duplicate doesn't trip a unique constraint on save. */
|
|
213
|
+
excludeAttributes?: ReadonlyArray<string>
|
|
214
|
+
/** Mutate the prepared replica before it's persisted. Receives the
|
|
215
|
+
* already-stripped attributes plus the source record; must return the
|
|
216
|
+
* attributes to persist (mutate or replace). Useful for stamping
|
|
217
|
+
* "Copy of" prefixes onto a name column, regenerating slugs, etc. */
|
|
218
|
+
beforeReplicaSaved?: (
|
|
219
|
+
replica: Record<string, unknown>,
|
|
220
|
+
source: unknown,
|
|
221
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
222
|
+
/** Override the success notification title. Single-row factories
|
|
223
|
+
* receive `{ replica, source }`; bulk factories receive
|
|
224
|
+
* `{ count, records }` (`replica`/`source` undefined). Return a
|
|
225
|
+
* string to use it; return `undefined` to fall back to the default
|
|
226
|
+
* (`"${labelSingular} replicated"` for single-row,
|
|
227
|
+
* `"${count} ${label(s)} replicated"` for bulk). Sync or async. */
|
|
228
|
+
getCreatedNotificationTitle?: (
|
|
229
|
+
ctx: ReplicateNotificationContext,
|
|
230
|
+
) => string | undefined | Promise<string | undefined>
|
|
231
|
+
/** Override the redirect URL. Single-row factories only — bulk
|
|
232
|
+
* variants don't redirect. Receives `{ replica, source }`. Return a
|
|
233
|
+
* string to use it; return `undefined` to fall back to the default
|
|
234
|
+
* (the new record's edit page for `replicate`; the manager list
|
|
235
|
+
* URL for `relationReplicate`, owned by the route layer). Sync or
|
|
236
|
+
* async. */
|
|
237
|
+
getRedirectUrl?: (
|
|
238
|
+
ctx: ReplicateRedirectContext,
|
|
239
|
+
) => string | undefined | Promise<string | undefined>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Structural shape of a Resource class for the factory functions —
|
|
243
|
+
* matches `Resource.ts` exactly but keeps Action.ts free of an import
|
|
244
|
+
* cycle. The optional fields are the Plan #10 policy predicates; their
|
|
245
|
+
* defaults (return `true`) mean missing methods are equivalent to
|
|
246
|
+
* "always allowed." */
|
|
247
|
+
interface ResourceLike {
|
|
248
|
+
labelSingular: string
|
|
249
|
+
/** Plural label. When unset, factories fall back to
|
|
250
|
+
* `${labelSingular}s` (naive). Used by bulk-action notification
|
|
251
|
+
* copy so "1 Post" / "5 Posts" render correctly. */
|
|
252
|
+
label?: string
|
|
253
|
+
getSlug(): string
|
|
254
|
+
/** Cluster the resource belongs to, when registered via `Pilotiq.clusters`.
|
|
255
|
+
* Action factories thread the cluster slug into URL builders so
|
|
256
|
+
* `${basePath}/<cluster>/<slug>/...` round-trips correctly. */
|
|
257
|
+
cluster?: { getSlug(): string }
|
|
258
|
+
/** Plan #13 — soft-delete opt-in flag. When true, `Action.delete`
|
|
259
|
+
* auto-hides on already-trashed rows; `Action.restore` /
|
|
260
|
+
* `Action.forceDelete` auto-show on trashed rows. */
|
|
261
|
+
softDeletes?: boolean
|
|
262
|
+
/** Plan #13 — column name carrying the soft-delete timestamp.
|
|
263
|
+
* Defaults to `'deletedAt'` when undefined. */
|
|
264
|
+
deletedAtColumn?: string
|
|
265
|
+
canCreate?(user: unknown): boolean | Promise<boolean>
|
|
266
|
+
canEdit?(user: unknown, record: unknown): boolean | Promise<boolean>
|
|
267
|
+
canView?(user: unknown, record: unknown): boolean | Promise<boolean>
|
|
268
|
+
canViewAny?(user: unknown): boolean | Promise<boolean>
|
|
269
|
+
canDelete?(user: unknown, record: unknown): boolean | Promise<boolean>
|
|
270
|
+
canRestore?(user: unknown, record: unknown): boolean | Promise<boolean>
|
|
271
|
+
canForceDelete?(user: unknown, record: unknown): boolean | Promise<boolean>
|
|
272
|
+
/** Resource model adapter (set when the resource opts into auto-CRUD
|
|
273
|
+
* via `static model = M`). Import / Export factories need access to
|
|
274
|
+
* `create / update / find / query` to drive their writes / reads. */
|
|
275
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
276
|
+
model?: any
|
|
277
|
+
/** Resource table configurator. Export factory calls
|
|
278
|
+
* `R.table(Table.make())` to discover columns + the records handler
|
|
279
|
+
* for the "filtered" / "all" scope. */
|
|
280
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
281
|
+
table?(t: any): any
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Cluster-aware resource base path. Mirrors `clusterPaths.resourceBasePath`
|
|
285
|
+
* but uses the structural `ResourceLike` shape so `Action.ts` stays
|
|
286
|
+
* cycle-free against `Resource.ts`. */
|
|
287
|
+
function resourceBase(basePath: string, R: ResourceLike): string {
|
|
288
|
+
if (R.cluster) return `${basePath}/${R.cluster.getSlug()}/${R.getSlug()}`
|
|
289
|
+
return `${basePath}/${R.getSlug()}`
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Pick the right label form for a count — `labelSingular` for 1,
|
|
293
|
+
* `label` (plural, lowercased) for any other count. Fall back to a
|
|
294
|
+
* naive `${labelSingular}s` when no plural label is set. Used by bulk
|
|
295
|
+
* notification copy so we don't ship "1 posts moved to trash". */
|
|
296
|
+
function labelForCount(R: ResourceLike, n: number): string {
|
|
297
|
+
if (n === 1) return R.labelSingular.toLowerCase()
|
|
298
|
+
const plural = R.label?.toLowerCase()
|
|
299
|
+
return plural ?? `${R.labelSingular.toLowerCase()}s`
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** True when a `RelationManagerContext.mode` denotes a pivot-mutation
|
|
303
|
+
* shape — i.e. a many-to-many relation. All three modes share the
|
|
304
|
+
* `attach` / `detach` / `sync` accessor surface (the rudder ORM stamps
|
|
305
|
+
* + filters the polymorphic discriminator transparently for the morph
|
|
306
|
+
* variants). The `relationCreate / Edit / Delete` factories auto-hide
|
|
307
|
+
* under any of these modes because per-pivot-row create / edit / delete
|
|
308
|
+
* is meaningless — users create the related record via its own Resource,
|
|
309
|
+
* then attach via `relationAttach`. */
|
|
310
|
+
function isM2MMode(mode: RelationManagerContext['mode']): boolean {
|
|
311
|
+
return mode === 'belongsToMany' || mode === 'morphToMany' || mode === 'morphedByMany'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Phase B — build the URL prefix for a relation factory action. Without
|
|
316
|
+
* a `chain` (depth-1 manager), this is the familiar
|
|
317
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}`. With a chain
|
|
318
|
+
* (depth-2 nested manager), it threads the outer record + relationship
|
|
319
|
+
* between the parent slug and the leaf parent id:
|
|
320
|
+
*
|
|
321
|
+
* `${base}/${parentSlug}/${chain[0].recordId}/${chain[0].relationship}/${parentId}/${relationship}`
|
|
322
|
+
*
|
|
323
|
+
* Pure; takes a `RelationManagerContext` and emits a string. The leaf
|
|
324
|
+
* record id (and trailing `/edit`, `/delete`, etc.) gets appended by
|
|
325
|
+
* the caller.
|
|
326
|
+
*/
|
|
327
|
+
function relationUrlPrefix(ctx: RelationManagerContext): string {
|
|
328
|
+
const head = `${ctx.basePath}/${ctx.parentSlug}`
|
|
329
|
+
const chain = ctx.chain ?? []
|
|
330
|
+
let mid = ''
|
|
331
|
+
for (const step of chain) {
|
|
332
|
+
mid += `/${step.recordId}/${step.relationship}`
|
|
333
|
+
}
|
|
334
|
+
return `${head}${mid}/${ctx.parentId}/${ctx.relationship}`
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Compute the parent-attachment payload to force-pin onto a relation
|
|
339
|
+
* replica. For `hasMany`, returns `{ [foreignKey]: parentId }` from the
|
|
340
|
+
* parent's `static relations[name]` descriptor. For `morphMany` /
|
|
341
|
+
* `morphOne`, returns `{ <morphName>Id, <morphName>Type }` via
|
|
342
|
+
* `computeMorphPayload(parentRecord)`. Returns `{}` when no descriptor
|
|
343
|
+
* matches — the route dispatcher already auto-hides under M2M / morphTo,
|
|
344
|
+
* so missing descriptors there are a no-op rather than an error. Pure;
|
|
345
|
+
* exported for tests and re-used by both factories.
|
|
346
|
+
*/
|
|
347
|
+
function computeRelationPin(
|
|
348
|
+
ctx: RelationManagerContext,
|
|
349
|
+
): Record<string, unknown> {
|
|
350
|
+
const parentModel = (ctx.parentRecord as { constructor?: ModelLike } | null | undefined)?.constructor
|
|
351
|
+
if (!parentModel) return {}
|
|
352
|
+
const rel = ctx.relationship
|
|
353
|
+
// Polymorphic owner side first — `morphMany` carries no foreignKey
|
|
354
|
+
// and would fail the hasMany descriptor's gate.
|
|
355
|
+
if (ctx.mode === 'morphMany') {
|
|
356
|
+
const morph = getMorphRelationDescriptor(parentModel, rel)
|
|
357
|
+
if (!morph) return {}
|
|
358
|
+
try { return computeMorphPayload(ctx.parentRecord, morph) }
|
|
359
|
+
catch { return {} }
|
|
360
|
+
}
|
|
361
|
+
const desc = getParentRelationDescriptor(parentModel, rel)
|
|
362
|
+
if (!desc) return {}
|
|
363
|
+
return { [desc.foreignKey]: ctx.parentId }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Build + persist a single relation replica. Runs the strip set
|
|
368
|
+
* (PK + soft-delete column on the **related** Resource +
|
|
369
|
+
* `opts.excludeAttributes`), force-pins the parent attachment columns,
|
|
370
|
+
* runs the optional `beforeReplicaSaved` hook, and calls
|
|
371
|
+
* `Related.model.create(...)`. Returns the model's create result so
|
|
372
|
+
* callers can read its primary key for redirect targeting.
|
|
373
|
+
*
|
|
374
|
+
* Throws when the related Resource has no model — caller (single-row
|
|
375
|
+
* factory) catches and surfaces an error notification; bulk caller
|
|
376
|
+
* checks the model presence ahead of the loop.
|
|
377
|
+
*/
|
|
378
|
+
async function persistRelationReplica(
|
|
379
|
+
_M: typeof RelationManager,
|
|
380
|
+
ctx: RelationManagerContext,
|
|
381
|
+
source: unknown,
|
|
382
|
+
opts: ReplicateOptions,
|
|
383
|
+
): Promise<unknown> {
|
|
384
|
+
const Related = ctx.related
|
|
385
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
386
|
+
throw new Error('Related Resource has no model.create')
|
|
387
|
+
}
|
|
388
|
+
const M2 = Related.model as ModelLike
|
|
389
|
+
const pkCol = (M2 as { primaryKey?: string }).primaryKey ?? 'id'
|
|
390
|
+
const trashedCol = Related.deletedAtColumn ?? 'deletedAt'
|
|
391
|
+
const skip = new Set<string>([pkCol, trashedCol, ...(opts.excludeAttributes ?? [])])
|
|
392
|
+
let replica: Record<string, unknown> = {}
|
|
393
|
+
for (const [k, v] of Object.entries(source as Record<string, unknown>)) {
|
|
394
|
+
if (skip.has(k)) continue
|
|
395
|
+
replica[k] = v
|
|
396
|
+
}
|
|
397
|
+
// Force-pin the parent attachment AFTER the strip but BEFORE the
|
|
398
|
+
// user mutator, so `beforeReplicaSaved` can read / override the FK
|
|
399
|
+
// if it really wants to (rare). Tampered source rows can't slip a
|
|
400
|
+
// different parent in by riding their own FK column — the pin
|
|
401
|
+
// overwrites whatever value was there.
|
|
402
|
+
Object.assign(replica, computeRelationPin(ctx))
|
|
403
|
+
if (opts.beforeReplicaSaved) {
|
|
404
|
+
replica = await opts.beforeReplicaSaved(replica, source)
|
|
405
|
+
}
|
|
406
|
+
return M2.create(replica)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Single-row dispatch for `Action.relationReplicate`. Resolves
|
|
411
|
+
* `ctx.record` (loaded by the route's resolveRecord hook), validates,
|
|
412
|
+
* persists the replica, and shapes the success notification. Errors
|
|
413
|
+
* are caught and surfaced as error toasts.
|
|
414
|
+
*/
|
|
415
|
+
async function runRelationReplicateRow(
|
|
416
|
+
M: typeof RelationManager,
|
|
417
|
+
ctx: RelationManagerContext,
|
|
418
|
+
hctx: ActionContext,
|
|
419
|
+
opts: ReplicateOptions,
|
|
420
|
+
): Promise<ActionResult> {
|
|
421
|
+
const source = hctx.record
|
|
422
|
+
if (!source || typeof source !== 'object') {
|
|
423
|
+
return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
|
|
424
|
+
}
|
|
425
|
+
const Related = ctx.related
|
|
426
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
427
|
+
return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
|
|
428
|
+
}
|
|
429
|
+
let created: unknown
|
|
430
|
+
try {
|
|
431
|
+
created = await persistRelationReplica(M, ctx, source, opts)
|
|
432
|
+
} catch (err) {
|
|
433
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
434
|
+
}
|
|
435
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
436
|
+
? await opts.getCreatedNotificationTitle({ replica: created, source })
|
|
437
|
+
: undefined
|
|
438
|
+
const title = overrideTitle !== undefined ? overrideTitle : `${M.getLabelSingular()} replicated`
|
|
439
|
+
// The manager-scoped `_action/:actionName` route falls back to the
|
|
440
|
+
// manager list URL when `result.redirect` is undefined, so we only
|
|
441
|
+
// emit `redirect` when the user override returned a string. That
|
|
442
|
+
// way default behavior (route owns the fallback) is unchanged.
|
|
443
|
+
const overrideRedirect = opts.getRedirectUrl
|
|
444
|
+
? await opts.getRedirectUrl({ replica: created, source })
|
|
445
|
+
: undefined
|
|
446
|
+
return {
|
|
447
|
+
...(overrideRedirect !== undefined ? { redirect: overrideRedirect } : {}),
|
|
448
|
+
notify: { title, type: 'success' } as never,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Read `record[R.deletedAtColumn ?? 'deletedAt']` and return true when
|
|
453
|
+
* the row is currently trashed (soft-deleted). Permissive on shape —
|
|
454
|
+
* bare `null` / `undefined` count as live; any other truthy value is
|
|
455
|
+
* trashed. */
|
|
456
|
+
function isTrashed(record: unknown, R: ResourceLike): boolean {
|
|
457
|
+
if (!record || typeof record !== 'object') return false
|
|
458
|
+
const col = R.deletedAtColumn ?? 'deletedAt'
|
|
459
|
+
const v = (record as Record<string, unknown>)[col]
|
|
460
|
+
return v !== null && v !== undefined
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Lazy-load the `Table` class for use inside Action handlers. Direct
|
|
464
|
+
* module-level import would cycle (Table → Action → Table); dynamic
|
|
465
|
+
* import inside a handler runs after both modules have finished
|
|
466
|
+
* loading. Cached after first call so we don't pay the import cost
|
|
467
|
+
* on every dispatched export. */
|
|
468
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
469
|
+
let _TableClass: any | undefined
|
|
470
|
+
async function loadTableClass(): Promise<unknown> {
|
|
471
|
+
if (_TableClass !== undefined) return _TableClass.make()
|
|
472
|
+
const mod = await import('../elements/Table.js')
|
|
473
|
+
_TableClass = mod.Table
|
|
474
|
+
return _TableClass.make()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Call a (possibly undefined) Resource predicate. When unset, the
|
|
478
|
+
* predicate is treated as "allowed" (returns true) so the factory
|
|
479
|
+
* doesn't hide actions on Resources that haven't opted into Plan #10. */
|
|
480
|
+
function callPredicate(
|
|
481
|
+
fn: ((user: unknown, record?: unknown) => boolean | Promise<boolean>) | undefined,
|
|
482
|
+
user: unknown,
|
|
483
|
+
record?: unknown,
|
|
484
|
+
): boolean | Promise<boolean> {
|
|
485
|
+
if (!fn) return true
|
|
486
|
+
return fn(user, record)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** Render-time meta for an action that opens a modal (with or without a
|
|
490
|
+
* form schema). When `meta.children` is also populated by the resolver,
|
|
491
|
+
* the modal renders those Elements as a form whose values pass through
|
|
492
|
+
* to the handler as `ctx.values`. */
|
|
493
|
+
export interface ActionModalMeta {
|
|
494
|
+
heading?: string
|
|
495
|
+
description?: string
|
|
496
|
+
submitLabel?: string
|
|
497
|
+
cancelLabel?: string
|
|
498
|
+
icon?: string
|
|
499
|
+
/** Color preset applied to the heading icon (`modalIconColor()`).
|
|
500
|
+
* Renderer maps to a tailwind text-color class. */
|
|
501
|
+
iconColor?: ActionModalIconColor
|
|
502
|
+
width?: ActionModalWidth
|
|
503
|
+
slideOver?: boolean
|
|
504
|
+
/** Default `true`; emitted only when the user passed `false`. The
|
|
505
|
+
* renderer turns this into Base UI's `disablePointerDismissal`. */
|
|
506
|
+
closeByClickingAway?: boolean
|
|
507
|
+
/** Default `true`; emitted only when `false`. Renderer cancels the
|
|
508
|
+
* Base UI `onOpenChange` event when reason is `'escapeKey'`. */
|
|
509
|
+
closeByEscaping?: boolean
|
|
510
|
+
/** Sticky header inside a scrolling modal body. Default `false`. */
|
|
511
|
+
stickyHeader?: boolean
|
|
512
|
+
/** Sticky footer inside a scrolling modal body. Default `false`. */
|
|
513
|
+
stickyFooter?: boolean
|
|
514
|
+
/** Override the default autofocus behaviour. When `false` no element
|
|
515
|
+
* inside the modal receives focus on mount; when `true` the renderer
|
|
516
|
+
* focuses the first form input (or the submit button when there is
|
|
517
|
+
* no form). When omitted, the legacy default applies (the submit
|
|
518
|
+
* button autofocuses for confirm-only modals). */
|
|
519
|
+
autofocus?: boolean
|
|
520
|
+
/** Horizontal alignment for the heading / body / footer text. */
|
|
521
|
+
alignment?: ActionModalAlignment
|
|
522
|
+
/** When `true`, an X close-button renders in the top-right of the
|
|
523
|
+
* popup. Default `false`. */
|
|
524
|
+
closeButton?: boolean
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface ActionMeta extends ElementMeta {
|
|
528
|
+
type: 'action'
|
|
529
|
+
name: string
|
|
530
|
+
label: string
|
|
531
|
+
placement: ActionPlacement
|
|
532
|
+
destructive: boolean
|
|
533
|
+
icon?: string
|
|
534
|
+
confirm?: ActionConfirm
|
|
535
|
+
href?: string
|
|
536
|
+
method?: ActionMethod
|
|
537
|
+
action?: string
|
|
538
|
+
/** POST URL for handler-style actions. Set server-side by the route
|
|
539
|
+
* registrar so the client knows where to dispatch. */
|
|
540
|
+
dispatchUrl?: string
|
|
541
|
+
/** True when this action submits its enclosing `<form>` — renders as
|
|
542
|
+
* `<button type="submit">` and lets the form's `action`/`method`
|
|
543
|
+
* attributes drive the request. */
|
|
544
|
+
submit?: boolean
|
|
545
|
+
/** When `submit` is true and this id is set, the rendered button uses
|
|
546
|
+
* the HTML `form="<id>"` attribute so it can submit a form it lives
|
|
547
|
+
* outside of (e.g. a Save action in the page header). */
|
|
548
|
+
form?: string
|
|
549
|
+
/** When `submit` is true, attach `name`+`value` to the `<button>` so
|
|
550
|
+
* the clicked button's pair lands in the form body. Used by the
|
|
551
|
+
* "Create & create another" pattern: a secondary submit posts a
|
|
552
|
+
* sentinel like `{ _continueCreate: '1' }` that the server reads to
|
|
553
|
+
* route the redirect back to the create page. */
|
|
554
|
+
formField?: { name: string; value: string }
|
|
555
|
+
/** Modal-style action chrome. Present when `.schema()` and/or any of
|
|
556
|
+
* the `modalXxx` builders ran. The fields themselves arrive on
|
|
557
|
+
* `meta.children` via the schema resolver. */
|
|
558
|
+
modal?: ActionModalMeta
|
|
559
|
+
/** Color preset — drives button colors at render time. `destructive`
|
|
560
|
+
* coincides with `destructive: true` (kept for back-compat). */
|
|
561
|
+
color?: ActionColor
|
|
562
|
+
/** Size preset — drives button height/padding/text-size. */
|
|
563
|
+
size?: ActionSize
|
|
564
|
+
/** Hover tooltip text. Wraps the rendered button in a Tooltip. */
|
|
565
|
+
tooltip?: string
|
|
566
|
+
/** Outlined trigger style — replaces the solid color background with
|
|
567
|
+
* a border + transparent bg. */
|
|
568
|
+
outlined?: boolean
|
|
569
|
+
/** Icon-only trigger style — hides the label and renders a square
|
|
570
|
+
* button. Requires `icon` to be set. */
|
|
571
|
+
iconOnly?: boolean
|
|
572
|
+
/** Optional badge shown on the trigger (e.g. unread count). */
|
|
573
|
+
badge?: string | number
|
|
574
|
+
badgeColor?: string
|
|
575
|
+
/** Disabled flag set at evaluation time. The trigger renders greyed-out
|
|
576
|
+
* and skips dispatch when true. */
|
|
577
|
+
disabled?: boolean
|
|
578
|
+
/** True when the action has `.visible()`, `.hidden()`, or `.disabled()`
|
|
579
|
+
* rules — the row renderer uses this to know whether to consult the
|
|
580
|
+
* row's `_visibleActions` / `_disabledActions` lookup. Static actions
|
|
581
|
+
* without rules render unconditionally. */
|
|
582
|
+
conditional?: boolean
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Action — a button-or-menu-entry that performs work when clicked.
|
|
587
|
+
*
|
|
588
|
+
* One class for all four placements; pick one via `.inline()` / `.row()` /
|
|
589
|
+
* `.bulk()` / `.header()` (or `.placement(...)`). Actions can sit inline
|
|
590
|
+
* inside any container Element (Card, Section, etc.) or attach to a
|
|
591
|
+
* Resource's list page.
|
|
592
|
+
*
|
|
593
|
+
* Phase 1.4 ships the shape + serialization. Handler dispatch and
|
|
594
|
+
* confirmation-form support land in Phase 2 alongside Resource lifecycle.
|
|
595
|
+
*/
|
|
596
|
+
export class Action extends Element {
|
|
597
|
+
readonly name: string
|
|
598
|
+
|
|
599
|
+
protected _label: string
|
|
600
|
+
protected _icon?: string
|
|
601
|
+
protected _placement: ActionPlacement = 'inline'
|
|
602
|
+
protected _destructive = false
|
|
603
|
+
protected _confirm?: ActionConfirm
|
|
604
|
+
protected _handler?: ActionHandler
|
|
605
|
+
/**
|
|
606
|
+
* Notification-registry handler key — set by `.handler(string)`. The
|
|
607
|
+
* notification-action route looks this up against
|
|
608
|
+
* `Pilotiq.notificationHandlers({…})` at request time. Mutually
|
|
609
|
+
* exclusive with the closure `_handler` — passing a string clears
|
|
610
|
+
* `_handler`, passing a function clears `_handlerName`. Closures can't
|
|
611
|
+
* round-trip through a `data` JSON column; the registry path is the
|
|
612
|
+
* persisted-notification escape hatch.
|
|
613
|
+
*/
|
|
614
|
+
protected _handlerName?: string
|
|
615
|
+
/**
|
|
616
|
+
* Per-fire context for registry-handler dispatch. Round-trips through
|
|
617
|
+
* the notification's `data.actions` JSON column verbatim and arrives
|
|
618
|
+
* on the handler's `ctx.payload`.
|
|
619
|
+
*/
|
|
620
|
+
protected _payload?: Record<string, unknown>
|
|
621
|
+
/**
|
|
622
|
+
* Filament-style chain modifier — when set on an action used inside a
|
|
623
|
+
* `Notification.actions([…])` slot, firing the action also flips the
|
|
624
|
+
* notification's `read_at`. No-op when the action isn't in a
|
|
625
|
+
* notification context.
|
|
626
|
+
*/
|
|
627
|
+
protected _markAsReadOnFire = false
|
|
628
|
+
/**
|
|
629
|
+
* When set, click-through opens in a new tab. Honored by the
|
|
630
|
+
* notification action strip renderers (bell + toast); ignored by
|
|
631
|
+
* everything else (Resource action triggers don't expose this knob —
|
|
632
|
+
* use `target` on the underlying anchor if you need it elsewhere).
|
|
633
|
+
*/
|
|
634
|
+
protected _openUrlInNewTab = false
|
|
635
|
+
protected _href?: string
|
|
636
|
+
protected _method?: ActionMethod
|
|
637
|
+
protected _actionUrl?: string
|
|
638
|
+
protected _dispatchUrl?: string
|
|
639
|
+
protected _submit = false
|
|
640
|
+
protected _formTarget?: string
|
|
641
|
+
protected _formField?: { name: string; value: string }
|
|
642
|
+
|
|
643
|
+
// Modal chrome — present whenever `.schema()` or any of the modal
|
|
644
|
+
// builders below have been called.
|
|
645
|
+
protected _hasModal = false
|
|
646
|
+
protected _modalHeading?: string
|
|
647
|
+
protected _modalDescription?: string
|
|
648
|
+
protected _modalSubmitLabel?: string
|
|
649
|
+
protected _modalCancelLabel?: string
|
|
650
|
+
protected _modalIcon?: string
|
|
651
|
+
protected _modalIconColor?: ActionModalIconColor
|
|
652
|
+
protected _modalWidth?: ActionModalWidth
|
|
653
|
+
protected _slideOver = false
|
|
654
|
+
// Defaults match the existing renderer behaviour (closeable both ways,
|
|
655
|
+
// no sticky chrome, no X button). Setters with a default arg of `false`
|
|
656
|
+
// / `true` mirror Filament's call shapes — `closeModalByClickingAway()`
|
|
657
|
+
// disables pointer dismiss, `stickyModalHeader()` enables sticky.
|
|
658
|
+
protected _closeModalByClickingAway = true
|
|
659
|
+
protected _closeModalByEscaping = true
|
|
660
|
+
protected _stickyModalHeader = false
|
|
661
|
+
protected _stickyModalFooter = false
|
|
662
|
+
protected _modalAutofocus?: boolean
|
|
663
|
+
protected _modalAlignment?: ActionModalAlignment
|
|
664
|
+
protected _modalCloseButton = false
|
|
665
|
+
|
|
666
|
+
// Trigger variants & cosmetics
|
|
667
|
+
protected _color?: ActionColor
|
|
668
|
+
protected _size?: ActionSize
|
|
669
|
+
protected _tooltip?: string
|
|
670
|
+
protected _outlined = false
|
|
671
|
+
protected _iconOnly = false
|
|
672
|
+
protected _badge?: string | number
|
|
673
|
+
protected _badgeColor?: string
|
|
674
|
+
|
|
675
|
+
// Conditional visibility / disabled rules
|
|
676
|
+
protected _visible?: VisibilityRule
|
|
677
|
+
protected _hidden?: VisibilityRule
|
|
678
|
+
protected _isDisabled?: VisibilityRule
|
|
679
|
+
|
|
680
|
+
private constructor(name: string) {
|
|
681
|
+
super()
|
|
682
|
+
this.name = name
|
|
683
|
+
this._label = name.charAt(0).toUpperCase() + name.slice(1)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
static make(name: string): Action {
|
|
687
|
+
return new Action(name)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ─── Resource-aware factories ─────────────────────────
|
|
691
|
+
//
|
|
692
|
+
// Pre-configured Action shapes that target a Resource's standard CRUD
|
|
693
|
+
// pages. Drop into `Table.recordActions([…])`, `headerActions([…])`,
|
|
694
|
+
// or `ViewPage.getActions(...)` — placement is stamped by the slot.
|
|
695
|
+
// Filament-style: explicit, but ergonomic.
|
|
696
|
+
//
|
|
697
|
+
// Each factory uses `:id` template substitution for row context; the
|
|
698
|
+
// renderer fills in the row's id when rendering. Header / view actions
|
|
699
|
+
// ignore the template (no `:id` needed for create / list URLs).
|
|
700
|
+
//
|
|
701
|
+
// Plan #10 — each factory auto-attaches a visibility rule that
|
|
702
|
+
// delegates to the Resource's matching policy method (`R.canCreate`
|
|
703
|
+
// for `Action.create`, etc). When `R.canX` is unset (default returns
|
|
704
|
+
// `true`) the action stays visible. Pass an explicit `.visible(...)`
|
|
705
|
+
// after the factory to override.
|
|
706
|
+
|
|
707
|
+
/** Create-action factory — link to `${basePath}/${R.slug}/create`.
|
|
708
|
+
* Auto-hides when `R.canCreate(user)` returns false. */
|
|
709
|
+
static create(R: ResourceLike, basePath: string): Action {
|
|
710
|
+
return Action.make('create')
|
|
711
|
+
.label(`New ${R.labelSingular}`)
|
|
712
|
+
.href(`${resourceBase(basePath, R)}/create`)
|
|
713
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Edit-action factory — link to the resource's edit page.
|
|
718
|
+
*
|
|
719
|
+
* Pass `recordId` when building actions for a single-record context
|
|
720
|
+
* (e.g. `ViewPage.getActions()`); the URL is baked at config time.
|
|
721
|
+
* Omit `recordId` for row context (`Table.recordActions(...)`); the
|
|
722
|
+
* URL keeps the `:id` template and the renderer substitutes per-row.
|
|
723
|
+
*
|
|
724
|
+
* Auto-hides when `R.canEdit(user, record)` returns false. For row
|
|
725
|
+
* context the per-row record threads in via `loadTableRecords`'s
|
|
726
|
+
* per-row eval; for view-page context, `resolveSchema` provides the
|
|
727
|
+
* resolved record on the eval context.
|
|
728
|
+
*/
|
|
729
|
+
static edit(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
730
|
+
const id = recordId ?? ':id'
|
|
731
|
+
return Action.make('edit')
|
|
732
|
+
.label('Edit')
|
|
733
|
+
.href(`${resourceBase(basePath, R)}/${id}/edit`)
|
|
734
|
+
.visible(({ user, record }) => callPredicate(R.canEdit, user, record))
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** View-action factory — link to the resource's view page. See `Action.edit` for the `recordId` semantics.
|
|
738
|
+
* Auto-hides when `R.canView(user, record)` returns false. */
|
|
739
|
+
static view(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
740
|
+
const id = recordId ?? ':id'
|
|
741
|
+
return Action.make('view')
|
|
742
|
+
.label('View')
|
|
743
|
+
.href(`${resourceBase(basePath, R)}/${id}`)
|
|
744
|
+
.visible(({ user, record }) => callPredicate(R.canView, user, record))
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Delete-action factory — POSTs to the resource's delete route,
|
|
749
|
+
* destructive style, with a confirmation prompt referencing the
|
|
750
|
+
* resource label. Same `recordId` semantics as `Action.edit`.
|
|
751
|
+
* Auto-hides when `R.canDelete(user, record)` returns false.
|
|
752
|
+
*
|
|
753
|
+
* Plan #13 — when `R.softDeletes = true`, additionally hides on
|
|
754
|
+
* rows whose `deletedAtColumn` is set (already-trashed rows get the
|
|
755
|
+
* Restore + ForceDelete pair instead, surfaced via the matching
|
|
756
|
+
* factories below).
|
|
757
|
+
*/
|
|
758
|
+
static delete(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
759
|
+
const id = recordId ?? ':id'
|
|
760
|
+
return Action.make('delete')
|
|
761
|
+
.label('Delete')
|
|
762
|
+
.destructive()
|
|
763
|
+
.method('post')
|
|
764
|
+
.action(`${resourceBase(basePath, R)}/${id}/delete`)
|
|
765
|
+
.confirm(`Delete this ${R.labelSingular.toLowerCase()}?`)
|
|
766
|
+
.visible(async ({ user, record }) => {
|
|
767
|
+
if (R.softDeletes && isTrashed(record, R)) return false
|
|
768
|
+
return callPredicate(R.canDelete, user, record)
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Replicate-action factory — handler-style. Loads the source record
|
|
774
|
+
* from `ctx.record` (the `_action/:actionName` route already resolves
|
|
775
|
+
* it through `R.query(ctx)` for row + single-target placements),
|
|
776
|
+
* strips PK + soft-delete column + any `opts.excludeAttributes`,
|
|
777
|
+
* optionally runs `opts.beforeReplicaSaved`, and creates a new row
|
|
778
|
+
* via `R.model.create(...)`. Redirects to the new record's edit page
|
|
779
|
+
* on success so the user can review + tweak before saving again.
|
|
780
|
+
*
|
|
781
|
+
* `recordId` kept in the signature for parity with `delete / edit /
|
|
782
|
+
* view` so users can swap factories without rewriting call sites; the
|
|
783
|
+
* dispatcher resolves the source record from the URL and hands it to
|
|
784
|
+
* the handler as `ctx.record`, so we don't reference `recordId` here.
|
|
785
|
+
*
|
|
786
|
+
* Auto-hides when `R.canCreate(user)` returns false — replicating
|
|
787
|
+
* writes a new row, so the gate is `canCreate`, not `canView`.
|
|
788
|
+
*/
|
|
789
|
+
static replicate(
|
|
790
|
+
R: ResourceLike,
|
|
791
|
+
basePath: string,
|
|
792
|
+
recordId?: string,
|
|
793
|
+
opts: ReplicateOptions = {},
|
|
794
|
+
): Action {
|
|
795
|
+
void recordId
|
|
796
|
+
return Action.make('replicate')
|
|
797
|
+
.label('Replicate')
|
|
798
|
+
.handler(async (ctx) => {
|
|
799
|
+
const source = ctx.record
|
|
800
|
+
if (!source || typeof source !== 'object') {
|
|
801
|
+
return { notify: { title: 'Replicate failed: source record missing', type: 'error' } as never }
|
|
802
|
+
}
|
|
803
|
+
const M = R.model
|
|
804
|
+
if (!M || typeof M.create !== 'function') {
|
|
805
|
+
return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const pkCol = (M as { primaryKey?: string }).primaryKey ?? 'id'
|
|
809
|
+
const trashedCol = R.deletedAtColumn ?? 'deletedAt'
|
|
810
|
+
const skip = new Set<string>([pkCol, trashedCol, ...(opts.excludeAttributes ?? [])])
|
|
811
|
+
let replica: Record<string, unknown> = {}
|
|
812
|
+
for (const [k, v] of Object.entries(source as Record<string, unknown>)) {
|
|
813
|
+
if (skip.has(k)) continue
|
|
814
|
+
replica[k] = v
|
|
815
|
+
}
|
|
816
|
+
if (opts.beforeReplicaSaved) {
|
|
817
|
+
try { replica = await opts.beforeReplicaSaved(replica, source) }
|
|
818
|
+
catch (err) {
|
|
819
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
let created: unknown
|
|
824
|
+
try {
|
|
825
|
+
created = await M.create(replica)
|
|
826
|
+
} catch (err) {
|
|
827
|
+
return { notify: { title: `Replicate failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const newId = (created as Record<string, unknown> | null | undefined)?.[pkCol]
|
|
831
|
+
const defaultRedirect = newId !== undefined && newId !== null
|
|
832
|
+
? `${resourceBase(basePath, R)}/${String(newId)}/edit`
|
|
833
|
+
: `${resourceBase(basePath, R)}`
|
|
834
|
+
// `!== undefined` rather than `??` so an override returning
|
|
835
|
+
// `null`/empty-string isn't silently swallowed (see
|
|
836
|
+
// feedback_nullish_swallows_explicit_null).
|
|
837
|
+
const overrideRedirect = opts.getRedirectUrl
|
|
838
|
+
? await opts.getRedirectUrl({ replica: created, source })
|
|
839
|
+
: undefined
|
|
840
|
+
const redirect = overrideRedirect !== undefined ? overrideRedirect : defaultRedirect
|
|
841
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
842
|
+
? await opts.getCreatedNotificationTitle({ replica: created, source })
|
|
843
|
+
: undefined
|
|
844
|
+
const title = overrideTitle !== undefined ? overrideTitle : `${R.labelSingular} replicated`
|
|
845
|
+
return {
|
|
846
|
+
redirect,
|
|
847
|
+
notify: { title, type: 'success' } as never,
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Plan #13 — Restore factory. POSTs to the resource's restore route,
|
|
855
|
+
* success-styled, no confirm prompt (restoration is reversible).
|
|
856
|
+
* Auto-hides on live (non-trashed) rows AND when `R.canRestore(user,
|
|
857
|
+
* record)` returns false. Same `recordId` semantics as `Action.edit`.
|
|
858
|
+
*/
|
|
859
|
+
static restore(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
860
|
+
const id = recordId ?? ':id'
|
|
861
|
+
return Action.make('restore')
|
|
862
|
+
.label('Restore')
|
|
863
|
+
.color('success')
|
|
864
|
+
.method('post')
|
|
865
|
+
.action(`${resourceBase(basePath, R)}/${id}/restore`)
|
|
866
|
+
.visible(async ({ user, record }) => {
|
|
867
|
+
if (!isTrashed(record, R)) return false
|
|
868
|
+
return callPredicate(R.canRestore, user, record)
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Plan #13 — Force-delete factory. POSTs to the resource's
|
|
874
|
+
* force-delete route, destructive-styled, with a stricter confirm
|
|
875
|
+
* prompt referencing permanence. Auto-hides on live (non-trashed)
|
|
876
|
+
* rows AND when `R.canForceDelete(user, record)` returns false.
|
|
877
|
+
*/
|
|
878
|
+
static forceDelete(R: ResourceLike, basePath: string, recordId?: string): Action {
|
|
879
|
+
const id = recordId ?? ':id'
|
|
880
|
+
return Action.make('forceDelete')
|
|
881
|
+
.label('Delete forever')
|
|
882
|
+
.destructive()
|
|
883
|
+
.method('post')
|
|
884
|
+
.action(`${resourceBase(basePath, R)}/${id}/force-delete`)
|
|
885
|
+
.confirm(`Permanently delete this ${R.labelSingular.toLowerCase()}? This cannot be undone.`)
|
|
886
|
+
.visible(async ({ user, record }) => {
|
|
887
|
+
if (!isTrashed(record, R)) return false
|
|
888
|
+
return callPredicate(R.canForceDelete, user, record)
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// ─── Notification factories ───────────────────────────────────
|
|
893
|
+
//
|
|
894
|
+
// Pre-configured Action shapes that target the bell-table read /
|
|
895
|
+
// unread endpoints mounted by `Pilotiq.databaseNotifications()`. Use
|
|
896
|
+
// inside a custom notification inbox page's `recordActions(...)` (or
|
|
897
|
+
// alongside a `Notification.actions([…])` slot when one ships) to
|
|
898
|
+
// give end-users explicit "Mark as read" / "Mark as unread" buttons.
|
|
899
|
+
//
|
|
900
|
+
// Filament-style chain modifier `Action::make('view')->markAsRead()`
|
|
901
|
+
// is a separate concern — it'd add an implicit mark-read side-effect
|
|
902
|
+
// to a custom action. v1 ships only the explicit factory; the chain
|
|
903
|
+
// modifier is deferred until a consumer asks.
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Mark-as-read factory — POSTs to the panel's notification read
|
|
907
|
+
* endpoint for the given notification id. The endpoint
|
|
908
|
+
* (`${base}/_notifications/:id/read`) is mounted by
|
|
909
|
+
* `Pilotiq.databaseNotifications()`, so calling this without
|
|
910
|
+
* opting into the bell surface produces an Action whose POST 404s.
|
|
911
|
+
*
|
|
912
|
+
* `notificationId` is baked at config time. For row context where
|
|
913
|
+
* the id varies per row, omit it and the URL keeps the `:id`
|
|
914
|
+
* template; the renderer substitutes per-row at render time
|
|
915
|
+
* (parallel to `Action.edit`'s row form).
|
|
916
|
+
*
|
|
917
|
+
* No auto-visibility. Wrap in `.visible(({ record }) => !record.readAt)`
|
|
918
|
+
* to hide on already-read rows.
|
|
919
|
+
*/
|
|
920
|
+
static markAsRead(basePath: string, notificationId?: string): Action {
|
|
921
|
+
const id = notificationId ?? ':id'
|
|
922
|
+
return Action.make('markAsRead')
|
|
923
|
+
.label('Mark as read')
|
|
924
|
+
.method('post')
|
|
925
|
+
.action(`${basePath}/_notifications/${id}/read`)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ─── Bulk factories (Plan #13) ────────────────────────────────
|
|
929
|
+
//
|
|
930
|
+
// Handler-style bulk actions that iterate `ctx.records`, run policy
|
|
931
|
+
// per-row, and call the matching Resource / Model method. No new
|
|
932
|
+
// routes — the existing `/_action/:actionName` dispatcher already
|
|
933
|
+
// handles bulk via `ctx.records`. Drop into `bulkActions([...])`
|
|
934
|
+
// from inside `Resource.table()`.
|
|
935
|
+
//
|
|
936
|
+
// Each returns a notification with the count succeeded; rows whose
|
|
937
|
+
// policy denied (or whose call threw) are silently skipped — surface
|
|
938
|
+
// them via your own logging if needed.
|
|
939
|
+
|
|
940
|
+
/** Bulk delete — calls `R.deleteRecord(id)` per row. On a
|
|
941
|
+
* soft-delete resource that hits `Model.delete()` which writes
|
|
942
|
+
* `deletedAt`. Notification: "N posts moved to trash" / "N posts
|
|
943
|
+
* deleted" depending on `R.softDeletes`. */
|
|
944
|
+
static bulkDelete(R: ResourceLike, _basePath: string): Action {
|
|
945
|
+
return Action.make('bulkDelete')
|
|
946
|
+
.label('Delete selected')
|
|
947
|
+
.destructive()
|
|
948
|
+
.bulk()
|
|
949
|
+
.confirm(`Delete the selected ${labelForCount(R, 0)}?`)
|
|
950
|
+
.handler(async (ctx) => {
|
|
951
|
+
const records = ctx.records ?? []
|
|
952
|
+
const Rfull = R as ResourceLike & { deleteRecord(id: string): Promise<void> }
|
|
953
|
+
let n = 0
|
|
954
|
+
for (const record of records) {
|
|
955
|
+
const id = String((record as { id?: unknown }).id ?? '')
|
|
956
|
+
if (!id) continue
|
|
957
|
+
const allowed = await callPredicate(R.canDelete, ctx.user, record)
|
|
958
|
+
if (!allowed) continue
|
|
959
|
+
try { await Rfull.deleteRecord(id); n++ } catch { /* skip — agg notify shows total */ }
|
|
960
|
+
}
|
|
961
|
+
const verb = R.softDeletes ? 'moved to trash' : 'deleted'
|
|
962
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} ${verb}`, type: 'success' } as never }
|
|
963
|
+
})
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** Bulk restore — calls `R.model.restore(id)` per row. Visible only
|
|
967
|
+
* on soft-delete resources (the entire bulk-restore concept is
|
|
968
|
+
* specific to them). */
|
|
969
|
+
static bulkRestore(R: ResourceLike, _basePath: string): Action {
|
|
970
|
+
return Action.make('bulkRestore')
|
|
971
|
+
.label('Restore selected')
|
|
972
|
+
.color('success')
|
|
973
|
+
.bulk()
|
|
974
|
+
.confirm(`Restore the selected ${labelForCount(R, 0)}?`)
|
|
975
|
+
.handler(async (ctx) => {
|
|
976
|
+
const records = ctx.records ?? []
|
|
977
|
+
const Rfull = R as ResourceLike & { model?: { restore?(id: string | number): Promise<unknown> } }
|
|
978
|
+
const restore = Rfull.model?.restore
|
|
979
|
+
if (!restore) {
|
|
980
|
+
return { notify: { title: 'Restore not configured', type: 'error' } as never }
|
|
981
|
+
}
|
|
982
|
+
let n = 0
|
|
983
|
+
for (const record of records) {
|
|
984
|
+
const id = String((record as { id?: unknown }).id ?? '')
|
|
985
|
+
if (!id) continue
|
|
986
|
+
const allowed = await callPredicate(R.canRestore, ctx.user, record)
|
|
987
|
+
if (!allowed) continue
|
|
988
|
+
try { await restore(id); n++ } catch { /* skip */ }
|
|
989
|
+
}
|
|
990
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} restored`, type: 'success' } as never }
|
|
991
|
+
})
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Bulk force-delete — calls `R.model.forceDelete(id)` per row. Same
|
|
995
|
+
* destructive confirm as the per-row variant. Visible only on
|
|
996
|
+
* soft-delete resources. */
|
|
997
|
+
static bulkForceDelete(R: ResourceLike, _basePath: string): Action {
|
|
998
|
+
return Action.make('bulkForceDelete')
|
|
999
|
+
.label('Delete forever')
|
|
1000
|
+
.destructive()
|
|
1001
|
+
.bulk()
|
|
1002
|
+
.confirm(`Permanently delete the selected ${labelForCount(R, 0)}? This cannot be undone.`)
|
|
1003
|
+
.handler(async (ctx) => {
|
|
1004
|
+
const records = ctx.records ?? []
|
|
1005
|
+
const Rfull = R as ResourceLike & { model?: { forceDelete?(id: string | number): Promise<void> } }
|
|
1006
|
+
const forceDelete = Rfull.model?.forceDelete
|
|
1007
|
+
if (!forceDelete) {
|
|
1008
|
+
return { notify: { title: 'Force-delete not configured', type: 'error' } as never }
|
|
1009
|
+
}
|
|
1010
|
+
let n = 0
|
|
1011
|
+
for (const record of records) {
|
|
1012
|
+
const id = String((record as { id?: unknown }).id ?? '')
|
|
1013
|
+
if (!id) continue
|
|
1014
|
+
const allowed = await callPredicate(R.canForceDelete, ctx.user, record)
|
|
1015
|
+
if (!allowed) continue
|
|
1016
|
+
try { await forceDelete(id); n++ } catch { /* skip */ }
|
|
1017
|
+
}
|
|
1018
|
+
return { notify: { title: `${n} ${labelForCount(R, n)} permanently deleted`, type: 'success' } as never }
|
|
1019
|
+
})
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Bulk replicate — calls `R.model.create(...)` once per selected row
|
|
1024
|
+
* with the source row's attributes minus PK / soft-delete column /
|
|
1025
|
+
* `opts.excludeAttributes`. Optional `opts.beforeReplicaSaved(replica,
|
|
1026
|
+
* source)` runs per-row. Rows that throw during create are skipped
|
|
1027
|
+
* silently so a single bad row doesn't abort the batch (the user sees
|
|
1028
|
+
* the success count on the toast). Visibility delegates to
|
|
1029
|
+
* `R.canCreate(user)`.
|
|
1030
|
+
*
|
|
1031
|
+
* Sibling of `Action.replicate` — same options bag, same strip set,
|
|
1032
|
+
* same authorization gate. Stays on the list page (no per-row
|
|
1033
|
+
* redirect possible for N rows).
|
|
1034
|
+
*/
|
|
1035
|
+
static bulkReplicate(
|
|
1036
|
+
R: ResourceLike,
|
|
1037
|
+
_basePath: string,
|
|
1038
|
+
opts: ReplicateOptions = {},
|
|
1039
|
+
): Action {
|
|
1040
|
+
return Action.make('bulkReplicate')
|
|
1041
|
+
.label('Replicate selected')
|
|
1042
|
+
.bulk()
|
|
1043
|
+
.confirm(`Replicate the selected ${labelForCount(R, 0)}?`)
|
|
1044
|
+
.handler(async (ctx) => {
|
|
1045
|
+
const M = R.model
|
|
1046
|
+
if (!M || typeof M.create !== 'function') {
|
|
1047
|
+
return { notify: { title: 'Replicate not configured (resource has no model.create)', type: 'error' } as never }
|
|
1048
|
+
}
|
|
1049
|
+
const records = ctx.records ?? []
|
|
1050
|
+
const pkCol = (M as { primaryKey?: string }).primaryKey ?? 'id'
|
|
1051
|
+
const trashedCol = R.deletedAtColumn ?? 'deletedAt'
|
|
1052
|
+
const skip = new Set<string>([pkCol, trashedCol, ...(opts.excludeAttributes ?? [])])
|
|
1053
|
+
let n = 0
|
|
1054
|
+
for (const source of records) {
|
|
1055
|
+
if (!source || typeof source !== 'object') continue
|
|
1056
|
+
const allowed = await callPredicate(R.canCreate, ctx.user)
|
|
1057
|
+
if (!allowed) continue
|
|
1058
|
+
let replica: Record<string, unknown> = {}
|
|
1059
|
+
for (const [k, v] of Object.entries(source as Record<string, unknown>)) {
|
|
1060
|
+
if (skip.has(k)) continue
|
|
1061
|
+
replica[k] = v
|
|
1062
|
+
}
|
|
1063
|
+
if (opts.beforeReplicaSaved) {
|
|
1064
|
+
try { replica = await opts.beforeReplicaSaved(replica, source) }
|
|
1065
|
+
catch { continue }
|
|
1066
|
+
}
|
|
1067
|
+
try { await M.create(replica); n++ } catch { /* skip — agg notify shows total */ }
|
|
1068
|
+
}
|
|
1069
|
+
const defaultTitle = `${n} ${labelForCount(R, n)} replicated`
|
|
1070
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
1071
|
+
? await opts.getCreatedNotificationTitle({ count: n, records })
|
|
1072
|
+
: undefined
|
|
1073
|
+
const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
|
|
1074
|
+
return { notify: { title, type: 'success' } as never }
|
|
1075
|
+
})
|
|
1076
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ─── Import / Export factories ────────────────────────────────
|
|
1080
|
+
//
|
|
1081
|
+
// Pre-built CSV / JSON in/out for any Resource that has `R.model`.
|
|
1082
|
+
// `Action.export` walks the configured `R.table()` records handler
|
|
1083
|
+
// in pages (so it picks up the active filter/search/sort by default,
|
|
1084
|
+
// and stays consistent with what the user is looking at). The bulk
|
|
1085
|
+
// variant exports `ctx.records` instead. `Action.import` opens a
|
|
1086
|
+
// form-modal with a `FileUpload`, parses the uploaded file, and runs
|
|
1087
|
+
// create / upsert through `R.model`.
|
|
1088
|
+
//
|
|
1089
|
+
// Internals live in `./exportFactory.ts` and `./importFactory.ts`
|
|
1090
|
+
// — kept out of this file because they need to import `Table` /
|
|
1091
|
+
// field types that themselves import `Action`. Lazy-import inside
|
|
1092
|
+
// the handler avoids the cycle (handlers run at request-time, well
|
|
1093
|
+
// after module load).
|
|
1094
|
+
|
|
1095
|
+
/** Header-placement export — downloads the table as CSV (default) or
|
|
1096
|
+
* JSON. Visibility defaults to `R.canViewAny(user)`. Drop into
|
|
1097
|
+
* `headerActions([...])` from inside `Resource.table()`. See
|
|
1098
|
+
* `docs/plans/import-export-actions.md` for the full options bag. */
|
|
1099
|
+
static export(
|
|
1100
|
+
R: ResourceLike,
|
|
1101
|
+
_basePath: string,
|
|
1102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1103
|
+
opts: import('./exportFactory.js').ExportOptions = {},
|
|
1104
|
+
): Action {
|
|
1105
|
+
return Action.make('export')
|
|
1106
|
+
.label('Export')
|
|
1107
|
+
.handler(async (ctx) => {
|
|
1108
|
+
const ef = await import('./exportFactory.js')
|
|
1109
|
+
const tableInst = R.table?.(await loadTableClass())
|
|
1110
|
+
if (!tableInst) {
|
|
1111
|
+
return { notify: { title: 'Export not configured (resource has no table())', type: 'error' } as never }
|
|
1112
|
+
}
|
|
1113
|
+
const cols = ef.resolveExportColumns(opts.columns, R)
|
|
1114
|
+
if (cols.length === 0) {
|
|
1115
|
+
return { notify: { title: 'Export has no columns', type: 'error' } as never }
|
|
1116
|
+
}
|
|
1117
|
+
const maxRows = opts.maxRows ?? 50_000
|
|
1118
|
+
const records = await ef.collectExportRows(tableInst, ctx, opts.scope ?? 'filtered', maxRows, opts.chunkSize)
|
|
1119
|
+
if (records.length > maxRows) {
|
|
1120
|
+
return { notify: { title: `Export exceeded ${maxRows} rows`, type: 'error' } as never }
|
|
1121
|
+
}
|
|
1122
|
+
const rows = records.map(r => ef.buildExportRow(r, cols))
|
|
1123
|
+
const format = opts.format ?? 'csv'
|
|
1124
|
+
const { body, contentType } = ef.encodeExport(rows, cols, format)
|
|
1125
|
+
const filename = typeof opts.filename === 'function'
|
|
1126
|
+
? opts.filename(ctx)
|
|
1127
|
+
: (opts.filename ?? ef.defaultExportFilename(R.getSlug(), format))
|
|
1128
|
+
return { download: { filename, contentType, body } }
|
|
1129
|
+
})
|
|
1130
|
+
.visible(({ user }) => callPredicate(R.canViewAny, user))
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/** Bulk-placement export — downloads the rows the user selected via
|
|
1134
|
+
* the bulk-select checkboxes. Same options as `Action.export` minus
|
|
1135
|
+
* `scope` (always operates on `ctx.records`). Drop into
|
|
1136
|
+
* `bulkActions([...])`. */
|
|
1137
|
+
static bulkExport(
|
|
1138
|
+
R: ResourceLike,
|
|
1139
|
+
_basePath: string,
|
|
1140
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1141
|
+
opts: Omit<import('./exportFactory.js').ExportOptions, 'scope'> = {},
|
|
1142
|
+
): Action {
|
|
1143
|
+
return Action.make('bulkExport')
|
|
1144
|
+
.label('Export selected')
|
|
1145
|
+
.bulk()
|
|
1146
|
+
.handler(async (ctx) => {
|
|
1147
|
+
const ef = await import('./exportFactory.js')
|
|
1148
|
+
const cols = ef.resolveExportColumns(opts.columns, R)
|
|
1149
|
+
if (cols.length === 0) {
|
|
1150
|
+
return { notify: { title: 'Export has no columns', type: 'error' } as never }
|
|
1151
|
+
}
|
|
1152
|
+
const records = ctx.records ?? []
|
|
1153
|
+
const maxRows = opts.maxRows ?? 50_000
|
|
1154
|
+
if (records.length > maxRows) {
|
|
1155
|
+
return { notify: { title: `Export exceeded ${maxRows} rows`, type: 'error' } as never }
|
|
1156
|
+
}
|
|
1157
|
+
const rows = records.map(r => ef.buildExportRow(r, cols))
|
|
1158
|
+
const format = opts.format ?? 'csv'
|
|
1159
|
+
const { body, contentType } = ef.encodeExport(rows, cols, format)
|
|
1160
|
+
const filename = typeof opts.filename === 'function'
|
|
1161
|
+
? opts.filename(ctx)
|
|
1162
|
+
: (opts.filename ?? ef.defaultExportFilename(R.getSlug(), format))
|
|
1163
|
+
return { download: { filename, contentType, body } }
|
|
1164
|
+
})
|
|
1165
|
+
.visible(({ user }) => callPredicate(R.canViewAny, user))
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/** Header-placement import — opens a modal with a `FileUpload` (and a
|
|
1169
|
+
* Mode select when `upsertBy` is set), parses the uploaded file, and
|
|
1170
|
+
* walks each row through `R.model.create` / `R.model.update`.
|
|
1171
|
+
* Visibility defaults to `R.canCreate(user)`. Drop into
|
|
1172
|
+
* `headerActions([...])` from inside `Resource.table()`. See
|
|
1173
|
+
* `docs/plans/import-export-actions.md` for the full options bag. */
|
|
1174
|
+
static import(
|
|
1175
|
+
R: ResourceLike,
|
|
1176
|
+
_basePath: string,
|
|
1177
|
+
opts: import('./importFactory.js').ImportOptions = {},
|
|
1178
|
+
): Action {
|
|
1179
|
+
const upsertable = typeof opts.upsertBy === 'string' && opts.upsertBy.length > 0
|
|
1180
|
+
const a = Action.make('import')
|
|
1181
|
+
.label('Import')
|
|
1182
|
+
.modalHeading(`Import ${R.label ?? `${R.labelSingular}s`}`)
|
|
1183
|
+
.modalSubmitLabel('Import')
|
|
1184
|
+
.modalCancelLabel('Cancel')
|
|
1185
|
+
.handler(async (ctx) => {
|
|
1186
|
+
const M = R.model
|
|
1187
|
+
if (!M || typeof M.create !== 'function') {
|
|
1188
|
+
return { notify: { title: 'Import not configured (resource has no model.create)', type: 'error' } as never }
|
|
1189
|
+
}
|
|
1190
|
+
if (upsertable && typeof M.update !== 'function') {
|
|
1191
|
+
return { notify: { title: 'Upsert import requires model.update', type: 'error' } as never }
|
|
1192
|
+
}
|
|
1193
|
+
const fileUrl = String((ctx.values?.['file'] as unknown) ?? '')
|
|
1194
|
+
if (fileUrl.length === 0) {
|
|
1195
|
+
return { notify: { title: 'No file uploaded', type: 'error' } as never }
|
|
1196
|
+
}
|
|
1197
|
+
const ifac = await import('./importFactory.js')
|
|
1198
|
+
let text: string
|
|
1199
|
+
try {
|
|
1200
|
+
const r = await fetch(fileUrl)
|
|
1201
|
+
if (!r.ok) {
|
|
1202
|
+
return { notify: { title: `Failed to fetch upload (${r.status})`, type: 'error' } as never }
|
|
1203
|
+
}
|
|
1204
|
+
text = await r.text()
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
return { notify: { title: `Failed to read upload: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
1207
|
+
}
|
|
1208
|
+
const format = opts.format ?? (fileUrl.toLowerCase().endsWith('.json') ? 'json' : 'csv')
|
|
1209
|
+
let rows: Array<Record<string, unknown>>
|
|
1210
|
+
try {
|
|
1211
|
+
rows = ifac.parseImportText(text, format, opts.columns)
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
return { notify: { title: `Failed to parse ${format.toUpperCase()}: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
1214
|
+
}
|
|
1215
|
+
const maxRows = opts.maxRows ?? 10_000
|
|
1216
|
+
if (rows.length > maxRows) {
|
|
1217
|
+
return { notify: { title: `Import too large (${rows.length} > ${maxRows})`, type: 'error' } as never }
|
|
1218
|
+
}
|
|
1219
|
+
const mode = upsertable && (ctx.values?.['mode'] === 'upsert') ? 'upsert' : 'create'
|
|
1220
|
+
const summary = await ifac.runImport(rows, M, mode, opts, ctx)
|
|
1221
|
+
return { notify: ifac.buildImportNotification(summary, { upsert: upsertable }) as never }
|
|
1222
|
+
})
|
|
1223
|
+
.visible(({ user }) => callPredicate(R.canCreate, user))
|
|
1224
|
+
|
|
1225
|
+
// Build the modal-form schema synchronously. importFactory has no
|
|
1226
|
+
// runtime import edge back to Action.ts (only a type-import, erased
|
|
1227
|
+
// at compile time), so the top-of-file static import below is
|
|
1228
|
+
// cycle-free.
|
|
1229
|
+
a.schema(buildImportModalSchema(opts))
|
|
1230
|
+
return a
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ─── Relation-manager factories (Plan #11 polish) ─────────────
|
|
1234
|
+
//
|
|
1235
|
+
// Mirror `Action.create / edit / delete` but build URLs under the
|
|
1236
|
+
// parent record: `${base}/${parentSlug}/${parentId}/${rel}/...`.
|
|
1237
|
+
// Designed to be called inside `RelationManager.static table()` —
|
|
1238
|
+
// the page-data builder pipes `RelationManagerContext` into that
|
|
1239
|
+
// configurator so users get `basePath`, `parentId`, and the
|
|
1240
|
+
// discovered Related resource without threading them by hand.
|
|
1241
|
+
//
|
|
1242
|
+
// Visibility predicates use `safeManagerPolicy` so the manager's
|
|
1243
|
+
// `canX` (when overridden) wins, otherwise falls through to the
|
|
1244
|
+
// related Resource's `canX`. Throws absorb as `false`.
|
|
1245
|
+
//
|
|
1246
|
+
// `:id` template substitution still happens at render time for row
|
|
1247
|
+
// context — the same mechanism that drives `Action.edit / delete`.
|
|
1248
|
+
// The parent's id is baked into the URL at config time (it's known
|
|
1249
|
+
// upfront from `ctx.parentId`), so `:id` unambiguously refers to
|
|
1250
|
+
// the row's *child* id.
|
|
1251
|
+
|
|
1252
|
+
/** Relation create-action factory — link to
|
|
1253
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/create`.
|
|
1254
|
+
*
|
|
1255
|
+
* Visibility delegates to `M.canCreate(user, parentRecord)` (or the
|
|
1256
|
+
* related Resource's `canCreate(user)` when the manager hasn't
|
|
1257
|
+
* overridden). Drop into `headerActions([...])` from inside
|
|
1258
|
+
* `RelationManager.table(table, ctx)`.
|
|
1259
|
+
*/
|
|
1260
|
+
static relationCreate(
|
|
1261
|
+
M: typeof RelationManager,
|
|
1262
|
+
ctx: RelationManagerContext,
|
|
1263
|
+
): Action {
|
|
1264
|
+
const labelSingular = M.getLabelSingular()
|
|
1265
|
+
return Action.make('create')
|
|
1266
|
+
.label(`New ${labelSingular}`)
|
|
1267
|
+
.href(`${relationUrlPrefix(ctx)}/create`)
|
|
1268
|
+
.visible(({ user }) => {
|
|
1269
|
+
// M2M managers don't have a per-pivot-row create surface — the
|
|
1270
|
+
// related record is created via its own Resource, then attached
|
|
1271
|
+
// via `relationAttach`. Auto-hide so dropping this factory into
|
|
1272
|
+
// any M2M manager (belongsToMany / morphToMany / morphedByMany)
|
|
1273
|
+
// is a no-op (visible=false) instead of a 404-on-click foot-gun.
|
|
1274
|
+
if (isM2MMode(ctx.mode)) return false
|
|
1275
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
1276
|
+
})
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
/** Relation edit-action factory — link to
|
|
1280
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/edit`.
|
|
1281
|
+
*
|
|
1282
|
+
* Same `recordId` semantics as `Action.edit`: omit for row context
|
|
1283
|
+
* so the renderer substitutes `:id` per row; pass explicitly when
|
|
1284
|
+
* building actions for a single-record context. Visibility delegates
|
|
1285
|
+
* to `M.canEdit(user, child, parentRecord)` with fall-through to the
|
|
1286
|
+
* related Resource's `canEdit(user, record)`.
|
|
1287
|
+
*/
|
|
1288
|
+
static relationEdit(
|
|
1289
|
+
M: typeof RelationManager,
|
|
1290
|
+
ctx: RelationManagerContext,
|
|
1291
|
+
recordId?: string,
|
|
1292
|
+
): Action {
|
|
1293
|
+
const id = recordId ?? ':id'
|
|
1294
|
+
return Action.make('edit')
|
|
1295
|
+
.label('Edit')
|
|
1296
|
+
.href(`${relationUrlPrefix(ctx)}/${id}/edit`)
|
|
1297
|
+
.visible(({ user, record }) => {
|
|
1298
|
+
// M2M: per-pivot-row "edit" doesn't exist; users edit the
|
|
1299
|
+
// related record via its own Resource. Auto-hide for every M2M
|
|
1300
|
+
// mode (belongsToMany / morphToMany / morphedByMany).
|
|
1301
|
+
if (isM2MMode(ctx.mode)) return false
|
|
1302
|
+
return safeManagerPolicy(M, 'canEdit', ctx.related, user, ctx.parentRecord, record)
|
|
1303
|
+
})
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/** Relation delete-action factory — POST to
|
|
1307
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/delete`,
|
|
1308
|
+
* destructive style with a labeled confirmation. Visibility delegates
|
|
1309
|
+
* to `M.canDelete(user, child, parentRecord)` with fall-through to the
|
|
1310
|
+
* related Resource's `canDelete(user, record)`.
|
|
1311
|
+
*/
|
|
1312
|
+
static relationDelete(
|
|
1313
|
+
M: typeof RelationManager,
|
|
1314
|
+
ctx: RelationManagerContext,
|
|
1315
|
+
recordId?: string,
|
|
1316
|
+
): Action {
|
|
1317
|
+
const id = recordId ?? ':id'
|
|
1318
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
1319
|
+
return Action.make('delete')
|
|
1320
|
+
.label('Delete')
|
|
1321
|
+
.destructive()
|
|
1322
|
+
.method('post')
|
|
1323
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/delete`)
|
|
1324
|
+
.confirm(`Delete this ${singular}?`)
|
|
1325
|
+
.visible(async ({ user, record }) => {
|
|
1326
|
+
// M2M: "delete" of the related record is destructive in a way
|
|
1327
|
+
// that "detach" isn't — surface only `relationDetach` on every
|
|
1328
|
+
// M2M manager (belongsToMany / morphToMany / morphedByMany).
|
|
1329
|
+
// Users who genuinely want to delete the related record reach
|
|
1330
|
+
// for `Action.delete(R)` on the related Resource instead.
|
|
1331
|
+
if (isM2MMode(ctx.mode)) return false
|
|
1332
|
+
if (ctx.related?.softDeletes && isTrashed(record, ctx.related as ResourceLike)) return false
|
|
1333
|
+
return safeManagerPolicy(M, 'canDelete', ctx.related, user, ctx.parentRecord, record)
|
|
1334
|
+
})
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Plan #13 polish — Restore factory for relation managers. POSTs to
|
|
1339
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/restore`,
|
|
1340
|
+
* success-styled, no confirm prompt. Auto-hides on live (non-trashed)
|
|
1341
|
+
* rows AND when `M.canRestore` (or related Resource fall-through)
|
|
1342
|
+
* denies. Drop into `recordActions([...])` from `RelationManager.table(table, ctx)`.
|
|
1343
|
+
*/
|
|
1344
|
+
static relationRestore(
|
|
1345
|
+
M: typeof RelationManager,
|
|
1346
|
+
ctx: RelationManagerContext,
|
|
1347
|
+
recordId?: string,
|
|
1348
|
+
): Action {
|
|
1349
|
+
const id = recordId ?? ':id'
|
|
1350
|
+
return Action.make('restore')
|
|
1351
|
+
.label('Restore')
|
|
1352
|
+
.color('success')
|
|
1353
|
+
.method('post')
|
|
1354
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/restore`)
|
|
1355
|
+
.visible(async ({ user, record }) => {
|
|
1356
|
+
if (!ctx.related?.softDeletes) return false
|
|
1357
|
+
if (!isTrashed(record, ctx.related as ResourceLike)) return false
|
|
1358
|
+
return safeManagerPolicy(M, 'canRestore', ctx.related, user, ctx.parentRecord, record)
|
|
1359
|
+
})
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Plan #13 polish — Force-delete factory for relation managers. POSTs
|
|
1364
|
+
* to `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/force-delete`,
|
|
1365
|
+
* destructive style with a permanence-aware confirmation. Auto-hides on
|
|
1366
|
+
* live (non-trashed) rows and when policy denies.
|
|
1367
|
+
*/
|
|
1368
|
+
static relationForceDelete(
|
|
1369
|
+
M: typeof RelationManager,
|
|
1370
|
+
ctx: RelationManagerContext,
|
|
1371
|
+
recordId?: string,
|
|
1372
|
+
): Action {
|
|
1373
|
+
const id = recordId ?? ':id'
|
|
1374
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
1375
|
+
return Action.make('forceDelete')
|
|
1376
|
+
.label('Delete forever')
|
|
1377
|
+
.destructive()
|
|
1378
|
+
.method('post')
|
|
1379
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/force-delete`)
|
|
1380
|
+
.confirm(`Permanently delete this ${singular}? This cannot be undone.`)
|
|
1381
|
+
.visible(async ({ user, record }) => {
|
|
1382
|
+
if (!ctx.related?.softDeletes) return false
|
|
1383
|
+
if (!isTrashed(record, ctx.related as ResourceLike)) return false
|
|
1384
|
+
return safeManagerPolicy(M, 'canForceDelete', ctx.related, user, ctx.parentRecord, record)
|
|
1385
|
+
})
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ─── Relation-manager replicate factories ─────────────────
|
|
1389
|
+
//
|
|
1390
|
+
// Sibling of `Action.replicate / bulkReplicate` scoped to a
|
|
1391
|
+
// RelationManager. Operates on the **related** Resource's model and
|
|
1392
|
+
// **forces** the parent attachment back onto the replica:
|
|
1393
|
+
//
|
|
1394
|
+
// - `hasMany` — re-stamps `<foreignKey> = ctx.parentId` from the
|
|
1395
|
+
// parent's `static relations[name]` descriptor. Defends against a
|
|
1396
|
+
// tampered POST body (or a source row loaded from a different
|
|
1397
|
+
// parent's children) silently re-attaching the new row to a
|
|
1398
|
+
// different parent.
|
|
1399
|
+
// - `morphMany` — re-stamps `<morphName>Id` + `<morphName>Type` via
|
|
1400
|
+
// `computeMorphPayload(parentRecord, descriptor)`, same auto-fill
|
|
1401
|
+
// the relation-create POST handler uses on the create form.
|
|
1402
|
+
// - M2M (`belongsToMany / morphToMany / morphedByMany`) — auto-hides.
|
|
1403
|
+
// Replicate doesn't fit pivot semantics; users create the related
|
|
1404
|
+
// record via its own Resource, then attach via `relationAttach`.
|
|
1405
|
+
// - `morphTo` — auto-hides. Child-side polymorphic relations don't
|
|
1406
|
+
// have a single owner to pin to (the row's existing morph cols
|
|
1407
|
+
// already point somewhere; cloning is the user's job, not ours).
|
|
1408
|
+
//
|
|
1409
|
+
// Both factories dispatch through the manager-scoped
|
|
1410
|
+
// `_action/:actionName` route already wired in `routes.ts`. The route
|
|
1411
|
+
// resolves `ctx.record` (and `ctx.records` for bulk) via
|
|
1412
|
+
// `Related.model.find(id)`, and stamps `ctx.relation = { parent,
|
|
1413
|
+
// parentId, relationship }` so the handlers can read the live parent
|
|
1414
|
+
// without re-loading.
|
|
1415
|
+
//
|
|
1416
|
+
// Visibility delegates to `safeManagerPolicy(M, 'canCreate', Related,
|
|
1417
|
+
// user, parentRecord)` — the manager's `canCreate` (when overridden)
|
|
1418
|
+
// wins, otherwise falls through to the related Resource's `canCreate`.
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Relation row-replicate factory. Clones the row's child record
|
|
1422
|
+
* inside the manager's parent scope.
|
|
1423
|
+
*
|
|
1424
|
+
* Strips the related model's primary key, soft-delete column, and
|
|
1425
|
+
* `opts.excludeAttributes`. Re-applies the parent attachment columns
|
|
1426
|
+
* after the strip + before the optional `beforeReplicaSaved` hook,
|
|
1427
|
+
* so user code can still mutate non-FK fields without accidentally
|
|
1428
|
+
* unlinking the replica.
|
|
1429
|
+
*
|
|
1430
|
+
* On success the manager-scoped route falls back to the manager
|
|
1431
|
+
* list URL (`${base}/${parentSlug}/${parentId}/${relationship}`)
|
|
1432
|
+
* because no explicit `redirect` is returned — same default as the
|
|
1433
|
+
* other handler-style relation factories.
|
|
1434
|
+
*
|
|
1435
|
+
* `recordId` kept in the signature for parity with the rest of the
|
|
1436
|
+
* relation factory family. The dispatcher resolves the source row
|
|
1437
|
+
* from the request body, so it isn't referenced here.
|
|
1438
|
+
*/
|
|
1439
|
+
static relationReplicate(
|
|
1440
|
+
M: typeof RelationManager,
|
|
1441
|
+
ctx: RelationManagerContext,
|
|
1442
|
+
recordId?: string,
|
|
1443
|
+
opts: ReplicateOptions = {},
|
|
1444
|
+
): Action {
|
|
1445
|
+
void recordId
|
|
1446
|
+
return Action.make('relationReplicate')
|
|
1447
|
+
.label('Replicate')
|
|
1448
|
+
.row()
|
|
1449
|
+
.handler(async (hctx) => {
|
|
1450
|
+
const result = await runRelationReplicateRow(M, ctx, hctx, opts)
|
|
1451
|
+
return result
|
|
1452
|
+
})
|
|
1453
|
+
.visible(({ user }) => {
|
|
1454
|
+
if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
|
|
1455
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
1456
|
+
})
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Bulk sibling — replicates every selected child row inside the
|
|
1461
|
+
* manager's parent scope. Same strip + force-pin pipeline applied
|
|
1462
|
+
* per row. Per-row `safeManagerPolicy(M, 'canCreate', …)` runs
|
|
1463
|
+
* inside the loop so a partially-permitted selection still proceeds
|
|
1464
|
+
* for the rows that pass. Rows that throw are skipped silently —
|
|
1465
|
+
* the toast count reflects only successful creates.
|
|
1466
|
+
*/
|
|
1467
|
+
static relationBulkReplicate(
|
|
1468
|
+
M: typeof RelationManager,
|
|
1469
|
+
ctx: RelationManagerContext,
|
|
1470
|
+
opts: ReplicateOptions = {},
|
|
1471
|
+
): Action {
|
|
1472
|
+
return Action.make('relationBulkReplicate')
|
|
1473
|
+
.label('Replicate selected')
|
|
1474
|
+
.bulk()
|
|
1475
|
+
.confirm(`Replicate the selected ${M.getLabel().toLowerCase()}?`)
|
|
1476
|
+
.handler(async (hctx) => {
|
|
1477
|
+
const Related = ctx.related
|
|
1478
|
+
if (!Related?.model || typeof Related.model.create !== 'function') {
|
|
1479
|
+
return { notify: { title: 'Replicate not configured (related Resource has no model.create)', type: 'error' } as never }
|
|
1480
|
+
}
|
|
1481
|
+
const records = hctx.records ?? []
|
|
1482
|
+
let n = 0
|
|
1483
|
+
for (const source of records) {
|
|
1484
|
+
if (!source || typeof source !== 'object') continue
|
|
1485
|
+
const allowed = await safeManagerPolicy(M, 'canCreate', Related, hctx.user, ctx.parentRecord)
|
|
1486
|
+
if (!allowed) continue
|
|
1487
|
+
try {
|
|
1488
|
+
await persistRelationReplica(M, ctx, source, opts)
|
|
1489
|
+
n++
|
|
1490
|
+
} catch { /* skip — agg notify shows total */ }
|
|
1491
|
+
}
|
|
1492
|
+
const labelPlural = M.getLabel().toLowerCase()
|
|
1493
|
+
const labelSingular = M.getLabelSingular().toLowerCase()
|
|
1494
|
+
const defaultTitle = `${n} ${n === 1 ? labelSingular : labelPlural} replicated`
|
|
1495
|
+
const overrideTitle = opts.getCreatedNotificationTitle
|
|
1496
|
+
? await opts.getCreatedNotificationTitle({ count: n, records })
|
|
1497
|
+
: undefined
|
|
1498
|
+
const title = overrideTitle !== undefined ? overrideTitle : defaultTitle
|
|
1499
|
+
return {
|
|
1500
|
+
notify: { title, type: 'success' } as never,
|
|
1501
|
+
}
|
|
1502
|
+
})
|
|
1503
|
+
.visible(({ user }) => {
|
|
1504
|
+
if (isM2MMode(ctx.mode) || ctx.mode === 'morphTo') return false
|
|
1505
|
+
return safeManagerPolicy(M, 'canCreate', ctx.related, user, ctx.parentRecord)
|
|
1506
|
+
})
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// ─── M2M relation factories ───────────────────────────────
|
|
1510
|
+
//
|
|
1511
|
+
// Sibling of `relationCreate / Edit / Delete` for every M2M mode
|
|
1512
|
+
// (`belongsToMany`, `morphToMany` (owning polymorphic side),
|
|
1513
|
+
// `morphedByMany` (inverse polymorphic side)). All three modes share
|
|
1514
|
+
// the same `attach` / `detach` / `sync` accessor surface — the rudder
|
|
1515
|
+
// ORM stamps + filters the polymorphic discriminator on the morph
|
|
1516
|
+
// variants automatically, so pilotiq's pivot factories are mode-agnostic
|
|
1517
|
+
// beyond the visibility gate.
|
|
1518
|
+
//
|
|
1519
|
+
// Three factories: `relationAttach` (header, modal-form picker →
|
|
1520
|
+
// POST `_action/relationAttach`), `relationDetach` (row, direct POST
|
|
1521
|
+
// to `_detach/:childId`), `relationBulkDetach` (bulk, handler-
|
|
1522
|
+
// dispatched). The first and third route through the manager-scoped
|
|
1523
|
+
// `_action/:actionName` endpoint (added in routes.ts) so handlers
|
|
1524
|
+
// see `ctx.relation = { parent, parentId, relationship }`.
|
|
1525
|
+
//
|
|
1526
|
+
// All three auto-hide outside any M2M mode so dropping a factory into
|
|
1527
|
+
// a non-M2M manager is a no-op (visible=false) instead of a confusing
|
|
1528
|
+
// 404.
|
|
1529
|
+
|
|
1530
|
+
/** Header-placement attach factory — opens a modal with a SelectField
|
|
1531
|
+
* listing related records that aren't already attached, and POSTs the
|
|
1532
|
+
* selected id to the manager's `_action/relationAttach` endpoint.
|
|
1533
|
+
*
|
|
1534
|
+
* Visibility delegates to `M.canAttach(user, parentRecord)` AND
|
|
1535
|
+
* guards against being dropped into a non-M2M manager. */
|
|
1536
|
+
static relationAttach(
|
|
1537
|
+
M: typeof RelationManager,
|
|
1538
|
+
ctx: RelationManagerContext,
|
|
1539
|
+
): Action {
|
|
1540
|
+
const labelSingular = M.getLabelSingular()
|
|
1541
|
+
const a = Action.make('relationAttach')
|
|
1542
|
+
.label(`Attach ${labelSingular}`)
|
|
1543
|
+
.header()
|
|
1544
|
+
.modalHeading(`Attach ${labelSingular}`)
|
|
1545
|
+
.modalSubmitLabel('Attach')
|
|
1546
|
+
.modalCancelLabel('Cancel')
|
|
1547
|
+
.handler(async (hctx) => {
|
|
1548
|
+
const rel = hctx.relation
|
|
1549
|
+
if (!rel) {
|
|
1550
|
+
return { notify: { title: 'Attach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
|
|
1551
|
+
}
|
|
1552
|
+
const Related = ctx.related
|
|
1553
|
+
if (!Related?.model) {
|
|
1554
|
+
return { notify: { title: 'Cannot attach: related Resource has no model', type: 'error' } as never }
|
|
1555
|
+
}
|
|
1556
|
+
const idStr = String((hctx.values?.['_attachId'] as unknown) ?? '')
|
|
1557
|
+
if (idStr.length === 0) {
|
|
1558
|
+
return { notify: { title: 'Pick a record to attach', type: 'error' } as never }
|
|
1559
|
+
}
|
|
1560
|
+
const accessor = resolveM2MAccessor(rel.parent, rel.relationship)
|
|
1561
|
+
if (!accessor || typeof accessor.attach !== 'function') {
|
|
1562
|
+
return { notify: { title: `Pivot accessor missing on ${rel.relationship} — wrong relation type or ORM version?`, type: 'error' } as never }
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
await accessor.attach([idStr])
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
return { notify: { title: `Attach failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
1568
|
+
}
|
|
1569
|
+
return { notify: { title: `${labelSingular} attached`, type: 'success' } as never }
|
|
1570
|
+
})
|
|
1571
|
+
.visible(({ user }) => {
|
|
1572
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
1573
|
+
return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
// Build the modal-form schema only when this is actually an M2M
|
|
1577
|
+
// manager — non-M2M drops keep the action hidden via the visibility
|
|
1578
|
+
// predicate, but still need a schema-less Action so the meta walker
|
|
1579
|
+
// doesn't blow up. Static import is fine: `attachFactory` only
|
|
1580
|
+
// depends on `SelectField` + ORM helpers, no cycle back to Action.
|
|
1581
|
+
if (isM2MMode(ctx.mode) && ctx.related?.model) {
|
|
1582
|
+
a.schema(buildAttachModalSchema({
|
|
1583
|
+
Related: ctx.related,
|
|
1584
|
+
relationship: ctx.relationship,
|
|
1585
|
+
recordTitleAttr: M.getRecordTitleAttribute() ?? ctx.related.recordTitleAttribute,
|
|
1586
|
+
labelSingular,
|
|
1587
|
+
}))
|
|
1588
|
+
}
|
|
1589
|
+
return a
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/** Row-placement detach factory — POSTs to
|
|
1593
|
+
* `${base}/${parentSlug}/${parentId}/${relationship}/${recordId ?? ':id'}/_detach`,
|
|
1594
|
+
* destructive style with a confirmation prompt that says "Detach"
|
|
1595
|
+
* (not "Delete") so users understand the target record stays.
|
|
1596
|
+
* Visibility delegates to `M.canDetach`. */
|
|
1597
|
+
static relationDetach(
|
|
1598
|
+
M: typeof RelationManager,
|
|
1599
|
+
ctx: RelationManagerContext,
|
|
1600
|
+
recordId?: string,
|
|
1601
|
+
): Action {
|
|
1602
|
+
const id = recordId ?? ':id'
|
|
1603
|
+
const singular = M.getLabelSingular().toLowerCase()
|
|
1604
|
+
return Action.make('relationDetach')
|
|
1605
|
+
.label('Detach')
|
|
1606
|
+
.destructive()
|
|
1607
|
+
.method('post')
|
|
1608
|
+
.action(`${relationUrlPrefix(ctx)}/${id}/_detach`)
|
|
1609
|
+
.confirm(`Detach this ${singular}? The ${singular} record stays in place; only the link is removed.`)
|
|
1610
|
+
.visible(async ({ user, record }) => {
|
|
1611
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
1612
|
+
return safeManagerPolicy(M, 'canDetach', ctx.related, user, ctx.parentRecord, record)
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/** Bulk-placement bulk-detach factory — handler-dispatched. Calls
|
|
1617
|
+
* `parent.related(rel).detach(ids)` for the selected rows. Visibility
|
|
1618
|
+
* delegates to `M.canAttach` (acts like a "manager admin" gate; we
|
|
1619
|
+
* intentionally don't enforce per-row `canDetach` on the visibility
|
|
1620
|
+
* side because the bulk button needs to be visible before the user
|
|
1621
|
+
* has selected anything — per-row gating happens inside the handler). */
|
|
1622
|
+
static relationBulkDetach(
|
|
1623
|
+
M: typeof RelationManager,
|
|
1624
|
+
ctx: RelationManagerContext,
|
|
1625
|
+
): Action {
|
|
1626
|
+
const labelPlural = M.getLabel().toLowerCase()
|
|
1627
|
+
return Action.make('relationBulkDetach')
|
|
1628
|
+
.label('Detach selected')
|
|
1629
|
+
.destructive()
|
|
1630
|
+
.bulk()
|
|
1631
|
+
.confirm(`Detach the selected ${labelPlural}? The records stay in place; only the links are removed.`)
|
|
1632
|
+
.handler(async (hctx) => {
|
|
1633
|
+
const rel = hctx.relation
|
|
1634
|
+
if (!rel) {
|
|
1635
|
+
return { notify: { title: 'Bulk-detach handler missing parent context — manager-scoped _action route not wired', type: 'error' } as never }
|
|
1636
|
+
}
|
|
1637
|
+
const records = hctx.records ?? []
|
|
1638
|
+
const ids: string[] = []
|
|
1639
|
+
for (const r of records) {
|
|
1640
|
+
const id = String((r as { id?: unknown }).id ?? '')
|
|
1641
|
+
if (!id) continue
|
|
1642
|
+
const allowed = await safeManagerPolicy(M, 'canDetach', ctx.related, hctx.user, ctx.parentRecord, r)
|
|
1643
|
+
if (!allowed) continue
|
|
1644
|
+
ids.push(id)
|
|
1645
|
+
}
|
|
1646
|
+
if (ids.length === 0) {
|
|
1647
|
+
return { notify: { title: 'Nothing to detach (no permitted rows)', type: 'warning' } as never }
|
|
1648
|
+
}
|
|
1649
|
+
const accessor = resolveM2MAccessor(rel.parent, rel.relationship)
|
|
1650
|
+
if (!accessor || typeof accessor.detach !== 'function') {
|
|
1651
|
+
return { notify: { title: `Pivot accessor missing on ${rel.relationship} — wrong relation type or ORM version?`, type: 'error' } as never }
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
await accessor.detach(ids)
|
|
1655
|
+
} catch (err) {
|
|
1656
|
+
return { notify: { title: `Bulk detach failed: ${err instanceof Error ? err.message : String(err)}`, type: 'error' } as never }
|
|
1657
|
+
}
|
|
1658
|
+
return { notify: { title: `${ids.length} ${labelPlural} detached`, type: 'success' } as never }
|
|
1659
|
+
})
|
|
1660
|
+
.visible(({ user }) => {
|
|
1661
|
+
if (!isM2MMode(ctx.mode)) return false
|
|
1662
|
+
// Bulk gate uses canAttach as a stand-in for "manager admin" —
|
|
1663
|
+
// per-row canDetach is enforced inside the handler.
|
|
1664
|
+
return safeManagerPolicy(M, 'canAttach', ctx.related, user, ctx.parentRecord)
|
|
1665
|
+
})
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
label(l: string): this { this._label = l; return this }
|
|
1669
|
+
icon(i: string): this { this._icon = i; return this }
|
|
1670
|
+
|
|
1671
|
+
// ─── Placement ────────────────────────────────────────
|
|
1672
|
+
|
|
1673
|
+
placement(p: ActionPlacement): this { this._placement = p; return this }
|
|
1674
|
+
inline(): this { return this.placement('inline') }
|
|
1675
|
+
row(): this { return this.placement('row') }
|
|
1676
|
+
bulk(): this { return this.placement('bulk') }
|
|
1677
|
+
header(): this { return this.placement('header') }
|
|
1678
|
+
|
|
1679
|
+
// ─── Behavior ─────────────────────────────────────────
|
|
1680
|
+
|
|
1681
|
+
destructive(v = true): this {
|
|
1682
|
+
this._destructive = v
|
|
1683
|
+
if (v && this._color === undefined) this._color = 'destructive'
|
|
1684
|
+
return this
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/** Set the visual color. `destructive` is also set by `.destructive()`. */
|
|
1688
|
+
color(c: ActionColor): this { this._color = c; return this }
|
|
1689
|
+
|
|
1690
|
+
/** Set the size preset (sm | md | lg). Default is `md`. */
|
|
1691
|
+
size(s: ActionSize): this { this._size = s; return this }
|
|
1692
|
+
|
|
1693
|
+
/** Hover tooltip. Wraps the button in a Tooltip primitive. */
|
|
1694
|
+
tooltip(t: string): this { this._tooltip = t; return this }
|
|
1695
|
+
|
|
1696
|
+
/** Outlined trigger style — border + transparent bg instead of solid color. */
|
|
1697
|
+
outlined(v = true): this { this._outlined = v; return this }
|
|
1698
|
+
|
|
1699
|
+
/** Icon-only trigger style. Renders a square button with just the icon;
|
|
1700
|
+
* the label is used as `aria-label`. Requires `.icon()` to be set. */
|
|
1701
|
+
iconButton(v = true): this { this._iconOnly = v; return this }
|
|
1702
|
+
|
|
1703
|
+
/** Show a small badge on the trigger (e.g. unread count). */
|
|
1704
|
+
badge(value: string | number): this { this._badge = value; return this }
|
|
1705
|
+
|
|
1706
|
+
/** Optional color class for the badge (e.g. 'bg-emerald-500'). */
|
|
1707
|
+
badgeColor(c: string): this { this._badgeColor = c; return this }
|
|
1708
|
+
|
|
1709
|
+
// ─── Conditional visibility / disabled ───────────────
|
|
1710
|
+
|
|
1711
|
+
/** Show the action only when `rule` is truthy. Pair with a function for
|
|
1712
|
+
* record-aware visibility (e.g. `({ record }) => !record.archived`).
|
|
1713
|
+
* Row-placement actions are evaluated per-row at table-load time;
|
|
1714
|
+
* other placements are evaluated at schema-resolve time with the
|
|
1715
|
+
* page-level context. */
|
|
1716
|
+
visible(rule: VisibilityRule): this { this._visible = rule; return this }
|
|
1717
|
+
|
|
1718
|
+
/** Inverse of `visible` — hide the action when `rule` is truthy.
|
|
1719
|
+
* Both rules combine via AND: visible if `visible !== false` AND
|
|
1720
|
+
* `hidden !== true`. */
|
|
1721
|
+
hidden(rule: VisibilityRule): this { this._hidden = rule; return this }
|
|
1722
|
+
|
|
1723
|
+
/** Disable (render greyed-out and skip dispatch) when `rule` is truthy.
|
|
1724
|
+
* Disabled actions still appear in the UI, unlike hidden ones. */
|
|
1725
|
+
disabled(rule: VisibilityRule): this { this._isDisabled = rule; return this }
|
|
1726
|
+
|
|
1727
|
+
/** Policy-style alias for `.visible(fn)` — semantically identical
|
|
1728
|
+
* but reads better when guarding by user permissions. */
|
|
1729
|
+
authorize(rule: VisibilityRule): this { return this.visible(rule) }
|
|
1730
|
+
|
|
1731
|
+
/** Evaluate the visibility / disabled rules with the given context.
|
|
1732
|
+
* Defaults: visible = true, disabled = false. Both `visible` and
|
|
1733
|
+
* `hidden` are folded in: `visible: visible !== false && hidden !== true`.
|
|
1734
|
+
*
|
|
1735
|
+
* Async to support Plan #10 — visibility rules can return Promise<bool>
|
|
1736
|
+
* (for `Resource.canX(user, record)` integration). Throwing rules are
|
|
1737
|
+
* treated as fail-closed (`visible: false` / `disabled: true`). */
|
|
1738
|
+
async evaluate(ctx: ActionVisibilityContext = {}): Promise<{ visible: boolean; disabled: boolean }> {
|
|
1739
|
+
const evalRule = async (rule: VisibilityRule | undefined, fallback: boolean): Promise<boolean> => {
|
|
1740
|
+
if (rule === undefined) return fallback
|
|
1741
|
+
if (typeof rule !== 'function') return rule
|
|
1742
|
+
try {
|
|
1743
|
+
return await rule(ctx)
|
|
1744
|
+
} catch {
|
|
1745
|
+
// Fail closed — a throwing rule shouldn't accidentally show a
|
|
1746
|
+
// destructive action.
|
|
1747
|
+
return !fallback
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const [visibleRaw, hiddenRaw, disabledRaw] = await Promise.all([
|
|
1751
|
+
evalRule(this._visible, true),
|
|
1752
|
+
evalRule(this._hidden, false),
|
|
1753
|
+
evalRule(this._isDisabled, false),
|
|
1754
|
+
])
|
|
1755
|
+
return {
|
|
1756
|
+
visible: visibleRaw && !hiddenRaw,
|
|
1757
|
+
disabled: disabledRaw,
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
/** True when any visibility / hidden / disabled rule is set. Useful for
|
|
1762
|
+
* the resolver to know whether per-row evaluation is needed for a
|
|
1763
|
+
* row-placement action. */
|
|
1764
|
+
hasVisibilityRules(): boolean {
|
|
1765
|
+
return this._visible !== undefined || this._hidden !== undefined || this._isDisabled !== undefined
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
/**
|
|
1769
|
+
* Prompt the user before running the handler. Pass a string for a simple
|
|
1770
|
+
* "are you sure?" message, or an object for full control.
|
|
1771
|
+
*/
|
|
1772
|
+
confirm(prompt: string | ActionConfirm): this {
|
|
1773
|
+
this._confirm = typeof prompt === 'string' ? { message: prompt } : prompt
|
|
1774
|
+
return this
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
/**
|
|
1778
|
+
* Server-side handler. Two shapes:
|
|
1779
|
+
*
|
|
1780
|
+
* - **Closure** — `handler(async ctx => …)`. Dispatched against the
|
|
1781
|
+
* current page's `_action/:name` endpoint. Closures don't survive
|
|
1782
|
+
* serialization, so they can't ride a persisted notification —
|
|
1783
|
+
* `Notification.toDatabase()` rejects them with a clear error.
|
|
1784
|
+
*
|
|
1785
|
+
* - **Registry key** — `handler('archive-project')`. Looked up against
|
|
1786
|
+
* `Pilotiq.notificationHandlers({…})` at request time. Round-trips
|
|
1787
|
+
* through a `Notification`'s `data.actions` JSON column, so this is
|
|
1788
|
+
* the path to take when an action lives on a row that may be
|
|
1789
|
+
* clicked days later.
|
|
1790
|
+
*
|
|
1791
|
+
* Mutually exclusive — setting one clears the other.
|
|
1792
|
+
*/
|
|
1793
|
+
handler(fnOrName: ActionHandler | string): this {
|
|
1794
|
+
if (typeof fnOrName === 'string') {
|
|
1795
|
+
this._handlerName = fnOrName
|
|
1796
|
+
delete this._handler
|
|
1797
|
+
} else {
|
|
1798
|
+
this._handler = fnOrName
|
|
1799
|
+
delete this._handlerName
|
|
1800
|
+
}
|
|
1801
|
+
return this
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Per-fire context for registry-handler dispatch. JSON-serializable
|
|
1806
|
+
* keys only — values flow through the notification's `data.actions`
|
|
1807
|
+
* column and arrive on the handler's `ctx.payload`.
|
|
1808
|
+
*
|
|
1809
|
+
* Action.make('archive')
|
|
1810
|
+
* .handler('archive-project')
|
|
1811
|
+
* .payload({ projectId: 123 })
|
|
1812
|
+
*/
|
|
1813
|
+
payload(data: Record<string, unknown>): this {
|
|
1814
|
+
this._payload = data
|
|
1815
|
+
return this
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Filament-style chain modifier — when this action lives inside a
|
|
1820
|
+
* `Notification.actions([…])` slot, firing it also flips the
|
|
1821
|
+
* notification's `read_at`. No-op for actions used outside the
|
|
1822
|
+
* notification context (Resource actions, etc).
|
|
1823
|
+
*
|
|
1824
|
+
* Action.make('view').url('/projects/123').markAsRead()
|
|
1825
|
+
*
|
|
1826
|
+
* On url/post actions the bell client fires the read POST as a
|
|
1827
|
+
* client-side side-effect *before* navigating/submitting; on
|
|
1828
|
+
* registry-handler actions the route flips `read_at` server-side
|
|
1829
|
+
* after the handler runs (one round-trip, not two).
|
|
1830
|
+
*/
|
|
1831
|
+
markAsRead(v = true): this {
|
|
1832
|
+
this._markAsReadOnFire = v
|
|
1833
|
+
return this
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* Open the action's url in a new tab. Honored by the notification
|
|
1838
|
+
* action-strip renderers; equivalent to `target="_blank"` on the
|
|
1839
|
+
* underlying anchor.
|
|
1840
|
+
*/
|
|
1841
|
+
openUrlInNewTab(v = true): this {
|
|
1842
|
+
this._openUrlInNewTab = v
|
|
1843
|
+
return this
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// ─── Modal / form-modal action ────────────────────────
|
|
1847
|
+
|
|
1848
|
+
/**
|
|
1849
|
+
* Attach a form schema that opens in a modal Dialog when the action is
|
|
1850
|
+
* triggered. The submitted values flow through validation + coercion
|
|
1851
|
+
* server-side and arrive on the handler's `ctx.values`. Triggers modal
|
|
1852
|
+
* chrome (heading / submit button / cancel button) if not configured
|
|
1853
|
+
* via the other modal builders below.
|
|
1854
|
+
*/
|
|
1855
|
+
schema(elements: Element[]): this {
|
|
1856
|
+
this._children = elements
|
|
1857
|
+
this._hasModal = true
|
|
1858
|
+
return this
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
modalHeading(s: string): this { this._modalHeading = s; this._hasModal = true; return this }
|
|
1862
|
+
modalDescription(s: string): this { this._modalDescription = s; this._hasModal = true; return this }
|
|
1863
|
+
modalSubmitLabel(s: string): this { this._modalSubmitLabel = s; this._hasModal = true; return this }
|
|
1864
|
+
modalCancelLabel(s: string): this { this._modalCancelLabel = s; this._hasModal = true; return this }
|
|
1865
|
+
/** Filament v5 alias for `modalSubmitLabel`. Reads more naturally
|
|
1866
|
+
* alongside `modalCancelActionLabel` when both are set. */
|
|
1867
|
+
modalSubmitActionLabel(s: string): this { return this.modalSubmitLabel(s) }
|
|
1868
|
+
/** Filament v5 alias for `modalCancelLabel`. */
|
|
1869
|
+
modalCancelActionLabel(s: string): this { return this.modalCancelLabel(s) }
|
|
1870
|
+
modalIcon(i: string): this { this._modalIcon = i; this._hasModal = true; return this }
|
|
1871
|
+
modalIconColor(c: ActionModalIconColor): this {
|
|
1872
|
+
this._modalIconColor = c
|
|
1873
|
+
this._hasModal = true
|
|
1874
|
+
return this
|
|
1875
|
+
}
|
|
1876
|
+
modalWidth(w: ActionModalWidth): this { this._modalWidth = w; this._hasModal = true; return this }
|
|
1877
|
+
modalAlignment(a: ActionModalAlignment): this {
|
|
1878
|
+
this._modalAlignment = a
|
|
1879
|
+
this._hasModal = true
|
|
1880
|
+
return this
|
|
1881
|
+
}
|
|
1882
|
+
slideOver(v = true): this { this._slideOver = v; this._hasModal = true; return this }
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* Disable / re-enable closing the modal by clicking outside the popup.
|
|
1886
|
+
* Default: enabled. Calling without an arg disables (matches Filament's
|
|
1887
|
+
* `->closeModalByClickingAway(false)` shape — most uses are to gate
|
|
1888
|
+
* accidental dismissal of important modals).
|
|
1889
|
+
*/
|
|
1890
|
+
closeModalByClickingAway(v: boolean = false): this {
|
|
1891
|
+
this._closeModalByClickingAway = v
|
|
1892
|
+
this._hasModal = true
|
|
1893
|
+
return this
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Disable / re-enable closing the modal with the Escape key. Default:
|
|
1898
|
+
* enabled. Same shape as `closeModalByClickingAway`. Useful when a
|
|
1899
|
+
* partially-completed multi-step modal would lose work on a stray Esc.
|
|
1900
|
+
*/
|
|
1901
|
+
closeModalByEscaping(v: boolean = false): this {
|
|
1902
|
+
this._closeModalByEscaping = v
|
|
1903
|
+
this._hasModal = true
|
|
1904
|
+
return this
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/** Sticky header inside the modal body. Useful when the body scrolls
|
|
1908
|
+
* past the heading on long forms. Default: off. */
|
|
1909
|
+
stickyModalHeader(v: boolean = true): this {
|
|
1910
|
+
this._stickyModalHeader = v
|
|
1911
|
+
this._hasModal = true
|
|
1912
|
+
return this
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/** Sticky footer inside the modal body. Pairs naturally with
|
|
1916
|
+
* `stickyModalHeader` on long forms — keeps the Submit / Cancel
|
|
1917
|
+
* buttons visible while the user scrolls. Default: off. */
|
|
1918
|
+
stickyModalFooter(v: boolean = true): this {
|
|
1919
|
+
this._stickyModalFooter = v
|
|
1920
|
+
this._hasModal = true
|
|
1921
|
+
return this
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Override the default autofocus behaviour. `true` focuses the first
|
|
1926
|
+
* form input on mount (or the submit button when there is no form);
|
|
1927
|
+
* `false` disables autofocus entirely. Omit to keep the legacy default
|
|
1928
|
+
* (submit button autofocuses for confirm-only modals).
|
|
1929
|
+
*/
|
|
1930
|
+
modalAutofocus(v: boolean = true): this {
|
|
1931
|
+
this._modalAutofocus = v
|
|
1932
|
+
this._hasModal = true
|
|
1933
|
+
return this
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/** Render an X close button in the top-right corner of the popup.
|
|
1937
|
+
* Default: off (the Cancel button in the footer is the documented
|
|
1938
|
+
* affordance). Useful for slide-over panels where the footer is
|
|
1939
|
+
* far from the top of the viewport. */
|
|
1940
|
+
modalCloseButton(v: boolean = true): this {
|
|
1941
|
+
this._modalCloseButton = v
|
|
1942
|
+
this._hasModal = true
|
|
1943
|
+
return this
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// ─── Link / form modes ────────────────────────────────
|
|
1947
|
+
|
|
1948
|
+
/**
|
|
1949
|
+
* Render this action as a link to `url`. Mutually exclusive with
|
|
1950
|
+
* `.method()` — setting `href` clears any prior method/action URL.
|
|
1951
|
+
*/
|
|
1952
|
+
href(url: string): this {
|
|
1953
|
+
this._href = url
|
|
1954
|
+
delete this._method
|
|
1955
|
+
delete this._actionUrl
|
|
1956
|
+
return this
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Sugar alias for `.href(url)` — matches Filament's `->url(...)` API
|
|
1961
|
+
* and reads more naturally inside `Notification.actions([…])`.
|
|
1962
|
+
*/
|
|
1963
|
+
url(href: string): this { return this.href(href) }
|
|
1964
|
+
|
|
1965
|
+
/**
|
|
1966
|
+
* Render this action as a form-style submit button using `method`. Pair
|
|
1967
|
+
* with `.action(url)` to set the form's action URL — falls back to the
|
|
1968
|
+
* current page URL otherwise.
|
|
1969
|
+
*/
|
|
1970
|
+
method(m: ActionMethod): this {
|
|
1971
|
+
this._method = m
|
|
1972
|
+
delete this._href
|
|
1973
|
+
return this
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/** Form action URL — only meaningful when `.method()` is set. */
|
|
1977
|
+
action(url: string): this {
|
|
1978
|
+
this._actionUrl = url
|
|
1979
|
+
delete this._href
|
|
1980
|
+
return this
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Render-time URL the client should POST to when invoking this
|
|
1985
|
+
* action's handler. Set by the route registrar — users don't normally
|
|
1986
|
+
* call this directly. Format: `${pageUrl}/_action/${action.name}`.
|
|
1987
|
+
*/
|
|
1988
|
+
dispatchUrl(url: string): this {
|
|
1989
|
+
this._dispatchUrl = url
|
|
1990
|
+
return this
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* Mark this action as the form-submit button for its enclosing
|
|
1995
|
+
* `<form>`. Renders as `<button type="submit">` and relies on the form
|
|
1996
|
+
* itself to carry `action` + `method`. Mutually exclusive with
|
|
1997
|
+
* `.href()` / `.method()` / handler-style.
|
|
1998
|
+
*/
|
|
1999
|
+
submit(): this {
|
|
2000
|
+
this._submit = true
|
|
2001
|
+
delete this._href
|
|
2002
|
+
delete this._method
|
|
2003
|
+
delete this._actionUrl
|
|
2004
|
+
delete this._dispatchUrl
|
|
2005
|
+
return this
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* Target a specific `<form id="">` when this is a submit action — uses
|
|
2010
|
+
* the HTML `form` attribute so the button can submit a form it doesn't
|
|
2011
|
+
* live inside. Required when the submit action sits in the page
|
|
2012
|
+
* header (outside the form's DOM subtree).
|
|
2013
|
+
*/
|
|
2014
|
+
form(formId: string): this {
|
|
2015
|
+
this._formTarget = formId
|
|
2016
|
+
return this
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Attach a `name`+`value` pair to a submit button so it round-trips
|
|
2021
|
+
* through the form body when this specific button is clicked. Wires
|
|
2022
|
+
* the "Create & create another" pattern: the secondary submit posts
|
|
2023
|
+
* `{ _continueCreate: '1' }` and the server routes the redirect back
|
|
2024
|
+
* to the create page. Browsers natively only include the clicked
|
|
2025
|
+
* submit button's name/value in `FormData`; the FormRenderer threads
|
|
2026
|
+
* `event.submitter` into the `FormData` constructor to preserve that.
|
|
2027
|
+
*/
|
|
2028
|
+
formField(name: string, value: string = '1'): this {
|
|
2029
|
+
this._formField = { name, value }
|
|
2030
|
+
return this
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// ─── Getters ──────────────────────────────────────────
|
|
2034
|
+
|
|
2035
|
+
getLabel(): string { return this._label }
|
|
2036
|
+
getPlacement(): ActionPlacement { return this._placement }
|
|
2037
|
+
isDestructive(): boolean { return this._destructive }
|
|
2038
|
+
getHandler(): ActionHandler | undefined { return this._handler }
|
|
2039
|
+
/** Registry key set by `.handler(string)`; undefined when the handler
|
|
2040
|
+
* is a closure (or unset). Mutually exclusive with `getHandler()`. */
|
|
2041
|
+
getHandlerName(): string | undefined { return this._handlerName }
|
|
2042
|
+
/** Per-fire context set by `.payload({…})`. Empty `{}` when unset
|
|
2043
|
+
* (callers can spread without a null check). */
|
|
2044
|
+
getPayload(): Record<string, unknown> { return this._payload ?? {} }
|
|
2045
|
+
/** Filament chain modifier — true when `.markAsRead()` was called.
|
|
2046
|
+
* Read by `Notification.actions([…])` serializer; ignored elsewhere. */
|
|
2047
|
+
isMarkAsReadOnFire(): boolean { return this._markAsReadOnFire }
|
|
2048
|
+
/** Honored by the notification action-strip renderers; equivalent to
|
|
2049
|
+
* `target="_blank"` on the underlying anchor. */
|
|
2050
|
+
isOpenUrlInNewTab(): boolean { return this._openUrlInNewTab }
|
|
2051
|
+
getHref(): string | undefined { return this._href }
|
|
2052
|
+
getMethod(): ActionMethod | undefined { return this._method }
|
|
2053
|
+
getActionUrl(): string | undefined { return this._actionUrl }
|
|
2054
|
+
getDispatchUrl(): string | undefined { return this._dispatchUrl }
|
|
2055
|
+
isSubmit(): boolean { return this._submit }
|
|
2056
|
+
getFormTarget(): string | undefined { return this._formTarget }
|
|
2057
|
+
getFormField(): { name: string; value: string } | undefined { return this._formField }
|
|
2058
|
+
hasModal(): boolean { return this._hasModal }
|
|
2059
|
+
/** Schema fields stored as children; `getChildren()` returns the same. */
|
|
2060
|
+
getSchema(): Element[] { return this._children ?? [] }
|
|
2061
|
+
getColor(): ActionColor | undefined { return this._color }
|
|
2062
|
+
getIcon(): string | undefined { return this._icon }
|
|
2063
|
+
getSize(): ActionSize | undefined { return this._size }
|
|
2064
|
+
getTooltip(): string | undefined { return this._tooltip }
|
|
2065
|
+
isOutlined(): boolean { return this._outlined }
|
|
2066
|
+
isIconOnly(): boolean { return this._iconOnly }
|
|
2067
|
+
getBadge(): string | number | undefined { return this._badge }
|
|
2068
|
+
|
|
2069
|
+
// ─── Element contract ────────────────────────────────
|
|
2070
|
+
|
|
2071
|
+
getType(): string { return 'action' }
|
|
2072
|
+
|
|
2073
|
+
override toMeta(): ActionMeta {
|
|
2074
|
+
const modal: ActionModalMeta | undefined = this._hasModal ? {
|
|
2075
|
+
...(this._modalHeading !== undefined ? { heading: this._modalHeading } : {}),
|
|
2076
|
+
...(this._modalDescription !== undefined ? { description: this._modalDescription } : {}),
|
|
2077
|
+
...(this._modalSubmitLabel !== undefined ? { submitLabel: this._modalSubmitLabel } : {}),
|
|
2078
|
+
...(this._modalCancelLabel !== undefined ? { cancelLabel: this._modalCancelLabel } : {}),
|
|
2079
|
+
...(this._modalIcon !== undefined ? { icon: this._modalIcon } : {}),
|
|
2080
|
+
...(this._modalIconColor !== undefined ? { iconColor: this._modalIconColor } : {}),
|
|
2081
|
+
...(this._modalWidth !== undefined ? { width: this._modalWidth } : {}),
|
|
2082
|
+
...(this._modalAlignment !== undefined ? { alignment: this._modalAlignment } : {}),
|
|
2083
|
+
...(this._slideOver ? { slideOver: true } : {}),
|
|
2084
|
+
// Boolean defaults are emitted only when the user has flipped them
|
|
2085
|
+
// away from the default — keeps the wire-shape unchanged for the
|
|
2086
|
+
// common case (no setter ever called).
|
|
2087
|
+
...(this._closeModalByClickingAway === false ? { closeByClickingAway: false } : {}),
|
|
2088
|
+
...(this._closeModalByEscaping === false ? { closeByEscaping: false } : {}),
|
|
2089
|
+
...(this._stickyModalHeader ? { stickyHeader: true } : {}),
|
|
2090
|
+
...(this._stickyModalFooter ? { stickyFooter: true } : {}),
|
|
2091
|
+
...(this._modalCloseButton ? { closeButton: true } : {}),
|
|
2092
|
+
...(this._modalAutofocus !== undefined ? { autofocus: this._modalAutofocus } : {}),
|
|
2093
|
+
} : undefined
|
|
2094
|
+
return {
|
|
2095
|
+
type: 'action',
|
|
2096
|
+
name: this.name,
|
|
2097
|
+
label: this._label,
|
|
2098
|
+
placement: this._placement,
|
|
2099
|
+
destructive: this._destructive,
|
|
2100
|
+
...(this._icon ? { icon: this._icon } : {}),
|
|
2101
|
+
...(this._confirm ? { confirm: this._confirm } : {}),
|
|
2102
|
+
...(this._href ? { href: this._href } : {}),
|
|
2103
|
+
...(this._method ? { method: this._method } : {}),
|
|
2104
|
+
...(this._actionUrl ? { action: this._actionUrl } : {}),
|
|
2105
|
+
...(this._dispatchUrl ? { dispatchUrl: this._dispatchUrl } : {}),
|
|
2106
|
+
...(this._submit ? { submit: true } : {}),
|
|
2107
|
+
...(this._formTarget ? { form: this._formTarget } : {}),
|
|
2108
|
+
...(this._formField ? { formField: this._formField } : {}),
|
|
2109
|
+
...(modal ? { modal } : {}),
|
|
2110
|
+
...(this._color ? { color: this._color } : {}),
|
|
2111
|
+
...(this._size ? { size: this._size } : {}),
|
|
2112
|
+
...(this._tooltip ? { tooltip: this._tooltip } : {}),
|
|
2113
|
+
...(this._outlined ? { outlined: true } : {}),
|
|
2114
|
+
...(this._iconOnly ? { iconOnly: true } : {}),
|
|
2115
|
+
...(this._badge !== undefined ? { badge: this._badge } : {}),
|
|
2116
|
+
...(this._badgeColor ? { badgeColor: this._badgeColor } : {}),
|
|
2117
|
+
...(this.hasVisibilityRules() ? { conditional: true } : {}),
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
/** Re-export for routes/dispatch consumers that need to type-narrow on
|
|
2123
|
+
* action validation failures. */
|
|
2124
|
+
export type { ValidationErrors }
|