@pilotiq/pilotiq 0.23.1 → 0.24.2
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/CHANGELOG.md +91 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/dist/actions/exportFactory.d.ts +10 -0
- package/dist/actions/exportFactory.d.ts.map +1 -1
- package/dist/actions/exportFactory.js +10 -0
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/react/CollabRoomContext.d.ts +5 -5
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +0 -1
- package/dist/react/index.js.map +1 -1
- package/dist/routes/helpers.d.ts.map +1 -1
- package/dist/routes/helpers.js +6 -2
- package/dist/routes/helpers.js.map +1 -1
- package/dist/routes/relations.d.ts.map +1 -1
- package/dist/routes/relations.js +12 -0
- package/dist/routes/relations.js.map +1 -1
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/dist/react/useCollabSeed.d.ts +0 -23
- package/dist/react/useCollabSeed.d.ts.map +0 -1
- package/dist/react/useCollabSeed.js +0 -82
- package/dist/react/useCollabSeed.js.map +0 -1
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -215
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -195
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useCollabSeed.ts +0 -86
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -700
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1227
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
package/src/Pilotiq.ts
DELETED
|
@@ -1,1158 +0,0 @@
|
|
|
1
|
-
import type * as React from 'react'
|
|
2
|
-
import type { Router } from '@rudderjs/router'
|
|
3
|
-
import type { ResourceClass } from './Resource.js'
|
|
4
|
-
import type { GlobalClass } from './Global.js'
|
|
5
|
-
import type { ClusterClass } from './Cluster.js'
|
|
6
|
-
import type { Page } from './Page.js'
|
|
7
|
-
import type { SchemaDefinition } from './schema/resolveSchema.js'
|
|
8
|
-
import type { ThemeConfig } from './theme/types.js'
|
|
9
|
-
import type { ThemeStorageAdapter } from './theme/storage.js'
|
|
10
|
-
import type { UploadAdapter } from './uploads/UploadAdapter.js'
|
|
11
|
-
import type { UserMenuItem } from './UserMenuItem.js'
|
|
12
|
-
import type { NavigationBadgeColor } from './Resource.js'
|
|
13
|
-
import type {
|
|
14
|
-
RenderHookEntry,
|
|
15
|
-
RenderHookFn,
|
|
16
|
-
RenderHookName,
|
|
17
|
-
RenderHookScope,
|
|
18
|
-
} from './RenderHook.js'
|
|
19
|
-
import type { NotificationActionHandler } from './notifications/types.js'
|
|
20
|
-
import {
|
|
21
|
-
validateRightPanel,
|
|
22
|
-
findDuplicateRightPanelId,
|
|
23
|
-
type RightPanelContribution,
|
|
24
|
-
} from './RightPanel.js'
|
|
25
|
-
import type {
|
|
26
|
-
NavComponentProps,
|
|
27
|
-
HeaderComponentProps,
|
|
28
|
-
FooterComponentProps,
|
|
29
|
-
} from './react/component-slots.js'
|
|
30
|
-
|
|
31
|
-
export type PilotiqLayout = 'sidebar' | 'topbar'
|
|
32
|
-
|
|
33
|
-
/** Plugin interface for extending Pilotiq panels. */
|
|
34
|
-
export interface PilotiqPlugin {
|
|
35
|
-
name: string
|
|
36
|
-
/** Called when `.use()` / `.plugins([…])` runs at panel-build time —
|
|
37
|
-
* mutate config as needed (register a right-sidebar contribution,
|
|
38
|
-
* register a field renderer, seed an internal registry, install a
|
|
39
|
-
* `Field.prototype` augment, etc). Idempotent under HMR / re-mount. */
|
|
40
|
-
register(panel: Pilotiq): void
|
|
41
|
-
/**
|
|
42
|
-
* Optional — called by `registerPilotiqRoutes(router, pilotiq)` AFTER
|
|
43
|
-
* the core routes are mounted, so plugins can register their own
|
|
44
|
-
* HTTP surface alongside the panel's. The mount order matches the
|
|
45
|
-
* order plugins were registered via `.use()` / `.plugins([…])`.
|
|
46
|
-
*
|
|
47
|
-
* Use this for plugin-owned endpoints (`POST {base}/_chat`,
|
|
48
|
-
* `POST {base}/_collab`, …) — the path prefix should mirror
|
|
49
|
-
* pilotiq's own underscore-prefixed precedent (`_search`,
|
|
50
|
-
* `_uploads`, `_widget`, `_notifications`).
|
|
51
|
-
*
|
|
52
|
-
* Skip this hook for plugins that only mutate panel config; nothing
|
|
53
|
-
* inside core walks plugins outside `register()` / `registerRoutes()`.
|
|
54
|
-
*/
|
|
55
|
-
registerRoutes?(router: Router, pilotiq: Pilotiq): void
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* User resolver — receives the request and returns the current user (or
|
|
60
|
-
* null). Pilotiq treats the user object as opaque; whatever the resolver
|
|
61
|
-
* returns is forwarded into `Resource.canX(user, …)` / `Global.canX(...)` /
|
|
62
|
-
* `Page.canAccess(user)` and into `Action` visibility rules. Sync or async.
|
|
63
|
-
*
|
|
64
|
-
* Apps using `@rudderjs/auth` typically pass `req => Auth.user()`. The
|
|
65
|
-
* resolver is optional — when unset, every `can*` predicate runs with
|
|
66
|
-
* `user === null` and the defaults (which return `true`) keep the panel
|
|
67
|
-
* working with no auth wired up.
|
|
68
|
-
*/
|
|
69
|
-
export type UserResolver = (req: unknown) => unknown | null | Promise<unknown | null>
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Server-side hook handed to `Pilotiq.editPageHydrator(fn)`. The
|
|
73
|
-
* resource-edit data builder calls every registered hydrator after the
|
|
74
|
-
* standard fill pipeline (`loadRecord` → `mutateFormDataBeforeFill` →
|
|
75
|
-
* `fillFromRecord` → `mutateFormDataAfterFill` →
|
|
76
|
-
* `applyRelationshipRepeaterFill` → `applyRelationshipBuilderFill`) and
|
|
77
|
-
* merges each non-null return onto the form's default values.
|
|
78
|
-
*
|
|
79
|
-
* Multiple hydrators are walked in registration order; later hydrators
|
|
80
|
-
* override keys produced by earlier ones. A hydrator returning `null`
|
|
81
|
-
* (or throwing) is a pass-through — the DB row values survive intact.
|
|
82
|
-
*
|
|
83
|
-
* Designed for the SSR-from-Y.Doc consumer in `@pilotiq-pro/collab`:
|
|
84
|
-
* read persisted Y.Text + Y.Map values for the record and override the
|
|
85
|
-
* matching field defaults, killing the DB → Y.Doc value flicker on
|
|
86
|
-
* hydration. Other consumers (per-tenant overrides, A/B experiments,
|
|
87
|
-
* draft-snapshot resume) compose the same way.
|
|
88
|
-
*
|
|
89
|
-
* Pilotiq core stays Yjs-free — the hook's `Record<string, unknown>`
|
|
90
|
-
* return is opaque, so the collab consumer's Yjs imports stay confined
|
|
91
|
-
* to its own `/server` subpath.
|
|
92
|
-
*/
|
|
93
|
-
export interface EditPageHydratorContext {
|
|
94
|
-
/** The Resource class being edited (server-side reference, not the
|
|
95
|
-
* serialized meta — gives access to `model`, `getSlug()`, etc.). */
|
|
96
|
-
resource: ResourceClass
|
|
97
|
-
/** Record id from the URL, stringified. */
|
|
98
|
-
recordId: string
|
|
99
|
-
/** Values produced by the fill pipeline so far — DB row → hooks →
|
|
100
|
-
* relationship fills. Hydrators read this to make merge decisions
|
|
101
|
-
* (e.g. "only override fields that have content in Y.Doc"). */
|
|
102
|
-
currentValues: Record<string, unknown>
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export type EditPageHydrator = (
|
|
106
|
-
ctx: EditPageHydratorContext,
|
|
107
|
-
) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Upload configuration. Apps register an adapter via `Pilotiq.uploads({
|
|
111
|
-
* adapter })`; the `_uploads` route hands every incoming file to it.
|
|
112
|
-
* Without an adapter, `FileUpload` fields render but the upload POST
|
|
113
|
-
* fails with a clear "no upload adapter configured" error.
|
|
114
|
-
*/
|
|
115
|
-
export interface UploadConfig {
|
|
116
|
-
adapter: UploadAdapter
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Database notifications configuration. Wired through
|
|
121
|
-
* `Pilotiq.databaseNotifications(opts?)`.
|
|
122
|
-
*
|
|
123
|
-
* Pilotiq stores rows on the `notification` table shipped by
|
|
124
|
-
* `@rudderjs/notification` (`prisma/schema/notification.prisma`). The
|
|
125
|
-
* panel consumes that table directly via `@rudderjs/orm`'s
|
|
126
|
-
* `ModelRegistry` adapter so apps that already have rudder's
|
|
127
|
-
* `NotificationProvider` running can read existing rows alongside ones
|
|
128
|
-
* authored by `Notification.make(...).sendToDatabase(recipient)`.
|
|
129
|
-
*
|
|
130
|
-
* `position` chooses where the bell mounts in the panel chrome.
|
|
131
|
-
* `'topbar'` (default) sits between `<ThemeToggle>` and `<UserMenu>` in
|
|
132
|
-
* both the sidebar and topbar layouts; `'sidebar'` moves it into the
|
|
133
|
-
* sidebar footer (parity with Filament's
|
|
134
|
-
* `DatabaseNotificationsPosition::Sidebar`).
|
|
135
|
-
*
|
|
136
|
-
* `polling` is the seconds between background refetches. `null`
|
|
137
|
-
* disables auto-polling — the bell fetches once on mount and after
|
|
138
|
-
* mark-read mutations. Filament default is 30 seconds; we mirror that.
|
|
139
|
-
*
|
|
140
|
-
* `pageSize` caps the list endpoint's page size (default 25).
|
|
141
|
-
*
|
|
142
|
-
* `badgeColor` recolors the unread-count pill on the bell trigger.
|
|
143
|
-
* Default `'primary'`. Reuses the same `NavigationBadgeColor` palette
|
|
144
|
-
* that the sidebar nav badges use.
|
|
145
|
-
*
|
|
146
|
-
* `trigger.icon` overrides the bell icon (registry name); `trigger.label`
|
|
147
|
-
* overrides the trigger's `aria-label`.
|
|
148
|
-
*
|
|
149
|
-
* `notifiableType` is the value pilotiq writes/reads against the
|
|
150
|
-
* `notifiable_type` column. Defaults to `'users'` to match
|
|
151
|
-
* `@rudderjs/notification`'s `DatabaseChannel`. Override when your app
|
|
152
|
-
* stores rows tagged for a different notifiable.
|
|
153
|
-
*/
|
|
154
|
-
export interface DatabaseNotificationsConfig {
|
|
155
|
-
/** Where the bell mounts. Default `'topbar'`. */
|
|
156
|
-
position?: 'topbar' | 'sidebar'
|
|
157
|
-
/** Auto-poll interval in seconds. `null` disables. Default `30`. */
|
|
158
|
-
polling?: number | null
|
|
159
|
-
/** Max rows the list endpoint returns. Default `25`. */
|
|
160
|
-
pageSize?: number
|
|
161
|
-
/** Unread badge color on the bell trigger. Default `'primary'`. */
|
|
162
|
-
badgeColor?: NavigationBadgeColor
|
|
163
|
-
/** Bell trigger overrides. */
|
|
164
|
-
trigger?: { icon?: string; label?: string }
|
|
165
|
-
/** `notifiable_type` column value. Default `'users'`. */
|
|
166
|
-
notifiableType?: string
|
|
167
|
-
/**
|
|
168
|
-
* Phase 2 — push new rows over WebSocket on
|
|
169
|
-
* `private-pilotiq-notifications.${user.id}` so the bell client can
|
|
170
|
-
* refetch immediately instead of waiting for the next poll. Requires
|
|
171
|
-
* `@rudderjs/broadcast` (the `BroadcastingProvider` must be in the
|
|
172
|
-
* app's providers list and the `RudderSocket` client must be vendored).
|
|
173
|
-
*
|
|
174
|
-
* Soft-fails when broadcast isn't installed — pilotiq still works,
|
|
175
|
-
* the bell just falls back to polling.
|
|
176
|
-
*
|
|
177
|
-
* Pass a string to override the WebSocket URL the client connects to
|
|
178
|
-
* (default: same-origin `ws://…/ws`). Most apps leave this on the
|
|
179
|
-
* default — `@rudderjs/broadcast`'s `BroadcastConfig.path` already
|
|
180
|
-
* mounts the upgrade handler at `/ws`.
|
|
181
|
-
*/
|
|
182
|
-
broadcast?: boolean | { wsUrl?: string }
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Sign-out configuration. Wired through `Pilotiq.signOut(url | config)`.
|
|
187
|
-
*
|
|
188
|
-
* The user menu renders the sign-out entry as a `<form method="POST"
|
|
189
|
-
* action={url}>` so CSRF middleware downstream can validate the request
|
|
190
|
-
* (a bare `<a href>` would be GET-only). Set `method: 'GET'` for
|
|
191
|
-
* traditional logout endpoints that redirect on a normal navigation.
|
|
192
|
-
*/
|
|
193
|
-
export interface SignOutConfig {
|
|
194
|
-
url: string
|
|
195
|
-
label?: string
|
|
196
|
-
method?: 'POST' | 'GET'
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface PilotiqConfig {
|
|
200
|
-
name: string
|
|
201
|
-
path: string
|
|
202
|
-
layout: PilotiqLayout
|
|
203
|
-
resources: ResourceClass[]
|
|
204
|
-
globals: GlobalClass[]
|
|
205
|
-
pages: (typeof Page)[]
|
|
206
|
-
clusters: ClusterClass[]
|
|
207
|
-
branding: { title?: string; logo?: string }
|
|
208
|
-
schema?: SchemaDefinition
|
|
209
|
-
/**
|
|
210
|
-
* Plan #15 — homepage page class. When set, `GET ${base}` resolves
|
|
211
|
-
* this Page's `static schema()` instead of the builder-level
|
|
212
|
-
* `schema()`. Set via `panel.dashboard(P)`. The page is also added
|
|
213
|
-
* to `cfg.pages` for nav + canAccess gating, with its nav URL
|
|
214
|
-
* collapsed back to `${base}` so the sidebar entry deep-links to
|
|
215
|
-
* the panel root.
|
|
216
|
-
*/
|
|
217
|
-
dashboardPage?: typeof Page
|
|
218
|
-
/**
|
|
219
|
-
* Profile page — when set, the user-menu dropdown auto-prepends an
|
|
220
|
-
* entry pointing at this Page's URL. The Page is registered in
|
|
221
|
-
* `cfg.pages` for routing + nav + canAccess gating. The menu entry's
|
|
222
|
-
* label and icon read from the Page's `static label` / `static icon`
|
|
223
|
-
* (with `'Edit profile'` and `'user-circle'` defaults).
|
|
224
|
-
*
|
|
225
|
-
* The Page is otherwise a normal `Page` subclass — author the form
|
|
226
|
-
* (`static schema()`) against your own auth model since pilotiq
|
|
227
|
-
* treats the user object as opaque.
|
|
228
|
-
*/
|
|
229
|
-
profilePage?: typeof Page
|
|
230
|
-
theme?: ThemeConfig
|
|
231
|
-
themeEditor?: boolean
|
|
232
|
-
/**
|
|
233
|
-
* Theme override persistence adapter — wired via
|
|
234
|
-
* `themeEditor({ storage })`. Reads/writes the JSON blob the editor
|
|
235
|
-
* page produces. Without this, the service provider falls back to
|
|
236
|
-
* the implicit Prisma adapter (auto-resolved via
|
|
237
|
-
* `app.make('prisma')`) for back-compat — that fallback is
|
|
238
|
-
* deprecated and will be removed in a future minor; pass `storage`
|
|
239
|
-
* explicitly.
|
|
240
|
-
*/
|
|
241
|
-
themeStorage?: ThemeStorageAdapter
|
|
242
|
-
guard?: (req: unknown) => boolean | Promise<boolean>
|
|
243
|
-
user?: UserResolver
|
|
244
|
-
uploads?: UploadConfig
|
|
245
|
-
/**
|
|
246
|
-
* Top-right user-menu entries (between user identity and sign-out).
|
|
247
|
-
* Order: items with explicit `.sort(n)` ascending, ties → registration
|
|
248
|
-
* order; unsorted items follow in registration order.
|
|
249
|
-
*/
|
|
250
|
-
userMenuItems?: UserMenuItem[]
|
|
251
|
-
/** Sign-out endpoint config — when set, the user menu appends a
|
|
252
|
-
* separator + Sign-out entry. Without this, the menu shows custom
|
|
253
|
-
* items (if any) and the user identity, but no sign-out affordance. */
|
|
254
|
-
signOut?: SignOutConfig
|
|
255
|
-
/** Server-side hydrators applied after the fill pipeline on every
|
|
256
|
-
* resource edit page. Empty / unset → behaviour unchanged. See
|
|
257
|
-
* `EditPageHydrator` for the contract; plugins register via
|
|
258
|
-
* `panel.editPageHydrator(fn)`. */
|
|
259
|
-
editPageHydrators?: EditPageHydrator[]
|
|
260
|
-
/** Database notifications — opt-in. Mounts the bell + 4 endpoints
|
|
261
|
-
* (`_notifications` list / `:id/read` / `:id/unread` / `read-all`).
|
|
262
|
-
* Reads rows from `@rudderjs/notification`'s `notification` table via
|
|
263
|
-
* `@rudderjs/orm`'s `ModelRegistry` adapter, scoped to
|
|
264
|
-
* `pilotiq.resolveUser(req).id`. */
|
|
265
|
-
databaseNotifications?: DatabaseNotificationsConfig & { enabled: true }
|
|
266
|
-
/**
|
|
267
|
-
* Named notification-action handlers. Registered via
|
|
268
|
-
* `Pilotiq.notificationHandlers({ name: fn })`. Looked up by the
|
|
269
|
-
* `_notifications/:id/_action/:actionName` route at request time when
|
|
270
|
-
* a stored action carries `handler: 'name'`.
|
|
271
|
-
*
|
|
272
|
-
* The closure-handler path on Action (`.handler(async ctx => …)`)
|
|
273
|
-
* works for transient toasts but doesn't survive a `data` JSON
|
|
274
|
-
* round-trip — this registry is the persistence-safe escape hatch.
|
|
275
|
-
*/
|
|
276
|
-
notificationHandlers?: Record<string, NotificationActionHandler>
|
|
277
|
-
/**
|
|
278
|
-
* Render-hook entries registered via `Pilotiq.renderHook(name, fn,
|
|
279
|
-
* scope?)`. Resolved per-request by `panelInfo()` (chrome slots) and
|
|
280
|
-
* the per-page-role data builders (page-role slots).
|
|
281
|
-
*/
|
|
282
|
-
renderHooks?: RenderHookEntry[]
|
|
283
|
-
/**
|
|
284
|
-
* Right-sidebar plugin contributions registered via
|
|
285
|
-
* `Pilotiq.rightPanel(c)` / `Pilotiq.rightPanels([c, …])`. Each entry
|
|
286
|
-
* is auth-gated, sorted, and serialized into `panel.rightSidebar`
|
|
287
|
-
* under `panelInfo()`. Empty / all-gated → `panel.rightSidebar` is
|
|
288
|
-
* absent and the chrome doesn't mount.
|
|
289
|
-
*/
|
|
290
|
-
rightPanels?: RightPanelContribution[]
|
|
291
|
-
/**
|
|
292
|
-
* Layout-level provider components registered via
|
|
293
|
-
* `Pilotiq.layoutProvider(C)` / `Pilotiq.layoutProviders([…])`. Plugins
|
|
294
|
-
* register React providers (e.g. an AI chat queue context, a tenant
|
|
295
|
-
* theme switcher) that wrap the panel's `<AppShell>` children — so
|
|
296
|
-
* the providers are in scope for every page in the panel, not just
|
|
297
|
-
* specific component slots. Order matters: the first registered
|
|
298
|
-
* provider sits closest to the root (outermost wrap); the last sits
|
|
299
|
-
* closest to the page tree (innermost wrap).
|
|
300
|
-
*/
|
|
301
|
-
layoutProviders?: LayoutProviderComponent[]
|
|
302
|
-
/**
|
|
303
|
-
* Build-time component overrides for panel chrome slots. Registered
|
|
304
|
-
* via `Pilotiq.components({ nav })`. The Vite plugin harvests the
|
|
305
|
-
* actual React refs into `pages/(pilotiq)/_components.ts`; component
|
|
306
|
-
* refs never travel over the wire.
|
|
307
|
-
*
|
|
308
|
-
* v1 ships only the `nav` slot — full replacement of the sidebar's
|
|
309
|
-
* `<SidebarContent>` body and the topbar's `<nav>` element. Other
|
|
310
|
-
* slots (`header`, `footer`, …) will land when a concrete consumer
|
|
311
|
-
* asks; the shape is open-ended so additions don't break this
|
|
312
|
-
* surface.
|
|
313
|
-
*/
|
|
314
|
-
components?: ComponentSlots
|
|
315
|
-
/**
|
|
316
|
-
* AI suggestion mode — controls what happens when an AI agent calls a
|
|
317
|
-
* write tool against a form field.
|
|
318
|
-
*
|
|
319
|
-
* - `'auto'` (default): the write applies immediately to the form state.
|
|
320
|
-
* Existing behavior — agents take effect as soon as the tool returns.
|
|
321
|
-
* - `'review'`: the write is staged as a `PendingSuggestion` and the
|
|
322
|
-
* field shows an inline diff (text fields) or current → suggested
|
|
323
|
-
* comparison (other types) with Approve / Reject buttons. Approve
|
|
324
|
-
* runs the field's registered applier; Reject discards.
|
|
325
|
-
*
|
|
326
|
-
* VS Code-style review flow. Plan: `docs/plans/ai-review-mode.md`.
|
|
327
|
-
*/
|
|
328
|
-
aiSuggestionsMode?: 'auto' | 'review'
|
|
329
|
-
/** @internal Runtime theme overrides from DB. */
|
|
330
|
-
_themeOverrides?: Partial<ThemeConfig>
|
|
331
|
-
/**
|
|
332
|
-
* TTL (milliseconds) for the per-user navigation badge cache. Set to
|
|
333
|
-
* `0` (or `null` via the builder) to disable caching. Default 30000.
|
|
334
|
-
*/
|
|
335
|
-
navigationBadgeTtlMs?: number
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Shape a layout-provider component must implement. Receives the
|
|
340
|
-
* standard layout context (`basePath`, panel children) so providers can
|
|
341
|
-
* thread panel-aware values into their context — e.g. an AI chat
|
|
342
|
-
* provider needs `basePath` to know where to POST chat requests.
|
|
343
|
-
*/
|
|
344
|
-
export type LayoutProviderComponent = React.ComponentType<{
|
|
345
|
-
children: React.ReactNode
|
|
346
|
-
basePath?: string
|
|
347
|
-
}>
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Chrome-slot overrides registered through `Pilotiq.components({ … })`.
|
|
351
|
-
*
|
|
352
|
-
* Three slots ship today:
|
|
353
|
-
*
|
|
354
|
-
* - `nav` — replaces the default nav tree (`<SidebarMenu>` body in
|
|
355
|
-
* `SidebarLayout`, the `<nav>` cluster in `TopbarLayout`). Surrounding
|
|
356
|
-
* chrome (branding header, render-hook splices, footer, sign-out
|
|
357
|
-
* menu) stays.
|
|
358
|
-
* - `header` — replaces the whole `<header>` chrome bar. In
|
|
359
|
-
* `SidebarLayout` that's the top bar with search / theme / bell /
|
|
360
|
-
* user menu; in `TopbarLayout` that's the whole top region including
|
|
361
|
-
* the brand cluster AND the nav (setting `header` makes `nav`
|
|
362
|
-
* irrelevant in `TopbarLayout`). Render hooks that splice INSIDE the
|
|
363
|
-
* default header (`panels::topbar.*`, `panels::user-menu.*`) don't
|
|
364
|
-
* fire when the header is replaced — the surrounding container is
|
|
365
|
-
* gone. The consumer reimplements the chrome controls they want
|
|
366
|
-
* (import `<SearchTrigger>`, `<ThemeToggle>`, `<NotificationBell>`,
|
|
367
|
-
* `<RightSidebarTrigger>`, `<UserMenu>` from `@pilotiq/pilotiq/react`).
|
|
368
|
-
* - `footer` — mounts a `<footer>` element BELOW the main content area
|
|
369
|
-
* in both layouts (outside the scroll region). Separate from the
|
|
370
|
-
* `panels::footer` render hook, which keeps firing inside the content
|
|
371
|
-
* area for per-page trailing chrome.
|
|
372
|
-
*
|
|
373
|
-
* The shape is open-ended so additional slots can land without a
|
|
374
|
-
* breaking change when a concrete consumer asks for them.
|
|
375
|
-
*/
|
|
376
|
-
export interface ComponentSlots {
|
|
377
|
-
/** Component rendered in place of the default nav tree. */
|
|
378
|
-
nav?: React.ComponentType<NavComponentProps>
|
|
379
|
-
/** Component rendered in place of the default `<header>` chrome. */
|
|
380
|
-
header?: React.ComponentType<HeaderComponentProps>
|
|
381
|
-
/** Component rendered as the panel footer, below the main content. */
|
|
382
|
-
footer?: React.ComponentType<FooterComponentProps>
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export class Pilotiq {
|
|
386
|
-
private config: PilotiqConfig
|
|
387
|
-
private installedPlugins: PilotiqPlugin[] = []
|
|
388
|
-
/** Lazy slug-indexed caches. Built on first lookup; invalidated when
|
|
389
|
-
* the underlying setter mutates the matching array. Resources /
|
|
390
|
-
* globals / pages are looked up by slug 16+ times per request across
|
|
391
|
-
* the page-data builders — the linear `Array.find` adds up around 50+
|
|
392
|
-
* resources. */
|
|
393
|
-
private _resourceBySlug?: Map<string, ResourceClass>
|
|
394
|
-
private _globalBySlug?: Map<string, GlobalClass>
|
|
395
|
-
private _pageBySlug?: Map<string, typeof Page>
|
|
396
|
-
/**
|
|
397
|
-
* Per-user navigation badge cache. Keyed by `${ownerName}|${userKey}`
|
|
398
|
-
* — `userKey` derived from `user.id` (or the primitive user / JSON
|
|
399
|
-
* fallback / `''` for anon). Each entry expires after
|
|
400
|
-
* `getNavigationBadgeTtl()` ms.
|
|
401
|
-
*/
|
|
402
|
-
private _navigationBadgeCache: Map<string, { value: string | undefined; expires: number }> = new Map()
|
|
403
|
-
|
|
404
|
-
private constructor(name: string) {
|
|
405
|
-
this.config = {
|
|
406
|
-
name,
|
|
407
|
-
path: '/admin',
|
|
408
|
-
layout: 'sidebar',
|
|
409
|
-
resources: [],
|
|
410
|
-
globals: [],
|
|
411
|
-
pages: [],
|
|
412
|
-
clusters: [],
|
|
413
|
-
branding: {},
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
static make(name: string): Pilotiq {
|
|
418
|
-
return new Pilotiq(name)
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
path(p: string): this {
|
|
422
|
-
this.config.path = `/${p.replace(/^\/+/, '')}`
|
|
423
|
-
return this
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
branding(b: { title?: string; logo?: string }): this {
|
|
427
|
-
this.config.branding = b
|
|
428
|
-
return this
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
resources(r: ResourceClass[]): this {
|
|
432
|
-
this.config.resources = r
|
|
433
|
-
delete this._resourceBySlug
|
|
434
|
-
return this
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
globals(g: GlobalClass[]): this {
|
|
438
|
-
this.config.globals = g
|
|
439
|
-
delete this._globalBySlug
|
|
440
|
-
return this
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
pages(p: (typeof Page)[]): this {
|
|
444
|
-
this.config.pages = p
|
|
445
|
-
delete this._pageBySlug
|
|
446
|
-
return this
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
clusters(c: ClusterClass[]): this {
|
|
450
|
-
this.config.clusters = c
|
|
451
|
-
return this
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
schema(def: SchemaDefinition): this {
|
|
455
|
-
this.config.schema = def
|
|
456
|
-
return this
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Plan #15 — mark a Page as the panel-root entry. Sugar for "render
|
|
461
|
-
* this page's `schema()` at `${base}` instead of the default
|
|
462
|
-
* dashboard layout."
|
|
463
|
-
*
|
|
464
|
-
* Effects:
|
|
465
|
-
* 1. `GET ${base}` resolves the page's schema (instead of the
|
|
466
|
-
* builder-level `schema()` definition).
|
|
467
|
-
* 2. The page is registered in `cfg.pages` if not already there
|
|
468
|
-
* (so canAccess + the action / form-state routes wire up).
|
|
469
|
-
* 3. Navigation collapses the page's nav URL to `${base}` (no
|
|
470
|
-
* trailing slug segment) so the sidebar entry deep-links to
|
|
471
|
-
* the panel root.
|
|
472
|
-
*
|
|
473
|
-
* The page subclass is plain — no special Page subclass required.
|
|
474
|
-
* The convention is `static slug = ''` so the regular slug-route
|
|
475
|
-
* doesn't also catch it; `panel.dashboard()` enforces this by
|
|
476
|
-
* skipping the page in the slug-route registration.
|
|
477
|
-
*
|
|
478
|
-
* @example
|
|
479
|
-
* class MyDashboard extends Page {
|
|
480
|
-
* static slug = ''
|
|
481
|
-
* static label = 'Dashboard'
|
|
482
|
-
* static schema() { return [Heading.make('Welcome')] }
|
|
483
|
-
* }
|
|
484
|
-
* panel.dashboard(MyDashboard)
|
|
485
|
-
*/
|
|
486
|
-
dashboard(P: typeof Page): this {
|
|
487
|
-
this.config.dashboardPage = P
|
|
488
|
-
if (!this.config.pages.includes(P)) {
|
|
489
|
-
this.config.pages = [...this.config.pages, P]
|
|
490
|
-
delete this._pageBySlug
|
|
491
|
-
}
|
|
492
|
-
return this
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
/**
|
|
496
|
-
* Mark a Page as the panel's profile page. The page is auto-added to
|
|
497
|
-
* `cfg.pages` (routing + canAccess wired up) and the user-menu
|
|
498
|
-
* dropdown prepends an "Edit profile" entry pointing at it.
|
|
499
|
-
*
|
|
500
|
-
* class ProfilePage extends Page {
|
|
501
|
-
* static slug = 'profile'
|
|
502
|
-
* static label = 'My profile'
|
|
503
|
-
* static icon = 'user-circle'
|
|
504
|
-
* static schema(ctx) {
|
|
505
|
-
* return [Form.make().schema([TextField.make('name')…])]
|
|
506
|
-
* }
|
|
507
|
-
* }
|
|
508
|
-
* panel.profile(ProfilePage)
|
|
509
|
-
*/
|
|
510
|
-
profile(P: typeof Page): this {
|
|
511
|
-
this.config.profilePage = P
|
|
512
|
-
if (!this.config.pages.includes(P)) {
|
|
513
|
-
this.config.pages = [...this.config.pages, P]
|
|
514
|
-
delete this._pageBySlug
|
|
515
|
-
}
|
|
516
|
-
return this
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
layout(l: PilotiqLayout): this {
|
|
520
|
-
this.config.layout = l
|
|
521
|
-
return this
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
theme(config: ThemeConfig): this {
|
|
525
|
-
this.config.theme = config
|
|
526
|
-
return this
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
guard(fn: (req: unknown) => boolean | Promise<boolean>): this {
|
|
530
|
-
this.config.guard = fn
|
|
531
|
-
return this
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Configure the current-user resolver. Pilotiq calls `fn(req)` once per
|
|
536
|
-
* request and forwards the return value into every `Resource.canX(...)`,
|
|
537
|
-
* `Global.canX(...)`, `Page.canAccess(...)`, and `Action.visible(({ user })
|
|
538
|
-
* => ...)` callback. The user object is opaque to pilotiq.
|
|
539
|
-
*
|
|
540
|
-
* Apps using `@rudderjs/auth`:
|
|
541
|
-
*
|
|
542
|
-
* import { Auth } from '@rudderjs/auth'
|
|
543
|
-
* Pilotiq.make('admin').user(() => Auth.user())
|
|
544
|
-
*
|
|
545
|
-
* Apps with custom auth pass whatever resolves their user. When unset,
|
|
546
|
-
* `resolveUser` returns `null` and the default `can*` predicates (which
|
|
547
|
-
* ignore `user`) all resolve `true`.
|
|
548
|
-
*/
|
|
549
|
-
user(fn: UserResolver): this {
|
|
550
|
-
this.config.user = fn
|
|
551
|
-
return this
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
/**
|
|
555
|
-
* Configure file uploads. Pass an adapter implementing
|
|
556
|
-
* `UploadAdapter`; `localUpload({ root, urlPrefix })` is bundled for
|
|
557
|
-
* disk-backed storage. Apps using S3 / R2 / a custom storage backend
|
|
558
|
-
* provide their own adapter conforming to the same interface.
|
|
559
|
-
*
|
|
560
|
-
* import { localUpload } from '@pilotiq/pilotiq/uploads'
|
|
561
|
-
* Pilotiq.make('admin').uploads({
|
|
562
|
-
* adapter: localUpload({ root: 'public/uploads', urlPrefix: '/uploads' })
|
|
563
|
-
* })
|
|
564
|
-
*/
|
|
565
|
-
uploads(config: UploadConfig): this {
|
|
566
|
-
this.config.uploads = config
|
|
567
|
-
return this
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Register entries for the panel's top-right user-menu dropdown.
|
|
572
|
-
* Replaces any previously registered set (call once with the full
|
|
573
|
-
* list). The dropdown only mounts when `Pilotiq.user(req => …)` is
|
|
574
|
-
* configured AND resolves a non-null user; otherwise the items are
|
|
575
|
-
* silently ignored.
|
|
576
|
-
*
|
|
577
|
-
* import { UserMenuItem } from '@pilotiq/pilotiq'
|
|
578
|
-
* Pilotiq.make('admin')
|
|
579
|
-
* .user(req => Auth.user())
|
|
580
|
-
* .userMenuItems([
|
|
581
|
-
* UserMenuItem.make('profile').label('My profile').url('/profile'),
|
|
582
|
-
* UserMenuItem.make('billing').label('Billing').url('/billing').sort(10),
|
|
583
|
-
* ])
|
|
584
|
-
* .signOut('/logout')
|
|
585
|
-
*/
|
|
586
|
-
userMenuItems(items: UserMenuItem[]): this {
|
|
587
|
-
this.config.userMenuItems = items
|
|
588
|
-
return this
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Configure the sign-out entry on the user menu. Pass a string for the
|
|
593
|
-
* default POST shape, or a `SignOutConfig` to override label/method.
|
|
594
|
-
*
|
|
595
|
-
* .signOut('/logout')
|
|
596
|
-
* .signOut({ url: '/auth/logout', method: 'GET', label: 'Log out' })
|
|
597
|
-
*/
|
|
598
|
-
signOut(config: string | SignOutConfig): this {
|
|
599
|
-
this.config.signOut = typeof config === 'string' ? { url: config } : config
|
|
600
|
-
return this
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/**
|
|
604
|
-
* Register a server-side hydrator that runs after the fill pipeline
|
|
605
|
-
* on every resource edit page. Each registered hydrator's non-null
|
|
606
|
-
* return merges onto the form's default values; multiple registrations
|
|
607
|
-
* are walked in registration order (later wins on key conflicts).
|
|
608
|
-
*
|
|
609
|
-
* panel.editPageHydrator(async ({ resource, recordId }) => {
|
|
610
|
-
* // override defaults for this record from your alt source
|
|
611
|
-
* return { title: await draftStore.read(resource.getSlug(), recordId) }
|
|
612
|
-
* })
|
|
613
|
-
*
|
|
614
|
-
* Plugins typically register from inside their `register(panel)` hook.
|
|
615
|
-
* Throwing or returning `null` is a pass-through — the DB row values
|
|
616
|
-
* survive. See `EditPageHydrator` for the full contract.
|
|
617
|
-
*/
|
|
618
|
-
editPageHydrator(fn: EditPageHydrator): this {
|
|
619
|
-
if (!this.config.editPageHydrators) this.config.editPageHydrators = []
|
|
620
|
-
this.config.editPageHydrators.push(fn)
|
|
621
|
-
return this
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
/**
|
|
625
|
-
* Enable persistent database-backed notifications. Mounts the bell in
|
|
626
|
-
* the panel chrome + a small set of `_notifications` endpoints. The
|
|
627
|
-
* bell only renders when a non-null user resolves from
|
|
628
|
-
* `Pilotiq.user(req => …)` — anonymous requests never see the
|
|
629
|
-
* affordance.
|
|
630
|
-
*
|
|
631
|
-
* Sends originate from `Notification.make('Saved')
|
|
632
|
-
* .sendToDatabase(user)` (and the rudder `Notifier.send(user, ...)` /
|
|
633
|
-
* `notify(user, ...)` paths when the app uses the upstream
|
|
634
|
-
* notification class). Both write to the same `notification` table so
|
|
635
|
-
* the bell surfaces both.
|
|
636
|
-
*
|
|
637
|
-
* Pilotiq.make('admin')
|
|
638
|
-
* .user(req => Auth.user())
|
|
639
|
-
* .databaseNotifications() // defaults
|
|
640
|
-
* .databaseNotifications({ position: 'sidebar' }) // bell in sidebar
|
|
641
|
-
* .databaseNotifications({ polling: null }) // disable polling
|
|
642
|
-
* .databaseNotifications({ polling: 10, pageSize: 50 }) // custom
|
|
643
|
-
*/
|
|
644
|
-
databaseNotifications(opts: DatabaseNotificationsConfig = {}): this {
|
|
645
|
-
this.config.databaseNotifications = { ...opts, enabled: true }
|
|
646
|
-
return this
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Sugar setter for the polling interval. `null` disables auto-poll.
|
|
651
|
-
* Has no effect unless `.databaseNotifications()` was called.
|
|
652
|
-
*/
|
|
653
|
-
databaseNotificationsPolling(seconds: number | null): this {
|
|
654
|
-
if (!this.config.databaseNotifications) return this
|
|
655
|
-
this.config.databaseNotifications = {
|
|
656
|
-
...this.config.databaseNotifications,
|
|
657
|
-
polling: seconds,
|
|
658
|
-
}
|
|
659
|
-
return this
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Sugar setter for the bell mount position. Has no effect unless
|
|
664
|
-
* `.databaseNotifications()` was called.
|
|
665
|
-
*/
|
|
666
|
-
databaseNotificationsPosition(position: 'topbar' | 'sidebar'): this {
|
|
667
|
-
if (!this.config.databaseNotifications) return this
|
|
668
|
-
this.config.databaseNotifications = {
|
|
669
|
-
...this.config.databaseNotifications,
|
|
670
|
-
position,
|
|
671
|
-
}
|
|
672
|
-
return this
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Phase 2 — enable broadcast push for the bell. Requires
|
|
677
|
-
* `@rudderjs/broadcast` to be installed and `BroadcastingProvider`
|
|
678
|
-
* registered. Pass `{ wsUrl }` to override the WebSocket URL (default:
|
|
679
|
-
* same-origin `/ws`). Has no effect unless `.databaseNotifications()`
|
|
680
|
-
* was called.
|
|
681
|
-
*
|
|
682
|
-
* .databaseNotifications().databaseNotificationsBroadcast()
|
|
683
|
-
* .databaseNotifications().databaseNotificationsBroadcast({ wsUrl: 'wss://…/ws' })
|
|
684
|
-
*/
|
|
685
|
-
databaseNotificationsBroadcast(opts: boolean | { wsUrl?: string } = true): this {
|
|
686
|
-
if (!this.config.databaseNotifications) return this
|
|
687
|
-
this.config.databaseNotifications = {
|
|
688
|
-
...this.config.databaseNotifications,
|
|
689
|
-
broadcast: opts,
|
|
690
|
-
}
|
|
691
|
-
return this
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Register named handlers for `Notification.actions([…])` slots.
|
|
696
|
-
* Calling this twice merges — later registrations override earlier
|
|
697
|
-
* keys.
|
|
698
|
-
*
|
|
699
|
-
* panel.notificationHandlers({
|
|
700
|
-
* 'archive-project': async (ctx) => {
|
|
701
|
-
* const { projectId } = ctx.payload as { projectId: number }
|
|
702
|
-
* await Project.update(projectId, { archivedAt: new Date() })
|
|
703
|
-
* return { notify: { title: 'Archived', type: 'success' } }
|
|
704
|
-
* },
|
|
705
|
-
* })
|
|
706
|
-
*
|
|
707
|
-
* Names must match `^[A-Za-z0-9_-]+$` (URL-safe — they ride in the
|
|
708
|
-
* action endpoint path). Empty / whitespace / non-conforming keys
|
|
709
|
-
* throw at registration time so a typo fails fast instead of 404ing
|
|
710
|
-
* three days later when a user clicks the action on an old row.
|
|
711
|
-
*
|
|
712
|
-
* Closures registered here survive a `data` JSON round-trip (only
|
|
713
|
-
* the name does — the closure stays in memory). For transient toasts
|
|
714
|
-
* authored against the current page, `Action.handler(async ctx => …)`
|
|
715
|
-
* still works inline — the registry is only required for persisted
|
|
716
|
-
* actions.
|
|
717
|
-
*/
|
|
718
|
-
notificationHandlers(map: Record<string, NotificationActionHandler>): this {
|
|
719
|
-
const namePattern = /^[A-Za-z0-9_-]+$/
|
|
720
|
-
for (const key of Object.keys(map)) {
|
|
721
|
-
if (!namePattern.test(key)) {
|
|
722
|
-
throw new Error(
|
|
723
|
-
`[Pilotiq] notificationHandlers: handler name "${key}" contains characters ` +
|
|
724
|
-
`outside [A-Za-z0-9_-]. Names ride in the action URL path; pick a URL-safe key.`,
|
|
725
|
-
)
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
this.config.notificationHandlers = {
|
|
729
|
-
...(this.config.notificationHandlers ?? {}),
|
|
730
|
-
...map,
|
|
731
|
-
}
|
|
732
|
-
return this
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/** @internal — looked up by the notification-action route. */
|
|
736
|
-
getNotificationHandler(name: string): NotificationActionHandler | undefined {
|
|
737
|
-
return this.config.notificationHandlers?.[name]
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
/**
|
|
741
|
-
* Register a render hook — a callback that returns `Element[]` to
|
|
742
|
-
* mount at a named slot in the panel chrome or page renderers. Multiple
|
|
743
|
-
* hooks against the same name run in registration order; their outputs
|
|
744
|
-
* concatenate.
|
|
745
|
-
*
|
|
746
|
-
* import { Alert } from '@pilotiq/pilotiq'
|
|
747
|
-
*
|
|
748
|
-
* Pilotiq.make('admin')
|
|
749
|
-
* .renderHook('panels::topbar.start', ({ user }) => [
|
|
750
|
-
* Alert.make(`Hi, ${(user as any)?.name ?? 'there'}`).info(),
|
|
751
|
-
* ])
|
|
752
|
-
* .renderHook(
|
|
753
|
-
* 'panels::resource.pages.list-records.table.before',
|
|
754
|
-
* () => [Heading.make('Bulk import')],
|
|
755
|
-
* { resource: ArticleResource },
|
|
756
|
-
* )
|
|
757
|
-
*
|
|
758
|
-
* Scope (optional) restricts the hook to a single resource / page /
|
|
759
|
-
* global. Scope keys are OR'd within the object — passing
|
|
760
|
-
* `{ resource: A, page: P }` fires when EITHER `A` OR `P` is the
|
|
761
|
-
* active route's identifier.
|
|
762
|
-
*
|
|
763
|
-
* Throwing hooks fail closed: their slot's contribution drops; other
|
|
764
|
-
* hooks at the same slot still ship.
|
|
765
|
-
*
|
|
766
|
-
* Plan + guide: docs/plans/render-hooks.md, docs/guide/render-hooks.md.
|
|
767
|
-
*/
|
|
768
|
-
renderHook(name: RenderHookName, fn: RenderHookFn, scope?: RenderHookScope): this {
|
|
769
|
-
if (!this.config.renderHooks) this.config.renderHooks = []
|
|
770
|
-
const entry: RenderHookEntry = { name, fn }
|
|
771
|
-
if (scope !== undefined) entry.scope = scope
|
|
772
|
-
this.config.renderHooks.push(entry)
|
|
773
|
-
return this
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Resolve the current user for a request. Internal helper called by
|
|
778
|
-
* routes + `panelInfo()`. Returns `null` when the resolver is unset or
|
|
779
|
-
* throws. Errors are swallowed deliberately — a failing user resolver
|
|
780
|
-
* should fail closed (no user) rather than 500 the page.
|
|
781
|
-
*/
|
|
782
|
-
async resolveUser(req?: unknown): Promise<unknown | null> {
|
|
783
|
-
if (!this.config.user) return null
|
|
784
|
-
try {
|
|
785
|
-
const u = await this.config.user(req)
|
|
786
|
-
return u ?? null
|
|
787
|
-
} catch {
|
|
788
|
-
return null
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
use(plugin: PilotiqPlugin): this {
|
|
793
|
-
this.installedPlugins.push(plugin)
|
|
794
|
-
plugin.register(this)
|
|
795
|
-
return this
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* Register multiple plugins in a single call. Each plugin's `register()`
|
|
800
|
-
* runs in array order — equivalent to chaining `.use(p)` per item.
|
|
801
|
-
*
|
|
802
|
-
* @example
|
|
803
|
-
* ```ts
|
|
804
|
-
* import { tiptap } from '@pilotiq/tiptap'
|
|
805
|
-
* import { codeEditor } from '@pilotiq/codemirror'
|
|
806
|
-
* import { recharts } from '@pilotiq/recharts'
|
|
807
|
-
* import { json } from '@codemirror/lang-json'
|
|
808
|
-
*
|
|
809
|
-
* Pilotiq.make('Admin')
|
|
810
|
-
* .plugins([
|
|
811
|
-
* tiptap(),
|
|
812
|
-
* codeEditor({ languages: { json } }),
|
|
813
|
-
* recharts(),
|
|
814
|
-
* ])
|
|
815
|
-
* ```
|
|
816
|
-
*/
|
|
817
|
-
plugins(list: PilotiqPlugin[]): this {
|
|
818
|
-
for (const plugin of list) this.use(plugin)
|
|
819
|
-
return this
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Register a single right-sidebar contribution. Plugins surface chat
|
|
824
|
-
* boxes, presence panels, document outlines, etc. through this — pilotiq
|
|
825
|
-
* core ships the sidebar chrome (collapse / resize / tab strip / mobile
|
|
826
|
-
* sheet); each contribution provides only its body component.
|
|
827
|
-
*
|
|
828
|
-
* @example
|
|
829
|
-
* ```ts
|
|
830
|
-
* Pilotiq.make('Admin').rightPanel({
|
|
831
|
-
* id: 'ai.chat',
|
|
832
|
-
* label: 'AI Assistant',
|
|
833
|
-
* icon: 'sparkles',
|
|
834
|
-
* render: AiChatBody,
|
|
835
|
-
* defaultWidth: 360,
|
|
836
|
-
* })
|
|
837
|
-
* ```
|
|
838
|
-
*
|
|
839
|
-
* Adapter packages typically call this from inside their plugin's
|
|
840
|
-
* `register(panel)` so consumers wire everything via `.plugins([…])`.
|
|
841
|
-
*
|
|
842
|
-
* @throws when `id` is missing/invalid, when `render` isn't a
|
|
843
|
-
* component, when `defaultWidth` is out of [240, 800], or when
|
|
844
|
-
* the same `id` was already registered.
|
|
845
|
-
*/
|
|
846
|
-
rightPanel(contribution: RightPanelContribution): this {
|
|
847
|
-
validateRightPanel(contribution)
|
|
848
|
-
const existing = this.config.rightPanels ?? []
|
|
849
|
-
const dup = findDuplicateRightPanelId(existing, contribution)
|
|
850
|
-
if (dup) {
|
|
851
|
-
throw new Error(
|
|
852
|
-
`[Pilotiq] rightPanel: contribution id "${contribution.id}" is already ` +
|
|
853
|
-
`registered. Each right-sidebar contribution must use a unique id.`,
|
|
854
|
-
)
|
|
855
|
-
}
|
|
856
|
-
this.config.rightPanels = [...existing, contribution]
|
|
857
|
-
return this
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
/**
|
|
861
|
-
* Bulk variant of {@link rightPanel}. Registers each contribution in
|
|
862
|
-
* array order; throws on the first invalid or duplicate id.
|
|
863
|
-
*
|
|
864
|
-
* @example
|
|
865
|
-
* ```ts
|
|
866
|
-
* Pilotiq.make('Admin').rightPanels([
|
|
867
|
-
* { id: 'ai.chat', render: AiChatBody },
|
|
868
|
-
* { id: 'outline', render: OutlineBody, defaultWidth: 280 },
|
|
869
|
-
* ])
|
|
870
|
-
* ```
|
|
871
|
-
*/
|
|
872
|
-
rightPanels(list: RightPanelContribution[]): this {
|
|
873
|
-
for (const c of list) this.rightPanel(c)
|
|
874
|
-
return this
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/** @internal — `panelInfo()` reads this to build `RightSidebarMeta`. */
|
|
878
|
-
getRightPanels(): readonly RightPanelContribution[] {
|
|
879
|
-
return this.config.rightPanels ?? []
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
/**
|
|
883
|
-
* Register a component that wraps the panel's `<AppShell>` children at
|
|
884
|
-
* the layout root. Plugins use this to mount React providers (AI chat
|
|
885
|
-
* queue context, tenant theme switcher, feature-flag overlay, …) so
|
|
886
|
-
* they're in scope for every page in the panel without consumers
|
|
887
|
-
* having to manually wrap their `+Layout.tsx`.
|
|
888
|
-
*
|
|
889
|
-
* Adapter packages typically call this from inside their plugin's
|
|
890
|
-
* `register(panel)` so consumers wire everything via `.plugins([…])`.
|
|
891
|
-
*
|
|
892
|
-
* Provider components receive `{ children, basePath? }` props.
|
|
893
|
-
* Registration order is preservation order: the first registered
|
|
894
|
-
* provider sits OUTERMOST (closest to the layout root); the last sits
|
|
895
|
-
* INNERMOST (closest to the page tree). When two providers depend on
|
|
896
|
-
* each other, register the producer first.
|
|
897
|
-
*
|
|
898
|
-
* @example
|
|
899
|
-
* ```ts
|
|
900
|
-
* // In a plugin's register(panel):
|
|
901
|
-
* panel.layoutProvider(({ children, basePath }) =>
|
|
902
|
-
* <AiUiProvider panelPath={basePath}>{children}</AiUiProvider>
|
|
903
|
-
* )
|
|
904
|
-
* ```
|
|
905
|
-
*
|
|
906
|
-
* @throws when `provider` isn't a function (component).
|
|
907
|
-
*/
|
|
908
|
-
layoutProvider(provider: LayoutProviderComponent): this {
|
|
909
|
-
if (typeof provider !== 'function') {
|
|
910
|
-
throw new Error(
|
|
911
|
-
`[Pilotiq] layoutProvider: expected a React component, got ${typeof provider}.`,
|
|
912
|
-
)
|
|
913
|
-
}
|
|
914
|
-
const existing = this.config.layoutProviders ?? []
|
|
915
|
-
this.config.layoutProviders = [...existing, provider]
|
|
916
|
-
return this
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
/**
|
|
920
|
-
* Bulk variant of {@link layoutProvider}. Registers each provider in
|
|
921
|
-
* array order.
|
|
922
|
-
*/
|
|
923
|
-
layoutProviders(list: LayoutProviderComponent[]): this {
|
|
924
|
-
for (const c of list) this.layoutProvider(c)
|
|
925
|
-
return this
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
/** @internal — read by the Vite plugin's `_components.ts` emitter. */
|
|
929
|
-
getLayoutProviders(): readonly LayoutProviderComponent[] {
|
|
930
|
-
return this.config.layoutProviders ?? []
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
/**
|
|
934
|
-
* Override one of pilotiq's built-in chrome slots with a custom React
|
|
935
|
-
* component. Calling twice merges — the latest registration wins per
|
|
936
|
-
* slot; unset keys preserve the prior value (so a plugin can override
|
|
937
|
-
* `nav` without clearing a host app's `footer`).
|
|
938
|
-
*
|
|
939
|
-
* Three slots ship today: `nav` (replaces the default nav tree),
|
|
940
|
-
* `header` (replaces the whole `<header>` chrome bar in both
|
|
941
|
-
* layouts — in `TopbarLayout` this subsumes the nav), and `footer`
|
|
942
|
-
* (mounts a `<footer>` below the main content area in both layouts).
|
|
943
|
-
* See {@link ComponentSlots} for the per-slot semantics and which
|
|
944
|
-
* render hooks compose vs. stop firing under each replacement.
|
|
945
|
-
*
|
|
946
|
-
* Component refs are harvested by the Vite plugin into
|
|
947
|
-
* `pages/(pilotiq)/_components.ts` — they never travel over the wire,
|
|
948
|
-
* so authoring inside the panel module (which is import-safe on both
|
|
949
|
-
* server and client) is the supported entry point.
|
|
950
|
-
*
|
|
951
|
-
* @example
|
|
952
|
-
* ```ts
|
|
953
|
-
* import { MyCustomSidebar } from './MyCustomSidebar.js'
|
|
954
|
-
*
|
|
955
|
-
* Pilotiq.make('Admin').components({ nav: MyCustomSidebar })
|
|
956
|
-
* ```
|
|
957
|
-
*
|
|
958
|
-
* The supplied component receives `{ navigation, basePath, currentPath }`
|
|
959
|
-
* — the same shape `panelInfo()` produces for the default renderers.
|
|
960
|
-
* Import `NavComponentProps` and `isNavItemActive` from
|
|
961
|
-
* `@pilotiq/pilotiq/react` to author a typed component that reuses
|
|
962
|
-
* the framework's longest-prefix active-link semantics.
|
|
963
|
-
*/
|
|
964
|
-
components(slots: ComponentSlots): this {
|
|
965
|
-
this.config.components = { ...(this.config.components ?? {}), ...slots }
|
|
966
|
-
return this
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/** @internal — read by the Vite plugin's `_components.ts` emitter. */
|
|
970
|
-
getComponentSlots(): Readonly<ComponentSlots> {
|
|
971
|
-
return this.config.components ?? {}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Set the panel-wide AI suggestion mode.
|
|
976
|
-
*
|
|
977
|
-
* - `'auto'` (default) — agent writes apply immediately.
|
|
978
|
-
* - `'review'` — agent writes stage as `PendingSuggestion`s; the user
|
|
979
|
-
* approves/rejects via inline diff (text) or value-comparison panel
|
|
980
|
-
* (non-text). Reuses the Phase 8.5 applier registry on approve.
|
|
981
|
-
*
|
|
982
|
-
* Plan: `docs/plans/ai-review-mode.md`.
|
|
983
|
-
*/
|
|
984
|
-
aiSuggestionsMode(mode: 'auto' | 'review'): this {
|
|
985
|
-
this.config.aiSuggestionsMode = mode
|
|
986
|
-
return this
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
/** @internal — read by `panelInfo()` and stamped onto the wire shape so
|
|
990
|
-
* the AI client tool knows which branch (apply vs queue) to take. */
|
|
991
|
-
getAiSuggestionsMode(): 'auto' | 'review' {
|
|
992
|
-
return this.config.aiSuggestionsMode ?? 'auto'
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
/** @internal */
|
|
996
|
-
enableThemeEditor(): void {
|
|
997
|
-
this.config.themeEditor = true
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/** @internal — assign the storage adapter resolved by the
|
|
1001
|
-
* `themeEditor({ storage })` plugin OR by the service provider's
|
|
1002
|
-
* back-compat Prisma fallback. Both writers funnel through this
|
|
1003
|
-
* setter so the route handlers consume a single slot. */
|
|
1004
|
-
_setThemeStorage(adapter: ThemeStorageAdapter | undefined): void {
|
|
1005
|
-
if (adapter === undefined) {
|
|
1006
|
-
delete this.config.themeStorage
|
|
1007
|
-
} else {
|
|
1008
|
-
this.config.themeStorage = adapter
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/** @internal — the active theme storage adapter (explicit or the
|
|
1013
|
-
* boot-time Prisma fallback). Routes read from here. */
|
|
1014
|
-
getThemeStorage(): ThemeStorageAdapter | undefined {
|
|
1015
|
-
return this.config.themeStorage
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
/** @internal */
|
|
1019
|
-
setThemeOverrides(overrides: Partial<ThemeConfig> | undefined): void {
|
|
1020
|
-
if (overrides === undefined) {
|
|
1021
|
-
delete this.config._themeOverrides
|
|
1022
|
-
} else {
|
|
1023
|
-
this.config._themeOverrides = overrides
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/** @internal — returns code defaults merged with DB overrides. Returns an
|
|
1028
|
-
* empty config when the theme editor is on so the built-in default preset
|
|
1029
|
-
* still resolves and the editor can persist overrides on top. */
|
|
1030
|
-
getMergedTheme(): ThemeConfig | undefined {
|
|
1031
|
-
const base = this.config.theme
|
|
1032
|
-
const overrides = this.config._themeOverrides
|
|
1033
|
-
if (!base && !overrides && !this.config.themeEditor) return undefined
|
|
1034
|
-
return { ...base, ...overrides }
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Slug-indexed lookup for resources. O(1) replacement for
|
|
1039
|
-
* `cfg.resources.find(r => r.getSlug() === slug)`. Built lazily on
|
|
1040
|
-
* first call; invalidated when `.resources([…])` is reassigned.
|
|
1041
|
-
*/
|
|
1042
|
-
findResource(slug: string): ResourceClass | undefined {
|
|
1043
|
-
if (!this._resourceBySlug) {
|
|
1044
|
-
this._resourceBySlug = new Map(this.config.resources.map(r => [r.getSlug(), r]))
|
|
1045
|
-
}
|
|
1046
|
-
return this._resourceBySlug.get(slug)
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
/** Slug-indexed lookup for globals. See `findResource`. */
|
|
1050
|
-
findGlobal(slug: string): GlobalClass | undefined {
|
|
1051
|
-
if (!this._globalBySlug) {
|
|
1052
|
-
this._globalBySlug = new Map(this.config.globals.map(g => [g.getSlug(), g]))
|
|
1053
|
-
}
|
|
1054
|
-
return this._globalBySlug.get(slug)
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/** Slug-indexed lookup for pages. See `findResource`. */
|
|
1058
|
-
findPage(slug: string): typeof Page | undefined {
|
|
1059
|
-
if (!this._pageBySlug) {
|
|
1060
|
-
this._pageBySlug = new Map(this.config.pages.map(p => [p.getSlug(), p]))
|
|
1061
|
-
}
|
|
1062
|
-
return this._pageBySlug.get(slug)
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/**
|
|
1066
|
-
* TTL (milliseconds) for the per-user navigation badge cache. Badges
|
|
1067
|
-
* resolve once per `(owner, userIdentity)` pair and serve from the
|
|
1068
|
-
* in-memory cache until the TTL elapses; the cache covers the
|
|
1069
|
-
* common case where a panel with N resources each running
|
|
1070
|
-
* `Model.count()` for a sidebar badge would otherwise issue N queries
|
|
1071
|
-
* on every page nav.
|
|
1072
|
-
*
|
|
1073
|
-
* Pass `0` (or `null`) to disable caching entirely. Default 30000.
|
|
1074
|
-
*/
|
|
1075
|
-
navigationBadgeTtl(ms: number | null): this {
|
|
1076
|
-
if (ms === null) {
|
|
1077
|
-
delete this.config.navigationBadgeTtlMs
|
|
1078
|
-
} else {
|
|
1079
|
-
this.config.navigationBadgeTtlMs = Math.max(0, ms)
|
|
1080
|
-
}
|
|
1081
|
-
// Bust on reconfigure so the new TTL doesn't reuse stale slots.
|
|
1082
|
-
this._navigationBadgeCache.clear()
|
|
1083
|
-
return this
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
/** @internal — resolved TTL in milliseconds. Default 30s. `0`
|
|
1087
|
-
* disables caching (each request re-resolves). */
|
|
1088
|
-
getNavigationBadgeTtl(): number {
|
|
1089
|
-
return this.config.navigationBadgeTtlMs ?? 30_000
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/** @internal — cache key for one (owner, user) pair. */
|
|
1093
|
-
navigationBadgeCacheKey(ownerName: string, user: unknown): string {
|
|
1094
|
-
return `${ownerName}|${userIdentityKey(user)}`
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
/** @internal — read-through cache for a single owner's badge value.
|
|
1098
|
-
* Caller supplies the resolver; cache wraps it with the configured
|
|
1099
|
-
* TTL. When TTL is 0 the resolver is invoked unconditionally and
|
|
1100
|
-
* nothing is stored. */
|
|
1101
|
-
async resolveNavigationBadge(
|
|
1102
|
-
ownerName: string,
|
|
1103
|
-
user: unknown,
|
|
1104
|
-
resolver: () => Promise<string | undefined>,
|
|
1105
|
-
): Promise<string | undefined> {
|
|
1106
|
-
const ttl = this.getNavigationBadgeTtl()
|
|
1107
|
-
if (ttl <= 0) return resolver()
|
|
1108
|
-
|
|
1109
|
-
const key = this.navigationBadgeCacheKey(ownerName, user)
|
|
1110
|
-
const now = Date.now()
|
|
1111
|
-
const hit = this._navigationBadgeCache.get(key)
|
|
1112
|
-
if (hit && hit.expires > now) return hit.value
|
|
1113
|
-
|
|
1114
|
-
const value = await resolver()
|
|
1115
|
-
this._navigationBadgeCache.set(key, { value, expires: now + ttl })
|
|
1116
|
-
return value
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
/** @internal — test seam; clears the per-user badge cache. */
|
|
1120
|
-
_clearNavigationBadgeCache(): void {
|
|
1121
|
-
this._navigationBadgeCache.clear()
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
/** @internal */
|
|
1125
|
-
getConfig(): Readonly<PilotiqConfig> {
|
|
1126
|
-
return this.config
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
/** @internal */
|
|
1130
|
-
getPlugins(): readonly PilotiqPlugin[] {
|
|
1131
|
-
return this.installedPlugins
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
/**
|
|
1136
|
-
* Stable cache key derived from a user object. Pilotiq treats the user
|
|
1137
|
-
* as opaque, so we sniff the common shapes:
|
|
1138
|
-
*
|
|
1139
|
-
* 1. `null` / `undefined` — anonymous request; everyone shares one slot.
|
|
1140
|
-
* 2. Primitive (string / number / bigint / boolean) — stringify directly.
|
|
1141
|
-
* 3. Object with `id` — `String(user.id)` (the 99% case for app-supplied users).
|
|
1142
|
-
* 4. Other objects — `JSON.stringify` as a last resort; falls back to a
|
|
1143
|
-
* sentinel if stringify throws (cycles).
|
|
1144
|
-
*
|
|
1145
|
-
* Two distinct users with the same `id` collide, but that's the same
|
|
1146
|
-
* collision the rest of the framework already trusts.
|
|
1147
|
-
*/
|
|
1148
|
-
function userIdentityKey(user: unknown): string {
|
|
1149
|
-
if (user === null || user === undefined) return ''
|
|
1150
|
-
const t = typeof user
|
|
1151
|
-
if (t === 'string' || t === 'number' || t === 'bigint' || t === 'boolean') return String(user)
|
|
1152
|
-
if (t === 'object') {
|
|
1153
|
-
const u = user as { id?: unknown }
|
|
1154
|
-
if (u.id !== undefined && u.id !== null) return String(u.id)
|
|
1155
|
-
try { return JSON.stringify(user) } catch { return '__opaque__' }
|
|
1156
|
-
}
|
|
1157
|
-
return '__opaque__'
|
|
1158
|
-
}
|