@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/schema/sanitize.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type sanitizeHtmlNs from 'sanitize-html'
|
|
2
|
-
|
|
3
|
-
export type SanitizeConfig = sanitizeHtmlNs.IOptions
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Default-secure allowlist for `Markdown` and `Html` primes — covers prose
|
|
7
|
-
* content (headings, lists, code, links, images, tables) while blocking
|
|
8
|
-
* `<script>` / `<iframe>`, inline event handlers, `javascript:` URIs, and
|
|
9
|
-
* `style` attribute overrides that could break the surrounding admin chrome.
|
|
10
|
-
*
|
|
11
|
-
* Pass a custom config to `Markdown.sanitize(...)` / `Html.sanitize(...)`
|
|
12
|
-
* when an admin-trusted source needs a wider allowlist (e.g. legacy CMS
|
|
13
|
-
* HTML with embedded media, or a custom block highlighter that emits
|
|
14
|
-
* `style="color:..."` on inline spans).
|
|
15
|
-
*/
|
|
16
|
-
export const DEFAULT_SANITIZE_CONFIG: SanitizeConfig = {
|
|
17
|
-
allowedTags: [
|
|
18
|
-
'p', 'br', 'hr', 'span', 'div',
|
|
19
|
-
'strong', 'em', 'b', 'i', 'u', 's', 'del', 'ins', 'mark',
|
|
20
|
-
'sub', 'sup', 'small', 'abbr', 'kbd',
|
|
21
|
-
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
22
|
-
'ul', 'ol', 'li',
|
|
23
|
-
'blockquote', 'pre', 'code',
|
|
24
|
-
'a', 'img',
|
|
25
|
-
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
|
|
26
|
-
],
|
|
27
|
-
allowedAttributes: {
|
|
28
|
-
a: ['href', 'title', 'rel', 'target', 'name'],
|
|
29
|
-
img: ['src', 'alt', 'title', 'width', 'height'],
|
|
30
|
-
abbr: ['title'],
|
|
31
|
-
th: ['colspan', 'rowspan', 'scope'],
|
|
32
|
-
td: ['colspan', 'rowspan'],
|
|
33
|
-
code: ['class'],
|
|
34
|
-
pre: ['class'],
|
|
35
|
-
},
|
|
36
|
-
allowedSchemes: ['http', 'https', 'mailto', 'tel'],
|
|
37
|
-
allowedSchemesByTag: { img: ['http', 'https', 'data'] },
|
|
38
|
-
allowProtocolRelative: false,
|
|
39
|
-
disallowedTagsMode: 'discard',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Lazy import — keeps `sanitize-html` (and its transitive `postcss` chain that
|
|
43
|
-
// reaches into Node built-ins) out of the client bundle. Cached after first
|
|
44
|
-
// resolve so per-call overhead is just the `sanitize-html` invocation itself.
|
|
45
|
-
let sanitizerPromise: Promise<typeof sanitizeHtmlNs> | null = null
|
|
46
|
-
function loadSanitizer(): Promise<typeof sanitizeHtmlNs> {
|
|
47
|
-
return sanitizerPromise ??= import('sanitize-html').then(m => m.default ?? m)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Sanitizes an HTML string against `DEFAULT_SANITIZE_CONFIG` (or a
|
|
52
|
-
* caller-supplied config). Server-side only — never call from a renderer,
|
|
53
|
-
* since the wire shape ships pre-rendered HTML.
|
|
54
|
-
*/
|
|
55
|
-
export async function sanitizeHtml(html: string, config?: SanitizeConfig): Promise<string> {
|
|
56
|
-
const sanitizer = await loadSanitizer()
|
|
57
|
-
return sanitizer(html, config ?? DEFAULT_SANITIZE_CONFIG)
|
|
58
|
-
}
|
package/src/search.test.ts
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { Pilotiq } from './Pilotiq.js'
|
|
5
|
-
import { Resource } from './Resource.js'
|
|
6
|
-
import { Column } from './Column.js'
|
|
7
|
-
import { Table } from './elements/Table.js'
|
|
8
|
-
import type { ModelLike, ModelQuery, ModelWhereOperator } from './orm/modelDefaults.js'
|
|
9
|
-
import { searchAllResources } from './search.js'
|
|
10
|
-
import { searchData } from './pageData.js'
|
|
11
|
-
import { Heading } from './schema/Heading.js'
|
|
12
|
-
import { Text } from './schema/Text.js'
|
|
13
|
-
|
|
14
|
-
// ─── Fake ModelQuery / ModelLike ────────────────────────────
|
|
15
|
-
|
|
16
|
-
interface FakeRow { id: string | number; [key: string]: unknown }
|
|
17
|
-
|
|
18
|
-
function fakeModel(rows: FakeRow[]): ModelLike & {
|
|
19
|
-
recordedQueries: Array<{ wheres: Array<[string, ModelWhereOperator | unknown, unknown?]>; limit?: number }>
|
|
20
|
-
} {
|
|
21
|
-
const recordedQueries: Array<{ wheres: Array<[string, ModelWhereOperator | unknown, unknown?]>; limit?: number }> = []
|
|
22
|
-
|
|
23
|
-
const M: ModelLike & { recordedQueries: typeof recordedQueries } = {
|
|
24
|
-
primaryKey: 'id',
|
|
25
|
-
async find(id) { return rows.find(r => String(r['id']) === String(id)) ?? null },
|
|
26
|
-
async create(data) { return data },
|
|
27
|
-
async update(_id, data) { return data },
|
|
28
|
-
async delete(_id) {},
|
|
29
|
-
query(): ModelQuery {
|
|
30
|
-
const wheres: Array<[string, ModelWhereOperator | unknown, unknown?]> = []
|
|
31
|
-
const matches = (row: FakeRow): boolean => {
|
|
32
|
-
// OR-of-LIKE only for our tests; column LIKE '%needle%'
|
|
33
|
-
if (wheres.length === 0) return true
|
|
34
|
-
return wheres.some(([col, op, val]) => {
|
|
35
|
-
const haystack = String(row[col as string] ?? '').toLowerCase()
|
|
36
|
-
if (op === 'LIKE') {
|
|
37
|
-
const needle = String(val).replace(/^%|%$/g, '').toLowerCase()
|
|
38
|
-
return haystack.includes(needle)
|
|
39
|
-
}
|
|
40
|
-
if (op === undefined) {
|
|
41
|
-
return String(row[col as string] ?? '') === String(val)
|
|
42
|
-
}
|
|
43
|
-
return false
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
const q: ModelQuery = {
|
|
47
|
-
where(col: string, opOrVal: unknown, val?: unknown): ModelQuery {
|
|
48
|
-
wheres.push([col, val !== undefined ? opOrVal : undefined, val !== undefined ? val : opOrVal])
|
|
49
|
-
return q
|
|
50
|
-
},
|
|
51
|
-
orWhere(col: string, opOrVal: unknown, val?: unknown): ModelQuery {
|
|
52
|
-
wheres.push([col, val !== undefined ? opOrVal : undefined, val !== undefined ? val : opOrVal])
|
|
53
|
-
return q
|
|
54
|
-
},
|
|
55
|
-
orderBy(_col: string): ModelQuery { return q },
|
|
56
|
-
async paginate(page: number, perPage = 10) {
|
|
57
|
-
recordedQueries.push({ wheres: [...wheres], limit: perPage })
|
|
58
|
-
const matched = rows.filter(matches)
|
|
59
|
-
const start = (page - 1) * perPage
|
|
60
|
-
return { data: matched.slice(start, start + perPage), total: matched.length }
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
return q
|
|
64
|
-
},
|
|
65
|
-
recordedQueries,
|
|
66
|
-
}
|
|
67
|
-
return M
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─── Tests ──────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
describe('searchAllResources (Plan #12)', () => {
|
|
73
|
-
it('returns [] for queries shorter than 2 chars', async () => {
|
|
74
|
-
const M = fakeModel([{ id: '1', title: 'Hello' }])
|
|
75
|
-
class A extends Resource {
|
|
76
|
-
static override label = 'Articles'
|
|
77
|
-
static override slug = 'articles'
|
|
78
|
-
static override globalSearch = true
|
|
79
|
-
static override recordTitleAttribute = 'title'
|
|
80
|
-
static override model = M
|
|
81
|
-
static override table(table: Table): Table {
|
|
82
|
-
return table.columns([Column.make('title').searchable()])
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
86
|
-
assert.deepEqual(await searchAllResources(pilotiq, '', null), [])
|
|
87
|
-
assert.deepEqual(await searchAllResources(pilotiq, 'a', null), [])
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('skips resources with globalSearch=false', async () => {
|
|
91
|
-
const M = fakeModel([{ id: '1', title: 'Hello world' }])
|
|
92
|
-
class A extends Resource {
|
|
93
|
-
static override label = 'Articles'
|
|
94
|
-
static override slug = 'articles'
|
|
95
|
-
// globalSearch defaults to false
|
|
96
|
-
static override recordTitleAttribute = 'title'
|
|
97
|
-
static override model = M
|
|
98
|
-
static override table(table: Table): Table {
|
|
99
|
-
return table.columns([Column.make('title').searchable()])
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
103
|
-
assert.deepEqual(await searchAllResources(pilotiq, 'hello', null), [])
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('returns matching rows from opted-in resources', async () => {
|
|
107
|
-
const M = fakeModel([
|
|
108
|
-
{ id: '1', title: 'Hello world' },
|
|
109
|
-
{ id: '2', title: 'Goodbye' },
|
|
110
|
-
{ id: '3', title: 'Hello again' },
|
|
111
|
-
])
|
|
112
|
-
class A extends Resource {
|
|
113
|
-
static override label = 'Articles'
|
|
114
|
-
static override slug = 'articles'
|
|
115
|
-
static override globalSearch = true
|
|
116
|
-
static override recordTitleAttribute = 'title'
|
|
117
|
-
static override model = M
|
|
118
|
-
static override table(table: Table): Table {
|
|
119
|
-
return table.columns([Column.make('title').searchable()])
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
123
|
-
const results = await searchAllResources(pilotiq, 'hello', null)
|
|
124
|
-
assert.equal(results.length, 2)
|
|
125
|
-
assert.equal(results[0]!.resource, 'articles')
|
|
126
|
-
assert.equal(results[0]!.resourceLabel, 'Articles')
|
|
127
|
-
assert.equal(results[0]!.title, 'Hello world')
|
|
128
|
-
assert.equal(results[0]!.url, '/admin/articles/1')
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('applies subtitle override when set', async () => {
|
|
132
|
-
const M = fakeModel([{ id: '1', title: 'Post', publishedAt: '2026-04-01' }])
|
|
133
|
-
class A extends Resource {
|
|
134
|
-
static override label = 'Articles'
|
|
135
|
-
static override slug = 'articles'
|
|
136
|
-
static override globalSearch = true
|
|
137
|
-
static override recordTitleAttribute = 'title'
|
|
138
|
-
static override model = M
|
|
139
|
-
static override table(table: Table): Table {
|
|
140
|
-
return table.columns([Column.make('title').searchable()])
|
|
141
|
-
}
|
|
142
|
-
static override getGlobalSearchResultSubtitle(record: unknown): string | undefined {
|
|
143
|
-
const r = record as { publishedAt?: string }
|
|
144
|
-
return r.publishedAt ? `Published ${r.publishedAt}` : undefined
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
148
|
-
const results = await searchAllResources(pilotiq, 'post', null)
|
|
149
|
-
assert.equal(results[0]!.subtitle, 'Published 2026-04-01')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('honors getGlobalSearchResultUrl override', async () => {
|
|
153
|
-
const M = fakeModel([{ id: '99', title: 'Edit me' }])
|
|
154
|
-
class A extends Resource {
|
|
155
|
-
static override label = 'Articles'
|
|
156
|
-
static override slug = 'articles'
|
|
157
|
-
static override globalSearch = true
|
|
158
|
-
static override recordTitleAttribute = 'title'
|
|
159
|
-
static override model = M
|
|
160
|
-
static override table(table: Table): Table {
|
|
161
|
-
return table.columns([Column.make('title').searchable()])
|
|
162
|
-
}
|
|
163
|
-
static override getGlobalSearchResultUrl(record: unknown, base: string): string {
|
|
164
|
-
return `${base}/articles/${(record as { id: string }).id}/edit`
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
168
|
-
const results = await searchAllResources(pilotiq, 'edit', null)
|
|
169
|
-
assert.equal(results[0]!.url, '/admin/articles/99/edit')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('drops resources where canAccess returns false', async () => {
|
|
173
|
-
const M1 = fakeModel([{ id: '1', title: 'A1' }])
|
|
174
|
-
const M2 = fakeModel([{ id: '2', title: 'A2' }])
|
|
175
|
-
class Visible extends Resource {
|
|
176
|
-
static override label = 'Visible'
|
|
177
|
-
static override slug = 'visible'
|
|
178
|
-
static override globalSearch = true
|
|
179
|
-
static override recordTitleAttribute = 'title'
|
|
180
|
-
static override model = M1
|
|
181
|
-
static override table(table: Table): Table {
|
|
182
|
-
return table.columns([Column.make('title').searchable()])
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
class Hidden extends Resource {
|
|
186
|
-
static override label = 'Hidden'
|
|
187
|
-
static override slug = 'hidden'
|
|
188
|
-
static override globalSearch = true
|
|
189
|
-
static override recordTitleAttribute = 'title'
|
|
190
|
-
static override model = M2
|
|
191
|
-
static override table(table: Table): Table {
|
|
192
|
-
return table.columns([Column.make('title').searchable()])
|
|
193
|
-
}
|
|
194
|
-
static override async canAccess(_user: unknown): Promise<boolean> { return false }
|
|
195
|
-
}
|
|
196
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([Visible, Hidden])
|
|
197
|
-
const results = await searchAllResources(pilotiq, 'A1', null)
|
|
198
|
-
assert.equal(results.length, 1)
|
|
199
|
-
assert.equal(results[0]!.resource, 'visible')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('drops resources where canViewAny returns false', async () => {
|
|
203
|
-
const M = fakeModel([{ id: '1', title: 'Hidden' }])
|
|
204
|
-
class A extends Resource {
|
|
205
|
-
static override label = 'Articles'
|
|
206
|
-
static override slug = 'articles'
|
|
207
|
-
static override globalSearch = true
|
|
208
|
-
static override recordTitleAttribute = 'title'
|
|
209
|
-
static override model = M
|
|
210
|
-
static override table(table: Table): Table {
|
|
211
|
-
return table.columns([Column.make('title').searchable()])
|
|
212
|
-
}
|
|
213
|
-
static override async canViewAny(_user: unknown): Promise<boolean> { return false }
|
|
214
|
-
}
|
|
215
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
216
|
-
const results = await searchAllResources(pilotiq, 'hidden', null)
|
|
217
|
-
assert.equal(results.length, 0)
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('continues other resources when one throws on getGlobalSearchQuery', async () => {
|
|
221
|
-
const M1 = fakeModel([{ id: '1', title: 'works' }])
|
|
222
|
-
class Broken extends Resource {
|
|
223
|
-
static override label = 'Broken'
|
|
224
|
-
static override slug = 'broken'
|
|
225
|
-
static override globalSearch = true
|
|
226
|
-
static override getGlobalSearchQuery(_needle: string): ModelQuery | undefined {
|
|
227
|
-
throw new Error('intentional')
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
class OK extends Resource {
|
|
231
|
-
static override label = 'OK'
|
|
232
|
-
static override slug = 'ok'
|
|
233
|
-
static override globalSearch = true
|
|
234
|
-
static override recordTitleAttribute = 'title'
|
|
235
|
-
static override model = M1
|
|
236
|
-
static override table(table: Table): Table {
|
|
237
|
-
return table.columns([Column.make('title').searchable()])
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([Broken, OK])
|
|
241
|
-
// Silence the warn during the test.
|
|
242
|
-
const originalWarn = console.warn
|
|
243
|
-
console.warn = () => {}
|
|
244
|
-
try {
|
|
245
|
-
const results = await searchAllResources(pilotiq, 'works', null)
|
|
246
|
-
assert.equal(results.length, 1)
|
|
247
|
-
assert.equal(results[0]!.resource, 'ok')
|
|
248
|
-
} finally {
|
|
249
|
-
console.warn = originalWarn
|
|
250
|
-
}
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
it('returns [] when a resource has no model AND no override', async () => {
|
|
254
|
-
class A extends Resource {
|
|
255
|
-
static override label = 'Modeless'
|
|
256
|
-
static override slug = 'modeless'
|
|
257
|
-
static override globalSearch = true
|
|
258
|
-
// No model. No getGlobalSearchQuery override. No data source → skip.
|
|
259
|
-
}
|
|
260
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
261
|
-
const results = await searchAllResources(pilotiq, 'anything', null)
|
|
262
|
-
assert.equal(results.length, 0)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('caps results per resource via opts.limitPerResource', async () => {
|
|
266
|
-
const M = fakeModel(Array.from({ length: 20 }, (_, i) => ({ id: String(i), title: `row ${i}` })))
|
|
267
|
-
class A extends Resource {
|
|
268
|
-
static override label = 'Many'
|
|
269
|
-
static override slug = 'many'
|
|
270
|
-
static override globalSearch = true
|
|
271
|
-
static override recordTitleAttribute = 'title'
|
|
272
|
-
static override model = M
|
|
273
|
-
static override table(table: Table): Table {
|
|
274
|
-
return table.columns([Column.make('title').searchable()])
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
278
|
-
const results = await searchAllResources(pilotiq, 'row', null, { limitPerResource: 3 })
|
|
279
|
-
assert.equal(results.length, 3)
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
it('caps total results across all resources', async () => {
|
|
283
|
-
const rows = Array.from({ length: 10 }, (_, i) => ({ id: String(i), title: `row ${i}` }))
|
|
284
|
-
const Ma = fakeModel(rows.map(r => ({ ...r, id: `a-${r.id}` })))
|
|
285
|
-
const Mb = fakeModel(rows.map(r => ({ ...r, id: `b-${r.id}` })))
|
|
286
|
-
class A extends Resource {
|
|
287
|
-
static override label = 'A'
|
|
288
|
-
static override slug = 'a'
|
|
289
|
-
static override globalSearch = true
|
|
290
|
-
static override recordTitleAttribute = 'title'
|
|
291
|
-
static override model = Ma
|
|
292
|
-
static override table(table: Table): Table {
|
|
293
|
-
return table.columns([Column.make('title').searchable()])
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
class B extends Resource {
|
|
297
|
-
static override label = 'B'
|
|
298
|
-
static override slug = 'b'
|
|
299
|
-
static override globalSearch = true
|
|
300
|
-
static override recordTitleAttribute = 'title'
|
|
301
|
-
static override model = Mb
|
|
302
|
-
static override table(table: Table): Table {
|
|
303
|
-
return table.columns([Column.make('title').searchable()])
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A, B])
|
|
307
|
-
const results = await searchAllResources(pilotiq, 'row', null, {
|
|
308
|
-
limitPerResource: 10,
|
|
309
|
-
limitTotal: 7,
|
|
310
|
-
})
|
|
311
|
-
assert.equal(results.length, 7)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('uses getGlobalSearchQuery override when present', async () => {
|
|
315
|
-
const M = fakeModel([{ id: '1', title: 'pre-override' }])
|
|
316
|
-
let queryArg = ''
|
|
317
|
-
class A extends Resource {
|
|
318
|
-
static override label = 'Override'
|
|
319
|
-
static override slug = 'override'
|
|
320
|
-
static override globalSearch = true
|
|
321
|
-
static override recordTitleAttribute = 'title'
|
|
322
|
-
static override model = M
|
|
323
|
-
static override table(table: Table): Table {
|
|
324
|
-
return table.columns([Column.make('title').searchable()])
|
|
325
|
-
}
|
|
326
|
-
static override getGlobalSearchQuery(needle: string): ModelQuery | undefined {
|
|
327
|
-
queryArg = needle
|
|
328
|
-
// Just return the unmodified query so paginate still produces the row.
|
|
329
|
-
return M.query()
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
333
|
-
const results = await searchAllResources(pilotiq, 'anything', null)
|
|
334
|
-
assert.equal(queryArg, 'anything')
|
|
335
|
-
assert.equal(results.length, 1)
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
it('searchData wraps searchAllResources with resolveUser', async () => {
|
|
339
|
-
const M = fakeModel([{ id: '1', title: 'searchable' }])
|
|
340
|
-
let userSeen: unknown = 'untouched'
|
|
341
|
-
class A extends Resource {
|
|
342
|
-
static override label = 'Articles'
|
|
343
|
-
static override slug = 'articles'
|
|
344
|
-
static override globalSearch = true
|
|
345
|
-
static override recordTitleAttribute = 'title'
|
|
346
|
-
static override model = M
|
|
347
|
-
static override table(table: Table): Table {
|
|
348
|
-
return table.columns([Column.make('title').searchable()])
|
|
349
|
-
}
|
|
350
|
-
static override async canAccess(user: unknown): Promise<boolean> {
|
|
351
|
-
userSeen = user
|
|
352
|
-
return true
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A]).user(() => ({ role: 'admin' }))
|
|
356
|
-
const result = await searchData(pilotiq, 'searchable', { fakeReq: true })
|
|
357
|
-
assert.equal(result.ok, true)
|
|
358
|
-
assert.equal(result.results.length, 1)
|
|
359
|
-
assert.deepEqual(userSeen, { role: 'admin' })
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('serializes the resource icon onto each result', async () => {
|
|
363
|
-
const M = fakeModel([{ id: '1', title: 'AB' }])
|
|
364
|
-
class A extends Resource {
|
|
365
|
-
static override label = 'Articles'
|
|
366
|
-
static override slug = 'articles'
|
|
367
|
-
static override icon = 'newspaper'
|
|
368
|
-
static override globalSearch = true
|
|
369
|
-
static override recordTitleAttribute = 'title'
|
|
370
|
-
static override model = M
|
|
371
|
-
static override table(table: Table): Table {
|
|
372
|
-
return table.columns([Column.make('title').searchable()])
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
376
|
-
const results = await searchAllResources(pilotiq, 'AB', null)
|
|
377
|
-
assert.equal(results[0]!.icon, 'newspaper')
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('omits renderHooks key when no global-search hooks registered', async () => {
|
|
381
|
-
const M = fakeModel([{ id: '1', title: 'searchable' }])
|
|
382
|
-
class A extends Resource {
|
|
383
|
-
static override label = 'Articles'
|
|
384
|
-
static override slug = 'articles'
|
|
385
|
-
static override globalSearch = true
|
|
386
|
-
static override recordTitleAttribute = 'title'
|
|
387
|
-
static override model = M
|
|
388
|
-
static override table(table: Table): Table {
|
|
389
|
-
return table.columns([Column.make('title').searchable()])
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
393
|
-
const result = await searchData(pilotiq, 'searchable')
|
|
394
|
-
assert.equal('renderHooks' in result, false)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('resolves panels::global-search.results.before/.after when registered', async () => {
|
|
398
|
-
const M = fakeModel([{ id: '1', title: 'searchable' }])
|
|
399
|
-
class A extends Resource {
|
|
400
|
-
static override label = 'Articles'
|
|
401
|
-
static override slug = 'articles'
|
|
402
|
-
static override globalSearch = true
|
|
403
|
-
static override recordTitleAttribute = 'title'
|
|
404
|
-
static override model = M
|
|
405
|
-
static override table(table: Table): Table {
|
|
406
|
-
return table.columns([Column.make('title').searchable()])
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
410
|
-
.renderHook('panels::global-search.results.before', () => [Heading.make('Before')])
|
|
411
|
-
.renderHook('panels::global-search.results.after', () => [Text.make('After')])
|
|
412
|
-
const result = await searchData(pilotiq, 'searchable')
|
|
413
|
-
assert.equal(result.ok, true)
|
|
414
|
-
const hooks = result.renderHooks
|
|
415
|
-
assert.ok(hooks, 'renderHooks should be present')
|
|
416
|
-
const before = hooks!['panels::global-search.results.before']
|
|
417
|
-
const after = hooks!['panels::global-search.results.after']
|
|
418
|
-
assert.equal(before?.length, 1)
|
|
419
|
-
assert.equal(after?.length, 1)
|
|
420
|
-
assert.equal(before![0]!['type'], 'heading')
|
|
421
|
-
assert.equal(after![0]!['type'], 'text')
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('passes resolved user into the global-search hook context', async () => {
|
|
425
|
-
const M = fakeModel([{ id: '1', title: 'searchable' }])
|
|
426
|
-
class A extends Resource {
|
|
427
|
-
static override label = 'Articles'
|
|
428
|
-
static override slug = 'articles'
|
|
429
|
-
static override globalSearch = true
|
|
430
|
-
static override recordTitleAttribute = 'title'
|
|
431
|
-
static override model = M
|
|
432
|
-
static override table(table: Table): Table {
|
|
433
|
-
return table.columns([Column.make('title').searchable()])
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
let userSeen: unknown = 'untouched'
|
|
437
|
-
const pilotiq = Pilotiq.make('test').path('/admin').resources([A])
|
|
438
|
-
.user(() => ({ role: 'editor' }))
|
|
439
|
-
.renderHook('panels::global-search.results.before', (ctx) => {
|
|
440
|
-
userSeen = ctx.user
|
|
441
|
-
return [Heading.make('Hi')]
|
|
442
|
-
})
|
|
443
|
-
await searchData(pilotiq, 'searchable', { fakeReq: true })
|
|
444
|
-
assert.deepEqual(userSeen, { role: 'editor' })
|
|
445
|
-
})
|
|
446
|
-
})
|
package/src/search.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plan #12 — global search.
|
|
3
|
-
*
|
|
4
|
-
* Walks every opted-in resource on a panel, runs a per-resource search
|
|
5
|
-
* query in parallel, and returns a flat list of results suitable for
|
|
6
|
-
* rendering inside a Cmd+K palette.
|
|
7
|
-
*
|
|
8
|
-
* What "opted in" means:
|
|
9
|
-
* 1. `Resource.globalSearch === true` — the static toggle.
|
|
10
|
-
* 2. `R.canAccess(user)` AND `R.canViewAny(user)` (Plan #10) both
|
|
11
|
-
* resolve true. Failures fail closed (resource silently dropped).
|
|
12
|
-
* 3. Either `R.getGlobalSearchQuery(needle)` returns a query, OR the
|
|
13
|
-
* resource has `static model = …` AND a non-empty
|
|
14
|
-
* `globallySearchableAttributes()` array.
|
|
15
|
-
*
|
|
16
|
-
* Each surviving resource hits `query.paginate(1, limitPerResource)`
|
|
17
|
-
* (the same method `modelTableRecords` uses, no new ORM contract).
|
|
18
|
-
* Results map through `getGlobalSearchResultTitle / Subtitle / Url`
|
|
19
|
-
* and `serializeIcon(R.icon, R.name)` for transport. The caller
|
|
20
|
-
* receives a flat array sorted by registration order — the palette
|
|
21
|
-
* handles visual grouping.
|
|
22
|
-
*/
|
|
23
|
-
import type { Pilotiq } from './Pilotiq.js'
|
|
24
|
-
import type { ResourceClass } from './Resource.js'
|
|
25
|
-
import type { ModelQuery } from './orm/modelDefaults.js'
|
|
26
|
-
import { serializeIcon, type SerializedIcon } from './icons/types.js'
|
|
27
|
-
|
|
28
|
-
export interface GlobalSearchResult {
|
|
29
|
-
/** URL slug of the resource, e.g. `'articles'`. Useful for grouping. */
|
|
30
|
-
resource: string
|
|
31
|
-
/** Plural label for headings. */
|
|
32
|
-
resourceLabel: string
|
|
33
|
-
/** Pre-serialized icon — string registry key or `{ class }` manifest ref. */
|
|
34
|
-
icon?: SerializedIcon
|
|
35
|
-
/** Stringified primary key. */
|
|
36
|
-
id: string
|
|
37
|
-
/** Big text in the palette row. */
|
|
38
|
-
title: string
|
|
39
|
-
/** Optional second line. */
|
|
40
|
-
subtitle?: string
|
|
41
|
-
/** URL the palette navigates to on Enter. */
|
|
42
|
-
url: string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface GlobalSearchOptions {
|
|
46
|
-
/** Cap results PER RESOURCE before merge. Default 5. */
|
|
47
|
-
limitPerResource?: number
|
|
48
|
-
/** Cap total results returned. Default 25. */
|
|
49
|
-
limitTotal?: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const DEFAULT_PER_RESOURCE = 5
|
|
53
|
-
const DEFAULT_TOTAL = 25
|
|
54
|
-
const MIN_QUERY_LENGTH = 2
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Search all opted-in resources for `query`. Returns up to
|
|
58
|
-
* `opts.limitTotal` (default 25) results. Empty/short queries return
|
|
59
|
-
* `[]` cheaply without touching the DB.
|
|
60
|
-
*/
|
|
61
|
-
export async function searchAllResources(
|
|
62
|
-
pilotiq: Pilotiq,
|
|
63
|
-
query: string,
|
|
64
|
-
user: unknown,
|
|
65
|
-
opts: GlobalSearchOptions = {},
|
|
66
|
-
): Promise<GlobalSearchResult[]> {
|
|
67
|
-
const needle = query.trim()
|
|
68
|
-
if (needle.length < MIN_QUERY_LENGTH) return []
|
|
69
|
-
|
|
70
|
-
const limitPerResource = opts.limitPerResource ?? DEFAULT_PER_RESOURCE
|
|
71
|
-
const limitTotal = opts.limitTotal ?? DEFAULT_TOTAL
|
|
72
|
-
|
|
73
|
-
const cfg = pilotiq.getConfig()
|
|
74
|
-
const candidates = cfg.resources.filter(R => R.globalSearch === true)
|
|
75
|
-
if (candidates.length === 0) return []
|
|
76
|
-
|
|
77
|
-
// Authorization gate. `canAccess` AND `canViewAny` must both pass —
|
|
78
|
-
// we don't surface records the user can't list. Throwing predicates
|
|
79
|
-
// fail closed (drop the resource).
|
|
80
|
-
const accessChecks = await Promise.all(
|
|
81
|
-
candidates.map(async (R) => {
|
|
82
|
-
try {
|
|
83
|
-
const ok = (await R.canAccess(user)) && (await R.canViewAny(user))
|
|
84
|
-
return ok ? R : null
|
|
85
|
-
} catch {
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
|
-
}),
|
|
89
|
-
)
|
|
90
|
-
const allowed = accessChecks.filter((R): R is ResourceClass => R !== null)
|
|
91
|
-
if (allowed.length === 0) return []
|
|
92
|
-
|
|
93
|
-
const perResource = await Promise.all(
|
|
94
|
-
allowed.map(R => searchResource(R, needle, limitPerResource, cfg.path, user)),
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
// Flatten + cap. Registration order preserved.
|
|
98
|
-
const flat: GlobalSearchResult[] = []
|
|
99
|
-
for (const chunk of perResource) {
|
|
100
|
-
for (const r of chunk) {
|
|
101
|
-
flat.push(r)
|
|
102
|
-
if (flat.length >= limitTotal) return flat
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return flat
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function searchResource(
|
|
109
|
-
R: ResourceClass,
|
|
110
|
-
needle: string,
|
|
111
|
-
limit: number,
|
|
112
|
-
base: string,
|
|
113
|
-
user: unknown,
|
|
114
|
-
): Promise<GlobalSearchResult[]> {
|
|
115
|
-
try {
|
|
116
|
-
const q = buildSearchQuery(R, needle, user)
|
|
117
|
-
if (!q) return []
|
|
118
|
-
|
|
119
|
-
const result = await q.paginate(1, limit)
|
|
120
|
-
const rows = (result?.data ?? []) as unknown[]
|
|
121
|
-
const slug = R.getSlug()
|
|
122
|
-
const icon = serializeIcon(R.icon, R.name)
|
|
123
|
-
|
|
124
|
-
return rows.map((record) => {
|
|
125
|
-
const r = record as Record<string, unknown>
|
|
126
|
-
const id = r['id']
|
|
127
|
-
const title = R.getGlobalSearchResultTitle(record)
|
|
128
|
-
const subtitle = R.getGlobalSearchResultSubtitle(record)
|
|
129
|
-
const url = R.getGlobalSearchResultUrl(record, base)
|
|
130
|
-
const out: GlobalSearchResult = {
|
|
131
|
-
resource: slug,
|
|
132
|
-
resourceLabel: R.label,
|
|
133
|
-
id: id !== undefined && id !== null ? String(id) : '',
|
|
134
|
-
title,
|
|
135
|
-
url,
|
|
136
|
-
}
|
|
137
|
-
if (icon !== undefined) out.icon = icon
|
|
138
|
-
if (subtitle !== undefined) out.subtitle = subtitle
|
|
139
|
-
return out
|
|
140
|
-
})
|
|
141
|
-
} catch (err) {
|
|
142
|
-
// One resource throwing must not blank the palette — log and drop
|
|
143
|
-
// its results, the rest still resolve via Promise.all.
|
|
144
|
-
console.warn(`[pilotiq] global search failed on ${R.name}:`, err)
|
|
145
|
-
return []
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Build the `ModelQuery` we run for a resource. Picks in this order:
|
|
151
|
-
* 1. User override `R.getGlobalSearchQuery(needle)`.
|
|
152
|
-
* 2. Default chain: `R.query({ user })` chained with LIKE on each
|
|
153
|
-
* attribute returned from `R.globallySearchableAttributes()`.
|
|
154
|
-
*
|
|
155
|
-
* Routes through `R.query(ctx)` (instead of `R.model.query()` directly)
|
|
156
|
-
* so user-installed scopes — tenant filters, default ordering, etc. —
|
|
157
|
-
* apply to global-search hits the same way they apply to list pages.
|
|
158
|
-
*
|
|
159
|
-
* Returns `undefined` when the resource has no model AND no override —
|
|
160
|
-
* pilotiq won't load every record into memory just to LIKE-match.
|
|
161
|
-
*/
|
|
162
|
-
function buildSearchQuery(R: ResourceClass, needle: string, user: unknown): ModelQuery | undefined {
|
|
163
|
-
const override = R.getGlobalSearchQuery(needle)
|
|
164
|
-
if (override) return override
|
|
165
|
-
if (!R.model) return undefined
|
|
166
|
-
|
|
167
|
-
const attrs = R.globallySearchableAttributes()
|
|
168
|
-
if (attrs.length === 0) return undefined
|
|
169
|
-
|
|
170
|
-
const wildcarded = `%${needle}%`
|
|
171
|
-
let q = R.query(user != null ? { user } : undefined)
|
|
172
|
-
attrs.forEach((col, i) => {
|
|
173
|
-
q = i === 0
|
|
174
|
-
? q.where(col, 'LIKE', wildcarded)
|
|
175
|
-
: q.orWhere(col, 'LIKE', wildcarded)
|
|
176
|
-
})
|
|
177
|
-
return q
|
|
178
|
-
}
|