@pilotiq/pilotiq 0.23.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/dist/actions/exportFactory.d.ts +10 -0
- package/dist/actions/exportFactory.d.ts.map +1 -1
- package/dist/actions/exportFactory.js +10 -0
- package/dist/actions/exportFactory.js.map +1 -1
- package/dist/react/CollabRoomContext.d.ts +5 -5
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +0 -1
- package/dist/react/index.js.map +1 -1
- package/dist/routes/helpers.d.ts.map +1 -1
- package/dist/routes/helpers.js +6 -2
- package/dist/routes/helpers.js.map +1 -1
- package/dist/routes/relations.d.ts.map +1 -1
- package/dist/routes/relations.js +12 -0
- package/dist/routes/relations.js.map +1 -1
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/dist/react/useCollabSeed.d.ts +0 -23
- package/dist/react/useCollabSeed.d.ts.map +0 -1
- package/dist/react/useCollabSeed.js +0 -82
- package/dist/react/useCollabSeed.js.map +0 -1
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -215
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -195
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useCollabSeed.ts +0 -86
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -700
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1227
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pilotiq-fields
|
|
3
|
+
description: Form fields in pilotiq — the 24 built-in field types, common setters, validators, and reactive ($get/$set + live + afterStateUpdated) patterns
|
|
4
|
+
license: MIT
|
|
5
|
+
appliesTo:
|
|
6
|
+
- '@pilotiq/pilotiq'
|
|
7
|
+
trigger: defining or editing a form field inside a `Resource.form()` schema — `TextField` / `SelectField` / `Repeater` / etc., setting `validate()` / `unique()` / `distinct()`, or wiring reactive `live()` / `afterStateUpdated`
|
|
8
|
+
skip: working with read-only display primitives (`TextEntry` / `BadgeEntry` / etc.) — those are infolist entries, not form fields
|
|
9
|
+
metadata:
|
|
10
|
+
author: pilotiq
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Pilotiq Fields
|
|
14
|
+
|
|
15
|
+
## When to use this skill
|
|
16
|
+
|
|
17
|
+
Load when you're:
|
|
18
|
+
|
|
19
|
+
- Picking a field type for a form (text vs textarea vs markdown vs richtext, single-select vs multi-select)
|
|
20
|
+
- Setting common chrome — labels, placeholders, helper text, prefixes, suffixes
|
|
21
|
+
- Adding validators (`required`, `email`, `min/maxLength`, `pattern`, `unique`, `distinct`)
|
|
22
|
+
- Wiring reactive behavior — `live()` to re-resolve schema on change, `afterStateUpdated()` to imperatively update sibling fields
|
|
23
|
+
|
|
24
|
+
For Resource-level concerns (when to override `form()` itself), use `pilotiq-resource`. For relation-backed array fields (`Repeater.relationship()`), see `pilotiq-relations`.
|
|
25
|
+
|
|
26
|
+
## Quick Reference
|
|
27
|
+
|
|
28
|
+
| Task | Open |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Field types — full catalog of 24 built-ins, when to use each | `rules/field-catalog.md` |
|
|
31
|
+
| Validation — built-in validators, `unique()` async DB probe, `distinct()` cross-row uniqueness | `rules/validation.md` |
|
|
32
|
+
| Reactive fields — `live()`, `afterStateUpdated`, `afterStateUpdatedJs`, `$get` / `$set`, multi-form `formId` | `rules/reactive-fields.md` |
|
|
33
|
+
|
|
34
|
+
## Key concepts (load once)
|
|
35
|
+
|
|
36
|
+
- **Every field is a static `make(name)` builder.** `TextField.make('title').required().label('Title')` — the `name` is the form field name AND the model column name.
|
|
37
|
+
- **Common setters cascade from `Field` base.** `.label() / .helperText() / .placeholder() / .default() / .prefix() / .suffix() / .required() / .validate() / .visible() / .hidden() / .disabled() / .columnSpan() / .live() / .afterStateUpdated() / .dehydrated() / .formatStateUsing() / .autofocus() / .hiddenLabel() / .disabledOn() / .hiddenOn() / .visibleOn()` work on every subclass.
|
|
38
|
+
- **Operation-aware shortcuts.** `disabledOn(['edit'])` / `hiddenOn(['view'])` / `visibleOn(['create', 'edit'])` resolve against page mode (`'create' | 'edit' | 'view'`). They no-op on custom Pages (mode unset). `readonly()` still wins over `disabledOn`.
|
|
39
|
+
- **Validators are async.** `Validator = (value, ctx?) => string | null | Promise<string | null>` — return a message string to fail, `null` to pass. `validateSchema()` awaits each field's validators in declaration order.
|
|
40
|
+
- **Reactive fields require `live()`.** Without it, `afterStateUpdated` only fires on submit. With it, every change POSTs to a partial-resolve endpoint that re-runs the schema with fresh `$get/$set` state.
|
|
41
|
+
- **Live forms must pin `formId`.** `Form.make().formId('details')` is required when more than one form lives on a single page. The auto-fallback covers single-form pages.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
- `playground/app/Pilotiq/Articles/Schemas/form.ts` — typical form with TextField / Textarea / SelectField / DateField.
|
|
46
|
+
- `playground/app/Pilotiq/Posts/Schemas/form.ts` — reactive fields (`live()` + `afterStateUpdated`) for title→slug.
|
|
47
|
+
- `playground/app/Pilotiq/BlocksDemo/Schemas/form.ts` — Builder field with heterogeneous block types.
|
|
@@ -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.
|