@pilotiq/pilotiq 0.24.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 +33 -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/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- 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 -225
- 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 -194
- 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/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 -704
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1243
- 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
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
import type { Pilotiq, PilotiqConfig } from '../Pilotiq.js'
|
|
2
|
-
import type { Page, PageCollabConfig } from '../Page.js'
|
|
3
|
-
import type { ResourceClass, NavigationBadgeColor, ResourceCollabConfig } from '../Resource.js'
|
|
4
|
-
import type { GlobalClass } from '../Global.js'
|
|
5
|
-
import type { ClusterClass } from '../Cluster.js'
|
|
6
|
-
import { resourceBasePath, globalBasePath, pageBasePath } from '../clusterPaths.js'
|
|
7
|
-
import type { ElementMeta } from '../schema/Element.js'
|
|
8
|
-
import { resolveSchema, type SchemaContext } from '../schema/resolveSchema.js'
|
|
9
|
-
import { resolveTheme } from '../theme/resolve.js'
|
|
10
|
-
import type { ThemeMeta } from '../theme/types.js'
|
|
11
|
-
import { serializeIcon, type SerializedIcon } from '../icons/types.js'
|
|
12
|
-
import {
|
|
13
|
-
RIGHT_PANEL_DEFAULT_WIDTH,
|
|
14
|
-
RIGHT_PANEL_MIN_WIDTH,
|
|
15
|
-
RIGHT_PANEL_MAX_WIDTH,
|
|
16
|
-
} from '../RightPanel.js'
|
|
17
|
-
import type { UserMenuItemMeta } from '../UserMenuItem.js'
|
|
18
|
-
import {
|
|
19
|
-
resolveRenderHooks,
|
|
20
|
-
CHROME_HOOK_NAMES,
|
|
21
|
-
type RenderHookContext,
|
|
22
|
-
type RenderHookMap,
|
|
23
|
-
type RenderHookName,
|
|
24
|
-
} from '../RenderHook.js'
|
|
25
|
-
import { applyPageHooks, pageHooksFor, type PageRole } from '../applyPageHooks.js'
|
|
26
|
-
import {
|
|
27
|
-
notificationChannel,
|
|
28
|
-
NOTIFICATION_CREATED_EVENT,
|
|
29
|
-
} from '../notifications/broadcast.js'
|
|
30
|
-
import { safeBool } from './helpers.js'
|
|
31
|
-
|
|
32
|
-
// ─── Navigation chrome ──────────────────────────────────────
|
|
33
|
-
//
|
|
34
|
-
// `panelInfo` is the entry point every SSR + SPA-nav data hook calls
|
|
35
|
-
// to build the static chrome envelope (branding / theme / navigation
|
|
36
|
-
// tree / user menu / database-notifications meta / right sidebar meta
|
|
37
|
-
// / page-role render hooks). Returns a snapshot that's safe to ship
|
|
38
|
-
// over the wire; references like component classes get rendered down
|
|
39
|
-
// to serializable shapes here.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// ─── Shared helpers ──────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Top-right user dropdown shipped to the renderer in `viewProps.panel`.
|
|
46
|
-
* `null` when no `Pilotiq.user(req => …)` resolver is configured or the
|
|
47
|
-
* resolver returns `null` (no logged-in user) — the renderer suppresses
|
|
48
|
-
* the dropdown entirely in that case.
|
|
49
|
-
*
|
|
50
|
-
* `user.name / user.email / user.avatar` are duck-typed off the
|
|
51
|
-
* resolver's return value; whichever fields are present round-trip into
|
|
52
|
-
* the dropdown trigger (initials fall back to the first two letters of
|
|
53
|
-
* `name` when no avatar URL is set).
|
|
54
|
-
*/
|
|
55
|
-
export interface UserMenuMeta {
|
|
56
|
-
user: { name?: string; email?: string; avatar?: string }
|
|
57
|
-
items: UserMenuItemMeta[]
|
|
58
|
-
signOut?: { url: string; label: string; method: 'POST' | 'GET' }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Bell-icon dropdown configuration shipped under `viewProps.panel`. Sparse —
|
|
63
|
-
* absent when `Pilotiq.databaseNotifications()` wasn't called OR when no
|
|
64
|
-
* user resolves (anonymous request → no inbox to surface). Renderer mounts
|
|
65
|
-
* the bell only when this is set.
|
|
66
|
-
*
|
|
67
|
-
* Routes are absolute URLs (panel `basePath` already applied). Client
|
|
68
|
-
* substitutes `:id` per row when calling read / unread; `_widget`-style
|
|
69
|
-
* params aren't used here because the bell only ever issues these four
|
|
70
|
-
* fetch shapes.
|
|
71
|
-
*
|
|
72
|
-
* `polling` mirrors `DatabaseNotificationsConfig.polling` — `null` ships
|
|
73
|
-
* over the wire to disable client-side polling. The bell still fetches on
|
|
74
|
-
* mount + after every mark-read mutation.
|
|
75
|
-
*/
|
|
76
|
-
export interface DatabaseNotificationsMeta {
|
|
77
|
-
position: 'topbar' | 'sidebar'
|
|
78
|
-
polling: number | null
|
|
79
|
-
pageSize: number
|
|
80
|
-
badgeColor: NavigationBadgeColor
|
|
81
|
-
trigger?: { icon?: string; label?: string }
|
|
82
|
-
listUrl: string
|
|
83
|
-
readAllUrl: string
|
|
84
|
-
/** Template URL with literal `:id` placeholder. Client replaces. */
|
|
85
|
-
readUrl: string
|
|
86
|
-
/** Template URL with literal `:id` placeholder. Client replaces. */
|
|
87
|
-
unreadUrl: string
|
|
88
|
-
/**
|
|
89
|
-
* Template URL for the notification-action dispatch endpoint with
|
|
90
|
-
* literal `:id` and `:actionName` placeholders. Bell client builds
|
|
91
|
-
* per-action URLs by substituting both at render time. Used only by
|
|
92
|
-
* `handler`-mode actions; `url` / `post` actions ride their own URL
|
|
93
|
-
* verbatim.
|
|
94
|
-
*/
|
|
95
|
-
actionUrl: string
|
|
96
|
-
/**
|
|
97
|
-
* Phase 2 — broadcast hint. Sparse — absent when
|
|
98
|
-
* `databaseNotifications({ broadcast: true })` wasn't set OR when no
|
|
99
|
-
* resolved user has an `id` to scope the channel to.
|
|
100
|
-
*
|
|
101
|
-
* Client connects to `wsUrl` via `@rudderjs/broadcast`'s
|
|
102
|
-
* `RudderSocket`, subscribes to the `channel` (already includes the
|
|
103
|
-
* `private-` prefix), and listens for `event` to trigger refetches.
|
|
104
|
-
*/
|
|
105
|
-
broadcast?: {
|
|
106
|
-
wsUrl: string
|
|
107
|
-
channel: string
|
|
108
|
-
event: string
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Right-sidebar shipped under `viewProps.panel.rightSidebar`. Sparse —
|
|
114
|
-
* absent from `panelInfo()` when no contributions are registered, every
|
|
115
|
-
* registered contribution failed `canAccess(user)`, or every visible
|
|
116
|
-
* contribution is `hidden: true`. Renderer mounts the chrome only when
|
|
117
|
-
* this is set.
|
|
118
|
-
*
|
|
119
|
-
* The React component reference for each contribution does NOT travel
|
|
120
|
-
* here — only its tab-strip metadata. The actual body component is
|
|
121
|
-
* resolved client-side from the Vite plugin's `_components.ts` manifest
|
|
122
|
-
* keyed by contribution `id`, mirroring the icon-class round-trip.
|
|
123
|
-
*
|
|
124
|
-
* `defaultWidth` rolls up: contribution-level value when one
|
|
125
|
-
* contribution was registered with one, otherwise the panel-level
|
|
126
|
-
* baseline (`RIGHT_PANEL_DEFAULT_WIDTH`). Client also clamps
|
|
127
|
-
* localStorage values to `[minWidth, maxWidth]`.
|
|
128
|
-
*/
|
|
129
|
-
export interface RightPanelMeta {
|
|
130
|
-
id: string
|
|
131
|
-
label: string
|
|
132
|
-
icon?: SerializedIcon
|
|
133
|
-
defaultWidth: number
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export interface RightSidebarMeta {
|
|
137
|
-
panels: RightPanelMeta[]
|
|
138
|
-
defaultWidth: number
|
|
139
|
-
minWidth: number
|
|
140
|
-
maxWidth: number
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Single nav-tree entry. `name` is the JS class name (`R.name` /
|
|
145
|
-
* `G.name` / `P.name`) — also the lookup key into the build-time
|
|
146
|
-
* `_components.ts` manifest the Vite plugin emits, so component-typed
|
|
147
|
-
* icons resolve from the same identifier.
|
|
148
|
-
*/
|
|
149
|
-
export interface NavItem {
|
|
150
|
-
name: string
|
|
151
|
-
label: string
|
|
152
|
-
url: string
|
|
153
|
-
icon?: SerializedIcon
|
|
154
|
-
group?: string
|
|
155
|
-
sort?: number
|
|
156
|
-
badge?: string
|
|
157
|
-
badgeColor?: NavigationBadgeColor
|
|
158
|
-
children?: NavItem[]
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Build the panel header summary + the unified navigation tree.
|
|
163
|
-
*
|
|
164
|
-
* Pipeline:
|
|
165
|
-
* 1. flatten resources + globals + pages into raw NavItem records
|
|
166
|
-
* 2. drop items whose `canAccess(user)` (Plan #10) returns false
|
|
167
|
-
* 3. resolve `navigationParentItem` references → nest under parents
|
|
168
|
-
* (cycles broken with a console warn; dangling parents render at top level)
|
|
169
|
-
* 4. sort within each grouping (top-level *and* every parent's children)
|
|
170
|
-
* by `navigationSort` ascending → registration order
|
|
171
|
-
* 5. resolve every `navigationBadge()` in parallel via `Promise.all`;
|
|
172
|
-
* handler errors are swallowed (badge omitted) so a flaky count
|
|
173
|
-
* never blanks the page
|
|
174
|
-
*
|
|
175
|
-
* `req` is the active request; pilotiq calls `pilotiq.resolveUser(req)`
|
|
176
|
-
* once and threads the user into every Resource/Global/Page `canAccess`
|
|
177
|
-
* check. When `Pilotiq.user(fn)` isn't configured, user is `null` and the
|
|
178
|
-
* default `canAccess` returns true → no items dropped.
|
|
179
|
-
*/
|
|
180
|
-
/**
|
|
181
|
-
* Optional route-context for `panelInfo()`. When set, render-hook
|
|
182
|
-
* `scope: { resource | page | global }` filters fire correctly for the
|
|
183
|
-
* active route. Missing keys mean the slot has no scope-able identifier
|
|
184
|
-
* (chrome-only routes); scope-less hooks still fire either way.
|
|
185
|
-
*
|
|
186
|
-
* `url` defaults to `cfg.path` when unset. `recordId` rides through to
|
|
187
|
-
* `RenderHookContext.recordId` for hooks that need it.
|
|
188
|
-
*/
|
|
189
|
-
export interface PanelInfoRoute {
|
|
190
|
-
resource?: ResourceClass
|
|
191
|
-
page?: typeof Page
|
|
192
|
-
global?: GlobalClass
|
|
193
|
-
recordId?: string
|
|
194
|
-
url?: string
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Per-resource collab opt-in map, keyed by the URL slug
|
|
199
|
-
* `parseRecordPageUrl` produces. Non-clustered resource → `getSlug()`;
|
|
200
|
-
* clustered resource → `${cluster.getSlug()}/${R.getSlug()}`.
|
|
201
|
-
*
|
|
202
|
-
* `RecordWrapperGate` reads this map to decide whether the page tree
|
|
203
|
-
* needs the plugin-registered RecordWrapper (collab room, audit, …)
|
|
204
|
-
* mounted around the record view/edit content area.
|
|
205
|
-
*
|
|
206
|
-
* Nested-relation edit URLs (`/articles/123/comments/456/edit`) have a
|
|
207
|
-
* dynamic-id segment in the gate's URL slug and don't match here in v1.
|
|
208
|
-
* Collab on nested-relation edits is a follow-up — top-level resource
|
|
209
|
-
* edits are the common case and ship now.
|
|
210
|
-
*/
|
|
211
|
-
export type RecordCollabMap = Record<string, ResourceCollabConfig>
|
|
212
|
-
|
|
213
|
-
function resourceSlugForGate(R: ResourceClass): string {
|
|
214
|
-
return R.cluster ? `${R.cluster.getSlug()}/${R.getSlug()}` : R.getSlug()
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function buildRecordCollabMap(cfg: Readonly<PilotiqConfig>): RecordCollabMap | undefined {
|
|
218
|
-
const map: RecordCollabMap = {}
|
|
219
|
-
for (const R of cfg.resources) {
|
|
220
|
-
const collab = R.getResolvedCollabConfig()
|
|
221
|
-
if (collab) map[resourceSlugForGate(R)] = collab
|
|
222
|
-
}
|
|
223
|
-
return Object.keys(map).length > 0 ? map : undefined
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Per-custom-page collab opt-in map, keyed by the page's URL slug
|
|
228
|
-
* (cluster-prefixed for clustered pages). `CustomPageWrapperGate`
|
|
229
|
-
* reads this to decide whether to mount the plugin-registered
|
|
230
|
-
* custom-page wrapper (collab room, audit trail, …) around the page
|
|
231
|
-
* content area.
|
|
232
|
-
*
|
|
233
|
-
* Resource-bound default pages (List/Create/Edit/View) never appear
|
|
234
|
-
* here — `Page.getResolvedCollabConfig()` returns `null` for them.
|
|
235
|
-
* Record-scoped collab is governed by `Resource.collab` and lands on
|
|
236
|
-
* `recordCollab`.
|
|
237
|
-
*/
|
|
238
|
-
export type PageCollabMap = Record<string, PageCollabConfig>
|
|
239
|
-
|
|
240
|
-
function pageSlugForGate(P: typeof Page): string {
|
|
241
|
-
const slug = P.getSlug()
|
|
242
|
-
return P.cluster ? `${P.cluster.getSlug()}/${slug}` : slug
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function buildPageCollabMap(cfg: Readonly<PilotiqConfig>): PageCollabMap | undefined {
|
|
246
|
-
const map: PageCollabMap = {}
|
|
247
|
-
for (const P of cfg.pages) {
|
|
248
|
-
const collab = P.getResolvedCollabConfig()
|
|
249
|
-
if (collab) map[pageSlugForGate(P)] = collab
|
|
250
|
-
}
|
|
251
|
-
return Object.keys(map).length > 0 ? map : undefined
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export async function panelInfo(
|
|
255
|
-
pilotiq: Pilotiq,
|
|
256
|
-
req?: unknown,
|
|
257
|
-
route: PanelInfoRoute = {},
|
|
258
|
-
) {
|
|
259
|
-
const cfg = pilotiq.getConfig()
|
|
260
|
-
const merged = pilotiq.getMergedTheme()
|
|
261
|
-
const theme: ThemeMeta | undefined = merged ? resolveTheme(merged) : undefined
|
|
262
|
-
const user = await pilotiq.resolveUser(req)
|
|
263
|
-
const [navigation, userMenu, renderHooks, rightSidebar] = await Promise.all([
|
|
264
|
-
buildNavigation(pilotiq, user),
|
|
265
|
-
buildUserMenu(pilotiq, user),
|
|
266
|
-
resolveChromeHooks(pilotiq, user, route),
|
|
267
|
-
buildRightSidebarMeta(cfg, user),
|
|
268
|
-
])
|
|
269
|
-
const databaseNotifications = buildDatabaseNotificationsMeta(cfg, user)
|
|
270
|
-
const recordCollab = buildRecordCollabMap(cfg)
|
|
271
|
-
const pageCollab = buildPageCollabMap(cfg)
|
|
272
|
-
// AI suggestion mode — sparse: omit when 'auto' (the default) so the
|
|
273
|
-
// wire shape stays minimal for panels that don't opt into review mode.
|
|
274
|
-
// Plugin clients (e.g. @pilotiq-pro/ai's `AiClientToolBindings`) read
|
|
275
|
-
// this to decide whether to apply writes immediately or stage them as
|
|
276
|
-
// PendingSuggestions for user approval.
|
|
277
|
-
const aiSuggestionsMode = pilotiq.getAiSuggestionsMode()
|
|
278
|
-
return {
|
|
279
|
-
name: cfg.name,
|
|
280
|
-
branding: cfg.branding,
|
|
281
|
-
navigation,
|
|
282
|
-
theme,
|
|
283
|
-
themeEditor: cfg.themeEditor ?? false,
|
|
284
|
-
...(userMenu ? { userMenu } : {}),
|
|
285
|
-
...(databaseNotifications ? { databaseNotifications } : {}),
|
|
286
|
-
...(rightSidebar ? { rightSidebar } : {}),
|
|
287
|
-
...(recordCollab ? { recordCollab } : {}),
|
|
288
|
-
...(pageCollab ? { pageCollab } : {}),
|
|
289
|
-
...(Object.keys(renderHooks).length > 0 ? { renderHooks } : {}),
|
|
290
|
-
...(aiSuggestionsMode !== 'auto' ? { aiSuggestionsMode } : {}),
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Build the bell-icon meta. Returns `null` when:
|
|
296
|
-
* - `Pilotiq.databaseNotifications()` was never called, OR
|
|
297
|
-
* - no user resolves (no inbox to surface).
|
|
298
|
-
*
|
|
299
|
-
* Defaults follow Filament: 30s polling, 25 rows per page, primary
|
|
300
|
-
* badge color, topbar position.
|
|
301
|
-
*/
|
|
302
|
-
export function buildDatabaseNotificationsMeta(
|
|
303
|
-
cfg: Readonly<PilotiqConfig>,
|
|
304
|
-
user: unknown,
|
|
305
|
-
): DatabaseNotificationsMeta | null {
|
|
306
|
-
if (!cfg.databaseNotifications?.enabled) return null
|
|
307
|
-
if (user === null || user === undefined) return null
|
|
308
|
-
|
|
309
|
-
const dn = cfg.databaseNotifications
|
|
310
|
-
const base = cfg.path
|
|
311
|
-
const meta: DatabaseNotificationsMeta = {
|
|
312
|
-
position: dn.position ?? 'topbar',
|
|
313
|
-
polling: dn.polling === null ? null : (dn.polling ?? 30),
|
|
314
|
-
pageSize: dn.pageSize ?? 25,
|
|
315
|
-
badgeColor: dn.badgeColor ?? 'primary',
|
|
316
|
-
listUrl: `${base}/_notifications`,
|
|
317
|
-
readAllUrl: `${base}/_notifications/read-all`,
|
|
318
|
-
readUrl: `${base}/_notifications/:id/read`,
|
|
319
|
-
unreadUrl: `${base}/_notifications/:id/unread`,
|
|
320
|
-
actionUrl: `${base}/_notifications/:id/_action/:actionName`,
|
|
321
|
-
}
|
|
322
|
-
if (dn.trigger) meta.trigger = { ...dn.trigger }
|
|
323
|
-
// Phase 2 broadcast hint — only ship when broadcast is enabled AND the
|
|
324
|
-
// resolved user has an `id` to scope the channel to. The client uses
|
|
325
|
-
// `wsUrl` for the WebSocket connection and `channel` for the subscribe
|
|
326
|
-
// call (the private- prefix is already baked in).
|
|
327
|
-
if (dn.broadcast) {
|
|
328
|
-
const userId = (user as { id?: unknown } | null | undefined)?.id
|
|
329
|
-
if (userId !== undefined && userId !== null) {
|
|
330
|
-
const wsUrl = typeof dn.broadcast === 'object' && dn.broadcast.wsUrl
|
|
331
|
-
? dn.broadcast.wsUrl
|
|
332
|
-
: '' // empty = client falls back to same-origin /ws
|
|
333
|
-
meta.broadcast = {
|
|
334
|
-
wsUrl,
|
|
335
|
-
channel: notificationChannel(String(userId)),
|
|
336
|
-
event: NOTIFICATION_CREATED_EVENT,
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return meta
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Build the right-sidebar meta from registered contributions. Returns
|
|
345
|
-
* `null` when:
|
|
346
|
-
*
|
|
347
|
-
* - no contributions were registered, OR
|
|
348
|
-
* - every contribution failed `canAccess(user)` (or its predicate
|
|
349
|
-
* threw — fail-closed), OR
|
|
350
|
-
* - every passing contribution is `hidden: true` (no tab-strip
|
|
351
|
-
* surface to mount; programmatic-open consumers should ship at
|
|
352
|
-
* least one visible tab).
|
|
353
|
-
*
|
|
354
|
-
* Visible contributions are sorted by `sort` ascending (default 100),
|
|
355
|
-
* with registration order as a stable tiebreaker. Each entry's icon is
|
|
356
|
-
* serialized through `serializeIcon` keyed on the contribution `id`
|
|
357
|
-
* (Phase B's Vite plugin extends `_components.ts` to round-trip
|
|
358
|
-
* component-typed icons under that key). `defaultWidth` rolls up:
|
|
359
|
-
* panel-level baseline is `RIGHT_PANEL_DEFAULT_WIDTH`; per-contribution
|
|
360
|
-
* overrides ride on `RightPanelMeta.defaultWidth`.
|
|
361
|
-
*
|
|
362
|
-
* Errors thrown by `canAccess` are swallowed (the contribution is
|
|
363
|
-
* dropped + a single console warn is emitted) so a flaky predicate on
|
|
364
|
-
* one pane never blanks the whole sidebar.
|
|
365
|
-
*/
|
|
366
|
-
export async function buildRightSidebarMeta(
|
|
367
|
-
cfg: Readonly<PilotiqConfig>,
|
|
368
|
-
user: unknown,
|
|
369
|
-
): Promise<RightSidebarMeta | null> {
|
|
370
|
-
const list = cfg.rightPanels ?? []
|
|
371
|
-
if (list.length === 0) return null
|
|
372
|
-
|
|
373
|
-
const indexed = list.map((c, idx) => ({ c, idx }))
|
|
374
|
-
const gated = await Promise.all(
|
|
375
|
-
indexed.map(async ({ c, idx }) => {
|
|
376
|
-
if (c.canAccess) {
|
|
377
|
-
try {
|
|
378
|
-
const ok = await c.canAccess(user)
|
|
379
|
-
if (!ok) return null
|
|
380
|
-
} catch (err) {
|
|
381
|
-
// eslint-disable-next-line no-console
|
|
382
|
-
console.warn(`[Pilotiq] rightPanel "${c.id}" canAccess threw — dropping`, err)
|
|
383
|
-
return null
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
return { c, idx }
|
|
387
|
-
}),
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
const visible = gated
|
|
391
|
-
.filter((x): x is { c: typeof list[number]; idx: number } => x !== null)
|
|
392
|
-
.filter((x) => !x.c.hidden)
|
|
393
|
-
.sort((a, b) => {
|
|
394
|
-
const sa = a.c.sort ?? 100
|
|
395
|
-
const sb = b.c.sort ?? 100
|
|
396
|
-
if (sa !== sb) return sa - sb
|
|
397
|
-
return a.idx - b.idx
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
if (visible.length === 0) return null
|
|
401
|
-
|
|
402
|
-
const panels: RightPanelMeta[] = visible.map(({ c }) => {
|
|
403
|
-
const meta: RightPanelMeta = {
|
|
404
|
-
id: c.id,
|
|
405
|
-
label: c.label ?? c.id,
|
|
406
|
-
defaultWidth: c.defaultWidth ?? RIGHT_PANEL_DEFAULT_WIDTH,
|
|
407
|
-
}
|
|
408
|
-
if (c.icon !== undefined) {
|
|
409
|
-
meta.icon = serializeIcon(c.icon, c.id)
|
|
410
|
-
}
|
|
411
|
-
return meta
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
return {
|
|
415
|
-
panels,
|
|
416
|
-
defaultWidth: panels[0]?.defaultWidth ?? RIGHT_PANEL_DEFAULT_WIDTH,
|
|
417
|
-
minWidth: RIGHT_PANEL_MIN_WIDTH,
|
|
418
|
-
maxWidth: RIGHT_PANEL_MAX_WIDTH,
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Resolve every chrome render hook (body / topbar / sidebar / user-menu
|
|
424
|
-
* / footer / head). Returns a sparse map — slots with no matching
|
|
425
|
-
* registered entries are omitted so the wire payload stays minimal on
|
|
426
|
-
* panels that don't use render hooks at all.
|
|
427
|
-
*/
|
|
428
|
-
export async function resolveChromeHooks(
|
|
429
|
-
pilotiq: Pilotiq,
|
|
430
|
-
user: unknown,
|
|
431
|
-
route: PanelInfoRoute,
|
|
432
|
-
): Promise<RenderHookMap> {
|
|
433
|
-
const cfg = pilotiq.getConfig()
|
|
434
|
-
const entries = cfg.renderHooks ?? []
|
|
435
|
-
if (entries.length === 0) return {}
|
|
436
|
-
const ctx: RenderHookContext = {
|
|
437
|
-
user,
|
|
438
|
-
basePath: cfg.path,
|
|
439
|
-
url: route.url ?? cfg.path,
|
|
440
|
-
}
|
|
441
|
-
if (route.resource !== undefined) ctx.resource = route.resource
|
|
442
|
-
if (route.page !== undefined) ctx.page = route.page
|
|
443
|
-
if (route.global !== undefined) ctx.global = route.global
|
|
444
|
-
if (route.recordId !== undefined) ctx.recordId = route.recordId
|
|
445
|
-
return resolveRenderHooks(entries, CHROME_HOOK_NAMES, ctx)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Resolve a subset of page-role render hooks (e.g. `panels::page.start`
|
|
450
|
-
* + the list-records / create-record / view-record / edit-record /
|
|
451
|
-
* global-search slot families). Per-page-role data builders call this
|
|
452
|
-
* after schema resolution and stamp the result on `viewProps.renderHooks`.
|
|
453
|
-
*
|
|
454
|
-
* `names` lets each builder declare exactly which slots it serves so a
|
|
455
|
-
* list-page builder doesn't ship slots that only fire on the edit page.
|
|
456
|
-
*/
|
|
457
|
-
/**
|
|
458
|
-
* Per-builder one-shot — resolve the role's slot set + splice the
|
|
459
|
-
* results into the resolved schema. Wraps the two steps a per-builder
|
|
460
|
-
* data fn always does in lockstep:
|
|
461
|
-
*
|
|
462
|
-
* 1. `resolvePageHooks(pilotiq, user, pageHooksFor(role), route)`
|
|
463
|
-
* 2. `applyPageHooks(schemaData, hooks, role)`
|
|
464
|
-
*
|
|
465
|
-
* Returns the wrapped `ElementMeta[]`. No-op when the panel has no
|
|
466
|
-
* registered hooks. Pass through what you'd pass to `panelInfo()`'s
|
|
467
|
-
* route arg — same shape.
|
|
468
|
-
*/
|
|
469
|
-
export async function applyRoleHooks(
|
|
470
|
-
pilotiq: Pilotiq,
|
|
471
|
-
user: unknown,
|
|
472
|
-
role: PageRole,
|
|
473
|
-
schemaData: ElementMeta[],
|
|
474
|
-
route: PanelInfoRoute = {},
|
|
475
|
-
): Promise<ElementMeta[]> {
|
|
476
|
-
const cfg = pilotiq.getConfig()
|
|
477
|
-
if (!cfg.renderHooks || cfg.renderHooks.length === 0) return schemaData
|
|
478
|
-
const hooks = await resolvePageHooks(pilotiq, user, pageHooksFor(role), route)
|
|
479
|
-
return applyPageHooks(schemaData, hooks, role)
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export async function resolvePageHooks(
|
|
483
|
-
pilotiq: Pilotiq,
|
|
484
|
-
user: unknown,
|
|
485
|
-
names: readonly RenderHookName[],
|
|
486
|
-
route: PanelInfoRoute,
|
|
487
|
-
): Promise<RenderHookMap> {
|
|
488
|
-
const cfg = pilotiq.getConfig()
|
|
489
|
-
const entries = cfg.renderHooks ?? []
|
|
490
|
-
if (entries.length === 0 || names.length === 0) return {}
|
|
491
|
-
const ctx: RenderHookContext = {
|
|
492
|
-
user,
|
|
493
|
-
basePath: cfg.path,
|
|
494
|
-
url: route.url ?? cfg.path,
|
|
495
|
-
}
|
|
496
|
-
if (route.resource !== undefined) ctx.resource = route.resource
|
|
497
|
-
if (route.page !== undefined) ctx.page = route.page
|
|
498
|
-
if (route.global !== undefined) ctx.global = route.global
|
|
499
|
-
if (route.recordId !== undefined) ctx.recordId = route.recordId
|
|
500
|
-
return resolveRenderHooks(entries, names, ctx)
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Build the top-right user-menu meta. Returns `null` when:
|
|
506
|
-
* - `Pilotiq.user()` isn't configured, or
|
|
507
|
-
* - the resolver returned `null` (anonymous request), or
|
|
508
|
-
* - the user object has no extractable identity AND the panel
|
|
509
|
-
* configured no items / no sign-out (nothing to render).
|
|
510
|
-
*
|
|
511
|
-
* Items resolve in parallel with their visibility predicates
|
|
512
|
-
* (`UserMenuItem.visible`). Throwing predicates fail closed (item
|
|
513
|
-
* dropped). Sort by `.sort(n)` ascending → registration order.
|
|
514
|
-
*/
|
|
515
|
-
export async function buildUserMenu(pilotiq: Pilotiq, user: unknown): Promise<UserMenuMeta | null> {
|
|
516
|
-
if (user === null || user === undefined) return null
|
|
517
|
-
|
|
518
|
-
const cfg = pilotiq.getConfig()
|
|
519
|
-
const items = cfg.userMenuItems ?? []
|
|
520
|
-
const ctx = { user }
|
|
521
|
-
|
|
522
|
-
// Resolve every item in parallel. `null` returns mean "filtered by
|
|
523
|
-
// visibility predicate" — drop them. Indexed pre-sort so stable ties
|
|
524
|
-
// resolve to registration order.
|
|
525
|
-
const resolved = await Promise.all(
|
|
526
|
-
items.map(async (item, idx) => {
|
|
527
|
-
try {
|
|
528
|
-
const meta = await item.resolve(ctx)
|
|
529
|
-
return meta ? { meta, idx, sort: item.getSort() } : null
|
|
530
|
-
} catch {
|
|
531
|
-
return null
|
|
532
|
-
}
|
|
533
|
-
}),
|
|
534
|
-
)
|
|
535
|
-
const visibleItems = resolved
|
|
536
|
-
.filter((x): x is { meta: UserMenuItemMeta; idx: number; sort: number | undefined } => x !== null)
|
|
537
|
-
.sort((a, b) => {
|
|
538
|
-
const aHas = a.sort !== undefined, bHas = b.sort !== undefined
|
|
539
|
-
if (aHas && bHas) return a.sort! - b.sort! || a.idx - b.idx
|
|
540
|
-
if (aHas) return -1
|
|
541
|
-
if (bHas) return 1
|
|
542
|
-
return a.idx - b.idx
|
|
543
|
-
})
|
|
544
|
-
.map(x => x.meta)
|
|
545
|
-
|
|
546
|
-
// Auto-inject the profile entry from `cfg.profilePage` when set.
|
|
547
|
-
// Prepended (Filament-style) so it always sits at the top of the
|
|
548
|
-
// dropdown regardless of user-authored item ordering. Falls through
|
|
549
|
-
// its own `canAccess(user)` so per-user gating works without the
|
|
550
|
-
// user repeating the predicate at the menu level.
|
|
551
|
-
const profileItem = await buildProfileMenuItem(cfg, user)
|
|
552
|
-
const finalItems = profileItem ? [profileItem, ...visibleItems] : visibleItems
|
|
553
|
-
|
|
554
|
-
const meta: UserMenuMeta = {
|
|
555
|
-
user: extractUserIdentity(user),
|
|
556
|
-
items: finalItems,
|
|
557
|
-
}
|
|
558
|
-
if (cfg.signOut) {
|
|
559
|
-
meta.signOut = {
|
|
560
|
-
url: cfg.signOut.url,
|
|
561
|
-
label: cfg.signOut.label ?? 'Sign out',
|
|
562
|
-
method: cfg.signOut.method ?? 'POST',
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return meta
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/** Build the auto-injected profile entry from `cfg.profilePage`. The
|
|
569
|
-
* Page's `static label` / `static icon` win; defaults `'Edit profile'`
|
|
570
|
-
* + `'user-circle'` (registry-resolved). Returns `null` when no
|
|
571
|
-
* profile page is configured or `Page.canAccess(user)` denies. */
|
|
572
|
-
export async function buildProfileMenuItem(
|
|
573
|
-
cfg: Readonly<PilotiqConfig>,
|
|
574
|
-
user: unknown,
|
|
575
|
-
): Promise<UserMenuItemMeta | null> {
|
|
576
|
-
const P = cfg.profilePage
|
|
577
|
-
if (!P) return null
|
|
578
|
-
if (!(await safeBool(() => P.canAccess(user)))) return null
|
|
579
|
-
const url = pageBasePath(cfg.path, P)
|
|
580
|
-
const icon = serializeIcon(P.icon ?? 'user-circle', P.name)
|
|
581
|
-
const meta: UserMenuItemMeta = {
|
|
582
|
-
name: '__profile',
|
|
583
|
-
label: P.label ?? 'Edit profile',
|
|
584
|
-
url,
|
|
585
|
-
}
|
|
586
|
-
if (icon !== undefined) meta.icon = icon
|
|
587
|
-
return meta
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/** Duck-type the user object for display fields. We never throw — a
|
|
591
|
-
* user resolver might return literally anything (a primitive, a class
|
|
592
|
-
* instance with getters, a plain object) and the dropdown should
|
|
593
|
-
* degrade gracefully (initials fallback to '?' when no name found). */
|
|
594
|
-
export function extractUserIdentity(user: unknown): { name?: string; email?: string; avatar?: string } {
|
|
595
|
-
if (user === null || user === undefined) return {}
|
|
596
|
-
if (typeof user !== 'object') return { name: String(user) }
|
|
597
|
-
const obj = user as Record<string, unknown>
|
|
598
|
-
const out: { name?: string; email?: string; avatar?: string } = {}
|
|
599
|
-
const name = obj.name ?? obj.fullName ?? obj.displayName ?? obj.username
|
|
600
|
-
if (typeof name === 'string' && name) out.name = name
|
|
601
|
-
if (typeof obj.email === 'string' && obj.email) out.email = obj.email
|
|
602
|
-
const avatar = obj.avatar ?? obj.avatarUrl ?? obj.image
|
|
603
|
-
if (typeof avatar === 'string' && avatar) out.avatar = avatar
|
|
604
|
-
return out
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/** @internal Internal node before nesting; carries the registration index
|
|
608
|
-
* so we can stable-sort by it as the tie-breaker. */
|
|
609
|
-
interface RawNavItem extends NavItem {
|
|
610
|
-
parent?: string
|
|
611
|
-
/** Registration index across resources → globals → pages (in that order),
|
|
612
|
-
* so resources beat globals on a sort tie within the same group. */
|
|
613
|
-
_idx: number
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/** Plan #10 — stamp the resolved user onto a SchemaContext so action
|
|
617
|
-
* visibility predicates can see it during `resolveSchema`. The `user`
|
|
618
|
-
* field is opaque (whatever `Pilotiq.user(req => …)` returns); skipped
|
|
619
|
-
* when null/undefined to keep ctx tidy. */
|
|
620
|
-
export async function buildNavigation(pilotiq: Pilotiq, user: unknown): Promise<NavItem[]> {
|
|
621
|
-
const cfg = pilotiq.getConfig()
|
|
622
|
-
const base = cfg.path
|
|
623
|
-
|
|
624
|
-
// Flatten + resolve badges in parallel. We build the raw list first so
|
|
625
|
-
// every entry has its identity (`name`) and parent set; badges resolve
|
|
626
|
-
// alongside.
|
|
627
|
-
const raw: RawNavItem[] = []
|
|
628
|
-
let idx = 0
|
|
629
|
-
|
|
630
|
-
const pushBadge: Array<{ item: RawNavItem; handler: () => unknown; owner: string }> = []
|
|
631
|
-
|
|
632
|
-
// Plan #10 — pre-evaluate canAccess for every owner in parallel so we
|
|
633
|
-
// can drop forbidden items before flattening. Failed predicates fail
|
|
634
|
-
// closed (treated as `false`) so a thrown auth check doesn't accidentally
|
|
635
|
-
// expose nav items. Clusters compose: a child gated through its
|
|
636
|
-
// cluster's `canAccess` returning false drops the child even when the
|
|
637
|
-
// child's own predicate would have passed.
|
|
638
|
-
const [resourceAccess, globalAccess, pageAccess, clusterAccess] = await Promise.all([
|
|
639
|
-
Promise.all(cfg.resources.map(R => safeBool(() => R.canAccess(user)))),
|
|
640
|
-
Promise.all(cfg.globals.map(G => safeBool(() => G.canAccess(user)))),
|
|
641
|
-
Promise.all(cfg.pages.map(P => safeBool(() => P.canAccess(user)))),
|
|
642
|
-
Promise.all(cfg.clusters.map(C => safeBool(() => C.canAccess(user)))),
|
|
643
|
-
])
|
|
644
|
-
|
|
645
|
-
// Identity-keyed so two clusters that happen to share a `.name`
|
|
646
|
-
// (minifier collisions, hot-reload duplicate imports) don't clobber.
|
|
647
|
-
const clusterAccessByClass = new Map<ClusterClass, boolean>()
|
|
648
|
-
cfg.clusters.forEach((C, i) => clusterAccessByClass.set(C, !!clusterAccess[i]))
|
|
649
|
-
|
|
650
|
-
const firstChildUrlByCluster = new Map<ClusterClass, string>()
|
|
651
|
-
const recordChildUrl = (cluster: ClusterClass, url: string) => {
|
|
652
|
-
if (!firstChildUrlByCluster.has(cluster)) firstChildUrlByCluster.set(cluster, url)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
for (let i = 0; i < cfg.resources.length; i++) {
|
|
656
|
-
const R = cfg.resources[i]!
|
|
657
|
-
if (!resourceAccess[i]) continue
|
|
658
|
-
if (R.cluster && !clusterAccessByClass.get(R.cluster)) continue
|
|
659
|
-
const url = resourceBasePath(base, R)
|
|
660
|
-
if (R.cluster) recordChildUrl(R.cluster, url)
|
|
661
|
-
const item: RawNavItem = {
|
|
662
|
-
name: R.name,
|
|
663
|
-
label: R.getNavigationLabel(),
|
|
664
|
-
url,
|
|
665
|
-
icon: serializeIcon(R.getNavigationIcon(), R.name),
|
|
666
|
-
_idx: idx++,
|
|
667
|
-
}
|
|
668
|
-
if (R.navigationGroup !== undefined) item.group = R.navigationGroup
|
|
669
|
-
if (R.navigationSort !== undefined) item.sort = R.navigationSort
|
|
670
|
-
// Cluster nesting wins over `navigationParentItem`. Both being set
|
|
671
|
-
// is a misconfiguration; cluster placement is the structural one.
|
|
672
|
-
if (R.cluster) item.parent = R.cluster.name
|
|
673
|
-
else if (R.navigationParentItem !== undefined) item.parent = R.navigationParentItem
|
|
674
|
-
if (R.navigationBadgeColor !== 'default') item.badgeColor = R.navigationBadgeColor
|
|
675
|
-
if (R.navigationBadge) pushBadge.push({ item, handler: R.navigationBadge, owner: R.name })
|
|
676
|
-
raw.push(item)
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
for (let i = 0; i < cfg.globals.length; i++) {
|
|
680
|
-
if (!globalAccess[i]) continue
|
|
681
|
-
const G = cfg.globals[i]!
|
|
682
|
-
if (G.cluster && !clusterAccessByClass.get(G.cluster)) continue
|
|
683
|
-
// Globals default `navigationGroup` to `'Settings'`. Allow `null` as
|
|
684
|
-
// an explicit opt-out → render at top level.
|
|
685
|
-
const group = G.navigationGroup === null ? undefined : G.navigationGroup
|
|
686
|
-
const url = globalBasePath(base, G)
|
|
687
|
-
if (G.cluster) recordChildUrl(G.cluster, url)
|
|
688
|
-
const item: RawNavItem = {
|
|
689
|
-
name: G.name,
|
|
690
|
-
label: G.getNavigationLabel(),
|
|
691
|
-
url,
|
|
692
|
-
icon: serializeIcon(G.getNavigationIcon(), G.name),
|
|
693
|
-
_idx: idx++,
|
|
694
|
-
}
|
|
695
|
-
if (group !== undefined) item.group = group
|
|
696
|
-
if (G.navigationSort !== undefined) item.sort = G.navigationSort
|
|
697
|
-
if (G.cluster) item.parent = G.cluster.name
|
|
698
|
-
else if (G.navigationParentItem !== undefined) item.parent = G.navigationParentItem
|
|
699
|
-
if (G.navigationBadgeColor !== 'default') item.badgeColor = G.navigationBadgeColor
|
|
700
|
-
if (G.navigationBadge) pushBadge.push({ item, handler: G.navigationBadge, owner: G.name })
|
|
701
|
-
raw.push(item)
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
for (let i = 0; i < cfg.pages.length; i++) {
|
|
705
|
-
if (!pageAccess[i]) continue
|
|
706
|
-
const P = cfg.pages[i]!
|
|
707
|
-
if (P.cluster && !clusterAccessByClass.get(P.cluster)) continue
|
|
708
|
-
// The dashboard page collapses its nav URL to `${base}` so the
|
|
709
|
-
// sidebar entry deep-links to the panel root rather than
|
|
710
|
-
// `${base}/${P.getSlug()}` (which would 404 — the slug route skips
|
|
711
|
-
// the dashboard page at boot).
|
|
712
|
-
const isDashboard = cfg.dashboardPage === P
|
|
713
|
-
const url = isDashboard ? base : pageBasePath(base, P)
|
|
714
|
-
if (P.cluster && !isDashboard) recordChildUrl(P.cluster, url)
|
|
715
|
-
const item: RawNavItem = {
|
|
716
|
-
name: P.name,
|
|
717
|
-
label: P.getNavigationLabel(),
|
|
718
|
-
url,
|
|
719
|
-
icon: serializeIcon(P.getNavigationIcon(), P.name),
|
|
720
|
-
_idx: idx++,
|
|
721
|
-
}
|
|
722
|
-
if (P.navigationGroup !== undefined) item.group = P.navigationGroup
|
|
723
|
-
if (P.navigationSort !== undefined) item.sort = P.navigationSort
|
|
724
|
-
if (P.cluster && !isDashboard) item.parent = P.cluster.name
|
|
725
|
-
else if (P.navigationParentItem !== undefined) item.parent = P.navigationParentItem
|
|
726
|
-
if (P.navigationBadgeColor !== 'default') item.badgeColor = P.navigationBadgeColor
|
|
727
|
-
if (P.navigationBadge) pushBadge.push({ item, handler: P.navigationBadge, owner: P.name })
|
|
728
|
-
raw.push(item)
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Clusters render as first-class nav items. Each gets a URL pointing
|
|
732
|
-
// at its `landingPage` (when set + accessible) or its first accessible
|
|
733
|
-
// child. Clusters whose every child was gated out are dropped silently
|
|
734
|
-
// — same posture as `navigationParentItem` with no resolvable parent.
|
|
735
|
-
for (let i = 0; i < cfg.clusters.length; i++) {
|
|
736
|
-
if (!clusterAccess[i]) continue
|
|
737
|
-
const C = cfg.clusters[i]!
|
|
738
|
-
let url: string | undefined
|
|
739
|
-
if (C.landingPage) {
|
|
740
|
-
const lpIdx = cfg.pages.indexOf(C.landingPage)
|
|
741
|
-
if (lpIdx !== -1 && pageAccess[lpIdx]) {
|
|
742
|
-
url = cfg.dashboardPage === C.landingPage ? base : pageBasePath(base, C.landingPage)
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
if (url === undefined) url = firstChildUrlByCluster.get(C)
|
|
746
|
-
if (url === undefined) continue // empty cluster — drop entirely
|
|
747
|
-
const item: RawNavItem = {
|
|
748
|
-
name: C.name,
|
|
749
|
-
label: C.getNavigationLabel(),
|
|
750
|
-
url,
|
|
751
|
-
icon: serializeIcon(C.getNavigationIcon(), C.name),
|
|
752
|
-
_idx: idx++,
|
|
753
|
-
}
|
|
754
|
-
if (C.navigationGroup !== undefined) item.group = C.navigationGroup
|
|
755
|
-
if (C.navigationSort !== undefined) item.sort = C.navigationSort
|
|
756
|
-
if (C.navigationParentItem !== undefined) item.parent = C.navigationParentItem
|
|
757
|
-
if (C.navigationBadgeColor !== 'default') item.badgeColor = C.navigationBadgeColor
|
|
758
|
-
if (C.navigationBadge) pushBadge.push({ item, handler: C.navigationBadge, owner: C.name })
|
|
759
|
-
raw.push(item)
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
await Promise.all(pushBadge.map(async ({ item, handler, owner }) => {
|
|
763
|
-
try {
|
|
764
|
-
const v = await pilotiq.resolveNavigationBadge(owner, user, async () => {
|
|
765
|
-
const raw = await handler()
|
|
766
|
-
return raw === undefined || raw === null ? undefined : String(raw)
|
|
767
|
-
})
|
|
768
|
-
if (v !== undefined) item.badge = v
|
|
769
|
-
} catch {
|
|
770
|
-
// Per-badge errors stay silent.
|
|
771
|
-
}
|
|
772
|
-
}))
|
|
773
|
-
|
|
774
|
-
return nestAndSort(raw)
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
/**
|
|
778
|
-
* Resolve `parent` references → nest, drop cycles, sort within each
|
|
779
|
-
* grouping, then strip internal scaffolding (`parent`, `_idx`).
|
|
780
|
-
*/
|
|
781
|
-
export function nestAndSort(raw: RawNavItem[]): NavItem[] {
|
|
782
|
-
const byName = new Map<string, RawNavItem>()
|
|
783
|
-
for (const it of raw) byName.set(it.name, it)
|
|
784
|
-
|
|
785
|
-
// Detect parent cycles: walk upwards from each item; any name seen
|
|
786
|
-
// twice → cycle. Items in a cycle get treated as top-level.
|
|
787
|
-
const inCycle = new Set<string>()
|
|
788
|
-
for (const it of raw) {
|
|
789
|
-
if (it.parent === undefined) continue
|
|
790
|
-
const seen = new Set<string>([it.name])
|
|
791
|
-
let cur: string | undefined = it.parent
|
|
792
|
-
while (cur !== undefined) {
|
|
793
|
-
if (seen.has(cur)) {
|
|
794
|
-
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
|
|
795
|
-
console.warn(`[Pilotiq] navigationParentItem cycle detected at "${it.name}" — rendering at top level.`)
|
|
796
|
-
}
|
|
797
|
-
inCycle.add(it.name)
|
|
798
|
-
break
|
|
799
|
-
}
|
|
800
|
-
seen.add(cur)
|
|
801
|
-
const parent = byName.get(cur)
|
|
802
|
-
if (!parent) break
|
|
803
|
-
cur = parent.parent
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
const childrenOf = new Map<string, RawNavItem[]>()
|
|
808
|
-
const top: RawNavItem[] = []
|
|
809
|
-
for (const it of raw) {
|
|
810
|
-
const parent = it.parent
|
|
811
|
-
if (parent && byName.has(parent) && !inCycle.has(it.name)) {
|
|
812
|
-
const list = childrenOf.get(parent) ?? []
|
|
813
|
-
list.push(it)
|
|
814
|
-
childrenOf.set(parent, list)
|
|
815
|
-
} else {
|
|
816
|
-
top.push(it)
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// Sort items in a sibling group by sort (asc), ties → registration order.
|
|
821
|
-
const sortItems = (items: RawNavItem[]): RawNavItem[] => {
|
|
822
|
-
return [...items].sort((a, b) => {
|
|
823
|
-
const aHas = a.sort !== undefined, bHas = b.sort !== undefined
|
|
824
|
-
if (aHas && bHas) return a.sort! - b.sort! || a._idx - b._idx
|
|
825
|
-
if (aHas) return -1 // sorted items come before unsorted
|
|
826
|
-
if (bHas) return 1
|
|
827
|
-
return a._idx - b._idx
|
|
828
|
-
})
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// Strip internals + recurse into children.
|
|
832
|
-
const finalize = (items: RawNavItem[]): NavItem[] =>
|
|
833
|
-
sortItems(items).map(it => {
|
|
834
|
-
const kids = childrenOf.get(it.name)
|
|
835
|
-
const { parent, _idx, ...rest } = it
|
|
836
|
-
const out: NavItem = { ...rest }
|
|
837
|
-
if (kids && kids.length > 0) out.children = finalize(kids)
|
|
838
|
-
return out
|
|
839
|
-
})
|
|
840
|
-
|
|
841
|
-
return finalize(top)
|
|
842
|
-
}
|