@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
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { FormFilter, parseFormFilterValue, encodeFormFilterValue } from './FormFilter.js'
|
|
5
|
-
import { TextField } from '../fields/TextField.js'
|
|
6
|
-
import { NumberField } from '../fields/NumberField.js'
|
|
7
|
-
import { SelectField } from '../fields/SelectField.js'
|
|
8
|
-
import { Section } from '../schema/Section.js'
|
|
9
|
-
import type { ModelQuery } from '../orm/modelDefaults.js'
|
|
10
|
-
|
|
11
|
-
describe('parseFormFilterValue / encodeFormFilterValue', () => {
|
|
12
|
-
it('round-trips a typical multi-field payload', () => {
|
|
13
|
-
const enc = encodeFormFilterValue({ min: 100, max: 500 })
|
|
14
|
-
assert.equal(enc, '{"min":100,"max":500}')
|
|
15
|
-
const dec = parseFormFilterValue(enc)
|
|
16
|
-
assert.deepEqual(dec, { min: 100, max: 500 })
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it('parse returns {} for empty / null / undefined / unparseable / non-object', () => {
|
|
20
|
-
assert.deepEqual(parseFormFilterValue(''), {})
|
|
21
|
-
assert.deepEqual(parseFormFilterValue(undefined), {})
|
|
22
|
-
assert.deepEqual(parseFormFilterValue('not-json'), {})
|
|
23
|
-
assert.deepEqual(parseFormFilterValue('"a"'), {})
|
|
24
|
-
assert.deepEqual(parseFormFilterValue('[1,2,3]'), {})
|
|
25
|
-
assert.deepEqual(parseFormFilterValue('null'), {})
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('encode strips undefined / null / "" / [] entries', () => {
|
|
29
|
-
const enc = encodeFormFilterValue({ a: 1, b: undefined, c: null, d: '', e: [], f: 'x' })
|
|
30
|
-
assert.equal(enc, '{"a":1,"f":"x"}')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('encode returns "" for an all-empty payload', () => {
|
|
34
|
-
assert.equal(encodeFormFilterValue({}), '')
|
|
35
|
-
assert.equal(encodeFormFilterValue({ a: undefined }), '')
|
|
36
|
-
assert.equal(encodeFormFilterValue({ a: '', b: null }), '')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('encode keeps zero / false (legit values)', () => {
|
|
40
|
-
const enc = encodeFormFilterValue({ count: 0, active: false })
|
|
41
|
-
assert.equal(enc, '{"count":0,"active":false}')
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('FormFilter shape', () => {
|
|
46
|
-
it('emits kind:form', async () => {
|
|
47
|
-
const meta = await FormFilter.make('amount')
|
|
48
|
-
.form([NumberField.make('min'), NumberField.make('max')])
|
|
49
|
-
.toMeta()
|
|
50
|
-
assert.equal(meta.kind, 'form')
|
|
51
|
-
assert.equal(meta.name, 'amount')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('emits formSchema with the inner field metas', async () => {
|
|
55
|
-
const meta = await FormFilter.make('amount')
|
|
56
|
-
.form([NumberField.make('min').label('Min'), NumberField.make('max').label('Max')])
|
|
57
|
-
.toMeta()
|
|
58
|
-
assert.ok(Array.isArray(meta.formSchema))
|
|
59
|
-
assert.equal(meta.formSchema!.length, 2)
|
|
60
|
-
assert.equal((meta.formSchema![0] as unknown as { name: string }).name, 'min')
|
|
61
|
-
assert.equal((meta.formSchema![1] as unknown as { name: string }).name, 'max')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('default placeholder is "Filter"', async () => {
|
|
65
|
-
const meta = await FormFilter.make('x').toMeta()
|
|
66
|
-
assert.equal(meta.placeholder, 'Filter')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('placeholder() override wins', async () => {
|
|
70
|
-
const meta = await FormFilter.make('x').placeholder('Refine').toMeta()
|
|
71
|
-
assert.equal(meta.placeholder, 'Refine')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('label override + auto-derived label work the same as the base', async () => {
|
|
75
|
-
const auto = await FormFilter.make('priceRange').toMeta()
|
|
76
|
-
const custom = await FormFilter.make('priceRange').label('Price').toMeta()
|
|
77
|
-
assert.equal(auto.label, 'PriceRange')
|
|
78
|
-
assert.equal(custom.label, 'Price')
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('handle((q, values)) wraps the base query() with a parsing decorator', () => {
|
|
82
|
-
const f = FormFilter.make('amount')
|
|
83
|
-
.form([NumberField.make('min'), NumberField.make('max')])
|
|
84
|
-
.handle((q: ModelQuery, { min, max }: Record<string, unknown>) => {
|
|
85
|
-
if (min !== undefined) q = q.where('price', '>=', Number(min))
|
|
86
|
-
if (max !== undefined) q = q.where('price', '<=', Number(max))
|
|
87
|
-
return q
|
|
88
|
-
})
|
|
89
|
-
const calls: Array<[string, string, unknown]> = []
|
|
90
|
-
const stubQuery = makeStubQuery(calls)
|
|
91
|
-
const fn = f.getQuery()!
|
|
92
|
-
fn(stubQuery, '{"min":100,"max":500}')
|
|
93
|
-
assert.deepEqual(calls, [
|
|
94
|
-
['price', '>=', 100],
|
|
95
|
-
['price', '<=', 500],
|
|
96
|
-
])
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('handle callback receives an empty object for an empty / unparseable URL value', () => {
|
|
100
|
-
let seen: Record<string, unknown> | undefined
|
|
101
|
-
const f = FormFilter.make('x')
|
|
102
|
-
.form([])
|
|
103
|
-
.handle((q, vals) => { seen = vals; return q })
|
|
104
|
-
f.getQuery()!(makeStubQuery([]), '')
|
|
105
|
-
assert.deepEqual(seen, {})
|
|
106
|
-
|
|
107
|
-
f.getQuery()!(makeStubQuery([]), 'not-json')
|
|
108
|
-
assert.deepEqual(seen, {})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('default queryFn (no .handle()) is a no-op so modelDefaults takes the customQuery branch', () => {
|
|
112
|
-
const f = FormFilter.make('x').form([])
|
|
113
|
-
const stubQuery = makeStubQuery([])
|
|
114
|
-
const fn = f.getQuery()
|
|
115
|
-
assert.equal(typeof fn, 'function')
|
|
116
|
-
const result = fn!(stubQuery, '{"a":1}')
|
|
117
|
-
assert.equal(result, stubQuery, 'no-op queryFn returns the input query unchanged')
|
|
118
|
-
})
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
describe('FormFilter.toMeta — value hydration', () => {
|
|
122
|
-
it('hydrates inner field defaultValues from the parsed URL value', async () => {
|
|
123
|
-
const f = FormFilter.make('amount')
|
|
124
|
-
.form([NumberField.make('min'), NumberField.make('max')])
|
|
125
|
-
.withValue('{"min":100,"max":500}')
|
|
126
|
-
const meta = await f.toMeta()
|
|
127
|
-
const fields = meta.formSchema as unknown as Array<{ name: string; defaultValue?: unknown }>
|
|
128
|
-
assert.equal(fields[0]!.defaultValue, 100)
|
|
129
|
-
assert.equal(fields[1]!.defaultValue, 500)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('only hydrates fields whose name appears in the parsed value', async () => {
|
|
133
|
-
const f = FormFilter.make('search')
|
|
134
|
-
.form([
|
|
135
|
-
TextField.make('needle'),
|
|
136
|
-
SelectField.make('category').options([{ value: 'a', label: 'A' }]),
|
|
137
|
-
])
|
|
138
|
-
.withValue('{"needle":"hello"}')
|
|
139
|
-
const meta = await f.toMeta()
|
|
140
|
-
const fields = meta.formSchema as unknown as Array<{ name: string; defaultValue?: unknown }>
|
|
141
|
-
assert.equal(fields[0]!.defaultValue, 'hello')
|
|
142
|
-
assert.equal('defaultValue' in fields[1]!, false)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('recurses into containers (Section.children)', async () => {
|
|
146
|
-
const f = FormFilter.make('combo')
|
|
147
|
-
.form([
|
|
148
|
-
Section.make('nested').schema([
|
|
149
|
-
TextField.make('inner'),
|
|
150
|
-
]),
|
|
151
|
-
])
|
|
152
|
-
.withValue('{"inner":"x"}')
|
|
153
|
-
const meta = await f.toMeta()
|
|
154
|
-
const section = meta.formSchema![0] as unknown as { children?: Array<{ name: string; defaultValue?: unknown }> }
|
|
155
|
-
assert.equal(section.children![0]!.defaultValue, 'x')
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
describe('FormFilter.toMeta — indicator', () => {
|
|
160
|
-
it('omits indicator when no value is set', async () => {
|
|
161
|
-
const meta = await FormFilter.make('x').form([]).toMeta()
|
|
162
|
-
assert.equal(meta.indicator, undefined)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('omits indicator when the parsed value is an empty object', async () => {
|
|
166
|
-
const meta = await FormFilter.make('x').form([]).withValue('{}').toMeta()
|
|
167
|
-
assert.equal(meta.indicator, undefined)
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
it('omits indicator when every parsed entry is empty / null / undefined', async () => {
|
|
171
|
-
const meta = await FormFilter.make('x').form([]).withValue('{"a":"","b":null}').toMeta()
|
|
172
|
-
assert.equal(meta.indicator, undefined)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('default indicator joins non-empty entries as "key: value, …"', async () => {
|
|
176
|
-
const meta = await FormFilter.make('amount')
|
|
177
|
-
.label('Price')
|
|
178
|
-
.form([NumberField.make('min'), NumberField.make('max')])
|
|
179
|
-
.withValue('{"min":100,"max":500}')
|
|
180
|
-
.toMeta()
|
|
181
|
-
assert.equal(meta.indicator, 'Price: min: 100, max: 500')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('formIndicator((values, filter) => string) overrides with parsed values', async () => {
|
|
185
|
-
const meta = await FormFilter.make('amount')
|
|
186
|
-
.label('Price')
|
|
187
|
-
.form([NumberField.make('min'), NumberField.make('max')])
|
|
188
|
-
.formIndicator(({ min, max }) => {
|
|
189
|
-
if (min !== undefined && max !== undefined) return `Price: ${min}–${max}`
|
|
190
|
-
if (min !== undefined) return `Price: ≥ ${min}`
|
|
191
|
-
if (max !== undefined) return `Price: ≤ ${max}`
|
|
192
|
-
return 'Price'
|
|
193
|
-
})
|
|
194
|
-
.withValue('{"min":100,"max":500}')
|
|
195
|
-
.toMeta()
|
|
196
|
-
assert.equal(meta.indicator, 'Price: 100–500')
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('inherited indicator(string) override still works', async () => {
|
|
200
|
-
const meta = await FormFilter.make('x')
|
|
201
|
-
.form([])
|
|
202
|
-
.indicator('Static label')
|
|
203
|
-
.withValue('{"a":1}')
|
|
204
|
-
.toMeta()
|
|
205
|
-
assert.equal(meta.indicator, 'Static label')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('booleans format as Yes/No in the default formatter', async () => {
|
|
209
|
-
const meta = await FormFilter.make('flags')
|
|
210
|
-
.label('Flags')
|
|
211
|
-
.form([])
|
|
212
|
-
.withValue('{"published":true,"featured":false}')
|
|
213
|
-
.toMeta()
|
|
214
|
-
assert.equal(meta.indicator, 'Flags: published: Yes, featured: No')
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
// ─── Test helpers ──────────────────────────────────────────────────────
|
|
219
|
-
|
|
220
|
-
function makeStubQuery(calls: Array<[string, string, unknown]>): ModelQuery {
|
|
221
|
-
const stub = {
|
|
222
|
-
where(...args: unknown[]) {
|
|
223
|
-
const [a, b, c] = args
|
|
224
|
-
if (typeof a === 'string') {
|
|
225
|
-
if (typeof b === 'string' && c !== undefined) {
|
|
226
|
-
calls.push([a, b, c])
|
|
227
|
-
} else if (b !== undefined) {
|
|
228
|
-
calls.push([a, '=', b])
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return stub
|
|
232
|
-
},
|
|
233
|
-
orWhere() { return stub },
|
|
234
|
-
orderBy() { return stub },
|
|
235
|
-
paginate() { return Promise.resolve({ data: [], total: 0 }) },
|
|
236
|
-
} as unknown as ModelQuery
|
|
237
|
-
return stub
|
|
238
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import { Filter, type FilterKind, type FilterMeta } from './Filter.js'
|
|
2
|
-
import type { Element, ElementMeta } from '../schema/Element.js'
|
|
3
|
-
import type { FieldMeta } from '../fields/Field.js'
|
|
4
|
-
import type { ModelQuery } from '../orm/modelDefaults.js'
|
|
5
|
-
import { resolveSchema, type RenderContext } from '../schema/resolveSchema.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Parsed shape of a `FormFilter`'s URL value. JSON-encoded under a single
|
|
9
|
-
* URL key (`?amount={"min":100,"max":500}`). Empty / unparseable / non-
|
|
10
|
-
* object payloads round-trip as `{}`.
|
|
11
|
-
*
|
|
12
|
-
* Exposed as a public helper so users writing a custom `Filter.query(fn)`
|
|
13
|
-
* (or building their own server-side dispatch) can avoid re-parsing the
|
|
14
|
-
* encoding by hand.
|
|
15
|
-
*/
|
|
16
|
-
export type FormFilterValue = Record<string, unknown>
|
|
17
|
-
|
|
18
|
-
export function parseFormFilterValue(value: string | undefined): FormFilterValue {
|
|
19
|
-
if (!value) return {}
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(value)
|
|
22
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
23
|
-
return parsed as FormFilterValue
|
|
24
|
-
}
|
|
25
|
-
return {}
|
|
26
|
-
} catch {
|
|
27
|
-
return {}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Encode a `Record<string, unknown>` of form values as the canonical
|
|
33
|
-
* single-URL-key JSON payload. Empty / null / `''` entries are dropped
|
|
34
|
-
* so the URL doesn't bloat with no-op keys; the all-empty case returns
|
|
35
|
-
* `''` (caller should drop the URL key entirely rather than emitting `{}`).
|
|
36
|
-
*/
|
|
37
|
-
export function encodeFormFilterValue(values: FormFilterValue): string {
|
|
38
|
-
const clean: FormFilterValue = {}
|
|
39
|
-
for (const [k, v] of Object.entries(values)) {
|
|
40
|
-
if (v === undefined || v === null || v === '') continue
|
|
41
|
-
if (Array.isArray(v) && v.length === 0) continue
|
|
42
|
-
clean[k] = v
|
|
43
|
-
}
|
|
44
|
-
if (Object.keys(clean).length === 0) return ''
|
|
45
|
-
return JSON.stringify(clean)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* User-supplied query callback for `FormFilter` — the parsed form values
|
|
50
|
-
* arrive as a `Record<string, unknown>` keyed by inner-field name (versus
|
|
51
|
-
* the base `FilterQueryHandler`, which sees the raw URL string).
|
|
52
|
-
*/
|
|
53
|
-
export type FormFilterQueryHandler = (
|
|
54
|
-
query: ModelQuery,
|
|
55
|
-
values: FormFilterValue,
|
|
56
|
-
) => ModelQuery
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* User-supplied indicator-pill formatter for `FormFilter`. Mirrors the
|
|
60
|
-
* base `FilterIndicatorHandler` but pre-parses the URL value.
|
|
61
|
-
*/
|
|
62
|
-
export type FormFilterIndicatorHandler = (
|
|
63
|
-
values: FormFilterValue,
|
|
64
|
-
filter: FormFilter,
|
|
65
|
-
) => string
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Multi-field filter with arbitrary form schema. The popover renders the
|
|
69
|
-
* declared fields verbatim (using the same SchemaRenderer field dispatch
|
|
70
|
-
* as a regular form), the values are JSON-encoded into a single URL key,
|
|
71
|
-
* and the user's `.handle((q, values) => …)` callback applies the values
|
|
72
|
-
* to the ORM query.
|
|
73
|
-
*
|
|
74
|
-
* Designed for cases where the existing typed kinds (Select / Boolean /
|
|
75
|
-
* Ternary / DateRange / MultiSelect) don't fit — e.g. a price filter that
|
|
76
|
-
* needs both `min` and `max`, or a search filter that combines a needle
|
|
77
|
-
* with a category dropdown.
|
|
78
|
-
*
|
|
79
|
-
* Inner fields are resolved at meta-build time with the same `RenderContext`
|
|
80
|
-
* that `resolveSchema` uses for the surrounding table, so dependent-options
|
|
81
|
-
* resolvers (e.g. user-aware role lookups) work the same as in a regular form.
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* FormFilter.make('amount')
|
|
85
|
-
* .label('Price')
|
|
86
|
-
* .form([
|
|
87
|
-
* NumberField.make('min').label('Min'),
|
|
88
|
-
* NumberField.make('max').label('Max'),
|
|
89
|
-
* ])
|
|
90
|
-
* .handle((q, { min, max }) => {
|
|
91
|
-
* if (min !== undefined) q = q.where('price', '>=', Number(min))
|
|
92
|
-
* if (max !== undefined) q = q.where('price', '<=', Number(max))
|
|
93
|
-
* return q
|
|
94
|
-
* })
|
|
95
|
-
* .indicator(({ min, max }) => {
|
|
96
|
-
* if (min !== undefined && max !== undefined) return `Price: ${min}–${max}`
|
|
97
|
-
* if (min !== undefined) return `Price: ≥ ${min}`
|
|
98
|
-
* if (max !== undefined) return `Price: ≤ ${max}`
|
|
99
|
-
* return 'Price'
|
|
100
|
-
* })
|
|
101
|
-
*/
|
|
102
|
-
export class FormFilter extends Filter {
|
|
103
|
-
private _formSchema: Element[] = []
|
|
104
|
-
|
|
105
|
-
static make(name: string): FormFilter {
|
|
106
|
-
const f = new FormFilter(name)
|
|
107
|
-
// Default no-op queryFn so modelDefaults' `if (customQuery)` short-
|
|
108
|
-
// circuits cleanly when the user hasn't called `.handle()` yet —
|
|
109
|
-
// otherwise the default `where(name, jsonString)` clause would fire
|
|
110
|
-
// with a JSON blob and most ORMs would NOT enjoy that.
|
|
111
|
-
f.query((q) => q)
|
|
112
|
-
return f
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Set the inner form schema. Any pilotiq Element is allowed — Field /
|
|
117
|
-
* Section / Grid / Group — though the filter popover is a small surface,
|
|
118
|
-
* so plain fields read best in v1.
|
|
119
|
-
*/
|
|
120
|
-
form(elements: Element[]): this {
|
|
121
|
-
this._formSchema = elements
|
|
122
|
-
return this
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Typed query callback. Receives the parsed form-values object
|
|
127
|
-
* (`Record<string, unknown>` keyed by inner-field name) plus the running
|
|
128
|
-
* ORM query. Wraps the inherited `Filter.query()` hook — when this is
|
|
129
|
-
* called, a `_queryFn` is installed that parses the URL value before
|
|
130
|
-
* delegating to `fn`.
|
|
131
|
-
*/
|
|
132
|
-
handle(fn: FormFilterQueryHandler): this {
|
|
133
|
-
this.query((q, value) => fn(q, parseFormFilterValue(value)))
|
|
134
|
-
return this
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Typed indicator override. Receives the parsed form-values object so
|
|
139
|
-
* users don't have to call `parseFormFilterValue` themselves. String
|
|
140
|
-
* arguments still work via the inherited `Filter.indicator(string)`.
|
|
141
|
-
*/
|
|
142
|
-
formIndicator(fn: FormFilterIndicatorHandler): this {
|
|
143
|
-
this.indicator((value: string) => fn(parseFormFilterValue(value), this))
|
|
144
|
-
return this
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
getFormSchema(): Element[] { return this._formSchema }
|
|
148
|
-
|
|
149
|
-
override getKind(): FilterKind { return 'form' }
|
|
150
|
-
|
|
151
|
-
protected override formatActiveValue(value: string): string {
|
|
152
|
-
const parsed = parseFormFilterValue(value)
|
|
153
|
-
const entries = Object.entries(parsed).filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
154
|
-
if (entries.length === 0) return ''
|
|
155
|
-
return entries.map(([k, v]) => `${k}: ${formatScalar(v)}`).join(', ')
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Override so that a stored value of `'{}'` (legitimately parsed JSON
|
|
160
|
-
* with zero entries) doesn't paint an empty `"Label: "` pill. The base
|
|
161
|
-
* `getIndicator` only checks for raw empty-string / undefined.
|
|
162
|
-
*/
|
|
163
|
-
override getIndicator(): string | undefined {
|
|
164
|
-
const value = this.getValue()
|
|
165
|
-
if (value === undefined || value === '') return undefined
|
|
166
|
-
const parsed = parseFormFilterValue(value)
|
|
167
|
-
const hasAny = Object.values(parsed).some(v => v !== undefined && v !== null && v !== '')
|
|
168
|
-
if (!hasAny) return undefined
|
|
169
|
-
return super.getIndicator()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
override async toMeta(ctx?: RenderContext): Promise<FilterMeta> {
|
|
173
|
-
const base = this.buildBaseMeta()
|
|
174
|
-
const resolved = await resolveSchema(this._formSchema, ctx ?? {})
|
|
175
|
-
const parsed = parseFormFilterValue(this.getValue())
|
|
176
|
-
const hydrated = applyValuesToMeta(resolved, parsed)
|
|
177
|
-
return {
|
|
178
|
-
...base,
|
|
179
|
-
formSchema: hydrated,
|
|
180
|
-
placeholder: this.getPlaceholder() ?? 'Filter',
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** Walk a resolved meta tree and stamp each Field's `defaultValue` from
|
|
186
|
-
* the parsed URL values map. Recurses into containers via `children`. */
|
|
187
|
-
function applyValuesToMeta(metas: ElementMeta[], values: FormFilterValue): ElementMeta[] {
|
|
188
|
-
return metas.map(m => applyToOne(m, values))
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function applyToOne(meta: ElementMeta, values: FormFilterValue): ElementMeta {
|
|
192
|
-
const next: ElementMeta = { ...meta }
|
|
193
|
-
if (next.type === 'field') {
|
|
194
|
-
const f = next as FieldMeta
|
|
195
|
-
if (f.name in values) {
|
|
196
|
-
(next as FieldMeta).defaultValue = values[f.name]
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
const children = (next as { children?: ElementMeta[] }).children
|
|
200
|
-
if (Array.isArray(children)) {
|
|
201
|
-
(next as { children?: ElementMeta[] }).children = applyValuesToMeta(children, values)
|
|
202
|
-
}
|
|
203
|
-
return next
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/** Friendly display for an arbitrary JSON-decoded value. Booleans / numbers
|
|
207
|
-
* / strings stringify directly; arrays join with ", "; objects collapse to
|
|
208
|
-
* `[object]` (rare — most form fields produce scalars). */
|
|
209
|
-
function formatScalar(v: unknown): string {
|
|
210
|
-
if (v === null || v === undefined) return ''
|
|
211
|
-
if (Array.isArray(v)) return v.map(x => formatScalar(x)).join(', ')
|
|
212
|
-
if (typeof v === 'object') return '[object]'
|
|
213
|
-
if (typeof v === 'boolean') return v ? 'Yes' : 'No'
|
|
214
|
-
return String(v)
|
|
215
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { Table } from '../elements/Table.js'
|
|
5
|
-
import { Column } from '../Column.js'
|
|
6
|
-
import {
|
|
7
|
-
MultiSelectFilter,
|
|
8
|
-
parseMultiSelectValue,
|
|
9
|
-
encodeMultiSelectValue,
|
|
10
|
-
} from './MultiSelectFilter.js'
|
|
11
|
-
import { modelTableRecords } from '../orm/modelDefaults.js'
|
|
12
|
-
import type { ModelLike, ModelQuery, ModelWhereOperator } from '../orm/modelDefaults.js'
|
|
13
|
-
|
|
14
|
-
interface FakeOp { op: string; args: unknown[] }
|
|
15
|
-
class FakeQuery implements ModelQuery {
|
|
16
|
-
ops: FakeOp[] = []
|
|
17
|
-
where(...args: unknown[]): ModelQuery { this.ops.push({ op: 'where', args }); return this }
|
|
18
|
-
orWhere(...args: unknown[]): ModelQuery { this.ops.push({ op: 'orWhere', args }); return this }
|
|
19
|
-
orderBy(c: string, d: 'ASC' | 'DESC' = 'ASC'): ModelQuery { this.ops.push({ op: 'orderBy', args: [c, d] }); return this }
|
|
20
|
-
async paginate(p: number, pp?: number) { this.ops.push({ op: 'paginate', args: [p, pp] }); return { data: [], total: 0 } }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function fakeModel(): ModelLike & { name: string; model: ModelLike; lastQuery: FakeQuery | null } {
|
|
24
|
-
let lastQuery: FakeQuery | null = null
|
|
25
|
-
const M = {
|
|
26
|
-
name: 'TestResource',
|
|
27
|
-
async find() { return null },
|
|
28
|
-
async create(d: Record<string, unknown>) { return d },
|
|
29
|
-
async update(_id: string | number, d: Record<string, unknown>) { return d },
|
|
30
|
-
async delete() {},
|
|
31
|
-
query(): ModelQuery {
|
|
32
|
-
const q = new FakeQuery()
|
|
33
|
-
lastQuery = q
|
|
34
|
-
return q
|
|
35
|
-
},
|
|
36
|
-
get lastQuery() { return lastQuery },
|
|
37
|
-
} as unknown as ModelLike & { name: string; model: ModelLike; lastQuery: FakeQuery | null }
|
|
38
|
-
M.model = M
|
|
39
|
-
return M
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
describe('parseMultiSelectValue / encodeMultiSelectValue', () => {
|
|
43
|
-
it('round-trips a typical comma-separated value', () => {
|
|
44
|
-
const tokens = parseMultiSelectValue('draft,published,archived')
|
|
45
|
-
assert.deepEqual(tokens, ['draft', 'published', 'archived'])
|
|
46
|
-
assert.equal(encodeMultiSelectValue(tokens), 'draft,published,archived')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('drops empty / whitespace-only tokens', () => {
|
|
50
|
-
assert.deepEqual(parseMultiSelectValue('draft, ,published,'), ['draft', 'published'])
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('parses empty / single-value strings', () => {
|
|
54
|
-
assert.deepEqual(parseMultiSelectValue(''), [])
|
|
55
|
-
assert.deepEqual(parseMultiSelectValue('draft'), ['draft'])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('encodes the empty list as the empty string', () => {
|
|
59
|
-
assert.equal(encodeMultiSelectValue([]), '')
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe('MultiSelectFilter.toMeta', () => {
|
|
64
|
-
it('emits kind:multiSelect + options + Any placeholder', () => {
|
|
65
|
-
const meta = MultiSelectFilter.make('status')
|
|
66
|
-
.options([
|
|
67
|
-
{ value: 'draft', label: 'Draft' },
|
|
68
|
-
{ value: 'published', label: 'Published' },
|
|
69
|
-
])
|
|
70
|
-
.toMeta()
|
|
71
|
-
assert.equal(meta.kind, 'multiSelect')
|
|
72
|
-
assert.equal(meta.placeholder, 'Any')
|
|
73
|
-
assert.deepEqual(meta.options, [
|
|
74
|
-
{ value: 'draft', label: 'Draft' },
|
|
75
|
-
{ value: 'published', label: 'Published' },
|
|
76
|
-
])
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('indicator joins selected option labels', () => {
|
|
80
|
-
const meta = MultiSelectFilter.make('status')
|
|
81
|
-
.options([
|
|
82
|
-
{ value: 'draft', label: 'Draft' },
|
|
83
|
-
{ value: 'published', label: 'Published' },
|
|
84
|
-
{ value: 'archived', label: 'Archived' },
|
|
85
|
-
])
|
|
86
|
-
.withValue('draft,archived')
|
|
87
|
-
.toMeta()
|
|
88
|
-
assert.equal(meta.indicator, 'Status: Draft, Archived')
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
describe('MultiSelectFilter ORM integration', () => {
|
|
93
|
-
it('contributes a where(name, IN, values) clause via the default queryFn', async () => {
|
|
94
|
-
const M = fakeModel()
|
|
95
|
-
const table = Table.make()
|
|
96
|
-
.columns([Column.make('title')])
|
|
97
|
-
.filters([MultiSelectFilter.make('status').options([
|
|
98
|
-
{ value: 'draft', label: 'Draft' },
|
|
99
|
-
{ value: 'published', label: 'Published' },
|
|
100
|
-
])])
|
|
101
|
-
const handler = modelTableRecords(M, table)
|
|
102
|
-
await handler({ filters: { status: 'draft,published' }, page: 1 })
|
|
103
|
-
const ops = M.lastQuery!.ops
|
|
104
|
-
const operator: ModelWhereOperator = 'IN'
|
|
105
|
-
assert.deepEqual(ops[0], { op: 'where', args: ['status', operator, ['draft', 'published']] })
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('skips the clause when the parsed value list is empty', async () => {
|
|
109
|
-
const M = fakeModel()
|
|
110
|
-
const table = Table.make()
|
|
111
|
-
.columns([Column.make('title').searchable()])
|
|
112
|
-
.filters([MultiSelectFilter.make('status')])
|
|
113
|
-
const handler = modelTableRecords(M, table)
|
|
114
|
-
// Only whitespace inside the comma-separated value: parses to [].
|
|
115
|
-
await handler({ filters: { status: ' , , ' }, search: 'hi', page: 1 })
|
|
116
|
-
const ops = M.lastQuery!.ops
|
|
117
|
-
assert.equal(ops.find(o => o.op === 'where' && (o.args as unknown[])[0] === 'status'), undefined)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { Filter, type FilterKind, type FilterMeta } from './Filter.js'
|
|
2
|
-
import type { ModelQuery } from '../orm/modelDefaults.js'
|
|
3
|
-
|
|
4
|
-
export interface MultiSelectFilterOption {
|
|
5
|
-
value: string
|
|
6
|
-
label: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse a comma-separated multi-select URL value into its individual
|
|
11
|
-
* tokens. Empty / whitespace-only entries drop. Exposed as a public
|
|
12
|
-
* helper so callers writing a custom `Filter.query(fn)` can reuse the
|
|
13
|
-
* same encoding without re-implementing it.
|
|
14
|
-
*/
|
|
15
|
-
export function parseMultiSelectValue(value: string): string[] {
|
|
16
|
-
if (!value) return []
|
|
17
|
-
return value.split(',').map(s => s.trim()).filter(s => s !== '')
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Encode a list of selected values into the canonical comma-separated
|
|
22
|
-
* URL value. The empty case returns `''` so the renderer can drop the
|
|
23
|
-
* URL key entirely rather than emitting a trailing `&name=`.
|
|
24
|
-
*/
|
|
25
|
-
export function encodeMultiSelectValue(values: ReadonlyArray<string>): string {
|
|
26
|
-
return values.filter(v => v !== '').join(',')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Multi-value dropdown filter. Renders as a checkbox list inside the
|
|
31
|
-
* filter popover; the URL value is comma-separated (`?status=draft,published`).
|
|
32
|
-
* Default ORM clause is `where(name, 'IN', values)` — backends without
|
|
33
|
-
* SQL `IN` can swap in their own logic via `Filter.query(fn)`.
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* MultiSelectFilter.make('status').options([
|
|
37
|
-
* { value: 'draft', label: 'Draft' },
|
|
38
|
-
* { value: 'published', label: 'Published' },
|
|
39
|
-
* { value: 'archived', label: 'Archived' },
|
|
40
|
-
* ])
|
|
41
|
-
*/
|
|
42
|
-
export class MultiSelectFilter extends Filter {
|
|
43
|
-
private _options: MultiSelectFilterOption[] = []
|
|
44
|
-
|
|
45
|
-
static make(name: string): MultiSelectFilter {
|
|
46
|
-
const f = new MultiSelectFilter(name)
|
|
47
|
-
f.query((q: ModelQuery, value: string) => {
|
|
48
|
-
const values = parseMultiSelectValue(value)
|
|
49
|
-
if (values.length === 0) return q
|
|
50
|
-
return q.where(f.name, 'IN', values)
|
|
51
|
-
})
|
|
52
|
-
return f
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
options(opts: MultiSelectFilterOption[]): this {
|
|
56
|
-
this._options = opts
|
|
57
|
-
return this
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
getOptions(): MultiSelectFilterOption[] { return this._options }
|
|
61
|
-
|
|
62
|
-
override getKind(): FilterKind { return 'multiSelect' }
|
|
63
|
-
|
|
64
|
-
protected override formatActiveValue(value: string): string {
|
|
65
|
-
const tokens = parseMultiSelectValue(value)
|
|
66
|
-
if (tokens.length === 0) return value
|
|
67
|
-
const labels = tokens.map(v => this._options.find(o => o.value === v)?.label ?? v)
|
|
68
|
-
return labels.join(', ')
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
override toMeta(): FilterMeta {
|
|
72
|
-
return {
|
|
73
|
-
...this.buildBaseMeta(),
|
|
74
|
-
options: this._options,
|
|
75
|
-
placeholder: this.getPlaceholder() ?? 'Any',
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|