@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
package/src/pageData/helpers.ts
DELETED
|
@@ -1,857 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Pilotiq,
|
|
3
|
-
PilotiqConfig,
|
|
4
|
-
EditPageHydrator,
|
|
5
|
-
EditPageHydratorContext,
|
|
6
|
-
} from '../Pilotiq.js'
|
|
7
|
-
import type { Page } from '../Page.js'
|
|
8
|
-
import { Element } from '../schema/Element.js'
|
|
9
|
-
import { Field } from '../fields/Field.js'
|
|
10
|
-
import type { SchemaContext, RenderContext } from '../schema/resolveSchema.js'
|
|
11
|
-
import { Form } from '../elements/Form.js'
|
|
12
|
-
import { Table } from '../elements/Table.js'
|
|
13
|
-
import { Column } from '../Column.js'
|
|
14
|
-
import { SelectField } from '../fields/SelectField.js'
|
|
15
|
-
import { isRepeaterField, RepeaterField } from '../fields/RepeaterField.js'
|
|
16
|
-
import { isBuilderField, BuilderField } from '../fields/BuilderField.js'
|
|
17
|
-
import { isServerDataElement, type ServerDataElement } from '../schema/ServerDataElement.js'
|
|
18
|
-
import { findActions, findRowExtraActions } from '../elements/dispatchAction.js'
|
|
19
|
-
import { findForms, loadRelationRows } from '../elements/dispatchForm.js'
|
|
20
|
-
import { findTables } from '../elements/dispatchTable.js'
|
|
21
|
-
import {
|
|
22
|
-
getMorphRelationDescriptor,
|
|
23
|
-
getPrimaryKey,
|
|
24
|
-
type ModelLike,
|
|
25
|
-
} from '../orm/modelDefaults.js'
|
|
26
|
-
|
|
27
|
-
// ─── pageData shared helpers ────────────────────────────────
|
|
28
|
-
//
|
|
29
|
-
// URL-tag helpers (stamp dispatch URLs / cell-edit endpoints / wizard
|
|
30
|
-
// endpoints / mention-resolve endpoints), the load-record fill pipeline
|
|
31
|
-
// (`applyFillPipeline` + relationship Repeater/Builder fills),
|
|
32
|
-
// server-data widget resolution, and the two `*Ctx` helpers that the
|
|
33
|
-
// per-role builders use to construct the `SchemaContext` they pass into
|
|
34
|
-
// `resolveSchema`. Pure (mostly stateless) helpers — no per-page-role
|
|
35
|
-
// orchestration.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export function userCtx<C extends SchemaContext>(ctx: C, user: unknown): C {
|
|
39
|
-
if (user === null || user === undefined) return ctx
|
|
40
|
-
return { ...ctx, user: user as NonNullable<SchemaContext['user']> }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Run a (possibly async) boolean predicate; fail closed when it throws.
|
|
45
|
-
*
|
|
46
|
-
* Every page-builder pre-flight that consults `canX(user, …)` needs this
|
|
47
|
-
* shape — predicates may be sync booleans, Promises, or throw at the
|
|
48
|
-
* remote-auth layer; a throw must never propagate to the wire as an
|
|
49
|
-
* unhandled 500.
|
|
50
|
-
*/
|
|
51
|
-
export async function safeBool(fn: () => boolean | Promise<boolean>): Promise<boolean> {
|
|
52
|
-
try { return Boolean(await fn()) } catch { return false }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Plan #6 — stamp the panel-wide upload URL so `FileUpload` fields
|
|
56
|
-
* emit it on their meta. Single URL for the whole panel; no per-field
|
|
57
|
-
* variation. The route is always registered (see `_uploads` in
|
|
58
|
-
* `routes.ts`) — meta is stamped regardless of whether an adapter is
|
|
59
|
-
* configured so the renderer can show a clear error rather than
|
|
60
|
-
* silently breaking. The companion `hasUploadAdapter` flag distinguishes
|
|
61
|
-
* "URL exists but adapter missing" so fields with optional upload
|
|
62
|
-
* affordances (e.g. `MarkdownField`'s `attachFiles` button) can hide
|
|
63
|
-
* themselves rather than render a broken control. */
|
|
64
|
-
export function uploadCtx<C extends SchemaContext>(ctx: C, cfg: PilotiqConfig): C {
|
|
65
|
-
return {
|
|
66
|
-
...ctx,
|
|
67
|
-
uploadUrl: `${cfg.path}/_uploads`,
|
|
68
|
-
...(cfg.uploads ? { hasUploadAdapter: true } : {}),
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
export async function callPageSchema(PageClass: typeof Page, ctx: SchemaContext): Promise<Element[]> {
|
|
74
|
-
return Promise.resolve(PageClass.schema(ctx))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Mark every Form on the page with its action URL so the rendered <form> posts to itself. */
|
|
78
|
-
export function tagFormActions(elements: ReadonlyArray<Element>, action: string): void {
|
|
79
|
-
for (const form of findForms(elements)) {
|
|
80
|
-
if (!form.getAction()) form.action(action)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Plan #5 — stamp the partial-resolve endpoint URL on every form whose
|
|
86
|
-
* descendants include at least one `live()` field. The client uses
|
|
87
|
-
* `FormMeta.stateUrl` to flip into controlled-state mode; forms without
|
|
88
|
-
* any live fields stay uncontrolled (zero-cost legacy path).
|
|
89
|
-
*
|
|
90
|
-
* `urlBuilder(formId)` lets the caller compose a per-form URL — the
|
|
91
|
-
* endpoint shape is `${base}/${slug}/_form/${formId}/state` so each
|
|
92
|
-
* form on a multi-form page gets its own route segment.
|
|
93
|
-
*/
|
|
94
|
-
export function tagFormStateUrls(
|
|
95
|
-
elements: ReadonlyArray<Element>,
|
|
96
|
-
urlBuilder: (formId: string) => string,
|
|
97
|
-
): void {
|
|
98
|
-
for (const form of findForms(elements)) {
|
|
99
|
-
if (formHasLiveField(form)) {
|
|
100
|
-
form.withStateUrl(urlBuilder(form.getFormId()))
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Reorderable rows — stamp the POST-reorder URL on every `Table` that
|
|
107
|
-
* has `Table.reorderable()` set. The renderer reads `TableMeta.reorderUrl`
|
|
108
|
-
* to wire the drop handler; tables that aren't reorderable skip wiring
|
|
109
|
-
* entirely. Same shape as `tagFormStateUrls` so the call site stays
|
|
110
|
-
* consistent.
|
|
111
|
-
*/
|
|
112
|
-
export function tagTableReorderUrls(
|
|
113
|
-
elements: ReadonlyArray<Element>,
|
|
114
|
-
url: string,
|
|
115
|
-
): void {
|
|
116
|
-
for (const table of findTables(elements)) {
|
|
117
|
-
if (table.isReorderable() && !table.getReorderUrl()) {
|
|
118
|
-
table.withReorderUrl(url)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Marks every Table on the page deferred and stamps the URL the
|
|
124
|
-
// renderer will fetch from after mount. Must run BEFORE `loadTableRecords`
|
|
125
|
-
// so the records handler short-circuits.
|
|
126
|
-
export function tagTableDeferred(
|
|
127
|
-
elements: ReadonlyArray<Element>,
|
|
128
|
-
url: string,
|
|
129
|
-
): void {
|
|
130
|
-
for (const table of findTables(elements)) {
|
|
131
|
-
table.withDeferred(true)
|
|
132
|
-
table.withTableUrl(url)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Editable cell columns — walk every table on the page and stamp
|
|
138
|
-
* `_cellEditUrls[colName]` per row, but only on rows that already
|
|
139
|
-
* carry a `_cellEditable[colName]` marker (set by `loadTableRecords`
|
|
140
|
-
* after `R.canEdit(user, row)` passed). The dispatcher stays
|
|
141
|
-
* URL-shape-agnostic; URL building lives here parallel to
|
|
142
|
-
* `tagFormStateUrls / tagTableReorderUrls`.
|
|
143
|
-
*
|
|
144
|
-
* `idOf` extracts the per-row primary key. Defaults to reading `id` —
|
|
145
|
-
* works for the rudder ORM convention. Resources with a different
|
|
146
|
-
* primary-key column should pass an override (none in v1).
|
|
147
|
-
*/
|
|
148
|
-
export function tagCellEditUrls(
|
|
149
|
-
elements: ReadonlyArray<Element>,
|
|
150
|
-
resourceUrl: string,
|
|
151
|
-
idOf: (row: Record<string, unknown>) => unknown = row => row['id'],
|
|
152
|
-
): void {
|
|
153
|
-
for (const table of findTables(elements)) {
|
|
154
|
-
const rows = table.getRows() as ReadonlyArray<Record<string, unknown>> | undefined
|
|
155
|
-
if (!rows || rows.length === 0) continue
|
|
156
|
-
// Optimisation: skip the table when none of its columns are editable.
|
|
157
|
-
const editable = (table.getChildren() ?? []).some(c => c instanceof Column && c.isEditable())
|
|
158
|
-
if (!editable) continue
|
|
159
|
-
for (const row of rows) {
|
|
160
|
-
const editableMap = row['_cellEditable'] as Record<string, true> | undefined
|
|
161
|
-
if (!editableMap) continue
|
|
162
|
-
const id = idOf(row)
|
|
163
|
-
if (id === undefined || id === null || id === '') continue
|
|
164
|
-
const urls: Record<string, string> = {}
|
|
165
|
-
for (const colName of Object.keys(editableMap)) {
|
|
166
|
-
urls[colName] = `${resourceUrl}/${encodeURIComponent(String(id))}/_cell/${encodeURIComponent(colName)}`
|
|
167
|
-
}
|
|
168
|
-
;(row as Record<string, unknown>)['_cellEditUrls'] = urls
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Plan #8 — stamp the wizard step-validate endpoint URL on every form
|
|
175
|
-
* whose descendants include a `Wizard` element. `FormMeta.wizardUrl` is
|
|
176
|
-
* what the client posts to on Next-button clicks; forms without a wizard
|
|
177
|
-
* descendant skip wiring.
|
|
178
|
-
*/
|
|
179
|
-
export function tagFormWizardUrls(
|
|
180
|
-
elements: ReadonlyArray<Element>,
|
|
181
|
-
urlBuilder: (formId: string) => string,
|
|
182
|
-
): void {
|
|
183
|
-
for (const form of findForms(elements)) {
|
|
184
|
-
if (formHasWizard(form)) {
|
|
185
|
-
form.withWizardUrl(urlBuilder(form.getFormId()))
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Stamp `_agentRunBase` on every field element in the resolved
|
|
192
|
-
* `ElementMeta[]` tree that carries `aiActions`. Operates on the
|
|
193
|
-
* post-`resolveSchema` wire shape (plain objects) rather than on
|
|
194
|
-
* `Element` instances — `aiActions` is added by the `field-ai.ts`
|
|
195
|
-
* wrapper during `toMeta()`, so it isn't visible to pre-resolve walkers.
|
|
196
|
-
*
|
|
197
|
-
* Only called on edit pages where a `recordId` is known. Create pages
|
|
198
|
-
* deliberately skip it — field AI actions target existing content.
|
|
199
|
-
*/
|
|
200
|
-
export function tagFieldAiUrls(
|
|
201
|
-
elements: ReadonlyArray<Record<string, unknown>>,
|
|
202
|
-
agentBase: string,
|
|
203
|
-
): void {
|
|
204
|
-
for (const el of elements) {
|
|
205
|
-
if (Array.isArray(el['aiActions']) && (el['aiActions'] as unknown[]).length > 0) {
|
|
206
|
-
;(el as Record<string, unknown>)['_agentRunBase'] = agentBase
|
|
207
|
-
}
|
|
208
|
-
const children = el['children']
|
|
209
|
-
if (Array.isArray(children)) tagFieldAiUrls(children as Record<string, unknown>[], agentBase)
|
|
210
|
-
// Repeater rows
|
|
211
|
-
const rows = el['rows']
|
|
212
|
-
if (Array.isArray(rows)) {
|
|
213
|
-
for (const row of rows as Record<string, unknown>[]) {
|
|
214
|
-
const rowChildren = row['children']
|
|
215
|
-
if (Array.isArray(rowChildren)) tagFieldAiUrls(rowChildren as Record<string, unknown>[], agentBase)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Audit row 2026-05-07 cont'd⁸ — stamp the inline-create-option endpoint
|
|
223
|
-
* URL on every `SelectField` that has called `createOptionForm()`. Walks
|
|
224
|
-
* every form on the page so the URL carries the parent form's id; URL
|
|
225
|
-
* shape `${formScopeUrl}/_form/${formId}/create-option/${fieldName}` so
|
|
226
|
-
* the route handler can pick the form by id and the field by name.
|
|
227
|
-
*
|
|
228
|
-
* Mirrors `tagFormStateUrls / tagFormWizardUrls` — operates on the
|
|
229
|
-
* un-resolved Element tree, mutates field-instance state via
|
|
230
|
-
* `field.withCreateOptionUrl(url)`, and the field's `toMeta()` reads it
|
|
231
|
-
* back to emit `createOption.url`.
|
|
232
|
-
*
|
|
233
|
-
* Stops at Repeater / Builder boundaries (parallel to the form-state /
|
|
234
|
-
* wizard walkers): inside-row schemas are dispatched per-row and the
|
|
235
|
-
* createOption shape doesn't compose with row body coercion in v1.
|
|
236
|
-
*/
|
|
237
|
-
export function tagSelectCreateOptionUrls(
|
|
238
|
-
elements: ReadonlyArray<Element>,
|
|
239
|
-
urlBuilder: (formId: string, fieldName: string) => string,
|
|
240
|
-
): void {
|
|
241
|
-
for (const form of findForms(elements)) {
|
|
242
|
-
const formId = form.getFormId()
|
|
243
|
-
walkSelectFields(form.getChildren() as Element[] ?? [], (field) => {
|
|
244
|
-
if (field.hasCreateOption() && !field.getCreateOptionUrl()) {
|
|
245
|
-
field.withCreateOptionUrl(urlBuilder(formId, field.name))
|
|
246
|
-
}
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function walkSelectFields(elements: Element[], visit: (f: SelectField) => void): void {
|
|
252
|
-
for (const el of elements) {
|
|
253
|
-
if (el instanceof SelectField) {
|
|
254
|
-
visit(el)
|
|
255
|
-
// SelectField has no children of its own — no recursion needed.
|
|
256
|
-
continue
|
|
257
|
-
}
|
|
258
|
-
// Stop at row-array boundaries — see comment on `tagSelectCreateOptionUrls`.
|
|
259
|
-
if (el instanceof RepeaterField) continue
|
|
260
|
-
if (el instanceof BuilderField) continue
|
|
261
|
-
const children = el.getChildren()
|
|
262
|
-
if (children && children.length > 0) walkSelectFields(children as Element[], visit)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Adapter-package async-resolve walker. Stamps the per-form mentions URL
|
|
268
|
-
* on every field that ducks like a "rich text with at least one async
|
|
269
|
-
* mention provider". The duck-typed contract lives here (as opposed to
|
|
270
|
-
* importing from `@pilotiq/tiptap`) so pilotiq core stays adapter-free —
|
|
271
|
-
* any future field type with an async-resolve trigger can satisfy the
|
|
272
|
-
* same shape and pick up URL stamping for free.
|
|
273
|
-
*
|
|
274
|
-
* Contract:
|
|
275
|
-
* - `getType() === 'richtext'` (fast filter)
|
|
276
|
-
* - `hasAsyncMentions(): boolean`
|
|
277
|
-
* - `withMentionsUrl(url: string): unknown`
|
|
278
|
-
*
|
|
279
|
-
* Walks every form on the page so the URL builder can mint a per-form
|
|
280
|
-
* URL (mirrors `tagFormStateUrls / tagFormWizardUrls`). The route handler
|
|
281
|
-
* uses formId in the URL to select the form; the body carries `field`
|
|
282
|
-
* + `trigger` + `query`. One URL per (form, scope), reused across every
|
|
283
|
-
* async-mention field on that form.
|
|
284
|
-
*/
|
|
285
|
-
interface AsyncMentionFieldLike {
|
|
286
|
-
hasAsyncMentions(): boolean
|
|
287
|
-
withMentionsUrl(url: string): unknown
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function isAsyncMentionField(el: Element): el is Element & AsyncMentionFieldLike {
|
|
291
|
-
if (el.getType() !== 'richtext') return false
|
|
292
|
-
const candidate = el as unknown as Partial<AsyncMentionFieldLike>
|
|
293
|
-
return typeof candidate.hasAsyncMentions === 'function'
|
|
294
|
-
&& typeof candidate.withMentionsUrl === 'function'
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function tagRichTextMentionUrls(
|
|
298
|
-
elements: ReadonlyArray<Element>,
|
|
299
|
-
urlBuilder: (formId: string) => string,
|
|
300
|
-
): void {
|
|
301
|
-
for (const form of findForms(elements)) {
|
|
302
|
-
const url = urlBuilder(form.getFormId())
|
|
303
|
-
const visit = (els: ReadonlyArray<Element>): void => {
|
|
304
|
-
for (const el of els) {
|
|
305
|
-
// Don't cross into nested forms — each form gets its own URL.
|
|
306
|
-
if (el !== form && el.getType() === 'form') continue
|
|
307
|
-
if (isAsyncMentionField(el) && el.hasAsyncMentions()) {
|
|
308
|
-
el.withMentionsUrl(url)
|
|
309
|
-
}
|
|
310
|
-
// Builder.getChildren() returns undefined to keep the field-level
|
|
311
|
-
// walkers from treating heterogeneous rows as flat children. Manual
|
|
312
|
-
// descent into each block's schema covers the URL-stamping path
|
|
313
|
-
// without changing the no-cross posture for save/coerce.
|
|
314
|
-
if (isBuilderField(el)) {
|
|
315
|
-
for (const block of (el as BuilderField).getBlocks()) visit(block.getSchema())
|
|
316
|
-
continue
|
|
317
|
-
}
|
|
318
|
-
const children = el.getChildren()
|
|
319
|
-
if (children) visit(children)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
const children = form.getChildren()
|
|
323
|
-
if (children) visit(children)
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
export function formHasLiveField(form: Form): boolean {
|
|
328
|
-
let found = false
|
|
329
|
-
const visit = (els: ReadonlyArray<Element>): void => {
|
|
330
|
-
for (const el of els) {
|
|
331
|
-
if (found) return
|
|
332
|
-
// Either a server-side `live()` (drives a roundtrip) OR a
|
|
333
|
-
// client-side `afterStateUpdatedJs(body)` (JS-only) is enough to
|
|
334
|
-
// mount the controlled-form path: the FormStateProvider holds the
|
|
335
|
-
// values map either path needs, and the client gates the actual
|
|
336
|
-
// network POST on `live` separately. Cost of the over-stamp for
|
|
337
|
-
// JS-only forms is one unused endpoint URL per form — endpoint
|
|
338
|
-
// never gets hit because the client only POSTs on `live`.
|
|
339
|
-
if (el instanceof Field && (el.isLive() || el.getAfterStateUpdatedJs() !== undefined)) {
|
|
340
|
-
found = true
|
|
341
|
-
return
|
|
342
|
-
}
|
|
343
|
-
const children = el.getChildren()
|
|
344
|
-
if (children) visit(children)
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
const children = form.getChildren()
|
|
348
|
-
if (children) visit(children)
|
|
349
|
-
return found
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function formHasWizard(form: Form): boolean {
|
|
353
|
-
let found = false
|
|
354
|
-
const visit = (els: ReadonlyArray<Element>): void => {
|
|
355
|
-
for (const el of els) {
|
|
356
|
-
if (found) return
|
|
357
|
-
if (el.getType() === 'wizard') { found = true; return }
|
|
358
|
-
const children = el.getChildren()
|
|
359
|
-
if (children) visit(children)
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
const children = form.getChildren()
|
|
363
|
-
if (children) visit(children)
|
|
364
|
-
return found
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Run the edit-mode fill pipeline on a loaded record:
|
|
369
|
-
* mutateFormDataBeforeFill → fillFromRecord → mutateFormDataAfterFill
|
|
370
|
-
*
|
|
371
|
-
* `fillFromRecord` defaults to `{ ...record }` when not configured. Both
|
|
372
|
-
* mutators are optional and may be async. `ctx.record` is the loaded
|
|
373
|
-
* record so mutators can read from fields the form doesn't surface.
|
|
374
|
-
*/
|
|
375
|
-
export async function applyFillPipeline<R>(
|
|
376
|
-
form: Form<R>,
|
|
377
|
-
record: R,
|
|
378
|
-
): Promise<Record<string, unknown>> {
|
|
379
|
-
const recordObj = record as unknown as Record<string, unknown>
|
|
380
|
-
let values: Record<string, unknown> = { ...recordObj }
|
|
381
|
-
|
|
382
|
-
const before = form.getMutateFormDataBeforeFill()
|
|
383
|
-
if (before) values = await before(values, { values, record })
|
|
384
|
-
|
|
385
|
-
const fill = form.getFillFromRecord()
|
|
386
|
-
if (fill) values = fill(record)
|
|
387
|
-
|
|
388
|
-
const after = form.getMutateFormDataAfterFill()
|
|
389
|
-
if (after) values = await after(values, { values, record })
|
|
390
|
-
|
|
391
|
-
return normalizeArrayFieldStrings(form, values)
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Walk the form for Repeater / Builder field names whose value on `values`
|
|
396
|
-
* is a JSON string (the shape a `String?` column produces when records are
|
|
397
|
-
* seeded outside the pilotiq save path — raw SQL, migrations, imports).
|
|
398
|
-
* Pilotiq's save path stringifies these arrays via `coerceFormValues`, but
|
|
399
|
-
* the load path doesn't auto-parse — so a fresh `String?` column lands here
|
|
400
|
-
* as a string, then `resolveRepeaterRows` sees `Array.isArray(string) === false`
|
|
401
|
-
* and falls through to empty rows on first paint. The collab path is
|
|
402
|
-
* symmetric: the string lands in the form-data Y.Map, `migrateLegacyArrays`
|
|
403
|
-
* sees `Array.isArray(string) === false`, and silently skips migration.
|
|
404
|
-
*
|
|
405
|
-
* Parse defensively — anything that doesn't deserialize to an array is left
|
|
406
|
-
* verbatim so a stray non-JSON string doesn't get clobbered. Non-string
|
|
407
|
-
* values pass through untouched (already-array, null, undefined, number).
|
|
408
|
-
*/
|
|
409
|
-
export function normalizeArrayFieldStrings<R>(
|
|
410
|
-
form: Form<R>,
|
|
411
|
-
values: Record<string, unknown>,
|
|
412
|
-
): Record<string, unknown> {
|
|
413
|
-
const arrayFieldNames = collectArrayFieldNames(form.getChildren() ?? [])
|
|
414
|
-
if (arrayFieldNames.length === 0) return values
|
|
415
|
-
let out: Record<string, unknown> | null = null
|
|
416
|
-
for (const name of arrayFieldNames) {
|
|
417
|
-
const raw = values[name]
|
|
418
|
-
if (typeof raw !== 'string') continue
|
|
419
|
-
let parsed: unknown
|
|
420
|
-
try { parsed = JSON.parse(raw) } catch { continue }
|
|
421
|
-
if (!Array.isArray(parsed)) continue
|
|
422
|
-
if (!out) out = { ...values }
|
|
423
|
-
out[name] = parsed
|
|
424
|
-
}
|
|
425
|
-
return out ?? values
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/** Walk the form's children for top-level Repeater + Builder field names.
|
|
429
|
-
* Stops at array-row boundaries — nested Repeater/Builder fields live
|
|
430
|
-
* inside their parent's inner schema and aren't top-level form fields. */
|
|
431
|
-
function collectArrayFieldNames(elements: ReadonlyArray<Element>): string[] {
|
|
432
|
-
const out: string[] = []
|
|
433
|
-
const walk = (els: ReadonlyArray<Element>): void => {
|
|
434
|
-
for (const el of els) {
|
|
435
|
-
if (isRepeaterField(el) || isBuilderField(el)) {
|
|
436
|
-
const name = (el as RepeaterField | BuilderField).name
|
|
437
|
-
if (name) out.push(name)
|
|
438
|
-
continue
|
|
439
|
-
}
|
|
440
|
-
const children = el.getChildren()
|
|
441
|
-
if (children && children.length > 0) walk(children)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
walk(elements)
|
|
445
|
-
return out
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Walk every hydrator registered via `Pilotiq.editPageHydrator(fn)` and
|
|
450
|
-
* merge non-null returns onto `currentValues`. Hydrators run sequentially
|
|
451
|
-
* in registration order; later returns override keys from earlier ones.
|
|
452
|
-
*
|
|
453
|
-
* Failure mode is permissive: a hydrator that throws or returns `null`
|
|
454
|
-
* contributes nothing; the page still renders against the fill-pipeline
|
|
455
|
-
* values it received. Errors emit a `console.warn` so the page isn't
|
|
456
|
-
* silently relying on missing data.
|
|
457
|
-
*
|
|
458
|
-
* Returns the merged overlay (NOT the final values). The caller composes
|
|
459
|
-
* the overlay onto the form via `form.withValues({ ...current, ...overlay })`
|
|
460
|
-
* — keeps the merge order explicit at the call site (overlay wins).
|
|
461
|
-
*
|
|
462
|
-
* Empty hydrators array / no overrides → returns `{}` so callers can
|
|
463
|
-
* skip the `withValues` call cheaply.
|
|
464
|
-
*/
|
|
465
|
-
export async function applyEditPageHydrators(
|
|
466
|
-
hydrators: ReadonlyArray<EditPageHydrator>,
|
|
467
|
-
ctx: EditPageHydratorContext,
|
|
468
|
-
): Promise<Record<string, unknown>> {
|
|
469
|
-
if (hydrators.length === 0) return {}
|
|
470
|
-
let overlay: Record<string, unknown> = {}
|
|
471
|
-
for (const fn of hydrators) {
|
|
472
|
-
try {
|
|
473
|
-
const result = await fn(ctx)
|
|
474
|
-
if (result && typeof result === 'object') overlay = { ...overlay, ...result }
|
|
475
|
-
} catch (err) {
|
|
476
|
-
console.warn('[pilotiq] editPageHydrator threw — falling back to fill-pipeline values', err)
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return overlay
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Walk the form's top-level Repeaters and replace `values[fieldName]`
|
|
484
|
-
* with rows fetched from `parent.related(name)` for any
|
|
485
|
-
* relationship-backed Repeater. Each loaded row stamps `__id` to the
|
|
486
|
-
* child's primary key so the renderer can round-trip identity through
|
|
487
|
-
* a hidden input and the save-side diff can match submitted rows back
|
|
488
|
-
* to existing records.
|
|
489
|
-
*
|
|
490
|
-
* No-op when the parent record is null (create mode), when no
|
|
491
|
-
* relationship-backed Repeaters exist on the form, or when the
|
|
492
|
-
* resource has no `R.model` (relation queries need it).
|
|
493
|
-
*
|
|
494
|
-
* Mutates and returns a fresh values object — never the input.
|
|
495
|
-
*/
|
|
496
|
-
export async function applyRelationshipRepeaterFill(
|
|
497
|
-
form: Form,
|
|
498
|
-
values: Record<string, unknown>,
|
|
499
|
-
record: unknown,
|
|
500
|
-
parentModel: ModelLike | undefined,
|
|
501
|
-
): Promise<Record<string, unknown>> {
|
|
502
|
-
if (record == null) return values
|
|
503
|
-
if (!parentModel) return values
|
|
504
|
-
const repeaters = findRelationshipRepeaters(form.getChildren() ?? [])
|
|
505
|
-
if (repeaters.length === 0) return values
|
|
506
|
-
|
|
507
|
-
const out: Record<string, unknown> = { ...values }
|
|
508
|
-
for (const repeater of repeaters) {
|
|
509
|
-
const cfg = repeater.getRelationship()!
|
|
510
|
-
const pivotColumns = cfg.pivotColumns
|
|
511
|
-
let rows: unknown[]
|
|
512
|
-
try {
|
|
513
|
-
rows = await loadRelationRows(parentModel, record, cfg.name, pivotColumns)
|
|
514
|
-
} catch {
|
|
515
|
-
// Failed lookup (e.g. missing `relations` map on a test stub)
|
|
516
|
-
// — fall back to whatever value applyFillPipeline produced
|
|
517
|
-
// rather than wiping the field. Better to render stale data
|
|
518
|
-
// than to silently empty the row list.
|
|
519
|
-
continue
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// The child model is opaque here — we don't have the full
|
|
523
|
-
// descriptor at this seam, so use the configured override or
|
|
524
|
-
// peek the parent's relations map for the FK column. Strip it
|
|
525
|
-
// (and the PK) from each row's payload so the inner schema
|
|
526
|
-
// doesn't surface them as form values. For morphMany the
|
|
527
|
-
// attachment is two columns instead of one — strip both.
|
|
528
|
-
const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id'
|
|
529
|
-
const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name)
|
|
530
|
-
const morph = getMorphRelationDescriptor(parentModel, cfg.name)
|
|
531
|
-
const morphIdCol = morph ? `${morph.morphName}Id` : undefined
|
|
532
|
-
const morphTyCol = morph ? `${morph.morphName}Type` : undefined
|
|
533
|
-
|
|
534
|
-
out[repeater.name] = rows.map(row => {
|
|
535
|
-
const r = (row && typeof row === 'object') ? { ...(row as Record<string, unknown>) } : {}
|
|
536
|
-
const pkValue = r[pkColumn]
|
|
537
|
-
delete r[pkColumn]
|
|
538
|
-
if (fkColumn) delete r[fkColumn]
|
|
539
|
-
if (morphIdCol) delete r[morphIdCol]
|
|
540
|
-
if (morphTyCol) delete r[morphTyCol]
|
|
541
|
-
// M2M pivot extras — flatten `row.pivot[col]` onto the row's data
|
|
542
|
-
// so each pivot column round-trips through the inner schema as a
|
|
543
|
-
// regular form field. The pivot envelope itself is dropped from
|
|
544
|
-
// the values shape — the persist side splits pivot vs child
|
|
545
|
-
// columns by name lookup against `cfg.pivotColumns`.
|
|
546
|
-
const pivotEnvelope = r['pivot']
|
|
547
|
-
delete r['pivot']
|
|
548
|
-
const stamped: Record<string, unknown> = { ...r }
|
|
549
|
-
if (pivotColumns && pivotColumns.length > 0
|
|
550
|
-
&& pivotEnvelope && typeof pivotEnvelope === 'object'
|
|
551
|
-
) {
|
|
552
|
-
const pe = pivotEnvelope as Record<string, unknown>
|
|
553
|
-
for (const col of pivotColumns) {
|
|
554
|
-
if (col in pe) stamped[col] = pe[col]
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (pkValue !== undefined && pkValue !== null) {
|
|
558
|
-
stamped['__id'] = String(pkValue)
|
|
559
|
-
}
|
|
560
|
-
return stamped
|
|
561
|
-
})
|
|
562
|
-
}
|
|
563
|
-
return out
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/** Walk the form's children for top-level relationship-backed Repeaters. */
|
|
567
|
-
export function findRelationshipRepeaters(elements: ReadonlyArray<Element>): RepeaterField[] {
|
|
568
|
-
const out: RepeaterField[] = []
|
|
569
|
-
const walk = (els: ReadonlyArray<Element>): void => {
|
|
570
|
-
for (const el of els) {
|
|
571
|
-
if (isRepeaterField(el)) {
|
|
572
|
-
const r = el as RepeaterField
|
|
573
|
-
if (r.getRelationship()) out.push(r)
|
|
574
|
-
// Don't dive into Repeater children — relationship-on-relationship
|
|
575
|
-
// isn't supported in v1.
|
|
576
|
-
continue
|
|
577
|
-
}
|
|
578
|
-
// Don't dive into Builder children either — relationship-backed
|
|
579
|
-
// Builders are resolved separately by `findRelationshipBuilders`.
|
|
580
|
-
if (isBuilderField(el)) continue
|
|
581
|
-
const children = el.getChildren()
|
|
582
|
-
if (children && children.length > 0) walk(children)
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
walk(elements)
|
|
586
|
-
return out
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Walk the form's top-level Builders and replace `values[fieldName]` with
|
|
591
|
-
* rows fetched from `parent.related(name)` for any relationship-backed
|
|
592
|
-
* Builder. Each loaded row stamps `__id` (child PK) + `type` (block
|
|
593
|
-
* discriminator) + `data` (per-block JSON payload) so the renderer can
|
|
594
|
-
* round-trip the heterogeneous envelope.
|
|
595
|
-
*
|
|
596
|
-
* Mirrors `applyRelationshipRepeaterFill`. No-op when the parent record
|
|
597
|
-
* is null (create mode), the resource has no `R.model`, or no
|
|
598
|
-
* relationship-backed Builders exist on the form.
|
|
599
|
-
*/
|
|
600
|
-
export async function applyRelationshipBuilderFill(
|
|
601
|
-
form: Form,
|
|
602
|
-
values: Record<string, unknown>,
|
|
603
|
-
record: unknown,
|
|
604
|
-
parentModel: ModelLike | undefined,
|
|
605
|
-
): Promise<Record<string, unknown>> {
|
|
606
|
-
if (record == null) return values
|
|
607
|
-
if (!parentModel) return values
|
|
608
|
-
const builders = findRelationshipBuilders(form.getChildren() ?? [])
|
|
609
|
-
if (builders.length === 0) return values
|
|
610
|
-
|
|
611
|
-
const out: Record<string, unknown> = { ...values }
|
|
612
|
-
for (const builder of builders) {
|
|
613
|
-
const cfg = builder.getRelationship()!
|
|
614
|
-
let rows: unknown[]
|
|
615
|
-
try {
|
|
616
|
-
rows = await loadRelationRows(parentModel, record, cfg.name)
|
|
617
|
-
} catch {
|
|
618
|
-
// Failed lookup (e.g. missing `relations` map on a test stub) —
|
|
619
|
-
// fall back to whatever value applyFillPipeline produced rather
|
|
620
|
-
// than wiping the field. Better stale than silently empty.
|
|
621
|
-
continue
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const pkColumn = pickChildPrimaryKey(parentModel, cfg.name) ?? 'id'
|
|
625
|
-
const fkColumn = cfg.foreignKey ?? pickChildForeignKey(parentModel, cfg.name)
|
|
626
|
-
const typeColumn = cfg.typeColumn ?? 'type'
|
|
627
|
-
const dataColumn = cfg.dataColumn ?? 'data'
|
|
628
|
-
|
|
629
|
-
out[builder.name] = rows.map(row => {
|
|
630
|
-
const r = (row && typeof row === 'object') ? { ...(row as Record<string, unknown>) } : {}
|
|
631
|
-
const pkValue = r[pkColumn]
|
|
632
|
-
const blockType = typeof r[typeColumn] === 'string' ? (r[typeColumn] as string) : ''
|
|
633
|
-
const dataRaw = r[dataColumn]
|
|
634
|
-
const blockData = parseBuilderDataPayload(dataRaw)
|
|
635
|
-
|
|
636
|
-
const stamped: Record<string, unknown> = {
|
|
637
|
-
type: blockType,
|
|
638
|
-
data: blockData,
|
|
639
|
-
}
|
|
640
|
-
if (pkValue !== undefined && pkValue !== null) {
|
|
641
|
-
stamped['__id'] = String(pkValue)
|
|
642
|
-
}
|
|
643
|
-
// Non-`type` / `data` / FK / PK columns aren't surfaced — the
|
|
644
|
-
// JSON envelope is the source of truth for per-block fields. If
|
|
645
|
-
// a user denormalizes a column, they handle it via per-block
|
|
646
|
-
// mutate hooks, not by leaking the column into row values.
|
|
647
|
-
void fkColumn
|
|
648
|
-
return stamped
|
|
649
|
-
})
|
|
650
|
-
}
|
|
651
|
-
return out
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Normalize the JSON payload column into a plain object. Prisma
|
|
656
|
-
* hydrates `Json` columns to objects; some adapters return strings.
|
|
657
|
-
* Anything that isn't a parseable object falls back to `{}` so the
|
|
658
|
-
* inner schema renders fresh defaults.
|
|
659
|
-
*/
|
|
660
|
-
export function parseBuilderDataPayload(raw: unknown): Record<string, unknown> {
|
|
661
|
-
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
662
|
-
return raw as Record<string, unknown>
|
|
663
|
-
}
|
|
664
|
-
if (typeof raw === 'string') {
|
|
665
|
-
try {
|
|
666
|
-
const parsed: unknown = JSON.parse(raw)
|
|
667
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
668
|
-
return parsed as Record<string, unknown>
|
|
669
|
-
}
|
|
670
|
-
} catch {
|
|
671
|
-
// fall through to {}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
return {}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/** Walk the form's children for top-level relationship-backed Builders. */
|
|
678
|
-
export function findRelationshipBuilders(elements: ReadonlyArray<Element>): BuilderField[] {
|
|
679
|
-
const out: BuilderField[] = []
|
|
680
|
-
const walk = (els: ReadonlyArray<Element>): void => {
|
|
681
|
-
for (const el of els) {
|
|
682
|
-
if (isBuilderField(el)) {
|
|
683
|
-
const b = el as BuilderField
|
|
684
|
-
if (b.getRelationship()) out.push(b)
|
|
685
|
-
continue
|
|
686
|
-
}
|
|
687
|
-
// Don't dive into Repeater children either — both array-row
|
|
688
|
-
// boundaries are walker stops here.
|
|
689
|
-
if (isRepeaterField(el)) continue
|
|
690
|
-
const children = el.getChildren()
|
|
691
|
-
if (children && children.length > 0) walk(children)
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
walk(elements)
|
|
695
|
-
return out
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
/** Read the child model's PK column from the parent's relations map, when present. */
|
|
699
|
-
export function pickChildPrimaryKey(parentModel: ModelLike, name: string): string | undefined {
|
|
700
|
-
const relations = (parentModel as unknown as Record<string, unknown>)['relations']
|
|
701
|
-
if (!relations || typeof relations !== 'object') return undefined
|
|
702
|
-
const entry = (relations as Record<string, unknown>)[name]
|
|
703
|
-
if (!entry || typeof entry !== 'object') return undefined
|
|
704
|
-
const e = entry as Record<string, unknown>
|
|
705
|
-
if (typeof e['model'] !== 'function') return undefined
|
|
706
|
-
try {
|
|
707
|
-
const child = (e['model'] as () => ModelLike)()
|
|
708
|
-
return getPrimaryKey(child)
|
|
709
|
-
} catch {
|
|
710
|
-
return undefined
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/** Read the FK column from the parent's relations map, when present. */
|
|
715
|
-
export function pickChildForeignKey(parentModel: ModelLike, name: string): string | undefined {
|
|
716
|
-
const relations = (parentModel as unknown as Record<string, unknown>)['relations']
|
|
717
|
-
if (!relations || typeof relations !== 'object') return undefined
|
|
718
|
-
const entry = (relations as Record<string, unknown>)[name]
|
|
719
|
-
if (!entry || typeof entry !== 'object') return undefined
|
|
720
|
-
const e = entry as Record<string, unknown>
|
|
721
|
-
return typeof e['foreignKey'] === 'string' ? (e['foreignKey'] as string) : undefined
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// ─── Plan #15 server-data widgets ─────────────────────────────
|
|
725
|
-
|
|
726
|
-
/** Wire-shape of the per-widget data map shipped to the client.
|
|
727
|
-
* Lazy elements stamp `null` (renderer paints skeleton + fetches);
|
|
728
|
-
* eager elements stamp their resolved payload. Errors stamp
|
|
729
|
-
* `{ error: '<message>' }` so the renderer can surface a per-widget
|
|
730
|
-
* failure without blanking the page. */
|
|
731
|
-
export type ServerDataMap = Record<string, unknown>
|
|
732
|
-
|
|
733
|
-
/**
|
|
734
|
-
* Plan #15 — collect every `ServerDataElement` in the schema tree and
|
|
735
|
-
* resolve their `getServerData(ctx)` payloads in parallel. Returns a
|
|
736
|
-
* map keyed by element id, ready to ship as `viewProps._widgetData`.
|
|
737
|
-
*
|
|
738
|
-
* Lazy elements (default — `lazy(false)` opts out) skip the hook and
|
|
739
|
-
* stamp `null` so the renderer paints a skeleton and fetches the
|
|
740
|
-
* payload via `POST {base}/_widget/:id` on mount. Eager elements
|
|
741
|
-
* resolve synchronously and ship the data with the page.
|
|
742
|
-
*
|
|
743
|
-
* Per-widget errors are caught and surfaced as `{ error: '...' }` —
|
|
744
|
-
* one flaky `getStats()` shouldn't 500 the entire dashboard.
|
|
745
|
-
*
|
|
746
|
-
* Visibility is **not** re-evaluated here. The schema resolver
|
|
747
|
-
* (`resolveSchema → evaluateVisibility`) drops hidden layout elements
|
|
748
|
-
* before any widget code runs. Widgets inside still-rendered branches
|
|
749
|
-
* always resolve (or stamp lazy null).
|
|
750
|
-
*/
|
|
751
|
-
export async function resolveServerDataElements(
|
|
752
|
-
elements: ReadonlyArray<Element>,
|
|
753
|
-
ctx: RenderContext,
|
|
754
|
-
): Promise<ServerDataMap> {
|
|
755
|
-
const widgets = collectServerDataElements(elements)
|
|
756
|
-
if (widgets.length === 0) return {}
|
|
757
|
-
const out: ServerDataMap = {}
|
|
758
|
-
await Promise.all(widgets.map(async (el) => {
|
|
759
|
-
const id = el.getId()
|
|
760
|
-
if (el.isLazy()) {
|
|
761
|
-
out[id] = null // sentinel — renderer paints skeleton, fetches on mount
|
|
762
|
-
return
|
|
763
|
-
}
|
|
764
|
-
try {
|
|
765
|
-
out[id] = await el.resolveServerData(ctx)
|
|
766
|
-
} catch (err) {
|
|
767
|
-
out[id] = { error: err instanceof Error ? err.message : 'Widget failed to load' }
|
|
768
|
-
}
|
|
769
|
-
}))
|
|
770
|
-
return out
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/** Walk the tree collecting every `ServerDataElement`. Walks into
|
|
774
|
-
* containers but stops at Form/Repeater/Builder boundaries — widgets
|
|
775
|
-
* inside an editable form don't make sense in v1. */
|
|
776
|
-
export function collectServerDataElements(elements: ReadonlyArray<Element>): ServerDataElement[] {
|
|
777
|
-
const out: ServerDataElement[] = []
|
|
778
|
-
const walk = (els: ReadonlyArray<Element>): void => {
|
|
779
|
-
for (const el of els) {
|
|
780
|
-
if (isServerDataElement(el)) {
|
|
781
|
-
out.push(el)
|
|
782
|
-
// Don't recurse into a widget's children — `View` etc. are leaves
|
|
783
|
-
// for v1 (no nested widgets inside widgets).
|
|
784
|
-
continue
|
|
785
|
-
}
|
|
786
|
-
// Skip walkers that imply per-row resolution — widgets inside
|
|
787
|
-
// Repeater/Builder rows don't have a stable id space.
|
|
788
|
-
const type = el.getType()
|
|
789
|
-
if (type === 'form' || type === 'repeater' || type === 'builder' || type === 'table' || type === 'tableWidget') continue
|
|
790
|
-
const children = el.getChildren()
|
|
791
|
-
if (children) walk(children)
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
walk(elements)
|
|
795
|
-
return out
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* Plan #15 — stamp the polling-endpoint URL on every `ServerDataElement`
|
|
800
|
-
* in the tree. Mirrors `tagFormStateUrls / tagTableReorderUrls`. Walks
|
|
801
|
-
* with the same boundaries as `collectServerDataElements` so the wire
|
|
802
|
-
* stays in sync (no orphan widgets without URLs and vice versa).
|
|
803
|
-
*
|
|
804
|
-
* `urlBuilder(id)` typically produces `${base}/_widget/${id}` for
|
|
805
|
-
* dashboard widgets and `${base}/${pageSlug}/_widget/${id}` for
|
|
806
|
-
* custom-page widgets — the route handlers for both shapes are wired up
|
|
807
|
-
* in `routes.ts` (see Phase A.4).
|
|
808
|
-
*/
|
|
809
|
-
export function tagWidgetUrls(
|
|
810
|
-
elements: ReadonlyArray<Element>,
|
|
811
|
-
urlBuilder: (id: string) => string,
|
|
812
|
-
): void {
|
|
813
|
-
for (const widget of collectServerDataElements(elements)) {
|
|
814
|
-
if (widget.getWidgetUrl()) continue // user-set wins
|
|
815
|
-
widget.withWidgetUrl(urlBuilder(widget.getId()))
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* Stamp every form-subresource URL the page might need in one pass:
|
|
821
|
-
* - `_form/${formId}/state` (Plan #5 partial-resolve)
|
|
822
|
-
* - `_form/${formId}/wizard` (Plan #8 step-validate)
|
|
823
|
-
* - `_form/${formId}/mentions` (async-mention typeahead)
|
|
824
|
-
* - `_form/${formId}/create-option/${fieldName}` (SelectField inline-create)
|
|
825
|
-
*
|
|
826
|
-
* Every page-builder that mounts a form (dashboard / resource create+edit /
|
|
827
|
-
* global edit / custom page) needs all four — the underlying taggers skip
|
|
828
|
-
* forms that don't carry the matching feature, so this is always safe to
|
|
829
|
-
* call. `base` is the route prefix (e.g. `${cfg.path}` / `${resourceBase}` /
|
|
830
|
-
* `${resourceBase}/${recordId}` / `${pageUrl}`).
|
|
831
|
-
*/
|
|
832
|
-
export function tagFormSubresourceUrls(elements: ReadonlyArray<Element>, base: string): void {
|
|
833
|
-
tagFormStateUrls(elements, formId => `${base}/_form/${formId}/state`)
|
|
834
|
-
tagFormWizardUrls(elements, formId => `${base}/_form/${formId}/wizard`)
|
|
835
|
-
tagRichTextMentionUrls(elements, formId => `${base}/_form/${formId}/mentions`)
|
|
836
|
-
tagSelectCreateOptionUrls(elements, (formId, fieldName) => `${base}/_form/${formId}/create-option/${fieldName}`)
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/** Stamp dispatchUrl on every handler-style Action so the client knows where to POST. */
|
|
840
|
-
export function tagActionDispatch(elements: ReadonlyArray<Element>, baseUrl: string): void {
|
|
841
|
-
for (const action of findActions(elements)) {
|
|
842
|
-
if (!action.getHandler()) continue
|
|
843
|
-
if (action.getHref() || action.getMethod()) continue
|
|
844
|
-
if (action.getDispatchUrl()) continue
|
|
845
|
-
action.dispatchUrl(`${baseUrl}/_action/${action.name}`)
|
|
846
|
-
}
|
|
847
|
-
// Row-scoped extraItemActions (Repeater/Builder). Stamped here too so
|
|
848
|
-
// the client can POST to the same `_action/:name` route — the renderer
|
|
849
|
-
// attaches `_rowPath=<fieldName>.<index>` per click; the server's
|
|
850
|
-
// dispatcher uses that to walk into the right row when building
|
|
851
|
-
// `ctx.row`. See `findRowExtraActions` in `dispatchAction.ts`.
|
|
852
|
-
for (const { action } of findRowExtraActions(elements)) {
|
|
853
|
-
if (!action.getHandler()) continue
|
|
854
|
-
if (action.getDispatchUrl()) continue
|
|
855
|
-
action.dispatchUrl(`${baseUrl}/_action/${action.name}`)
|
|
856
|
-
}
|
|
857
|
-
}
|