@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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# Field Catalog
|
|
2
|
+
|
|
3
|
+
24 built-in field types. Every field is a static `make(name)` builder; all share the `Field` base setters (see "Common setters" at the bottom).
|
|
4
|
+
|
|
5
|
+
## Text-like
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
TextField.make('title')
|
|
9
|
+
.placeholder('e.g. My first article')
|
|
10
|
+
.prefix('https://')
|
|
11
|
+
.suffix('.com')
|
|
12
|
+
.copyable() // value-side copy icon
|
|
13
|
+
.password() // <input type="password">
|
|
14
|
+
.revealable() // password reveal toggle
|
|
15
|
+
.mask('+1 (999) 999-9999') // input mask
|
|
16
|
+
.datalist(['draft', 'review', 'final'])// browser-native datalist
|
|
17
|
+
.stripCharacters(/[^\w-]/g) // sanitize on input
|
|
18
|
+
.trim() // trim whitespace
|
|
19
|
+
.inputMode('email') // mobile keyboard hint
|
|
20
|
+
.autocapitalize('words')
|
|
21
|
+
|
|
22
|
+
EmailField.make('email') // auto-attaches email() validator
|
|
23
|
+
|
|
24
|
+
NumberField.make('price')
|
|
25
|
+
.min(0).max(10_000).step(0.01)
|
|
26
|
+
|
|
27
|
+
Slider.make('volume')
|
|
28
|
+
.range(0, 100).step(5)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Long text
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
Textarea.make('bio')
|
|
35
|
+
.rows(5)
|
|
36
|
+
.cols(80)
|
|
37
|
+
.autosize() // grow with content
|
|
38
|
+
.disableGrammarly()
|
|
39
|
+
|
|
40
|
+
MarkdownField.make('body')
|
|
41
|
+
.toolbarButtons(['bold', 'italic', 'link', 'codeBlock'])
|
|
42
|
+
.minHeight('20rem')
|
|
43
|
+
|
|
44
|
+
RichTextField.make('description') // requires @pilotiq/tiptap
|
|
45
|
+
.toolbarButtons([...])
|
|
46
|
+
|
|
47
|
+
CodeEditorField.make('snippet') // requires @pilotiq/codemirror
|
|
48
|
+
.language('javascript')
|
|
49
|
+
.lineNumbers()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`RichTextField` and `CodeEditorField` ship in separate adapter packages — install `@pilotiq/tiptap` / `@pilotiq/codemirror` and register via `.plugins([tiptap(), codeEditor()])` on the panel.
|
|
53
|
+
|
|
54
|
+
## Choice
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
SelectField.make('status')
|
|
58
|
+
.options({ draft: 'Draft', review: 'In review', published: 'Published' })
|
|
59
|
+
.searchable()
|
|
60
|
+
.nullable() // adds a clear option
|
|
61
|
+
.preload() // fetch all options on mount (default)
|
|
62
|
+
|
|
63
|
+
// Dynamic options
|
|
64
|
+
SelectField.make('region')
|
|
65
|
+
.options(async ({ $get }) => {
|
|
66
|
+
const country = $get('country')
|
|
67
|
+
return country ? regionsFor(country) : {}
|
|
68
|
+
})
|
|
69
|
+
.live()
|
|
70
|
+
|
|
71
|
+
// Inline create
|
|
72
|
+
SelectField.make('tagId')
|
|
73
|
+
.options(async () => Object.fromEntries((await Tag.all()).map(t => [t.id, t.name])))
|
|
74
|
+
.createOptionForm([TextField.make('name').required()])
|
|
75
|
+
.createOptionUsing(async ({ name }) => {
|
|
76
|
+
const tag = await Tag.create({ name })
|
|
77
|
+
return tag.id // return the new value
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
RadioField.make('priority') // single-select radio stack
|
|
81
|
+
.options({ low: 'Low', med: 'Medium', high: 'High' })
|
|
82
|
+
|
|
83
|
+
ToggleButtons.make('size') // chip-style segmented (sugar over Radio)
|
|
84
|
+
.options({ s: 'S', m: 'M', l: 'L', xl: 'XL' })
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Boolean
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
CheckboxField.make('agreeToTerms') // single bool, renders as checkbox
|
|
91
|
+
.required('You must agree to continue')
|
|
92
|
+
|
|
93
|
+
ToggleField.make('isPublic') // single bool, renders as switch
|
|
94
|
+
|
|
95
|
+
CheckboxList.make('topics') // string[] value, checkbox stack
|
|
96
|
+
.options({ js: 'JavaScript', ts: 'TypeScript', rust: 'Rust' })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tags / collections
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
TagsInput.make('keywords') // string[] value, JSON-encoded
|
|
103
|
+
.suggestions(['design', 'code', 'process'])
|
|
104
|
+
.reorderable() // HTML5 drag-and-drop
|
|
105
|
+
.maxTags(8)
|
|
106
|
+
.separator(',')
|
|
107
|
+
|
|
108
|
+
KeyValueField.make('metadata') // Record<string, string>
|
|
109
|
+
.keyLabel('Field')
|
|
110
|
+
.valueLabel('Value')
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Date / time / color / file
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
DateField.make('publishedAt')
|
|
117
|
+
.minDate(new Date()) // future-only
|
|
118
|
+
|
|
119
|
+
DateTimePicker.make('eventAt')
|
|
120
|
+
.seconds(false)
|
|
121
|
+
|
|
122
|
+
ColorPicker.make('brandColor')
|
|
123
|
+
.palette(['#ef4444', '#10b981', '#3b82f6'])
|
|
124
|
+
|
|
125
|
+
FileUpload.make('cover')
|
|
126
|
+
.accept('image/*')
|
|
127
|
+
.maxSize(5 * 1024 * 1024) // 5 MB
|
|
128
|
+
.multiple() // string[] when on
|
|
129
|
+
.imageEditor() // crop / rotate before upload
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`FileUpload` requires an upload adapter wired at the panel level:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { localUpload } from '@pilotiq/pilotiq/uploads'
|
|
136
|
+
|
|
137
|
+
adminPanel.uploads({
|
|
138
|
+
adapter: localUpload({ root: 'public/uploads', urlPrefix: '/uploads' }),
|
|
139
|
+
})
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
S3 / R2 / custom adapters implement the same `UploadAdapter` interface.
|
|
143
|
+
|
|
144
|
+
## Array-of-rows (Repeater / Builder)
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
Repeater.make('items') // uniform rows
|
|
148
|
+
.schema([
|
|
149
|
+
TextField.make('label').required(),
|
|
150
|
+
NumberField.make('qty').required(),
|
|
151
|
+
])
|
|
152
|
+
.min(1)
|
|
153
|
+
.maxItems(10)
|
|
154
|
+
.reorderable()
|
|
155
|
+
.cloneable()
|
|
156
|
+
.collapsible()
|
|
157
|
+
.itemLabel(row => row.label || 'New item')
|
|
158
|
+
|
|
159
|
+
Builder.make('content') // heterogeneous rows
|
|
160
|
+
.blocks([
|
|
161
|
+
Block.make('heading').icon('heading').schema([
|
|
162
|
+
TextField.make('text').required(),
|
|
163
|
+
SelectField.make('level').options({ h1: 'H1', h2: 'H2', h3: 'H3' }),
|
|
164
|
+
]),
|
|
165
|
+
Block.make('paragraph').icon('text').schema([
|
|
166
|
+
MarkdownField.make('body'),
|
|
167
|
+
]),
|
|
168
|
+
Block.make('image').icon('image').schema([
|
|
169
|
+
FileUpload.make('src').accept('image/*').required(),
|
|
170
|
+
TextField.make('alt'),
|
|
171
|
+
]),
|
|
172
|
+
])
|
|
173
|
+
.reorderable()
|
|
174
|
+
.blockPickerColumns(2)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
For relation-backed rows (real `hasMany` / `morph*` / M2M children instead of JSON-blob storage), see the `pilotiq-relations` skill.
|
|
178
|
+
|
|
179
|
+
## Hidden
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
HiddenField.make('authorId') // always submitted, never rendered
|
|
183
|
+
.default(({ user }) => user.id)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Common setters
|
|
187
|
+
|
|
188
|
+
Every field inherits these from `Field`:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
Field.make('name')
|
|
192
|
+
.label('Display label') // sr-only if empty
|
|
193
|
+
.helperText('Shown below the input')
|
|
194
|
+
.placeholder('e.g. Hello world')
|
|
195
|
+
.default('initial value') // or () => value, or (ctx) => value
|
|
196
|
+
.prefix('$') // or .prefix({ icon: 'dollar' })
|
|
197
|
+
.suffix('USD')
|
|
198
|
+
.required() // implicit required validator
|
|
199
|
+
.validate([Field.email(), Field.unique({ model: User })])
|
|
200
|
+
.visible(({ user }) => user.role === 'admin')
|
|
201
|
+
.hidden(rule)
|
|
202
|
+
.disabled(rule)
|
|
203
|
+
.columnSpan(2) // inside a Grid / Section.columns(n)
|
|
204
|
+
.live() // re-resolve schema on change
|
|
205
|
+
.afterStateUpdated((value, ctx) => ctx.$set('slug', slugify(value)))
|
|
206
|
+
.dehydrated(false) // exclude from POST body
|
|
207
|
+
.formatStateUsing(v => `${v} px`) // display transform (read paths)
|
|
208
|
+
.autofocus()
|
|
209
|
+
.hiddenLabel() // visually hidden, sr-only kept
|
|
210
|
+
.validationAttribute('email address') // tunes the implicit-required text
|
|
211
|
+
.extraAttributes({ 'data-cy': 'name' }) // outer wrapper attrs
|
|
212
|
+
.extraInputAttributes({ autocomplete: 'off' }) // <input> attrs
|
|
213
|
+
.disabledOn(['edit']) // page-mode sugar
|
|
214
|
+
.hiddenOn(['view'])
|
|
215
|
+
.visibleOn(['create', 'edit'])
|
|
216
|
+
.readonly() // disabled + non-submittable
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Operation-aware shortcuts
|
|
220
|
+
|
|
221
|
+
`disabledOn` / `hiddenOn` / `visibleOn` are sugar over `disabled(ctx => ctx.mode === 'edit')` / `hidden(ctx => ctx.mode === 'view')` / `visible(ctx => ['create', 'edit'].includes(ctx.mode))`.
|
|
222
|
+
|
|
223
|
+
They resolve against page mode (`'table' | 'create' | 'edit' | 'view'`) and no-op on custom Pages (mode is unset). `readonly()` wins over `disabledOn`.
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
TextField.make('email')
|
|
227
|
+
.disabledOn(['edit']) // can set on create, locked on edit
|
|
228
|
+
.visibleOn(['create', 'edit']) // never on view
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Conditional visibility
|
|
232
|
+
|
|
233
|
+
`.visible(rule)` / `.hidden(rule)` / `.disabled(rule)` accept `boolean | (ctx: ConditionContext) => bool | Promise<bool>`:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
ConditionContext = {
|
|
237
|
+
record?: unknown // current record on edit/view
|
|
238
|
+
values?: Record<string, unknown> // form values (only when reactive)
|
|
239
|
+
user?: unknown // from Pilotiq.user()
|
|
240
|
+
mode?: 'create' | 'edit' | 'view'
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
TextField.make('publishUrl')
|
|
246
|
+
.visible(({ values }) => values?.status === 'published')
|
|
247
|
+
|
|
248
|
+
TextField.make('adminNotes')
|
|
249
|
+
.visible(({ user }) => user.role === 'admin')
|
|
250
|
+
|
|
251
|
+
TextField.make('signature')
|
|
252
|
+
.disabled(({ record }) => record?.locked === true)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
For visibility that depends on form values to change in real-time, ALSO add `.live()` to the source field — otherwise the dependent field only re-evaluates on submit.
|
|
256
|
+
|
|
257
|
+
## Display-only transforms
|
|
258
|
+
|
|
259
|
+
`formatStateUsing(v => …)` runs on the read path (loadRecord → fill) to transform the value for display. It does NOT affect the submitted value:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
NumberField.make('priceInCents')
|
|
263
|
+
.formatStateUsing(v => (v / 100).toFixed(2))
|
|
264
|
+
// user sees "9.99"; column stores 999
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For two-way conversion, pair with an accessor / mutator on the underlying model (e.g. `Attribute.make({ get: c => c / 100, set: d => d * 100 })`).
|
|
268
|
+
|
|
269
|
+
## Mass-assignment + `dehydrated`
|
|
270
|
+
|
|
271
|
+
Fields submit by default. `dehydrated(false)` excludes the field from the POST body:
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
TextField.make('computedSlug')
|
|
275
|
+
.dehydrated(false)
|
|
276
|
+
.formatStateUsing(({ values }) => slugify(values?.title ?? ''))
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Useful for derived display, server-computed values, or admin-only debug toggles. The model never receives the field; coerce + validate skip it.
|
|
280
|
+
|
|
281
|
+
## Common pitfalls
|
|
282
|
+
|
|
283
|
+
- **`SelectField.options(fn)` without `.live()` upstream** — when options depend on another field via `$get`, the source field must be `.live()` for the dependent options to re-fetch. Otherwise the options resolve once at form load.
|
|
284
|
+
- **`TagsInput` stores `string[]` via JSON-encoded hidden input** — if you read it directly with `parseFormBody`, decode the JSON. The framework's `coerceFormValues` already handles it.
|
|
285
|
+
- **`FileUpload` without an upload adapter** silently hides the drop zone via `RenderContext.hasUploadAdapter`. Wire `panel.uploads({ adapter })` to expose it.
|
|
286
|
+
- **`Repeater.simple(field)` is a different storage shape.** `Repeater.make().schema([TextField.make('value')])` stores `[{ value: 'a' }, { value: 'b' }]`. `Repeater.simple(TextField.make('value'))` stores `['a', 'b']` — flat array. The framework wraps/unwraps internally; the inner schema must be a single field.
|
|
287
|
+
- **`HiddenField` is still in the submitted body** — `dehydrated(false)` exists for the case where you want a value rendered but not submitted (typically for `formatStateUsing` display).
|
|
288
|
+
- **`columnSpan(n)` only works inside a layout that defines a column grid** (`Section.columns(n)` / `Grid.columns(n)`). Bare schema arrays don't grid.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Reactive Fields
|
|
2
|
+
|
|
3
|
+
Forms are static by default — the schema resolves once on page load, the form renders, the user fills + submits. Reactive fields make schema resolution dynamic: fields can re-resolve on every keystroke (`live()`), and a field's value can imperatively update sibling fields via `afterStateUpdated`.
|
|
4
|
+
|
|
5
|
+
## `Field.live()`
|
|
6
|
+
|
|
7
|
+
`live()` marks a field as a re-resolve trigger:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
TextField.make('title')
|
|
11
|
+
.live() // re-resolve on every change
|
|
12
|
+
.afterStateUpdated((title, ctx) => ctx.$set('slug', slugify(title)))
|
|
13
|
+
|
|
14
|
+
SelectField.make('country')
|
|
15
|
+
.options(countries)
|
|
16
|
+
.live()
|
|
17
|
+
|
|
18
|
+
SelectField.make('region') // dependent on country
|
|
19
|
+
.options(({ $get }) => {
|
|
20
|
+
const country = $get('country')
|
|
21
|
+
return country ? regionsFor(country) : {}
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When a `live()` field changes:
|
|
26
|
+
|
|
27
|
+
1. Client POSTs `{ changed: 'title', values: {...} }` to `Form.stateUrl`.
|
|
28
|
+
2. Server runs `applyStateUpdate()` — finds the changed field, runs its `afterStateUpdated` hook (which may `$set` siblings), then re-resolves the form with the updated values.
|
|
29
|
+
3. Server returns the new `FormMeta` (with refreshed conditional visibility, options, helper text).
|
|
30
|
+
4. Client diffs and re-renders.
|
|
31
|
+
|
|
32
|
+
Tune the trigger:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
TextField.make('search')
|
|
36
|
+
.live({ debounce: 300 }) // wait 300ms after last keystroke
|
|
37
|
+
.afterStateUpdated((q, ctx) => ctx.$set('results', searchFor(q)))
|
|
38
|
+
|
|
39
|
+
TextField.make('rawJson')
|
|
40
|
+
.live({ onBlur: true }) // only fire on blur, not per keystroke
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Bare `.live()` fires immediately on every change.
|
|
44
|
+
|
|
45
|
+
## `afterStateUpdated`
|
|
46
|
+
|
|
47
|
+
The server-side hook that fires when a `live()` field changes:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
TextField.make('title')
|
|
51
|
+
.live()
|
|
52
|
+
.afterStateUpdated(async (value, ctx) => {
|
|
53
|
+
// value: the new value of THIS field
|
|
54
|
+
// ctx: { $get, $set, values, user, record?, basePath?, row? }
|
|
55
|
+
|
|
56
|
+
if (!ctx.$get('slug')) { // only auto-fill if blank
|
|
57
|
+
ctx.$set('slug', slugify(value))
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `ctx` API:
|
|
63
|
+
|
|
64
|
+
- **`$get(name)`** — read another field's current value. Works for nested paths (`$get('contacts.0.email')`).
|
|
65
|
+
- **`$set(name, value)`** — write another field's value. The new value lands in the next `FormMeta` response.
|
|
66
|
+
- **`values`** — snapshot of all form values at this moment.
|
|
67
|
+
- **`row`** — when the hook fires inside a `Repeater` / `Builder` row, this is the row context `{ index, id, values, fieldName, blockType? }`. The plain `$get` / `$set` are scoped to the row — `$set('label', 'X')` writes `items.<row>.label`.
|
|
68
|
+
|
|
69
|
+
Resolve-time `$set` is a no-op closure during schema re-resolution; only the `afterStateUpdated` write survives. This prevents infinite re-resolve loops.
|
|
70
|
+
|
|
71
|
+
Throws fail-loudly — the server returns 500 and the client falls back to the previous form state.
|
|
72
|
+
|
|
73
|
+
## Client-only reactivity: `afterStateUpdatedJs`
|
|
74
|
+
|
|
75
|
+
For trivial transformations (title → slug, sum of two fields), the server round-trip is overkill. `afterStateUpdatedJs` compiles a string body via `new Function` and runs it in the browser:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
TextField.make('title')
|
|
79
|
+
.afterStateUpdatedJs(`$set('slug', $state.title.toLowerCase().replace(/\\s+/g, '-'))`)
|
|
80
|
+
|
|
81
|
+
NumberField.make('subtotal')
|
|
82
|
+
.afterStateUpdatedJs(`$set('total', $state.subtotal * 1.0875)`)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The body has these bindings:
|
|
86
|
+
|
|
87
|
+
- `$state` — the form's current values
|
|
88
|
+
- `$get(name)` / `$set(name, value)` — same shape as the server hook
|
|
89
|
+
- `$value` — the changed field's new value (sugar over `$state[$name]`)
|
|
90
|
+
- `$name` — the changed field's name
|
|
91
|
+
|
|
92
|
+
Compiled once per source-string (cached via identity in `react/fieldJsHandler.ts`). Runs synchronously on every change. No `live()` required — the JS fires regardless.
|
|
93
|
+
|
|
94
|
+
Compose with the server hook:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
TextField.make('title')
|
|
98
|
+
.live()
|
|
99
|
+
.afterStateUpdatedJs(`$set('slug', $value.toLowerCase().replace(/\\s+/g, '-'))`)
|
|
100
|
+
.afterStateUpdated(async (value, ctx) => {
|
|
101
|
+
// Server-side: validate the auto-slug is unique
|
|
102
|
+
const existing = await Article.where('slug', ctx.$get('slug')).count()
|
|
103
|
+
if (existing > 0) ctx.$set('slug', `${ctx.$get('slug')}-${Date.now()}`)
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
JS runs first, server response overlays sibling values when it comes back.
|
|
108
|
+
|
|
109
|
+
Note: `afterStateUpdatedJs` requires CSP `unsafe-eval`. If your CSP is locked down, stick with the server hook.
|
|
110
|
+
|
|
111
|
+
## Multi-form pages: pin `formId`
|
|
112
|
+
|
|
113
|
+
The auto-fall-back covers single-form pages. For pages with multiple forms (a record-page with both a "Settings" form and a "Notifications" form), you MUST pin `formId` explicitly:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
Form.make()
|
|
117
|
+
.formId('settings') // stable across re-renders
|
|
118
|
+
.schema([...])
|
|
119
|
+
|
|
120
|
+
Form.make()
|
|
121
|
+
.formId('notifications')
|
|
122
|
+
.schema([...])
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Without `formId`, the framework can't tell which form's state-update endpoint to POST to — live() silently fails. The auto-fallback uses the page slug; multi-form pages need distinct names.
|
|
126
|
+
|
|
127
|
+
The same applies to Repeaters / Builders that live inside live() forms — the form's `formId` is what disambiguates them.
|
|
128
|
+
|
|
129
|
+
## `$get` inside `SelectField.options(fn)`
|
|
130
|
+
|
|
131
|
+
Dependent options are the most common reactive pattern:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
SelectField.make('country').options(countries).live()
|
|
135
|
+
|
|
136
|
+
SelectField.make('region')
|
|
137
|
+
.options(async ({ $get, user }) => {
|
|
138
|
+
const country = $get('country')
|
|
139
|
+
if (!country) return {}
|
|
140
|
+
return await regionsFor(country)
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The options resolver receives the same `ctx` as `afterStateUpdated`. It runs every re-resolve cycle (so always sees fresh `$get`). Without `live()` on the source, the dependent options resolve ONCE on form load and never update.
|
|
145
|
+
|
|
146
|
+
## Conditional visibility based on live values
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
SelectField.make('billingType')
|
|
150
|
+
.options({ none: 'No billing', card: 'Credit card', invoice: 'Net 30' })
|
|
151
|
+
.live()
|
|
152
|
+
|
|
153
|
+
TextField.make('cardNumber')
|
|
154
|
+
.visible(({ values }) => values?.billingType === 'card')
|
|
155
|
+
|
|
156
|
+
TextField.make('purchaseOrder')
|
|
157
|
+
.visible(({ values }) => values?.billingType === 'invoice')
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Both `visible(({ values }) => …)` rules see fresh `values` on every re-resolve. Without `.live()` on `billingType`, the dependent fields re-evaluate only on submit.
|
|
161
|
+
|
|
162
|
+
## Reactive fields inside Repeater / Builder
|
|
163
|
+
|
|
164
|
+
Inside an array-row container, `$get` / `$set` accept dotted paths AND scope to the row by default:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
Repeater.make('items')
|
|
168
|
+
.schema([
|
|
169
|
+
SelectField.make('product').options(products).live(),
|
|
170
|
+
NumberField.make('quantity').default(1).live(),
|
|
171
|
+
NumberField.make('lineTotal')
|
|
172
|
+
.disabled()
|
|
173
|
+
.afterStateUpdatedJs(`
|
|
174
|
+
const product = $get('product')
|
|
175
|
+
const qty = Number($get('quantity'))
|
|
176
|
+
const price = product ? PRICES[product] : 0
|
|
177
|
+
$set('lineTotal', price * qty)
|
|
178
|
+
`),
|
|
179
|
+
])
|
|
180
|
+
|
|
181
|
+
// Or cross-row read with dotted path:
|
|
182
|
+
NumberField.make('discount')
|
|
183
|
+
.afterStateUpdated((v, ctx) => {
|
|
184
|
+
const subtotal = (ctx.values.items ?? []).reduce((s, r) => s + (r.lineTotal ?? 0), 0)
|
|
185
|
+
ctx.$set('total', subtotal - v)
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Inside the row, `$get('product')` reads the row's `product`. Outside the Repeater, `$get('items.0.product')` reads the same value via dotted path.
|
|
190
|
+
|
|
191
|
+
## Common pitfalls
|
|
192
|
+
|
|
193
|
+
- **`afterStateUpdated` without `.live()`** only fires on submit. The hook still exists but the partial-resolve endpoint never gets called.
|
|
194
|
+
- **Multi-form pages without `.formId('id')`** — live() silently no-ops because the framework can't route the partial-resolve POST. See `feedback_pilotiq_live_forms_pin_formid.md`.
|
|
195
|
+
- **Infinite loops via cross-`$set`** — if A's `afterStateUpdated` sets B, and B's `afterStateUpdated` sets A, the resolve-time `$set` no-op prevents the loop. But synchronous JS loops in `afterStateUpdatedJs` will hang the browser — write idempotent JS.
|
|
196
|
+
- **`$get` returning `undefined`** — fields not yet rendered or `dehydrated(false)` aren't in `values`. Guard with `?? defaultValue`.
|
|
197
|
+
- **`debounce` on dependent SelectFields** — the source's debounce delays the partial-resolve POST, which delays the dependent's options refresh. For "type to search" patterns, debounce the source 200-400ms.
|
|
198
|
+
- **`afterStateUpdatedJs` CSP** — if your app has a strict CSP without `unsafe-eval`, JS handlers throw at registration. Use the server hook instead.
|
|
199
|
+
- **Reading `values.items` outside a row context** — when reading the full Repeater array from a sibling field (e.g. computing `total` from all rows), use `ctx.values` (the form-wide snapshot), not `$get`. `$get('items')` returns the array; `$get('items.0.qty')` returns one row's field; both work.
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
Validators are functions: `(value, ctx?) => string | null | Promise<string | null>`. Return a message string to fail; return `null` to pass. The framework runs them in declaration order, awaits async ones, and aggregates per-field errors before calling the form's `save` handler.
|
|
4
|
+
|
|
5
|
+
## Built-in validators
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Field } from '@pilotiq/pilotiq'
|
|
9
|
+
|
|
10
|
+
TextField.make('email')
|
|
11
|
+
.validate([
|
|
12
|
+
Field.required(), // or just .required()
|
|
13
|
+
Field.email(),
|
|
14
|
+
Field.maxLength(255),
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
TextField.make('password')
|
|
18
|
+
.password()
|
|
19
|
+
.validate([
|
|
20
|
+
Field.required(),
|
|
21
|
+
Field.minLength(8, 'At least 8 characters'),
|
|
22
|
+
Field.pattern(/[A-Z]/, 'Must contain an uppercase letter'),
|
|
23
|
+
Field.pattern(/[0-9]/, 'Must contain a digit'),
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
NumberField.make('age')
|
|
27
|
+
.validate([
|
|
28
|
+
Field.min(18, 'Must be 18 or older'),
|
|
29
|
+
Field.max(120),
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
TextField.make('slug')
|
|
33
|
+
.validate([
|
|
34
|
+
Field.required(),
|
|
35
|
+
Field.pattern(/^[a-z0-9-]+$/, 'Lowercase letters, numbers, hyphens only'),
|
|
36
|
+
])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The full set: `required(message?) / email(message?) / minLength(n, message?) / maxLength(n, message?) / min(n, message?) / max(n, message?) / pattern(regex, message)`.
|
|
40
|
+
|
|
41
|
+
`Field.required()` is auto-contributed when you call `.required()` directly — no need to add it twice.
|
|
42
|
+
|
|
43
|
+
## Custom validators
|
|
44
|
+
|
|
45
|
+
A validator is just a function. Inline:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
TextField.make('username')
|
|
49
|
+
.validate(async (value, ctx) => {
|
|
50
|
+
if (typeof value !== 'string') return null
|
|
51
|
+
if (value.length < 3) return 'At least 3 characters'
|
|
52
|
+
if (value.startsWith('admin')) return 'Reserved prefix'
|
|
53
|
+
return null // pass
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The `ctx` shape:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
{
|
|
61
|
+
record?: unknown // current record on edit/view
|
|
62
|
+
values?: Record<string, unknown> // all form values
|
|
63
|
+
user?: unknown
|
|
64
|
+
mode?: 'create' | 'edit' | 'view'
|
|
65
|
+
basePath?: string
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Async is fine — the framework awaits each one.
|
|
70
|
+
|
|
71
|
+
## `Field.unique()` async DB probe
|
|
72
|
+
|
|
73
|
+
The standard "this field must be unique across all records" check:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { Field } from '@pilotiq/pilotiq'
|
|
77
|
+
|
|
78
|
+
TextField.make('slug')
|
|
79
|
+
.validate(Field.unique({
|
|
80
|
+
model: Article,
|
|
81
|
+
column: 'slug', // optional, defaults to field name
|
|
82
|
+
ignoreRecord: true, // skip the row matching ctx.record[pk]
|
|
83
|
+
where: { status: 'published' }, // optional scope
|
|
84
|
+
caseInsensitive: true,
|
|
85
|
+
message: 'Slug is already in use', // optional custom message
|
|
86
|
+
}))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
How it works:
|
|
90
|
+
|
|
91
|
+
- Issues `M.query().where(column, value).paginate(1, 2)` — limit 2 to detect uniqueness without scanning the table.
|
|
92
|
+
- `ignoreRecord: true` (default `true`) skips the row matching `ctx.record[primaryKey]` so edit-no-change saves don't conflict.
|
|
93
|
+
- `caseInsensitive: true` switches to SQL `LIKE` with `%` / `_` / `\` escaped (SQLite + MySQL friendly; Postgres collation-dependent).
|
|
94
|
+
- `where: { status: 'published' }` adds AND-clauses to the lookup query (useful for soft scopes — "unique among published rows").
|
|
95
|
+
- Inside a `Repeater`, `unique()` probes the database but does NOT see unsaved sibling rows. Pair with `distinct()` for cross-row uniqueness within the form.
|
|
96
|
+
|
|
97
|
+
`Field.unique` accepts a `Model`-like object (anything with `.query()`). The Resource doesn't need to be using the same Model.
|
|
98
|
+
|
|
99
|
+
## `Field.distinct()` cross-row uniqueness inside Repeater / Builder
|
|
100
|
+
|
|
101
|
+
Inside a `Repeater` or `Builder`, `unique()` only checks the database. To enforce that values are unique ACROSS rows in the form itself (before submit), use `distinct()`:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
Repeater.make('contacts')
|
|
105
|
+
.schema([
|
|
106
|
+
TextField.make('email')
|
|
107
|
+
.validate(Field.email())
|
|
108
|
+
.distinct(), // unique within this Repeater
|
|
109
|
+
TextField.make('label'),
|
|
110
|
+
])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Options:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
TextField.make('email')
|
|
117
|
+
.distinct({
|
|
118
|
+
caseInsensitive: true, // default false
|
|
119
|
+
ignoreNulls: true, // default true — skip empty rows
|
|
120
|
+
message: 'Email already used in another row',
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
For `Builder`, distinctness is per-block-type — `heading.text="X"` never conflicts with `paragraph.text="X"`.
|
|
125
|
+
|
|
126
|
+
Pair with `unique({ model })` for in-form + cross-record uniqueness:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
TextField.make('slug')
|
|
130
|
+
.validate(Field.unique({ model: Article })) // unique in DB
|
|
131
|
+
.distinct() // unique in this form too
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Form-level validators
|
|
135
|
+
|
|
136
|
+
`Form.validate(fn)` runs after every field's validators have passed:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
form
|
|
140
|
+
.schema([
|
|
141
|
+
DateField.make('startsAt').required(),
|
|
142
|
+
DateField.make('endsAt').required(),
|
|
143
|
+
])
|
|
144
|
+
.validate(({ values }) => {
|
|
145
|
+
if (values.endsAt < values.startsAt) {
|
|
146
|
+
return { endsAt: 'Must be after start date' }
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
})
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Return shape:
|
|
153
|
+
- `null` (or `{}`) — pass
|
|
154
|
+
- `{ [fieldName]: 'message' }` — per-field errors (replaces any field-level errors)
|
|
155
|
+
- `{ _form: 'Top-level message' }` — form-wide error, rendered at the top of the form
|
|
156
|
+
|
|
157
|
+
Form-level validators run AFTER coercion (so values are typed) but BEFORE save.
|
|
158
|
+
|
|
159
|
+
## Validation order
|
|
160
|
+
|
|
161
|
+
For each form submit, the framework runs:
|
|
162
|
+
|
|
163
|
+
1. **Coerce raw FormData values** to typed values (string → number for NumberField, JSON-string → object for KeyValueField, etc.). Coerce errors aren't reported per-field; raw values fall through.
|
|
164
|
+
2. **Field-level validators** — for every Field with `validate([...])` or `.required()`. Aggregated by field name.
|
|
165
|
+
3. **Form-level `validate(fn)`** — if present, runs only when no field errors fired (early exit).
|
|
166
|
+
4. **`save(ctx)` / model.create / model.update** — only reached when validation passes.
|
|
167
|
+
|
|
168
|
+
Important: **validate runs BEFORE coerce in `dispatchFormSubmit`**, then field-types do their own coerce-fold during their `runValidators`. The order is "validate the raw value, then coerce the validated value" — useful to remember when writing custom validators.
|
|
169
|
+
|
|
170
|
+
## Errors on the wire
|
|
171
|
+
|
|
172
|
+
Field errors land on `FormMeta.errors` as `Record<fieldName, string[]>`. The renderer auto-stamps them under each field's input. Repeater/Builder errors key as `items.<i>.<name>` / `name.<i>.data.<child>` respectively; `min/maxItems` lands under the bare field name.
|
|
173
|
+
|
|
174
|
+
For form-level errors, the `_form` key is special — the renderer surfaces it as an Alert at the top of the form.
|
|
175
|
+
|
|
176
|
+
## Throwing validators
|
|
177
|
+
|
|
178
|
+
A validator that throws is treated as a failure. The message comes from `err.message`:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
TextField.make('email')
|
|
182
|
+
.validate(async (value) => {
|
|
183
|
+
if (typeof value !== 'string') throw new Error('Email is required')
|
|
184
|
+
if (!value.includes('@')) return 'Invalid email'
|
|
185
|
+
return null
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For unique-across-DB or other async lookups, prefer returning `null` for "skip the check" rather than throwing — throws log + fail with the error message verbatim.
|
|
190
|
+
|
|
191
|
+
## Common pitfalls
|
|
192
|
+
|
|
193
|
+
- **`required()` doesn't see `null` as empty** — by default `required()` checks for `value !== null && value !== undefined && value !== ''`. For domain-specific empty (`[]`, `{}`), write a custom validator.
|
|
194
|
+
- **`Field.unique()` without `ignoreRecord: true`** trips on edit-no-change saves — the row's own value conflicts with itself. Default is `true`; only set to `false` for "even my own row counts."
|
|
195
|
+
- **`distinct()` outside a Repeater/Builder** is a no-op. The framework only evaluates it inside an array-row container.
|
|
196
|
+
- **Async validators in parallel** — validators within ONE field run serially in declaration order; validators across DIFFERENT fields run in parallel. Don't rely on cross-field ordering.
|
|
197
|
+
- **`pattern(/regex/)` without anchors** matches anywhere in the string. Add `^…$` if you mean "the whole value."
|
|
198
|
+
- **Validation result `{}`** (empty object) means "pass" — the framework treats no-keys as no-errors. Return `null` for clarity.
|