@pilotiq/pilotiq 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -225
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -194
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -704
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1243
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
|
@@ -1,1233 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useState } from 'react'
|
|
2
|
-
import { CheckIcon, Columns3Icon, FilterIcon } from 'lucide-react'
|
|
3
|
-
import type { ElementMeta } from '../../../schema/Element.js'
|
|
4
|
-
import { Checkbox } from '../../ui/checkbox.js'
|
|
5
|
-
import { Input } from '../../ui/input.js'
|
|
6
|
-
import { Popover, PopoverContent, PopoverTrigger } from '../../ui/popover.js'
|
|
7
|
-
import {
|
|
8
|
-
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
|
9
|
-
} from '../../ui/select.js'
|
|
10
|
-
import {
|
|
11
|
-
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
|
|
12
|
-
} from '../../ui/dropdown-menu.js'
|
|
13
|
-
import { useNavigate } from '../../navigate.js'
|
|
14
|
-
import {
|
|
15
|
-
parseDateRangeValue, encodeDateRangeValue,
|
|
16
|
-
} from '../../../filters/DateRangeFilter.js'
|
|
17
|
-
import {
|
|
18
|
-
parseMultiSelectValue, encodeMultiSelectValue,
|
|
19
|
-
} from '../../../filters/MultiSelectFilter.js'
|
|
20
|
-
import { encodeFormFilterValue } from '../../../filters/FormFilter.js'
|
|
21
|
-
import {
|
|
22
|
-
parseQueryBuilderValue, encodeQueryBuilderValue, isQueryBuilderTree,
|
|
23
|
-
type QueryBuilderRule, type QueryBuilderTree, type QueryBuilderTreeChild,
|
|
24
|
-
} from '../../../filters/QueryBuilderFilter.js'
|
|
25
|
-
import type {
|
|
26
|
-
ConstraintMeta, ConstraintOperator, ConstraintOperatorName, ConstraintValueKind,
|
|
27
|
-
} from '../../../filters/queryBuilder/Constraint.js'
|
|
28
|
-
import {
|
|
29
|
-
COLUMN_COLOR_CLASSES,
|
|
30
|
-
} from '../constants.js'
|
|
31
|
-
import { resolveIcon } from '../helpers.js'
|
|
32
|
-
import { patchFilterUrl } from './url.js'
|
|
33
|
-
|
|
34
|
-
// ─── Filter chrome + table-toolbar dropdowns ────────────────
|
|
35
|
-
//
|
|
36
|
-
// Every filter UI piece — from the per-filter widget (Select / MultiSelect
|
|
37
|
-
// / DateRange / Form / QueryBuilder) to the wrapping Popover / inline
|
|
38
|
-
// strip / collapsible toggle — plus toolbar siblings (SortByPicker,
|
|
39
|
-
// ColumnsToggleDropdown, TableGroupPicker) and the renderFilterControl
|
|
40
|
-
// switch that picks the right widget per filter kind.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Active-filters bar — pill row above the table summarising every filter
|
|
45
|
-
* with a current value. Each pill shows the filter's `indicator` text
|
|
46
|
-
* (server-formatted via `Filter.indicator()` / per-subclass defaults) and
|
|
47
|
-
* an `×` button that clears that filter's URL key in place. Clicking ×
|
|
48
|
-
* also drops `?page` so users land on the first page of the relaxed set.
|
|
49
|
-
*
|
|
50
|
-
* Renders nothing when no filter has an indicator.
|
|
51
|
-
*/
|
|
52
|
-
export function ActiveFiltersBar({ filters, prefix }: { filters: ElementMeta[]; prefix?: string | undefined }) {
|
|
53
|
-
const navigate = useNavigate()
|
|
54
|
-
const active = filters.filter(f => typeof f['indicator'] === 'string' && f['indicator'] !== '')
|
|
55
|
-
if (active.length === 0) return null
|
|
56
|
-
|
|
57
|
-
const clear = (name: string): void => {
|
|
58
|
-
patchFilterUrl(navigate, prefix, { [name]: null })
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const clearAll = (): void => {
|
|
62
|
-
const patches: Record<string, string | null> = {}
|
|
63
|
-
for (const f of active) patches[String(f['name'] ?? '')] = null
|
|
64
|
-
patchFilterUrl(navigate, prefix, patches)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
69
|
-
{active.map((f, i) => {
|
|
70
|
-
const name = String(f['name'] ?? '')
|
|
71
|
-
const indicator = String(f['indicator'] ?? '')
|
|
72
|
-
return (
|
|
73
|
-
<span
|
|
74
|
-
key={i}
|
|
75
|
-
className="inline-flex items-center gap-1 rounded-full border border-border bg-muted/40 pl-2.5 pr-1 py-0.5"
|
|
76
|
-
>
|
|
77
|
-
<span>{indicator}</span>
|
|
78
|
-
<button
|
|
79
|
-
type="button"
|
|
80
|
-
onClick={() => clear(name)}
|
|
81
|
-
aria-label={`Clear filter ${indicator}`}
|
|
82
|
-
className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
83
|
-
>
|
|
84
|
-
×
|
|
85
|
-
</button>
|
|
86
|
-
</span>
|
|
87
|
-
)
|
|
88
|
-
})}
|
|
89
|
-
{active.length > 1 && (
|
|
90
|
-
<button
|
|
91
|
-
type="button"
|
|
92
|
-
onClick={clearAll}
|
|
93
|
-
className="text-muted-foreground hover:text-foreground underline-offset-2 hover:underline"
|
|
94
|
-
>
|
|
95
|
-
Clear all
|
|
96
|
-
</button>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Filter icon button + Popover containing every filter control.
|
|
104
|
-
* Opens on click; the inner Selects don't dismiss the outer Popover when
|
|
105
|
-
* an option is chosen (Base UI Popover doesn't auto-close on inner clicks).
|
|
106
|
-
*
|
|
107
|
-
* Each FilterSelect navigates the page on change (window.location), so the
|
|
108
|
-
* filter form is no longer needed — keeps the search input in its own
|
|
109
|
-
* lightweight form for native Enter-to-submit.
|
|
110
|
-
*/
|
|
111
|
-
export function FilterPopover({ filters, prefix, renderFormChild }: {
|
|
112
|
-
filters: ElementMeta[]
|
|
113
|
-
prefix?: string | undefined
|
|
114
|
-
renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
|
|
115
|
-
}) {
|
|
116
|
-
const activeCount = filters.filter(f => {
|
|
117
|
-
const v = f['value']
|
|
118
|
-
return typeof v === 'string' && v !== ''
|
|
119
|
-
}).length
|
|
120
|
-
|
|
121
|
-
return (
|
|
122
|
-
<Popover>
|
|
123
|
-
<PopoverTrigger
|
|
124
|
-
render={(props) => (
|
|
125
|
-
<button
|
|
126
|
-
{...props}
|
|
127
|
-
type="button"
|
|
128
|
-
aria-label="Filters"
|
|
129
|
-
className="relative inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
|
130
|
-
>
|
|
131
|
-
<FilterIcon className="size-4" />
|
|
132
|
-
<span>Filters</span>
|
|
133
|
-
{activeCount > 0 && (
|
|
134
|
-
<span className="ml-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
|
|
135
|
-
{activeCount}
|
|
136
|
-
</span>
|
|
137
|
-
)}
|
|
138
|
-
</button>
|
|
139
|
-
)}
|
|
140
|
-
/>
|
|
141
|
-
<PopoverContent align="start" className={
|
|
142
|
-
filters.some(f => f['kind'] === 'queryBuilder')
|
|
143
|
-
? 'w-[36rem] max-w-[calc(100vw-2rem)] p-3'
|
|
144
|
-
: 'w-72 p-3'
|
|
145
|
-
}>
|
|
146
|
-
<div className="flex flex-col gap-3">
|
|
147
|
-
{filters.map((f, i) => renderFilterControl(f, i, prefix, renderFormChild))}
|
|
148
|
-
</div>
|
|
149
|
-
</PopoverContent>
|
|
150
|
-
</Popover>
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Inline strip of filter controls — used by `Table.filtersLayout('above-content'
|
|
156
|
-
* | 'above-content-collapsible' | 'below-content')`. Mirrors `FilterPopover`'s
|
|
157
|
-
* inner body but lays the controls out in a wrapping row instead of a
|
|
158
|
-
* vertical stack inside a popover.
|
|
159
|
-
*/
|
|
160
|
-
export function FilterStrip({ filters, prefix, renderFormChild }: {
|
|
161
|
-
filters: ElementMeta[]
|
|
162
|
-
prefix?: string | undefined
|
|
163
|
-
renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
|
|
164
|
-
}) {
|
|
165
|
-
if (filters.length === 0) return null
|
|
166
|
-
return (
|
|
167
|
-
<div className="flex flex-col gap-3 rounded-md border bg-muted/30 p-3 sm:flex-row sm:flex-wrap sm:items-end">
|
|
168
|
-
{filters.map((f, i) => (
|
|
169
|
-
<div key={i} className="min-w-[12rem] flex-1 sm:max-w-xs">
|
|
170
|
-
{renderFilterControl(f, i, prefix, renderFormChild)}
|
|
171
|
-
</div>
|
|
172
|
-
))}
|
|
173
|
-
</div>
|
|
174
|
-
)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Toolbar button paired with `FilterStrip` for `Table.filtersLayout(
|
|
179
|
-
* 'above-content-collapsible')`. Visually matches the modal-mode trigger
|
|
180
|
-
* (filter icon + "Filters" label + active-count badge) but flips a parent-
|
|
181
|
-
* owned `open` state instead of opening a Popover.
|
|
182
|
-
*/
|
|
183
|
-
export function FilterStripToggle({
|
|
184
|
-
filters, open, onToggle,
|
|
185
|
-
}: {
|
|
186
|
-
filters: ElementMeta[]
|
|
187
|
-
open: boolean
|
|
188
|
-
onToggle: () => void
|
|
189
|
-
}) {
|
|
190
|
-
const activeCount = filters.filter(f => {
|
|
191
|
-
const v = f['value']
|
|
192
|
-
return typeof v === 'string' && v !== ''
|
|
193
|
-
}).length
|
|
194
|
-
return (
|
|
195
|
-
<button
|
|
196
|
-
type="button"
|
|
197
|
-
aria-label="Filters"
|
|
198
|
-
aria-expanded={open}
|
|
199
|
-
onClick={onToggle}
|
|
200
|
-
className="relative inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
|
201
|
-
>
|
|
202
|
-
<FilterIcon className="size-4" />
|
|
203
|
-
<span>Filters</span>
|
|
204
|
-
{activeCount > 0 && (
|
|
205
|
-
<span className="ml-1 inline-flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1.5 text-xs font-medium text-primary-foreground">
|
|
206
|
-
{activeCount}
|
|
207
|
-
</span>
|
|
208
|
-
)}
|
|
209
|
-
</button>
|
|
210
|
-
)
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Filter dropdown that updates the URL directly on change. We don't rely
|
|
215
|
-
* on a wrapping `<form>` because filters now live inside a portaled
|
|
216
|
-
* Popover (the search input keeps its own form for Enter-to-submit).
|
|
217
|
-
*
|
|
218
|
-
* Empty value (`''`) is the "All" sentinel — the param is removed from
|
|
219
|
-
* the URL rather than serialized as `&name=`.
|
|
220
|
-
*/
|
|
221
|
-
export function FilterSelect({
|
|
222
|
-
name, label, defaultValue, placeholder, options, prefix,
|
|
223
|
-
}: {
|
|
224
|
-
name: string
|
|
225
|
-
label: string
|
|
226
|
-
defaultValue: string
|
|
227
|
-
placeholder: string
|
|
228
|
-
options: Array<{ value: string; label: string }>
|
|
229
|
-
prefix?: string | undefined
|
|
230
|
-
}) {
|
|
231
|
-
const [value, setValue] = useState(defaultValue)
|
|
232
|
-
const navigate = useNavigate()
|
|
233
|
-
|
|
234
|
-
const onChange = (next: unknown) => {
|
|
235
|
-
const v = typeof next === 'string' ? next : ''
|
|
236
|
-
setValue(v)
|
|
237
|
-
patchFilterUrl(navigate, prefix, { [name]: v })
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return (
|
|
241
|
-
<div className="flex flex-col gap-1 text-xs">
|
|
242
|
-
<span className="text-muted-foreground">{label}</span>
|
|
243
|
-
<Select value={value} onValueChange={onChange}>
|
|
244
|
-
<SelectTrigger size="sm" className="w-full">
|
|
245
|
-
<SelectValue placeholder={placeholder} />
|
|
246
|
-
</SelectTrigger>
|
|
247
|
-
<SelectContent>
|
|
248
|
-
<SelectItem value="">{placeholder}</SelectItem>
|
|
249
|
-
{options.map(o => (
|
|
250
|
-
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
|
|
251
|
-
))}
|
|
252
|
-
</SelectContent>
|
|
253
|
-
</Select>
|
|
254
|
-
</div>
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Heading-row text for a group band. Shows `<label>: <value-or-title>`
|
|
260
|
-
* with an optional description below. Reused for both collapsible and
|
|
261
|
-
* static heading rows.
|
|
262
|
-
*/
|
|
263
|
-
export function GroupHeaderText({
|
|
264
|
-
label, value, title, description,
|
|
265
|
-
}: {
|
|
266
|
-
label?: string | undefined
|
|
267
|
-
value?: string | undefined
|
|
268
|
-
title?: string | undefined
|
|
269
|
-
description?: string | undefined
|
|
270
|
-
}) {
|
|
271
|
-
const display = title ?? value ?? ''
|
|
272
|
-
return (
|
|
273
|
-
<span className="flex flex-col gap-0.5">
|
|
274
|
-
<span>
|
|
275
|
-
{label && <span className="text-muted-foreground/70">{label}: </span>}
|
|
276
|
-
<span className="text-foreground">{display || 'Ungrouped'}</span>
|
|
277
|
-
</span>
|
|
278
|
-
{description && (
|
|
279
|
-
<span className="text-[10px] font-normal normal-case text-muted-foreground/80">
|
|
280
|
-
{description}
|
|
281
|
-
</span>
|
|
282
|
-
)}
|
|
283
|
-
</span>
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* "Group by" dropdown rendered above the table when 2+ TableGroups
|
|
289
|
-
* are registered (or 1 group with rich metadata). Selecting "None"
|
|
290
|
-
* sets `?group=` (empty) which explicitly overrides `defaultGroup`.
|
|
291
|
-
*
|
|
292
|
-
* URL-driven — `onChange` builds the next href via `buildTableQuery`
|
|
293
|
-
* and SPA-navigates; the page re-renders with the new active group.
|
|
294
|
-
*/
|
|
295
|
-
export function TableGroupPicker({
|
|
296
|
-
options, active, onChange,
|
|
297
|
-
}: {
|
|
298
|
-
options: Array<{ column: string; label: string }>
|
|
299
|
-
active: string | undefined
|
|
300
|
-
onChange: (column: string) => void
|
|
301
|
-
}) {
|
|
302
|
-
const value = active ?? ''
|
|
303
|
-
return (
|
|
304
|
-
<Select value={value} onValueChange={(v) => onChange(typeof v === 'string' ? v : '')}>
|
|
305
|
-
<SelectTrigger size="sm" className="h-9 w-44">
|
|
306
|
-
<SelectValue placeholder="Group by…" />
|
|
307
|
-
</SelectTrigger>
|
|
308
|
-
<SelectContent>
|
|
309
|
-
<SelectItem value="">No grouping</SelectItem>
|
|
310
|
-
{options.map(o => (
|
|
311
|
-
<SelectItem key={o.column} value={o.column}>{o.label}</SelectItem>
|
|
312
|
-
))}
|
|
313
|
-
</SelectContent>
|
|
314
|
-
</Select>
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Pair-of-date-inputs filter for `kind === 'dateRange'`. Each side
|
|
320
|
-
* navigates the URL on change, encoding the pair as `from..to` keyed
|
|
321
|
-
* off the filter name. Empty pair drops the URL key.
|
|
322
|
-
*/
|
|
323
|
-
export function FilterDateRange({
|
|
324
|
-
name, label, defaultValue, placeholder, includesTime, minDate, maxDate, prefix,
|
|
325
|
-
}: {
|
|
326
|
-
name: string
|
|
327
|
-
label: string
|
|
328
|
-
defaultValue: string
|
|
329
|
-
placeholder: string
|
|
330
|
-
includesTime: boolean
|
|
331
|
-
minDate?: string
|
|
332
|
-
maxDate?: string
|
|
333
|
-
prefix?: string | undefined
|
|
334
|
-
}) {
|
|
335
|
-
const initial = parseDateRangeValue(defaultValue)
|
|
336
|
-
const [from, setFrom] = useState(initial.from ?? '')
|
|
337
|
-
const [to, setTo] = useState(initial.to ?? '')
|
|
338
|
-
const navigate = useNavigate()
|
|
339
|
-
|
|
340
|
-
const inputType = includesTime ? 'datetime-local' : 'date'
|
|
341
|
-
|
|
342
|
-
const navigateTo = (nextFrom: string, nextTo: string): void => {
|
|
343
|
-
patchFilterUrl(navigate, prefix, {
|
|
344
|
-
[name]: encodeDateRangeValue({ from: nextFrom, to: nextTo }),
|
|
345
|
-
})
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const onFromChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
349
|
-
const v = e.target.value
|
|
350
|
-
setFrom(v)
|
|
351
|
-
navigateTo(v, to)
|
|
352
|
-
}
|
|
353
|
-
const onToChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
|
354
|
-
const v = e.target.value
|
|
355
|
-
setTo(v)
|
|
356
|
-
navigateTo(from, v)
|
|
357
|
-
}
|
|
358
|
-
const onClear = (): void => {
|
|
359
|
-
setFrom('')
|
|
360
|
-
setTo('')
|
|
361
|
-
navigateTo('', '')
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const hasValue = from !== '' || to !== ''
|
|
365
|
-
|
|
366
|
-
return (
|
|
367
|
-
<div className="flex flex-col gap-1 text-xs">
|
|
368
|
-
<span className="text-muted-foreground">{label}</span>
|
|
369
|
-
<div className="flex items-center gap-1">
|
|
370
|
-
<Input
|
|
371
|
-
type={inputType}
|
|
372
|
-
value={from}
|
|
373
|
-
onChange={onFromChange}
|
|
374
|
-
placeholder={placeholder}
|
|
375
|
-
aria-label={`${label} from`}
|
|
376
|
-
{...(minDate !== undefined ? { min: minDate } : {})}
|
|
377
|
-
{...(maxDate !== undefined ? { max: maxDate } : {})}
|
|
378
|
-
className="h-8 text-xs"
|
|
379
|
-
/>
|
|
380
|
-
<span className="text-muted-foreground">→</span>
|
|
381
|
-
<Input
|
|
382
|
-
type={inputType}
|
|
383
|
-
value={to}
|
|
384
|
-
onChange={onToChange}
|
|
385
|
-
placeholder={placeholder}
|
|
386
|
-
aria-label={`${label} to`}
|
|
387
|
-
{...(minDate !== undefined ? { min: minDate } : {})}
|
|
388
|
-
{...(maxDate !== undefined ? { max: maxDate } : {})}
|
|
389
|
-
className="h-8 text-xs"
|
|
390
|
-
/>
|
|
391
|
-
{hasValue && (
|
|
392
|
-
<button
|
|
393
|
-
type="button"
|
|
394
|
-
onClick={onClear}
|
|
395
|
-
aria-label={`Clear ${label}`}
|
|
396
|
-
className="text-muted-foreground hover:text-foreground px-1"
|
|
397
|
-
>
|
|
398
|
-
×
|
|
399
|
-
</button>
|
|
400
|
-
)}
|
|
401
|
-
</div>
|
|
402
|
-
</div>
|
|
403
|
-
)
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* Multi-value filter for `kind === 'multiSelect'`. Renders a checkbox
|
|
408
|
-
* stack inside the popover; toggling a box patches the comma-separated
|
|
409
|
-
* URL value for the filter's name. Empty selection drops the URL key.
|
|
410
|
-
*/
|
|
411
|
-
export function FilterMultiSelect({
|
|
412
|
-
name, label, defaultValue, options, prefix,
|
|
413
|
-
}: {
|
|
414
|
-
name: string
|
|
415
|
-
label: string
|
|
416
|
-
defaultValue: string
|
|
417
|
-
options: Array<{ value: string; label: string }>
|
|
418
|
-
prefix?: string | undefined
|
|
419
|
-
}) {
|
|
420
|
-
const [selected, setSelected] = useState<string[]>(() => parseMultiSelectValue(defaultValue))
|
|
421
|
-
const navigate = useNavigate()
|
|
422
|
-
|
|
423
|
-
const apply = (next: string[]): void => {
|
|
424
|
-
setSelected(next)
|
|
425
|
-
patchFilterUrl(navigate, prefix, { [name]: encodeMultiSelectValue(next) })
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const toggle = (value: string, checked: boolean): void => {
|
|
429
|
-
const next = checked
|
|
430
|
-
? [...selected.filter(v => v !== value), value]
|
|
431
|
-
: selected.filter(v => v !== value)
|
|
432
|
-
apply(next)
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return (
|
|
436
|
-
<div className="flex flex-col gap-1 text-xs">
|
|
437
|
-
<span className="text-muted-foreground">{label}</span>
|
|
438
|
-
<div className="flex flex-col gap-1.5">
|
|
439
|
-
{options.map(o => {
|
|
440
|
-
const checked = selected.includes(o.value)
|
|
441
|
-
return (
|
|
442
|
-
<label
|
|
443
|
-
key={o.value}
|
|
444
|
-
className="flex items-center gap-2 text-sm cursor-pointer"
|
|
445
|
-
>
|
|
446
|
-
<Checkbox
|
|
447
|
-
checked={checked}
|
|
448
|
-
onCheckedChange={(c: boolean | 'indeterminate') => toggle(o.value, c === true)}
|
|
449
|
-
/>
|
|
450
|
-
<span>{o.label}</span>
|
|
451
|
-
</label>
|
|
452
|
-
)
|
|
453
|
-
})}
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
)
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Multi-field filter for `kind === 'form'`. The popover renders an inner
|
|
461
|
-
* sub-form with the user-declared schema; submitting bundles all named
|
|
462
|
-
* inputs into a `Record<string, unknown>`, JSON-encodes the non-empty
|
|
463
|
-
* subset under the filter's URL key, and SPA-navigates. Empty submit
|
|
464
|
-
* drops the URL key entirely.
|
|
465
|
-
*
|
|
466
|
-
* The fields' `defaultValue` were pre-hydrated server-side from the
|
|
467
|
-
* active URL value (see `FormFilter.toMeta`), so an existing filter
|
|
468
|
-
* round-trips into the form on render. Inputs are uncontrolled — we
|
|
469
|
-
* read state via `new FormData(form)` on submit, matching how the
|
|
470
|
-
* outer page-level Form works on full submit.
|
|
471
|
-
*/
|
|
472
|
-
export function FilterForm({
|
|
473
|
-
name, label, defaultValue, formSchema, prefix, renderFormChild,
|
|
474
|
-
}: {
|
|
475
|
-
name: string
|
|
476
|
-
label: string
|
|
477
|
-
defaultValue: string
|
|
478
|
-
formSchema: ElementMeta[]
|
|
479
|
-
prefix?: string | undefined
|
|
480
|
-
// Injected from the top-level dispatch — `renderFormChild` lives in
|
|
481
|
-
// the form layer (Phase 4) and can't be imported directly without
|
|
482
|
-
// a cycle through `SchemaRenderer.tsx`.
|
|
483
|
-
renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
|
|
484
|
-
}) {
|
|
485
|
-
const formRef = useRef<HTMLFormElement>(null)
|
|
486
|
-
const navigate = useNavigate()
|
|
487
|
-
const hasValue = defaultValue !== '' && defaultValue !== '{}'
|
|
488
|
-
|
|
489
|
-
const onApply = (e?: React.FormEvent | React.MouseEvent): void => {
|
|
490
|
-
e?.preventDefault()
|
|
491
|
-
if (!formRef.current) return
|
|
492
|
-
const fd = new FormData(formRef.current)
|
|
493
|
-
const values: Record<string, unknown> = {}
|
|
494
|
-
for (const [key, val] of fd.entries()) {
|
|
495
|
-
const existing = values[key]
|
|
496
|
-
if (existing === undefined) {
|
|
497
|
-
values[key] = val
|
|
498
|
-
} else if (Array.isArray(existing)) {
|
|
499
|
-
(existing as unknown[]).push(val)
|
|
500
|
-
} else {
|
|
501
|
-
values[key] = [existing, val]
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
patchFilterUrl(navigate, prefix, { [name]: encodeFormFilterValue(values) })
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const onClear = (): void => {
|
|
508
|
-
patchFilterUrl(navigate, prefix, { [name]: null })
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return (
|
|
512
|
-
<div className="flex flex-col gap-2">
|
|
513
|
-
<span className="text-muted-foreground text-xs">{label}</span>
|
|
514
|
-
<form ref={formRef} onSubmit={onApply} className="flex flex-col gap-2">
|
|
515
|
-
{formSchema.map((child, i) => renderFormChild(child, i, {}, {}))}
|
|
516
|
-
<div className="flex gap-2 pt-1">
|
|
517
|
-
<button
|
|
518
|
-
type="submit"
|
|
519
|
-
className="inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
520
|
-
>
|
|
521
|
-
Apply
|
|
522
|
-
</button>
|
|
523
|
-
{hasValue && (
|
|
524
|
-
<button
|
|
525
|
-
type="button"
|
|
526
|
-
onClick={onClear}
|
|
527
|
-
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
|
|
528
|
-
>
|
|
529
|
-
Clear
|
|
530
|
-
</button>
|
|
531
|
-
)}
|
|
532
|
-
</div>
|
|
533
|
-
</form>
|
|
534
|
-
</div>
|
|
535
|
-
)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Composable advanced filter for `kind === 'queryBuilder'`. v2 emits a
|
|
540
|
-
* full tree — root AND/OR connector + nested groups arbitrarily deep —
|
|
541
|
-
* JSON-encoded into a single URL key on Apply (see
|
|
542
|
-
* `encodeQueryBuilderValue`).
|
|
543
|
-
*
|
|
544
|
-
* State is local — typing into a value input doesn't navigate. Only the
|
|
545
|
-
* Apply button writes the URL. This mirrors `FilterForm`'s behavior and
|
|
546
|
-
* keeps the popover quiet under the cursor.
|
|
547
|
-
*/
|
|
548
|
-
export function FilterQueryBuilder({
|
|
549
|
-
name, label, defaultValue, constraints, prefix,
|
|
550
|
-
}: {
|
|
551
|
-
name: string
|
|
552
|
-
label: string
|
|
553
|
-
defaultValue: string
|
|
554
|
-
constraints: ConstraintMeta[]
|
|
555
|
-
prefix?: string | undefined
|
|
556
|
-
}) {
|
|
557
|
-
const navigate = useNavigate()
|
|
558
|
-
const initialTree = parseQueryBuilderValue(defaultValue)
|
|
559
|
-
const [tree, setTree] = useState<QueryBuilderTree>(initialTree)
|
|
560
|
-
const hasValue = defaultValue !== '' && initialTree.rules.length > 0
|
|
561
|
-
|
|
562
|
-
const onApply = (e?: React.FormEvent | React.MouseEvent): void => {
|
|
563
|
-
e?.preventDefault()
|
|
564
|
-
patchFilterUrl(navigate, prefix, { [name]: encodeQueryBuilderValue(tree) })
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
const onClear = (): void => {
|
|
568
|
-
setTree({ operator: 'and', rules: [] })
|
|
569
|
-
patchFilterUrl(navigate, prefix, { [name]: null })
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
if (constraints.length === 0) {
|
|
573
|
-
return (
|
|
574
|
-
<div className="text-muted-foreground text-xs">
|
|
575
|
-
{label}: no constraints declared.
|
|
576
|
-
</div>
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return (
|
|
581
|
-
<div className="flex flex-col gap-2 min-w-[24rem]">
|
|
582
|
-
<span className="text-muted-foreground text-xs">{label}</span>
|
|
583
|
-
<form onSubmit={onApply} className="flex flex-col gap-2">
|
|
584
|
-
<QueryBuilderGroup
|
|
585
|
-
tree={tree}
|
|
586
|
-
constraints={constraints}
|
|
587
|
-
isRoot={true}
|
|
588
|
-
onChange={setTree}
|
|
589
|
-
/>
|
|
590
|
-
<div className="flex items-center gap-2 pt-1">
|
|
591
|
-
<div className="flex-1" />
|
|
592
|
-
<button
|
|
593
|
-
type="submit"
|
|
594
|
-
className="inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
|
595
|
-
>
|
|
596
|
-
Apply
|
|
597
|
-
</button>
|
|
598
|
-
{(hasValue || tree.rules.length > 0) && (
|
|
599
|
-
<button
|
|
600
|
-
type="button"
|
|
601
|
-
onClick={onClear}
|
|
602
|
-
className="inline-flex h-8 items-center justify-center rounded-md border border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
|
|
603
|
-
>
|
|
604
|
-
Clear
|
|
605
|
-
</button>
|
|
606
|
-
)}
|
|
607
|
-
</div>
|
|
608
|
-
</form>
|
|
609
|
-
</div>
|
|
610
|
-
)
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Recursive group renderer — emits a connector picker (AND / OR) at the
|
|
615
|
-
* top, a vertical stack of children (rules and sub-groups), and footer
|
|
616
|
-
* buttons for "+ Add condition" and "+ Add group". Calls `onChange` with
|
|
617
|
-
* the updated sub-tree so parents can splice it back into their own
|
|
618
|
-
* `rules` array. Root groups skip the outer border so the popover doesn't
|
|
619
|
-
* carry a redundant frame; nested groups draw a faint left rule + soft
|
|
620
|
-
* background so the nesting is visible without blowing up the width.
|
|
621
|
-
*/
|
|
622
|
-
export function QueryBuilderGroup({
|
|
623
|
-
tree, constraints, isRoot, onChange, onRemove,
|
|
624
|
-
}: {
|
|
625
|
-
tree: QueryBuilderTree
|
|
626
|
-
constraints: ConstraintMeta[]
|
|
627
|
-
isRoot: boolean
|
|
628
|
-
onChange: (next: QueryBuilderTree) => void
|
|
629
|
-
onRemove?: () => void
|
|
630
|
-
}) {
|
|
631
|
-
const constraintMap = new Map<string, ConstraintMeta>()
|
|
632
|
-
for (const c of constraints) constraintMap.set(c.name, c)
|
|
633
|
-
|
|
634
|
-
const setOperator = (op: 'and' | 'or'): void => {
|
|
635
|
-
onChange({ ...tree, operator: op })
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const updateChildAt = (index: number, next: QueryBuilderTreeChild): void => {
|
|
639
|
-
onChange({ ...tree, rules: tree.rules.map((r, i) => i === index ? next : r) })
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const removeChildAt = (index: number): void => {
|
|
643
|
-
onChange({ ...tree, rules: tree.rules.filter((_, i) => i !== index) })
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
const addRule = (): void => {
|
|
647
|
-
const first = constraints[0]
|
|
648
|
-
if (!first) return
|
|
649
|
-
onChange({
|
|
650
|
-
...tree,
|
|
651
|
-
rules: [...tree.rules, {
|
|
652
|
-
constraint: first.name,
|
|
653
|
-
operator: first.defaultOperator ?? first.operators[0]?.name ?? 'equals',
|
|
654
|
-
value: undefined,
|
|
655
|
-
}],
|
|
656
|
-
})
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const addGroup = (): void => {
|
|
660
|
-
onChange({
|
|
661
|
-
...tree,
|
|
662
|
-
rules: [...tree.rules, { operator: 'and', rules: [] }],
|
|
663
|
-
})
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const wrapper = isRoot
|
|
667
|
-
? 'flex flex-col gap-2'
|
|
668
|
-
: 'flex flex-col gap-2 rounded-md border-l-2 border-primary/40 bg-muted/30 pl-2 py-2 pr-2'
|
|
669
|
-
|
|
670
|
-
return (
|
|
671
|
-
<div className={wrapper}>
|
|
672
|
-
<div className="flex items-center gap-2">
|
|
673
|
-
<ConnectorToggle value={tree.operator} onChange={setOperator} />
|
|
674
|
-
<span className="text-muted-foreground text-[11px]">
|
|
675
|
-
{tree.operator === 'and' ? 'Match all of the following' : 'Match any of the following'}
|
|
676
|
-
</span>
|
|
677
|
-
{!isRoot && onRemove && (
|
|
678
|
-
<>
|
|
679
|
-
<div className="flex-1" />
|
|
680
|
-
<button
|
|
681
|
-
type="button"
|
|
682
|
-
onClick={onRemove}
|
|
683
|
-
aria-label="Remove group"
|
|
684
|
-
className="inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
685
|
-
>
|
|
686
|
-
×
|
|
687
|
-
</button>
|
|
688
|
-
</>
|
|
689
|
-
)}
|
|
690
|
-
</div>
|
|
691
|
-
|
|
692
|
-
{tree.rules.length === 0 && (
|
|
693
|
-
<div className="text-muted-foreground text-xs italic">No conditions yet.</div>
|
|
694
|
-
)}
|
|
695
|
-
|
|
696
|
-
{tree.rules.map((child, i) => {
|
|
697
|
-
if (isQueryBuilderTree(child)) {
|
|
698
|
-
return (
|
|
699
|
-
<QueryBuilderGroup
|
|
700
|
-
key={i}
|
|
701
|
-
tree={child}
|
|
702
|
-
constraints={constraints}
|
|
703
|
-
isRoot={false}
|
|
704
|
-
onChange={(next) => updateChildAt(i, next)}
|
|
705
|
-
onRemove={() => removeChildAt(i)}
|
|
706
|
-
/>
|
|
707
|
-
)
|
|
708
|
-
}
|
|
709
|
-
return (
|
|
710
|
-
<QueryBuilderRow
|
|
711
|
-
key={i}
|
|
712
|
-
rule={child}
|
|
713
|
-
constraints={constraints}
|
|
714
|
-
constraintMeta={constraintMap.get(child.constraint)}
|
|
715
|
-
onConstraintChange={(v) => {
|
|
716
|
-
const c = constraintMap.get(v)
|
|
717
|
-
if (!c) return
|
|
718
|
-
updateChildAt(i, {
|
|
719
|
-
constraint: v,
|
|
720
|
-
operator: c.defaultOperator ?? c.operators[0]?.name ?? 'equals',
|
|
721
|
-
value: undefined,
|
|
722
|
-
})
|
|
723
|
-
}}
|
|
724
|
-
onOperatorChange={(v) => {
|
|
725
|
-
updateChildAt(i, {
|
|
726
|
-
...child,
|
|
727
|
-
operator: v as ConstraintOperatorName,
|
|
728
|
-
value: undefined,
|
|
729
|
-
})
|
|
730
|
-
}}
|
|
731
|
-
onValueChange={(v) => updateChildAt(i, { ...child, value: v })}
|
|
732
|
-
onRemove={() => removeChildAt(i)}
|
|
733
|
-
/>
|
|
734
|
-
)
|
|
735
|
-
})}
|
|
736
|
-
|
|
737
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
738
|
-
<button
|
|
739
|
-
type="button"
|
|
740
|
-
onClick={addRule}
|
|
741
|
-
className="inline-flex h-8 items-center justify-center rounded-md border border-dashed border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
|
|
742
|
-
>
|
|
743
|
-
+ Add condition
|
|
744
|
-
</button>
|
|
745
|
-
<button
|
|
746
|
-
type="button"
|
|
747
|
-
onClick={addGroup}
|
|
748
|
-
className="inline-flex h-8 items-center justify-center rounded-md border border-dashed border-input bg-background px-3 text-xs font-medium hover:bg-accent hover:text-accent-foreground"
|
|
749
|
-
>
|
|
750
|
-
+ Add group
|
|
751
|
-
</button>
|
|
752
|
-
</div>
|
|
753
|
-
</div>
|
|
754
|
-
)
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Compact AND/OR segmented control used at the head of every group. Pure
|
|
759
|
-
* presentation — the parent owns the value.
|
|
760
|
-
*/
|
|
761
|
-
export function ConnectorToggle({
|
|
762
|
-
value, onChange,
|
|
763
|
-
}: {
|
|
764
|
-
value: 'and' | 'or'
|
|
765
|
-
onChange: (next: 'and' | 'or') => void
|
|
766
|
-
}) {
|
|
767
|
-
const base = 'inline-flex h-7 items-center px-2 text-[11px] font-medium uppercase tracking-wide transition'
|
|
768
|
-
const on = 'bg-primary text-primary-foreground'
|
|
769
|
-
const off = 'bg-background text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
770
|
-
return (
|
|
771
|
-
<div className="inline-flex overflow-hidden rounded-md border border-input">
|
|
772
|
-
<button
|
|
773
|
-
type="button"
|
|
774
|
-
onClick={() => onChange('and')}
|
|
775
|
-
className={`${base} ${value === 'and' ? on : off}`}
|
|
776
|
-
aria-pressed={value === 'and'}
|
|
777
|
-
>
|
|
778
|
-
AND
|
|
779
|
-
</button>
|
|
780
|
-
<button
|
|
781
|
-
type="button"
|
|
782
|
-
onClick={() => onChange('or')}
|
|
783
|
-
className={`${base} ${value === 'or' ? on : off}`}
|
|
784
|
-
aria-pressed={value === 'or'}
|
|
785
|
-
>
|
|
786
|
-
OR
|
|
787
|
-
</button>
|
|
788
|
-
</div>
|
|
789
|
-
)
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* One condition row inside `FilterQueryBuilder`. Three controls
|
|
794
|
-
* left-to-right: constraint picker, operator picker, value input. The
|
|
795
|
-
* value input dispatches off the operator's `valueKind` — `none` hides
|
|
796
|
-
* it entirely, `numberRange` / `dateRange` mount a pair, otherwise a
|
|
797
|
-
* single typed input.
|
|
798
|
-
*/
|
|
799
|
-
export function QueryBuilderRow({
|
|
800
|
-
rule, constraints, constraintMeta,
|
|
801
|
-
onConstraintChange, onOperatorChange, onValueChange, onRemove,
|
|
802
|
-
}: {
|
|
803
|
-
rule: QueryBuilderRule
|
|
804
|
-
constraints: ConstraintMeta[]
|
|
805
|
-
constraintMeta: ConstraintMeta | undefined
|
|
806
|
-
onConstraintChange: (name: string) => void
|
|
807
|
-
onOperatorChange: (name: string) => void
|
|
808
|
-
onValueChange: (value: unknown) => void
|
|
809
|
-
onRemove: () => void
|
|
810
|
-
}) {
|
|
811
|
-
const operators: ConstraintOperator[] = constraintMeta?.operators ?? []
|
|
812
|
-
const activeOp = operators.find(o => o.name === rule.operator)
|
|
813
|
-
const valueKind: ConstraintValueKind = activeOp?.valueKind ?? 'text'
|
|
814
|
-
|
|
815
|
-
return (
|
|
816
|
-
<div className="flex items-start gap-1.5 rounded-md border border-input bg-background p-2">
|
|
817
|
-
<div className="flex flex-1 flex-wrap items-center gap-1.5">
|
|
818
|
-
<Select value={rule.constraint} onValueChange={(v) => onConstraintChange(typeof v === 'string' ? v : '')}>
|
|
819
|
-
<SelectTrigger size="sm" className="h-8 w-36 text-xs">
|
|
820
|
-
<SelectValue />
|
|
821
|
-
</SelectTrigger>
|
|
822
|
-
<SelectContent>
|
|
823
|
-
{constraints.map(c => (
|
|
824
|
-
<SelectItem key={c.name} value={c.name}>{c.label}</SelectItem>
|
|
825
|
-
))}
|
|
826
|
-
</SelectContent>
|
|
827
|
-
</Select>
|
|
828
|
-
|
|
829
|
-
<Select value={rule.operator} onValueChange={(v) => onOperatorChange(typeof v === 'string' ? v : '')}>
|
|
830
|
-
<SelectTrigger size="sm" className="h-8 w-32 text-xs">
|
|
831
|
-
<SelectValue />
|
|
832
|
-
</SelectTrigger>
|
|
833
|
-
<SelectContent>
|
|
834
|
-
{operators.map(o => (
|
|
835
|
-
<SelectItem key={o.name} value={o.name}>{o.label}</SelectItem>
|
|
836
|
-
))}
|
|
837
|
-
</SelectContent>
|
|
838
|
-
</Select>
|
|
839
|
-
|
|
840
|
-
<QueryBuilderValueInput
|
|
841
|
-
kind={valueKind}
|
|
842
|
-
value={rule.value}
|
|
843
|
-
options={constraintMeta?.options}
|
|
844
|
-
onChange={onValueChange}
|
|
845
|
-
/>
|
|
846
|
-
</div>
|
|
847
|
-
<button
|
|
848
|
-
type="button"
|
|
849
|
-
onClick={onRemove}
|
|
850
|
-
aria-label="Remove condition"
|
|
851
|
-
className="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
852
|
-
>
|
|
853
|
-
×
|
|
854
|
-
</button>
|
|
855
|
-
</div>
|
|
856
|
-
)
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* Operator-aware value control. Switches over the constraint operator's
|
|
861
|
-
* `valueKind` and mounts the matching input. Value shapes:
|
|
862
|
-
* - `text / number / date / dateTime / select` → scalar
|
|
863
|
-
* - `multiSelect` → string[]
|
|
864
|
-
* - `numberRange / dateRange` → [string, string]
|
|
865
|
-
* - `boolean / none` → null / undefined
|
|
866
|
-
*/
|
|
867
|
-
export function QueryBuilderValueInput({
|
|
868
|
-
kind, value, options, onChange,
|
|
869
|
-
}: {
|
|
870
|
-
kind: ConstraintValueKind
|
|
871
|
-
value: unknown
|
|
872
|
-
options: Array<{ value: string; label: string }> | undefined
|
|
873
|
-
onChange: (next: unknown) => void
|
|
874
|
-
}) {
|
|
875
|
-
if (kind === 'none' || kind === 'boolean') return null
|
|
876
|
-
|
|
877
|
-
if (kind === 'select') {
|
|
878
|
-
const opts = options ?? []
|
|
879
|
-
const v = value === undefined || value === null ? '' : String(value)
|
|
880
|
-
return (
|
|
881
|
-
<Select value={v} onValueChange={(next) => onChange(typeof next === 'string' ? next : '')}>
|
|
882
|
-
<SelectTrigger size="sm" className="h-8 min-w-32 text-xs">
|
|
883
|
-
<SelectValue placeholder="Pick…" />
|
|
884
|
-
</SelectTrigger>
|
|
885
|
-
<SelectContent>
|
|
886
|
-
{opts.map(o => (
|
|
887
|
-
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
|
|
888
|
-
))}
|
|
889
|
-
</SelectContent>
|
|
890
|
-
</Select>
|
|
891
|
-
)
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
if (kind === 'multiSelect') {
|
|
895
|
-
const opts = options ?? []
|
|
896
|
-
const list = Array.isArray(value) ? value.map(v => String(v)) : []
|
|
897
|
-
const toggle = (val: string): void => {
|
|
898
|
-
if (list.includes(val)) onChange(list.filter(v => v !== val))
|
|
899
|
-
else onChange([...list, val])
|
|
900
|
-
}
|
|
901
|
-
return (
|
|
902
|
-
<div className="flex flex-wrap items-center gap-1">
|
|
903
|
-
{opts.map(o => {
|
|
904
|
-
const active = list.includes(o.value)
|
|
905
|
-
return (
|
|
906
|
-
<button
|
|
907
|
-
key={o.value}
|
|
908
|
-
type="button"
|
|
909
|
-
onClick={() => toggle(o.value)}
|
|
910
|
-
className={
|
|
911
|
-
'inline-flex h-7 items-center rounded-md border px-2 text-xs ' +
|
|
912
|
-
(active
|
|
913
|
-
? 'border-primary bg-primary text-primary-foreground'
|
|
914
|
-
: 'border-input bg-background hover:bg-accent')
|
|
915
|
-
}
|
|
916
|
-
>
|
|
917
|
-
{o.label}
|
|
918
|
-
</button>
|
|
919
|
-
)
|
|
920
|
-
})}
|
|
921
|
-
</div>
|
|
922
|
-
)
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (kind === 'numberRange') {
|
|
926
|
-
const [min, max] = Array.isArray(value) ? [value[0], value[1]] : [undefined, undefined]
|
|
927
|
-
return (
|
|
928
|
-
<div className="flex items-center gap-1">
|
|
929
|
-
<Input
|
|
930
|
-
type="number"
|
|
931
|
-
className="h-8 w-24 text-xs"
|
|
932
|
-
value={min === undefined || min === null ? '' : String(min)}
|
|
933
|
-
onChange={(e) => onChange([e.target.value, max ?? ''])}
|
|
934
|
-
placeholder="Min"
|
|
935
|
-
/>
|
|
936
|
-
<span className="text-muted-foreground text-xs">–</span>
|
|
937
|
-
<Input
|
|
938
|
-
type="number"
|
|
939
|
-
className="h-8 w-24 text-xs"
|
|
940
|
-
value={max === undefined || max === null ? '' : String(max)}
|
|
941
|
-
onChange={(e) => onChange([min ?? '', e.target.value])}
|
|
942
|
-
placeholder="Max"
|
|
943
|
-
/>
|
|
944
|
-
</div>
|
|
945
|
-
)
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (kind === 'dateRange') {
|
|
949
|
-
const [from, to] = Array.isArray(value) ? [value[0], value[1]] : [undefined, undefined]
|
|
950
|
-
return (
|
|
951
|
-
<div className="flex items-center gap-1">
|
|
952
|
-
<Input
|
|
953
|
-
type="date"
|
|
954
|
-
className="h-8 w-36 text-xs"
|
|
955
|
-
value={from === undefined || from === null ? '' : String(from)}
|
|
956
|
-
onChange={(e) => onChange([e.target.value, to ?? ''])}
|
|
957
|
-
/>
|
|
958
|
-
<span className="text-muted-foreground text-xs">→</span>
|
|
959
|
-
<Input
|
|
960
|
-
type="date"
|
|
961
|
-
className="h-8 w-36 text-xs"
|
|
962
|
-
value={to === undefined || to === null ? '' : String(to)}
|
|
963
|
-
onChange={(e) => onChange([from ?? '', e.target.value])}
|
|
964
|
-
/>
|
|
965
|
-
</div>
|
|
966
|
-
)
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
if (kind === 'date' || kind === 'dateTime') {
|
|
970
|
-
const v = value === undefined || value === null ? '' : String(value)
|
|
971
|
-
return (
|
|
972
|
-
<Input
|
|
973
|
-
type={kind === 'dateTime' ? 'datetime-local' : 'date'}
|
|
974
|
-
className="h-8 w-44 text-xs"
|
|
975
|
-
value={v}
|
|
976
|
-
onChange={(e) => onChange(e.target.value)}
|
|
977
|
-
/>
|
|
978
|
-
)
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
if (kind === 'number') {
|
|
982
|
-
const v = value === undefined || value === null ? '' : String(value)
|
|
983
|
-
return (
|
|
984
|
-
<Input
|
|
985
|
-
type="number"
|
|
986
|
-
className="h-8 w-32 text-xs"
|
|
987
|
-
value={v}
|
|
988
|
-
onChange={(e) => onChange(e.target.value)}
|
|
989
|
-
placeholder="Value"
|
|
990
|
-
/>
|
|
991
|
-
)
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Default: text
|
|
995
|
-
const v = value === undefined || value === null ? '' : String(value)
|
|
996
|
-
return (
|
|
997
|
-
<Input
|
|
998
|
-
type="text"
|
|
999
|
-
className="h-8 min-w-32 flex-1 text-xs"
|
|
1000
|
-
value={v}
|
|
1001
|
-
onChange={(e) => onChange(e.target.value)}
|
|
1002
|
-
placeholder="Value"
|
|
1003
|
-
/>
|
|
1004
|
-
)
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
export function renderFilterControl(
|
|
1008
|
-
el: ElementMeta,
|
|
1009
|
-
index: number,
|
|
1010
|
-
prefix: string | undefined,
|
|
1011
|
-
renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode,
|
|
1012
|
-
): React.ReactNode {
|
|
1013
|
-
const name = String(el['name'] ?? '')
|
|
1014
|
-
const label = String(el['label'] ?? name)
|
|
1015
|
-
const kind = String(el['kind'] ?? 'select')
|
|
1016
|
-
const value = el['value'] ? String(el['value']) : ''
|
|
1017
|
-
const placeholder = el['placeholder'] ? String(el['placeholder']) : 'All'
|
|
1018
|
-
|
|
1019
|
-
if (kind === 'queryBuilder') {
|
|
1020
|
-
const constraints = (el['constraints'] as ConstraintMeta[] | undefined) ?? []
|
|
1021
|
-
return (
|
|
1022
|
-
<FilterQueryBuilder
|
|
1023
|
-
key={index}
|
|
1024
|
-
name={name}
|
|
1025
|
-
label={label}
|
|
1026
|
-
defaultValue={value}
|
|
1027
|
-
constraints={constraints}
|
|
1028
|
-
prefix={prefix}
|
|
1029
|
-
/>
|
|
1030
|
-
)
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
if (kind === 'form') {
|
|
1034
|
-
const formSchema = (el['formSchema'] as ElementMeta[] | undefined) ?? []
|
|
1035
|
-
return (
|
|
1036
|
-
<FilterForm
|
|
1037
|
-
key={index}
|
|
1038
|
-
name={name}
|
|
1039
|
-
label={label}
|
|
1040
|
-
defaultValue={value}
|
|
1041
|
-
formSchema={formSchema}
|
|
1042
|
-
prefix={prefix}
|
|
1043
|
-
renderFormChild={renderFormChild}
|
|
1044
|
-
/>
|
|
1045
|
-
)
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
if (kind === 'boolean') {
|
|
1049
|
-
return (
|
|
1050
|
-
<FilterSelect
|
|
1051
|
-
key={index}
|
|
1052
|
-
name={name}
|
|
1053
|
-
label={label}
|
|
1054
|
-
defaultValue={value}
|
|
1055
|
-
placeholder={placeholder}
|
|
1056
|
-
options={[{ value: '1', label: 'Yes' }, { value: '0', label: 'No' }]}
|
|
1057
|
-
prefix={prefix}
|
|
1058
|
-
/>
|
|
1059
|
-
)
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (kind === 'multiSelect') {
|
|
1063
|
-
const options = (el['options'] as Array<{ value: string; label: string }> | undefined) ?? []
|
|
1064
|
-
return (
|
|
1065
|
-
<FilterMultiSelect
|
|
1066
|
-
key={index}
|
|
1067
|
-
name={name}
|
|
1068
|
-
label={label}
|
|
1069
|
-
defaultValue={value}
|
|
1070
|
-
options={options}
|
|
1071
|
-
prefix={prefix}
|
|
1072
|
-
/>
|
|
1073
|
-
)
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
if (kind === 'dateRange') {
|
|
1077
|
-
const includesTime = Boolean(el['includesTime'])
|
|
1078
|
-
const minDate = el['minDate'] ? String(el['minDate']) : undefined
|
|
1079
|
-
const maxDate = el['maxDate'] ? String(el['maxDate']) : undefined
|
|
1080
|
-
return (
|
|
1081
|
-
<FilterDateRange
|
|
1082
|
-
key={index}
|
|
1083
|
-
name={name}
|
|
1084
|
-
label={label}
|
|
1085
|
-
defaultValue={value}
|
|
1086
|
-
placeholder={placeholder}
|
|
1087
|
-
includesTime={includesTime}
|
|
1088
|
-
prefix={prefix}
|
|
1089
|
-
{...(minDate !== undefined ? { minDate } : {})}
|
|
1090
|
-
{...(maxDate !== undefined ? { maxDate } : {})}
|
|
1091
|
-
/>
|
|
1092
|
-
)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// 'ternary' and 'select' both render as a single-select dropdown,
|
|
1096
|
-
// differing only in their server-supplied option set.
|
|
1097
|
-
const options = (el['options'] as Array<{ value: string; label: string }> | undefined) ?? []
|
|
1098
|
-
return (
|
|
1099
|
-
<FilterSelect
|
|
1100
|
-
key={index}
|
|
1101
|
-
name={name}
|
|
1102
|
-
label={label}
|
|
1103
|
-
defaultValue={value}
|
|
1104
|
-
placeholder={placeholder}
|
|
1105
|
-
options={options}
|
|
1106
|
-
prefix={prefix}
|
|
1107
|
-
/>
|
|
1108
|
-
)
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
/**
|
|
1112
|
-
* Resolve the record URL for a single data cell. Column-level override
|
|
1113
|
-
* (`Column.recordUrl(fn)` → `_columnRecordUrls[name]`) wins over the
|
|
1114
|
-
* table-level `Table.recordUrl(fn)` (`_recordUrl`). Explicit per-column
|
|
1115
|
-
* opt-out (`Column.recordUrl(false)` → `meta.recordUrl === false`)
|
|
1116
|
-
* suppresses the link entirely. Returns `undefined` when the cell is
|
|
1117
|
-
* not linkable, in which case the renderer leaves it unwrapped.
|
|
1118
|
-
*/
|
|
1119
|
-
export function resolveColumnUrl(
|
|
1120
|
-
col: ElementMeta,
|
|
1121
|
-
tableUrl: string | undefined,
|
|
1122
|
-
colUrls: Record<string, string>,
|
|
1123
|
-
): string | undefined {
|
|
1124
|
-
if (col['recordUrl'] === false) return undefined
|
|
1125
|
-
const own = colUrls[String(col['name'] ?? '')]
|
|
1126
|
-
if (own !== undefined) return own
|
|
1127
|
-
return tableUrl
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Toolbar "Sort by" picker — surfaces in cards layout where there's no
|
|
1132
|
-
* column-header click affordance to flip sort order. In standard table
|
|
1133
|
-
* mode, this picker appears in the top bar instead. Each `Column` flagged
|
|
1134
|
-
* `.sortable()` contributes two options — ascending and descending —
|
|
1135
|
-
* yielding "Title (A→Z) / Title (Z→A) / Date (oldest first) / Date (newest
|
|
1136
|
-
* first)" style entries. Selecting an option resets `?page=1`.
|
|
1137
|
-
*/
|
|
1138
|
-
export function SortByPicker({
|
|
1139
|
-
columns, active, onChange,
|
|
1140
|
-
}: {
|
|
1141
|
-
columns: ElementMeta[]
|
|
1142
|
-
active: { column: string; direction: 'asc' | 'desc' } | undefined
|
|
1143
|
-
onChange: (column: string, direction: 'asc' | 'desc') => void
|
|
1144
|
-
}) {
|
|
1145
|
-
const sortable = columns.filter(c => Boolean(c['sortable']))
|
|
1146
|
-
if (sortable.length === 0) return null
|
|
1147
|
-
const value = active ? `${active.column}:${active.direction}` : ''
|
|
1148
|
-
return (
|
|
1149
|
-
<Select
|
|
1150
|
-
value={value}
|
|
1151
|
-
onValueChange={(v) => {
|
|
1152
|
-
if (typeof v !== 'string' || v === '') return
|
|
1153
|
-
const idx = v.indexOf(':')
|
|
1154
|
-
if (idx < 0) return
|
|
1155
|
-
const col = v.slice(0, idx)
|
|
1156
|
-
const dir = v.slice(idx + 1) === 'desc' ? 'desc' : 'asc'
|
|
1157
|
-
onChange(col, dir as 'asc' | 'desc')
|
|
1158
|
-
}}
|
|
1159
|
-
>
|
|
1160
|
-
<SelectTrigger size="sm" className="h-9 w-44">
|
|
1161
|
-
<SelectValue placeholder="Sort by…" />
|
|
1162
|
-
</SelectTrigger>
|
|
1163
|
-
<SelectContent>
|
|
1164
|
-
{sortable.map(col => {
|
|
1165
|
-
const name = String(col['name'] ?? '')
|
|
1166
|
-
const label = String(col['label'] ?? name)
|
|
1167
|
-
return (
|
|
1168
|
-
<React.Fragment key={name}>
|
|
1169
|
-
<SelectItem value={`${name}:asc`}>{label} (A→Z)</SelectItem>
|
|
1170
|
-
<SelectItem value={`${name}:desc`}>{label} (Z→A)</SelectItem>
|
|
1171
|
-
</React.Fragment>
|
|
1172
|
-
)
|
|
1173
|
-
})}
|
|
1174
|
-
</SelectContent>
|
|
1175
|
-
</Select>
|
|
1176
|
-
)
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* Toolbar dropdown for `Column.toggleable()` columns. Lists every
|
|
1181
|
-
* toggleable column with a checkbox; toggling writes through to a
|
|
1182
|
-
* caller-supplied `onToggle` (the `TableRendererBody` owns the state
|
|
1183
|
-
* + the localStorage round-trip). Mounted only when at least one
|
|
1184
|
-
* column is toggleable.
|
|
1185
|
-
*/
|
|
1186
|
-
export function ColumnsToggleDropdown({
|
|
1187
|
-
columns, hidden, onToggle,
|
|
1188
|
-
}: {
|
|
1189
|
-
columns: ElementMeta[]
|
|
1190
|
-
hidden: Set<string>
|
|
1191
|
-
onToggle: (name: string, nextHidden: boolean) => void
|
|
1192
|
-
}) {
|
|
1193
|
-
if (columns.length === 0) return null
|
|
1194
|
-
return (
|
|
1195
|
-
<DropdownMenu>
|
|
1196
|
-
<DropdownMenuTrigger
|
|
1197
|
-
render={(props) => (
|
|
1198
|
-
<button
|
|
1199
|
-
{...props}
|
|
1200
|
-
type="button"
|
|
1201
|
-
className="inline-flex h-9 items-center gap-1.5 rounded-md border border-input bg-background px-3 text-sm font-medium text-foreground hover:bg-accent"
|
|
1202
|
-
aria-label="Show or hide columns"
|
|
1203
|
-
>
|
|
1204
|
-
<Columns3Icon className="h-4 w-4" aria-hidden="true" />
|
|
1205
|
-
<span>Columns</span>
|
|
1206
|
-
</button>
|
|
1207
|
-
)}
|
|
1208
|
-
/>
|
|
1209
|
-
<DropdownMenuContent align="end" className="min-w-[12rem]">
|
|
1210
|
-
{columns.map((col, i) => {
|
|
1211
|
-
const name = String(col['name'] ?? '')
|
|
1212
|
-
const label = String(col['label'] ?? name)
|
|
1213
|
-
const isHidden = hidden.has(name)
|
|
1214
|
-
return (
|
|
1215
|
-
<DropdownMenuItem
|
|
1216
|
-
key={i}
|
|
1217
|
-
// Suppress menu-close so users can toggle multiple columns
|
|
1218
|
-
// without re-opening the dropdown.
|
|
1219
|
-
closeOnClick={false}
|
|
1220
|
-
onClick={() => onToggle(name, !isHidden)}
|
|
1221
|
-
>
|
|
1222
|
-
<span className="inline-flex w-4 items-center justify-center">
|
|
1223
|
-
{!isHidden && <CheckIcon className="h-4 w-4" aria-hidden="true" />}
|
|
1224
|
-
</span>
|
|
1225
|
-
<span>{label}</span>
|
|
1226
|
-
</DropdownMenuItem>
|
|
1227
|
-
)
|
|
1228
|
-
})}
|
|
1229
|
-
</DropdownMenuContent>
|
|
1230
|
-
</DropdownMenu>
|
|
1231
|
-
)
|
|
1232
|
-
}
|
|
1233
|
-
|