@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,974 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react'
|
|
2
|
-
import { ChevronDownIcon, GripVerticalIcon, InboxIcon } from 'lucide-react'
|
|
3
|
-
import type { ElementMeta } from '../../../schema/Element.js'
|
|
4
|
-
import { useNavigate } from '../../navigate.js'
|
|
5
|
-
import { readStoredFlag, writeStoredFlag } from '../../persistedState.js'
|
|
6
|
-
import { useToast } from '../../Toaster.js'
|
|
7
|
-
import { Checkbox } from '../../ui/checkbox.js'
|
|
8
|
-
import { Input } from '../../ui/input.js'
|
|
9
|
-
import {
|
|
10
|
-
Table as DataTable, TableBody, TableCell, TableFooter,
|
|
11
|
-
TableHead, TableHeader, TableRow,
|
|
12
|
-
} from '../../ui/table.js'
|
|
13
|
-
import { pickEditableCell } from '../../cells/EditableCell.js'
|
|
14
|
-
import { resolveIcon } from '../helpers.js'
|
|
15
|
-
import type { RenderActionOptions } from '../action/buttons.js'
|
|
16
|
-
import {
|
|
17
|
-
buildTableQuery, nextSortDir, prefixK, SearchFormHiddenInputs,
|
|
18
|
-
type TableUrlState,
|
|
19
|
-
} from './url.js'
|
|
20
|
-
import { formatCell, rowId } from './formatCell.js'
|
|
21
|
-
import {
|
|
22
|
-
ActiveFiltersBar, ColumnsToggleDropdown, FilterPopover, FilterStrip, FilterStripToggle,
|
|
23
|
-
GroupHeaderText, resolveColumnUrl, SortByPicker, TableGroupPicker,
|
|
24
|
-
} from './filters.js'
|
|
25
|
-
import { ActiveGroupKeyChip, GroupHeadingLink, RecordCellLink } from './links.js'
|
|
26
|
-
import { CardsLayoutBody } from './CardsLayoutBody.js'
|
|
27
|
-
import { renderRowActions } from './renderRowActions.js'
|
|
28
|
-
import { useRowReorderDnd } from '../../fields/useRowReorderDnd.js'
|
|
29
|
-
|
|
30
|
-
// ─── Table body ─────────────────────────────────────────────
|
|
31
|
-
//
|
|
32
|
-
// The biggest component in the renderer. Handles column rendering,
|
|
33
|
-
// sorting + pagination + search URL state, group banding + drill-in,
|
|
34
|
-
// per-row inline edit / delete, bulk-action toolbar, deferred-load
|
|
35
|
-
// state, optional cards layout dispatch, and the empty / loading
|
|
36
|
-
// states.
|
|
37
|
-
|
|
38
|
-
/** Dependencies threaded in from the top-level dispatch — the body
|
|
39
|
-
* recurses into the main element renderer (for column cells that hold
|
|
40
|
-
* Heading / Text / Element-typed content), dispatches row + bulk
|
|
41
|
-
* actions through `renderActionLike`, and reads form schema for
|
|
42
|
-
* inline-edit modals via `renderFormChild`. */
|
|
43
|
-
export interface TableBodyDeps {
|
|
44
|
-
renderElement: (el: ElementMeta, index: number) => React.ReactNode
|
|
45
|
-
renderActionLike: (el: ElementMeta, index: number, opts?: RenderActionOptions) => React.ReactNode
|
|
46
|
-
renderFormChild: (child: ElementMeta, index: number, values: Record<string, unknown>, errors: Record<string, string[]>) => React.ReactNode
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export function TableRendererBody({ el, deps }: { el: ElementMeta; deps: TableBodyDeps }) {
|
|
51
|
-
const { renderElement, renderActionLike, renderFormChild } = deps
|
|
52
|
-
const navigate = useNavigate()
|
|
53
|
-
const children = el.children ?? []
|
|
54
|
-
const columns = children.filter(c => c.type === 'column')
|
|
55
|
-
// `Column.toggleable()` columns — sourced from the resolved meta. The
|
|
56
|
-
// user's per-table visibility map is owned + persisted below; the full
|
|
57
|
-
// `columns` list stays available for the toolbar dropdown so hidden
|
|
58
|
-
// columns can be re-shown without a roundtrip.
|
|
59
|
-
const toggleableColumns = columns.filter(c => c['toggleable'] !== undefined)
|
|
60
|
-
// Actions and ActionGroups share placement — both show up in the
|
|
61
|
-
// header/bulk/row toolbars depending on their `placement` field.
|
|
62
|
-
const actionLike = children.filter(c => c.type === 'action' || c.type === 'actionGroup' || c.type === 'slotComponent')
|
|
63
|
-
const filters = children.filter(c => c.type === 'filter')
|
|
64
|
-
const hasRecordUrl = Boolean(el['recordUrl'])
|
|
65
|
-
const hasRecordClasses = Boolean(el['recordClasses'])
|
|
66
|
-
const pollInterval = typeof el['pollInterval'] === 'number' ? el['pollInterval'] as number : undefined
|
|
67
|
-
const defaultGroup = typeof el['defaultGroup'] === 'string' ? el['defaultGroup'] as string : undefined
|
|
68
|
-
const activeGroupKey = typeof el['activeGroupKey'] === 'string' ? el['activeGroupKey'] as string : undefined
|
|
69
|
-
const summaries = el['summaries'] as Record<string, Array<{ kind: string; value: string; label?: string }>> | undefined
|
|
70
|
-
const groupSummaries = el['groupSummaries'] as
|
|
71
|
-
Record<string, Record<string, Array<{ kind: string; value: string; label?: string }>>> | undefined
|
|
72
|
-
const groupOptions = (el['groups'] as Array<{
|
|
73
|
-
column: string
|
|
74
|
-
label: string
|
|
75
|
-
collapsible?: true
|
|
76
|
-
collapsed?: true
|
|
77
|
-
date?: true
|
|
78
|
-
scopable?: true
|
|
79
|
-
}> | undefined) ?? []
|
|
80
|
-
// Active group's registered metadata (if any). Falls back to a synth
|
|
81
|
-
// for the bare-column form so the heading row still has a label.
|
|
82
|
-
const activeGroupMeta = defaultGroup
|
|
83
|
-
? (groupOptions.find(g => g.column === defaultGroup) ?? {
|
|
84
|
-
column: defaultGroup,
|
|
85
|
-
label: (() => {
|
|
86
|
-
const col = columns.find(c => c['name'] === defaultGroup)
|
|
87
|
-
return col ? String(col['label'] ?? defaultGroup) : defaultGroup
|
|
88
|
-
})(),
|
|
89
|
-
})
|
|
90
|
-
: undefined
|
|
91
|
-
const groupColumnLabel = activeGroupMeta?.label
|
|
92
|
-
// Heading text becomes a real `<a href>` when the active group opts in
|
|
93
|
-
// via `.scopable()`. Synthesized bare-column groups can't be scopable
|
|
94
|
-
// (no builder call ran).
|
|
95
|
-
const groupHeadingScopable = activeGroupMeta !== undefined
|
|
96
|
-
&& (activeGroupMeta as { scopable?: true }).scopable === true
|
|
97
|
-
|
|
98
|
-
// Auto-refresh: re-visit current URL on a timer so sort/filter/pagination
|
|
99
|
-
// state survives. Pause while the document is hidden — background tabs
|
|
100
|
-
// shouldn't keep hammering the server.
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (!pollInterval || pollInterval <= 0) return
|
|
103
|
-
if (typeof document === 'undefined') return
|
|
104
|
-
let timerId: ReturnType<typeof setInterval> | undefined
|
|
105
|
-
const tick = () => navigate(window.location.pathname + window.location.search)
|
|
106
|
-
const start = () => {
|
|
107
|
-
if (timerId === undefined) timerId = setInterval(tick, pollInterval * 1000)
|
|
108
|
-
}
|
|
109
|
-
const stop = () => {
|
|
110
|
-
if (timerId !== undefined) {
|
|
111
|
-
clearInterval(timerId)
|
|
112
|
-
timerId = undefined
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (document.visibilityState === 'visible') start()
|
|
116
|
-
const onVis = () => {
|
|
117
|
-
if (document.visibilityState === 'visible') start()
|
|
118
|
-
else stop()
|
|
119
|
-
}
|
|
120
|
-
document.addEventListener('visibilitychange', onVis)
|
|
121
|
-
return () => {
|
|
122
|
-
document.removeEventListener('visibilitychange', onVis)
|
|
123
|
-
stop()
|
|
124
|
-
}
|
|
125
|
-
}, [pollInterval, navigate])
|
|
126
|
-
|
|
127
|
-
// Group actions by placement. `inline` defaults to header so it shows up
|
|
128
|
-
// somewhere visible — explicit placements always win.
|
|
129
|
-
const placementOf = (a: ElementMeta): string => String(a['placement'] ?? 'inline')
|
|
130
|
-
const headerActions = actionLike.filter(a => { const p = placementOf(a); return p === 'header' || p === 'inline' })
|
|
131
|
-
const bulkActions = actionLike.filter(a => placementOf(a) === 'bulk')
|
|
132
|
-
const rowActions = actionLike.filter(a => placementOf(a) === 'row')
|
|
133
|
-
|
|
134
|
-
const rawRows = (el['rows'] as unknown[] | undefined) ?? []
|
|
135
|
-
const total = (el['total'] as number | undefined) ?? rawRows.length
|
|
136
|
-
const search = el['search'] as string | undefined
|
|
137
|
-
const currentSort = el['currentSort'] as { column: string; direction: 'asc' | 'desc' } | undefined
|
|
138
|
-
const currentPage = (el['currentPage'] as number | undefined) ?? 1
|
|
139
|
-
const perPage = el['perPage'] as number | undefined
|
|
140
|
-
const searchable = Boolean(el['searchable'])
|
|
141
|
-
const currentPath = (el['currentPath'] as string | undefined) ?? ''
|
|
142
|
-
|
|
143
|
-
// `Column.toggleable()` user-visibility map. Persisted per-table at
|
|
144
|
-
// `pilotiq.table.<currentPath>.columns.<name>` ('1' = hidden,
|
|
145
|
-
// '0' = visible). On first paint, fall back to `meta.toggleable.initiallyHidden`.
|
|
146
|
-
// SSR returns the meta default — the localStorage hydrate happens
|
|
147
|
-
// inside the effect so server + first client render match.
|
|
148
|
-
const columnsVisibilityKey = (name: string): string =>
|
|
149
|
-
`pilotiq.table.${currentPath}.columns.${name}`
|
|
150
|
-
const initialHidden = (): Set<string> => {
|
|
151
|
-
const out = new Set<string>()
|
|
152
|
-
for (const col of toggleableColumns) {
|
|
153
|
-
const cfg = col['toggleable'] as { initiallyHidden?: boolean } | undefined
|
|
154
|
-
if (cfg?.initiallyHidden) out.add(String(col['name']))
|
|
155
|
-
}
|
|
156
|
-
return out
|
|
157
|
-
}
|
|
158
|
-
const [hiddenColumns, setHiddenColumns] = useState<Set<string>>(initialHidden)
|
|
159
|
-
useEffect(() => {
|
|
160
|
-
if (toggleableColumns.length === 0) return
|
|
161
|
-
const next = new Set<string>()
|
|
162
|
-
for (const col of toggleableColumns) {
|
|
163
|
-
const name = String(col['name'])
|
|
164
|
-
const cfg = col['toggleable'] as { initiallyHidden?: boolean } | undefined
|
|
165
|
-
if (readStoredFlag(columnsVisibilityKey(name), Boolean(cfg?.initiallyHidden))) {
|
|
166
|
-
next.add(name)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
setHiddenColumns(next)
|
|
170
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
171
|
-
}, [currentPath, toggleableColumns.length])
|
|
172
|
-
const toggleColumnHidden = (name: string, nextHidden: boolean): void => {
|
|
173
|
-
setHiddenColumns(prev => {
|
|
174
|
-
const next = new Set(prev)
|
|
175
|
-
if (nextHidden) next.add(name)
|
|
176
|
-
else next.delete(name)
|
|
177
|
-
writeStoredFlag(columnsVisibilityKey(name), nextHidden)
|
|
178
|
-
return next
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
// Filtered column list used by every render path (header, body cells,
|
|
182
|
-
// group + footer summaries, empty-state colSpan). Non-toggleable
|
|
183
|
-
// columns always survive.
|
|
184
|
-
const visibleColumns = columns.filter(c => !hiddenColumns.has(String(c['name'])))
|
|
185
|
-
|
|
186
|
-
// Tier-3 — when the table opts into `Table.queryStringIdentifier(...)`,
|
|
187
|
-
// every URL key (search / sort / page / perPage / group / filter names)
|
|
188
|
-
// gets prefixed with `${id}_` so multiple tables on one page don't
|
|
189
|
-
// collide on `?search=` etc. Bare keys still apply when unset.
|
|
190
|
-
const queryPrefix = typeof el['queryStringIdentifier'] === 'string'
|
|
191
|
-
? el['queryStringIdentifier'] as string
|
|
192
|
-
: undefined
|
|
193
|
-
|
|
194
|
-
// Reorderable rows — grip column + HTML5 DnD wiring. Rows live in
|
|
195
|
-
// local state during a drag so the optimistic reorder happens
|
|
196
|
-
// immediately; on POST failure we roll back to the server's order.
|
|
197
|
-
const reorderableColumn = typeof el['reorderableColumn'] === 'string' ? el['reorderableColumn'] as string : undefined
|
|
198
|
-
const reorderUrl = typeof el['reorderUrl'] === 'string' ? el['reorderUrl'] as string : undefined
|
|
199
|
-
const [reorderRowsLocal, setReorderRowsLocal] = useState<unknown[] | null>(null)
|
|
200
|
-
const rows = reorderRowsLocal ?? rawRows
|
|
201
|
-
const { notify } = useToast()
|
|
202
|
-
|
|
203
|
-
// Read the explicit `?group=` value out of the URL so sort/pagination
|
|
204
|
-
// links preserve "None" overrides (`?group=`). Server render: no URL,
|
|
205
|
-
// so we fall back to `defaultGroup` from the meta — which is already
|
|
206
|
-
// the reconciled active column.
|
|
207
|
-
const urlGroup: string | undefined = typeof window === 'undefined'
|
|
208
|
-
? undefined
|
|
209
|
-
: (() => {
|
|
210
|
-
const sp = new URLSearchParams(window.location.search)
|
|
211
|
-
const k = prefixK(queryPrefix, 'group')
|
|
212
|
-
return sp.has(k) ? sp.get(k)! : undefined
|
|
213
|
-
})()
|
|
214
|
-
|
|
215
|
-
// Collapsible groups — per-group fold state. Keyed by `_groupValue`
|
|
216
|
-
// (the raw column value, NOT the resolved title) so rows that share a
|
|
217
|
-
// group key fold together. Persisted in localStorage at
|
|
218
|
-
// `pilotiq.table.<currentPath>.groups.<column>.<value>`. Default-
|
|
219
|
-
// collapsed groups derive their initial state from `meta.collapsed`.
|
|
220
|
-
const groupCollapsible = activeGroupMeta?.collapsible === true
|
|
221
|
-
const groupDefaultCollapsed = activeGroupMeta?.collapsed === true
|
|
222
|
-
const groupStorageKey = (groupValue: string): string =>
|
|
223
|
-
`pilotiq.table.${currentPath}.groups.${defaultGroup ?? ''}.${groupValue}`
|
|
224
|
-
// Lazy-init from localStorage on mount; SSR returns the meta default.
|
|
225
|
-
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({})
|
|
226
|
-
useEffect(() => {
|
|
227
|
-
if (!groupCollapsible || !defaultGroup) return
|
|
228
|
-
// Walk the rendered rows once on mount, picking up persisted state.
|
|
229
|
-
const next: Record<string, boolean> = {}
|
|
230
|
-
const seen = new Set<string>()
|
|
231
|
-
for (const row of rows) {
|
|
232
|
-
const v = String((row as Record<string, unknown>)['_groupValue'] ?? '')
|
|
233
|
-
if (seen.has(v)) continue
|
|
234
|
-
seen.add(v)
|
|
235
|
-
next[v] = readStoredFlag(groupStorageKey(v), groupDefaultCollapsed)
|
|
236
|
-
}
|
|
237
|
-
setCollapsedGroups(next)
|
|
238
|
-
// Re-run if the active group changes — different values, different
|
|
239
|
-
// localStorage namespace.
|
|
240
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
241
|
-
}, [defaultGroup, groupCollapsible, groupDefaultCollapsed, currentPath])
|
|
242
|
-
const toggleGroupCollapsed = (groupValue: string): void => {
|
|
243
|
-
setCollapsedGroups(prev => {
|
|
244
|
-
const nextOpen = !prev[groupValue]
|
|
245
|
-
const next = { ...prev, [groupValue]: nextOpen }
|
|
246
|
-
writeStoredFlag(groupStorageKey(groupValue), nextOpen)
|
|
247
|
-
return next
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
const state: TableUrlState = {
|
|
251
|
-
...(search !== undefined ? { search } : {}),
|
|
252
|
-
...(currentSort !== undefined ? { sort: currentSort } : {}),
|
|
253
|
-
page: currentPage,
|
|
254
|
-
...(urlGroup !== undefined ? { group: urlGroup }
|
|
255
|
-
: defaultGroup !== undefined ? { group: defaultGroup }
|
|
256
|
-
: {}),
|
|
257
|
-
...(activeGroupKey !== undefined ? { groupKey: activeGroupKey } : {}),
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Snapshot active filter values for sort/pagination href construction.
|
|
261
|
-
// Filter form submits already carry these (selects are inside the
|
|
262
|
-
// form); `<a href>` links don't, so we re-emit them here.
|
|
263
|
-
const activeFilters: Record<string, string> = {}
|
|
264
|
-
for (const f of filters) {
|
|
265
|
-
const v = f['value']
|
|
266
|
-
if (typeof v === 'string' && v !== '') activeFilters[String(f['name'])] = v
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Drill-in / drill-out URL builders for the group heading link and the
|
|
270
|
-
// active-key chip's clear button. Drill-in sets `?<prefix>groupKey=v`
|
|
271
|
-
// and resets `page`; drill-out clears it. Both round-trip foreign
|
|
272
|
-
// params (other tables' state) through `buildTableQuery`.
|
|
273
|
-
const buildGroupKeyHref = (value: string): string => buildTableQuery(
|
|
274
|
-
state, { groupKey: value, page: 1 }, currentPath, activeFilters, queryPrefix,
|
|
275
|
-
)
|
|
276
|
-
const drillOutHref = (): string => buildTableQuery(
|
|
277
|
-
state, { groupKey: '', page: 1 }, currentPath, activeFilters, queryPrefix,
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
// Track which row ids are currently checked. Keyed by id (string), not
|
|
281
|
-
// by index, so pagination and re-renders don't drop selection state.
|
|
282
|
-
const [selected, setSelected] = useState<Set<string>>(() => new Set())
|
|
283
|
-
const visibleIds = rows.map((row, i) => rowId(row, i))
|
|
284
|
-
const allChecked = visibleIds.length > 0 && visibleIds.every(id => selected.has(id))
|
|
285
|
-
const someChecked = selected.size > 0
|
|
286
|
-
|
|
287
|
-
const toggleRow = (id: string) => {
|
|
288
|
-
setSelected(prev => {
|
|
289
|
-
const next = new Set(prev)
|
|
290
|
-
if (next.has(id)) next.delete(id); else next.add(id)
|
|
291
|
-
return next
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
const toggleAll = () => {
|
|
295
|
-
setSelected(prev => {
|
|
296
|
-
if (visibleIds.every(id => prev.has(id))) {
|
|
297
|
-
const next = new Set(prev)
|
|
298
|
-
for (const id of visibleIds) next.delete(id)
|
|
299
|
-
return next
|
|
300
|
-
}
|
|
301
|
-
const next = new Set(prev)
|
|
302
|
-
for (const id of visibleIds) next.add(id)
|
|
303
|
-
return next
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (columns.length === 0) {
|
|
309
|
-
return (
|
|
310
|
-
<div className="rounded-xl border bg-card p-6 text-sm text-muted-foreground">
|
|
311
|
-
No columns configured for this table.
|
|
312
|
-
</div>
|
|
313
|
-
)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const isCardsLayout = el['contentLayout'] === 'cards'
|
|
317
|
-
const cardsPerRow = el['cardsPerRow'] as Record<string, number> | undefined
|
|
318
|
-
|
|
319
|
-
const totalPages = perPage && perPage > 0 ? Math.max(1, Math.ceil(total / perPage)) : 1
|
|
320
|
-
const showPagination = totalPages > 1
|
|
321
|
-
const hasFilters = filters.length > 0
|
|
322
|
-
// Filter layout positions (Filament v5). `'modal'` (default) keeps the
|
|
323
|
-
// toolbar Filters button + popover. The three inline modes lay every
|
|
324
|
-
// filter widget out as a wrapping strip in the matching slot. The
|
|
325
|
-
// collapsible variant adds a toolbar toggle + per-table-path persisted
|
|
326
|
-
// open state.
|
|
327
|
-
const filtersLayout = (el['filtersLayout'] as
|
|
328
|
-
| 'above-content' | 'above-content-collapsible' | 'below-content'
|
|
329
|
-
| undefined) ?? 'modal'
|
|
330
|
-
const filtersInModal = filtersLayout === 'modal'
|
|
331
|
-
const filtersAbove = filtersLayout === 'above-content'
|
|
332
|
-
|| filtersLayout === 'above-content-collapsible'
|
|
333
|
-
const filtersBelow = filtersLayout === 'below-content'
|
|
334
|
-
const filtersCollapsible = filtersLayout === 'above-content-collapsible'
|
|
335
|
-
const filtersStripStorageKey = `pilotiq.table.${currentPath}.filters.open`
|
|
336
|
-
const [filtersOpen, setFiltersOpen] = useState<boolean>(() => {
|
|
337
|
-
if (!filtersCollapsible) return true
|
|
338
|
-
// Default to OPEN when filters are active (URL carried filter values
|
|
339
|
-
// in) so the user can see what's filtering — same UX cue as the
|
|
340
|
-
// active-filters pill row.
|
|
341
|
-
return readStoredFlag(filtersStripStorageKey, Object.keys(activeFilters).length > 0)
|
|
342
|
-
})
|
|
343
|
-
const toggleFiltersOpen = (): void => {
|
|
344
|
-
setFiltersOpen(prev => {
|
|
345
|
-
const next = !prev
|
|
346
|
-
writeStoredFlag(filtersStripStorageKey, next)
|
|
347
|
-
return next
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
// Show the "Group by" dropdown when 2+ groups are registered, or 1
|
|
351
|
-
// group with rich metadata (label/collapsible/etc.). A single bare
|
|
352
|
-
// `defaultGroup('col')` with no `groups([...])` registration shouldn't
|
|
353
|
-
// render the picker — there's nothing to pick.
|
|
354
|
-
const hasGroupPicker = groupOptions.length >= 2
|
|
355
|
-
|| (groupOptions.length === 1 && Boolean(
|
|
356
|
-
groupOptions[0]!.collapsible
|
|
357
|
-
|| groupOptions[0]!.collapsed
|
|
358
|
-
|| groupOptions[0]!.date,
|
|
359
|
-
))
|
|
360
|
-
const sortableColumns = isCardsLayout ? columns.filter(c => Boolean(c['sortable'])) : []
|
|
361
|
-
const hasSortPicker = isCardsLayout && sortableColumns.length > 0
|
|
362
|
-
// Only modal + collapsible mount a toolbar widget; the always-visible
|
|
363
|
-
// strip modes don't add anything to the header bar.
|
|
364
|
-
const showFiltersInToolbar = hasFilters && (filtersInModal || filtersCollapsible)
|
|
365
|
-
const hasColumnsToggle = toggleableColumns.length > 0
|
|
366
|
-
const showHeaderBar = searchable || headerActions.length > 0 || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle
|
|
367
|
-
const hasBulkActions = bulkActions.length > 0
|
|
368
|
-
const hasRowActions = rowActions.length > 0
|
|
369
|
-
|
|
370
|
-
// Drag-to-reorder is enabled only when the visible rows ARE the
|
|
371
|
-
// canonical sort. Filters / search / non-default sort / pagination
|
|
372
|
-
// beyond page 1 all break that invariant; we render the grip column
|
|
373
|
-
// greyed-out instead of letting the user reorder a slice that won't
|
|
374
|
-
// round-trip cleanly. `reorderableColumn` is set server-side when
|
|
375
|
-
// `Table.reorderable()` opts in.
|
|
376
|
-
const sortMatchesReorder =
|
|
377
|
-
currentSort?.column === reorderableColumn &&
|
|
378
|
-
currentSort?.direction === 'asc'
|
|
379
|
-
const filtersActive = Object.keys(activeFilters).length > 0
|
|
380
|
-
const searchActive = typeof search === 'string' && search !== ''
|
|
381
|
-
const reorderEnabled =
|
|
382
|
-
reorderableColumn !== undefined &&
|
|
383
|
-
reorderUrl !== undefined &&
|
|
384
|
-
sortMatchesReorder &&
|
|
385
|
-
!filtersActive &&
|
|
386
|
-
!searchActive &&
|
|
387
|
-
currentPage === 1
|
|
388
|
-
const reorderColumnVisible = reorderableColumn !== undefined
|
|
389
|
-
|
|
390
|
-
// ── Reorder DnD state + handlers ──────────────────────
|
|
391
|
-
const {
|
|
392
|
-
dragId, dropAt,
|
|
393
|
-
onDragStart: onRowDragStart,
|
|
394
|
-
onDragOver: onRowDragOver,
|
|
395
|
-
onDrop: onRowDrop,
|
|
396
|
-
onDragEnd: onRowDragEnd,
|
|
397
|
-
} = useRowReorderDnd({
|
|
398
|
-
enabled: reorderEnabled,
|
|
399
|
-
onDrop: async (fromId, at) => {
|
|
400
|
-
if (!reorderUrl) return
|
|
401
|
-
const fromIdx = visibleIds.findIndex(id => id === fromId)
|
|
402
|
-
if (fromIdx < 0) return
|
|
403
|
-
const target = at > fromIdx ? at - 1 : at
|
|
404
|
-
if (target === fromIdx) return
|
|
405
|
-
const reordered = rows.slice()
|
|
406
|
-
const moved = reordered.splice(fromIdx, 1)[0]
|
|
407
|
-
reordered.splice(target, 0, moved)
|
|
408
|
-
const newIds = reordered.map((row, i) => rowId(row, i))
|
|
409
|
-
const previousLocal = reorderRowsLocal
|
|
410
|
-
setReorderRowsLocal(reordered)
|
|
411
|
-
try {
|
|
412
|
-
const res = await fetch(reorderUrl, {
|
|
413
|
-
method: 'POST',
|
|
414
|
-
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
415
|
-
body: JSON.stringify({ ids: newIds }),
|
|
416
|
-
})
|
|
417
|
-
if (!res.ok) throw new Error(`Reorder failed (${res.status})`)
|
|
418
|
-
} catch (err) {
|
|
419
|
-
// Roll back to server order. The toast surfaces the failure;
|
|
420
|
-
// next page render fetches the persisted column.
|
|
421
|
-
setReorderRowsLocal(previousLocal)
|
|
422
|
-
notify({
|
|
423
|
-
type: 'error',
|
|
424
|
-
title: 'Could not save new order',
|
|
425
|
-
body: err instanceof Error ? err.message : 'Reorder failed',
|
|
426
|
-
})
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
})
|
|
430
|
-
const totalCols = visibleColumns.length
|
|
431
|
-
+ (hasBulkActions ? 1 : 0)
|
|
432
|
-
+ (hasRowActions ? 1 : 0)
|
|
433
|
-
+ (reorderColumnVisible ? 1 : 0)
|
|
434
|
-
|
|
435
|
-
// Top-bar chrome (heading / description / striped / emptyState).
|
|
436
|
-
const tableHeading = el['heading'] as string | undefined
|
|
437
|
-
const tableDescription = el['description'] as string | undefined
|
|
438
|
-
const striped = Boolean(el['striped'])
|
|
439
|
-
const emptyState = el['emptyState'] as { heading?: string; description?: string; icon?: string } | undefined
|
|
440
|
-
const filteredEmptyState = el['filteredEmptyState'] as { heading?: string; description?: string; icon?: string } | undefined
|
|
441
|
-
const hasFilterOrSearch = (search !== undefined && search !== '') ||
|
|
442
|
-
Object.keys(activeFilters).length > 0
|
|
443
|
-
// Distinct copy when a query / filter is active. Falls back to
|
|
444
|
-
// `emptyState` when `filteredEmptyState` is not set, preserving the
|
|
445
|
-
// pre-2026-05-04 behavior for tables that haven't opted in.
|
|
446
|
-
const activeEmpty = (hasFilterOrSearch && filteredEmptyState) ? filteredEmptyState : emptyState
|
|
447
|
-
const EmptyIcon = activeEmpty?.icon ? (resolveIcon(activeEmpty.icon) ?? InboxIcon) : InboxIcon
|
|
448
|
-
|
|
449
|
-
return (
|
|
450
|
-
<div className="flex flex-col gap-3">
|
|
451
|
-
{(tableHeading || tableDescription) && (
|
|
452
|
-
<div className="flex flex-col gap-1">
|
|
453
|
-
{tableHeading && <h2 className="text-lg font-semibold">{tableHeading}</h2>}
|
|
454
|
-
{tableDescription && <p className="text-sm text-muted-foreground">{tableDescription}</p>}
|
|
455
|
-
</div>
|
|
456
|
-
)}
|
|
457
|
-
{showHeaderBar && (
|
|
458
|
-
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
459
|
-
{(searchable || showFiltersInToolbar || hasGroupPicker || hasSortPicker || hasColumnsToggle) ? (
|
|
460
|
-
<div className="flex items-center gap-2">
|
|
461
|
-
{searchable && (
|
|
462
|
-
<form method="get" action={currentPath || undefined} className="flex items-end gap-2">
|
|
463
|
-
{/* Carry the table's own non-search slice forward via hidden
|
|
464
|
-
inputs so a native form submit (Enter) preserves sort /
|
|
465
|
-
page / filters. Other tables' params on the URL also
|
|
466
|
-
survive via the same loop. */}
|
|
467
|
-
<SearchFormHiddenInputs prefix={queryPrefix} />
|
|
468
|
-
<Input
|
|
469
|
-
type="search"
|
|
470
|
-
name={prefixK(queryPrefix, 'search')}
|
|
471
|
-
defaultValue={search ?? ''}
|
|
472
|
-
placeholder="Search…"
|
|
473
|
-
className="h-9 w-64"
|
|
474
|
-
/>
|
|
475
|
-
{/* Search submits via Enter natively. Hidden submit kept
|
|
476
|
-
for screen-reader form semantics. */}
|
|
477
|
-
<button type="submit" className="sr-only" tabIndex={-1} aria-hidden="true">
|
|
478
|
-
Apply
|
|
479
|
-
</button>
|
|
480
|
-
</form>
|
|
481
|
-
)}
|
|
482
|
-
{hasFilters && filtersInModal && (
|
|
483
|
-
<FilterPopover filters={filters} prefix={queryPrefix} renderFormChild={renderFormChild} />
|
|
484
|
-
)}
|
|
485
|
-
{hasFilters && filtersCollapsible && (
|
|
486
|
-
<FilterStripToggle
|
|
487
|
-
filters={filters}
|
|
488
|
-
open={filtersOpen}
|
|
489
|
-
onToggle={toggleFiltersOpen}
|
|
490
|
-
/>
|
|
491
|
-
)}
|
|
492
|
-
{hasGroupPicker && (
|
|
493
|
-
<TableGroupPicker
|
|
494
|
-
options={groupOptions}
|
|
495
|
-
active={defaultGroup}
|
|
496
|
-
onChange={(value) => {
|
|
497
|
-
// value === '' → explicit "None" (clears defaultGroup);
|
|
498
|
-
// value !== '' → switch to that column.
|
|
499
|
-
const href = buildTableQuery(
|
|
500
|
-
state,
|
|
501
|
-
{ page: 1, group: value },
|
|
502
|
-
currentPath,
|
|
503
|
-
activeFilters,
|
|
504
|
-
queryPrefix,
|
|
505
|
-
)
|
|
506
|
-
navigate(href)
|
|
507
|
-
}}
|
|
508
|
-
/>
|
|
509
|
-
)}
|
|
510
|
-
{hasSortPicker && (
|
|
511
|
-
<SortByPicker
|
|
512
|
-
columns={sortableColumns}
|
|
513
|
-
active={currentSort}
|
|
514
|
-
onChange={(column: string, direction: 'asc' | 'desc') => {
|
|
515
|
-
const href = buildTableQuery(
|
|
516
|
-
state,
|
|
517
|
-
{ sort: { column, direction }, page: 1 },
|
|
518
|
-
currentPath,
|
|
519
|
-
activeFilters,
|
|
520
|
-
queryPrefix,
|
|
521
|
-
)
|
|
522
|
-
navigate(href)
|
|
523
|
-
}}
|
|
524
|
-
/>
|
|
525
|
-
)}
|
|
526
|
-
{toggleableColumns.length > 0 && (
|
|
527
|
-
<ColumnsToggleDropdown
|
|
528
|
-
columns={toggleableColumns}
|
|
529
|
-
hidden={hiddenColumns}
|
|
530
|
-
onToggle={toggleColumnHidden}
|
|
531
|
-
/>
|
|
532
|
-
)}
|
|
533
|
-
</div>
|
|
534
|
-
) : <span />}
|
|
535
|
-
{headerActions.length > 0 && (
|
|
536
|
-
<div className="flex items-center gap-2">
|
|
537
|
-
{headerActions.map((a, i) => renderActionLike(a, i))}
|
|
538
|
-
</div>
|
|
539
|
-
)}
|
|
540
|
-
</div>
|
|
541
|
-
)}
|
|
542
|
-
{hasFilters && filtersInModal && <ActiveFiltersBar filters={filters} prefix={queryPrefix} />}
|
|
543
|
-
{hasFilters && filtersAbove && filtersOpen && (
|
|
544
|
-
<FilterStrip filters={filters} prefix={queryPrefix} renderFormChild={renderFormChild} />
|
|
545
|
-
)}
|
|
546
|
-
{activeGroupKey !== undefined && (
|
|
547
|
-
<ActiveGroupKeyChip
|
|
548
|
-
label={groupColumnLabel ?? defaultGroup ?? ''}
|
|
549
|
-
value={activeGroupKey}
|
|
550
|
-
displayValue={(() => {
|
|
551
|
-
// Prefer a row-resolved `_groupTitle` (server stamped via
|
|
552
|
-
// `getTitleFromRecordUsing`) so the chip reads the same as
|
|
553
|
-
// a banded heading. Falls back to the raw bucket key when
|
|
554
|
-
// no row matched — empty drilled-in pages still show what
|
|
555
|
-
// they're drilled into.
|
|
556
|
-
for (const r of rows) {
|
|
557
|
-
const obj = r as Record<string, unknown>
|
|
558
|
-
if (String(obj['_groupValue'] ?? '') !== activeGroupKey) continue
|
|
559
|
-
const t = obj['_groupTitle']
|
|
560
|
-
if (typeof t === 'string' && t !== '') return t
|
|
561
|
-
break
|
|
562
|
-
}
|
|
563
|
-
return activeGroupKey
|
|
564
|
-
})()}
|
|
565
|
-
clearHref={drillOutHref()}
|
|
566
|
-
navigate={navigate}
|
|
567
|
-
/>
|
|
568
|
-
)}
|
|
569
|
-
{hasBulkActions && someChecked && (
|
|
570
|
-
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/40 px-3 py-2 text-sm">
|
|
571
|
-
<span className="text-muted-foreground">
|
|
572
|
-
{selected.size} selected
|
|
573
|
-
</span>
|
|
574
|
-
<div className="flex items-center gap-2">
|
|
575
|
-
{bulkActions.map((a, i) =>
|
|
576
|
-
renderActionLike(a, i, { ids: Array.from(selected) }),
|
|
577
|
-
)}
|
|
578
|
-
<button
|
|
579
|
-
type="button"
|
|
580
|
-
onClick={() => setSelected(new Set())}
|
|
581
|
-
className="text-xs text-muted-foreground hover:text-foreground"
|
|
582
|
-
>
|
|
583
|
-
Clear
|
|
584
|
-
</button>
|
|
585
|
-
</div>
|
|
586
|
-
</div>
|
|
587
|
-
)}
|
|
588
|
-
{isCardsLayout ? (
|
|
589
|
-
<CardsLayoutBody
|
|
590
|
-
rows={rows}
|
|
591
|
-
visibleIds={visibleIds}
|
|
592
|
-
selected={selected}
|
|
593
|
-
toggleRow={toggleRow}
|
|
594
|
-
hasBulkActions={hasBulkActions}
|
|
595
|
-
hasRowActions={hasRowActions}
|
|
596
|
-
rowActions={rowActions}
|
|
597
|
-
hasRecordUrl={hasRecordUrl}
|
|
598
|
-
hasRecordClasses={hasRecordClasses}
|
|
599
|
-
activeEmpty={activeEmpty}
|
|
600
|
-
EmptyIcon={EmptyIcon}
|
|
601
|
-
hasFilterOrSearch={hasFilterOrSearch}
|
|
602
|
-
defaultGroup={defaultGroup}
|
|
603
|
-
groupColumnLabel={groupColumnLabel}
|
|
604
|
-
groupCollapsible={groupCollapsible}
|
|
605
|
-
collapsedGroups={collapsedGroups}
|
|
606
|
-
toggleGroupCollapsed={toggleGroupCollapsed}
|
|
607
|
-
cardsPerRow={cardsPerRow}
|
|
608
|
-
navigate={navigate}
|
|
609
|
-
groupHeadingScopable={groupHeadingScopable}
|
|
610
|
-
buildGroupKeyHref={buildGroupKeyHref}
|
|
611
|
-
renderElement={renderElement}
|
|
612
|
-
renderRowActions={(id, recordObj, actions) =>
|
|
613
|
-
renderRowActions(id, recordObj, actions, renderActionLike)
|
|
614
|
-
}
|
|
615
|
-
/>
|
|
616
|
-
) : (
|
|
617
|
-
<div className="rounded-xl border bg-card overflow-hidden">
|
|
618
|
-
<DataTable>
|
|
619
|
-
<TableHeader className="bg-muted">
|
|
620
|
-
<TableRow>
|
|
621
|
-
{reorderColumnVisible && (
|
|
622
|
-
<TableHead className="w-9 px-2" aria-label="Reorder" />
|
|
623
|
-
)}
|
|
624
|
-
{hasBulkActions && (
|
|
625
|
-
<TableHead className="w-9 px-3">
|
|
626
|
-
<Checkbox
|
|
627
|
-
aria-label="Select all rows"
|
|
628
|
-
checked={allChecked}
|
|
629
|
-
onCheckedChange={() => toggleAll()}
|
|
630
|
-
/>
|
|
631
|
-
</TableHead>
|
|
632
|
-
)}
|
|
633
|
-
{visibleColumns.map((col, i) => {
|
|
634
|
-
const name = String(col['name'] ?? '')
|
|
635
|
-
const label = String(col['label'] ?? name)
|
|
636
|
-
const sortable = Boolean(col['sortable'])
|
|
637
|
-
const isActive = currentSort?.column === name
|
|
638
|
-
|
|
639
|
-
if (!sortable) {
|
|
640
|
-
return (
|
|
641
|
-
<TableHead key={i} className="text-xs uppercase tracking-wider">
|
|
642
|
-
{label}
|
|
643
|
-
</TableHead>
|
|
644
|
-
)
|
|
645
|
-
}
|
|
646
|
-
const next = nextSortDir(currentSort, name)
|
|
647
|
-
const href = buildTableQuery(state, { sort: next, page: 1 }, currentPath, activeFilters, queryPrefix)
|
|
648
|
-
return (
|
|
649
|
-
<TableHead key={i} className="text-xs uppercase tracking-wider">
|
|
650
|
-
<a href={href} className="inline-flex items-center gap-1 hover:text-foreground">
|
|
651
|
-
{label}
|
|
652
|
-
<span className="text-muted-foreground/70">
|
|
653
|
-
{isActive ? (currentSort!.direction === 'asc' ? '↑' : '↓') : '↕'}
|
|
654
|
-
</span>
|
|
655
|
-
</a>
|
|
656
|
-
</TableHead>
|
|
657
|
-
)
|
|
658
|
-
})}
|
|
659
|
-
{hasRowActions && (
|
|
660
|
-
<TableHead className="w-px text-right text-xs uppercase tracking-wider">
|
|
661
|
-
<span className="sr-only">Actions</span>
|
|
662
|
-
</TableHead>
|
|
663
|
-
)}
|
|
664
|
-
</TableRow>
|
|
665
|
-
</TableHeader>
|
|
666
|
-
<TableBody>
|
|
667
|
-
{rows.length === 0 ? (
|
|
668
|
-
<TableRow>
|
|
669
|
-
<TableCell colSpan={totalCols} className="py-12 text-center">
|
|
670
|
-
<div className="flex flex-col items-center gap-2 text-muted-foreground">
|
|
671
|
-
<EmptyIcon className="size-8 opacity-60" />
|
|
672
|
-
<p className="text-base font-medium text-foreground">
|
|
673
|
-
{activeEmpty?.heading
|
|
674
|
-
?? (hasFilterOrSearch ? 'No matching records' : 'No records yet')}
|
|
675
|
-
</p>
|
|
676
|
-
{(activeEmpty?.description ||
|
|
677
|
-
(hasFilterOrSearch && !activeEmpty?.description)) && (
|
|
678
|
-
<p className="text-sm">
|
|
679
|
-
{activeEmpty?.description
|
|
680
|
-
?? 'Try clearing filters or adjusting your search.'}
|
|
681
|
-
</p>
|
|
682
|
-
)}
|
|
683
|
-
</div>
|
|
684
|
-
</TableCell>
|
|
685
|
-
</TableRow>
|
|
686
|
-
) : rows.map((row, ri) => {
|
|
687
|
-
const id = visibleIds[ri]!
|
|
688
|
-
const recordObj = row as Record<string, unknown>
|
|
689
|
-
const isSelected = selected.has(id)
|
|
690
|
-
const stripedClass = striped && ri % 2 === 1 ? 'bg-muted/30' : ''
|
|
691
|
-
// Group banding — emit a heading row whenever `_groupValue`
|
|
692
|
-
// differs from the previous row. The first row in any group
|
|
693
|
-
// gets the heading; rows within keep their normal chrome.
|
|
694
|
-
const groupValue = defaultGroup
|
|
695
|
-
? String(recordObj['_groupValue'] ?? '')
|
|
696
|
-
: undefined
|
|
697
|
-
const groupTitle = defaultGroup
|
|
698
|
-
? (recordObj['_groupTitle'] as string | undefined)
|
|
699
|
-
: undefined
|
|
700
|
-
const groupDescription = defaultGroup
|
|
701
|
-
? (recordObj['_groupDescription'] as string | undefined)
|
|
702
|
-
: undefined
|
|
703
|
-
const prevGroupValue = defaultGroup && ri > 0
|
|
704
|
-
? String(((rows[ri - 1] as Record<string, unknown>)['_groupValue'] ?? ''))
|
|
705
|
-
: undefined
|
|
706
|
-
const showGroupHeader =
|
|
707
|
-
defaultGroup !== undefined && groupValue !== prevGroupValue
|
|
708
|
-
// Hide data rows whose group is collapsed. The heading row
|
|
709
|
-
// for that group still renders (so the user can re-expand).
|
|
710
|
-
const isInCollapsedGroup =
|
|
711
|
-
groupCollapsible && groupValue !== undefined && collapsedGroups[groupValue] === true
|
|
712
|
-
// Filament-style per-cell linking. Each data cell wraps
|
|
713
|
-
// its content in a real `<a href>` when the column resolves
|
|
714
|
-
// to a record URL — column override (`Column.recordUrl(fn)`)
|
|
715
|
-
// beats inheritance from the table (`Table.recordUrl(fn)`),
|
|
716
|
-
// and `Column.recordUrl(false)` opts out. Action and bulk
|
|
717
|
-
// cells are never wrapped, so clicks there fire only their
|
|
718
|
-
// own handlers — no event-bubbling gymnastics.
|
|
719
|
-
const tableUrl = hasRecordUrl ? (recordObj['_recordUrl'] as string | undefined) : undefined
|
|
720
|
-
const colUrls = (recordObj['_columnRecordUrls'] as Record<string, string> | undefined) ?? {}
|
|
721
|
-
const rowHasAnyLink = tableUrl !== undefined || Object.keys(colUrls).length > 0
|
|
722
|
-
const customRowClasses = hasRecordClasses
|
|
723
|
-
? (recordObj['_recordClasses'] as string | undefined) ?? ''
|
|
724
|
-
: ''
|
|
725
|
-
const rowClassName = [stripedClass, rowHasAnyLink ? 'cursor-pointer' : '', customRowClasses]
|
|
726
|
-
.filter(Boolean)
|
|
727
|
-
.join(' ')
|
|
728
|
-
.trim()
|
|
729
|
-
return (
|
|
730
|
-
<React.Fragment key={id}>
|
|
731
|
-
{showGroupHeader && (
|
|
732
|
-
<TableRow key={`group-${id}`} className="bg-muted/40 hover:bg-muted/40">
|
|
733
|
-
<TableCell
|
|
734
|
-
colSpan={totalCols}
|
|
735
|
-
className="px-3 py-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground"
|
|
736
|
-
>
|
|
737
|
-
{(() => {
|
|
738
|
-
const drillable = groupHeadingScopable
|
|
739
|
-
&& groupValue !== undefined
|
|
740
|
-
&& groupValue !== ''
|
|
741
|
-
const headingText = (
|
|
742
|
-
<GroupHeaderText
|
|
743
|
-
label={groupColumnLabel}
|
|
744
|
-
value={groupValue}
|
|
745
|
-
title={groupTitle}
|
|
746
|
-
description={groupDescription}
|
|
747
|
-
/>
|
|
748
|
-
)
|
|
749
|
-
const headingNode = drillable
|
|
750
|
-
? <GroupHeadingLink href={buildGroupKeyHref(groupValue!)} navigate={navigate}>{headingText}</GroupHeadingLink>
|
|
751
|
-
: headingText
|
|
752
|
-
if (groupCollapsible) {
|
|
753
|
-
return (
|
|
754
|
-
<div className="flex w-full items-center gap-2">
|
|
755
|
-
<button
|
|
756
|
-
type="button"
|
|
757
|
-
className="inline-flex items-center"
|
|
758
|
-
onClick={() => toggleGroupCollapsed(groupValue!)}
|
|
759
|
-
aria-expanded={!isInCollapsedGroup}
|
|
760
|
-
aria-label={isInCollapsedGroup ? 'Expand group' : 'Collapse group'}
|
|
761
|
-
>
|
|
762
|
-
<ChevronDownIcon
|
|
763
|
-
className={[
|
|
764
|
-
'size-4 transition-transform',
|
|
765
|
-
isInCollapsedGroup ? '-rotate-90' : '',
|
|
766
|
-
].filter(Boolean).join(' ')}
|
|
767
|
-
/>
|
|
768
|
-
</button>
|
|
769
|
-
{headingNode}
|
|
770
|
-
</div>
|
|
771
|
-
)
|
|
772
|
-
}
|
|
773
|
-
return headingNode
|
|
774
|
-
})()}
|
|
775
|
-
</TableCell>
|
|
776
|
-
</TableRow>
|
|
777
|
-
)}
|
|
778
|
-
{isInCollapsedGroup ? null : (
|
|
779
|
-
<TableRow
|
|
780
|
-
data-state={isSelected ? 'selected' : undefined}
|
|
781
|
-
className={[
|
|
782
|
-
rowClassName,
|
|
783
|
-
dragId === id ? 'opacity-50' : '',
|
|
784
|
-
dropAt === ri && dragId !== null ? 'border-t-2 border-t-primary' : '',
|
|
785
|
-
].filter(Boolean).join(' ') || undefined}
|
|
786
|
-
draggable={reorderEnabled || undefined}
|
|
787
|
-
onDragStart={reorderEnabled ? onRowDragStart(id) : undefined}
|
|
788
|
-
onDragOver={reorderEnabled ? onRowDragOver(ri) : undefined}
|
|
789
|
-
onDrop={reorderEnabled ? onRowDrop : undefined}
|
|
790
|
-
onDragEnd={reorderEnabled ? onRowDragEnd : undefined}
|
|
791
|
-
>
|
|
792
|
-
{reorderColumnVisible && (
|
|
793
|
-
<TableCell className="w-9 px-2">
|
|
794
|
-
<span
|
|
795
|
-
aria-label={reorderEnabled ? 'Drag to reorder' : 'Reorder paused — clear filters and sort to enable'}
|
|
796
|
-
className={
|
|
797
|
-
reorderEnabled
|
|
798
|
-
? 'inline-flex cursor-grab text-muted-foreground hover:text-foreground active:cursor-grabbing'
|
|
799
|
-
: 'inline-flex cursor-not-allowed text-muted-foreground/40'
|
|
800
|
-
}
|
|
801
|
-
>
|
|
802
|
-
<GripVerticalIcon className="size-4" />
|
|
803
|
-
</span>
|
|
804
|
-
</TableCell>
|
|
805
|
-
)}
|
|
806
|
-
{hasBulkActions && (
|
|
807
|
-
<TableCell className="w-9 px-3">
|
|
808
|
-
<Checkbox
|
|
809
|
-
aria-label={`Select row ${id}`}
|
|
810
|
-
checked={isSelected}
|
|
811
|
-
onCheckedChange={() => toggleRow(id)}
|
|
812
|
-
/>
|
|
813
|
-
</TableCell>
|
|
814
|
-
)}
|
|
815
|
-
{visibleColumns.map((col, ci) => {
|
|
816
|
-
const name = String(col['name'] ?? '')
|
|
817
|
-
const value = recordObj[name]
|
|
818
|
-
const align = col['alignment'] === 'center' ? 'text-center'
|
|
819
|
-
: col['alignment'] === 'end' ? 'text-right'
|
|
820
|
-
: 'text-left'
|
|
821
|
-
const widthStyle = col['width']
|
|
822
|
-
? { width: String(col['width']) }
|
|
823
|
-
: undefined
|
|
824
|
-
|
|
825
|
-
// Inline-edit cells take priority over read-only chrome.
|
|
826
|
-
// `_cellEditable[name]` is set per row by `loadTableRecords`
|
|
827
|
-
// only when `R.canEdit(user, row)` passed; the URL was
|
|
828
|
-
// stamped by `tagCellEditUrls` immediately after.
|
|
829
|
-
const editableMap = recordObj['_cellEditable'] as Record<string, true> | undefined
|
|
830
|
-
const editUrlMap = recordObj['_cellEditUrls'] as Record<string, string> | undefined
|
|
831
|
-
const cellDisabledMap = recordObj['_cellDisabled'] as Record<string, true> | undefined
|
|
832
|
-
const editUrl = editableMap?.[name] ? editUrlMap?.[name] : undefined
|
|
833
|
-
const EditableComp = editUrl !== undefined
|
|
834
|
-
? pickEditableCell(String(col['columnType'] ?? 'text'))
|
|
835
|
-
: null
|
|
836
|
-
if (EditableComp && editUrl !== undefined) {
|
|
837
|
-
const cellDisabled = col['disabled'] === true || cellDisabledMap?.[name] === true
|
|
838
|
-
const cellSelectOptionsMap = recordObj['_cellSelectOptions'] as
|
|
839
|
-
Record<string, Array<{ value: string; label: string }>> | undefined
|
|
840
|
-
const rowOptions = cellSelectOptionsMap?.[name]
|
|
841
|
-
return (
|
|
842
|
-
<TableCell key={ci} className={`text-sm text-foreground ${align} p-0`} style={widthStyle}>
|
|
843
|
-
<EditableComp
|
|
844
|
-
url={editUrl}
|
|
845
|
-
col={col}
|
|
846
|
-
value={value}
|
|
847
|
-
disabled={cellDisabled}
|
|
848
|
-
{...(rowOptions ? { rowOptions } : {})}
|
|
849
|
-
/>
|
|
850
|
-
</TableCell>
|
|
851
|
-
)
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const cellContent = formatCell(value, col, recordObj)
|
|
855
|
-
const colUrl = resolveColumnUrl(col, tableUrl, colUrls)
|
|
856
|
-
return (
|
|
857
|
-
<TableCell key={ci} className={`text-sm text-foreground ${align} p-0`} style={widthStyle}>
|
|
858
|
-
{colUrl !== undefined
|
|
859
|
-
? <RecordCellLink href={colUrl} navigate={navigate}>{cellContent}</RecordCellLink>
|
|
860
|
-
: <div className="px-2 py-2">{cellContent}</div>}
|
|
861
|
-
</TableCell>
|
|
862
|
-
)
|
|
863
|
-
})}
|
|
864
|
-
{hasRowActions && (
|
|
865
|
-
<TableCell className="w-px text-right">
|
|
866
|
-
{renderRowActions(id, recordObj, rowActions, renderActionLike)}
|
|
867
|
-
</TableCell>
|
|
868
|
-
)}
|
|
869
|
-
</TableRow>
|
|
870
|
-
)}
|
|
871
|
-
{/* Per-group summary row — emitted at the end of each
|
|
872
|
-
group band (last row in group OR last row overall),
|
|
873
|
-
aligned to the same columns as the global tfoot.
|
|
874
|
-
Suppressed when the group is collapsed since the data
|
|
875
|
-
rows themselves are hidden. */}
|
|
876
|
-
{(() => {
|
|
877
|
-
if (!groupSummaries) return null
|
|
878
|
-
if (groupValue === undefined) return null
|
|
879
|
-
if (isInCollapsedGroup) return null
|
|
880
|
-
const isLastInGroup = ri === rows.length - 1
|
|
881
|
-
|| String(((rows[ri + 1] as Record<string, unknown>)['_groupValue'] ?? '')) !== groupValue
|
|
882
|
-
if (!isLastInGroup) return null
|
|
883
|
-
const perCol = groupSummaries[groupValue]
|
|
884
|
-
if (!perCol || Object.keys(perCol).length === 0) return null
|
|
885
|
-
return (
|
|
886
|
-
<TableRow key={`group-summary-${id}`} className="bg-muted/20 hover:bg-muted/20">
|
|
887
|
-
{reorderColumnVisible && <TableCell />}
|
|
888
|
-
{hasBulkActions && <TableCell />}
|
|
889
|
-
{visibleColumns.map((col, ci) => {
|
|
890
|
-
const name = String(col['name'] ?? '')
|
|
891
|
-
const align = col['alignment'] === 'center' ? 'text-center'
|
|
892
|
-
: col['alignment'] === 'end' ? 'text-right'
|
|
893
|
-
: 'text-left'
|
|
894
|
-
const items = perCol[name]
|
|
895
|
-
return (
|
|
896
|
-
<TableCell key={ci} className={`text-xs font-medium ${align} px-2 py-1.5`}>
|
|
897
|
-
{items?.map((s, i) => (
|
|
898
|
-
<div key={i} className="leading-tight">
|
|
899
|
-
{s.label && <span className="text-muted-foreground">{s.label}: </span>}
|
|
900
|
-
<span>{s.value}</span>
|
|
901
|
-
</div>
|
|
902
|
-
))}
|
|
903
|
-
</TableCell>
|
|
904
|
-
)
|
|
905
|
-
})}
|
|
906
|
-
{hasRowActions && <TableCell />}
|
|
907
|
-
</TableRow>
|
|
908
|
-
)
|
|
909
|
-
})()}
|
|
910
|
-
</React.Fragment>
|
|
911
|
-
)
|
|
912
|
-
})}
|
|
913
|
-
</TableBody>
|
|
914
|
-
{summaries && Object.keys(summaries).length > 0 && (
|
|
915
|
-
<TableFooter>
|
|
916
|
-
<TableRow>
|
|
917
|
-
{reorderColumnVisible && <TableCell />}
|
|
918
|
-
{hasBulkActions && <TableCell />}
|
|
919
|
-
{visibleColumns.map((col, ci) => {
|
|
920
|
-
const name = String(col['name'] ?? '')
|
|
921
|
-
const align = col['alignment'] === 'center' ? 'text-center'
|
|
922
|
-
: col['alignment'] === 'end' ? 'text-right'
|
|
923
|
-
: 'text-left'
|
|
924
|
-
const items = summaries[name]
|
|
925
|
-
return (
|
|
926
|
-
<TableCell key={ci} className={`text-sm font-medium ${align}`}>
|
|
927
|
-
{items?.map((s, i) => (
|
|
928
|
-
<div key={i} className="leading-tight">
|
|
929
|
-
{s.label && <span className="text-muted-foreground">{s.label}: </span>}
|
|
930
|
-
<span>{s.value}</span>
|
|
931
|
-
</div>
|
|
932
|
-
))}
|
|
933
|
-
</TableCell>
|
|
934
|
-
)
|
|
935
|
-
})}
|
|
936
|
-
{hasRowActions && <TableCell />}
|
|
937
|
-
</TableRow>
|
|
938
|
-
</TableFooter>
|
|
939
|
-
)}
|
|
940
|
-
</DataTable>
|
|
941
|
-
</div>
|
|
942
|
-
)}
|
|
943
|
-
{showPagination && (
|
|
944
|
-
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
945
|
-
<span>
|
|
946
|
-
Page {currentPage} of {totalPages}{total > 0 ? ` · ${total} record${total === 1 ? '' : 's'}` : ''}
|
|
947
|
-
</span>
|
|
948
|
-
<div className="flex items-center gap-2">
|
|
949
|
-
{currentPage > 1 && (
|
|
950
|
-
<a
|
|
951
|
-
href={buildTableQuery(state, { page: currentPage - 1 }, currentPath, activeFilters, queryPrefix)}
|
|
952
|
-
className="rounded-md border px-3 py-1 text-xs hover:bg-muted"
|
|
953
|
-
>
|
|
954
|
-
← Previous
|
|
955
|
-
</a>
|
|
956
|
-
)}
|
|
957
|
-
{currentPage < totalPages && (
|
|
958
|
-
<a
|
|
959
|
-
href={buildTableQuery(state, { page: currentPage + 1 }, currentPath, activeFilters, queryPrefix)}
|
|
960
|
-
className="rounded-md border px-3 py-1 text-xs hover:bg-muted"
|
|
961
|
-
>
|
|
962
|
-
Next →
|
|
963
|
-
</a>
|
|
964
|
-
)}
|
|
965
|
-
</div>
|
|
966
|
-
</div>
|
|
967
|
-
)}
|
|
968
|
-
{hasFilters && filtersBelow && (
|
|
969
|
-
<FilterStrip filters={filters} prefix={queryPrefix} renderFormChild={renderFormChild} />
|
|
970
|
-
)}
|
|
971
|
-
</div>
|
|
972
|
-
)
|
|
973
|
-
}
|
|
974
|
-
|