@rebasepro/cms 0.17.3 → 0.18.1
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/README.md +4 -0
- package/dist/{CollectionEditorDialog-B-akreOU.js → CollectionEditorDialog-BeX4b92r.js} +13 -7
- package/dist/{CollectionEditorDialog-B-akreOU.js.map → CollectionEditorDialog-BeX4b92r.js.map} +1 -1
- package/dist/{PropertyEditView-CiY84wkR.js → PropertyEditView-C7thfj4L.js} +3 -3
- package/dist/{PropertyEditView-CiY84wkR.js.map → PropertyEditView-C7thfj4L.js.map} +1 -1
- package/dist/{RouterCollectionsStudioView-DkAWda4N.js → RouterCollectionsStudioView-BtjzvB6E.js} +4 -4
- package/dist/{RouterCollectionsStudioView-DkAWda4N.js.map → RouterCollectionsStudioView-BtjzvB6E.js.map} +1 -1
- package/dist/{SchemaChangeDialog-DceT8QFX.js → SchemaChangeDialog-CkqK6KOC.js} +4 -2
- package/dist/{SchemaChangeDialog-DceT8QFX.js.map → SchemaChangeDialog-CkqK6KOC.js.map} +1 -1
- package/dist/collection_editor/api/generateCollectionApi.d.ts +1 -2
- package/dist/collection_editor_ui.d.ts +2 -0
- package/dist/collection_editor_ui.js +6 -5
- package/dist/collection_editor_ui.js.map +1 -1
- package/dist/components/DeleteEntityDialog.d.ts +2 -3
- package/dist/components/RebaseLayout.d.ts +11 -4
- package/dist/components/field_configs.d.ts +1 -1
- package/dist/editor/sanitize-html.d.ts +15 -0
- package/dist/editor.js +195 -10
- package/dist/editor.js.map +1 -1
- package/dist/{export-Ben8E355.js → export--arni_VY.js} +2 -2
- package/dist/{export-Ben8E355.js.map → export--arni_VY.js.map} +1 -1
- package/dist/form/components/LabelWithIcon.d.ts +10 -0
- package/dist/{history-DIEVd26f.js → history-C2TpoQwi.js} +2 -2
- package/dist/{history-DIEVd26f.js.map → history-C2TpoQwi.js.map} +1 -1
- package/dist/hooks/navigation/useBuildNavigationStateController.d.ts +1 -1
- package/dist/hooks/navigation/useResolvedViews.d.ts +1 -1
- package/dist/hooks/navigation/useTopLevelNavigation.d.ts +1 -1
- package/dist/{import-D25x7ko-.js → import-DTkXNoBg.js} +2 -2
- package/dist/{import-D25x7ko-.js.map → import-DTkXNoBg.js.map} +1 -1
- package/dist/index.js +78 -33
- package/dist/index.js.map +1 -1
- package/dist/util/propertyConfigMapper.d.ts +1 -1
- package/dist/util-CfLFggP7.js +57 -0
- package/dist/util-CfLFggP7.js.map +1 -0
- package/dist/{util-DVqnqlLn.js → util-DE1WQW6h.js} +134 -112
- package/dist/util-DE1WQW6h.js.map +1 -0
- package/package.json +42 -34
- package/dist/util-DVqnqlLn.js.map +0 -1
- package/src/collection_editor/CollectionEditorDialogsContext.tsx +0 -25
- package/src/collection_editor/ConfigControllerProvider.tsx +0 -395
- package/src/collection_editor/_cms_internals.ts +0 -41
- package/src/collection_editor/api/generateCollectionApi.ts +0 -126
- package/src/collection_editor/api/index.ts +0 -1
- package/src/collection_editor/extensibility_types.ts +0 -168
- package/src/collection_editor/index.ts +0 -108
- package/src/collection_editor/liveSchemaClient.ts +0 -285
- package/src/collection_editor/relation_kinds.ts +0 -44
- package/src/collection_editor/serializable_types.ts +0 -535
- package/src/collection_editor/serializable_utils.ts +0 -729
- package/src/collection_editor/types/collection_editor_controller.tsx +0 -62
- package/src/collection_editor/types/collection_inference.ts +0 -18
- package/src/collection_editor/types/config_controller.tsx +0 -121
- package/src/collection_editor/ui/AddKanbanColumnAction.tsx +0 -199
- package/src/collection_editor/ui/CollectionViewHeaderAction.tsx +0 -51
- package/src/collection_editor/ui/EditorCollectionAction.tsx +0 -54
- package/src/collection_editor/ui/EditorCollectionActionStart.tsx +0 -90
- package/src/collection_editor/ui/EditorEntityAction.tsx +0 -50
- package/src/collection_editor/ui/HomePageEditorCollectionAction.tsx +0 -114
- package/src/collection_editor/ui/KanbanSetupAction.tsx +0 -43
- package/src/collection_editor/ui/MissingReferenceWidget.tsx +0 -35
- package/src/collection_editor/ui/NewCollectionButton.tsx +0 -27
- package/src/collection_editor/ui/NewCollectionCard.tsx +0 -48
- package/src/collection_editor/ui/PropertyAddColumnComponent.tsx +0 -49
- package/src/collection_editor/ui/collection_editor/AICollectionGeneratorPopover.tsx +0 -260
- package/src/collection_editor/ui/collection_editor/AIModifiedPathsContext.tsx +0 -90
- package/src/collection_editor/ui/collection_editor/CollectionDetailsForm.tsx +0 -388
- package/src/collection_editor/ui/collection_editor/CollectionEditorDialog.tsx +0 -1098
- package/src/collection_editor/ui/collection_editor/CollectionEditorWelcomeView.tsx +0 -301
- package/src/collection_editor/ui/collection_editor/CollectionJsonImportDialog.tsx +0 -172
- package/src/collection_editor/ui/collection_editor/CollectionPropertiesEditorForm.tsx +0 -620
- package/src/collection_editor/ui/collection_editor/CollectionRLSTab.tsx +0 -530
- package/src/collection_editor/ui/collection_editor/CollectionRelationsTab.tsx +0 -462
- package/src/collection_editor/ui/collection_editor/CollectionStudioView.tsx +0 -104
- package/src/collection_editor/ui/collection_editor/CollectionYupValidation.tsx +0 -9
- package/src/collection_editor/ui/collection_editor/CollectionsStudioView.tsx +0 -199
- package/src/collection_editor/ui/collection_editor/DisplaySettingsForm.tsx +0 -315
- package/src/collection_editor/ui/collection_editor/EntityActionsEditTab.tsx +0 -164
- package/src/collection_editor/ui/collection_editor/EntityActionsSelectDialog.tsx +0 -40
- package/src/collection_editor/ui/collection_editor/EntityCustomViewsSelectDialog.tsx +0 -39
- package/src/collection_editor/ui/collection_editor/EnumForm.tsx +0 -373
- package/src/collection_editor/ui/collection_editor/GeneralSettingsForm.tsx +0 -288
- package/src/collection_editor/ui/collection_editor/GetCodeDialog.tsx +0 -141
- package/src/collection_editor/ui/collection_editor/KanbanConfigSection.tsx +0 -194
- package/src/collection_editor/ui/collection_editor/LayoutModeSwitch.tsx +0 -53
- package/src/collection_editor/ui/collection_editor/PropertyEditView.tsx +0 -980
- package/src/collection_editor/ui/collection_editor/PropertyFieldPreview.tsx +0 -164
- package/src/collection_editor/ui/collection_editor/PropertyTree.tsx +0 -364
- package/src/collection_editor/ui/collection_editor/RouterCollectionStudioView.tsx +0 -44
- package/src/collection_editor/ui/collection_editor/RouterCollectionsStudioView.tsx +0 -41
- package/src/collection_editor/ui/collection_editor/SchemaChangeDialog.tsx +0 -470
- package/src/collection_editor/ui/collection_editor/SubcollectionsEditTab.tsx +0 -256
- package/src/collection_editor/ui/collection_editor/SwitchControl.tsx +0 -39
- package/src/collection_editor/ui/collection_editor/ViewModeSwitch.tsx +0 -68
- package/src/collection_editor/ui/collection_editor/import/CollectionEditorImportDataPreview.tsx +0 -67
- package/src/collection_editor/ui/collection_editor/import/CollectionEditorImportMapping.tsx +0 -296
- package/src/collection_editor/ui/collection_editor/import/clean_import_data.ts +0 -57
- package/src/collection_editor/ui/collection_editor/properties/BlockPropertyField.tsx +0 -147
- package/src/collection_editor/ui/collection_editor/properties/BooleanPropertyField.tsx +0 -41
- package/src/collection_editor/ui/collection_editor/properties/CommonPropertyFields.tsx +0 -113
- package/src/collection_editor/ui/collection_editor/properties/DateTimePropertyField.tsx +0 -250
- package/src/collection_editor/ui/collection_editor/properties/EnumPropertyField.tsx +0 -126
- package/src/collection_editor/ui/collection_editor/properties/KeyValuePropertyField.tsx +0 -20
- package/src/collection_editor/ui/collection_editor/properties/MapPropertyField.tsx +0 -153
- package/src/collection_editor/ui/collection_editor/properties/MarkdownPropertyField.tsx +0 -217
- package/src/collection_editor/ui/collection_editor/properties/NumberPropertyField.tsx +0 -117
- package/src/collection_editor/ui/collection_editor/properties/ReferencePropertyField.tsx +0 -129
- package/src/collection_editor/ui/collection_editor/properties/RelationPropertyField.tsx +0 -335
- package/src/collection_editor/ui/collection_editor/properties/RepeatPropertyField.tsx +0 -107
- package/src/collection_editor/ui/collection_editor/properties/StoragePropertyField.tsx +0 -403
- package/src/collection_editor/ui/collection_editor/properties/StringPropertyField.tsx +0 -148
- package/src/collection_editor/ui/collection_editor/properties/UrlPropertyField.tsx +0 -91
- package/src/collection_editor/ui/collection_editor/properties/VectorPropertyField.tsx +0 -34
- package/src/collection_editor/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +0 -49
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsEditor.tsx +0 -925
- package/src/collection_editor/ui/collection_editor/properties/conditions/ConditionsPanel.tsx +0 -28
- package/src/collection_editor/ui/collection_editor/properties/conditions/EnumConditionsEditor.tsx +0 -626
- package/src/collection_editor/ui/collection_editor/properties/conditions/index.ts +0 -6
- package/src/collection_editor/ui/collection_editor/properties/conditions/property_paths.ts +0 -93
- package/src/collection_editor/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +0 -52
- package/src/collection_editor/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +0 -63
- package/src/collection_editor/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +0 -114
- package/src/collection_editor/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +0 -154
- package/src/collection_editor/ui/collection_editor/properties/validation/ValidationPanel.tsx +0 -28
- package/src/collection_editor/ui/collection_editor/templates/blog_template.ts +0 -112
- package/src/collection_editor/ui/collection_editor/templates/pages_template.ts +0 -183
- package/src/collection_editor/ui/collection_editor/templates/products_template.ts +0 -88
- package/src/collection_editor/ui/collection_editor/templates/users_template.ts +0 -42
- package/src/collection_editor/ui/collection_editor/util.ts +0 -28
- package/src/collection_editor/ui/collection_editor/utils/strings.ts +0 -1
- package/src/collection_editor/ui/collection_editor/utils/supported_fields.tsx +0 -39
- package/src/collection_editor/ui/collection_editor/utils/update_property_for_widget.ts +0 -276
- package/src/collection_editor/useCollectionEditorController.tsx +0 -9
- package/src/collection_editor/useCollectionsConfigController.tsx +0 -9
- package/src/collection_editor/useJsonCollectionsConfigController.tsx +0 -297
- package/src/collection_editor/useLiveSchemaEditing.tsx +0 -315
- package/src/collection_editor/useLocalCollectionsConfigController.tsx +0 -341
- package/src/collection_editor/useSafeSnackbarController.ts +0 -9
- package/src/collection_editor/validateCollectionJson.ts +0 -440
- package/src/collection_editor_ui.ts +0 -21
- package/src/components/AdditionalFieldValue.tsx +0 -68
- package/src/components/AdminModeSyncer.tsx +0 -56
- package/src/components/ArrayContainer.tsx +0 -542
- package/src/components/ClearFilterSortButton.tsx +0 -56
- package/src/components/CollectionEditorDialogs.tsx +0 -69
- package/src/components/CollectionPanel.tsx +0 -179
- package/src/components/CollectionTableBinding/CollectionRowActions.tsx +0 -244
- package/src/components/CollectionTableBinding/CollectionTableBinding.tsx +0 -394
- package/src/components/CollectionTableBinding/CollectionTableBindingProps.tsx +0 -172
- package/src/components/CollectionTableBinding/PropertyTableCell.tsx +0 -350
- package/src/components/CollectionTableBinding/column_utils.tsx +0 -155
- package/src/components/CollectionTableBinding/fields/TableMultipleRelationField.tsx +0 -131
- package/src/components/CollectionTableBinding/fields/TableReferenceField.tsx +0 -193
- package/src/components/CollectionTableBinding/fields/TableRelationField.tsx +0 -192
- package/src/components/CollectionTableBinding/fields/TableRelationSelectorField.tsx +0 -42
- package/src/components/CollectionTableBinding/fields/TableStorageUpload.tsx +0 -324
- package/src/components/CollectionTableBinding/fields/VirtualTableSelect.tsx +0 -132
- package/src/components/CollectionTableBinding/fields/VirtualTableUserSelect.tsx +0 -41
- package/src/components/CollectionTableBinding/index.tsx +0 -14
- package/src/components/CollectionTableBinding/internal/CollectionTableToolbar.tsx +0 -96
- package/src/components/CollectionTableBinding/internal/EntityTableCell.tsx +0 -309
- package/src/components/CollectionTableBinding/internal/EntityTableCellActions.tsx +0 -80
- package/src/components/CollectionTableBinding/internal/common.tsx +0 -74
- package/src/components/CollectionTableBinding/internal/popup_field/PopupFormField.tsx +0 -422
- package/src/components/CollectionTableBinding/internal/popup_field/useDraggable.tsx +0 -99
- package/src/components/CollectionTableBinding/internal/popup_field/useWindowSize.tsx +0 -22
- package/src/components/CollectionTableBinding/table_bindings.tsx +0 -350
- package/src/components/CollectionViewBinding/BoardCardBinding.tsx +0 -263
- package/src/components/CollectionViewBinding/CollectionBoardViewBinding.tsx +0 -662
- package/src/components/CollectionViewBinding/CollectionCardViewBinding.tsx +0 -159
- package/src/components/CollectionViewBinding/CollectionListViewBinding.tsx +0 -1430
- package/src/components/CollectionViewBinding/CollectionViewActions.tsx +0 -227
- package/src/components/CollectionViewBinding/CollectionViewBinding.tsx +0 -1603
- package/src/components/CollectionViewBinding/CollectionViewStartActions.tsx +0 -240
- package/src/components/CollectionViewBinding/EntityCardBinding.tsx +0 -239
- package/src/components/CollectionViewBinding/FilterPresetsButton.tsx +0 -298
- package/src/components/CollectionViewBinding/FiltersDialog.tsx +0 -218
- package/src/components/CollectionViewBinding/SearchExplanation.tsx +0 -106
- package/src/components/CollectionViewBinding/SearchHighlight.tsx +0 -311
- package/src/components/CollectionViewBinding/SlotValue.tsx +0 -156
- package/src/components/CollectionViewBinding/SortButton.tsx +0 -354
- package/src/components/CollectionViewBinding/SplitListCloseButton.tsx +0 -49
- package/src/components/CollectionViewBinding/SplitListShowButton.tsx +0 -28
- package/src/components/CollectionViewBinding/SplitListView.tsx +0 -488
- package/src/components/CollectionViewBinding/SplitViewContext.tsx +0 -34
- package/src/components/CollectionViewBinding/ViewModeToggle.tsx +0 -250
- package/src/components/CollectionViewBinding/hooks/useCollectionInlineEditor.ts +0 -81
- package/src/components/CollectionViewBinding/hooks/useKanbanDragAndDrop.ts +0 -181
- package/src/components/CollectionViewBinding/useBoardDataController.tsx +0 -802
- package/src/components/CollectionViewBinding/usePreviewSlots.ts +0 -703
- package/src/components/CollectionViewBinding/useSelectionController.tsx +0 -40
- package/src/components/CollectionViewBinding/utils.ts +0 -22
- package/src/components/DefaultAppBar.tsx +0 -264
- package/src/components/DefaultDrawer.tsx +0 -593
- package/src/components/DeleteEntityDialog.tsx +0 -205
- package/src/components/DetailViewBinding.tsx +0 -731
- package/src/components/DrawerNavigationGroup.tsx +0 -141
- package/src/components/DrawerNavigationItem.tsx +0 -115
- package/src/components/EditFormActions.tsx +0 -303
- package/src/components/EditViewBinding.tsx +0 -1069
- package/src/components/EntityDisplayHeader.tsx +0 -170
- package/src/components/EntityIdentityBar.tsx +0 -428
- package/src/components/EntityInspector.tsx +0 -156
- package/src/components/EntityPreviewBinding.tsx +0 -432
- package/src/components/EntityPreviewNesting.tsx +0 -50
- package/src/components/EntityViewBinding.tsx +0 -374
- package/src/components/FieldCaption.tsx +0 -14
- package/src/components/HomePage/ContentHomePage.tsx +0 -586
- package/src/components/HomePage/FavouritesView.tsx +0 -60
- package/src/components/HomePage/HomePageDnD.tsx +0 -704
- package/src/components/HomePage/NavigationCard.tsx +0 -81
- package/src/components/HomePage/NavigationCardBinding.tsx +0 -107
- package/src/components/HomePage/NavigationGroup.tsx +0 -156
- package/src/components/HomePage/RenameGroupDialog.tsx +0 -123
- package/src/components/HomePage/SmallNavigationCard.tsx +0 -41
- package/src/components/HomePage/index.tsx +0 -5
- package/src/components/InlineEntityPreview.tsx +0 -149
- package/src/components/JsonPreviewBinding.tsx +0 -66
- package/src/components/PropertyCollectionView.tsx +0 -336
- package/src/components/PropertyConfigBadge.tsx +0 -28
- package/src/components/PropertyIdCopyTooltip.tsx +0 -47
- package/src/components/RebaseAuthGate.tsx +0 -44
- package/src/components/RebaseCMS.tsx +0 -30
- package/src/components/RebaseLayout.tsx +0 -80
- package/src/components/RebaseNavigation.tsx +0 -369
- package/src/components/RebaseRouteDefs.tsx +0 -127
- package/src/components/RebaseShell.tsx +0 -83
- package/src/components/ReferenceTable/SelectionTableBinding.tsx +0 -389
- package/src/components/ReferenceWidget.tsx +0 -156
- package/src/components/RelationSelector.tsx +0 -926
- package/src/components/SearchIconsView.tsx +0 -78
- package/src/components/SelectableTable/SelectableTable.tsx +0 -309
- package/src/components/SelectableTable/SelectableTableContext.tsx +0 -7
- package/src/components/SelectableTable/SelectionStore.ts +0 -43
- package/src/components/SelectableTable/filters/BooleanFilterField.tsx +0 -59
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +0 -146
- package/src/components/SelectableTable/filters/FilterFieldBinding.tsx +0 -190
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +0 -250
- package/src/components/SelectableTable/filters/RelationFilterField.tsx +0 -238
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -282
- package/src/components/SelectableTable/filters/null_filter.ts +0 -63
- package/src/components/SideDialogs.tsx +0 -238
- package/src/components/SidePanelBinding.tsx +0 -361
- package/src/components/SidePanelProvider.tsx +0 -80
- package/src/components/UserSelector.tsx +0 -513
- package/src/components/admin/CreationResultDialog.tsx +0 -138
- package/src/components/admin/index.ts +0 -1
- package/src/components/app/AppBar.tsx +0 -17
- package/src/components/app/Drawer.tsx +0 -38
- package/src/components/app/Scaffold.tsx +0 -366
- package/src/components/app/index.ts +0 -4
- package/src/components/app/useApp.tsx +0 -36
- package/src/components/common/default_entity_actions.tsx +0 -440
- package/src/components/common/index.ts +0 -1
- package/src/components/field_configs.tsx +0 -506
- package/src/components/history/EntityHistoryEntry.tsx +0 -203
- package/src/components/history/EntityHistoryView.tsx +0 -206
- package/src/components/history/LastEditedByFormAction.tsx +0 -24
- package/src/components/history/LastEditedByIndicator.tsx +0 -95
- package/src/components/history/UserChip.tsx +0 -15
- package/src/components/history/index.tsx +0 -5
- package/src/components/index.ts +0 -63
- package/src/contexts/BreacrumbsContext.tsx +0 -45
- package/src/contexts/SideDialogsControllerContext.tsx +0 -5
- package/src/contexts/SidePanelControllerContext.tsx +0 -5
- package/src/data_export/export/BasicExportAction.tsx +0 -142
- package/src/data_export/export/ExportCollectionAction.tsx +0 -321
- package/src/data_export/export/export.ts +0 -295
- package/src/data_export/export/fetch_export_data.ts +0 -90
- package/src/data_export/export/index.ts +0 -4
- package/src/data_export/index.ts +0 -1
- package/src/data_import/components/DataNewPropertiesMapping.tsx +0 -261
- package/src/data_import/components/ImportFileUpload.tsx +0 -42
- package/src/data_import/components/ImportNewPropertyFieldPreview.tsx +0 -58
- package/src/data_import/components/ImportSaveInProgress.tsx +0 -121
- package/src/data_import/components/index.ts +0 -4
- package/src/data_import/hooks/index.ts +0 -1
- package/src/data_import/hooks/useImportConfig.tsx +0 -34
- package/src/data_import/import/ImportCollectionAction.tsx +0 -461
- package/src/data_import/import/index.ts +0 -1
- package/src/data_import/index.ts +0 -5
- package/src/data_import/types/column_mapping.ts +0 -32
- package/src/data_import/types/index.ts +0 -1
- package/src/data_import/utils/csv.ts +0 -153
- package/src/data_import/utils/data.ts +0 -181
- package/src/data_import/utils/file_headers.ts +0 -14
- package/src/data_import/utils/file_to_json.ts +0 -149
- package/src/data_import/utils/get_import_inference_type.ts +0 -33
- package/src/data_import/utils/get_properties_mapping.ts +0 -67
- package/src/data_import/utils/index.ts +0 -5
- package/src/data_import/utils/save_entities.ts +0 -138
- package/src/data_import/utils/transforms.ts +0 -70
- package/src/editor/components/SlashCommandMenu.tsx +0 -532
- package/src/editor/components/editor-bubble-item.tsx +0 -32
- package/src/editor/components/editor-bubble.tsx +0 -143
- package/src/editor/components/image-bubble.tsx +0 -183
- package/src/editor/components/index.ts +0 -14
- package/src/editor/components/table-bubble.tsx +0 -233
- package/src/editor/editor.tsx +0 -529
- package/src/editor/extensions/HighlightDecorationExtension.ts +0 -115
- package/src/editor/extensions/Image.ts +0 -159
- package/src/editor/extensions/TextLoadingDecorationExtension.tsx +0 -107
- package/src/editor/extensions/clipboard.ts +0 -72
- package/src/editor/extensions/custom-keymap.ts +0 -25
- package/src/editor/extensions/drag-and-drop.tsx +0 -508
- package/src/editor/hooks/useProseMirror.ts +0 -126
- package/src/editor/hooks/useProseMirrorContext.ts +0 -15
- package/src/editor/index.ts +0 -2
- package/src/editor/markdown-it-plugins.d.ts +0 -24
- package/src/editor/markdown.ts +0 -204
- package/src/editor/nodeViews/ImageComponent.tsx +0 -20
- package/src/editor/nodeViews/ReactNodeView.tsx +0 -89
- package/src/editor/nodeViews/TaskItemComponent.tsx +0 -30
- package/src/editor/nodeViews/index.ts +0 -35
- package/src/editor/plugins/index.ts +0 -58
- package/src/editor/plugins/inputrules.ts +0 -82
- package/src/editor/plugins/placeholderPlugin.ts +0 -55
- package/src/editor/plugins/slashCommandPlugin.ts +0 -65
- package/src/editor/schema.ts +0 -339
- package/src/editor/selectors/ai-selector.tsx +0 -110
- package/src/editor/selectors/color-selector.tsx +0 -199
- package/src/editor/selectors/link-selector.tsx +0 -122
- package/src/editor/selectors/node-selector.tsx +0 -159
- package/src/editor/selectors/text-buttons.tsx +0 -79
- package/src/editor/types.ts +0 -6
- package/src/editor/useProseMirror.ts +0 -125
- package/src/editor/utils/prosemirror-utils.ts +0 -113
- package/src/editor/utils/remove_classes.ts +0 -18
- package/src/editor/utils/useDebouncedCallback.ts +0 -25
- package/src/form/EntityForm.tsx +0 -761
- package/src/form/EntityFormActions.tsx +0 -160
- package/src/form/EntityFormBinding.tsx +0 -342
- package/src/form/PropertyFieldBinding.tsx +0 -348
- package/src/form/components/ErrorFocus.tsx +0 -44
- package/src/form/components/FieldBlock.tsx +0 -189
- package/src/form/components/FieldHelperText.tsx +0 -84
- package/src/form/components/FormRail.tsx +0 -153
- package/src/form/components/FormSections.tsx +0 -145
- package/src/form/components/LabelWithIcon.tsx +0 -43
- package/src/form/components/LabelWithIconAndTooltip.tsx +0 -28
- package/src/form/components/LocalChangesMenu.tsx +0 -147
- package/src/form/components/StorageItemPreview.tsx +0 -81
- package/src/form/components/StorageUploadProgress.tsx +0 -118
- package/src/form/components/index.tsx +0 -6
- package/src/form/components/useRailVisible.tsx +0 -47
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +0 -109
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -172
- package/src/form/field_bindings/BinaryFieldBinding.tsx +0 -177
- package/src/form/field_bindings/BlockFieldBinding.tsx +0 -282
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +0 -78
- package/src/form/field_bindings/GeopointFieldBinding.tsx +0 -200
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -574
- package/src/form/field_bindings/MapFieldBinding.tsx +0 -120
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +0 -259
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +0 -130
- package/src/form/field_bindings/MultipleRelationFieldBinding.tsx +0 -168
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +0 -94
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +0 -146
- package/src/form/field_bindings/RelationFieldBinding.tsx +0 -202
- package/src/form/field_bindings/RepeatFieldBinding.tsx +0 -143
- package/src/form/field_bindings/SelectFieldBinding.tsx +0 -111
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +0 -521
- package/src/form/field_bindings/SwitchFieldBinding.tsx +0 -70
- package/src/form/field_bindings/TextFieldBinding.tsx +0 -189
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +0 -71
- package/src/form/field_bindings/VectorFieldBinding.tsx +0 -209
- package/src/form/form_utils.ts +0 -243
- package/src/form/index.tsx +0 -36
- package/src/form/useClearRestoreValue.tsx +0 -36
- package/src/form/useUndoableDiscard.ts +0 -39
- package/src/form/validation.ts +0 -583
- package/src/hooks/index.ts +0 -16
- package/src/hooks/navigation/contexts/CollectionRegistryContext.tsx +0 -22
- package/src/hooks/navigation/contexts/NavigationStateContext.tsx +0 -16
- package/src/hooks/navigation/contexts/UrlContext.tsx +0 -24
- package/src/hooks/navigation/contexts/index.ts +0 -14
- package/src/hooks/navigation/useAsyncResolver.ts +0 -100
- package/src/hooks/navigation/useBuildCollectionRegistryController.tsx +0 -218
- package/src/hooks/navigation/useBuildNavigationStateController.tsx +0 -134
- package/src/hooks/navigation/useBuildUrlController.tsx +0 -66
- package/src/hooks/navigation/useNavigationResolution.ts +0 -271
- package/src/hooks/navigation/useResolvedCollections.ts +0 -102
- package/src/hooks/navigation/useResolvedViews.tsx +0 -139
- package/src/hooks/navigation/useTopLevelNavigation.ts +0 -273
- package/src/hooks/navigation/utils.ts +0 -200
- package/src/hooks/useAdminContext.tsx +0 -83
- package/src/hooks/useBreadcrumbsController.tsx +0 -17
- package/src/hooks/useBuildSideDialogsController.tsx +0 -145
- package/src/hooks/useBuildSidePanel.tsx +0 -315
- package/src/hooks/useChildViewSource.ts +0 -46
- package/src/hooks/useEntityDisplay.tsx +0 -262
- package/src/hooks/useEntityDisplayTitle.tsx +0 -49
- package/src/hooks/useHistory.ts +0 -178
- package/src/hooks/useRecordActions.tsx +0 -69
- package/src/hooks/useResolvedNavigationFrom.tsx +0 -167
- package/src/hooks/useResolvedUsers.ts +0 -148
- package/src/hooks/useSelectionDialog.tsx +0 -58
- package/src/hooks/useSideDialogsController.tsx +0 -25
- package/src/hooks/useSidePanel.tsx +0 -13
- package/src/index.ts +0 -124
- package/src/preview/PropertyPreview.tsx +0 -331
- package/src/preview/compact_text.ts +0 -68
- package/src/preview/components/ArrayEnumPreview.tsx +0 -39
- package/src/preview/components/BooleanPreview.tsx +0 -28
- package/src/preview/components/DatePreview.tsx +0 -106
- package/src/preview/components/EmptyValue.tsx +0 -10
- package/src/preview/components/EnumValuesChip.tsx +0 -39
- package/src/preview/components/ImagePreview.tsx +0 -144
- package/src/preview/components/InlineEntityListPreview.tsx +0 -42
- package/src/preview/components/ReferencePreview.tsx +0 -235
- package/src/preview/components/RelationPreview.tsx +0 -271
- package/src/preview/components/StorageThumbnail.tsx +0 -147
- package/src/preview/components/UrlComponentPreview.tsx +0 -116
- package/src/preview/components/UserPreview.tsx +0 -30
- package/src/preview/components/date_fns_locales.ts +0 -125
- package/src/preview/index.ts +0 -26
- package/src/preview/property_previews/ArrayOfMapsPreview.tsx +0 -73
- package/src/preview/property_previews/ArrayOfReferencesPreview.tsx +0 -67
- package/src/preview/property_previews/ArrayOfRelationsPreview.tsx +0 -77
- package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +0 -49
- package/src/preview/property_previews/ArrayOfStringsPreview.tsx +0 -70
- package/src/preview/property_previews/ArrayOneOfPreview.tsx +0 -79
- package/src/preview/property_previews/ArrayPropertyEnumPreview.tsx +0 -35
- package/src/preview/property_previews/ArrayPropertyPreview.tsx +0 -79
- package/src/preview/property_previews/MapPropertyPreview.tsx +0 -198
- package/src/preview/property_previews/NumberPropertyPreview.tsx +0 -39
- package/src/preview/property_previews/SkeletonPropertyComponent.tsx +0 -276
- package/src/preview/property_previews/StringPropertyPreview.tsx +0 -70
- package/src/preview/util.ts +0 -39
- package/src/routes/CustomViewRoute.tsx +0 -33
- package/src/routes/RebaseRoute.tsx +0 -467
- package/src/routes/index.ts +0 -2
- package/src/types/components/EntityFormActionsProps.tsx +0 -20
- package/src/types/components/EntityFormProps.tsx +0 -119
- package/src/types/components/PropertyPreviewProps.tsx +0 -85
- package/src/types/components/formex.tsx +0 -2
- package/src/types/components/index.ts +0 -3
- package/src/types/fields.tsx +0 -340
- package/src/types/index.ts +0 -4
- package/src/util/dataControllerAdapter.ts +0 -88
- package/src/util/entity_actions.ts +0 -28
- package/src/util/index.ts +0 -5
- package/src/util/navigation_utils.ts +0 -89
- package/src/util/number_format.ts +0 -46
- package/src/util/previews.ts +0 -131
- package/src/util/propertyConfigMapper.ts +0 -134
- package/src/util/property_utils.tsx +0 -236
- package/src/util/resolutions.ts +0 -96
- package/src/util/view_constants.ts +0 -33
- package/src/util/view_mode.ts +0 -176
|
@@ -1,1603 +0,0 @@
|
|
|
1
|
-
import type { OrderByTuple, Property } from "@rebasepro/types";
|
|
2
|
-
import { serializeOrderBy } from "@rebasepro/common";
|
|
3
|
-
import type { AdditionalFieldDelegate, EntityAction, AdminCollection } from "@rebasepro/cms-types";
|
|
4
|
-
import {
|
|
5
|
-
Entity,
|
|
6
|
-
EntityReference,
|
|
7
|
-
FilterValues,
|
|
8
|
-
getCollectionDataPath
|
|
9
|
-
} from "@rebasepro/types";
|
|
10
|
-
import {
|
|
11
|
-
CollectionCustomViewParams,
|
|
12
|
-
CollectionSize,
|
|
13
|
-
EntityTableController,
|
|
14
|
-
PartialCollectionConfig,
|
|
15
|
-
ViewMode
|
|
16
|
-
} from "@rebasepro/cms-types";
|
|
17
|
-
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
|
18
|
-
import { useSearchParams } from "react-router";
|
|
19
|
-
|
|
20
|
-
import { deepEqual as equal } from "fast-equals"
|
|
21
|
-
import { CollectionRowActions, CollectionTableBinding } from "../CollectionTableBinding";
|
|
22
|
-
import { CollectionTableToolbar } from "../CollectionTableBinding/internal/CollectionTableToolbar";
|
|
23
|
-
import { getEntityChildViews } from "@rebasepro/common";
|
|
24
|
-
import { useCollectionInlineEditor } from "./hooks/useCollectionInlineEditor";
|
|
25
|
-
import { navigateToEntity } from "../../util/navigation_utils";
|
|
26
|
-
import { mergeEntityActions } from "../../util/entity_actions";
|
|
27
|
-
import { resolveCollectionViews, resolveEntityAction } from "../../util/resolutions";
|
|
28
|
-
import { getPropertyInPath } from "../../util/property_utils";
|
|
29
|
-
import { ReferencePreview } from "../../preview";
|
|
30
|
-
import {
|
|
31
|
-
CollectionScopeProvider,
|
|
32
|
-
OnColumnResizeParams,
|
|
33
|
-
getRedundantChildViewColumnIds,
|
|
34
|
-
useAnalyticsController,
|
|
35
|
-
useAuthController,
|
|
36
|
-
useColumnIds,
|
|
37
|
-
useComponentOverride,
|
|
38
|
-
useCustomizationController,
|
|
39
|
-
useData,
|
|
40
|
-
useDataTableController,
|
|
41
|
-
useLargeLayout,
|
|
42
|
-
usePermissions,
|
|
43
|
-
useScrollRestoration,
|
|
44
|
-
useSlot,
|
|
45
|
-
useTranslation,
|
|
46
|
-
useUserConfigurationPersistence
|
|
47
|
-
} from "@rebasepro/app";
|
|
48
|
-
import { CollectionViewActions } from "./CollectionViewActions";
|
|
49
|
-
import { CollectionCardViewBinding } from "./CollectionCardViewBinding";
|
|
50
|
-
import { CollectionListViewBinding } from "./CollectionListViewBinding";
|
|
51
|
-
import { SplitListView } from "./SplitListView";
|
|
52
|
-
import { CollectionBoardViewBinding } from "./CollectionBoardViewBinding";
|
|
53
|
-
import { KanbanPropertyOption, ViewModeToggle } from "./ViewModeToggle";
|
|
54
|
-
import {
|
|
55
|
-
ArrowRightToLineIcon,
|
|
56
|
-
Button,
|
|
57
|
-
cls,
|
|
58
|
-
ErrorBoundary,
|
|
59
|
-
focusedDisabled,
|
|
60
|
-
IconButton,
|
|
61
|
-
iconSize,
|
|
62
|
-
PlusIcon,
|
|
63
|
-
Popover,
|
|
64
|
-
SearchIcon,
|
|
65
|
-
TextField,
|
|
66
|
-
Tooltip,
|
|
67
|
-
Typography,
|
|
68
|
-
VirtualTableColumn
|
|
69
|
-
} from "@rebasepro/ui";
|
|
70
|
-
import { getSubcollectionColumnId } from "../CollectionTableBinding/internal/common";
|
|
71
|
-
import { copyEntityAction, deleteEntityAction, editEntityAction, unlinkEntityAction } from "../common/default_entity_actions";
|
|
72
|
-
import { PopupFormField } from "../CollectionTableBinding/internal/popup_field/PopupFormField";
|
|
73
|
-
import { GetPropertyForProps } from "../CollectionTableBinding/CollectionTableBindingProps";
|
|
74
|
-
import { DeleteEntityDialog } from "../DeleteEntityDialog";
|
|
75
|
-
import { useSelectionController } from "./useSelectionController";
|
|
76
|
-
import { CollectionViewStartActions } from "./CollectionViewStartActions";
|
|
77
|
-
import { addRecentId, getRecentIds } from "./utils";
|
|
78
|
-
import { isViewMode, OpenEntityMode, resolveOpenEntityMode, VIEW_MODE_PARAM, VIEW_MODES } from "../../util/view_mode";
|
|
79
|
-
import { mergeDeep } from "@rebasepro/utils";
|
|
80
|
-
import { useAdminContext } from "../../hooks/useAdminContext";
|
|
81
|
-
import { useCollectionRegistryController } from "../../hooks/navigation/contexts/CollectionRegistryContext";
|
|
82
|
-
import { useSidePanel } from "../../hooks/useSidePanel";
|
|
83
|
-
import { useUrlController } from "../../hooks/navigation/contexts/UrlContext";
|
|
84
|
-
import { useChildViewSource } from "../../hooks/useChildViewSource";
|
|
85
|
-
import { useSelectionDialog } from "../../hooks/useSelectionDialog";
|
|
86
|
-
import { resolveComponentRef, saveEntityWithCallbacks } from "@rebasepro/app";
|
|
87
|
-
|
|
88
|
-
const EMPTY_ARRAY: never[] = [];
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @group Components
|
|
92
|
-
*/
|
|
93
|
-
export type CollectionViewBindingProps<M extends Record<string, unknown>> = {
|
|
94
|
-
/**
|
|
95
|
-
* Complete path where this collection is located.
|
|
96
|
-
* It defaults to the collection path if not provided.
|
|
97
|
-
*/
|
|
98
|
-
path?: string;
|
|
99
|
-
/**
|
|
100
|
-
* Full path using navigation ids.
|
|
101
|
-
*/
|
|
102
|
-
idPath?: string;
|
|
103
|
-
/**
|
|
104
|
-
* If this is a subcollection, specify the parent collection ids.
|
|
105
|
-
*/
|
|
106
|
-
parentCollectionSlugs?: string[], parentEntityIds?: string[];
|
|
107
|
-
/**
|
|
108
|
-
* Whether this is a subcollection or not.
|
|
109
|
-
*/
|
|
110
|
-
isSubCollection?: boolean;
|
|
111
|
-
|
|
112
|
-
className?: string;
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* If true, this view will store its filter and sorting status in the url params
|
|
116
|
-
*/
|
|
117
|
-
updateUrl?: boolean;
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* When provided, the split view will render this entity's detail panel.
|
|
121
|
-
* Used by the router to pass the entity ID from the URL path.
|
|
122
|
-
*/
|
|
123
|
-
selectedEntityId?: string | number;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* When provided, the split view will open this tab (e.g. a subcollection slug)
|
|
127
|
-
* in the entity detail panel. Used by the router to pass the subcollection from the URL.
|
|
128
|
-
*/
|
|
129
|
-
selectedTab?: string;
|
|
130
|
-
|
|
131
|
-
} & AdminCollection<M>;
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* This component is in charge of binding a driver path with an {@link AdminCollection}
|
|
135
|
-
* where it's configuration is defined. It includes an infinite scrolling table
|
|
136
|
-
* and a 'Add' new entities button,
|
|
137
|
-
*
|
|
138
|
-
* This component is the default one used for displaying entity collections
|
|
139
|
-
* and is in charge of generating all the specific actions and customization
|
|
140
|
-
* of the lower level {@link CollectionTableBinding}
|
|
141
|
-
*
|
|
142
|
-
* Please **note** that you only need to use this component if you are building
|
|
143
|
-
* a custom view. If you just need to create a default view you can do it
|
|
144
|
-
* exclusively with config options.
|
|
145
|
-
*
|
|
146
|
-
* If you need a lower level implementation with more granular options, you
|
|
147
|
-
* can use {@link CollectionTableBinding}.
|
|
148
|
-
*
|
|
149
|
-
* If you need a generic table that is not bound to the driver or entities and
|
|
150
|
-
* properties at all, you can check {@link VirtualTable}
|
|
151
|
-
*
|
|
152
|
-
* @param path
|
|
153
|
-
* @param collection
|
|
154
|
-
|
|
155
|
-
* @group Components
|
|
156
|
-
*/
|
|
157
|
-
const CollectionViewBindingInner = React.memo(
|
|
158
|
-
function CollectionViewBindingInner<M extends Record<string, unknown>>({
|
|
159
|
-
path: pathProp,
|
|
160
|
-
|
|
161
|
-
parentCollectionSlugs, parentEntityIds,
|
|
162
|
-
isSubCollection,
|
|
163
|
-
className,
|
|
164
|
-
updateUrl,
|
|
165
|
-
selectedEntityId: selectedEntityIdProp,
|
|
166
|
-
selectedTab: selectedTabProp,
|
|
167
|
-
...collectionProp
|
|
168
|
-
}: CollectionViewBindingProps<M>
|
|
169
|
-
) {
|
|
170
|
-
|
|
171
|
-
const { t } = useTranslation();
|
|
172
|
-
const context = useAdminContext();
|
|
173
|
-
const collectionRegistry = useCollectionRegistryController();
|
|
174
|
-
const urlController = useUrlController();
|
|
175
|
-
const path = pathProp ?? getCollectionDataPath(collectionProp);
|
|
176
|
-
const dataClient = useData();
|
|
177
|
-
const sidePanelController = useSidePanel();
|
|
178
|
-
const authController = useAuthController();
|
|
179
|
-
const userConfigPersistence = useUserConfigurationPersistence();
|
|
180
|
-
const analyticsController = useAnalyticsController();
|
|
181
|
-
const customizationController = useCustomizationController();
|
|
182
|
-
const { canCreate, canEdit, canDelete } = usePermissions();
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Rows shared with other parents through a junction. The parent owns
|
|
187
|
-
* the link, not the row — so the destructive action is a removal from
|
|
188
|
-
* this record, which is what the server performs for such a path.
|
|
189
|
-
*/
|
|
190
|
-
const childViewSource = useChildViewSource(path);
|
|
191
|
-
const isLinkedChildView = childViewSource?.kind === "relation" && childViewSource.mode === "linked";
|
|
192
|
-
const linkedTargetSlug = isLinkedChildView && childViewSource.kind === "relation"
|
|
193
|
-
? childViewSource.targetSlug
|
|
194
|
-
: undefined;
|
|
195
|
-
|
|
196
|
-
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
197
|
-
|
|
198
|
-
const scrollRestoration = useScrollRestoration();
|
|
199
|
-
|
|
200
|
-
const collection = useMemo(() => {
|
|
201
|
-
const registryCollection = collectionRegistry.getCollection(path) || collectionProp;
|
|
202
|
-
const userOverride = userConfigPersistence?.getCollectionConfig<M>(path);
|
|
203
|
-
return (userOverride ? mergeDeep(registryCollection, userOverride) : registryCollection) as AdminCollection<M>;
|
|
204
|
-
}, [collectionProp, path, userConfigPersistence, collectionRegistry]);
|
|
205
|
-
|
|
206
|
-
const collectionRef = React.useRef(collection);
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
collectionRef.current = collection;
|
|
209
|
-
}, [collection]);
|
|
210
|
-
|
|
211
|
-
const canCreateEntities = canCreate(collection, path);
|
|
212
|
-
const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
|
|
213
|
-
const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
|
|
214
|
-
|
|
215
|
-
const [lastDeleteTimestamp, setLastDeleteTimestamp] = React.useState<number>(0);
|
|
216
|
-
|
|
217
|
-
// Track recently deleted entities for optimistic Kanban count updates
|
|
218
|
-
const [deletedEntities, setDeletedEntities] = React.useState<Entity<M>[]>([]);
|
|
219
|
-
|
|
220
|
-
// number of entities in the collection (undefined = loading)
|
|
221
|
-
const [docsCount, setDocsCount] = useState<number | null | undefined>(null);
|
|
222
|
-
|
|
223
|
-
// Optimistic state for column order to prevent UI flickering during persistence
|
|
224
|
-
const [localPropertiesOrder, setLocalPropertiesOrder] = useState<string[] | undefined>(collection.propertiesOrder);
|
|
225
|
-
|
|
226
|
-
// Sync local state with collection's propertiesOrder when it changes from external sources
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
setLocalPropertiesOrder(collection.propertiesOrder);
|
|
229
|
-
}, [collection.propertiesOrder]);
|
|
230
|
-
|
|
231
|
-
const unselectNavigatedEntity = useCallback(() => {
|
|
232
|
-
const currentSelection = highlightedEntity;
|
|
233
|
-
setTimeout(() => {
|
|
234
|
-
if (currentSelection === highlightedEntity)
|
|
235
|
-
setHighlightedEntity(undefined);
|
|
236
|
-
}, 2400);
|
|
237
|
-
}, [highlightedEntity]);
|
|
238
|
-
|
|
239
|
-
const checkInlineEditing = useCallback((entity?: Entity<M>): boolean => {
|
|
240
|
-
const collection = collectionRef.current;
|
|
241
|
-
if (!canEdit(collection, path, entity ?? null)) {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
return collection.inlineEditing === undefined || collection.inlineEditing;
|
|
245
|
-
}, [canEdit, path]);
|
|
246
|
-
|
|
247
|
-
const selectionEnabled = collection.selectionEnabled === undefined || collection.selectionEnabled;
|
|
248
|
-
const hoverRow = !checkInlineEditing();
|
|
249
|
-
|
|
250
|
-
const [popOverOpen, setPopOverOpen] = useState(false);
|
|
251
|
-
|
|
252
|
-
// Custom view modes this collection can render: the ones declared
|
|
253
|
-
// inline plus the ones it names by key from the app-level registry.
|
|
254
|
-
// Anything unresolvable is dropped here, once, so every consumer below
|
|
255
|
-
// — the switcher, `enabledViews`, the URL and saved-config checks —
|
|
256
|
-
// agrees on what exists.
|
|
257
|
-
const resolvedCustomViews = useMemo(
|
|
258
|
-
() => resolveCollectionViews(collection.customViews, customizationController.collectionViews),
|
|
259
|
-
[collection.customViews, customizationController]
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const customViewKeys = useMemo(
|
|
263
|
-
() => resolvedCustomViews.map((v) => v.key),
|
|
264
|
-
[resolvedCustomViews]
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
// View mode priority: URL > saved user config > collection.defaultViewMode
|
|
268
|
-
const defaultViewMode = isViewMode(collection.defaultViewMode, customViewKeys)
|
|
269
|
-
? collection.defaultViewMode
|
|
270
|
-
// Covers a `defaultViewMode` naming a custom view that has since
|
|
271
|
-
// been removed from config; without this the collection would open
|
|
272
|
-
// on a mode nothing renders.
|
|
273
|
-
: "list";
|
|
274
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
275
|
-
|
|
276
|
-
// Read view from React Router's searchParams (reactive on back/forward)
|
|
277
|
-
const urlView = useMemo((): ViewMode | null => {
|
|
278
|
-
const v = searchParams.get(VIEW_MODE_PARAM);
|
|
279
|
-
return isViewMode(v, customViewKeys) ? v : null;
|
|
280
|
-
}, [searchParams, customViewKeys]);
|
|
281
|
-
|
|
282
|
-
// Get saved view from local persistence.
|
|
283
|
-
//
|
|
284
|
-
// Validated rather than cast: what is stored is whatever the user last
|
|
285
|
-
// picked, and a custom view removed from config since then would come
|
|
286
|
-
// back as a mode nothing renders — a blank panel with a working
|
|
287
|
-
// toolbar above it. An unrecognised value reads as "nothing saved",
|
|
288
|
-
// which falls through to the collection default.
|
|
289
|
-
const getSavedView = useCallback((): ViewMode | null => {
|
|
290
|
-
const saved = userConfigPersistence?.getCollectionConfig<M>(path)?.defaultViewMode;
|
|
291
|
-
return isViewMode(saved, customViewKeys) ? saved : null;
|
|
292
|
-
}, [userConfigPersistence, path, customViewKeys]);
|
|
293
|
-
|
|
294
|
-
const [viewMode, setViewModeState] = useState<ViewMode>(() => {
|
|
295
|
-
// Priority: URL > saved config > collection default
|
|
296
|
-
if (urlView) return urlView;
|
|
297
|
-
const savedView = getSavedView();
|
|
298
|
-
if (savedView) return savedView;
|
|
299
|
-
return defaultViewMode;
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
const activeCustomView = useMemo(
|
|
303
|
-
() => resolvedCustomViews.find((v) => v.key === viewMode),
|
|
304
|
-
[resolvedCustomViews, viewMode]
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
const openEntityMode = resolveOpenEntityMode({
|
|
308
|
-
collection,
|
|
309
|
-
viewMode,
|
|
310
|
-
customView: activeCustomView
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Sync URL with current view on init (if view came from saved config)
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
if (!urlView && viewMode !== defaultViewMode) {
|
|
316
|
-
// View came from saved config but URL doesn't have it - update URL without push
|
|
317
|
-
// Built from `window.location.search`, not React Router's
|
|
318
|
-
// `prev`. The table controller writes the filter, sort and
|
|
319
|
-
// search params with `history.replaceState`, which React Router
|
|
320
|
-
// never observes — so `prev` is a stale snapshot, and merging
|
|
321
|
-
// into it silently dropped whatever it had not seen. Switching
|
|
322
|
-
// view mode after typing a search wiped `search=` from the URL,
|
|
323
|
-
// and the next reload came back unfiltered.
|
|
324
|
-
setSearchParams(() => {
|
|
325
|
-
const next = new URLSearchParams(window.location.search);
|
|
326
|
-
next.set(VIEW_MODE_PARAM, viewMode);
|
|
327
|
-
return next;
|
|
328
|
-
}, { replace: true });
|
|
329
|
-
}
|
|
330
|
-
}, []); // Only on mount
|
|
331
|
-
|
|
332
|
-
// Update URL when view mode changes (user action)
|
|
333
|
-
const setViewMode = useCallback((newMode: ViewMode) => {
|
|
334
|
-
setViewModeState(newMode);
|
|
335
|
-
|
|
336
|
-
// Update URL with __view param via React Router
|
|
337
|
-
setSearchParams(() => {
|
|
338
|
-
// See the note above: the live URL is the only writer both
|
|
339
|
-
// this and the table controller agree on.
|
|
340
|
-
const next = new URLSearchParams(window.location.search);
|
|
341
|
-
if (newMode === defaultViewMode) {
|
|
342
|
-
next.delete(VIEW_MODE_PARAM);
|
|
343
|
-
} else {
|
|
344
|
-
next.set(VIEW_MODE_PARAM, newMode);
|
|
345
|
-
}
|
|
346
|
-
return next;
|
|
347
|
-
});
|
|
348
|
-
}, [defaultViewMode, setSearchParams]);
|
|
349
|
-
|
|
350
|
-
// Sync viewMode state when URL changes via back/forward navigation
|
|
351
|
-
useEffect(() => {
|
|
352
|
-
if (urlView) {
|
|
353
|
-
setViewModeState(urlView);
|
|
354
|
-
} else {
|
|
355
|
-
const savedView = getSavedView();
|
|
356
|
-
setViewModeState(savedView ?? defaultViewMode);
|
|
357
|
-
}
|
|
358
|
-
}, [urlView, getSavedView, defaultViewMode]);
|
|
359
|
-
|
|
360
|
-
// List view size state - controls row height and info density
|
|
361
|
-
const [listSize, setListSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
362
|
-
|
|
363
|
-
// Card view size state - controls the grid column count
|
|
364
|
-
const [cardSize, setCardSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
365
|
-
|
|
366
|
-
// Table view size state - controls row height
|
|
367
|
-
const [tableSize, setTableSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
368
|
-
|
|
369
|
-
// Size for custom views that opted in with `sizeable`. One piece of
|
|
370
|
-
// state for all of them: only the active view reads it, and switching
|
|
371
|
-
// between two sizeable custom views keeping the same density is the
|
|
372
|
-
// less surprising behaviour.
|
|
373
|
-
const [customViewSize, setCustomViewSize] = useState<CollectionSize>(collection.defaultSize ?? "m");
|
|
374
|
-
|
|
375
|
-
const selectionController = useSelectionController<M>();
|
|
376
|
-
const usedSelectionController = collection.selectionController ?? selectionController;
|
|
377
|
-
const {
|
|
378
|
-
selectedEntities,
|
|
379
|
-
setSelectedEntities
|
|
380
|
-
} = usedSelectionController;
|
|
381
|
-
|
|
382
|
-
const tableController = useDataTableController<M>({
|
|
383
|
-
path,
|
|
384
|
-
collection,
|
|
385
|
-
lastDeleteTimestamp,
|
|
386
|
-
scrollRestoration,
|
|
387
|
-
updateUrl
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
const tableKey = React.useRef<string>(Math.random().toString(36));
|
|
391
|
-
const popupCell = tableController.popupCell as {
|
|
392
|
-
entityId: string | number;
|
|
393
|
-
propertyKey: Extract<keyof M, string>;
|
|
394
|
-
cellRect?: DOMRect;
|
|
395
|
-
} | undefined;
|
|
396
|
-
|
|
397
|
-
const onPopupClose = useCallback(() => {
|
|
398
|
-
tableController.setPopupCell?.(undefined);
|
|
399
|
-
}, [tableController.setPopupCell]);
|
|
400
|
-
|
|
401
|
-
const onEntityClick = useCallback((clickedEntity: Entity<M>) => {
|
|
402
|
-
const collection = collectionRef.current;
|
|
403
|
-
setHighlightedEntity(clickedEntity);
|
|
404
|
-
analyticsController.onAnalyticsEvent?.("edit_entity_clicked", {
|
|
405
|
-
path: clickedEntity.path,
|
|
406
|
-
entityId: clickedEntity.id
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
if (collection) {
|
|
410
|
-
addRecentId(collection.slug, clickedEntity.id);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const entityPath = path ?? clickedEntity.path;
|
|
414
|
-
navigateToEntity({
|
|
415
|
-
navigation: urlController,
|
|
416
|
-
path: entityPath,
|
|
417
|
-
sidePanelController,
|
|
418
|
-
openEntityMode,
|
|
419
|
-
collection,
|
|
420
|
-
entityId: clickedEntity.id,
|
|
421
|
-
replace: openEntityMode === "split" && selectedEntityIdProp !== undefined
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
}, [sidePanelController, openEntityMode, selectedEntityIdProp, path, urlController, analyticsController]);
|
|
425
|
-
|
|
426
|
-
const onNewClick = useCallback(() => {
|
|
427
|
-
const collection = collectionRef.current;
|
|
428
|
-
analyticsController.onAnalyticsEvent?.("new_entity_click", {
|
|
429
|
-
path: path
|
|
430
|
-
});
|
|
431
|
-
navigateToEntity({
|
|
432
|
-
openEntityMode,
|
|
433
|
-
collection,
|
|
434
|
-
entityId: undefined,
|
|
435
|
-
path: path,
|
|
436
|
-
sidePanelController,
|
|
437
|
-
navigation: urlController,
|
|
438
|
-
onClose: unselectNavigatedEntity
|
|
439
|
-
})
|
|
440
|
-
}, [path, sidePanelController]);
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Attach rows that already exist to this parent.
|
|
444
|
-
*
|
|
445
|
-
* Writing to the row's own address under this parent — `PUT
|
|
446
|
-
* posts/1/tags/5` — is what asserts membership; the backend writes the
|
|
447
|
-
* junction row idempotently. Sending no values keeps it a pure link:
|
|
448
|
-
* the target's own fields are none of this tab's business.
|
|
449
|
-
*/
|
|
450
|
-
const linkExistingEntities = useCallback(async (entities: Entity<M>[]) => {
|
|
451
|
-
const alreadyLinked = new Set(tableController.data.map(e => String(e.id)));
|
|
452
|
-
const toLink = entities.filter(e => !alreadyLinked.has(String(e.id)));
|
|
453
|
-
if (toLink.length === 0) return;
|
|
454
|
-
|
|
455
|
-
try {
|
|
456
|
-
await Promise.all(toLink.map(entity =>
|
|
457
|
-
saveEntityWithCallbacks<M>({
|
|
458
|
-
path,
|
|
459
|
-
entityId: entity.id,
|
|
460
|
-
values: {} as Partial<M>,
|
|
461
|
-
status: "existing",
|
|
462
|
-
collection,
|
|
463
|
-
data: dataClient,
|
|
464
|
-
context
|
|
465
|
-
})
|
|
466
|
-
));
|
|
467
|
-
setLastDeleteTimestamp(Date.now());
|
|
468
|
-
} catch (e) {
|
|
469
|
-
context.snackbarController?.open({
|
|
470
|
-
type: "error",
|
|
471
|
-
message: e instanceof Error ? e.message : String(e)
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
}, [dataClient, path, tableController.data, collection, context]);
|
|
475
|
-
|
|
476
|
-
const selectionDialog = useSelectionDialog<M>({
|
|
477
|
-
// The whole target collection, not this parent's slice of it —
|
|
478
|
-
// otherwise the picker could only ever show rows already linked.
|
|
479
|
-
path: linkedTargetSlug ?? false,
|
|
480
|
-
multiselect: true,
|
|
481
|
-
onMultipleEntitiesSelected: linkExistingEntities
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
const openNewDocument = useCallback((defaultValues?: Record<string, unknown>) => {
|
|
485
|
-
const collection = collectionRef.current;
|
|
486
|
-
analyticsController.onAnalyticsEvent?.("new_entity_click", {
|
|
487
|
-
path: path
|
|
488
|
-
});
|
|
489
|
-
navigateToEntity({
|
|
490
|
-
openEntityMode,
|
|
491
|
-
collection,
|
|
492
|
-
entityId: undefined,
|
|
493
|
-
defaultValues,
|
|
494
|
-
path: path,
|
|
495
|
-
sidePanelController,
|
|
496
|
-
navigation: urlController,
|
|
497
|
-
onClose: unselectNavigatedEntity
|
|
498
|
-
});
|
|
499
|
-
}, [path, sidePanelController, openEntityMode, urlController, unselectNavigatedEntity]);
|
|
500
|
-
|
|
501
|
-
const onMultipleDeleteClick = () => {
|
|
502
|
-
analyticsController.onAnalyticsEvent?.("multiple_delete_dialog_open", {
|
|
503
|
-
path: path
|
|
504
|
-
});
|
|
505
|
-
setDeleteEntityClicked(selectedEntities);
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
const internalOnEntityDelete = (_path: string, entity: Entity<M>) => {
|
|
509
|
-
analyticsController.onAnalyticsEvent?.("single_entity_deleted", {
|
|
510
|
-
path: path
|
|
511
|
-
});
|
|
512
|
-
setSelectedEntities((selectedEntities) => selectedEntities.filter((e) => e.id !== entity.id));
|
|
513
|
-
setDeletedEntities(prev => [...prev, entity]);
|
|
514
|
-
setLastDeleteTimestamp(Date.now());
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
const internalOnMultipleEntitiesDelete = (_path: string, entities: Entity<M>[]) => {
|
|
518
|
-
analyticsController.onAnalyticsEvent?.("multiple_entities_deleted", {
|
|
519
|
-
path: path
|
|
520
|
-
});
|
|
521
|
-
setSelectedEntities([]);
|
|
522
|
-
setDeleteEntityClicked(undefined);
|
|
523
|
-
setDeletedEntities(prev => [...prev, ...entities]);
|
|
524
|
-
setLastDeleteTimestamp(Date.now());
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const pluginAddColumnComponents = useSlot("collection.add-column", {
|
|
528
|
-
path,
|
|
529
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
530
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
531
|
-
collection,
|
|
532
|
-
tableController
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
const pluginToolbarWidgets = useSlot("collection.toolbar", {
|
|
536
|
-
path,
|
|
537
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
538
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
539
|
-
collection: collection,
|
|
540
|
-
tableController: tableController,
|
|
541
|
-
selectionController: usedSelectionController
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const pluginEmptyStates = useSlot("collection.empty-state", {
|
|
545
|
-
path,
|
|
546
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
547
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
548
|
-
collection,
|
|
549
|
-
canCreate: canCreateEntities,
|
|
550
|
-
onNewClick
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
const pluginWidgets = useSlot("collection.widgets", {
|
|
554
|
-
path,
|
|
555
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
556
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
557
|
-
collection
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
const onCollectionModifiedForUser = useCallback((path: string, partialCollection: PartialCollectionConfig<M>) => {
|
|
561
|
-
if (userConfigPersistence) {
|
|
562
|
-
const currentStoredConfig = userConfigPersistence.getCollectionConfig(path);
|
|
563
|
-
const updatedConfig = mergeDeep(currentStoredConfig, partialCollection);
|
|
564
|
-
userConfigPersistence.onCollectionModified(path, updatedConfig);
|
|
565
|
-
}
|
|
566
|
-
}, [userConfigPersistence]);
|
|
567
|
-
|
|
568
|
-
const onColumnResize = useCallback(({
|
|
569
|
-
width,
|
|
570
|
-
key
|
|
571
|
-
}: OnColumnResizeParams) => {
|
|
572
|
-
|
|
573
|
-
const collection = collectionRef.current;
|
|
574
|
-
// Only for property columns
|
|
575
|
-
if (!getPropertyInPath(collection.properties, key)) return;
|
|
576
|
-
const localCollection = buildPropertyWidthOverwrite(key, width) as PartialCollectionConfig<M>;
|
|
577
|
-
onCollectionModifiedForUser(path, localCollection);
|
|
578
|
-
}, [onCollectionModifiedForUser, path]);
|
|
579
|
-
|
|
580
|
-
const onListSizeChanged = useCallback((size: CollectionSize) => {
|
|
581
|
-
setListSize(size);
|
|
582
|
-
if (userConfigPersistence)
|
|
583
|
-
onCollectionModifiedForUser(path, { defaultSize: size })
|
|
584
|
-
}, [onCollectionModifiedForUser, path, userConfigPersistence]);
|
|
585
|
-
|
|
586
|
-
const onTableSizeChanged = useCallback((size: CollectionSize) => {
|
|
587
|
-
setTableSize(size);
|
|
588
|
-
if (userConfigPersistence)
|
|
589
|
-
onCollectionModifiedForUser(path, { defaultSize: size })
|
|
590
|
-
}, [onCollectionModifiedForUser, path, userConfigPersistence]);
|
|
591
|
-
|
|
592
|
-
// View mode change: update URL + save to local persistence
|
|
593
|
-
const onViewModeChange = useCallback((mode: ViewMode) => {
|
|
594
|
-
analyticsController.onAnalyticsEvent?.("view_mode_changed", {
|
|
595
|
-
path: path,
|
|
596
|
-
from: viewMode,
|
|
597
|
-
to: mode
|
|
598
|
-
});
|
|
599
|
-
setViewMode(mode);
|
|
600
|
-
// Save to local persistence for next visit
|
|
601
|
-
if (userConfigPersistence) {
|
|
602
|
-
onCollectionModifiedForUser(path, { defaultViewMode: mode } as PartialCollectionConfig<M>);
|
|
603
|
-
}
|
|
604
|
-
}, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, path, analyticsController, viewMode]);
|
|
605
|
-
|
|
606
|
-
const createEnabled = canCreate(collection, path);
|
|
607
|
-
|
|
608
|
-
const { onValueChange, uniqueFieldValidator } = useCollectionInlineEditor({
|
|
609
|
-
path,
|
|
610
|
-
collection,
|
|
611
|
-
dataClient,
|
|
612
|
-
context
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// In v4, collections are already resolved, so we use collection directly
|
|
616
|
-
const resolvedCollection = collection;
|
|
617
|
-
|
|
618
|
-
// Check if Kanban view is available (needs kanban.columnProperty with enum)
|
|
619
|
-
const kanbanEnabled = useMemo(() => {
|
|
620
|
-
if (!collection.kanban?.columnProperty) return false;
|
|
621
|
-
const property = getPropertyInPath(resolvedCollection.properties, collection.kanban.columnProperty);
|
|
622
|
-
if (!property || property.type !== "string") return false;
|
|
623
|
-
return Boolean("enum" in property && property.enum);
|
|
624
|
-
}, [collection.kanban?.columnProperty, resolvedCollection.properties]);
|
|
625
|
-
|
|
626
|
-
// Compute the effective enabled views:
|
|
627
|
-
// - Start from collection.enabledViews (defaults to every built-in
|
|
628
|
-
// plus every custom view the collection declared)
|
|
629
|
-
// - Drop anything that resolves to nothing renderable
|
|
630
|
-
// - Filter out kanban if no enum properties exist
|
|
631
|
-
const hasEnumProperty = useMemo(() => {
|
|
632
|
-
return Object.values(resolvedCollection.properties).some((p: Property) => p.type === "string" && "enum" in p && p.enum);
|
|
633
|
-
}, [resolvedCollection.properties]);
|
|
634
|
-
|
|
635
|
-
const enabledViews: ViewMode[] = useMemo(() => {
|
|
636
|
-
// Declaring a custom view is enough to offer it — a collection
|
|
637
|
-
// should not have to restate the built-ins in `enabledViews` just
|
|
638
|
-
// to add one. Setting `enabledViews` explicitly still wins.
|
|
639
|
-
const configured = collection.enabledViews
|
|
640
|
-
?? [...VIEW_MODES, ...customViewKeys];
|
|
641
|
-
const renderable = configured.filter(v => isViewMode(v, customViewKeys));
|
|
642
|
-
if (!hasEnumProperty) {
|
|
643
|
-
return renderable.filter(v => v !== "kanban");
|
|
644
|
-
}
|
|
645
|
-
return renderable;
|
|
646
|
-
}, [collection.enabledViews, hasEnumProperty, customViewKeys]);
|
|
647
|
-
|
|
648
|
-
// Compute available enum properties for kanban column selection
|
|
649
|
-
const kanbanPropertyOptions: KanbanPropertyOption[] = useMemo(() => {
|
|
650
|
-
const options: KanbanPropertyOption[] = [];
|
|
651
|
-
const properties = resolvedCollection.properties;
|
|
652
|
-
|
|
653
|
-
for (const [key, property] of Object.entries(properties)) {
|
|
654
|
-
const prop = property;
|
|
655
|
-
if (prop && prop.type === "string" && "enum" in prop && prop.enum) {
|
|
656
|
-
options.push({
|
|
657
|
-
key,
|
|
658
|
-
label: prop.name || key
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return options;
|
|
664
|
-
}, [resolvedCollection.properties]);
|
|
665
|
-
|
|
666
|
-
// Get saved kanban property from user config
|
|
667
|
-
const getSavedKanbanProperty = useCallback((): string | undefined => {
|
|
668
|
-
const saved = userConfigPersistence?.getCollectionConfig<M>(path);
|
|
669
|
-
return (saved as Record<string, unknown>)?.kanbanColumnProperty as string | undefined;
|
|
670
|
-
}, [userConfigPersistence, path]);
|
|
671
|
-
|
|
672
|
-
// Selected kanban property state - priority: saved config > collection default > first available
|
|
673
|
-
const [selectedKanbanProperty, setSelectedKanbanProperty] = useState<string>(() => {
|
|
674
|
-
const saved = getSavedKanbanProperty();
|
|
675
|
-
if (saved && kanbanPropertyOptions.some(o => o.key === saved)) return saved;
|
|
676
|
-
if (collection.kanban?.columnProperty) return collection.kanban.columnProperty;
|
|
677
|
-
return kanbanPropertyOptions[0]?.key ?? "";
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
// Update selected property if options change and current selection is no longer valid
|
|
681
|
-
useEffect(() => {
|
|
682
|
-
if (kanbanPropertyOptions.length > 0 && !kanbanPropertyOptions.some(o => o.key === selectedKanbanProperty)) {
|
|
683
|
-
const saved = getSavedKanbanProperty();
|
|
684
|
-
if (saved && kanbanPropertyOptions.some(o => o.key === saved)) {
|
|
685
|
-
setSelectedKanbanProperty(saved);
|
|
686
|
-
} else if (collection.kanban?.columnProperty && kanbanPropertyOptions.some(o => o.key === collection.kanban?.columnProperty)) {
|
|
687
|
-
setSelectedKanbanProperty(collection.kanban.columnProperty);
|
|
688
|
-
} else {
|
|
689
|
-
setSelectedKanbanProperty(kanbanPropertyOptions[0]?.key ?? "");
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}, [kanbanPropertyOptions, selectedKanbanProperty, getSavedKanbanProperty, collection.kanban?.columnProperty]);
|
|
693
|
-
|
|
694
|
-
// Handle kanban property change
|
|
695
|
-
const onKanbanPropertyChange = useCallback((property: string) => {
|
|
696
|
-
analyticsController.onAnalyticsEvent?.("kanban_property_changed", {
|
|
697
|
-
path: path,
|
|
698
|
-
property
|
|
699
|
-
});
|
|
700
|
-
setSelectedKanbanProperty(property);
|
|
701
|
-
// Save to local persistence
|
|
702
|
-
if (userConfigPersistence) {
|
|
703
|
-
onCollectionModifiedForUser(path, { kanbanColumnProperty: property } as PartialCollectionConfig<M>);
|
|
704
|
-
}
|
|
705
|
-
}, [userConfigPersistence, onCollectionModifiedForUser, path, analyticsController]);
|
|
706
|
-
|
|
707
|
-
const getPropertyFor = useCallback(({
|
|
708
|
-
propertyKey,
|
|
709
|
-
entity
|
|
710
|
-
}: GetPropertyForProps<M>) => {
|
|
711
|
-
let property: Property | undefined = getPropertyInPath(collection.properties, propertyKey);
|
|
712
|
-
|
|
713
|
-
// we might not find the property in the collection if combining property builders and map spread
|
|
714
|
-
if (!property) {
|
|
715
|
-
// these 2 properties are coming from the resolved collection with default values
|
|
716
|
-
property = getPropertyInPath(resolvedCollection.properties, propertyKey);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// In v4, properties are already resolved, so we return them directly
|
|
720
|
-
return property ?? null;
|
|
721
|
-
}, [collection.properties, resolvedCollection.properties]);
|
|
722
|
-
|
|
723
|
-
// Use a collection with local propertiesOrder for optimistic UI updates
|
|
724
|
-
const collectionWithLocalOrder = useMemo(() => {
|
|
725
|
-
if (localPropertiesOrder && localPropertiesOrder !== resolvedCollection.propertiesOrder) {
|
|
726
|
-
return {
|
|
727
|
-
...resolvedCollection,
|
|
728
|
-
// Runtime column order, not authored config: these ids come
|
|
729
|
-
// from the user dragging columns and include additional-field
|
|
730
|
-
// and `subcollection:` ids minted below. `propertiesOrder` is
|
|
731
|
-
// typed against the collection's properties for *authoring*,
|
|
732
|
-
// so widening back to that type is the intent here.
|
|
733
|
-
propertiesOrder: localPropertiesOrder as typeof resolvedCollection.propertiesOrder
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
return resolvedCollection;
|
|
737
|
-
}, [resolvedCollection, localPropertiesOrder]);
|
|
738
|
-
|
|
739
|
-
const displayedColumnIds = useColumnIds(collectionWithLocalOrder, true);
|
|
740
|
-
|
|
741
|
-
const additionalFields = useMemo(() => {
|
|
742
|
-
// Each child view gets a column that jumps to its tab. Keyed by the
|
|
743
|
-
// view's collection slug, which is the relation key — two relations
|
|
744
|
-
// to the same target used to collapse onto one column id here.
|
|
745
|
-
//
|
|
746
|
-
// Except where the relation was declared as a property and so already
|
|
747
|
-
// has a column showing the child rows: the button then repeated that
|
|
748
|
-
// column's heading verbatim. Filtered here as well as in
|
|
749
|
-
// `useColumnIds` so no id is displayed without a delegate to build it,
|
|
750
|
-
// and so the column picker does not offer the one back.
|
|
751
|
-
const redundant = getRedundantChildViewColumnIds(collectionWithLocalOrder);
|
|
752
|
-
const subcollectionsList = getEntityChildViews(collection)
|
|
753
|
-
.map(v => v.collection as AdminCollection)
|
|
754
|
-
.filter(child => !redundant.has(getSubcollectionColumnId(child)));
|
|
755
|
-
const subcollectionColumns: AdditionalFieldDelegate<M, any>[] = subcollectionsList.map((subcollection: AdminCollection) => {
|
|
756
|
-
return {
|
|
757
|
-
key: getSubcollectionColumnId(subcollection),
|
|
758
|
-
name: subcollection.name,
|
|
759
|
-
width: 200,
|
|
760
|
-
dependencies: [],
|
|
761
|
-
Builder: ({ entity }: { entity: Entity }) => (
|
|
762
|
-
<Button
|
|
763
|
-
className={"max-w-full truncate justify-start"}
|
|
764
|
-
startIcon={<ArrowRightToLineIcon size={iconSize.small}/>}
|
|
765
|
-
onClick={(event: React.MouseEvent) => {
|
|
766
|
-
event.stopPropagation();
|
|
767
|
-
navigateToEntity({
|
|
768
|
-
openEntityMode,
|
|
769
|
-
collection,
|
|
770
|
-
entityId: entity.id,
|
|
771
|
-
selectedTab: subcollection.slug,
|
|
772
|
-
path: path,
|
|
773
|
-
navigation: urlController,
|
|
774
|
-
sidePanelController
|
|
775
|
-
})
|
|
776
|
-
}}>
|
|
777
|
-
{subcollection.name}
|
|
778
|
-
</Button>
|
|
779
|
-
)
|
|
780
|
-
};
|
|
781
|
-
}) ?? EMPTY_ARRAY;
|
|
782
|
-
|
|
783
|
-
return [
|
|
784
|
-
...(collection.additionalFields ?? EMPTY_ARRAY),
|
|
785
|
-
...subcollectionColumns
|
|
786
|
-
];
|
|
787
|
-
}, [collection, collectionWithLocalOrder, path, sidePanelController]);
|
|
788
|
-
|
|
789
|
-
const updateLastDeleteTimestamp = useCallback(() => {
|
|
790
|
-
setLastDeleteTimestamp(Date.now());
|
|
791
|
-
}, []);
|
|
792
|
-
|
|
793
|
-
const largeLayout = useLargeLayout();
|
|
794
|
-
|
|
795
|
-
const isSplitLayout = openEntityMode === "split";
|
|
796
|
-
const isCompact = isSplitLayout && selectedEntityIdProp !== undefined;
|
|
797
|
-
const activeSelectionEnabled = !isCompact && selectionEnabled;
|
|
798
|
-
|
|
799
|
-
const getActionsForEntity = useCallback(({
|
|
800
|
-
entity,
|
|
801
|
-
customEntityActions
|
|
802
|
-
}: {
|
|
803
|
-
entity?: Entity<M>,
|
|
804
|
-
customEntityActions?: EntityAction[]
|
|
805
|
-
}): EntityAction[] => {
|
|
806
|
-
const deleteEnabled = entity ? canDelete(collection, path, entity) : true;
|
|
807
|
-
const disableActions = collection.disableDefaultActions ?? [];
|
|
808
|
-
const actions: EntityAction[] = [];
|
|
809
|
-
if (!disableActions.includes("edit")) {
|
|
810
|
-
actions.push(editEntityAction);
|
|
811
|
-
}
|
|
812
|
-
if (createEnabled && !disableActions.includes("copy"))
|
|
813
|
-
actions.push(copyEntityAction);
|
|
814
|
-
if (deleteEnabled && !disableActions.includes("delete")) {
|
|
815
|
-
// On a junction-backed tab the server removes the link, not the
|
|
816
|
-
// row. Same request either way — this is the button telling the
|
|
817
|
-
// truth about what it will do.
|
|
818
|
-
actions.push(isLinkedChildView ? unlinkEntityAction : deleteEntityAction);
|
|
819
|
-
}
|
|
820
|
-
if (customEntityActions)
|
|
821
|
-
return mergeEntityActions(actions, customEntityActions);
|
|
822
|
-
return actions;
|
|
823
|
-
}, [canDelete, collection, path, createEnabled, isLinkedChildView]);
|
|
824
|
-
|
|
825
|
-
const getIdColumnWidth = useCallback(() => {
|
|
826
|
-
const entityActions = getActionsForEntity({});
|
|
827
|
-
const collapsedActions = entityActions.filter(a => a.collapsed !== false);
|
|
828
|
-
const uncollapsedActions = entityActions.filter(a => a.collapsed === false);
|
|
829
|
-
const actionsWidth = uncollapsedActions.length * (largeLayout ? 40 : 30);
|
|
830
|
-
return (largeLayout ? (80 + actionsWidth) : (70 + actionsWidth)) + (collapsedActions.length > 0 ? (largeLayout ? 40 : 30) : 0);
|
|
831
|
-
}, [getActionsForEntity, largeLayout]);
|
|
832
|
-
|
|
833
|
-
const tableRowActionsBuilder = useCallback(({
|
|
834
|
-
entity,
|
|
835
|
-
size,
|
|
836
|
-
width,
|
|
837
|
-
frozen
|
|
838
|
-
}: {
|
|
839
|
-
entity: Entity<any>,
|
|
840
|
-
size: CollectionSize,
|
|
841
|
-
width: number,
|
|
842
|
-
frozen?: boolean
|
|
843
|
-
}) => {
|
|
844
|
-
|
|
845
|
-
const isSelected = Boolean(usedSelectionController.selectedEntities.find(e => e.id == entity.id && e.path == entity.path));
|
|
846
|
-
const customEntityActions = (collection.entityActions ?? EMPTY_ARRAY)
|
|
847
|
-
.map(action => resolveEntityAction(action, customizationController.entityActions))
|
|
848
|
-
.filter(Boolean) as EntityAction<M>[];
|
|
849
|
-
|
|
850
|
-
const actions = getActionsForEntity({
|
|
851
|
-
entity,
|
|
852
|
-
customEntityActions
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
return (
|
|
856
|
-
<CollectionRowActions
|
|
857
|
-
entity={entity}
|
|
858
|
-
width={width}
|
|
859
|
-
frozen={frozen}
|
|
860
|
-
isSelected={isSelected}
|
|
861
|
-
selectionEnabled={activeSelectionEnabled}
|
|
862
|
-
size={size}
|
|
863
|
-
highlightEntity={setHighlightedEntity}
|
|
864
|
-
unhighlightEntity={unselectNavigatedEntity}
|
|
865
|
-
collection={collection}
|
|
866
|
-
path={path}
|
|
867
|
-
actions={actions}
|
|
868
|
-
hideId={collection?.hideIdFromCollection}
|
|
869
|
-
onCollectionChange={updateLastDeleteTimestamp}
|
|
870
|
-
selectionController={usedSelectionController}
|
|
871
|
-
openEntityMode={openEntityMode}
|
|
872
|
-
/>
|
|
873
|
-
);
|
|
874
|
-
|
|
875
|
-
}, [updateLastDeleteTimestamp, usedSelectionController]);
|
|
876
|
-
|
|
877
|
-
// EntitiesCount fetches the count and reports it — the toolbar renders it
|
|
878
|
-
const countFetcher = <EntitiesCount
|
|
879
|
-
path={path}
|
|
880
|
-
collection={collection}
|
|
881
|
-
filter={tableController.filterValues}
|
|
882
|
-
sortBy={tableController.sortBy}
|
|
883
|
-
searchString={tableController.searchString}
|
|
884
|
-
onCountChange={setDocsCount}
|
|
885
|
-
/>;
|
|
886
|
-
|
|
887
|
-
const { resolvedSlots } = customizationController;
|
|
888
|
-
|
|
889
|
-
// Pre-compute header action slot contributions (avoid useSlot inside callback)
|
|
890
|
-
const headerActionContributions = useMemo(() => resolvedSlots
|
|
891
|
-
.filter(s => s.slot === "collection.header.action")
|
|
892
|
-
.sort((a, b) => (a.order ?? 50) - (b.order ?? 50)), [resolvedSlots]);
|
|
893
|
-
|
|
894
|
-
const buildAdditionalHeaderWidget = useCallback(({
|
|
895
|
-
property,
|
|
896
|
-
propertyKey,
|
|
897
|
-
onHover
|
|
898
|
-
}: {
|
|
899
|
-
property: Property,
|
|
900
|
-
propertyKey: string,
|
|
901
|
-
onHover: boolean
|
|
902
|
-
}) => {
|
|
903
|
-
const collection = collectionRef.current;
|
|
904
|
-
const headerSlotProps = {
|
|
905
|
-
property,
|
|
906
|
-
propertyKey,
|
|
907
|
-
onHover,
|
|
908
|
-
path,
|
|
909
|
-
collection: collection as AdminCollection,
|
|
910
|
-
tableController: tableController as EntityTableController,
|
|
911
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
912
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY
|
|
913
|
-
};
|
|
914
|
-
return <>{headerActionContributions.map((s, i) => (
|
|
915
|
-
<ErrorBoundary key={`header_action_${propertyKey}_${i}`}>
|
|
916
|
-
<s.Component {...headerSlotProps} {...(s.props ?? {})}/>
|
|
917
|
-
</ErrorBoundary>
|
|
918
|
-
))}</>;
|
|
919
|
-
}, [headerActionContributions, path, parentCollectionSlugs]);
|
|
920
|
-
|
|
921
|
-
const addColumnComponentInternal = pluginAddColumnComponents.length > 0
|
|
922
|
-
? function () {
|
|
923
|
-
return (
|
|
924
|
-
<div className="flex flex-row items-center gap-2">
|
|
925
|
-
{pluginAddColumnComponents}
|
|
926
|
-
</div>
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
: undefined;
|
|
930
|
-
|
|
931
|
-
const onColumnsOrderChange = useCallback((newColumns: VirtualTableColumn[]) => {
|
|
932
|
-
// Extract property keys from the new column order
|
|
933
|
-
// Filter to only include actual property columns (not frozen columns, not additional fields, etc.)
|
|
934
|
-
// Deduplicate to clean up any previously duplicated keys
|
|
935
|
-
const seenKeys = new Set<string>();
|
|
936
|
-
const newPropertiesOrder = newColumns
|
|
937
|
-
.filter(col => !col.frozen && getPropertyInPath(collection.properties, col.key))
|
|
938
|
-
.map(col => col.key)
|
|
939
|
-
.filter(key => {
|
|
940
|
-
if (seenKeys.has(key)) return false;
|
|
941
|
-
seenKeys.add(key);
|
|
942
|
-
return true;
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
// Optimistically update local state to prevent UI flickering
|
|
946
|
-
setLocalPropertiesOrder(newPropertiesOrder);
|
|
947
|
-
|
|
948
|
-
// Call each plugin's onColumnsReorder callback
|
|
949
|
-
if (customizationController?.plugins) {
|
|
950
|
-
customizationController.plugins
|
|
951
|
-
.filter(plugin => plugin.hooks?.onColumnsReorder)
|
|
952
|
-
.forEach(plugin => {
|
|
953
|
-
plugin.hooks!.onColumnsReorder!({
|
|
954
|
-
fullPath: path,
|
|
955
|
-
parentCollectionSlugs: parentCollectionSlugs ?? EMPTY_ARRAY,
|
|
956
|
-
parentEntityIds: parentEntityIds ?? EMPTY_ARRAY,
|
|
957
|
-
collection,
|
|
958
|
-
newPropertiesOrder
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Save to user configuration persistence (local storage)
|
|
964
|
-
if (userConfigPersistence) {
|
|
965
|
-
onCollectionModifiedForUser(path, { propertiesOrder: newPropertiesOrder } as PartialCollectionConfig<M>);
|
|
966
|
-
}
|
|
967
|
-
}, [collection, setLocalPropertiesOrder, customizationController, path, parentCollectionSlugs, parentEntityIds, userConfigPersistence, onCollectionModifiedForUser]);
|
|
968
|
-
|
|
969
|
-
// Popover open state managed at parent level to prevent closing when view changes
|
|
970
|
-
const [viewModePopoverOpen, setViewModePopoverOpen] = useState(false);
|
|
971
|
-
|
|
972
|
-
// Create ViewModeToggle once to prevent remounting when view changes
|
|
973
|
-
const viewModeToggleElement = (
|
|
974
|
-
<ViewModeToggle
|
|
975
|
-
viewMode={viewMode}
|
|
976
|
-
onViewModeChange={onViewModeChange}
|
|
977
|
-
enabledViews={enabledViews}
|
|
978
|
-
customViews={resolvedCustomViews}
|
|
979
|
-
size={activeCustomView ? (activeCustomView.sizeable ? customViewSize : undefined) : viewMode === "list" ? listSize : viewMode === "table" ? tableSize : viewMode === "cards" ? cardSize : undefined}
|
|
980
|
-
onSizeChanged={activeCustomView ? (activeCustomView.sizeable ? setCustomViewSize : undefined) : viewMode === "list" ? onListSizeChanged : viewMode === "table" ? onTableSizeChanged : viewMode === "cards" ? setCardSize : undefined}
|
|
981
|
-
open={viewModePopoverOpen}
|
|
982
|
-
onOpenChange={setViewModePopoverOpen}
|
|
983
|
-
kanbanPropertyOptions={kanbanPropertyOptions}
|
|
984
|
-
selectedKanbanProperty={selectedKanbanProperty}
|
|
985
|
-
onKanbanPropertyChange={onKanbanPropertyChange}
|
|
986
|
-
/>
|
|
987
|
-
);
|
|
988
|
-
|
|
989
|
-
// Compute plugin-provided error view for collection loading errors
|
|
990
|
-
const pluginErrorViews = useSlot("collection.error", {
|
|
991
|
-
path,
|
|
992
|
-
collection,
|
|
993
|
-
parentCollectionSlugs,
|
|
994
|
-
parentEntityIds,
|
|
995
|
-
error: tableController.dataLoadingError as Error
|
|
996
|
-
});
|
|
997
|
-
const pluginErrorView = tableController.dataLoadingError && pluginErrorViews.length > 0
|
|
998
|
-
? pluginErrorViews[0]
|
|
999
|
-
: null;
|
|
1000
|
-
|
|
1001
|
-
// Shared empty state — plugin slot takes priority, then override, then default
|
|
1002
|
-
const isSearching = !!tableController.searchString;
|
|
1003
|
-
const isFilteredOrSorted = tableController.filterValues !== undefined || tableController.sortBy !== undefined || isSearching;
|
|
1004
|
-
const ResolvedEmptyState = useComponentOverride("Collection.EmptyState", DefaultCollectionEmptyState);
|
|
1005
|
-
const ResolvedCollectionActions = useComponentOverride("Collection.Actions", CollectionViewActions);
|
|
1006
|
-
const ResolvedCollectionTable = useComponentOverride("Collection.Table", CollectionTableBinding) as typeof CollectionTableBinding;
|
|
1007
|
-
const emptyComponent = pluginEmptyStates.length > 0
|
|
1008
|
-
? <>{pluginEmptyStates}</>
|
|
1009
|
-
: <ResolvedEmptyState
|
|
1010
|
-
canCreate={canCreateEntities && !isFilteredOrSorted}
|
|
1011
|
-
onNewClick={onNewClick}
|
|
1012
|
-
isSearching={isSearching}
|
|
1013
|
-
searchString={tableController.searchString ?? ""}
|
|
1014
|
-
/>;
|
|
1015
|
-
|
|
1016
|
-
const toolbarNode = (
|
|
1017
|
-
<CollectionTableToolbar
|
|
1018
|
-
compact={isCompact}
|
|
1019
|
-
loading={tableController.dataLoading}
|
|
1020
|
-
onTextSearch={tableController.setSearchString}
|
|
1021
|
-
initialSearchText={tableController.searchString}
|
|
1022
|
-
viewModeToggle={viewModeToggleElement}
|
|
1023
|
-
actionsStart={<CollectionViewStartActions
|
|
1024
|
-
parentCollectionSlugs={parentCollectionSlugs ?? EMPTY_ARRAY} parentEntityIds={parentEntityIds ?? EMPTY_ARRAY}
|
|
1025
|
-
collection={collection}
|
|
1026
|
-
tableController={tableController}
|
|
1027
|
-
path={path}
|
|
1028
|
-
relativePath={getCollectionDataPath(collection)}
|
|
1029
|
-
selectionController={usedSelectionController}
|
|
1030
|
-
collectionEntitiesCount={docsCount ?? undefined}
|
|
1031
|
-
resolvedProperties={resolvedCollection.properties}
|
|
1032
|
-
viewMode={viewMode}
|
|
1033
|
-
entitiesCount={docsCount}
|
|
1034
|
-
openNewDocument={openNewDocument}
|
|
1035
|
-
compact={isCompact}/>}
|
|
1036
|
-
actions={
|
|
1037
|
-
<ResolvedCollectionActions
|
|
1038
|
-
parentCollectionSlugs={parentCollectionSlugs ?? EMPTY_ARRAY} parentEntityIds={parentEntityIds ?? EMPTY_ARRAY}
|
|
1039
|
-
collection={collection}
|
|
1040
|
-
tableController={tableController}
|
|
1041
|
-
onMultipleDeleteClick={onMultipleDeleteClick}
|
|
1042
|
-
onNewClick={onNewClick}
|
|
1043
|
-
onAddExistingClick={linkedTargetSlug ? selectionDialog.open : undefined}
|
|
1044
|
-
openNewDocument={openNewDocument}
|
|
1045
|
-
path={path}
|
|
1046
|
-
relativePath={getCollectionDataPath(collection)}
|
|
1047
|
-
selectionController={usedSelectionController}
|
|
1048
|
-
selectionEnabled={activeSelectionEnabled}
|
|
1049
|
-
collectionEntitiesCount={docsCount ?? undefined}
|
|
1050
|
-
compact={isCompact}
|
|
1051
|
-
>
|
|
1052
|
-
{pluginToolbarWidgets}
|
|
1053
|
-
</ResolvedCollectionActions>
|
|
1054
|
-
}
|
|
1055
|
-
/>
|
|
1056
|
-
);
|
|
1057
|
-
|
|
1058
|
-
// A custom view wins over the built-in chain, but only after the
|
|
1059
|
-
// built-in keys have been ruled out — `resolveCollectionViews` refuses
|
|
1060
|
-
// a view keyed "table", so this cannot shadow one.
|
|
1061
|
-
const CustomViewBuilder = activeCustomView
|
|
1062
|
-
? resolveComponentRef<CollectionCustomViewParams<M>>(activeCustomView.Builder)
|
|
1063
|
-
: undefined;
|
|
1064
|
-
|
|
1065
|
-
const innerView = CustomViewBuilder ? (
|
|
1066
|
-
<ErrorBoundary>
|
|
1067
|
-
<CustomViewBuilder
|
|
1068
|
-
key={`custom-view-${activeCustomView!.key}-${path}`}
|
|
1069
|
-
collection={collection}
|
|
1070
|
-
tableController={tableController}
|
|
1071
|
-
path={path}
|
|
1072
|
-
parentCollectionSlugs={parentCollectionSlugs}
|
|
1073
|
-
parentEntityIds={parentEntityIds}
|
|
1074
|
-
onEntityClick={onEntityClick}
|
|
1075
|
-
onNewClick={canCreateEntities ? onNewClick : undefined}
|
|
1076
|
-
canCreate={canCreateEntities}
|
|
1077
|
-
selectionController={usedSelectionController}
|
|
1078
|
-
selectionEnabled={selectionEnabled}
|
|
1079
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1080
|
-
deletedEntities={deletedEntities}
|
|
1081
|
-
emptyComponent={emptyComponent}
|
|
1082
|
-
size={activeCustomView!.sizeable ? customViewSize : undefined}
|
|
1083
|
-
viewMode={viewMode}
|
|
1084
|
-
/>
|
|
1085
|
-
</ErrorBoundary>
|
|
1086
|
-
) : viewMode === "kanban" ? (
|
|
1087
|
-
<CollectionBoardViewBinding
|
|
1088
|
-
key={`kanban-view-${path}-${selectedKanbanProperty}`}
|
|
1089
|
-
collection={collection}
|
|
1090
|
-
tableController={tableController}
|
|
1091
|
-
fullPath={path}
|
|
1092
|
-
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
1093
|
-
columnProperty={selectedKanbanProperty}
|
|
1094
|
-
onEntityClick={onEntityClick}
|
|
1095
|
-
selectionController={usedSelectionController}
|
|
1096
|
-
selectionEnabled={selectionEnabled}
|
|
1097
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1098
|
-
deletedEntities={deletedEntities}
|
|
1099
|
-
emptyComponent={emptyComponent}
|
|
1100
|
-
/>
|
|
1101
|
-
) : viewMode === "cards" ? (
|
|
1102
|
-
<CollectionCardViewBinding
|
|
1103
|
-
key={`cards-view-${path}`}
|
|
1104
|
-
collection={collection}
|
|
1105
|
-
tableController={tableController}
|
|
1106
|
-
onEntityClick={onEntityClick}
|
|
1107
|
-
selectionController={usedSelectionController}
|
|
1108
|
-
selectionEnabled={selectionEnabled}
|
|
1109
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1110
|
-
onScroll={tableController.onScroll}
|
|
1111
|
-
initialScroll={tableController.initialScroll}
|
|
1112
|
-
size={cardSize}
|
|
1113
|
-
emptyComponent={emptyComponent}
|
|
1114
|
-
/>
|
|
1115
|
-
) : viewMode === "list" ? (
|
|
1116
|
-
<CollectionListViewBinding
|
|
1117
|
-
key={`list-view-${path}`}
|
|
1118
|
-
collection={collection}
|
|
1119
|
-
tableController={tableController}
|
|
1120
|
-
onEntityClick={onEntityClick}
|
|
1121
|
-
selectionController={usedSelectionController}
|
|
1122
|
-
selectionEnabled={selectionEnabled}
|
|
1123
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1124
|
-
size={listSize}
|
|
1125
|
-
emptyComponent={emptyComponent}
|
|
1126
|
-
selectedEntityId={selectedEntityIdProp}
|
|
1127
|
-
getActionsForEntity={getActionsForEntity}
|
|
1128
|
-
path={path}
|
|
1129
|
-
openEntityMode={openEntityMode}
|
|
1130
|
-
/>
|
|
1131
|
-
) : (
|
|
1132
|
-
<ResolvedCollectionTable
|
|
1133
|
-
key={`collection_table_${path}`}
|
|
1134
|
-
hideToolbar={true}
|
|
1135
|
-
additionalFields={additionalFields}
|
|
1136
|
-
tableController={tableController}
|
|
1137
|
-
enablePopupIcon={true}
|
|
1138
|
-
displayedColumnIds={displayedColumnIds}
|
|
1139
|
-
onSizeChanged={onTableSizeChanged}
|
|
1140
|
-
onEntityClick={onEntityClick}
|
|
1141
|
-
onColumnResize={onColumnResize}
|
|
1142
|
-
onValueChange={onValueChange}
|
|
1143
|
-
tableRowActionsBuilder={tableRowActionsBuilder}
|
|
1144
|
-
uniqueFieldValidator={uniqueFieldValidator}
|
|
1145
|
-
selectionController={usedSelectionController}
|
|
1146
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1147
|
-
defaultSize={tableSize}
|
|
1148
|
-
properties={resolvedCollection.properties}
|
|
1149
|
-
getPropertyFor={getPropertyFor}
|
|
1150
|
-
onScroll={tableController.onScroll}
|
|
1151
|
-
initialScroll={tableController.initialScroll}
|
|
1152
|
-
emptyComponent={emptyComponent}
|
|
1153
|
-
hoverRow={hoverRow}
|
|
1154
|
-
inlineEditing={checkInlineEditing()}
|
|
1155
|
-
AdditionalHeaderWidget={buildAdditionalHeaderWidget}
|
|
1156
|
-
AddColumnComponent={addColumnComponentInternal}
|
|
1157
|
-
getIdColumnWidth={getIdColumnWidth}
|
|
1158
|
-
additionalIDHeaderWidget={<EntityIdHeaderWidget
|
|
1159
|
-
path={path}
|
|
1160
|
-
idPath={path}
|
|
1161
|
-
collection={collection}
|
|
1162
|
-
openEntityMode={openEntityMode}/>}
|
|
1163
|
-
openEntityMode={openEntityMode}
|
|
1164
|
-
onColumnsOrderChange={onColumnsOrderChange}
|
|
1165
|
-
/>
|
|
1166
|
-
);
|
|
1167
|
-
|
|
1168
|
-
const detailOpen = selectedEntityIdProp !== undefined;
|
|
1169
|
-
|
|
1170
|
-
// List view gets a centered, titled reading surface. Opening a detail
|
|
1171
|
-
// panel collapses the title and drops the centering so the list can
|
|
1172
|
-
// shrink into the master column.
|
|
1173
|
-
const collectionSurface = viewMode === "list"
|
|
1174
|
-
? (
|
|
1175
|
-
<div
|
|
1176
|
-
className={cls(
|
|
1177
|
-
"flex flex-col w-full",
|
|
1178
|
-
detailOpen ? "" : "max-w-6xl mx-auto px-3 md:px-4 lg:px-6 py-4"
|
|
1179
|
-
)}
|
|
1180
|
-
>
|
|
1181
|
-
{/* Collapsible title — grid-rows for smooth height, transform for GPU */}
|
|
1182
|
-
<div
|
|
1183
|
-
className={cls(
|
|
1184
|
-
"grid transition-[grid-template-rows,transform,margin] duration-150 ease-out",
|
|
1185
|
-
detailOpen
|
|
1186
|
-
? "grid-rows-[0fr] -translate-y-2 mt-0 mb-0"
|
|
1187
|
-
: "grid-rows-[1fr] translate-y-0 mt-12 mb-6"
|
|
1188
|
-
)}
|
|
1189
|
-
>
|
|
1190
|
-
<div className="overflow-hidden flex items-center gap-4">
|
|
1191
|
-
<Typography gutterBottom variant="h4" className="grow mb-0" component="h4">
|
|
1192
|
-
{collection.name}
|
|
1193
|
-
</Typography>
|
|
1194
|
-
</div>
|
|
1195
|
-
</div>
|
|
1196
|
-
{pluginWidgets.length > 0 && (
|
|
1197
|
-
<div
|
|
1198
|
-
className={cls(
|
|
1199
|
-
"grid transition-[grid-template-rows] duration-150 ease-out",
|
|
1200
|
-
detailOpen ? "grid-rows-[0fr]" : "grid-rows-[1fr]"
|
|
1201
|
-
)}
|
|
1202
|
-
>
|
|
1203
|
-
<div className="overflow-hidden flex-shrink-0">
|
|
1204
|
-
{pluginWidgets}
|
|
1205
|
-
</div>
|
|
1206
|
-
</div>
|
|
1207
|
-
)}
|
|
1208
|
-
{innerView}
|
|
1209
|
-
</div>
|
|
1210
|
-
)
|
|
1211
|
-
: innerView;
|
|
1212
|
-
|
|
1213
|
-
const mainContent = (
|
|
1214
|
-
<div className={cls("overflow-hidden h-full w-full rounded-md flex flex-col dark:bg-surface-800", className)}
|
|
1215
|
-
ref={containerRef}>
|
|
1216
|
-
|
|
1217
|
-
{countFetcher}
|
|
1218
|
-
|
|
1219
|
-
{/* A plugin's error view takes priority over the built-in
|
|
1220
|
-
banner, the same precedence the empty state uses. This was
|
|
1221
|
-
computed above and then rendered nowhere, so `collection.error`
|
|
1222
|
-
— a declared, documented slot with its own props interface —
|
|
1223
|
-
did nothing at all, and a plugin author had no way to tell. */}
|
|
1224
|
-
{pluginErrorView ?? (tableController.dataLoadingError && tableController.data.length > 0 && (
|
|
1225
|
-
<div className="flex items-center gap-4 px-4 py-2 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800 flex-shrink-0">
|
|
1226
|
-
<Typography variant="body2" className="text-red-700 dark:text-red-300 flex-1">
|
|
1227
|
-
<strong>Warning:</strong> {tableController.dataLoadingError.message || "Failed to update data."}
|
|
1228
|
-
</Typography>
|
|
1229
|
-
</div>
|
|
1230
|
-
))}
|
|
1231
|
-
|
|
1232
|
-
{/* When isSplitLayout, SplitListView is ALWAYS mounted — regardless
|
|
1233
|
-
of viewMode. The toolbar + current view live in the left panel;
|
|
1234
|
-
the right panel (entity detail) shows/hides based on selection.
|
|
1235
|
-
This keeps the toolbar's React tree position stable across view
|
|
1236
|
-
mode switches, preventing the ViewModeToggle popover from flashing. */}
|
|
1237
|
-
{isSplitLayout ? (
|
|
1238
|
-
<SplitListView
|
|
1239
|
-
key={`split-list-view-${path}`}
|
|
1240
|
-
collection={collection}
|
|
1241
|
-
tableController={tableController}
|
|
1242
|
-
onEntityClick={onEntityClick}
|
|
1243
|
-
onNewClick={onNewClick}
|
|
1244
|
-
selectionController={usedSelectionController}
|
|
1245
|
-
selectionEnabled={selectionEnabled}
|
|
1246
|
-
highlightedEntities={highlightedEntity ? [highlightedEntity] : []}
|
|
1247
|
-
onScroll={tableController.onScroll}
|
|
1248
|
-
initialScroll={tableController.initialScroll}
|
|
1249
|
-
size={listSize}
|
|
1250
|
-
emptyComponent={emptyComponent}
|
|
1251
|
-
path={path}
|
|
1252
|
-
parentCollectionSlugs={parentCollectionSlugs} parentEntityIds={parentEntityIds}
|
|
1253
|
-
selectedEntityId={selectedEntityIdProp}
|
|
1254
|
-
selectedTab={selectedTabProp}
|
|
1255
|
-
toolbar={toolbarNode}
|
|
1256
|
-
>
|
|
1257
|
-
{/* The left panel keeps rendering whichever view mode is
|
|
1258
|
-
active. Swapping it to a list on selection would make
|
|
1259
|
-
a board or card grid vanish on every click. */}
|
|
1260
|
-
{collectionSurface}
|
|
1261
|
-
</SplitListView>
|
|
1262
|
-
) : (
|
|
1263
|
-
<div className="flex flex-col w-full h-full">
|
|
1264
|
-
{toolbarNode}
|
|
1265
|
-
{/* `min-h-0` is what lets the board scroll its columns.
|
|
1266
|
-
A flex item defaults to `min-height: auto`, so this
|
|
1267
|
-
one grew to the board's full content height — 1230px
|
|
1268
|
-
in a 883px area — and the ancestor's `overflow-hidden`
|
|
1269
|
-
simply cut the rest off, with each column's own
|
|
1270
|
-
scroller never reaching its limit. */}
|
|
1271
|
-
<div className={cls(
|
|
1272
|
-
"flex-1 min-h-0 flex flex-col",
|
|
1273
|
-
(viewMode === "list" || viewMode === "cards") && "overflow-y-auto"
|
|
1274
|
-
)}>
|
|
1275
|
-
{collectionSurface}
|
|
1276
|
-
</div>
|
|
1277
|
-
</div>
|
|
1278
|
-
)}
|
|
1279
|
-
|
|
1280
|
-
{popupCell && <PopupFormField
|
|
1281
|
-
key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
|
|
1282
|
-
open={Boolean(popupCell)}
|
|
1283
|
-
onClose={onPopupClose}
|
|
1284
|
-
cellRect={popupCell?.cellRect}
|
|
1285
|
-
propertyKey={popupCell?.propertyKey}
|
|
1286
|
-
collection={collection}
|
|
1287
|
-
entityId={popupCell.entityId}
|
|
1288
|
-
tableKey={tableKey.current}
|
|
1289
|
-
customFieldValidator={uniqueFieldValidator}
|
|
1290
|
-
path={path}
|
|
1291
|
-
onCellValueChange={onValueChange}
|
|
1292
|
-
container={containerRef.current}/>}
|
|
1293
|
-
|
|
1294
|
-
{deleteEntityClicked &&
|
|
1295
|
-
<DeleteEntityDialog
|
|
1296
|
-
entityOrEntitiesToDelete={deleteEntityClicked}
|
|
1297
|
-
path={path}
|
|
1298
|
-
collection={collection}
|
|
1299
|
-
callbacks={collection.callbacks}
|
|
1300
|
-
open={Boolean(deleteEntityClicked)}
|
|
1301
|
-
onEntityDelete={internalOnEntityDelete}
|
|
1302
|
-
onMultipleEntitiesDelete={internalOnMultipleEntitiesDelete}
|
|
1303
|
-
onClose={() => setDeleteEntityClicked(undefined)}/>}
|
|
1304
|
-
|
|
1305
|
-
</div>
|
|
1306
|
-
);
|
|
1307
|
-
|
|
1308
|
-
return mainContent;
|
|
1309
|
-
}, (a, b) => {
|
|
1310
|
-
return equal(a.path, b.path) &&
|
|
1311
|
-
equal(a.parentCollectionSlugs, b.parentCollectionSlugs) && equal(a.parentEntityIds, b.parentEntityIds) &&
|
|
1312
|
-
equal(a.isSubCollection, b.isSubCollection) &&
|
|
1313
|
-
equal(a.className, b.className) &&
|
|
1314
|
-
equal(a.properties, b.properties) &&
|
|
1315
|
-
equal(a.propertiesOrder, b.propertiesOrder) &&
|
|
1316
|
-
equal(a.hideIdFromCollection, b.hideIdFromCollection) &&
|
|
1317
|
-
equal(a.inlineEditing, b.inlineEditing) &&
|
|
1318
|
-
equal(a.selectionEnabled, b.selectionEnabled) &&
|
|
1319
|
-
equal(a.selectionController, b.selectionController) &&
|
|
1320
|
-
equal(a.Actions, b.Actions) &&
|
|
1321
|
-
equal(a.defaultSize, b.defaultSize) &&
|
|
1322
|
-
equal(a.includeJsonView, b.includeJsonView) &&
|
|
1323
|
-
equal(a.additionalFields, b.additionalFields) &&
|
|
1324
|
-
equal(a.sideDialogWidth, b.sideDialogWidth) &&
|
|
1325
|
-
equal(a.openEntityMode, b.openEntityMode) &&
|
|
1326
|
-
equal(a.exportable, b.exportable) &&
|
|
1327
|
-
equal(a.history, b.history) &&
|
|
1328
|
-
equal(a.fixedFilter, b.fixedFilter) &&
|
|
1329
|
-
equal(a.selectedEntityId, b.selectedEntityId) &&
|
|
1330
|
-
equal(a.selectedTab, b.selectedTab);
|
|
1331
|
-
}) as React.FunctionComponent<CollectionViewBindingProps<any>>;
|
|
1332
|
-
|
|
1333
|
-
export const CollectionViewBinding = React.memo(
|
|
1334
|
-
function CollectionViewBinding<M extends Record<string, unknown>>(props: CollectionViewBindingProps<M>) {
|
|
1335
|
-
const collectionRegistry = useCollectionRegistryController();
|
|
1336
|
-
const path = props.path ?? props.slug;
|
|
1337
|
-
const collection = collectionRegistry.getCollection(path) ?? props;
|
|
1338
|
-
|
|
1339
|
-
const content = <CollectionViewBindingInner {...props} />;
|
|
1340
|
-
|
|
1341
|
-
if (collection) {
|
|
1342
|
-
return (
|
|
1343
|
-
<CollectionScopeProvider collection={collection as AdminCollection}>
|
|
1344
|
-
{content}
|
|
1345
|
-
</CollectionScopeProvider>
|
|
1346
|
-
);
|
|
1347
|
-
}
|
|
1348
|
-
return content;
|
|
1349
|
-
}
|
|
1350
|
-
) as React.FunctionComponent<CollectionViewBindingProps<any>>;
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Default empty state shown when a collection has no entities.
|
|
1354
|
-
* Used as the fallback for the `"Collection.EmptyState"` component override.
|
|
1355
|
-
*
|
|
1356
|
-
* @internal
|
|
1357
|
-
*/
|
|
1358
|
-
function DefaultCollectionEmptyState({
|
|
1359
|
-
canCreate,
|
|
1360
|
-
onNewClick,
|
|
1361
|
-
isSearching,
|
|
1362
|
-
searchString
|
|
1363
|
-
}: {
|
|
1364
|
-
canCreate: boolean;
|
|
1365
|
-
onNewClick: () => void;
|
|
1366
|
-
isSearching: boolean;
|
|
1367
|
-
searchString: string;
|
|
1368
|
-
}) {
|
|
1369
|
-
const { t } = useTranslation();
|
|
1370
|
-
if (canCreate) {
|
|
1371
|
-
return (
|
|
1372
|
-
<div className="flex flex-col items-center justify-center">
|
|
1373
|
-
<Typography variant={"subtitle2"}>{t("so_empty")}</Typography>
|
|
1374
|
-
<Button
|
|
1375
|
-
onClick={onNewClick}
|
|
1376
|
-
className="mt-4"
|
|
1377
|
-
>
|
|
1378
|
-
<PlusIcon/>
|
|
1379
|
-
{t("create_your_first_entry")}
|
|
1380
|
-
</Button>
|
|
1381
|
-
</div>
|
|
1382
|
-
);
|
|
1383
|
-
}
|
|
1384
|
-
return (
|
|
1385
|
-
<Typography variant={"label"}>
|
|
1386
|
-
{isSearching
|
|
1387
|
-
? t("no_results_search", { search: searchString })
|
|
1388
|
-
: t("no_results_filter_sort")}
|
|
1389
|
-
</Typography>
|
|
1390
|
-
);
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
/**
|
|
1394
|
-
* Inflight count request deduplication map.
|
|
1395
|
-
* Keyed by `path|filterKey|sort|search` so that concurrent
|
|
1396
|
-
* callers (e.g. React StrictMode double-mount) share the same promise.
|
|
1397
|
-
*/
|
|
1398
|
-
const inflightCountRequests = new Map<string, Promise<number>>();
|
|
1399
|
-
|
|
1400
|
-
export function EntitiesCount({
|
|
1401
|
-
path,
|
|
1402
|
-
collection,
|
|
1403
|
-
filter,
|
|
1404
|
-
sortBy,
|
|
1405
|
-
searchString,
|
|
1406
|
-
onCountChange
|
|
1407
|
-
}: {
|
|
1408
|
-
path: string,
|
|
1409
|
-
collection: AdminCollection,
|
|
1410
|
-
filter?: FilterValues<any>,
|
|
1411
|
-
sortBy?: OrderByTuple[],
|
|
1412
|
-
/**
|
|
1413
|
-
* Required, not optional. The term sits in the same scope as the element
|
|
1414
|
-
* that mounts this and is passed to the toolbar and the empty state beside
|
|
1415
|
-
* it — it was simply not passed here, so a searched list showed the count
|
|
1416
|
-
* of the unsearched collection. A required prop is what stops the next
|
|
1417
|
-
* caller doing the same; there is no useful "count without the search".
|
|
1418
|
-
*/
|
|
1419
|
-
searchString: string | undefined,
|
|
1420
|
-
onCountChange?: (count: number | null | undefined) => void,
|
|
1421
|
-
}) {
|
|
1422
|
-
|
|
1423
|
-
const dataClient = useData();
|
|
1424
|
-
|
|
1425
|
-
// The whole sort as one string: it keys the dedup cache and drives the
|
|
1426
|
-
// effect, and a `sortBy` array is a new reference on every render.
|
|
1427
|
-
const sortKey = React.useMemo(() => sortBy ? serializeOrderBy(sortBy) ?? "" : "", [sortBy]);
|
|
1428
|
-
|
|
1429
|
-
// Use refs for values that should NOT trigger re-fetches
|
|
1430
|
-
const dataClientRef = React.useRef(dataClient);
|
|
1431
|
-
dataClientRef.current = dataClient;
|
|
1432
|
-
const onCountChangeRef = React.useRef(onCountChange);
|
|
1433
|
-
onCountChangeRef.current = onCountChange;
|
|
1434
|
-
|
|
1435
|
-
// Serialize filter to a stable string to avoid re-fetches on object identity changes
|
|
1436
|
-
const filterKey = React.useMemo(() => filter ? JSON.stringify(filter) : "", [filter]);
|
|
1437
|
-
|
|
1438
|
-
useEffect(() => {
|
|
1439
|
-
const accessor = dataClientRef.current.collection(path);
|
|
1440
|
-
if (!accessor.count) {
|
|
1441
|
-
onCountChangeRef.current?.(undefined);
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
let cancelled = false;
|
|
1446
|
-
|
|
1447
|
-
// filterValues is already FilterValues — pass directly
|
|
1448
|
-
const whereParams = filter && Object.keys(filter).length > 0 ? filter : undefined;
|
|
1449
|
-
const orderByParams = sortBy && sortBy.length > 0
|
|
1450
|
-
? sortBy.map(([field, direction]) => [String(field), direction] as OrderByTuple)
|
|
1451
|
-
: undefined;
|
|
1452
|
-
|
|
1453
|
-
// Deduplicate inflight count requests (e.g. React StrictMode double-mount)
|
|
1454
|
-
// The search term is part of the query, so it is part of the key. The
|
|
1455
|
-
// cache is module-level and outlives an unmount, so a key that omits it
|
|
1456
|
-
// does not merely lose precision — it answers one search with a
|
|
1457
|
-
// different search's total.
|
|
1458
|
-
const cacheKey = `${path}|${filterKey}|${sortKey}|${searchString ?? ""}`;
|
|
1459
|
-
let countPromise = inflightCountRequests.get(cacheKey);
|
|
1460
|
-
if (!countPromise) {
|
|
1461
|
-
countPromise = accessor.count({
|
|
1462
|
-
where: whereParams,
|
|
1463
|
-
orderBy: orderByParams,
|
|
1464
|
-
searchString
|
|
1465
|
-
});
|
|
1466
|
-
inflightCountRequests.set(cacheKey, countPromise);
|
|
1467
|
-
// Clean up the inflight entry once resolved/rejected
|
|
1468
|
-
countPromise.finally(() => inflightCountRequests.delete(cacheKey));
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
countPromise.then((c) => {
|
|
1472
|
-
if (!cancelled) onCountChangeRef.current?.(c);
|
|
1473
|
-
}).catch((e) => {
|
|
1474
|
-
console.warn("Error fetching count", e);
|
|
1475
|
-
if (!cancelled) onCountChangeRef.current?.(undefined);
|
|
1476
|
-
});
|
|
1477
|
-
|
|
1478
|
-
return () => { cancelled = true; };
|
|
1479
|
-
// `filterKey` and `sortKey` ARE `filter` and `sortBy` — the memoized
|
|
1480
|
-
// serializations a few lines up, and the only stable identity either
|
|
1481
|
-
// has. Both arrive as freshly built objects on every render, so
|
|
1482
|
-
// depending on them directly would re-run this effect each time and
|
|
1483
|
-
// fire a count request per render. The keys change exactly when the
|
|
1484
|
-
// values do, which is the condition this effect actually wants.
|
|
1485
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1486
|
-
}, [path, filterKey, sortKey, searchString]);
|
|
1487
|
-
|
|
1488
|
-
// The toolbar renders the count; this component only fetches and reports it
|
|
1489
|
-
return null;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
function buildPropertyWidthOverwrite(key: string, width: number): PartialCollectionConfig {
|
|
1493
|
-
if (key.includes(".")) {
|
|
1494
|
-
const [parentKey, ...childKey] = key.split(".");
|
|
1495
|
-
return { properties: { [parentKey]: buildPropertyWidthOverwrite(childKey.join("."), width) } } as PartialCollectionConfig;
|
|
1496
|
-
}
|
|
1497
|
-
return { properties: { [key]: { admin: { columnWidth: width } } } } as PartialCollectionConfig;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function EntityIdHeaderWidget({
|
|
1501
|
-
collection,
|
|
1502
|
-
path,
|
|
1503
|
-
idPath,
|
|
1504
|
-
openEntityMode
|
|
1505
|
-
}: {
|
|
1506
|
-
collection: AdminCollection,
|
|
1507
|
-
path: string,
|
|
1508
|
-
idPath: string,
|
|
1509
|
-
/**
|
|
1510
|
-
* Resolved by the parent so that finding an entity by id lands it in the
|
|
1511
|
-
* same surface a row click would — not a different one.
|
|
1512
|
-
*/
|
|
1513
|
-
openEntityMode: OpenEntityMode
|
|
1514
|
-
}) {
|
|
1515
|
-
|
|
1516
|
-
const { t } = useTranslation();
|
|
1517
|
-
const urlController = useUrlController();
|
|
1518
|
-
const [openPopup, setOpenPopup] = React.useState(false);
|
|
1519
|
-
const [searchString, setSearchString] = React.useState("");
|
|
1520
|
-
const [recentIds, setRecentIds] = React.useState<string[]>(getRecentIds(collection.slug).map(String));
|
|
1521
|
-
const sidePanelController = useSidePanel();
|
|
1522
|
-
|
|
1523
|
-
return (
|
|
1524
|
-
<Tooltip title={!openPopup ? t("find_by_id") : undefined} asChild={false}>
|
|
1525
|
-
<Popover
|
|
1526
|
-
open={openPopup}
|
|
1527
|
-
onOpenChange={setOpenPopup}
|
|
1528
|
-
sideOffset={0}
|
|
1529
|
-
align={"start"}
|
|
1530
|
-
alignOffset={-117}
|
|
1531
|
-
trigger={
|
|
1532
|
-
<IconButton size={"small"}>
|
|
1533
|
-
<SearchIcon size={iconSize.small}/>
|
|
1534
|
-
</IconButton>
|
|
1535
|
-
}>
|
|
1536
|
-
<div
|
|
1537
|
-
className={cls("my-2 rounded-lg bg-surface-50 dark:bg-surface-800 text-surface-900 dark:text-white")}>
|
|
1538
|
-
<form noValidate={true}
|
|
1539
|
-
onSubmit={(e) => {
|
|
1540
|
-
e.preventDefault();
|
|
1541
|
-
if (!searchString) return;
|
|
1542
|
-
setOpenPopup(false);
|
|
1543
|
-
const entityId = searchString.trim();
|
|
1544
|
-
setRecentIds(addRecentId(collection.slug, entityId).map(String));
|
|
1545
|
-
navigateToEntity({
|
|
1546
|
-
openEntityMode,
|
|
1547
|
-
collection,
|
|
1548
|
-
entityId,
|
|
1549
|
-
path,
|
|
1550
|
-
|
|
1551
|
-
sidePanelController,
|
|
1552
|
-
navigation: urlController
|
|
1553
|
-
})
|
|
1554
|
-
}}
|
|
1555
|
-
className={"w-96 max-w-full"}>
|
|
1556
|
-
|
|
1557
|
-
<div className="flex p-2 w-full gap-2">
|
|
1558
|
-
<TextField
|
|
1559
|
-
autoFocus={openPopup}
|
|
1560
|
-
aria-label={t("find_entity_by_id")}
|
|
1561
|
-
placeholder={t("find_entity_by_id")}
|
|
1562
|
-
size="small"
|
|
1563
|
-
onChange={(e) => {
|
|
1564
|
-
setSearchString(e.target.value);
|
|
1565
|
-
}}
|
|
1566
|
-
value={searchString}
|
|
1567
|
-
className="flex-grow"
|
|
1568
|
-
inputClassName={cls("rounded-lg bg-white dark:bg-surface-900", focusedDisabled)}
|
|
1569
|
-
/>
|
|
1570
|
-
<Button variant={"text"}
|
|
1571
|
-
disabled={!(searchString.trim())}
|
|
1572
|
-
type={"submit"}
|
|
1573
|
-
><ArrowRightToLineIcon/></Button>
|
|
1574
|
-
</div>
|
|
1575
|
-
</form>
|
|
1576
|
-
{recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
|
|
1577
|
-
{recentIds.map(id => (
|
|
1578
|
-
<ReferencePreview reference={new EntityReference({ id,
|
|
1579
|
-
path })}
|
|
1580
|
-
key={id}
|
|
1581
|
-
hover={true}
|
|
1582
|
-
onClick={() => {
|
|
1583
|
-
setOpenPopup(false);
|
|
1584
|
-
navigateToEntity({
|
|
1585
|
-
openEntityMode,
|
|
1586
|
-
collection,
|
|
1587
|
-
entityId: id,
|
|
1588
|
-
path,
|
|
1589
|
-
|
|
1590
|
-
sidePanelController,
|
|
1591
|
-
navigation: urlController
|
|
1592
|
-
})
|
|
1593
|
-
}}
|
|
1594
|
-
includeEntityLink={false}
|
|
1595
|
-
size={"small"}/>
|
|
1596
|
-
))}
|
|
1597
|
-
</div>}
|
|
1598
|
-
</div>
|
|
1599
|
-
</Popover>
|
|
1600
|
-
|
|
1601
|
-
</Tooltip>
|
|
1602
|
-
);
|
|
1603
|
-
}
|