@pilotiq/pilotiq 0.24.1 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/boost/guidelines.md +566 -0
- package/boost/skills/pilotiq-fields/SKILL.md +47 -0
- package/boost/skills/pilotiq-fields/rules/field-catalog.md +288 -0
- package/boost/skills/pilotiq-fields/rules/reactive-fields.md +199 -0
- package/boost/skills/pilotiq-fields/rules/validation.md +198 -0
- package/boost/skills/pilotiq-relations/SKILL.md +47 -0
- package/boost/skills/pilotiq-relations/rules/relation-managers.md +256 -0
- package/boost/skills/pilotiq-relations/rules/repeater-relationship.md +177 -0
- package/boost/skills/pilotiq-resource/SKILL.md +61 -0
- package/boost/skills/pilotiq-resource/rules/authorization.md +242 -0
- package/boost/skills/pilotiq-resource/rules/defining-resources.md +228 -0
- package/boost/skills/pilotiq-resource/rules/page-overrides.md +296 -0
- package/package.json +6 -1
- package/.turbo/turbo-build.log +0 -8
- package/CLAUDE.md +0 -265
- package/src/Cluster.test.ts +0 -283
- package/src/Cluster.ts +0 -83
- package/src/Column.test.ts +0 -199
- package/src/Column.ts +0 -710
- package/src/Global.test.ts +0 -367
- package/src/Global.ts +0 -169
- package/src/Page.test.ts +0 -114
- package/src/Page.ts +0 -208
- package/src/Pilotiq.perf.test.ts +0 -252
- package/src/Pilotiq.test.ts +0 -129
- package/src/Pilotiq.ts +0 -1158
- package/src/PilotiqRegistry.ts +0 -36
- package/src/PilotiqServiceProvider.ts +0 -121
- package/src/RelationManager.test.ts +0 -400
- package/src/RelationManager.ts +0 -527
- package/src/RenderHook.test.ts +0 -252
- package/src/RenderHook.ts +0 -242
- package/src/Resource.test.ts +0 -284
- package/src/Resource.ts +0 -526
- package/src/RightPanel.test.ts +0 -202
- package/src/RightPanel.ts +0 -132
- package/src/Tab.test.ts +0 -91
- package/src/Tab.ts +0 -156
- package/src/UserMenuItem.ts +0 -145
- package/src/actions/Action.test.ts +0 -2526
- package/src/actions/Action.ts +0 -1515
- package/src/actions/ActionGroup.test.ts +0 -112
- package/src/actions/ActionGroup.ts +0 -173
- package/src/actions/attachFactory.ts +0 -172
- package/src/actions/bulkFactories.ts +0 -168
- package/src/actions/crudFactories.ts +0 -220
- package/src/actions/exportFactory.ts +0 -225
- package/src/actions/factoryHelpers.ts +0 -177
- package/src/actions/importFactory.ts +0 -243
- package/src/actions/index.ts +0 -17
- package/src/actions/m2mFactories.ts +0 -193
- package/src/actions/relationFactories.ts +0 -372
- package/src/applyPageHooks.test.ts +0 -463
- package/src/applyPageHooks.ts +0 -330
- package/src/authorization.test.ts +0 -483
- package/src/breadcrumbs.test.ts +0 -238
- package/src/cells/coerce.test.ts +0 -85
- package/src/cells/coerce.ts +0 -84
- package/src/clusterPaths.ts +0 -35
- package/src/columns/BadgeColumn.test.ts +0 -54
- package/src/columns/BadgeColumn.ts +0 -32
- package/src/columns/BooleanColumn.test.ts +0 -41
- package/src/columns/BooleanColumn.ts +0 -18
- package/src/columns/ColorColumn.test.ts +0 -37
- package/src/columns/ColorColumn.ts +0 -38
- package/src/columns/IconColumn.test.ts +0 -54
- package/src/columns/IconColumn.ts +0 -37
- package/src/columns/ImageColumn.test.ts +0 -41
- package/src/columns/ImageColumn.ts +0 -28
- package/src/columns/SelectColumn.ts +0 -98
- package/src/columns/TextColumn.test.ts +0 -190
- package/src/columns/TextColumn.ts +0 -20
- package/src/columns/TextInputColumn.ts +0 -68
- package/src/columns/ToggleColumn.ts +0 -46
- package/src/columns/editableColumns.test.ts +0 -238
- package/src/columns/index.ts +0 -9
- package/src/defaultGlobalPages.ts +0 -95
- package/src/defaultPages.test.ts +0 -634
- package/src/defaultPages.ts +0 -617
- package/src/defaultViewPage.test.ts +0 -147
- package/src/elements/Form.test.ts +0 -223
- package/src/elements/Form.ts +0 -416
- package/src/elements/ListTabs.ts +0 -28
- package/src/elements/Table.test.ts +0 -422
- package/src/elements/Table.ts +0 -850
- package/src/elements/TableGroup.test.ts +0 -260
- package/src/elements/TableGroup.ts +0 -334
- package/src/elements/dispatchAction.test.ts +0 -463
- package/src/elements/dispatchAction.ts +0 -355
- package/src/elements/dispatchForm.test.ts +0 -477
- package/src/elements/dispatchForm.ts +0 -1993
- package/src/elements/dispatchTable.test.ts +0 -1514
- package/src/elements/dispatchTable.ts +0 -745
- package/src/elements/index.ts +0 -21
- package/src/entries/BadgeEntry.ts +0 -39
- package/src/entries/CodeEntry.test.ts +0 -40
- package/src/entries/CodeEntry.ts +0 -52
- package/src/entries/ColorEntry.ts +0 -63
- package/src/entries/ComponentEntry.test.ts +0 -173
- package/src/entries/ComponentEntry.ts +0 -95
- package/src/entries/Entry.ts +0 -304
- package/src/entries/IconEntry.ts +0 -49
- package/src/entries/ImageEntry.ts +0 -61
- package/src/entries/KeyValueEntry.ts +0 -47
- package/src/entries/RepeatableEntry.test.ts +0 -239
- package/src/entries/RepeatableEntry.ts +0 -173
- package/src/entries/TextEntry.test.ts +0 -394
- package/src/entries/TextEntry.ts +0 -60
- package/src/entries/index.ts +0 -12
- package/src/entries/leaves.test.ts +0 -306
- package/src/entries/registry.ts +0 -54
- package/src/fields/BuilderField.test.ts +0 -1188
- package/src/fields/BuilderField.ts +0 -605
- package/src/fields/BuilderRelationship.test.ts +0 -811
- package/src/fields/CheckboxField.test.ts +0 -44
- package/src/fields/CheckboxField.ts +0 -27
- package/src/fields/CheckboxListField.test.ts +0 -99
- package/src/fields/CheckboxListField.ts +0 -66
- package/src/fields/ColorPickerField.test.ts +0 -33
- package/src/fields/ColorPickerField.ts +0 -25
- package/src/fields/DateField.ts +0 -54
- package/src/fields/DateTimeField.test.ts +0 -55
- package/src/fields/EmailField.ts +0 -16
- package/src/fields/Field.test.ts +0 -654
- package/src/fields/Field.ts +0 -817
- package/src/fields/FileUploadField.test.ts +0 -143
- package/src/fields/FileUploadField.ts +0 -159
- package/src/fields/HiddenField.test.ts +0 -27
- package/src/fields/HiddenField.ts +0 -28
- package/src/fields/KeyValueField.test.ts +0 -105
- package/src/fields/KeyValueField.ts +0 -55
- package/src/fields/MarkdownField.test.ts +0 -167
- package/src/fields/MarkdownField.ts +0 -162
- package/src/fields/NumberField.ts +0 -33
- package/src/fields/RadioField.test.ts +0 -94
- package/src/fields/RadioField.ts +0 -67
- package/src/fields/RepeaterField.test.ts +0 -1806
- package/src/fields/RepeaterField.ts +0 -939
- package/src/fields/RepeaterRelationship.test.ts +0 -1923
- package/src/fields/RepeaterSimple.test.ts +0 -248
- package/src/fields/RowButton.test.ts +0 -219
- package/src/fields/RowButton.ts +0 -135
- package/src/fields/SelectField.test.ts +0 -192
- package/src/fields/SelectField.ts +0 -235
- package/src/fields/SliderField.test.ts +0 -50
- package/src/fields/SliderField.ts +0 -53
- package/src/fields/SlugField.ts +0 -24
- package/src/fields/TagsInputField.test.ts +0 -154
- package/src/fields/TagsInputField.ts +0 -133
- package/src/fields/TextField.test.ts +0 -213
- package/src/fields/TextField.ts +0 -177
- package/src/fields/TextareaField.test.ts +0 -58
- package/src/fields/TextareaField.ts +0 -59
- package/src/fields/ToggleButtonsField.test.ts +0 -106
- package/src/fields/ToggleButtonsField.ts +0 -59
- package/src/fields/ToggleField.ts +0 -16
- package/src/fields/disableOptionsWhenSelectedInSiblingRepeaterItems.test.ts +0 -319
- package/src/fields/optionsResolver.ts +0 -95
- package/src/fields/resolveField.ts +0 -28
- package/src/filters/BooleanFilter.ts +0 -35
- package/src/filters/DateRangeFilter.test.ts +0 -194
- package/src/filters/DateRangeFilter.ts +0 -148
- package/src/filters/Filter.test.ts +0 -268
- package/src/filters/Filter.ts +0 -184
- package/src/filters/FormFilter.test.ts +0 -238
- package/src/filters/FormFilter.ts +0 -215
- package/src/filters/MultiSelectFilter.test.ts +0 -119
- package/src/filters/MultiSelectFilter.ts +0 -78
- package/src/filters/QueryBuilderFilter.test.ts +0 -662
- package/src/filters/QueryBuilderFilter.ts +0 -398
- package/src/filters/SelectFilter.ts +0 -46
- package/src/filters/TernaryFilter.test.ts +0 -160
- package/src/filters/TernaryFilter.ts +0 -72
- package/src/filters/TrashedFilter.test.ts +0 -149
- package/src/filters/TrashedFilter.ts +0 -55
- package/src/filters/queryBuilder/BooleanConstraint.ts +0 -31
- package/src/filters/queryBuilder/Constraint.ts +0 -115
- package/src/filters/queryBuilder/DateConstraint.ts +0 -69
- package/src/filters/queryBuilder/NumberConstraint.ts +0 -66
- package/src/filters/queryBuilder/SelectConstraint.ts +0 -72
- package/src/filters/queryBuilder/TextConstraint.ts +0 -64
- package/src/filters/queryBuilder/index.ts +0 -12
- package/src/icons/index.ts +0 -2
- package/src/icons/lucide.ts +0 -204
- package/src/icons/registry.test.ts +0 -56
- package/src/icons/registry.ts +0 -41
- package/src/icons/types.ts +0 -47
- package/src/index.ts +0 -525
- package/src/io/csv.test.ts +0 -142
- package/src/io/csv.ts +0 -170
- package/src/nestedRelationManagerData.test.ts +0 -547
- package/src/notifications/Notification.test.ts +0 -210
- package/src/notifications/Notification.ts +0 -354
- package/src/notifications/broadcast.test.ts +0 -110
- package/src/notifications/broadcast.ts +0 -95
- package/src/notifications/database.test.ts +0 -383
- package/src/notifications/database.ts +0 -398
- package/src/notifications/databaseNotifications.test.ts +0 -187
- package/src/notifications/dispatchNotificationAction.test.ts +0 -341
- package/src/notifications/dispatchNotificationAction.ts +0 -142
- package/src/notifications/flash.test.ts +0 -89
- package/src/notifications/flash.ts +0 -71
- package/src/notifications/index.ts +0 -45
- package/src/notifications/registerBroadcastAuth.test.ts +0 -134
- package/src/notifications/registerBroadcastAuth.ts +0 -100
- package/src/notifications/resolveSavedNotification.test.ts +0 -82
- package/src/notifications/resolveSavedNotification.ts +0 -59
- package/src/notifications/types.ts +0 -93
- package/src/orm/m2mAccessor.ts +0 -66
- package/src/orm/modelDefaults.test.ts +0 -633
- package/src/orm/modelDefaults.ts +0 -666
- package/src/pageData/breadcrumbs.ts +0 -288
- package/src/pageData/forms.ts +0 -578
- package/src/pageData/helpers.ts +0 -857
- package/src/pageData/misc.ts +0 -347
- package/src/pageData/navigation.ts +0 -842
- package/src/pageData/relationPages.ts +0 -1248
- package/src/pageData/relationTabs.ts +0 -286
- package/src/pageData/resourcePages.ts +0 -609
- package/src/pageData.test.ts +0 -1545
- package/src/pageData.ts +0 -341
- package/src/plugins/index.ts +0 -8
- package/src/plugins/themeEditor.test.ts +0 -36
- package/src/plugins/themeEditor.ts +0 -45
- package/src/react/AppShell.tsx +0 -251
- package/src/react/CollabExtensionFactoryRegistry.ts +0 -55
- package/src/react/CollabRoomContext.ts +0 -98
- package/src/react/CollabTextRendererRegistry.ts +0 -102
- package/src/react/CommandPalette.tsx +0 -375
- package/src/react/CurrentUserContext.tsx +0 -50
- package/src/react/CustomPageWrapperGate.tsx +0 -69
- package/src/react/CustomPageWrapperRegistry.ts +0 -45
- package/src/react/FieldFocusReporterRegistry.ts +0 -37
- package/src/react/FieldLabelSlotRegistry.ts +0 -30
- package/src/react/FieldPresenceRegistry.ts +0 -46
- package/src/react/FormCollabBindingRegistry.ts +0 -242
- package/src/react/FormStateContext.tsx +0 -591
- package/src/react/HeadHooks.tsx +0 -126
- package/src/react/MarkdownEditorRegistry.test.ts +0 -38
- package/src/react/MarkdownEditorRegistry.ts +0 -107
- package/src/react/NotificationActionStrip.tsx +0 -263
- package/src/react/NotificationBell.tsx +0 -426
- package/src/react/PendingSuggestionApplierRegistry.test.ts +0 -97
- package/src/react/PendingSuggestionApplierRegistry.ts +0 -98
- package/src/react/PendingSuggestionOverlayRegistry.ts +0 -54
- package/src/react/PendingSuggestionsContext.tsx +0 -172
- package/src/react/RecordWrapperGate.tsx +0 -58
- package/src/react/RecordWrapperRegistry.ts +0 -39
- package/src/react/RenderHookSlot.tsx +0 -32
- package/src/react/RightSidebar.tsx +0 -257
- package/src/react/RightSidebarContext.tsx +0 -234
- package/src/react/RightSidebarTrigger.tsx +0 -53
- package/src/react/RowCoordsContext.tsx +0 -23
- package/src/react/SchemaRenderer.tsx +0 -549
- package/src/react/SearchTrigger.tsx +0 -46
- package/src/react/ThemeProvider.tsx +0 -93
- package/src/react/ThemeSettingsPage.tsx +0 -579
- package/src/react/ThemeToggle.tsx +0 -20
- package/src/react/Toaster.tsx +0 -158
- package/src/react/UserMenu.tsx +0 -196
- package/src/react/WidgetDataContext.tsx +0 -157
- package/src/react/cells/EditableCell.tsx +0 -389
- package/src/react/component-slots.test.ts +0 -103
- package/src/react/component-slots.ts +0 -116
- package/src/react/fieldJsHandler.test.ts +0 -166
- package/src/react/fieldJsHandler.ts +0 -79
- package/src/react/fields/BuilderInput.tsx +0 -1078
- package/src/react/fields/CheckboxInput.tsx +0 -39
- package/src/react/fields/CheckboxListInput.tsx +0 -102
- package/src/react/fields/ColorInput.tsx +0 -71
- package/src/react/fields/DateFieldInput.tsx +0 -70
- package/src/react/fields/DateTimeInput.tsx +0 -62
- package/src/react/fields/FieldShell.tsx +0 -348
- package/src/react/fields/FileUploadInput.tsx +0 -639
- package/src/react/fields/HiddenInput.tsx +0 -17
- package/src/react/fields/KeyValueInput.tsx +0 -230
- package/src/react/fields/MarkdownInput.tsx +0 -560
- package/src/react/fields/RadioInput.tsx +0 -81
- package/src/react/fields/RepeaterInput.test.ts +0 -116
- package/src/react/fields/RepeaterInput.tsx +0 -1420
- package/src/react/fields/SelectFieldInput.tsx +0 -280
- package/src/react/fields/SliderInput.tsx +0 -81
- package/src/react/fields/TagsInput.tsx +0 -283
- package/src/react/fields/TextLikeInput.tsx +0 -256
- package/src/react/fields/ToggleButtonsInput.tsx +0 -60
- package/src/react/fields/ToggleFieldInput.tsx +0 -56
- package/src/react/fields/relationshipRenameDispatch.test.ts +0 -106
- package/src/react/fields/relationshipRenameDispatch.ts +0 -97
- package/src/react/fields/repeaterReconcile.test.ts +0 -114
- package/src/react/fields/repeaterReconcile.ts +0 -104
- package/src/react/fields/rowChromeButton.tsx +0 -336
- package/src/react/fields/rowState.ts +0 -106
- package/src/react/fields/syncRowGates.test.ts +0 -202
- package/src/react/fields/syncRowGates.ts +0 -66
- package/src/react/fields/textInputControls.tsx +0 -238
- package/src/react/fields/useRowReorderDnd.ts +0 -78
- package/src/react/formStateHelpers.test.ts +0 -508
- package/src/react/formStateHelpers.ts +0 -381
- package/src/react/hooks/use-mobile.ts +0 -19
- package/src/react/icon-context.tsx +0 -60
- package/src/react/index.ts +0 -194
- package/src/react/layouts/SidebarLayout.tsx +0 -250
- package/src/react/layouts/TopbarLayout.tsx +0 -258
- package/src/react/navigate.tsx +0 -37
- package/src/react/onProviderSynced.test.ts +0 -90
- package/src/react/parseRecordEditUrl.test.ts +0 -122
- package/src/react/parseRecordEditUrl.ts +0 -94
- package/src/react/persistedState.ts +0 -40
- package/src/react/registry.ts +0 -48
- package/src/react/right-panel-registry.tsx +0 -47
- package/src/react/schemaRenderer/AlertRenderer.tsx +0 -112
- package/src/react/schemaRenderer/EntryRenderer.tsx +0 -501
- package/src/react/schemaRenderer/SectionRenderer.tsx +0 -120
- package/src/react/schemaRenderer/SimpleElements.tsx +0 -306
- package/src/react/schemaRenderer/TabsRenderer.tsx +0 -62
- package/src/react/schemaRenderer/WizardRenderer.tsx +0 -338
- package/src/react/schemaRenderer/action/ActionGroupTrigger.tsx +0 -177
- package/src/react/schemaRenderer/action/ActionModalDialog.tsx +0 -273
- package/src/react/schemaRenderer/action/ConfirmActionDialog.tsx +0 -61
- package/src/react/schemaRenderer/action/HandlerActionButton.tsx +0 -43
- package/src/react/schemaRenderer/action/MethodActionButton.tsx +0 -64
- package/src/react/schemaRenderer/action/buttons.tsx +0 -99
- package/src/react/schemaRenderer/action/helpers.ts +0 -140
- package/src/react/schemaRenderer/action/renderAction.tsx +0 -245
- package/src/react/schemaRenderer/columnFormat.ts +0 -65
- package/src/react/schemaRenderer/constants.ts +0 -50
- package/src/react/schemaRenderer/form/FormRenderer.tsx +0 -274
- package/src/react/schemaRenderer/form/renderField.tsx +0 -511
- package/src/react/schemaRenderer/helpers.tsx +0 -81
- package/src/react/schemaRenderer/table/CardsLayoutBody.tsx +0 -308
- package/src/react/schemaRenderer/table/TableRenderer.tsx +0 -123
- package/src/react/schemaRenderer/table/TableRendererBody.tsx +0 -974
- package/src/react/schemaRenderer/table/filters.tsx +0 -1233
- package/src/react/schemaRenderer/table/formatCell.tsx +0 -264
- package/src/react/schemaRenderer/table/links.tsx +0 -112
- package/src/react/schemaRenderer/table/renderRowActions.tsx +0 -52
- package/src/react/schemaRenderer/table/url.tsx +0 -143
- package/src/react/theme-preview/apply.ts +0 -99
- package/src/react/theme-preview/build-html.ts +0 -436
- package/src/react/ui/button.tsx +0 -51
- package/src/react/ui/calendar.tsx +0 -67
- package/src/react/ui/checkbox.tsx +0 -29
- package/src/react/ui/dialog.tsx +0 -108
- package/src/react/ui/dropdown-menu.tsx +0 -97
- package/src/react/ui/input.tsx +0 -20
- package/src/react/ui/label.tsx +0 -21
- package/src/react/ui/popover.tsx +0 -50
- package/src/react/ui/select.tsx +0 -169
- package/src/react/ui/separator.tsx +0 -25
- package/src/react/ui/sheet.tsx +0 -136
- package/src/react/ui/sidebar.tsx +0 -723
- package/src/react/ui/skeleton.tsx +0 -13
- package/src/react/ui/slider.tsx +0 -34
- package/src/react/ui/switch.tsx +0 -28
- package/src/react/ui/table.tsx +0 -105
- package/src/react/ui/tabs.tsx +0 -63
- package/src/react/ui/textarea.tsx +0 -18
- package/src/react/ui/tooltip.tsx +0 -64
- package/src/react/useResizableWidth.ts +0 -139
- package/src/react/utils.ts +0 -6
- package/src/react/widgetRegistry.test.ts +0 -43
- package/src/react/widgetRegistry.ts +0 -50
- package/src/react/widgets/StatsOverviewRenderer.tsx +0 -232
- package/src/react/widgets/TableWidgetRenderer.tsx +0 -231
- package/src/react/widgets/ViewRenderer.tsx +0 -71
- package/src/relationManagerData.test.ts +0 -1595
- package/src/richtext/index.ts +0 -8
- package/src/richtext/registry.ts +0 -89
- package/src/routes/globals.ts +0 -148
- package/src/routes/guard.test.ts +0 -325
- package/src/routes/helpers.ts +0 -704
- package/src/routes/pages.ts +0 -175
- package/src/routes/panel.ts +0 -204
- package/src/routes/relations.ts +0 -1243
- package/src/routes/resources.ts +0 -781
- package/src/routes/theme.ts +0 -91
- package/src/routes-nested-relations.test.ts +0 -676
- package/src/routes-relations.test.ts +0 -972
- package/src/routes.test.ts +0 -2027
- package/src/routes.ts +0 -303
- package/src/schema/Alert.test.ts +0 -109
- package/src/schema/Alert.ts +0 -131
- package/src/schema/Block.ts +0 -169
- package/src/schema/Breadcrumbs.ts +0 -40
- package/src/schema/Card.ts +0 -35
- package/src/schema/Divider.ts +0 -20
- package/src/schema/Element.ts +0 -219
- package/src/schema/EmptyState.test.ts +0 -37
- package/src/schema/EmptyState.ts +0 -63
- package/src/schema/Fieldset.ts +0 -43
- package/src/schema/Grid.ts +0 -43
- package/src/schema/Group.ts +0 -30
- package/src/schema/Heading.ts +0 -39
- package/src/schema/Html.ts +0 -67
- package/src/schema/Icon.ts +0 -54
- package/src/schema/Image.ts +0 -57
- package/src/schema/LinkTag.ts +0 -41
- package/src/schema/Markdown.ts +0 -85
- package/src/schema/MetaTag.ts +0 -41
- package/src/schema/RelationTabs.ts +0 -71
- package/src/schema/ScriptTag.ts +0 -55
- package/src/schema/Section.ts +0 -160
- package/src/schema/ServerDataElement.test.ts +0 -140
- package/src/schema/ServerDataElement.ts +0 -156
- package/src/schema/SlotComponent.test.ts +0 -77
- package/src/schema/SlotComponent.ts +0 -71
- package/src/schema/Split.ts +0 -50
- package/src/schema/Stat.test.ts +0 -118
- package/src/schema/Stat.ts +0 -154
- package/src/schema/StatsOverview.test.ts +0 -141
- package/src/schema/StatsOverview.ts +0 -119
- package/src/schema/StyleTag.ts +0 -35
- package/src/schema/TableWidget.test.ts +0 -297
- package/src/schema/TableWidget.ts +0 -289
- package/src/schema/Tabs.ts +0 -79
- package/src/schema/Text.ts +0 -58
- package/src/schema/UnorderedList.ts +0 -49
- package/src/schema/View.test.ts +0 -111
- package/src/schema/View.ts +0 -127
- package/src/schema/Wizard.ts +0 -220
- package/src/schema/containers.test.ts +0 -564
- package/src/schema/headTags.test.ts +0 -134
- package/src/schema/index.ts +0 -40
- package/src/schema/primes.test.ts +0 -269
- package/src/schema/resolveSchema.test.ts +0 -379
- package/src/schema/resolveSchema.ts +0 -917
- package/src/schema/sanitize.ts +0 -58
- package/src/search.test.ts +0 -446
- package/src/search.ts +0 -178
- package/src/sessionFilters.test.ts +0 -375
- package/src/sessionFilters.ts +0 -143
- package/src/slot-components/index.ts +0 -10
- package/src/slot-components/registry.ts +0 -56
- package/src/styles/file-upload.css +0 -13
- package/src/summarizers/Summarizer.test.ts +0 -84
- package/src/summarizers/Summarizer.ts +0 -123
- package/src/summarizers/index.ts +0 -11
- package/src/theme/base-colors.ts +0 -68
- package/src/theme/chart-colors.ts +0 -50
- package/src/theme/colors.ts +0 -447
- package/src/theme/generate-css.test.ts +0 -139
- package/src/theme/generate-css.ts +0 -44
- package/src/theme/generate-scale.test.ts +0 -106
- package/src/theme/generate-scale.ts +0 -97
- package/src/theme/icon-map.ts +0 -42
- package/src/theme/index.ts +0 -34
- package/src/theme/migrate.test.ts +0 -178
- package/src/theme/migrate.ts +0 -81
- package/src/theme/presets.ts +0 -135
- package/src/theme/radius.ts +0 -18
- package/src/theme/resolve.test.ts +0 -238
- package/src/theme/resolve.ts +0 -96
- package/src/theme/spacing.ts +0 -18
- package/src/theme/storage.test.ts +0 -126
- package/src/theme/storage.ts +0 -106
- package/src/theme/theme-colors.ts +0 -88
- package/src/theme/types.ts +0 -125
- package/src/uploads/UploadAdapter.ts +0 -35
- package/src/uploads/index.ts +0 -2
- package/src/uploads/localUpload.test.ts +0 -70
- package/src/uploads/localUpload.ts +0 -84
- package/src/validation/Validator.ts +0 -49
- package/src/validation/index.ts +0 -28
- package/src/validation/rules.ts +0 -78
- package/src/validation/runValidators.ts +0 -435
- package/src/validation/uniqueValidator.test.ts +0 -196
- package/src/validation/uniqueValidator.ts +0 -133
- package/src/validation/validators.test.ts +0 -268
- package/src/vite.test.ts +0 -184
- package/src/vite.ts +0 -787
- package/src/widgets/index.ts +0 -10
- package/src/widgets/registry.ts +0 -45
- package/src/widgets.test.ts +0 -592
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -10
- package/views/react/Dashboard.tsx +0 -27
- package/views/react/Resources/Form.tsx +0 -102
- package/views/react/Resources/Index.tsx +0 -49
|
@@ -1,1188 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
|
|
4
|
-
import { BuilderField, Builder, isBuilderField, type BuilderFieldMeta } from './BuilderField.js'
|
|
5
|
-
import { RepeaterField } from './RepeaterField.js'
|
|
6
|
-
import { TextField } from './TextField.js'
|
|
7
|
-
import { NumberField } from './NumberField.js'
|
|
8
|
-
import { ToggleField } from './ToggleField.js'
|
|
9
|
-
import { Block } from '../schema/Block.js'
|
|
10
|
-
import { resolveSchema } from '../schema/resolveSchema.js'
|
|
11
|
-
import { coerceFormValues, applyStateUpdate, findForms } from '../elements/dispatchForm.js'
|
|
12
|
-
import { findActions } from '../elements/dispatchAction.js'
|
|
13
|
-
import { findTables } from '../elements/dispatchTable.js'
|
|
14
|
-
import { Form } from '../elements/Form.js'
|
|
15
|
-
import { Table } from '../elements/Table.js'
|
|
16
|
-
import { Action } from '../actions/Action.js'
|
|
17
|
-
import { Section } from '../schema/Section.js'
|
|
18
|
-
import { validateSchema, isValid } from '../validation/index.js'
|
|
19
|
-
|
|
20
|
-
// ─── Block ──────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
describe('Block', () => {
|
|
23
|
-
it('label defaults to titlecased name', () => {
|
|
24
|
-
const meta = Block.make('heading').toMeta()
|
|
25
|
-
assert.equal(meta.label, 'Heading')
|
|
26
|
-
assert.equal(meta.name, 'heading')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('builders round-trip through toMeta', () => {
|
|
30
|
-
const meta = Block.make('heading')
|
|
31
|
-
.label('Title')
|
|
32
|
-
.icon('heading')
|
|
33
|
-
.columns(2)
|
|
34
|
-
.maxItems(1)
|
|
35
|
-
.toMeta()
|
|
36
|
-
assert.equal(meta.label, 'Title')
|
|
37
|
-
assert.equal(meta.icon, 'heading')
|
|
38
|
-
assert.equal(meta.columns, 2)
|
|
39
|
-
assert.equal(meta.maxItems, 1)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('schema() stores inner elements', () => {
|
|
43
|
-
const inner = [TextField.make('text'), NumberField.make('level')]
|
|
44
|
-
const b = Block.make('heading').schema(inner)
|
|
45
|
-
assert.deepEqual(b.getSchema(), inner)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('only sets meta keys when configured', () => {
|
|
49
|
-
const meta = Block.make('p').toMeta()
|
|
50
|
-
assert.equal('icon' in meta, false)
|
|
51
|
-
assert.equal('columns' in meta, false)
|
|
52
|
-
assert.equal('maxItems' in meta, false)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
describe('visible(rule)', () => {
|
|
56
|
-
it('returns true when no rule is set', async () => {
|
|
57
|
-
const visible = await Block.make('heading').evaluateVisibility()
|
|
58
|
-
assert.equal(visible, true)
|
|
59
|
-
assert.equal(Block.make('heading').hasVisibilityRule(), false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('boolean rule short-circuits', async () => {
|
|
63
|
-
const b = Block.make('heading').visible(false)
|
|
64
|
-
assert.equal(await b.evaluateVisibility(), false)
|
|
65
|
-
assert.equal(b.hasVisibilityRule(), true)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('callback receives the layout context', async () => {
|
|
69
|
-
let seen: unknown
|
|
70
|
-
const b = Block.make('heading').visible((ctx) => {
|
|
71
|
-
seen = ctx.user
|
|
72
|
-
return true
|
|
73
|
-
})
|
|
74
|
-
await b.evaluateVisibility({ user: { role: 'admin' } })
|
|
75
|
-
assert.deepEqual(seen, { role: 'admin' })
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('async callback is awaited', async () => {
|
|
79
|
-
const b = Block.make('heading').visible(async () => false)
|
|
80
|
-
assert.equal(await b.evaluateVisibility(), false)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('throwing rule fails closed (hidden)', async () => {
|
|
84
|
-
const b = Block.make('heading').visible(() => { throw new Error('boom') })
|
|
85
|
-
assert.equal(await b.evaluateVisibility(), false)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('hidden() inverts the rule', async () => {
|
|
89
|
-
assert.equal(await Block.make('h').hidden(true).evaluateVisibility(), false)
|
|
90
|
-
assert.equal(await Block.make('h').hidden(false).evaluateVisibility(), true)
|
|
91
|
-
const b = Block.make('h').hidden((ctx) => ctx.user === 'banned')
|
|
92
|
-
assert.equal(await b.evaluateVisibility({ user: 'banned' }), false)
|
|
93
|
-
assert.equal(await b.evaluateVisibility({ user: 'ok' }), true)
|
|
94
|
-
})
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
// ─── BuilderField ────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
describe('BuilderField', () => {
|
|
101
|
-
it('emits fieldType "builder"', () => {
|
|
102
|
-
const meta = BuilderField.make('content').toMeta()
|
|
103
|
-
assert.equal(meta.fieldType, 'builder')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('exports an alias `Builder`', () => {
|
|
107
|
-
assert.equal(Builder, BuilderField)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('uses `field` as the element type discriminator', () => {
|
|
111
|
-
assert.equal(BuilderField.make('content').getType(), 'field')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('rows defaults to empty', () => {
|
|
115
|
-
const meta = BuilderField.make('content').toMeta()
|
|
116
|
-
assert.deepEqual(meta.rows, [])
|
|
117
|
-
assert.deepEqual(meta.blocks, [])
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('blocks() ships picker meta (label/icon/columns/maxItems)', () => {
|
|
121
|
-
const meta = BuilderField.make('content')
|
|
122
|
-
.blocks([
|
|
123
|
-
Block.make('heading').label('Heading').icon('heading').columns(2),
|
|
124
|
-
Block.make('paragraph').label('Paragraph').maxItems(5),
|
|
125
|
-
])
|
|
126
|
-
.toMeta()
|
|
127
|
-
assert.equal(meta.blocks.length, 2)
|
|
128
|
-
assert.equal(meta.blocks[0]?.name, 'heading')
|
|
129
|
-
assert.equal(meta.blocks[0]?.label, 'Heading')
|
|
130
|
-
assert.equal(meta.blocks[0]?.icon, 'heading')
|
|
131
|
-
assert.equal(meta.blocks[0]?.columns, 2)
|
|
132
|
-
assert.equal(meta.blocks[1]?.maxItems, 5)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
describe('builders', () => {
|
|
136
|
-
it('minItems() / maxItems() emit only when set', () => {
|
|
137
|
-
const empty = BuilderField.make('x').toMeta()
|
|
138
|
-
assert.equal('minItems' in empty, false)
|
|
139
|
-
assert.equal('maxItems' in empty, false)
|
|
140
|
-
|
|
141
|
-
const set = BuilderField.make('x').minItems(1).maxItems(10).toMeta()
|
|
142
|
-
assert.equal(set.minItems, 1)
|
|
143
|
-
assert.equal(set.maxItems, 10)
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('reorderable() / collapsible() / cloneable() / collapsed() / blockNumbers() / itemNumbers() / blockIcons(false) flip flags', () => {
|
|
147
|
-
const meta = BuilderField.make('x')
|
|
148
|
-
.reorderable()
|
|
149
|
-
.reorderableWithButtons()
|
|
150
|
-
.collapsible()
|
|
151
|
-
.collapsed()
|
|
152
|
-
.cloneable()
|
|
153
|
-
.blockNumbers()
|
|
154
|
-
.itemNumbers()
|
|
155
|
-
.blockIcons(false)
|
|
156
|
-
.toMeta()
|
|
157
|
-
assert.equal(meta.reorderable, true)
|
|
158
|
-
assert.equal(meta.reorderableWithButtons, true)
|
|
159
|
-
assert.equal(meta.collapsible, true)
|
|
160
|
-
assert.equal(meta.defaultCollapsed, true)
|
|
161
|
-
assert.equal(meta.cloneable, true)
|
|
162
|
-
assert.equal(meta.blockNumbers, true)
|
|
163
|
-
assert.equal(meta.itemNumbers, true)
|
|
164
|
-
assert.equal(meta.blockIcons, false)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('accordion() emits only when set, auto-arms collapsible', () => {
|
|
168
|
-
assert.equal('accordion' in BuilderField.make('x').toMeta(), false)
|
|
169
|
-
const meta = BuilderField.make('x').accordion().toMeta()
|
|
170
|
-
assert.equal(meta.accordion, true)
|
|
171
|
-
assert.equal(meta.collapsible, true)
|
|
172
|
-
assert.equal(BuilderField.make('x').accordion().isAccordion(), true)
|
|
173
|
-
assert.equal(BuilderField.make('x').accordion().isCollapsible(), true)
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('accordion() composes with collapsed() to start all-collapsed', () => {
|
|
177
|
-
const meta = BuilderField.make('x').accordion().collapsed().toMeta()
|
|
178
|
-
assert.equal(meta.accordion, true)
|
|
179
|
-
assert.equal(meta.collapsible, true)
|
|
180
|
-
assert.equal(meta.defaultCollapsed, true)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
it('accordion(false) leaves collapsible alone and isAccordion() reflects setter', () => {
|
|
184
|
-
const meta = BuilderField.make('x').collapsible().accordion(false).toMeta()
|
|
185
|
-
assert.equal(meta.collapsible, true)
|
|
186
|
-
assert.equal('accordion' in meta, false)
|
|
187
|
-
assert.equal(BuilderField.make('x').isAccordion(), false)
|
|
188
|
-
assert.equal(BuilderField.make('x').accordion().isAccordion(), true)
|
|
189
|
-
assert.equal(BuilderField.make('x').accordion(false).isAccordion(), false)
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('addable(false) / deletable(false) emit only when off', () => {
|
|
193
|
-
const on = BuilderField.make('x').toMeta()
|
|
194
|
-
assert.equal('addable' in on, false)
|
|
195
|
-
assert.equal('deletable' in on, false)
|
|
196
|
-
const off = BuilderField.make('x').addable(false).deletable(false).toMeta()
|
|
197
|
-
assert.equal(off.addable, false)
|
|
198
|
-
assert.equal(off.deletable, false)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('addBetween() emits only when set', () => {
|
|
202
|
-
const off = BuilderField.make('x').toMeta()
|
|
203
|
-
assert.equal('addBetween' in off, false)
|
|
204
|
-
assert.equal(BuilderField.make('x').isAddBetween(), false)
|
|
205
|
-
const on = BuilderField.make('x').addBetween().toMeta()
|
|
206
|
-
assert.equal(on.addBetween, true)
|
|
207
|
-
assert.equal(BuilderField.make('x').addBetween().isAddBetween(), true)
|
|
208
|
-
// Toggleable back off.
|
|
209
|
-
const back = BuilderField.make('x').addBetween().addBetween(false).toMeta()
|
|
210
|
-
assert.equal('addBetween' in back, false)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('addActionAlignment defaults to start (omitted) and emits when changed', () => {
|
|
214
|
-
assert.equal('addActionAlignment' in BuilderField.make('x').toMeta(), false)
|
|
215
|
-
const center = BuilderField.make('x').addActionAlignment('center').toMeta()
|
|
216
|
-
assert.equal(center.addActionAlignment, 'center')
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('blockPickerColumns() emits only when set', () => {
|
|
220
|
-
assert.equal('blockPickerColumns' in BuilderField.make('x').toMeta(), false)
|
|
221
|
-
assert.equal(BuilderField.make('x').blockPickerColumns(2).toMeta().blockPickerColumns, 2)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('addActionLabel() emits only when set', () => {
|
|
225
|
-
assert.equal('addActionLabel' in BuilderField.make('x').toMeta(), false)
|
|
226
|
-
assert.equal(
|
|
227
|
-
BuilderField.make('x').addActionLabel('Add block').toMeta().addActionLabel,
|
|
228
|
-
'Add block',
|
|
229
|
-
)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('itemHidden() / itemLabel() store the rule (evaluated per row at resolve)', () => {
|
|
233
|
-
const labelFn = (data: Record<string, unknown>) => String(data['text'] ?? '')
|
|
234
|
-
const hiddenFn = () => false
|
|
235
|
-
const f = BuilderField.make('x').itemLabel(labelFn).itemHidden(hiddenFn)
|
|
236
|
-
assert.equal(f.getItemLabel(), labelFn)
|
|
237
|
-
assert.equal(f.getItemHidden(), hiddenFn)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
it('grid() emits only when set with n >= 2', () => {
|
|
241
|
-
// Mirrors RepeaterField.grid() — same semantics, same threshold.
|
|
242
|
-
// n < 2 acts as the off sentinel so users can toggle via a config
|
|
243
|
-
// value without a separate "no-grid" branch.
|
|
244
|
-
assert.equal('grid' in BuilderField.make('x').toMeta(), false)
|
|
245
|
-
assert.equal(BuilderField.make('x').grid(2).toMeta().grid, 2)
|
|
246
|
-
assert.equal('grid' in BuilderField.make('x').grid(1).toMeta(), false)
|
|
247
|
-
assert.equal('grid' in BuilderField.make('x').grid(0).toMeta(), false)
|
|
248
|
-
assert.equal(
|
|
249
|
-
BuilderField.make('x').grid(0).grid(3).toMeta().grid,
|
|
250
|
-
3,
|
|
251
|
-
)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('getGrid() reflects the setter', () => {
|
|
255
|
-
assert.equal(BuilderField.make('x').getGrid(), undefined)
|
|
256
|
-
assert.equal(BuilderField.make('x').grid(2).getGrid(), 2)
|
|
257
|
-
assert.equal(BuilderField.make('x').grid(2).grid(1).getGrid(), undefined)
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
it('grid() accepts a responsive object form (mirrors RepeaterField)', () => {
|
|
261
|
-
const meta = BuilderField.make('x')
|
|
262
|
-
.grid({ default: 1, md: 2, xl: 3 })
|
|
263
|
-
.toMeta()
|
|
264
|
-
assert.deepEqual(meta.grid, { default: 1, md: 2, xl: 3 })
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('grid() collapses single-default responsive object to scalar', () => {
|
|
268
|
-
const meta = BuilderField.make('x').grid({ default: 3 }).toMeta()
|
|
269
|
-
assert.equal(meta.grid, 3)
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
// ─── Resolution ───────────────────────────────────────────
|
|
274
|
-
|
|
275
|
-
describe('per-row resolve', () => {
|
|
276
|
-
function builder() {
|
|
277
|
-
return BuilderField.make('content').blocks([
|
|
278
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
279
|
-
Block.make('paragraph').schema([TextField.make('body'), ToggleField.make('emphasized')]),
|
|
280
|
-
])
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function metaOf(m: unknown): BuilderFieldMeta {
|
|
284
|
-
return m as BuilderFieldMeta
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
it('zero submitted rows → empty rows array', async () => {
|
|
288
|
-
const [raw] = await resolveSchema([builder()])
|
|
289
|
-
const m = metaOf(raw)
|
|
290
|
-
assert.deepEqual(m.rows, [])
|
|
291
|
-
assert.equal(m.blocks.length, 2)
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
it('rows resolve against the matching block schema (heterogeneous)', async () => {
|
|
295
|
-
const [raw] = await resolveSchema([builder()], {
|
|
296
|
-
values: { content: [
|
|
297
|
-
{ type: 'heading', data: { text: 'Welcome' } },
|
|
298
|
-
{ type: 'paragraph', data: { body: 'Hello', emphasized: true } },
|
|
299
|
-
] },
|
|
300
|
-
})
|
|
301
|
-
const m = metaOf(raw)
|
|
302
|
-
assert.equal(m.rows.length, 2)
|
|
303
|
-
assert.equal(m.rows[0]?.type, 'heading')
|
|
304
|
-
assert.equal(m.rows[1]?.type, 'paragraph')
|
|
305
|
-
assert.equal(m.rows[0]?.children.length, 1) // heading has 1 field
|
|
306
|
-
assert.equal(m.rows[1]?.children.length, 2) // paragraph has 2 fields
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
it('preserves __id from submitted row values', async () => {
|
|
310
|
-
const [raw] = await resolveSchema([builder()], {
|
|
311
|
-
values: { content: [{ __id: 'row-abc', type: 'heading', data: { text: 'X' } }] },
|
|
312
|
-
})
|
|
313
|
-
const m = metaOf(raw)
|
|
314
|
-
assert.equal(m.rows[0]?.id, 'row-abc')
|
|
315
|
-
})
|
|
316
|
-
|
|
317
|
-
it('falls back to deterministic id when __id missing', async () => {
|
|
318
|
-
const [raw] = await resolveSchema([builder()], {
|
|
319
|
-
values: { content: [{ type: 'heading', data: {} }, { type: 'paragraph', data: {} }] },
|
|
320
|
-
})
|
|
321
|
-
const m = metaOf(raw)
|
|
322
|
-
assert.equal(m.rows[0]?.id, 'content-0')
|
|
323
|
-
assert.equal(m.rows[1]?.id, 'content-1')
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
it('Block.visible() drops hidden blocks from picker meta only', async () => {
|
|
327
|
-
const f = BuilderField.make('content').blocks([
|
|
328
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
329
|
-
Block.make('admin')
|
|
330
|
-
.visible(({ user }) => (user as { role?: string } | undefined)?.role === 'admin')
|
|
331
|
-
.schema([TextField.make('secret')]),
|
|
332
|
-
])
|
|
333
|
-
const [rawGuest] = await resolveSchema([f], { user: { role: 'guest' } })
|
|
334
|
-
const mGuest = metaOf(rawGuest)
|
|
335
|
-
assert.equal(mGuest.blocks.length, 1)
|
|
336
|
-
assert.equal(mGuest.blocks[0]?.name, 'heading')
|
|
337
|
-
|
|
338
|
-
const [rawAdmin] = await resolveSchema([f], { user: { role: 'admin' } })
|
|
339
|
-
const mAdmin = metaOf(rawAdmin)
|
|
340
|
-
assert.equal(mAdmin.blocks.length, 2)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
it('Block.visible() does NOT hide existing rows of a hidden block', async () => {
|
|
344
|
-
// Toggling a feature flag must never silently destroy stored content.
|
|
345
|
-
const f = BuilderField.make('content').blocks([
|
|
346
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
347
|
-
Block.make('admin').visible(false).schema([TextField.make('secret')]),
|
|
348
|
-
])
|
|
349
|
-
const [raw] = await resolveSchema([f], {
|
|
350
|
-
values: { content: [
|
|
351
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
352
|
-
{ type: 'admin', data: { secret: 'shh' } },
|
|
353
|
-
] },
|
|
354
|
-
})
|
|
355
|
-
const m = metaOf(raw)
|
|
356
|
-
assert.equal(m.blocks.length, 1, 'picker drops admin')
|
|
357
|
-
assert.equal(m.rows.length, 2, 'rows still render')
|
|
358
|
-
assert.equal(m.rows[1]?.type, 'admin')
|
|
359
|
-
assert.equal(m.rows[1]?.children.length, 1, 'admin row inner schema resolves')
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('unknown block type → unknownType:true with empty children', async () => {
|
|
363
|
-
const [raw] = await resolveSchema([builder()], {
|
|
364
|
-
values: { content: [{ type: 'mysterious', data: { stuff: 42 } }] },
|
|
365
|
-
})
|
|
366
|
-
const m = metaOf(raw)
|
|
367
|
-
assert.equal(m.rows[0]?.unknownType, true)
|
|
368
|
-
assert.equal(m.rows[0]?.type, 'mysterious')
|
|
369
|
-
assert.deepEqual(m.rows[0]?.children, [])
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
it('row-scoped $get reads the row\'s data only', async () => {
|
|
373
|
-
let captured: unknown
|
|
374
|
-
const f = BuilderField.make('content').blocks([
|
|
375
|
-
Block.make('heading').schema([
|
|
376
|
-
TextField.make('text').showWhen(({ $get, row }) => {
|
|
377
|
-
if (row) captured = $get?.('text')
|
|
378
|
-
return true
|
|
379
|
-
}),
|
|
380
|
-
]),
|
|
381
|
-
])
|
|
382
|
-
await resolveSchema([f], {
|
|
383
|
-
values: { content: [{ type: 'heading', data: { text: 'fromRow' } }] },
|
|
384
|
-
})
|
|
385
|
-
assert.equal(captured, 'fromRow')
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
it('ctx.row.index reflects the row position', async () => {
|
|
389
|
-
const seen: number[] = []
|
|
390
|
-
const f = BuilderField.make('content').blocks([
|
|
391
|
-
Block.make('heading').schema([
|
|
392
|
-
TextField.make('text').showWhen(({ row }) => {
|
|
393
|
-
if (row) seen.push(row.index)
|
|
394
|
-
return true
|
|
395
|
-
}),
|
|
396
|
-
]),
|
|
397
|
-
])
|
|
398
|
-
await resolveSchema([f], {
|
|
399
|
-
values: { content: [
|
|
400
|
-
{ type: 'heading', data: {} },
|
|
401
|
-
{ type: 'heading', data: {} },
|
|
402
|
-
{ type: 'heading', data: {} },
|
|
403
|
-
] },
|
|
404
|
-
})
|
|
405
|
-
assert.deepEqual(seen, [0, 1, 2])
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
it('itemLabel(data, blockName) lands on row.itemLabel', async () => {
|
|
409
|
-
const f = BuilderField.make('content')
|
|
410
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
411
|
-
.itemLabel((data, blockName) => `${blockName}: ${String(data['text'] ?? '')}`)
|
|
412
|
-
const [raw] = await resolveSchema([f], {
|
|
413
|
-
values: { content: [{ type: 'heading', data: { text: 'Welcome' } }] },
|
|
414
|
-
})
|
|
415
|
-
const m = metaOf(raw)
|
|
416
|
-
assert.equal(m.rows[0]?.itemLabel, 'heading: Welcome')
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
it('itemHidden() truthy stamps row.hidden=true', async () => {
|
|
420
|
-
const f = BuilderField.make('content')
|
|
421
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
422
|
-
.itemHidden(({ values }) => (values as Record<string, unknown>)['text'] === 'skip')
|
|
423
|
-
const [raw] = await resolveSchema([f], {
|
|
424
|
-
values: { content: [
|
|
425
|
-
{ type: 'heading', data: { text: 'show' } },
|
|
426
|
-
{ type: 'heading', data: { text: 'skip' } },
|
|
427
|
-
] },
|
|
428
|
-
})
|
|
429
|
-
const m = metaOf(raw)
|
|
430
|
-
assert.equal(m.rows[0]?.hidden, undefined)
|
|
431
|
-
assert.equal(m.rows[1]?.hidden, true)
|
|
432
|
-
})
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
describe('itemCanDelete / itemCanClone / itemCanReorder (per-row capability gates)', () => {
|
|
436
|
-
function builder() {
|
|
437
|
-
return BuilderField.make('content').blocks([
|
|
438
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
439
|
-
Block.make('paragraph').schema([TextField.make('body')]),
|
|
440
|
-
])
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function metaOf(m: unknown): BuilderFieldMeta {
|
|
444
|
-
return m as BuilderFieldMeta
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
it('builders store + return their rules', () => {
|
|
448
|
-
const del = (_ctx: unknown) => true
|
|
449
|
-
const clone = (_ctx: unknown) => true
|
|
450
|
-
const reorder = (_ctx: unknown) => true
|
|
451
|
-
const f = BuilderField.make('content')
|
|
452
|
-
.itemCanDelete(del as never)
|
|
453
|
-
.itemCanClone(clone as never)
|
|
454
|
-
.itemCanReorder(reorder as never)
|
|
455
|
-
assert.equal(f.getItemCanDelete(), del)
|
|
456
|
-
assert.equal(f.getItemCanClone(), clone)
|
|
457
|
-
assert.equal(f.getItemCanReorder(), reorder)
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('rules unset → no row carries cap flags', async () => {
|
|
461
|
-
const [raw] = await resolveSchema([builder()], {
|
|
462
|
-
values: { content: [{ type: 'heading', data: { text: 'A' } }] },
|
|
463
|
-
})
|
|
464
|
-
const m = metaOf(raw)
|
|
465
|
-
assert.equal(m.rows[0]?.canDelete, undefined)
|
|
466
|
-
assert.equal(m.rows[0]?.canClone, undefined)
|
|
467
|
-
assert.equal(m.rows[0]?.canReorder, undefined)
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
it('static `itemCanDelete(false)` → every row stamps canDelete: false', async () => {
|
|
471
|
-
const f = builder().itemCanDelete(false)
|
|
472
|
-
const [raw] = await resolveSchema([f], {
|
|
473
|
-
values: { content: [
|
|
474
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
475
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
476
|
-
] },
|
|
477
|
-
})
|
|
478
|
-
const m = metaOf(raw)
|
|
479
|
-
assert.equal(m.rows[0]?.canDelete, false)
|
|
480
|
-
assert.equal(m.rows[1]?.canDelete, false)
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
it('predicate sees row.blockType so a single rule can branch by block', async () => {
|
|
484
|
-
const f = builder().itemCanDelete(({ row }) => row?.blockType !== 'heading')
|
|
485
|
-
const [raw] = await resolveSchema([f], {
|
|
486
|
-
values: { content: [
|
|
487
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
488
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
489
|
-
] },
|
|
490
|
-
})
|
|
491
|
-
const m = metaOf(raw)
|
|
492
|
-
assert.equal(m.rows[0]?.canDelete, false)
|
|
493
|
-
assert.equal(m.rows[1]?.canDelete, undefined)
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
it('itemCanClone gates clone per-row', async () => {
|
|
497
|
-
const f = builder()
|
|
498
|
-
.cloneable()
|
|
499
|
-
.itemCanClone(({ row }) => row?.index !== 0)
|
|
500
|
-
const [raw] = await resolveSchema([f], {
|
|
501
|
-
values: { content: [
|
|
502
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
503
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
504
|
-
] },
|
|
505
|
-
})
|
|
506
|
-
const m = metaOf(raw)
|
|
507
|
-
assert.equal(m.rows[0]?.canClone, false)
|
|
508
|
-
assert.equal(m.rows[1]?.canClone, undefined)
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
it('itemCanReorder gates reorder controls per-row', async () => {
|
|
512
|
-
const f = builder()
|
|
513
|
-
.reorderable()
|
|
514
|
-
.itemCanReorder(({ values }) => (values as Record<string, unknown>)['text'] !== 'pinned')
|
|
515
|
-
const [raw] = await resolveSchema([f], {
|
|
516
|
-
values: { content: [
|
|
517
|
-
{ type: 'heading', data: { text: 'free' } },
|
|
518
|
-
{ type: 'heading', data: { text: 'pinned' } },
|
|
519
|
-
] },
|
|
520
|
-
})
|
|
521
|
-
const m = metaOf(raw)
|
|
522
|
-
assert.equal(m.rows[0]?.canReorder, undefined)
|
|
523
|
-
assert.equal(m.rows[1]?.canReorder, false)
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
it('async predicate is awaited', async () => {
|
|
527
|
-
const f = builder().itemCanDelete(async ({ row }) => {
|
|
528
|
-
await Promise.resolve()
|
|
529
|
-
return row?.index !== 0
|
|
530
|
-
})
|
|
531
|
-
const [raw] = await resolveSchema([f], {
|
|
532
|
-
values: { content: [
|
|
533
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
534
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
535
|
-
] },
|
|
536
|
-
})
|
|
537
|
-
const m = metaOf(raw)
|
|
538
|
-
assert.equal(m.rows[0]?.canDelete, false)
|
|
539
|
-
assert.equal(m.rows[1]?.canDelete, undefined)
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
it('throwing predicate → capability stays enabled (fail-open) + warns', async () => {
|
|
543
|
-
const original = console.warn
|
|
544
|
-
const warnings: unknown[][] = []
|
|
545
|
-
console.warn = (...args: unknown[]) => { warnings.push(args) }
|
|
546
|
-
try {
|
|
547
|
-
const f = builder().itemCanReorder(() => { throw new Error('boom') })
|
|
548
|
-
const [raw] = await resolveSchema([f], {
|
|
549
|
-
values: { content: [{ type: 'heading', data: { text: 'A' } }] },
|
|
550
|
-
})
|
|
551
|
-
const m = metaOf(raw)
|
|
552
|
-
assert.equal(m.rows[0]?.canReorder, undefined)
|
|
553
|
-
assert.ok(warnings.length >= 1, 'expected at least one warning')
|
|
554
|
-
assert.match(String(warnings[0]?.[0]), /itemCanReorder\(\) on Builder "content" threw/)
|
|
555
|
-
} finally {
|
|
556
|
-
console.warn = original
|
|
557
|
-
}
|
|
558
|
-
})
|
|
559
|
-
})
|
|
560
|
-
|
|
561
|
-
// Live-reactive gate flow mirrors Repeater's. The renderer-side sync (in
|
|
562
|
-
// `BuilderInput`) is unit-tested in `react/fields/syncRowGates.test.ts`.
|
|
563
|
-
describe('reactive `itemHidden / itemCan*` (Form.live re-resolve)', () => {
|
|
564
|
-
function builder() {
|
|
565
|
-
return BuilderField.make('content').blocks([
|
|
566
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
567
|
-
Block.make('paragraph').schema([TextField.make('body')]),
|
|
568
|
-
])
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function metaOf(m: unknown): BuilderFieldMeta {
|
|
572
|
-
return m as BuilderFieldMeta
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
it('a row\'s value flip re-evaluates its itemHidden on the next resolve', async () => {
|
|
576
|
-
const f = builder().itemHidden(({ values }) => (values as Record<string, unknown>)['text'] === 'skip')
|
|
577
|
-
|
|
578
|
-
const before = metaOf((await resolveSchema([f], {
|
|
579
|
-
values: { content: [{ type: 'heading', data: { text: 'keep' } }] },
|
|
580
|
-
}))[0])
|
|
581
|
-
assert.equal(before.rows[0]?.hidden, undefined)
|
|
582
|
-
|
|
583
|
-
const after = metaOf((await resolveSchema([f], {
|
|
584
|
-
values: { content: [{ type: 'heading', data: { text: 'skip' } }] },
|
|
585
|
-
}))[0])
|
|
586
|
-
assert.equal(after.rows[0]?.hidden, true)
|
|
587
|
-
})
|
|
588
|
-
|
|
589
|
-
it('a row-scoped value flip re-evaluates that row\'s itemCanDelete', async () => {
|
|
590
|
-
const f = builder().itemCanDelete(({ values }) => (values as Record<string, unknown>)['text'] !== 'locked')
|
|
591
|
-
|
|
592
|
-
const before = metaOf((await resolveSchema([f], {
|
|
593
|
-
values: { content: [{ type: 'heading', data: { text: 'free' } }] },
|
|
594
|
-
}))[0])
|
|
595
|
-
assert.equal(before.rows[0]?.canDelete, undefined)
|
|
596
|
-
|
|
597
|
-
const after = metaOf((await resolveSchema([f], {
|
|
598
|
-
values: { content: [{ type: 'heading', data: { text: 'locked' } }] },
|
|
599
|
-
}))[0])
|
|
600
|
-
assert.equal(after.rows[0]?.canDelete, false)
|
|
601
|
-
})
|
|
602
|
-
|
|
603
|
-
it('a row\'s value flip re-evaluates its itemLabel', async () => {
|
|
604
|
-
const f = builder().itemLabel((data) => String((data as Record<string, unknown>)['text'] ?? 'Untitled'))
|
|
605
|
-
|
|
606
|
-
const before = metaOf((await resolveSchema([f], {
|
|
607
|
-
values: { content: [{ type: 'heading', data: { text: 'Hello' } }] },
|
|
608
|
-
}))[0])
|
|
609
|
-
assert.equal(before.rows[0]?.itemLabel, 'Hello')
|
|
610
|
-
|
|
611
|
-
const after = metaOf((await resolveSchema([f], {
|
|
612
|
-
values: { content: [{ type: 'heading', data: { text: 'World' } }] },
|
|
613
|
-
}))[0])
|
|
614
|
-
assert.equal(after.rows[0]?.itemLabel, 'World')
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
it('a row\'s value flip re-evaluates its extraItemActions visibility', async () => {
|
|
618
|
-
const promote = Action.make('promote').label('Promote').visible(({ values }) => (values as Record<string, unknown>)?.['text'] === 'ready')
|
|
619
|
-
const f = builder().extraItemActions([promote])
|
|
620
|
-
|
|
621
|
-
const before = metaOf((await resolveSchema([f], {
|
|
622
|
-
values: { content: [{ type: 'heading', data: { text: 'draft' } }] },
|
|
623
|
-
}))[0])
|
|
624
|
-
assert.equal(before.rows[0]?.extraActions, undefined)
|
|
625
|
-
|
|
626
|
-
const after = metaOf((await resolveSchema([f], {
|
|
627
|
-
values: { content: [{ type: 'heading', data: { text: 'ready' } }] },
|
|
628
|
-
}))[0])
|
|
629
|
-
assert.equal(after.rows[0]?.extraActions?.length, 1)
|
|
630
|
-
})
|
|
631
|
-
})
|
|
632
|
-
|
|
633
|
-
// ─── Coercion ─────────────────────────────────────────────
|
|
634
|
-
|
|
635
|
-
describe('coerceFormValues', () => {
|
|
636
|
-
function builder() {
|
|
637
|
-
return BuilderField.make('content').blocks([
|
|
638
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
639
|
-
Block.make('counter').schema([NumberField.make('count'), ToggleField.make('on')]),
|
|
640
|
-
])
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
it('JSON shape — array round-trips with inner-field coercion', () => {
|
|
644
|
-
const out = coerceFormValues([builder()], {
|
|
645
|
-
content: [
|
|
646
|
-
{ __id: 'a', type: 'heading', data: { text: 'X' } },
|
|
647
|
-
{ type: 'counter', data: { count: '5', on: 'on' } },
|
|
648
|
-
],
|
|
649
|
-
})
|
|
650
|
-
assert.deepEqual(out['content'], [
|
|
651
|
-
{ __id: 'a', type: 'heading', data: { text: 'X' } },
|
|
652
|
-
{ type: 'counter', data: { count: 5, on: true } },
|
|
653
|
-
])
|
|
654
|
-
})
|
|
655
|
-
|
|
656
|
-
it('flat-key shape — folds into {type, data} envelopes', () => {
|
|
657
|
-
const out = coerceFormValues([builder()], {
|
|
658
|
-
'content.0.__id': 'row-a',
|
|
659
|
-
'content.0.type': 'heading',
|
|
660
|
-
'content.0.data.text': 'Hello',
|
|
661
|
-
'content.1.type': 'counter',
|
|
662
|
-
'content.1.data.count': '3',
|
|
663
|
-
'content.1.data.on': 'on',
|
|
664
|
-
})
|
|
665
|
-
assert.deepEqual(out['content'], [
|
|
666
|
-
{ __id: 'row-a', type: 'heading', data: { text: 'Hello' } },
|
|
667
|
-
{ type: 'counter', data: { count: 3, on: true } },
|
|
668
|
-
])
|
|
669
|
-
assert.equal(out['content.0.type'], undefined) // flat keys cleaned up
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
it('trailing empty rows trimmed (data with no entered values)', () => {
|
|
673
|
-
const out = coerceFormValues([builder()], {
|
|
674
|
-
content: [
|
|
675
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
676
|
-
{ type: 'heading', data: { text: '' } },
|
|
677
|
-
{ type: 'heading', data: { text: '' } },
|
|
678
|
-
],
|
|
679
|
-
})
|
|
680
|
-
// Trailing empties trimmed. The non-empty first row stays.
|
|
681
|
-
assert.equal((out['content'] as unknown[]).length, 1)
|
|
682
|
-
})
|
|
683
|
-
|
|
684
|
-
it('keeps a non-empty trailing row even with empty siblings', () => {
|
|
685
|
-
const out = coerceFormValues([builder()], {
|
|
686
|
-
content: [
|
|
687
|
-
{ type: 'heading', data: { text: '' } },
|
|
688
|
-
{ type: 'heading', data: { text: 'kept' } },
|
|
689
|
-
],
|
|
690
|
-
})
|
|
691
|
-
// Only trailing emptiness trims; the first empty row survives the trim.
|
|
692
|
-
assert.equal((out['content'] as unknown[]).length, 2)
|
|
693
|
-
})
|
|
694
|
-
|
|
695
|
-
it('unknown block type — data passes through verbatim', () => {
|
|
696
|
-
const out = coerceFormValues([builder()], {
|
|
697
|
-
content: [{ type: 'mysterious', data: { stuff: 'preserved' } }],
|
|
698
|
-
})
|
|
699
|
-
assert.deepEqual(out['content'], [
|
|
700
|
-
{ type: 'mysterious', data: { stuff: 'preserved' } },
|
|
701
|
-
])
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
it('non-object row entries → empty rows preserved at non-trailing positions', () => {
|
|
705
|
-
const out = coerceFormValues([builder()], {
|
|
706
|
-
content: [null, 'oops', { type: 'heading', data: { text: 'real' } }],
|
|
707
|
-
})
|
|
708
|
-
const arr = out['content'] as Array<Record<string, unknown>>
|
|
709
|
-
// Non-object entries normalize to `{ type: '', data: {} }`. Only
|
|
710
|
-
// trailing empties trim, so the two empties at positions 0 and 1
|
|
711
|
-
// survive and the real row anchors them.
|
|
712
|
-
assert.equal(arr.length, 3)
|
|
713
|
-
assert.equal(arr[0]?.['type'], '')
|
|
714
|
-
assert.equal(arr[2]?.['type'], 'heading')
|
|
715
|
-
})
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
// ─── Validation ───────────────────────────────────────────
|
|
719
|
-
|
|
720
|
-
describe('validateSchema', () => {
|
|
721
|
-
function builder() {
|
|
722
|
-
return BuilderField.make('content')
|
|
723
|
-
.blocks([
|
|
724
|
-
Block.make('heading').schema([TextField.make('text').required()]),
|
|
725
|
-
Block.make('paragraph').schema([TextField.make('body').required()]),
|
|
726
|
-
])
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
it('inner required → error keyed name.<i>.data.<child>', async () => {
|
|
730
|
-
const errors = await validateSchema([builder()], {
|
|
731
|
-
content: [{ type: 'heading', data: { text: '' } }],
|
|
732
|
-
})
|
|
733
|
-
assert.ok(errors['content.0.data.text'])
|
|
734
|
-
assert.equal(isValid(errors), false)
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
it('minItems violated → bare-key error', async () => {
|
|
738
|
-
const errors = await validateSchema([builder().minItems(2)], {
|
|
739
|
-
content: [{ type: 'heading', data: { text: 'A' } }],
|
|
740
|
-
})
|
|
741
|
-
assert.ok(errors['content']?.some(e => e.includes('At least 2')))
|
|
742
|
-
})
|
|
743
|
-
|
|
744
|
-
it('maxItems violated → bare-key error', async () => {
|
|
745
|
-
const errors = await validateSchema([builder().maxItems(1)], {
|
|
746
|
-
content: [
|
|
747
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
748
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
749
|
-
],
|
|
750
|
-
})
|
|
751
|
-
assert.ok(errors['content']?.some(e => e.includes('At most 1')))
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
it('Block.maxItems violated → bare-key error mentioning block label', async () => {
|
|
755
|
-
const f = BuilderField.make('content').blocks([
|
|
756
|
-
Block.make('hero').label('Hero').maxItems(1).schema([TextField.make('h')]),
|
|
757
|
-
Block.make('p').schema([TextField.make('b')]),
|
|
758
|
-
])
|
|
759
|
-
const errors = await validateSchema([f], {
|
|
760
|
-
content: [
|
|
761
|
-
{ type: 'hero', data: { h: 'A' } },
|
|
762
|
-
{ type: 'hero', data: { h: 'B' } },
|
|
763
|
-
],
|
|
764
|
-
})
|
|
765
|
-
assert.ok(errors['content']?.some(e => e.includes('"Hero"')))
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
it('unknown block type → row-level error', async () => {
|
|
769
|
-
const errors = await validateSchema([builder()], {
|
|
770
|
-
content: [{ type: 'phantom', data: {} }],
|
|
771
|
-
})
|
|
772
|
-
assert.ok(errors['content.0']?.some(e => e.includes('Unknown')))
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
it('missing block type → row-level error', async () => {
|
|
776
|
-
const errors = await validateSchema([builder()], {
|
|
777
|
-
content: [{ type: '', data: {} }],
|
|
778
|
-
})
|
|
779
|
-
assert.ok(errors['content.0']?.some(e => e.includes('required')))
|
|
780
|
-
})
|
|
781
|
-
|
|
782
|
-
it('flat-key body folds + validates non-empty rows', async () => {
|
|
783
|
-
// Non-empty data anchors the row past the trailing-empty trim, so
|
|
784
|
-
// validation can run against it. (Trim semantics match Repeater:
|
|
785
|
-
// a row with no entered values is treated as never-created.)
|
|
786
|
-
const errors = await validateSchema([builder()], {
|
|
787
|
-
'content.0.type': 'paragraph',
|
|
788
|
-
'content.0.data.body': '', // touched-but-empty after sibling fills
|
|
789
|
-
// Anchor the row by adding a non-empty key inside data
|
|
790
|
-
'content.0.data.body.touched': 'x',
|
|
791
|
-
})
|
|
792
|
-
// The fold puts `body` AND `body.touched` into data — the latter
|
|
793
|
-
// is treated as a literal string key. The row is non-empty.
|
|
794
|
-
// Required validation fires for body.
|
|
795
|
-
assert.ok(errors['content.0.data.body'])
|
|
796
|
-
})
|
|
797
|
-
})
|
|
798
|
-
|
|
799
|
-
describe('distinct() — cross-row uniqueness (per block type)', () => {
|
|
800
|
-
function distinctBuilder() {
|
|
801
|
-
return BuilderField.make('content').blocks([
|
|
802
|
-
Block.make('heading').schema([TextField.make('text').distinct()]),
|
|
803
|
-
Block.make('paragraph').schema([TextField.make('body').distinct()]),
|
|
804
|
-
])
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
it('all-unique rows produce no error', async () => {
|
|
808
|
-
const errors = await validateSchema([distinctBuilder()], {
|
|
809
|
-
content: [
|
|
810
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
811
|
-
{ type: 'heading', data: { text: 'B' } },
|
|
812
|
-
{ type: 'paragraph', data: { body: 'A' } },
|
|
813
|
-
],
|
|
814
|
-
})
|
|
815
|
-
assert.equal(isValid(errors), true)
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
it('duplicate within the same block flags the second occurrence', async () => {
|
|
819
|
-
const errors = await validateSchema([distinctBuilder()], {
|
|
820
|
-
content: [
|
|
821
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
822
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
823
|
-
],
|
|
824
|
-
})
|
|
825
|
-
assert.equal('content.0.data.text' in errors, false)
|
|
826
|
-
assert.deepEqual(errors['content.1.data.text'], ['Must be unique'])
|
|
827
|
-
})
|
|
828
|
-
|
|
829
|
-
it('comparison is scoped to same block type — different blocks with the same field-name value never conflict', async () => {
|
|
830
|
-
// Both schemas happen to have a `text` field marked distinct (renamed
|
|
831
|
-
// here so the assertion is explicit). A heading block's value should
|
|
832
|
-
// never be compared against a paragraph block's value.
|
|
833
|
-
const f = BuilderField.make('content').blocks([
|
|
834
|
-
Block.make('heading').schema([TextField.make('val').distinct()]),
|
|
835
|
-
Block.make('paragraph').schema([TextField.make('val').distinct()]),
|
|
836
|
-
])
|
|
837
|
-
const errors = await validateSchema([f], {
|
|
838
|
-
content: [
|
|
839
|
-
{ type: 'heading', data: { val: 'A' } },
|
|
840
|
-
{ type: 'paragraph', data: { val: 'A' } }, // same value, different block
|
|
841
|
-
],
|
|
842
|
-
})
|
|
843
|
-
assert.equal(isValid(errors), true)
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
it('non-contiguous rows of the same type still conflict', async () => {
|
|
847
|
-
// heading(A) at idx 0, paragraph at idx 1, heading(A) at idx 2 → idx 2 fails.
|
|
848
|
-
const errors = await validateSchema([distinctBuilder()], {
|
|
849
|
-
content: [
|
|
850
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
851
|
-
{ type: 'paragraph', data: { body: 'B' } },
|
|
852
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
853
|
-
],
|
|
854
|
-
})
|
|
855
|
-
assert.deepEqual(errors['content.2.data.text'], ['Must be unique'])
|
|
856
|
-
})
|
|
857
|
-
|
|
858
|
-
it('caseInsensitive folds case before comparing', async () => {
|
|
859
|
-
const f = BuilderField.make('content').blocks([
|
|
860
|
-
Block.make('heading').schema([TextField.make('text').distinct({ caseInsensitive: true })]),
|
|
861
|
-
])
|
|
862
|
-
const errors = await validateSchema([f], {
|
|
863
|
-
content: [
|
|
864
|
-
{ type: 'heading', data: { text: 'Foo' } },
|
|
865
|
-
{ type: 'heading', data: { text: 'foo' } },
|
|
866
|
-
],
|
|
867
|
-
})
|
|
868
|
-
assert.deepEqual(errors['content.1.data.text'], ['Must be unique'])
|
|
869
|
-
})
|
|
870
|
-
|
|
871
|
-
it('default ignoreNulls=true skips empty values', async () => {
|
|
872
|
-
const errors = await validateSchema([distinctBuilder()], {
|
|
873
|
-
content: [
|
|
874
|
-
{ type: 'heading', data: { text: '' } },
|
|
875
|
-
{ type: 'heading', data: { text: '' } },
|
|
876
|
-
],
|
|
877
|
-
})
|
|
878
|
-
// The required-style check isn't on this field, so empty rows pass.
|
|
879
|
-
assert.equal(isValid(errors), true)
|
|
880
|
-
})
|
|
881
|
-
|
|
882
|
-
it('custom message overrides the default', async () => {
|
|
883
|
-
const f = BuilderField.make('content').blocks([
|
|
884
|
-
Block.make('heading').schema([
|
|
885
|
-
TextField.make('text').distinct({ message: 'Each heading text must be unique' }),
|
|
886
|
-
]),
|
|
887
|
-
])
|
|
888
|
-
const errors = await validateSchema([f], {
|
|
889
|
-
content: [
|
|
890
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
891
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
892
|
-
],
|
|
893
|
-
})
|
|
894
|
-
assert.deepEqual(errors['content.1.data.text'], ['Each heading text must be unique'])
|
|
895
|
-
})
|
|
896
|
-
|
|
897
|
-
it('unknown block rows are skipped (no crash, no false dup)', async () => {
|
|
898
|
-
const errors = await validateSchema([distinctBuilder()], {
|
|
899
|
-
content: [
|
|
900
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
901
|
-
{ type: 'phantom', data: { text: 'A' } }, // unknown block type
|
|
902
|
-
],
|
|
903
|
-
})
|
|
904
|
-
// Row 1 produces an "Unknown block type" error but no distinct error.
|
|
905
|
-
assert.ok(errors['content.1']?.some(e => e.includes('Unknown')))
|
|
906
|
-
assert.equal('content.1.data.text' in errors, false)
|
|
907
|
-
})
|
|
908
|
-
})
|
|
909
|
-
|
|
910
|
-
// ─── Live re-resolve (applyStateUpdate) ──────────────────
|
|
911
|
-
|
|
912
|
-
describe('applyStateUpdate (dotted path)', () => {
|
|
913
|
-
function form() {
|
|
914
|
-
return Form.make().schema([
|
|
915
|
-
BuilderField.make('content').blocks([
|
|
916
|
-
Block.make('heading').schema([TextField.make('text').live()]),
|
|
917
|
-
Block.make('counter').schema([NumberField.make('count').live()]),
|
|
918
|
-
]),
|
|
919
|
-
])
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
it('updates the leaf field at the dotted path', async () => {
|
|
923
|
-
const f = form()
|
|
924
|
-
const result = await applyStateUpdate(
|
|
925
|
-
f,
|
|
926
|
-
{ content: [{ type: 'heading', data: { text: 'Hello' } }] },
|
|
927
|
-
'content.0.data.text',
|
|
928
|
-
)
|
|
929
|
-
assert.notEqual(result, null)
|
|
930
|
-
const root = result!.values['content'] as Array<Record<string, unknown>>
|
|
931
|
-
const data = root[0]!['data'] as Record<string, unknown>
|
|
932
|
-
assert.equal(data['text'], 'Hello')
|
|
933
|
-
assert.deepEqual(result!.dirty, ['content.0.data.text'])
|
|
934
|
-
})
|
|
935
|
-
|
|
936
|
-
it('coerces the leaf only — sibling values untouched', async () => {
|
|
937
|
-
const f = form()
|
|
938
|
-
const result = await applyStateUpdate(
|
|
939
|
-
f,
|
|
940
|
-
{ content: [{ type: 'counter', data: { count: '7' } }] },
|
|
941
|
-
'content.0.data.count',
|
|
942
|
-
)
|
|
943
|
-
assert.notEqual(result, null)
|
|
944
|
-
const root = result!.values['content'] as Array<Record<string, unknown>>
|
|
945
|
-
const data = root[0]!['data'] as Record<string, unknown>
|
|
946
|
-
assert.equal(data['count'], 7)
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
it('routes by row.type, not field — wrong-type leaf 404s', async () => {
|
|
950
|
-
const f = form()
|
|
951
|
-
const result = await applyStateUpdate(
|
|
952
|
-
f,
|
|
953
|
-
{ content: [{ type: 'heading', data: {} }] },
|
|
954
|
-
'content.0.data.count', // 'count' belongs to counter, not heading
|
|
955
|
-
)
|
|
956
|
-
assert.equal(result, null)
|
|
957
|
-
})
|
|
958
|
-
|
|
959
|
-
it('returns null for unsupported nested array-row paths', async () => {
|
|
960
|
-
const f = form()
|
|
961
|
-
const result = await applyStateUpdate(
|
|
962
|
-
f,
|
|
963
|
-
{ content: [{ type: 'heading', data: {} }] },
|
|
964
|
-
'content.0.data.text.0.something',
|
|
965
|
-
)
|
|
966
|
-
assert.equal(result, null)
|
|
967
|
-
})
|
|
968
|
-
|
|
969
|
-
it('afterStateUpdated receives row-scoped $get + ctx.row.blockType', async () => {
|
|
970
|
-
let blockTypeSeen: string | undefined
|
|
971
|
-
let rowTextSeen: unknown
|
|
972
|
-
const f = Form.make().schema([
|
|
973
|
-
BuilderField.make('content').blocks([
|
|
974
|
-
Block.make('heading').schema([
|
|
975
|
-
TextField.make('text').live(),
|
|
976
|
-
TextField.make('subtitle').live().afterStateUpdated((value, ctx) => {
|
|
977
|
-
void value
|
|
978
|
-
if (ctx.row) {
|
|
979
|
-
blockTypeSeen = ctx.row.blockType
|
|
980
|
-
rowTextSeen = ctx.row.$get('text')
|
|
981
|
-
}
|
|
982
|
-
}),
|
|
983
|
-
]),
|
|
984
|
-
]),
|
|
985
|
-
])
|
|
986
|
-
await applyStateUpdate(
|
|
987
|
-
f,
|
|
988
|
-
{ content: [{ type: 'heading', data: { text: 'Existing', subtitle: 'New' } }] },
|
|
989
|
-
'content.0.data.subtitle',
|
|
990
|
-
)
|
|
991
|
-
assert.equal(blockTypeSeen, 'heading')
|
|
992
|
-
assert.equal(rowTextSeen, 'Existing')
|
|
993
|
-
})
|
|
994
|
-
})
|
|
995
|
-
|
|
996
|
-
// ─── Walkers stop at Builder boundary ────────────────────
|
|
997
|
-
|
|
998
|
-
describe('walker boundaries', () => {
|
|
999
|
-
it('findForms does not recurse into Builder rows', () => {
|
|
1000
|
-
const inner = Form.make().schema([TextField.make('inner')])
|
|
1001
|
-
const outer = Form.make().schema([
|
|
1002
|
-
BuilderField.make('content').blocks([
|
|
1003
|
-
Block.make('h').schema([inner as unknown as TextField]), // intentional misuse
|
|
1004
|
-
]),
|
|
1005
|
-
])
|
|
1006
|
-
const forms = findForms([outer])
|
|
1007
|
-
assert.equal(forms.length, 1)
|
|
1008
|
-
assert.equal(forms[0], outer)
|
|
1009
|
-
})
|
|
1010
|
-
|
|
1011
|
-
it('findActions does not recurse into Builder rows', () => {
|
|
1012
|
-
const innerAction = Action.make('inner-action').handler(() => {})
|
|
1013
|
-
const outer = BuilderField.make('content').blocks([
|
|
1014
|
-
Block.make('h').schema([innerAction as unknown as TextField]),
|
|
1015
|
-
])
|
|
1016
|
-
assert.deepEqual(findActions([outer]), [])
|
|
1017
|
-
})
|
|
1018
|
-
|
|
1019
|
-
it('findTables does not recurse into Builder rows', () => {
|
|
1020
|
-
const innerTable = Table.make().columns([])
|
|
1021
|
-
const outer = BuilderField.make('content').blocks([
|
|
1022
|
-
Block.make('h').schema([innerTable as unknown as TextField]),
|
|
1023
|
-
])
|
|
1024
|
-
assert.deepEqual(findTables([outer]), [])
|
|
1025
|
-
})
|
|
1026
|
-
|
|
1027
|
-
it('isBuilderField structural check', () => {
|
|
1028
|
-
assert.equal(isBuilderField(BuilderField.make('x')), true)
|
|
1029
|
-
assert.equal(isBuilderField(RepeaterField.make('x')), false)
|
|
1030
|
-
assert.equal(isBuilderField(TextField.make('x')), false)
|
|
1031
|
-
assert.equal(isBuilderField(Section.make('x')), false)
|
|
1032
|
-
})
|
|
1033
|
-
|
|
1034
|
-
it('isBuilderField narrow check passes structural shape (Vite SSR safety)', () => {
|
|
1035
|
-
// Simulates a duplicate-module-cache copy: a plain object whose
|
|
1036
|
-
// discriminators match. The structural check should still catch it.
|
|
1037
|
-
const fake = { getType: () => 'field', fieldType: 'builder' }
|
|
1038
|
-
assert.equal(isBuilderField(fake), true)
|
|
1039
|
-
})
|
|
1040
|
-
})
|
|
1041
|
-
|
|
1042
|
-
describe('extraItemActions (per-row buttons)', () => {
|
|
1043
|
-
it('builder stores extra actions; getter returns them', () => {
|
|
1044
|
-
const a = Action.make('promote').handler(() => undefined)
|
|
1045
|
-
const f = BuilderField.make('content').extraItemActions([a])
|
|
1046
|
-
assert.deepEqual(f.getExtraItemActions(), [a])
|
|
1047
|
-
})
|
|
1048
|
-
|
|
1049
|
-
it('per-row resolve stamps extraActions on each row', async () => {
|
|
1050
|
-
const promote = Action.make('promote').handler(() => undefined)
|
|
1051
|
-
const f = BuilderField.make('content')
|
|
1052
|
-
.blocks([
|
|
1053
|
-
Block.make('heading').schema([TextField.make('text')]),
|
|
1054
|
-
])
|
|
1055
|
-
.extraItemActions([promote])
|
|
1056
|
-
|
|
1057
|
-
const [meta] = await resolveSchema([f], { values: { content: [
|
|
1058
|
-
{ type: 'heading', data: { text: 'A' } },
|
|
1059
|
-
{ type: 'heading', data: { text: 'B' } },
|
|
1060
|
-
] } })
|
|
1061
|
-
const builder = meta as BuilderFieldMeta
|
|
1062
|
-
assert.equal(builder.rows.length, 2)
|
|
1063
|
-
assert.equal(builder.rows[0]!.extraActions?.length, 1)
|
|
1064
|
-
assert.equal(builder.rows[1]!.extraActions?.length, 1)
|
|
1065
|
-
})
|
|
1066
|
-
|
|
1067
|
-
it('predicate sees row data via ctx.values', async () => {
|
|
1068
|
-
const seen: Array<Record<string, unknown> | undefined> = []
|
|
1069
|
-
const a = Action.make('a')
|
|
1070
|
-
.handler(() => undefined)
|
|
1071
|
-
.visible(({ values }) => { seen.push(values); return true })
|
|
1072
|
-
const f = BuilderField.make('content')
|
|
1073
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1074
|
-
.extraItemActions([a])
|
|
1075
|
-
|
|
1076
|
-
await resolveSchema([f], { values: { content: [
|
|
1077
|
-
{ type: 'heading', data: { text: 'foo' } },
|
|
1078
|
-
] } })
|
|
1079
|
-
assert.deepEqual(seen, [{ text: 'foo' }])
|
|
1080
|
-
})
|
|
1081
|
-
|
|
1082
|
-
it('unknown-type rows skip extraActions resolve (no row context)', async () => {
|
|
1083
|
-
const promote = Action.make('promote').handler(() => undefined)
|
|
1084
|
-
const f = BuilderField.make('content')
|
|
1085
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1086
|
-
.extraItemActions([promote])
|
|
1087
|
-
|
|
1088
|
-
const [meta] = await resolveSchema([f], { values: { content: [
|
|
1089
|
-
{ type: 'mystery', data: { foo: 'bar' } },
|
|
1090
|
-
] } })
|
|
1091
|
-
const builder = meta as BuilderFieldMeta
|
|
1092
|
-
assert.equal(builder.rows[0]!.unknownType, true)
|
|
1093
|
-
assert.equal(builder.rows[0]!.extraActions, undefined)
|
|
1094
|
-
})
|
|
1095
|
-
})
|
|
1096
|
-
|
|
1097
|
-
describe('relationship(...) — setter / getter / meta', () => {
|
|
1098
|
-
it('string form stores the relationship name', () => {
|
|
1099
|
-
const f = BuilderField.make('content')
|
|
1100
|
-
.relationship('blocks')
|
|
1101
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1102
|
-
const cfg = f.getRelationship()
|
|
1103
|
-
assert.equal(f.isRelationship(), true)
|
|
1104
|
-
assert.equal(cfg?.name, 'blocks')
|
|
1105
|
-
assert.equal(cfg?.model, undefined)
|
|
1106
|
-
assert.equal(cfg?.foreignKey, undefined)
|
|
1107
|
-
assert.equal(cfg?.typeColumn, undefined)
|
|
1108
|
-
assert.equal(cfg?.dataColumn, undefined)
|
|
1109
|
-
assert.equal(cfg?.orderColumn, undefined)
|
|
1110
|
-
})
|
|
1111
|
-
|
|
1112
|
-
it('object form copies all explicit overrides verbatim', () => {
|
|
1113
|
-
const f = BuilderField.make('content')
|
|
1114
|
-
.relationship({
|
|
1115
|
-
name: 'blocks',
|
|
1116
|
-
foreignKey: 'pageId',
|
|
1117
|
-
typeColumn: 'kind',
|
|
1118
|
-
dataColumn: 'payload',
|
|
1119
|
-
orderColumn: 'sort',
|
|
1120
|
-
})
|
|
1121
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1122
|
-
const cfg = f.getRelationship()
|
|
1123
|
-
assert.equal(cfg?.name, 'blocks')
|
|
1124
|
-
assert.equal(cfg?.foreignKey, 'pageId')
|
|
1125
|
-
assert.equal(cfg?.typeColumn, 'kind')
|
|
1126
|
-
assert.equal(cfg?.dataColumn, 'payload')
|
|
1127
|
-
assert.equal(cfg?.orderColumn, 'sort')
|
|
1128
|
-
})
|
|
1129
|
-
|
|
1130
|
-
it('orderColumn() sugar sets the order column when relationship is configured', () => {
|
|
1131
|
-
const f = BuilderField.make('content')
|
|
1132
|
-
.relationship('blocks')
|
|
1133
|
-
.orderColumn('sort')
|
|
1134
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1135
|
-
assert.equal(f.getRelationship()?.orderColumn, 'sort')
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
it('orderColumn() throws when relationship() not called first', () => {
|
|
1139
|
-
assert.throws(
|
|
1140
|
-
() => BuilderField.make('content').orderColumn('sort'),
|
|
1141
|
-
/requires relationship\(\) to be configured first/,
|
|
1142
|
-
)
|
|
1143
|
-
})
|
|
1144
|
-
|
|
1145
|
-
it('relationship() is incompatible with dehydrated(false)', () => {
|
|
1146
|
-
assert.throws(
|
|
1147
|
-
() => BuilderField.make('content').dehydrated(false).relationship('blocks'),
|
|
1148
|
-
/incompatible with dehydrated\(false\)/,
|
|
1149
|
-
)
|
|
1150
|
-
})
|
|
1151
|
-
|
|
1152
|
-
it('toMeta serializes relationship under meta.relationship — only name when no overrides', () => {
|
|
1153
|
-
const meta = BuilderField.make('content')
|
|
1154
|
-
.relationship('blocks')
|
|
1155
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1156
|
-
.toMeta() as BuilderFieldMeta
|
|
1157
|
-
assert.deepEqual(meta.relationship, { name: 'blocks' })
|
|
1158
|
-
})
|
|
1159
|
-
|
|
1160
|
-
it('toMeta omits server-only model + foreignKey, preserves typeColumn / dataColumn / orderColumn', () => {
|
|
1161
|
-
const meta = BuilderField.make('content')
|
|
1162
|
-
.relationship({
|
|
1163
|
-
name: 'blocks',
|
|
1164
|
-
foreignKey: 'pageId',
|
|
1165
|
-
typeColumn: 'kind',
|
|
1166
|
-
dataColumn: 'payload',
|
|
1167
|
-
orderColumn: 'sort',
|
|
1168
|
-
})
|
|
1169
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1170
|
-
.toMeta() as BuilderFieldMeta
|
|
1171
|
-
assert.deepEqual(meta.relationship, {
|
|
1172
|
-
name: 'blocks',
|
|
1173
|
-
typeColumn: 'kind',
|
|
1174
|
-
dataColumn: 'payload',
|
|
1175
|
-
orderColumn: 'sort',
|
|
1176
|
-
})
|
|
1177
|
-
assert.equal('model' in (meta.relationship as object), false)
|
|
1178
|
-
assert.equal('foreignKey' in (meta.relationship as object), false)
|
|
1179
|
-
})
|
|
1180
|
-
|
|
1181
|
-
it('toMeta omits relationship key entirely when not configured', () => {
|
|
1182
|
-
const meta = BuilderField.make('content')
|
|
1183
|
-
.blocks([Block.make('heading').schema([TextField.make('text')])])
|
|
1184
|
-
.toMeta() as BuilderFieldMeta
|
|
1185
|
-
assert.equal(meta.relationship, undefined)
|
|
1186
|
-
})
|
|
1187
|
-
})
|
|
1188
|
-
})
|