@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
|
@@ -1,1420 +0,0 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { PlusIcon } from 'lucide-react'
|
|
3
|
-
import type { ElementMeta } from '../../schema/Element.js'
|
|
4
|
-
import { Button } from '../ui/button.js'
|
|
5
|
-
import { SchemaRenderer, dispatchHandlerAction } from '../SchemaRenderer.js'
|
|
6
|
-
import { FormIdContext, useFormState, useRowBinding } from '../FormStateContext.js'
|
|
7
|
-
import { findFieldMeta } from '../formStateHelpers.js'
|
|
8
|
-
import { RowCoordsContext } from '../RowCoordsContext.js'
|
|
9
|
-
import { useNavigate } from '../navigate.js'
|
|
10
|
-
import { useToast } from '../Toaster.js'
|
|
11
|
-
import type { RowButtonsMeta } from '../../fields/RowButton.js'
|
|
12
|
-
import {
|
|
13
|
-
RowChromeIconButton,
|
|
14
|
-
ReorderGrip,
|
|
15
|
-
CollapseChevron,
|
|
16
|
-
BulkCollapseHeader,
|
|
17
|
-
resolveRowChrome,
|
|
18
|
-
DEFAULT_MOVE_UP,
|
|
19
|
-
DEFAULT_MOVE_DOWN,
|
|
20
|
-
DEFAULT_CLONE,
|
|
21
|
-
DEFAULT_DELETE,
|
|
22
|
-
} from './rowChromeButton.js'
|
|
23
|
-
import { syncRowGates } from './syncRowGates.js'
|
|
24
|
-
import { consumeReconcileFlag, computeReconcilePlan } from './repeaterReconcile.js'
|
|
25
|
-
import {
|
|
26
|
-
generateRowId, makeAccordionStorage, makeCollapsedStorage,
|
|
27
|
-
} from './rowState.js'
|
|
28
|
-
import { useRowReorderDnd } from './useRowReorderDnd.js'
|
|
29
|
-
|
|
30
|
-
const collapsedStorage = makeCollapsedStorage('repeater')
|
|
31
|
-
const accordionStorage = makeAccordionStorage('repeater')
|
|
32
|
-
const initSeedCollapsed = collapsedStorage.seed
|
|
33
|
-
const writeCollapsedToStorage = collapsedStorage.write
|
|
34
|
-
const deleteCollapsedFromStorage = collapsedStorage.remove
|
|
35
|
-
const readAccordionFromStorage = accordionStorage.read
|
|
36
|
-
const writeAccordionToStorage = accordionStorage.write
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Pure reorder helper — used by both the HTML5 DnD path and the
|
|
40
|
-
* Up/Down button path. `insertBeforeIdx` is the boundary the dragged
|
|
41
|
-
* row should land at (range `0..rows.length`); after removing the
|
|
42
|
-
* source we adjust by -1 when the source sat below the target so the
|
|
43
|
-
* caller never has to think about post-removal index shifts.
|
|
44
|
-
*
|
|
45
|
-
* No-ops when the move would leave the array unchanged.
|
|
46
|
-
*/
|
|
47
|
-
export function reorderRows<T>(rows: T[], fromIdx: number, insertBeforeIdx: number): T[] {
|
|
48
|
-
if (fromIdx < 0 || fromIdx >= rows.length) return rows
|
|
49
|
-
if (insertBeforeIdx < 0 || insertBeforeIdx > rows.length) return rows
|
|
50
|
-
if (fromIdx === insertBeforeIdx || fromIdx + 1 === insertBeforeIdx) return rows
|
|
51
|
-
const next = rows.slice()
|
|
52
|
-
const moved = next.splice(fromIdx, 1)[0] as T
|
|
53
|
-
const target = insertBeforeIdx > fromIdx ? insertBeforeIdx - 1 : insertBeforeIdx
|
|
54
|
-
next.splice(target, 0, moved)
|
|
55
|
-
return next
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Tailwind v4 default breakpoint widths. Reused as container-query
|
|
60
|
-
* thresholds so authors can think in the same `sm/md/lg/xl/2xl` ladder
|
|
61
|
-
* regardless of viewport vs. container framing — at viewport width these
|
|
62
|
-
* fire identically to the corresponding `@media` queries.
|
|
63
|
-
*/
|
|
64
|
-
const RESPONSIVE_GRID_BREAKPOINTS: Array<{ key: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; min: string }> = [
|
|
65
|
-
{ key: 'sm', min: '40rem' },
|
|
66
|
-
{ key: 'md', min: '48rem' },
|
|
67
|
-
{ key: 'lg', min: '64rem' },
|
|
68
|
-
{ key: 'xl', min: '80rem' },
|
|
69
|
-
{ key: '2xl', min: '96rem' },
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Compute the grid container's className / style / scoped-CSS block from
|
|
74
|
-
* a `meta.grid` value. Shared between Repeater and Builder so both render
|
|
75
|
-
* responsive grids identically.
|
|
76
|
-
*
|
|
77
|
-
* - `meta.grid` undefined → `{ hasGrid: false }` and the caller falls back
|
|
78
|
-
* to a vertical flex stack.
|
|
79
|
-
* - Number form (`grid(2)`) → inline `gridTemplateColumns: repeat(N, …)`.
|
|
80
|
-
* - Object form (`grid({ default: 1, md: 2 })`) → a fresh `<style>` block
|
|
81
|
-
* keyed off `scopeId` + a matching className on the container; default
|
|
82
|
-
* columns drive the base rule, each declared breakpoint adds a
|
|
83
|
-
* `@container` query override.
|
|
84
|
-
*
|
|
85
|
-
* Responsive mode requires a CQ context — the caller paints
|
|
86
|
-
* `wrapperStyle` (`container-type: inline-size`) on the outer wrapper so
|
|
87
|
-
* the grid's `@container` rules resolve against the Repeater's
|
|
88
|
-
* parent-allocated width, not the viewport. A 2-column Repeater dropped
|
|
89
|
-
* into a `Split` aside therefore folds back to 1 column once the aside
|
|
90
|
-
* is narrower than `md`, even on a wide screen.
|
|
91
|
-
*
|
|
92
|
-
* `scopeId` should be a stable per-field identifier (we use `useId()` from
|
|
93
|
-
* React — already isolated to this render's component instance).
|
|
94
|
-
*/
|
|
95
|
-
export function buildGridContainer(
|
|
96
|
-
grid: number | Record<string, number | undefined> | undefined,
|
|
97
|
-
scopeId: string,
|
|
98
|
-
): {
|
|
99
|
-
hasGrid: boolean
|
|
100
|
-
className: string
|
|
101
|
-
style: React.CSSProperties | undefined
|
|
102
|
-
styleBlock: React.ReactElement | null
|
|
103
|
-
wrapperStyle: React.CSSProperties | undefined
|
|
104
|
-
} {
|
|
105
|
-
if (grid === undefined) {
|
|
106
|
-
return { hasGrid: false, className: 'flex flex-col gap-3', style: undefined, styleBlock: null, wrapperStyle: undefined }
|
|
107
|
-
}
|
|
108
|
-
if (typeof grid === 'number') {
|
|
109
|
-
return {
|
|
110
|
-
hasGrid: true,
|
|
111
|
-
className: 'grid gap-3',
|
|
112
|
-
style: { gridTemplateColumns: `repeat(${grid}, minmax(0, 1fr))` },
|
|
113
|
-
styleBlock: null,
|
|
114
|
-
wrapperStyle: undefined,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const cls = `pq-grid-${scopeId.replace(/:/g, '')}`
|
|
118
|
-
const baseCols = typeof grid['default'] === 'number' ? grid['default'] : 1
|
|
119
|
-
const rules: string[] = [
|
|
120
|
-
`.${cls} { display: grid; gap: 0.75rem; grid-template-columns: repeat(${baseCols}, minmax(0, 1fr)); }`,
|
|
121
|
-
]
|
|
122
|
-
for (const bp of RESPONSIVE_GRID_BREAKPOINTS) {
|
|
123
|
-
const cols = grid[bp.key]
|
|
124
|
-
if (typeof cols !== 'number') continue
|
|
125
|
-
rules.push(`@container (min-width: ${bp.min}) { .${cls} { grid-template-columns: repeat(${cols}, minmax(0, 1fr)); } }`)
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
hasGrid: true,
|
|
129
|
-
className: cls,
|
|
130
|
-
style: undefined,
|
|
131
|
-
styleBlock: <style>{rules.join('\n')}</style>,
|
|
132
|
-
wrapperStyle: { containerType: 'inline-size' },
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interface RowState {
|
|
137
|
-
id: string
|
|
138
|
-
children: ElementMeta[]
|
|
139
|
-
itemLabel?: string
|
|
140
|
-
hidden?: boolean
|
|
141
|
-
extraActions?: ElementMeta[]
|
|
142
|
-
// Per-row capability flags from `itemCan*(rule)`. Stamped only when the
|
|
143
|
-
// rule resolved falsy server-side; default ("allowed") leaves them unset.
|
|
144
|
-
// Cloned rows inherit nothing (a fresh row had no rule evaluation pass).
|
|
145
|
-
canDelete?: false
|
|
146
|
-
canClone?: false
|
|
147
|
-
canReorder?: false
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Repeater renderer (Plan #14).
|
|
152
|
-
*
|
|
153
|
-
* Rows are managed as local React state with stable `id` keys so
|
|
154
|
-
* uncontrolled inner inputs preserve their typed values across
|
|
155
|
-
* add/remove/reorder operations. Each row's resolved children meta is
|
|
156
|
-
* deep-cloned with a row-scoped prefix on every Field's `name` so
|
|
157
|
-
* submitted form bodies are flat-keyed (`items.0.product`, etc.) — the
|
|
158
|
-
* server's `coerceFormValues` re-groups them into an array.
|
|
159
|
-
*
|
|
160
|
-
* Reorder: native HTML5 drag-and-drop on each row, with a 2px drop
|
|
161
|
-
* indicator showing where the row will land. Up/Down buttons are kept
|
|
162
|
-
* as a keyboard fallback. Both paths route through `reorderRows()` so
|
|
163
|
-
* behavior is identical. Collapsed state persists per-row to
|
|
164
|
-
* `localStorage` under `pilotiq.repeater.<formId>.<fieldName>.<rowId>`
|
|
165
|
-
* when collapsible.
|
|
166
|
-
*
|
|
167
|
-
* Inner-field reactivity: this component does NOT integrate with
|
|
168
|
-
* `FormStateProvider` for nested-path live updates; that surgery is
|
|
169
|
-
* tracked separately. Repeaters with `live()` inner fields render
|
|
170
|
-
* today but the `live` trigger doesn't roundtrip.
|
|
171
|
-
*/
|
|
172
|
-
export function RepeaterInput({
|
|
173
|
-
el,
|
|
174
|
-
name,
|
|
175
|
-
disabled,
|
|
176
|
-
}: {
|
|
177
|
-
el: ElementMeta
|
|
178
|
-
name: string
|
|
179
|
-
disabled: boolean
|
|
180
|
-
}): React.ReactElement {
|
|
181
|
-
// The parent <form>'s id, scoped via context. Falls back to the field
|
|
182
|
-
// name when no Form is in scope (defensive — Repeaters always render
|
|
183
|
-
// inside a Form on real pages, but Storybook / unit tests can mount
|
|
184
|
-
// them bare).
|
|
185
|
-
const formIdFromCtx = useContext(FormIdContext)
|
|
186
|
-
const formId = formIdFromCtx || `repeater-${name}`
|
|
187
|
-
const meta = el as RepeaterMetaShape
|
|
188
|
-
const minItems = typeof meta.minItems === 'number' ? meta.minItems : undefined
|
|
189
|
-
const maxItems = typeof meta.maxItems === 'number' ? meta.maxItems : undefined
|
|
190
|
-
const collapsible = Boolean(meta.collapsible)
|
|
191
|
-
const defaultCollapsed = Boolean(meta.defaultCollapsed)
|
|
192
|
-
const accordion = Boolean(meta.accordion)
|
|
193
|
-
const reorderable = Boolean(meta.reorderable)
|
|
194
|
-
const cloneable = Boolean(meta.cloneable)
|
|
195
|
-
const simple = Boolean(meta.simple)
|
|
196
|
-
const buttons = meta.buttons
|
|
197
|
-
// Customizer wins over the legacy `addActionLabel`. Default 'Add' is the
|
|
198
|
-
// final fallback; documented in `RepeaterField.addActionLabel`.
|
|
199
|
-
const addLabel = buttons?.add?.label
|
|
200
|
-
?? (typeof meta.addActionLabel === 'string' ? meta.addActionLabel : 'Add')
|
|
201
|
-
const columns = typeof meta.columns === 'number' && meta.columns > 1 ? meta.columns : 1
|
|
202
|
-
// Row-grid mode: scalar `grid: N` or responsive object `grid: { default, md, … }`
|
|
203
|
-
// lays rows in an n-column grid. Distinct from `columns` which grids the inner
|
|
204
|
-
// schema *inside* a row. We suppress the drop indicator in grid mode (a horizontal
|
|
205
|
-
// accent line reads wrong across grid cells); button reorder still works.
|
|
206
|
-
const gridScopeId = useId()
|
|
207
|
-
const gridContainer = useMemo(
|
|
208
|
-
() => buildGridContainer(
|
|
209
|
-
meta.grid as number | Record<string, number | undefined> | undefined,
|
|
210
|
-
gridScopeId,
|
|
211
|
-
),
|
|
212
|
-
[meta.grid, gridScopeId],
|
|
213
|
-
)
|
|
214
|
-
// Table mode renders rows as `<tr>` and inner fields as `<td>`. Mutually
|
|
215
|
-
// exclusive with `simple` and `grid` (the field setters arbitrate).
|
|
216
|
-
// Collapsible / accordion are meaningless on `<tr>` rows so we ignore
|
|
217
|
-
// those flags in this path.
|
|
218
|
-
const tableColumns = meta.table?.columns
|
|
219
|
-
const tableMode = Array.isArray(tableColumns) && tableColumns.length > 0
|
|
220
|
-
|
|
221
|
-
const initialRows: RowState[] = useMemo(
|
|
222
|
-
() => (meta.rows ?? []).map(r => ({
|
|
223
|
-
id: r.id,
|
|
224
|
-
children: r.children,
|
|
225
|
-
...(r.itemLabel !== undefined ? { itemLabel: r.itemLabel } : {}),
|
|
226
|
-
...(r.hidden ? { hidden: true } : {}),
|
|
227
|
-
...(r.extraActions && r.extraActions.length > 0 ? { extraActions: r.extraActions } : {}),
|
|
228
|
-
...(r.canDelete === false ? { canDelete: false as const } : {}),
|
|
229
|
-
...(r.canClone === false ? { canClone: false as const } : {}),
|
|
230
|
-
...(r.canReorder === false ? { canReorder: false as const } : {}),
|
|
231
|
-
})),
|
|
232
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
233
|
-
[],
|
|
234
|
-
)
|
|
235
|
-
const [rows, setRows] = useState<RowState[]>(initialRows)
|
|
236
|
-
const metaRows = meta.rows
|
|
237
|
-
useEffect(() => {
|
|
238
|
-
if (!metaRows) return
|
|
239
|
-
setRows(prev => syncRowGates(prev, metaRows))
|
|
240
|
-
}, [metaRows])
|
|
241
|
-
// Phase F.5 — row-array CRDT binding. `null` outside a collab room
|
|
242
|
-
// OR when the active binding doesn't implement F.5 row methods OR when
|
|
243
|
-
// this Repeater opted out via `.collab(false)`. The four row mutations
|
|
244
|
-
// (`addRow / cloneRow / removeRow / moveRow + DnD drop`) below call into
|
|
245
|
-
// it when present so peers see the same lifecycle events; absent =
|
|
246
|
-
// today's local-only behaviour, unchanged.
|
|
247
|
-
const rowBinding = useRowBinding(name)
|
|
248
|
-
// Mirror row identities into the form's values map so dotted row-leaf
|
|
249
|
-
// consumers can resolve the row's `__id` via `rowIdAtIndex(ctx.values,
|
|
250
|
-
// name, i)`. Setting a `__id` key routes through `routeBindingWrite` →
|
|
251
|
-
// `parseRowFieldPath` which filters `__id` → no-op on the binding side,
|
|
252
|
-
// so the only effect is a row entry landing in `valuesState`.
|
|
253
|
-
// `formStateForIds` mirrors `formState` below; we read via
|
|
254
|
-
// `useFormState()` here too instead of forward-referencing the later
|
|
255
|
-
// binding.
|
|
256
|
-
const formStateForIds = useFormState()
|
|
257
|
-
const ctxSetValue = formStateForIds?.setValue
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
if (!ctxSetValue) return
|
|
260
|
-
for (let i = 0; i < rows.length; i++) {
|
|
261
|
-
const row = rows[i]
|
|
262
|
-
if (!row) continue
|
|
263
|
-
ctxSetValue(`${name}.${i}.__id`, row.id)
|
|
264
|
-
}
|
|
265
|
-
}, [rows, name, ctxSetValue])
|
|
266
|
-
// Phase F.5 — reconcile remote row events into the local `rows` state
|
|
267
|
-
// by `__id`. Local mutations also surface here (Yjs observers fire on
|
|
268
|
-
// local transactions); we dedupe by checking whether the rowId is
|
|
269
|
-
// already present in the current state. `template` seeds new rows so
|
|
270
|
-
// remote-added rows render with the same inner schema as locally-added
|
|
271
|
-
// ones.
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
if (!rowBinding) return
|
|
274
|
-
const tpl = meta.template ?? []
|
|
275
|
-
return rowBinding.subscribe((event) => {
|
|
276
|
-
if (event.kind === 'add') {
|
|
277
|
-
setRows((prev) => {
|
|
278
|
-
if (prev.some(r => r.id === event.rowId)) return prev
|
|
279
|
-
const incoming: RowState = { id: event.rowId, children: tpl }
|
|
280
|
-
const next = prev.slice()
|
|
281
|
-
const at = Math.max(0, Math.min(event.index, next.length))
|
|
282
|
-
next.splice(at, 0, incoming)
|
|
283
|
-
return next
|
|
284
|
-
})
|
|
285
|
-
return
|
|
286
|
-
}
|
|
287
|
-
if (event.kind === 'remove') {
|
|
288
|
-
setRows((prev) => {
|
|
289
|
-
if (!prev.some(r => r.id === event.rowId)) return prev
|
|
290
|
-
return prev.filter(r => r.id !== event.rowId)
|
|
291
|
-
})
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
// move — recompute the local row order by lifting the row at `from`
|
|
295
|
-
// and re-inserting at `to`. No-op when local already matches.
|
|
296
|
-
setRows((prev) => {
|
|
297
|
-
const fromIdx = prev.findIndex(r => r.id === event.rowId)
|
|
298
|
-
if (fromIdx < 0) return prev
|
|
299
|
-
if (fromIdx === event.to) return prev
|
|
300
|
-
const next = prev.slice()
|
|
301
|
-
const [moved] = next.splice(fromIdx, 1)
|
|
302
|
-
if (!moved) return prev
|
|
303
|
-
next.splice(event.to, 0, moved)
|
|
304
|
-
return next
|
|
305
|
-
})
|
|
306
|
-
})
|
|
307
|
-
}, [rowBinding, meta.template])
|
|
308
|
-
|
|
309
|
-
// Phase A reconciliation for `Repeater.relationship` PK-switch — when
|
|
310
|
-
// the surrounding form just submitted in this tab AND we're inside a
|
|
311
|
-
// collab room with a row binding, snapshot the CRDT order after a
|
|
312
|
-
// short settle (long enough for WS sync to deliver any persisted
|
|
313
|
-
// state) and reconcile against `initialRows`. Drops orphan UUIDs
|
|
314
|
-
// whose rows just persisted under a fresh DB PK; idempotent + no-op
|
|
315
|
-
// for non-relationship Repeaters where `__id` stays UUID across
|
|
316
|
-
// save+reload. Plan:
|
|
317
|
-
// `pilotiq-pro/docs/plans/repeater-relationship-pk-switch.md`.
|
|
318
|
-
useEffect(() => {
|
|
319
|
-
if (!rowBinding) return
|
|
320
|
-
if (!consumeReconcileFlag(formId)) return
|
|
321
|
-
// Give WS sync time to deliver any persisted rows before reading
|
|
322
|
-
// current(). 1500ms is conservative; typical sync settles in <300ms.
|
|
323
|
-
// The reconciler is one-shot per submit, so we accept the brief
|
|
324
|
-
// visual flicker over a tighter timer that might fire pre-sync.
|
|
325
|
-
const timer = setTimeout(() => {
|
|
326
|
-
const plan = computeReconcilePlan({
|
|
327
|
-
current: rowBinding.current(),
|
|
328
|
-
authoritative: initialRows.map(r => r.id),
|
|
329
|
-
})
|
|
330
|
-
for (const id of plan.toRemove) rowBinding.remove(id)
|
|
331
|
-
for (const id of plan.toAdd) rowBinding.add(id, {})
|
|
332
|
-
}, 1500)
|
|
333
|
-
return () => clearTimeout(timer)
|
|
334
|
-
// initialRows is a stable useMemo([]) ref so it's safe to omit. We
|
|
335
|
-
// intentionally key only on rowBinding + formId — the reconciler is
|
|
336
|
-
// tied to the submit lifecycle, not to row-state changes.
|
|
337
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
338
|
-
}, [rowBinding, formId])
|
|
339
|
-
const [collapsed, setCollapsed] = useState<Record<string, boolean>>(() =>
|
|
340
|
-
accordion ? {} : initSeedCollapsed(initialRows, formId, name, defaultCollapsed, collapsible),
|
|
341
|
-
)
|
|
342
|
-
// Accordion mode replaces the per-row collapsed map with a single open
|
|
343
|
-
// row id (or `null` for "all collapsed"). Persisted under a single
|
|
344
|
-
// `…accordion` storage key so reload + form swap restore the open row.
|
|
345
|
-
// Initial value: respect `defaultCollapsed` (start with everything
|
|
346
|
-
// collapsed when the author opted in), otherwise open the first
|
|
347
|
-
// visible row — Filament's posture, and matches the implicit user
|
|
348
|
-
// mental model that an accordion always shows *something*.
|
|
349
|
-
const [accordionOpenId, setAccordionOpenId] = useState<string | null>(() => {
|
|
350
|
-
if (!accordion) return null
|
|
351
|
-
const stored = readAccordionFromStorage(formId, name)
|
|
352
|
-
if (stored !== undefined) {
|
|
353
|
-
// Storage may hold a stale id from before a row was deleted; if so,
|
|
354
|
-
// fall through to the default.
|
|
355
|
-
if (stored === '' || initialRows.some(r => r.id === stored)) return stored === '' ? null : stored
|
|
356
|
-
}
|
|
357
|
-
if (defaultCollapsed) return null
|
|
358
|
-
const firstVisible = initialRows.find(r => !r.hidden)
|
|
359
|
-
return firstVisible?.id ?? null
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
const atMin = minItems !== undefined && rows.length <= minItems
|
|
363
|
-
const atMax = maxItems !== undefined && rows.length >= maxItems
|
|
364
|
-
|
|
365
|
-
const addRow = (): void => {
|
|
366
|
-
if (atMax) return
|
|
367
|
-
const newRow: RowState = {
|
|
368
|
-
id: generateRowId(),
|
|
369
|
-
children: meta.template ?? [],
|
|
370
|
-
}
|
|
371
|
-
setRows(prev => [...prev, newRow])
|
|
372
|
-
rowBinding?.add(newRow.id, {})
|
|
373
|
-
if (accordion) {
|
|
374
|
-
// New row should be the only one open — the user just asked for it.
|
|
375
|
-
setAccordionOpenId(newRow.id)
|
|
376
|
-
writeAccordionToStorage(formId, name, newRow.id)
|
|
377
|
-
return
|
|
378
|
-
}
|
|
379
|
-
if (collapsible && defaultCollapsed) {
|
|
380
|
-
setCollapsed(prev => ({ ...prev, [newRow.id]: true }))
|
|
381
|
-
writeCollapsedToStorage(formId, name, newRow.id, true)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const removeRow = (id: string): void => {
|
|
386
|
-
if (atMin) return
|
|
387
|
-
setRows(prev => prev.filter(r => r.id !== id))
|
|
388
|
-
rowBinding?.remove(id)
|
|
389
|
-
if (accordion) {
|
|
390
|
-
if (accordionOpenId === id) {
|
|
391
|
-
setAccordionOpenId(null)
|
|
392
|
-
writeAccordionToStorage(formId, name, null)
|
|
393
|
-
}
|
|
394
|
-
return
|
|
395
|
-
}
|
|
396
|
-
setCollapsed(prev => {
|
|
397
|
-
const { [id]: _drop, ...rest } = prev
|
|
398
|
-
return rest
|
|
399
|
-
})
|
|
400
|
-
deleteCollapsedFromStorage(formId, name, id)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const cloneRow = (id: string): void => {
|
|
404
|
-
if (atMax) return
|
|
405
|
-
let cloneId: string | null = null
|
|
406
|
-
setRows(prev => {
|
|
407
|
-
const idx = prev.findIndex(r => r.id === id)
|
|
408
|
-
if (idx < 0) return prev
|
|
409
|
-
const source = prev[idx]!
|
|
410
|
-
cloneId = generateRowId()
|
|
411
|
-
const clone: RowState = {
|
|
412
|
-
id: cloneId,
|
|
413
|
-
children: source.children,
|
|
414
|
-
...(source.itemLabel !== undefined ? { itemLabel: source.itemLabel } : {}),
|
|
415
|
-
}
|
|
416
|
-
const next = prev.slice()
|
|
417
|
-
next.splice(idx + 1, 0, clone)
|
|
418
|
-
return next
|
|
419
|
-
})
|
|
420
|
-
// F.5 — register the clone's stable id on the binding. Per-field
|
|
421
|
-
// clone-of-source values flow through `setRow` on the user's next
|
|
422
|
-
// edit; v1 doesn't lift the source row's values onto the clone (the
|
|
423
|
-
// binding's empty seed combined with the DOM's defaultValue-copied
|
|
424
|
-
// inputs gives the local user the right visual state).
|
|
425
|
-
if (cloneId !== null) rowBinding?.add(cloneId, {})
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const moveRow = (id: string, dir: -1 | 1): void => {
|
|
429
|
-
const idx = rows.findIndex(r => r.id === id)
|
|
430
|
-
if (idx < 0) return
|
|
431
|
-
// Skip past hidden neighbours so reorder operates between visible
|
|
432
|
-
// rows. Hidden rows hold their absolute slot — the visible row hops
|
|
433
|
-
// over them.
|
|
434
|
-
let next: RowState[]
|
|
435
|
-
if (dir === -1) {
|
|
436
|
-
let target = idx - 1
|
|
437
|
-
while (target >= 0 && rows[target]?.hidden) target--
|
|
438
|
-
if (target < 0) return
|
|
439
|
-
next = reorderRows(rows, idx, target)
|
|
440
|
-
} else {
|
|
441
|
-
let target = idx + 1
|
|
442
|
-
while (target < rows.length && rows[target]?.hidden) target++
|
|
443
|
-
if (target >= rows.length) return
|
|
444
|
-
next = reorderRows(rows, idx, target + 1)
|
|
445
|
-
}
|
|
446
|
-
if (next === rows) return
|
|
447
|
-
setRows(next)
|
|
448
|
-
rowBinding?.reorder(next.map(r => r.id))
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ── DnD state ───────────────────────────────────────────
|
|
452
|
-
const {
|
|
453
|
-
dragId, dropAt,
|
|
454
|
-
onDragStart: onRowDragStart,
|
|
455
|
-
onDragOver: onRowDragOver,
|
|
456
|
-
onDrop: onRowDrop,
|
|
457
|
-
onDragEnd: onRowDragEnd,
|
|
458
|
-
} = useRowReorderDnd({
|
|
459
|
-
enabled: reorderable && !disabled,
|
|
460
|
-
onDrop: (fromId, at) => {
|
|
461
|
-
// Compute next from the current `rows` directly. The previous
|
|
462
|
-
// setRows(updater) + closure-mutation pattern relied on React
|
|
463
|
-
// running the updater synchronously inside setState — which only
|
|
464
|
-
// happens when no other update is queued. `useRowReorderDnd`'s
|
|
465
|
-
// handleDrop sets dragId/dropAt to null right before calling
|
|
466
|
-
// this callback, so the updater runs in commit phase and the
|
|
467
|
-
// outer `newOrder` stayed null past the `if` check, silently
|
|
468
|
-
// skipping the rowBinding.reorder broadcast.
|
|
469
|
-
const fromIdx = rows.findIndex(r => r.id === fromId)
|
|
470
|
-
if (fromIdx < 0) return
|
|
471
|
-
const next = reorderRows(rows, fromIdx, at)
|
|
472
|
-
if (next === rows) return
|
|
473
|
-
setRows(next)
|
|
474
|
-
rowBinding?.reorder(next.map(r => r.id))
|
|
475
|
-
},
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
// ── Inner-field live re-resolve (Plan #14 v1.1) ─────────────
|
|
479
|
-
// Inner Repeater inputs are uncontrolled (so reorder/clone preserves
|
|
480
|
-
// typed values). To make `Field.live()` work on them, we delegate
|
|
481
|
-
// change/blur events at the container level: the dotted-path field
|
|
482
|
-
// name on `target.name` is enough to find the field meta and decide
|
|
483
|
-
// whether to fire. `triggerLive` then snapshots the form's full
|
|
484
|
-
// FormData and POSTs to the partial-resolve endpoint — see
|
|
485
|
-
// FormStateContext.
|
|
486
|
-
//
|
|
487
|
-
// React-controlled primitives that update via callbacks (Switch /
|
|
488
|
-
// Slider / Base UI Select / etc.) don't bubble native input events
|
|
489
|
-
// here. Each of those renderers calls `fs.triggerLive(value)`
|
|
490
|
-
// explicitly to compensate (Plan #14 v1.2). Native inputs
|
|
491
|
-
// (text/number/email/textarea/range/date/checkbox/radio) keep
|
|
492
|
-
// bubbling through this delegate as before.
|
|
493
|
-
const formState = useFormState()
|
|
494
|
-
const fireLive = (name: string, value: string, eventKind: 'change' | 'blur'): void => {
|
|
495
|
-
if (!formState) return
|
|
496
|
-
if (!name.includes('.')) return // top-level fields handle their own live trigger
|
|
497
|
-
const fieldMeta = findFieldMeta(formState.formMeta, name)
|
|
498
|
-
const liveCfg = fieldMeta?.['live']
|
|
499
|
-
const hasJs = (fieldMeta as { afterStateUpdatedJs?: string } | undefined)?.afterStateUpdatedJs !== undefined
|
|
500
|
-
if (!liveCfg && !hasJs) return
|
|
501
|
-
if (liveCfg) {
|
|
502
|
-
const onBlurMode = typeof liveCfg === 'object' && liveCfg !== null
|
|
503
|
-
&& (liveCfg as { onBlur?: boolean }).onBlur === true
|
|
504
|
-
if (eventKind === 'change' && onBlurMode) return
|
|
505
|
-
if (eventKind === 'blur' && !onBlurMode) return
|
|
506
|
-
} else {
|
|
507
|
-
// JS-only handlers always fire immediately on change.
|
|
508
|
-
if (eventKind === 'blur') return
|
|
509
|
-
}
|
|
510
|
-
formState.triggerLive(name, value)
|
|
511
|
-
}
|
|
512
|
-
const onContainerChange = (e: React.ChangeEvent<HTMLDivElement>): void => {
|
|
513
|
-
const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
514
|
-
if (!t.name) return
|
|
515
|
-
fireLive(t.name, t.value, 'change')
|
|
516
|
-
}
|
|
517
|
-
const onContainerBlur = (e: React.FocusEvent<HTMLDivElement>): void => {
|
|
518
|
-
const t = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
|
|
519
|
-
if (!t.name) return
|
|
520
|
-
fireLive(t.name, t.value, 'blur')
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const toggleCollapsed = (id: string): void => {
|
|
524
|
-
if (accordion) {
|
|
525
|
-
// Click the open row to collapse all; click any other row to swap.
|
|
526
|
-
// No "two rows open" state is reachable.
|
|
527
|
-
const next = accordionOpenId === id ? null : id
|
|
528
|
-
setAccordionOpenId(next)
|
|
529
|
-
writeAccordionToStorage(formId, name, next)
|
|
530
|
-
return
|
|
531
|
-
}
|
|
532
|
-
setCollapsed(prev => {
|
|
533
|
-
const nextValue = !prev[id]
|
|
534
|
-
writeCollapsedToStorage(formId, name, id, nextValue)
|
|
535
|
-
return { ...prev, [id]: nextValue }
|
|
536
|
-
})
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// ── Bulk expand / collapse ──────────────────────────────
|
|
540
|
-
// Accordion's "only one open" invariant survives both bulk actions:
|
|
541
|
-
// expandAll opens the first visible row (matches the implicit
|
|
542
|
-
// "always show something" mental model); collapseAll sets null.
|
|
543
|
-
// Per-row mode iterates every row id and writes the storage slot
|
|
544
|
-
// alongside the in-memory map so reload restores the bulk action.
|
|
545
|
-
const expandAll = (): void => {
|
|
546
|
-
if (accordion) {
|
|
547
|
-
const firstVisible = rows.find(r => !r.hidden)
|
|
548
|
-
const next = firstVisible?.id ?? null
|
|
549
|
-
setAccordionOpenId(next)
|
|
550
|
-
writeAccordionToStorage(formId, name, next)
|
|
551
|
-
return
|
|
552
|
-
}
|
|
553
|
-
setCollapsed({})
|
|
554
|
-
for (const r of rows) writeCollapsedToStorage(formId, name, r.id, false)
|
|
555
|
-
}
|
|
556
|
-
const collapseAll = (): void => {
|
|
557
|
-
if (accordion) {
|
|
558
|
-
setAccordionOpenId(null)
|
|
559
|
-
writeAccordionToStorage(formId, name, null)
|
|
560
|
-
return
|
|
561
|
-
}
|
|
562
|
-
const next: Record<string, boolean> = {}
|
|
563
|
-
for (const r of rows) {
|
|
564
|
-
next[r.id] = true
|
|
565
|
-
writeCollapsedToStorage(formId, name, r.id, true)
|
|
566
|
-
}
|
|
567
|
-
setCollapsed(next)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Visibility computed each render — hidden rows still occupy slots in
|
|
571
|
-
// `rows` (so values round-trip + reorder-around math stays simple), but
|
|
572
|
-
// they don't count for the user-facing empty state, drop indicator, or
|
|
573
|
-
// first/last-visible disable on Up/Down buttons.
|
|
574
|
-
const hasVisibleRow = rows.some(r => !r.hidden)
|
|
575
|
-
const firstVisibleIdx = rows.findIndex(r => !r.hidden)
|
|
576
|
-
const lastVisibleIdx = (() => {
|
|
577
|
-
for (let i = rows.length - 1; i >= 0; i--) if (!rows[i]?.hidden) return i
|
|
578
|
-
return -1
|
|
579
|
-
})()
|
|
580
|
-
|
|
581
|
-
if (tableMode && tableColumns) {
|
|
582
|
-
// Table mode renders rows as `<tr>` with the inner schema's fields
|
|
583
|
-
// as `<td>` cells. The reorder grip + extraActions + clone + delete
|
|
584
|
-
// strip lives in a final actions column (only mounted when any of
|
|
585
|
-
// those are configured). Hidden rows render with `display:none` on
|
|
586
|
-
// `<tr>` so values still round-trip on submit. Drop indicator is
|
|
587
|
-
// suppressed — a horizontal accent across `<td>` cells looks broken;
|
|
588
|
-
// button reorder + drag itself still move rows.
|
|
589
|
-
// Actions cell is always present in v1 — delete is implicit on every
|
|
590
|
-
// Repeater row, and reorder/clone/extraActions land here too. When
|
|
591
|
-
// every action happens to be disabled (e.g. atMin && no reorderable
|
|
592
|
-
// && no clone), the cell still renders for column-count consistency.
|
|
593
|
-
return (
|
|
594
|
-
<RepeaterTableLayout
|
|
595
|
-
rows={rows}
|
|
596
|
-
name={name}
|
|
597
|
-
disabled={disabled}
|
|
598
|
-
columns={tableColumns}
|
|
599
|
-
addLabel={addLabel}
|
|
600
|
-
buttons={buttons}
|
|
601
|
-
atMin={atMin}
|
|
602
|
-
atMax={atMax}
|
|
603
|
-
reorderable={reorderable}
|
|
604
|
-
cloneable={cloneable}
|
|
605
|
-
firstVisibleIdx={firstVisibleIdx}
|
|
606
|
-
lastVisibleIdx={lastVisibleIdx}
|
|
607
|
-
hasVisibleRow={hasVisibleRow}
|
|
608
|
-
dragId={dragId}
|
|
609
|
-
onAdd={addRow}
|
|
610
|
-
onMoveUp={(id) => moveRow(id, -1)}
|
|
611
|
-
onMoveDown={(id) => moveRow(id, 1)}
|
|
612
|
-
onClone={cloneRow}
|
|
613
|
-
onRemove={removeRow}
|
|
614
|
-
onContainerChange={onContainerChange}
|
|
615
|
-
onContainerBlur={onContainerBlur}
|
|
616
|
-
onRowDragStart={onRowDragStart}
|
|
617
|
-
onRowDragOver={onRowDragOver}
|
|
618
|
-
onRowDrop={onRowDrop}
|
|
619
|
-
onRowDragEnd={onRowDragEnd}
|
|
620
|
-
/>
|
|
621
|
-
)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// In grid mode the rows themselves are grid items — wrap them in a
|
|
625
|
-
// CSS grid; otherwise stack vertically. The empty state and Add
|
|
626
|
-
// button are rendered as siblings so they don't get pulled into the
|
|
627
|
-
// grid (Add stays at the natural bottom; empty state spans full).
|
|
628
|
-
return (
|
|
629
|
-
<div
|
|
630
|
-
className="flex flex-col gap-3"
|
|
631
|
-
style={gridContainer.wrapperStyle}
|
|
632
|
-
onChange={onContainerChange}
|
|
633
|
-
onBlur={onContainerBlur}
|
|
634
|
-
>
|
|
635
|
-
<BulkCollapseHeader
|
|
636
|
-
buttons={buttons}
|
|
637
|
-
disabled={disabled || !hasVisibleRow}
|
|
638
|
-
onExpandAll={expandAll}
|
|
639
|
-
onCollapseAll={collapseAll}
|
|
640
|
-
/>
|
|
641
|
-
|
|
642
|
-
{!hasVisibleRow && (
|
|
643
|
-
<div className="rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
|
644
|
-
No items yet. Click {addLabel} to start.
|
|
645
|
-
</div>
|
|
646
|
-
)}
|
|
647
|
-
|
|
648
|
-
{gridContainer.styleBlock}
|
|
649
|
-
<div
|
|
650
|
-
className={gridContainer.className}
|
|
651
|
-
style={gridContainer.style}
|
|
652
|
-
>
|
|
653
|
-
{rows.map((row, i) => (
|
|
654
|
-
<React.Fragment key={row.id}>
|
|
655
|
-
{!row.hidden && dropAt === i && !gridContainer.hasGrid && <DropIndicator />}
|
|
656
|
-
<RepeaterRow
|
|
657
|
-
row={row}
|
|
658
|
-
index={i}
|
|
659
|
-
isFirstVisible={i === firstVisibleIdx}
|
|
660
|
-
isLastVisible={i === lastVisibleIdx}
|
|
661
|
-
name={name}
|
|
662
|
-
disabled={disabled}
|
|
663
|
-
collapsible={collapsible}
|
|
664
|
-
isCollapsed={collapsible && (
|
|
665
|
-
accordion
|
|
666
|
-
? accordionOpenId !== row.id
|
|
667
|
-
: (collapsed[row.id] ?? false)
|
|
668
|
-
)}
|
|
669
|
-
reorderable={reorderable}
|
|
670
|
-
cloneable={cloneable}
|
|
671
|
-
simple={simple}
|
|
672
|
-
atMin={atMin}
|
|
673
|
-
atMax={atMax}
|
|
674
|
-
columns={columns}
|
|
675
|
-
buttons={buttons}
|
|
676
|
-
isDragging={dragId === row.id}
|
|
677
|
-
rowPath={`${name}.${i}`}
|
|
678
|
-
onMoveUp={() => moveRow(row.id, -1)}
|
|
679
|
-
onMoveDown={() => moveRow(row.id, 1)}
|
|
680
|
-
onClone={() => cloneRow(row.id)}
|
|
681
|
-
onRemove={() => removeRow(row.id)}
|
|
682
|
-
onToggleCollapse={() => toggleCollapsed(row.id)}
|
|
683
|
-
onDragStart={onRowDragStart(row.id)}
|
|
684
|
-
onDragOver={onRowDragOver(i)}
|
|
685
|
-
onDrop={onRowDrop}
|
|
686
|
-
onDragEnd={onRowDragEnd}
|
|
687
|
-
/>
|
|
688
|
-
</React.Fragment>
|
|
689
|
-
))}
|
|
690
|
-
{dropAt === rows.length && !gridContainer.hasGrid && <DropIndicator />}
|
|
691
|
-
</div>
|
|
692
|
-
|
|
693
|
-
<AddRowButton
|
|
694
|
-
label={addLabel}
|
|
695
|
-
buttons={buttons}
|
|
696
|
-
disabled={disabled || atMax}
|
|
697
|
-
onClick={addRow}
|
|
698
|
-
/>
|
|
699
|
-
</div>
|
|
700
|
-
)
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
/**
|
|
704
|
-
* Bottom Add button — outline shadcn `<Button>`. Reads the customizer
|
|
705
|
-
* (`addAction(RowButton.make()…)`) for icon + tooltip overrides; label
|
|
706
|
-
* is already pre-resolved upstream so the legacy `addActionLabel()` setter
|
|
707
|
-
* keeps working. Color override is intentionally ignored on the Add
|
|
708
|
-
* button to preserve the outline-button visual identity (icon-color
|
|
709
|
-
* tweaks would clash with the shadcn variant); use `Action.color()` on
|
|
710
|
-
* a custom header action if you need a different chrome there.
|
|
711
|
-
*/
|
|
712
|
-
function AddRowButton({
|
|
713
|
-
label,
|
|
714
|
-
buttons,
|
|
715
|
-
disabled,
|
|
716
|
-
onClick,
|
|
717
|
-
}: {
|
|
718
|
-
label: string
|
|
719
|
-
buttons: RowButtonsMeta | undefined
|
|
720
|
-
disabled: boolean
|
|
721
|
-
onClick: () => void
|
|
722
|
-
}): React.ReactElement {
|
|
723
|
-
const { Icon, tooltip } = resolveRowChrome(
|
|
724
|
-
{ Icon: PlusIcon, label, tooltip: '', colorClass: '' },
|
|
725
|
-
buttons?.add,
|
|
726
|
-
)
|
|
727
|
-
return (
|
|
728
|
-
<Button
|
|
729
|
-
type="button"
|
|
730
|
-
variant="outline"
|
|
731
|
-
size="sm"
|
|
732
|
-
onClick={onClick}
|
|
733
|
-
disabled={disabled}
|
|
734
|
-
title={tooltip || undefined}
|
|
735
|
-
className="self-start"
|
|
736
|
-
>
|
|
737
|
-
<Icon className="size-4" />
|
|
738
|
-
{label}
|
|
739
|
-
</Button>
|
|
740
|
-
)
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
function RepeaterRow({
|
|
744
|
-
row, index, isFirstVisible, isLastVisible, name, disabled,
|
|
745
|
-
collapsible, isCollapsed, reorderable, cloneable, simple, atMin, atMax, columns,
|
|
746
|
-
buttons,
|
|
747
|
-
isDragging,
|
|
748
|
-
rowPath,
|
|
749
|
-
onMoveUp, onMoveDown, onClone, onRemove, onToggleCollapse,
|
|
750
|
-
onDragStart, onDragOver, onDrop, onDragEnd,
|
|
751
|
-
}: {
|
|
752
|
-
row: RowState
|
|
753
|
-
index: number
|
|
754
|
-
isFirstVisible: boolean
|
|
755
|
-
isLastVisible: boolean
|
|
756
|
-
name: string
|
|
757
|
-
disabled: boolean
|
|
758
|
-
collapsible: boolean
|
|
759
|
-
isCollapsed: boolean
|
|
760
|
-
reorderable: boolean
|
|
761
|
-
cloneable: boolean
|
|
762
|
-
simple: boolean
|
|
763
|
-
atMin: boolean
|
|
764
|
-
atMax: boolean
|
|
765
|
-
columns: number
|
|
766
|
-
buttons: RowButtonsMeta | undefined
|
|
767
|
-
isDragging: boolean
|
|
768
|
-
rowPath: string
|
|
769
|
-
onMoveUp: () => void
|
|
770
|
-
onMoveDown: () => void
|
|
771
|
-
onClone: () => void
|
|
772
|
-
onRemove: () => void
|
|
773
|
-
onToggleCollapse: () => void
|
|
774
|
-
onDragStart: (e: React.DragEvent<HTMLElement>) => void
|
|
775
|
-
onDragOver: (e: React.DragEvent<HTMLElement>) => void
|
|
776
|
-
onDrop: (e: React.DragEvent<HTMLElement>) => void
|
|
777
|
-
onDragEnd: (e: React.DragEvent<HTMLElement>) => void
|
|
778
|
-
}): React.ReactElement {
|
|
779
|
-
const prefix = `${name}.${index}`
|
|
780
|
-
const namespaced = useMemo(
|
|
781
|
-
() => row.children.map(c => prefixFieldNames(c, prefix)),
|
|
782
|
-
[row.children, prefix],
|
|
783
|
-
)
|
|
784
|
-
const headerLabel = row.itemLabel ?? `Item ${index + 1}`
|
|
785
|
-
// Row coords for dotted-path text leaves — composes fragment-key
|
|
786
|
-
// `${arrayName}.${rowId}.${fieldName}` for the Tiptap-backed collab
|
|
787
|
-
// renderer (see collab-row-text-tiptap-backed.md Phase 1).
|
|
788
|
-
const rowCoords = useMemo(
|
|
789
|
-
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
790
|
-
[name, index, row.id],
|
|
791
|
-
)
|
|
792
|
-
|
|
793
|
-
// Hidden rows: render only the inputs (and __id) inside a display:none
|
|
794
|
-
// wrapper so their values round-trip through FormData on submit. No
|
|
795
|
-
// chrome, no drag wiring, no labels — `itemHidden` is purely UX.
|
|
796
|
-
if (row.hidden) {
|
|
797
|
-
return (
|
|
798
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
799
|
-
<div style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
|
|
800
|
-
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
801
|
-
<SchemaRenderer elements={namespaced} />
|
|
802
|
-
</div>
|
|
803
|
-
</RowCoordsContext.Provider>
|
|
804
|
-
)
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// Per-row capability gates — `itemCan*(rule)` server-resolved.
|
|
808
|
-
// `canDelete / canClone / canReorder === false` removes the matching
|
|
809
|
-
// button on this row; absent flags fall through to the global defaults.
|
|
810
|
-
const canDelete = row.canDelete !== false
|
|
811
|
-
const canClone = row.canClone !== false
|
|
812
|
-
const canReorder = row.canReorder !== false
|
|
813
|
-
|
|
814
|
-
// Drag source lives on the grip `<span>` (see `ReorderGrip`). The
|
|
815
|
-
// row container is only the drop target — `dragend` bubbles, so source
|
|
816
|
-
// cleanup still reaches it. Splitting source from target this way lets
|
|
817
|
-
// row contents host a Tiptap contenteditable without the editor's
|
|
818
|
-
// text-selection handler swallowing the row's dragstart.
|
|
819
|
-
// Pinned rows (`canReorder === false`) lose the grip; others can still
|
|
820
|
-
// accept drops — see itemCanReorder docstring.
|
|
821
|
-
const rowRef = useRef<HTMLDivElement>(null)
|
|
822
|
-
const dragEnabled = reorderable && !disabled && canReorder
|
|
823
|
-
const containerDropTargetProps = dragEnabled
|
|
824
|
-
? { onDragOver, onDrop, onDragEnd }
|
|
825
|
-
: {}
|
|
826
|
-
const gripDragHandleProps = dragEnabled
|
|
827
|
-
? {
|
|
828
|
-
draggable: true as const,
|
|
829
|
-
onDragStart: (e: React.DragEvent<HTMLElement>): void => {
|
|
830
|
-
// Use the row element as the drag preview so the user still
|
|
831
|
-
// sees the whole row floating, not just the grip icon.
|
|
832
|
-
if (rowRef.current) e.dataTransfer.setDragImage(rowRef.current, 0, 0)
|
|
833
|
-
onDragStart(e)
|
|
834
|
-
},
|
|
835
|
-
}
|
|
836
|
-
: undefined
|
|
837
|
-
|
|
838
|
-
// Simple-mode: flatten the row to one input + inline action strip — no
|
|
839
|
-
// header, no border, no collapse (a single field has nothing to collapse).
|
|
840
|
-
// Reorder + delete still work; clone + extraActions are intentionally
|
|
841
|
-
// dropped since there's no "row identity" worth duplicating in the flat
|
|
842
|
-
// shape, and per-row buttons read cluttered next to a one-input row.
|
|
843
|
-
// FieldShell renders a label by default — for simple rows we want flush
|
|
844
|
-
// inputs (Filament's posture too), so we suppress the inner label by
|
|
845
|
-
// wrapping in a class that hides the FieldShell's label slot.
|
|
846
|
-
if (simple) {
|
|
847
|
-
return (
|
|
848
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
849
|
-
<div
|
|
850
|
-
ref={rowRef}
|
|
851
|
-
className={`flex items-center gap-2 transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
852
|
-
data-pilotiq-repeater-row="simple"
|
|
853
|
-
{...containerDropTargetProps}
|
|
854
|
-
>
|
|
855
|
-
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
856
|
-
{reorderable && canReorder && (
|
|
857
|
-
<ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
|
|
858
|
-
)}
|
|
859
|
-
<div className="flex-1 [&_label]:sr-only">
|
|
860
|
-
<SchemaRenderer elements={namespaced} />
|
|
861
|
-
</div>
|
|
862
|
-
{reorderable && canReorder && (
|
|
863
|
-
<>
|
|
864
|
-
<RowChromeIconButton
|
|
865
|
-
defaults={DEFAULT_MOVE_UP}
|
|
866
|
-
override={buttons?.moveUp}
|
|
867
|
-
disabled={disabled || isFirstVisible}
|
|
868
|
-
onClick={onMoveUp}
|
|
869
|
-
/>
|
|
870
|
-
<RowChromeIconButton
|
|
871
|
-
defaults={DEFAULT_MOVE_DOWN}
|
|
872
|
-
override={buttons?.moveDown}
|
|
873
|
-
disabled={disabled || isLastVisible}
|
|
874
|
-
onClick={onMoveDown}
|
|
875
|
-
/>
|
|
876
|
-
</>
|
|
877
|
-
)}
|
|
878
|
-
{canDelete && (
|
|
879
|
-
<RowChromeIconButton
|
|
880
|
-
defaults={DEFAULT_DELETE}
|
|
881
|
-
override={buttons?.delete}
|
|
882
|
-
disabled={disabled || atMin}
|
|
883
|
-
onClick={onRemove}
|
|
884
|
-
/>
|
|
885
|
-
)}
|
|
886
|
-
</div>
|
|
887
|
-
</RowCoordsContext.Provider>
|
|
888
|
-
)
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return (
|
|
892
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
893
|
-
<div
|
|
894
|
-
ref={rowRef}
|
|
895
|
-
className={`rounded-md border bg-card transition-opacity ${isDragging ? 'opacity-50' : ''}`}
|
|
896
|
-
data-pilotiq-repeater-row=""
|
|
897
|
-
{...containerDropTargetProps}
|
|
898
|
-
>
|
|
899
|
-
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
900
|
-
{reorderable && canReorder && (
|
|
901
|
-
<ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
|
|
902
|
-
)}
|
|
903
|
-
{collapsible && (
|
|
904
|
-
<CollapseChevron
|
|
905
|
-
isCollapsed={isCollapsed}
|
|
906
|
-
disabled={disabled}
|
|
907
|
-
buttons={buttons}
|
|
908
|
-
onToggle={onToggleCollapse}
|
|
909
|
-
/>
|
|
910
|
-
)}
|
|
911
|
-
<span className="flex-1 truncate text-sm font-medium">{headerLabel}</span>
|
|
912
|
-
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
913
|
-
{reorderable && canReorder && (
|
|
914
|
-
<>
|
|
915
|
-
<RowChromeIconButton
|
|
916
|
-
defaults={DEFAULT_MOVE_UP}
|
|
917
|
-
override={buttons?.moveUp}
|
|
918
|
-
disabled={disabled || isFirstVisible}
|
|
919
|
-
onClick={onMoveUp}
|
|
920
|
-
/>
|
|
921
|
-
<RowChromeIconButton
|
|
922
|
-
defaults={DEFAULT_MOVE_DOWN}
|
|
923
|
-
override={buttons?.moveDown}
|
|
924
|
-
disabled={disabled || isLastVisible}
|
|
925
|
-
onClick={onMoveDown}
|
|
926
|
-
/>
|
|
927
|
-
</>
|
|
928
|
-
)}
|
|
929
|
-
{row.extraActions && row.extraActions.length > 0 && (
|
|
930
|
-
<ExtraActionStrip
|
|
931
|
-
actions={row.extraActions}
|
|
932
|
-
rowPath={rowPath}
|
|
933
|
-
disabled={disabled}
|
|
934
|
-
/>
|
|
935
|
-
)}
|
|
936
|
-
{cloneable && canClone && (
|
|
937
|
-
<RowChromeIconButton
|
|
938
|
-
defaults={DEFAULT_CLONE}
|
|
939
|
-
override={buttons?.clone}
|
|
940
|
-
disabled={disabled || atMax}
|
|
941
|
-
onClick={onClone}
|
|
942
|
-
/>
|
|
943
|
-
)}
|
|
944
|
-
{canDelete && (
|
|
945
|
-
<RowChromeIconButton
|
|
946
|
-
defaults={DEFAULT_DELETE}
|
|
947
|
-
override={buttons?.delete}
|
|
948
|
-
disabled={disabled || atMin}
|
|
949
|
-
onClick={onRemove}
|
|
950
|
-
/>
|
|
951
|
-
)}
|
|
952
|
-
</div>
|
|
953
|
-
|
|
954
|
-
{/* Body — kept mounted (display:none on collapse) so uncontrolled
|
|
955
|
-
input values persist across collapse toggles. */}
|
|
956
|
-
<div
|
|
957
|
-
className="p-3"
|
|
958
|
-
style={isCollapsed ? { display: 'none' } : undefined}
|
|
959
|
-
>
|
|
960
|
-
{columns > 1
|
|
961
|
-
? (
|
|
962
|
-
<div
|
|
963
|
-
className="grid gap-3"
|
|
964
|
-
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
|
|
965
|
-
>
|
|
966
|
-
<SchemaRenderer elements={namespaced} />
|
|
967
|
-
</div>
|
|
968
|
-
)
|
|
969
|
-
: <SchemaRenderer elements={namespaced} />}
|
|
970
|
-
</div>
|
|
971
|
-
</div>
|
|
972
|
-
</RowCoordsContext.Provider>
|
|
973
|
-
)
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
/**
|
|
977
|
-
* Per-row extraItemActions strip. Each button dispatches its handler
|
|
978
|
-
* action by snapshotting the parent `<form>` (so the server's
|
|
979
|
-
* `coerceFormValues` sees the row's submitted fields), then POSTs to the
|
|
980
|
-
* action's `dispatchUrl` with `_rowPath="<fieldName>.<index>"` in the
|
|
981
|
-
* body — the server uses that path to navigate into the field's row
|
|
982
|
-
* array and stamp `ctx.row = { index, id, values, fieldName }` on the
|
|
983
|
-
* handler context.
|
|
984
|
-
*
|
|
985
|
-
* v1 — handler-style only. `href` / `method` / modal-form actions inside
|
|
986
|
-
* `extraItemActions` are accepted by the type system but render here as
|
|
987
|
-
* no-op buttons (they have neither a `dispatchUrl` nor a row-aware fetch
|
|
988
|
-
* branch). Filament parity for those modes can land in a follow-up.
|
|
989
|
-
*
|
|
990
|
-
* Disabled actions render greyed out + skip dispatch (matches the
|
|
991
|
-
* `meta.disabled` stamp from `resolveExtraItemActions`).
|
|
992
|
-
*/
|
|
993
|
-
export function ExtraActionStrip({
|
|
994
|
-
actions, rowPath, disabled,
|
|
995
|
-
}: {
|
|
996
|
-
actions: ElementMeta[]
|
|
997
|
-
rowPath: string
|
|
998
|
-
disabled: boolean
|
|
999
|
-
}): React.ReactElement {
|
|
1000
|
-
const navigate = useNavigate()
|
|
1001
|
-
const { notify } = useToast()
|
|
1002
|
-
|
|
1003
|
-
const onClick = (action: ElementMeta) => async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
|
|
1004
|
-
if (disabled || action['disabled']) return
|
|
1005
|
-
const dispatchUrl = action['dispatchUrl'] as string | undefined
|
|
1006
|
-
if (!dispatchUrl) return
|
|
1007
|
-
const form = e.currentTarget.closest('form')
|
|
1008
|
-
const snapshot = form ? new FormData(form) : new FormData()
|
|
1009
|
-
await dispatchHandlerAction(
|
|
1010
|
-
dispatchUrl,
|
|
1011
|
-
[],
|
|
1012
|
-
navigate,
|
|
1013
|
-
notify,
|
|
1014
|
-
{ _rowPath: rowPath },
|
|
1015
|
-
snapshot,
|
|
1016
|
-
)
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
return (
|
|
1020
|
-
<>
|
|
1021
|
-
{actions.map((a, i) => {
|
|
1022
|
-
const label = String(a['label'] ?? a['name'] ?? '')
|
|
1023
|
-
const tooltip = (a['tooltip'] as string | undefined) ?? label
|
|
1024
|
-
const isDisabled = disabled || Boolean(a['disabled'])
|
|
1025
|
-
const destructive = Boolean(a['destructive'])
|
|
1026
|
-
return (
|
|
1027
|
-
<button
|
|
1028
|
-
key={i}
|
|
1029
|
-
type="button"
|
|
1030
|
-
onClick={onClick(a)}
|
|
1031
|
-
disabled={isDisabled}
|
|
1032
|
-
aria-label={label}
|
|
1033
|
-
title={tooltip}
|
|
1034
|
-
data-action-name={a['name']}
|
|
1035
|
-
className={`text-muted-foreground hover:text-foreground disabled:opacity-30 ${destructive ? 'hover:text-destructive' : ''}`.trim()}
|
|
1036
|
-
>
|
|
1037
|
-
<span className="text-xs font-medium">{label}</span>
|
|
1038
|
-
</button>
|
|
1039
|
-
)
|
|
1040
|
-
})}
|
|
1041
|
-
</>
|
|
1042
|
-
)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
/**
|
|
1046
|
-
* 2px-tall horizontal accent line rendered between rows when the user
|
|
1047
|
-
* drags a row over a valid drop boundary. Uses `pointer-events: none`
|
|
1048
|
-
* so the underlying row's `dragover` keeps firing — without this, the
|
|
1049
|
-
* indicator would steal events and the drop slot would flicker.
|
|
1050
|
-
*/
|
|
1051
|
-
function DropIndicator(): React.ReactElement {
|
|
1052
|
-
return (
|
|
1053
|
-
<div
|
|
1054
|
-
aria-hidden="true"
|
|
1055
|
-
className="pointer-events-none h-0.5 rounded-full bg-primary"
|
|
1056
|
-
/>
|
|
1057
|
-
)
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Table-mode layout. Renders rows as `<tr>` and inner schema fields as
|
|
1062
|
-
* `<td>` cells, with the supplied column headers in `<thead>`.
|
|
1063
|
-
*
|
|
1064
|
-
* Cells call `prefixFieldNames` to emit row-scoped flat dotted names
|
|
1065
|
-
* (`items.0.name`, etc.) — same wire shape as the card layout, so
|
|
1066
|
-
* `coerceFormValues` re-groups identically. The first inner FieldShell
|
|
1067
|
-
* label is suppressed via a parent `[&_label]:sr-only` since the
|
|
1068
|
-
* column header carries the labelling.
|
|
1069
|
-
*
|
|
1070
|
-
* The actions column hosts grip / Up / Down / clone / extraActions /
|
|
1071
|
-
* delete affordances. We render it unconditionally so column count
|
|
1072
|
-
* stays stable across rows even when individual buttons disable.
|
|
1073
|
-
*/
|
|
1074
|
-
function RepeaterTableLayout({
|
|
1075
|
-
rows, name, disabled, columns, addLabel, buttons, atMin, atMax,
|
|
1076
|
-
reorderable, cloneable,
|
|
1077
|
-
firstVisibleIdx, lastVisibleIdx, hasVisibleRow,
|
|
1078
|
-
dragId,
|
|
1079
|
-
onAdd, onMoveUp, onMoveDown, onClone, onRemove,
|
|
1080
|
-
onContainerChange, onContainerBlur,
|
|
1081
|
-
onRowDragStart, onRowDragOver, onRowDrop, onRowDragEnd,
|
|
1082
|
-
}: {
|
|
1083
|
-
rows: RowState[]
|
|
1084
|
-
name: string
|
|
1085
|
-
disabled: boolean
|
|
1086
|
-
columns: TableColumnShape[]
|
|
1087
|
-
addLabel: string
|
|
1088
|
-
buttons: RowButtonsMeta | undefined
|
|
1089
|
-
atMin: boolean
|
|
1090
|
-
atMax: boolean
|
|
1091
|
-
reorderable: boolean
|
|
1092
|
-
cloneable: boolean
|
|
1093
|
-
firstVisibleIdx: number
|
|
1094
|
-
lastVisibleIdx: number
|
|
1095
|
-
hasVisibleRow: boolean
|
|
1096
|
-
dragId: string | null
|
|
1097
|
-
onAdd: () => void
|
|
1098
|
-
onMoveUp: (id: string) => void
|
|
1099
|
-
onMoveDown: (id: string) => void
|
|
1100
|
-
onClone: (id: string) => void
|
|
1101
|
-
onRemove: (id: string) => void
|
|
1102
|
-
onContainerChange: (e: React.ChangeEvent<HTMLDivElement>) => void
|
|
1103
|
-
onContainerBlur: (e: React.FocusEvent<HTMLDivElement>) => void
|
|
1104
|
-
onRowDragStart: (id: string) => (e: React.DragEvent<HTMLElement>) => void
|
|
1105
|
-
onRowDragOver: (idx: number) => (e: React.DragEvent<HTMLElement>) => void
|
|
1106
|
-
onRowDrop: (e: React.DragEvent<HTMLElement>) => void
|
|
1107
|
-
onRowDragEnd: (e: React.DragEvent<HTMLElement>) => void
|
|
1108
|
-
}): React.ReactElement {
|
|
1109
|
-
// The container div carries the change/blur delegates so live() events
|
|
1110
|
-
// bubble identically to the card path. `[&_label]:sr-only` hides the
|
|
1111
|
-
// FieldShell label across every cell (column header carries it).
|
|
1112
|
-
return (
|
|
1113
|
-
<div
|
|
1114
|
-
className="flex flex-col gap-3"
|
|
1115
|
-
onChange={onContainerChange}
|
|
1116
|
-
onBlur={onContainerBlur}
|
|
1117
|
-
>
|
|
1118
|
-
{!hasVisibleRow && (
|
|
1119
|
-
<div className="rounded-md border border-dashed px-4 py-6 text-center text-sm text-muted-foreground">
|
|
1120
|
-
No items yet. Click {addLabel} to start.
|
|
1121
|
-
</div>
|
|
1122
|
-
)}
|
|
1123
|
-
|
|
1124
|
-
{hasVisibleRow && (
|
|
1125
|
-
<div className="overflow-x-auto rounded-md border [&_label]:sr-only">
|
|
1126
|
-
<table className="w-full border-collapse text-sm">
|
|
1127
|
-
<colgroup>
|
|
1128
|
-
{columns.map((c, i) => (
|
|
1129
|
-
<col key={i} style={c.width ? { width: c.width } : undefined} />
|
|
1130
|
-
))}
|
|
1131
|
-
<col />
|
|
1132
|
-
</colgroup>
|
|
1133
|
-
<thead className="bg-muted/40">
|
|
1134
|
-
<tr>
|
|
1135
|
-
{columns.map((c, i) => (
|
|
1136
|
-
<th
|
|
1137
|
-
key={i}
|
|
1138
|
-
scope="col"
|
|
1139
|
-
className={`px-3 py-2 text-xs font-medium text-muted-foreground ${alignClass(c.alignment)}`}
|
|
1140
|
-
>
|
|
1141
|
-
{c.label}
|
|
1142
|
-
{c.required && <span className="ml-0.5 text-destructive">*</span>}
|
|
1143
|
-
</th>
|
|
1144
|
-
))}
|
|
1145
|
-
<th scope="col" className="w-px" aria-label="Actions" />
|
|
1146
|
-
</tr>
|
|
1147
|
-
</thead>
|
|
1148
|
-
<tbody>
|
|
1149
|
-
{rows.map((row, i) => (
|
|
1150
|
-
<RepeaterTableRow
|
|
1151
|
-
key={row.id}
|
|
1152
|
-
row={row}
|
|
1153
|
-
index={i}
|
|
1154
|
-
name={name}
|
|
1155
|
-
disabled={disabled}
|
|
1156
|
-
columns={columns}
|
|
1157
|
-
reorderable={reorderable}
|
|
1158
|
-
cloneable={cloneable}
|
|
1159
|
-
buttons={buttons}
|
|
1160
|
-
isFirstVisible={i === firstVisibleIdx}
|
|
1161
|
-
isLastVisible={i === lastVisibleIdx}
|
|
1162
|
-
atMin={atMin}
|
|
1163
|
-
atMax={atMax}
|
|
1164
|
-
isDragging={dragId === row.id}
|
|
1165
|
-
rowPath={`${name}.${i}`}
|
|
1166
|
-
onMoveUp={() => onMoveUp(row.id)}
|
|
1167
|
-
onMoveDown={() => onMoveDown(row.id)}
|
|
1168
|
-
onClone={() => onClone(row.id)}
|
|
1169
|
-
onRemove={() => onRemove(row.id)}
|
|
1170
|
-
onDragStart={onRowDragStart(row.id)}
|
|
1171
|
-
onDragOver={onRowDragOver(i)}
|
|
1172
|
-
onDrop={onRowDrop}
|
|
1173
|
-
onDragEnd={onRowDragEnd}
|
|
1174
|
-
/>
|
|
1175
|
-
))}
|
|
1176
|
-
</tbody>
|
|
1177
|
-
</table>
|
|
1178
|
-
</div>
|
|
1179
|
-
)}
|
|
1180
|
-
|
|
1181
|
-
<AddRowButton
|
|
1182
|
-
label={addLabel}
|
|
1183
|
-
buttons={buttons}
|
|
1184
|
-
disabled={disabled || atMax}
|
|
1185
|
-
onClick={onAdd}
|
|
1186
|
-
/>
|
|
1187
|
-
</div>
|
|
1188
|
-
)
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function alignClass(a: 'left' | 'center' | 'right' | undefined): string {
|
|
1192
|
-
if (a === 'right') return 'text-right'
|
|
1193
|
-
if (a === 'center') return 'text-center'
|
|
1194
|
-
return 'text-left'
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function RepeaterTableRow({
|
|
1198
|
-
row, index, name, disabled, columns, reorderable, cloneable, buttons,
|
|
1199
|
-
isFirstVisible, isLastVisible, atMin, atMax, isDragging, rowPath,
|
|
1200
|
-
onMoveUp, onMoveDown, onClone, onRemove,
|
|
1201
|
-
onDragStart, onDragOver, onDrop, onDragEnd,
|
|
1202
|
-
}: {
|
|
1203
|
-
row: RowState
|
|
1204
|
-
index: number
|
|
1205
|
-
name: string
|
|
1206
|
-
disabled: boolean
|
|
1207
|
-
columns: TableColumnShape[]
|
|
1208
|
-
reorderable: boolean
|
|
1209
|
-
cloneable: boolean
|
|
1210
|
-
buttons: RowButtonsMeta | undefined
|
|
1211
|
-
isFirstVisible: boolean
|
|
1212
|
-
isLastVisible: boolean
|
|
1213
|
-
atMin: boolean
|
|
1214
|
-
atMax: boolean
|
|
1215
|
-
isDragging: boolean
|
|
1216
|
-
rowPath: string
|
|
1217
|
-
onMoveUp: () => void
|
|
1218
|
-
onMoveDown: () => void
|
|
1219
|
-
onClone: () => void
|
|
1220
|
-
onRemove: () => void
|
|
1221
|
-
onDragStart: (e: React.DragEvent<HTMLElement>) => void
|
|
1222
|
-
onDragOver: (e: React.DragEvent<HTMLElement>) => void
|
|
1223
|
-
onDrop: (e: React.DragEvent<HTMLElement>) => void
|
|
1224
|
-
onDragEnd: (e: React.DragEvent<HTMLElement>) => void
|
|
1225
|
-
}): React.ReactElement {
|
|
1226
|
-
const prefix = `${name}.${index}`
|
|
1227
|
-
const namespaced = useMemo(
|
|
1228
|
-
() => row.children.map(c => prefixFieldNames(c, prefix)),
|
|
1229
|
-
[row.children, prefix],
|
|
1230
|
-
)
|
|
1231
|
-
const rowCoords = useMemo(
|
|
1232
|
-
() => ({ arrayName: name, rowIndex: index, rowId: row.id }),
|
|
1233
|
-
[name, index, row.id],
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
if (row.hidden) {
|
|
1237
|
-
// Render the hidden envelope as a single full-span cell so column
|
|
1238
|
-
// count stays valid; `display:none` ensures the row is invisible but
|
|
1239
|
-
// still in the form's submit. Using `<tr style="display:none">`
|
|
1240
|
-
// would warn under React strict-mode in Firefox; the wrapping cell
|
|
1241
|
-
// keeps the markup HTML-valid.
|
|
1242
|
-
//
|
|
1243
|
-
// The provider wraps the cell rather than the `<tr>` because React's
|
|
1244
|
-
// table-row whitelisting only accepts `<th>/<td>` children, not a
|
|
1245
|
-
// context provider; the provider is a no-DOM wrapper so it sits
|
|
1246
|
-
// inside the cell fine.
|
|
1247
|
-
return (
|
|
1248
|
-
<tr style={{ display: 'none' }} data-pilotiq-repeater-row="hidden">
|
|
1249
|
-
<td colSpan={columns.length + 1}>
|
|
1250
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
1251
|
-
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
1252
|
-
<SchemaRenderer elements={namespaced} />
|
|
1253
|
-
</RowCoordsContext.Provider>
|
|
1254
|
-
</td>
|
|
1255
|
-
</tr>
|
|
1256
|
-
)
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Pair each column header (in order) with the corresponding inner
|
|
1260
|
-
// field meta. Extra fields beyond the column count fall through the
|
|
1261
|
-
// last cell as stacked items — a misconfiguration but better than
|
|
1262
|
-
// dropping them silently.
|
|
1263
|
-
const fieldsPerCell: ElementMeta[][] = columns.map((_c, i) =>
|
|
1264
|
-
i === columns.length - 1 ? namespaced.slice(i) : namespaced.slice(i, i + 1),
|
|
1265
|
-
)
|
|
1266
|
-
|
|
1267
|
-
// Per-row capability gates — see RepeaterRow for the contract.
|
|
1268
|
-
const canDelete = row.canDelete !== false
|
|
1269
|
-
const canClone = row.canClone !== false
|
|
1270
|
-
const canReorder = row.canReorder !== false
|
|
1271
|
-
|
|
1272
|
-
// Drag source on the grip, drop target on the `<tr>` — see RepeaterRow.
|
|
1273
|
-
const rowRef = useRef<HTMLTableRowElement>(null)
|
|
1274
|
-
const dragEnabled = reorderable && !disabled && canReorder
|
|
1275
|
-
const containerDropTargetProps = dragEnabled
|
|
1276
|
-
? { onDragOver, onDrop, onDragEnd }
|
|
1277
|
-
: {}
|
|
1278
|
-
const gripDragHandleProps = dragEnabled
|
|
1279
|
-
? {
|
|
1280
|
-
draggable: true as const,
|
|
1281
|
-
onDragStart: (e: React.DragEvent<HTMLElement>): void => {
|
|
1282
|
-
if (rowRef.current) e.dataTransfer.setDragImage(rowRef.current, 0, 0)
|
|
1283
|
-
onDragStart(e)
|
|
1284
|
-
},
|
|
1285
|
-
}
|
|
1286
|
-
: undefined
|
|
1287
|
-
|
|
1288
|
-
return (
|
|
1289
|
-
<RowCoordsContext.Provider value={rowCoords}>
|
|
1290
|
-
<tr
|
|
1291
|
-
ref={rowRef}
|
|
1292
|
-
className={`border-t align-top ${isDragging ? 'opacity-50' : ''}`}
|
|
1293
|
-
data-pilotiq-repeater-row=""
|
|
1294
|
-
{...containerDropTargetProps}
|
|
1295
|
-
>
|
|
1296
|
-
<input type="hidden" name={`${prefix}.__id`} value={row.id} readOnly />
|
|
1297
|
-
{columns.map((c, i) => (
|
|
1298
|
-
<td key={i} className={`px-3 py-2 ${alignClass(c.alignment)}`}>
|
|
1299
|
-
<SchemaRenderer elements={fieldsPerCell[i] ?? []} />
|
|
1300
|
-
</td>
|
|
1301
|
-
))}
|
|
1302
|
-
<td className="whitespace-nowrap px-3 py-2 text-right">
|
|
1303
|
-
<div className="inline-flex items-center gap-1">
|
|
1304
|
-
{reorderable && canReorder && (
|
|
1305
|
-
<>
|
|
1306
|
-
<ReorderGrip disabled={disabled} buttons={buttons} dragHandleProps={gripDragHandleProps} />
|
|
1307
|
-
<RowChromeIconButton
|
|
1308
|
-
defaults={DEFAULT_MOVE_UP}
|
|
1309
|
-
override={buttons?.moveUp}
|
|
1310
|
-
disabled={disabled || isFirstVisible}
|
|
1311
|
-
onClick={onMoveUp}
|
|
1312
|
-
/>
|
|
1313
|
-
<RowChromeIconButton
|
|
1314
|
-
defaults={DEFAULT_MOVE_DOWN}
|
|
1315
|
-
override={buttons?.moveDown}
|
|
1316
|
-
disabled={disabled || isLastVisible}
|
|
1317
|
-
onClick={onMoveDown}
|
|
1318
|
-
/>
|
|
1319
|
-
</>
|
|
1320
|
-
)}
|
|
1321
|
-
{row.extraActions && row.extraActions.length > 0 && (
|
|
1322
|
-
<ExtraActionStrip
|
|
1323
|
-
actions={row.extraActions}
|
|
1324
|
-
rowPath={rowPath}
|
|
1325
|
-
disabled={disabled}
|
|
1326
|
-
/>
|
|
1327
|
-
)}
|
|
1328
|
-
{cloneable && canClone && (
|
|
1329
|
-
<RowChromeIconButton
|
|
1330
|
-
defaults={DEFAULT_CLONE}
|
|
1331
|
-
override={buttons?.clone}
|
|
1332
|
-
disabled={disabled || atMax}
|
|
1333
|
-
onClick={onClone}
|
|
1334
|
-
/>
|
|
1335
|
-
)}
|
|
1336
|
-
{canDelete && (
|
|
1337
|
-
<RowChromeIconButton
|
|
1338
|
-
defaults={DEFAULT_DELETE}
|
|
1339
|
-
override={buttons?.delete}
|
|
1340
|
-
disabled={disabled || atMin}
|
|
1341
|
-
onClick={onRemove}
|
|
1342
|
-
/>
|
|
1343
|
-
)}
|
|
1344
|
-
</div>
|
|
1345
|
-
</td>
|
|
1346
|
-
</tr>
|
|
1347
|
-
</RowCoordsContext.Provider>
|
|
1348
|
-
)
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
interface RepeaterMetaShape {
|
|
1352
|
-
rows?: Array<{
|
|
1353
|
-
id: string
|
|
1354
|
-
children: ElementMeta[]
|
|
1355
|
-
itemLabel?: string
|
|
1356
|
-
hidden?: boolean
|
|
1357
|
-
extraActions?: ElementMeta[]
|
|
1358
|
-
canDelete?: false
|
|
1359
|
-
canClone?: false
|
|
1360
|
-
canReorder?: false
|
|
1361
|
-
}>
|
|
1362
|
-
template?: ElementMeta[]
|
|
1363
|
-
columns?: number
|
|
1364
|
-
minItems?: number
|
|
1365
|
-
maxItems?: number
|
|
1366
|
-
defaultItems?: number
|
|
1367
|
-
reorderable?: boolean
|
|
1368
|
-
collapsible?: boolean
|
|
1369
|
-
defaultCollapsed?: boolean
|
|
1370
|
-
accordion?: boolean
|
|
1371
|
-
cloneable?: boolean
|
|
1372
|
-
addActionLabel?: string
|
|
1373
|
-
simple?: boolean
|
|
1374
|
-
grid?: number
|
|
1375
|
-
table?: { columns: TableColumnShape[] }
|
|
1376
|
-
buttons?: RowButtonsMeta
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
|
-
interface TableColumnShape {
|
|
1380
|
-
label: string
|
|
1381
|
-
alignment?: 'left' | 'center' | 'right'
|
|
1382
|
-
width?: string
|
|
1383
|
-
required?: boolean
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
/**
|
|
1387
|
-
* Recursively prefix every Field meta's `name` with a row-scoped path.
|
|
1388
|
-
* Inner Repeaters get their own per-row prefixing so nested Repeater
|
|
1389
|
-
* row inputs land at `items.0.modifiers.1.name`.
|
|
1390
|
-
*/
|
|
1391
|
-
function prefixFieldNames(el: ElementMeta, prefix: string): ElementMeta {
|
|
1392
|
-
if (el.type === 'field' && typeof el['name'] === 'string') {
|
|
1393
|
-
const innerName = el['name']
|
|
1394
|
-
const newName = `${prefix}.${innerName}`
|
|
1395
|
-
if (el['fieldType'] === 'repeater') {
|
|
1396
|
-
const m = el as ElementMeta & RepeaterMetaShape
|
|
1397
|
-
const rows = m.rows ?? []
|
|
1398
|
-
const tpl = m.template ?? []
|
|
1399
|
-
return {
|
|
1400
|
-
...el,
|
|
1401
|
-
name: newName,
|
|
1402
|
-
rows: rows.map(r => ({
|
|
1403
|
-
...r,
|
|
1404
|
-
children: r.children.map(c => prefixFieldNames(c, `${newName}.${rows.indexOf(r)}`)),
|
|
1405
|
-
})),
|
|
1406
|
-
template: tpl.map(c => prefixFieldNames(c, `${newName}.0`)),
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
return { ...el, name: newName }
|
|
1410
|
-
}
|
|
1411
|
-
if (Array.isArray(el.children)) {
|
|
1412
|
-
return {
|
|
1413
|
-
...el,
|
|
1414
|
-
children: (el.children as ElementMeta[]).map(c => prefixFieldNames(c, prefix)),
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
return el
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
|