@maxal_studio/kratosjs-react 1.0.0
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 +44 -0
- package/dist/FieldRenderer.d.ts +13 -0
- package/dist/FieldRenderer.js +62 -0
- package/dist/FormRenderer.d.ts +7 -0
- package/dist/FormRenderer.js +78 -0
- package/dist/TableRenderer.d.ts +2 -0
- package/dist/TableRenderer.js +1 -0
- package/dist/api/actionsApi.d.ts +23 -0
- package/dist/api/actionsApi.js +46 -0
- package/dist/api/authenticatedFetch.d.ts +8 -0
- package/dist/api/authenticatedFetch.js +31 -0
- package/dist/api/exportApi.d.ts +18 -0
- package/dist/api/exportApi.js +50 -0
- package/dist/api/http.d.ts +24 -0
- package/dist/api/http.js +52 -0
- package/dist/api/resourceApi.d.ts +37 -0
- package/dist/api/resourceApi.js +52 -0
- package/dist/api/tableApi.d.ts +83 -0
- package/dist/api/tableApi.js +51 -0
- package/dist/api/urls.d.ts +19 -0
- package/dist/api/urls.js +46 -0
- package/dist/app.d.ts +101 -0
- package/dist/app.js +89 -0
- package/dist/auth/AuthContext.d.ts +22 -0
- package/dist/auth/AuthContext.js +147 -0
- package/dist/auth/LoginPage.d.ts +10 -0
- package/dist/auth/LoginPage.js +179 -0
- package/dist/auth/ProtectedRoute.d.ts +12 -0
- package/dist/auth/ProtectedRoute.js +22 -0
- package/dist/auth/authApiClient.d.ts +24 -0
- package/dist/auth/authApiClient.js +95 -0
- package/dist/auth/types.d.ts +103 -0
- package/dist/auth/types.js +1 -0
- package/dist/components/ActionFormModal.d.ts +22 -0
- package/dist/components/ActionFormModal.js +8 -0
- package/dist/components/AdminPanel.d.ts +11 -0
- package/dist/components/AdminPanel.js +194 -0
- package/dist/components/Checkbox.d.ts +10 -0
- package/dist/components/Checkbox.js +8 -0
- package/dist/components/CheckboxField.d.ts +7 -0
- package/dist/components/CheckboxField.js +26 -0
- package/dist/components/ColorPickerField.d.ts +7 -0
- package/dist/components/ColorPickerField.js +26 -0
- package/dist/components/DateTimePickerField.d.ts +7 -0
- package/dist/components/DateTimePickerField.js +64 -0
- package/dist/components/FileUploadField.d.ts +9 -0
- package/dist/components/FileUploadField.js +478 -0
- package/dist/components/GlobalSearch.d.ts +22 -0
- package/dist/components/GlobalSearch.js +181 -0
- package/dist/components/GroupField.d.ts +7 -0
- package/dist/components/GroupField.js +23 -0
- package/dist/components/HiddenField.d.ts +3 -0
- package/dist/components/HiddenField.js +10 -0
- package/dist/components/ModalBreadcrumb.d.ts +5 -0
- package/dist/components/ModalBreadcrumb.js +33 -0
- package/dist/components/ModalDrawer.d.ts +15 -0
- package/dist/components/ModalDrawer.js +40 -0
- package/dist/components/RadioField.d.ts +7 -0
- package/dist/components/RadioField.js +26 -0
- package/dist/components/RepeaterField.d.ts +3 -0
- package/dist/components/RepeaterField.js +191 -0
- package/dist/components/ResourceModalRenderer.d.ts +10 -0
- package/dist/components/ResourceModalRenderer.js +80 -0
- package/dist/components/RichEditorField.d.ts +3 -0
- package/dist/components/RichEditorField.js +655 -0
- package/dist/components/SectionField.d.ts +9 -0
- package/dist/components/SectionField.js +111 -0
- package/dist/components/SelectField.d.ts +8 -0
- package/dist/components/SelectField.js +523 -0
- package/dist/components/TabsField.d.ts +10 -0
- package/dist/components/TabsField.js +214 -0
- package/dist/components/TagsInputField.d.ts +7 -0
- package/dist/components/TagsInputField.js +172 -0
- package/dist/components/TextInputField.d.ts +7 -0
- package/dist/components/TextInputField.js +44 -0
- package/dist/components/TextareaField.d.ts +7 -0
- package/dist/components/TextareaField.js +31 -0
- package/dist/components/ToggleField.d.ts +7 -0
- package/dist/components/ToggleField.js +57 -0
- package/dist/components/ViewModal.d.ts +25 -0
- package/dist/components/ViewModal.js +159 -0
- package/dist/components/blocks/BlockRenderer.d.ts +7 -0
- package/dist/components/blocks/BlockRenderer.js +36 -0
- package/dist/components/blocks/FormBlockRenderer.d.ts +6 -0
- package/dist/components/blocks/FormBlockRenderer.js +110 -0
- package/dist/components/blocks/TableBlockRenderer.d.ts +6 -0
- package/dist/components/blocks/TableBlockRenderer.js +12 -0
- package/dist/components/blocks/TabsBlockRenderer.d.ts +7 -0
- package/dist/components/blocks/TabsBlockRenderer.js +11 -0
- package/dist/components/blocks/WidgetBlockRenderer.d.ts +6 -0
- package/dist/components/blocks/WidgetBlockRenderer.js +11 -0
- package/dist/components/columns/CheckboxColumnComponent.d.ts +6 -0
- package/dist/components/columns/CheckboxColumnComponent.js +21 -0
- package/dist/components/columns/ColorColumnComponent.d.ts +3 -0
- package/dist/components/columns/ColorColumnComponent.js +11 -0
- package/dist/components/columns/DeeplinkWrapper.d.ts +15 -0
- package/dist/components/columns/DeeplinkWrapper.js +85 -0
- package/dist/components/columns/IconColumnComponent.d.ts +3 -0
- package/dist/components/columns/IconColumnComponent.js +52 -0
- package/dist/components/columns/ImageColumnComponent.d.ts +3 -0
- package/dist/components/columns/ImageColumnComponent.js +98 -0
- package/dist/components/columns/MediaColumnComponent.d.ts +3 -0
- package/dist/components/columns/MediaColumnComponent.js +160 -0
- package/dist/components/columns/SelectColumnComponent.d.ts +6 -0
- package/dist/components/columns/SelectColumnComponent.js +26 -0
- package/dist/components/columns/TagsColumnComponent.d.ts +3 -0
- package/dist/components/columns/TagsColumnComponent.js +18 -0
- package/dist/components/columns/TextColumnComponent.d.ts +11 -0
- package/dist/components/columns/TextColumnComponent.js +107 -0
- package/dist/components/columns/TextInputColumnComponent.d.ts +6 -0
- package/dist/components/columns/TextInputColumnComponent.js +18 -0
- package/dist/components/columns/ToggleColumnComponent.d.ts +6 -0
- package/dist/components/columns/ToggleColumnComponent.js +25 -0
- package/dist/components/columns/VideoColumnComponent.d.ts +3 -0
- package/dist/components/columns/VideoColumnComponent.js +125 -0
- package/dist/components/columns/ViewColumnComponent.d.ts +3 -0
- package/dist/components/columns/ViewColumnComponent.js +7 -0
- package/dist/components/errors/ErrorBoundary.d.ts +23 -0
- package/dist/components/errors/ErrorBoundary.js +33 -0
- package/dist/components/filters/CustomFilterComponent.d.ts +10 -0
- package/dist/components/filters/CustomFilterComponent.js +33 -0
- package/dist/components/filters/DateFilterComponent.d.ts +15 -0
- package/dist/components/filters/DateFilterComponent.js +132 -0
- package/dist/components/filters/QueryBuilderFilterComponent.d.ts +11 -0
- package/dist/components/filters/QueryBuilderFilterComponent.js +200 -0
- package/dist/components/layout/Header.d.ts +10 -0
- package/dist/components/layout/Header.js +70 -0
- package/dist/components/layout/PanelBrandMark.d.ts +8 -0
- package/dist/components/layout/PanelBrandMark.js +28 -0
- package/dist/components/layout/Sidebar.d.ts +35 -0
- package/dist/components/layout/Sidebar.js +125 -0
- package/dist/components/modals/RelationCreateModal.d.ts +19 -0
- package/dist/components/modals/RelationCreateModal.js +57 -0
- package/dist/components/modals/ResourceFormModal.d.ts +37 -0
- package/dist/components/modals/ResourceFormModal.js +44 -0
- package/dist/components/modals/useResourceForm.d.ts +40 -0
- package/dist/components/modals/useResourceForm.js +138 -0
- package/dist/components/modals/view/RecordActions.d.ts +17 -0
- package/dist/components/modals/view/RecordActions.js +16 -0
- package/dist/components/modals/view/RecordDetails.d.ts +13 -0
- package/dist/components/modals/view/RecordDetails.js +29 -0
- package/dist/components/modals/view/RelationPanel.d.ts +18 -0
- package/dist/components/modals/view/RelationPanel.js +16 -0
- package/dist/components/modals/view/RelationTabs.d.ts +32 -0
- package/dist/components/modals/view/RelationTabs.js +42 -0
- package/dist/components/modals/view/useRecordView.d.ts +18 -0
- package/dist/components/modals/view/useRecordView.js +114 -0
- package/dist/components/pages/PageRenderer.d.ts +6 -0
- package/dist/components/pages/PageRenderer.js +107 -0
- package/dist/components/table/ColumnTogglePopup.d.ts +11 -0
- package/dist/components/table/ColumnTogglePopup.js +16 -0
- package/dist/components/table/GridCard.d.ts +21 -0
- package/dist/components/table/GridCard.js +30 -0
- package/dist/components/table/GridView.d.ts +23 -0
- package/dist/components/table/GridView.js +49 -0
- package/dist/components/table/LayoutToggle.d.ts +7 -0
- package/dist/components/table/LayoutToggle.js +9 -0
- package/dist/components/table/TableActionsDropdown.d.ts +13 -0
- package/dist/components/table/TableActionsDropdown.js +46 -0
- package/dist/components/table/TableBulkActions.d.ts +11 -0
- package/dist/components/table/TableBulkActions.js +21 -0
- package/dist/components/table/TableHeader.d.ts +14 -0
- package/dist/components/table/TableHeader.js +23 -0
- package/dist/components/table/TablePagination.d.ts +13 -0
- package/dist/components/table/TablePagination.js +55 -0
- package/dist/components/table/TableRow.d.ts +21 -0
- package/dist/components/table/TableRow.js +32 -0
- package/dist/components/table/TableSearchBar.d.ts +11 -0
- package/dist/components/table/TableSearchBar.js +12 -0
- package/dist/components/table/TableTabs.d.ts +14 -0
- package/dist/components/table/TableTabs.js +8 -0
- package/dist/components/ui/Badge.d.ts +6 -0
- package/dist/components/ui/Badge.js +12 -0
- package/dist/components/ui/Button.d.ts +22 -0
- package/dist/components/ui/Button.js +22 -0
- package/dist/components/ui/Card.d.ts +7 -0
- package/dist/components/ui/Card.js +5 -0
- package/dist/components/ui/ConfirmDialog.d.ts +19 -0
- package/dist/components/ui/ConfirmDialog.js +45 -0
- package/dist/components/ui/EmptyState.d.ts +9 -0
- package/dist/components/ui/EmptyState.js +6 -0
- package/dist/components/ui/ErrorAlert.d.ts +7 -0
- package/dist/components/ui/ErrorAlert.js +9 -0
- package/dist/components/ui/Input.d.ts +11 -0
- package/dist/components/ui/Input.js +10 -0
- package/dist/components/ui/Label.d.ts +5 -0
- package/dist/components/ui/Label.js +5 -0
- package/dist/components/ui/PillButton.d.ts +14 -0
- package/dist/components/ui/PillButton.js +19 -0
- package/dist/components/ui/Select.d.ts +7 -0
- package/dist/components/ui/Select.js +7 -0
- package/dist/components/ui/Spinner.d.ts +8 -0
- package/dist/components/ui/Spinner.js +14 -0
- package/dist/components/ui/Toast.d.ts +21 -0
- package/dist/components/ui/Toast.js +47 -0
- package/dist/components/ui/index.d.ts +24 -0
- package/dist/components/ui/index.js +12 -0
- package/dist/components/utils/HintDisplay.d.ts +11 -0
- package/dist/components/utils/HintDisplay.js +12 -0
- package/dist/components/utils/Icon.d.ts +22 -0
- package/dist/components/utils/Icon.js +22 -0
- package/dist/components/utils/MediaPreviewModal.d.ts +14 -0
- package/dist/components/utils/MediaPreviewModal.js +32 -0
- package/dist/components/utils/ViewFieldWrapper.d.ts +11 -0
- package/dist/components/utils/ViewFieldWrapper.js +9 -0
- package/dist/components/utils/layoutHelpers.d.ts +19 -0
- package/dist/components/utils/layoutHelpers.js +257 -0
- package/dist/components/widgets/ChartWidget.d.ts +16 -0
- package/dist/components/widgets/ChartWidget.js +192 -0
- package/dist/components/widgets/StatsWidget.d.ts +16 -0
- package/dist/components/widgets/StatsWidget.js +39 -0
- package/dist/components/widgets/WidgetRenderer.d.ts +10 -0
- package/dist/components/widgets/WidgetRenderer.js +50 -0
- package/dist/components/widgets/WidgetShell.d.ts +9 -0
- package/dist/components/widgets/WidgetShell.js +7 -0
- package/dist/contexts/AuthChallengeRegistryContext.d.ts +15 -0
- package/dist/contexts/AuthChallengeRegistryContext.js +15 -0
- package/dist/contexts/BlockRegistryContext.d.ts +18 -0
- package/dist/contexts/BlockRegistryContext.js +8 -0
- package/dist/contexts/ColumnRegistryContext.d.ts +8 -0
- package/dist/contexts/ColumnRegistryContext.js +30 -0
- package/dist/contexts/FieldRegistryContext.d.ts +13 -0
- package/dist/contexts/FieldRegistryContext.js +46 -0
- package/dist/contexts/PanelMetadataContext.d.ts +26 -0
- package/dist/contexts/PanelMetadataContext.js +26 -0
- package/dist/contexts/PanelProviders.d.ts +27 -0
- package/dist/contexts/PanelProviders.js +24 -0
- package/dist/contexts/ResourceModalContext.d.ts +26 -0
- package/dist/contexts/ResourceModalContext.js +76 -0
- package/dist/contexts/SlotRegistryContext.d.ts +19 -0
- package/dist/contexts/SlotRegistryContext.js +24 -0
- package/dist/contexts/TableRefreshContext.d.ts +10 -0
- package/dist/contexts/TableRefreshContext.js +30 -0
- package/dist/contexts/WidgetRegistryContext.d.ts +17 -0
- package/dist/contexts/WidgetRegistryContext.js +14 -0
- package/dist/contexts/createRegistryContext.d.ts +19 -0
- package/dist/contexts/createRegistryContext.js +20 -0
- package/dist/hooks/useAfterStateUpdated.d.ts +6 -0
- package/dist/hooks/useAfterStateUpdated.js +62 -0
- package/dist/hooks/useValidation.d.ts +26 -0
- package/dist/hooks/useValidation.js +76 -0
- package/dist/i18n/I18nProvider.d.ts +27 -0
- package/dist/i18n/I18nProvider.js +101 -0
- package/dist/i18n/LocaleSwitcher.d.ts +10 -0
- package/dist/i18n/LocaleSwitcher.js +30 -0
- package/dist/i18n/activeLocale.d.ts +11 -0
- package/dist/i18n/activeLocale.js +34 -0
- package/dist/i18n/buildClientI18n.d.ts +28 -0
- package/dist/i18n/buildClientI18n.js +67 -0
- package/dist/i18n/index.d.ts +11 -0
- package/dist/i18n/index.js +9 -0
- package/dist/i18n/locales/core/en.d.ts +225 -0
- package/dist/i18n/locales/core/en.js +252 -0
- package/dist/i18n/locales/core/index.d.ts +2 -0
- package/dist/i18n/locales/core/index.js +4 -0
- package/dist/i18n/locales/core/sq.d.ts +253 -0
- package/dist/i18n/locales/core/sq.js +255 -0
- package/dist/i18n/useFormatter.d.ts +18 -0
- package/dist/i18n/useFormatter.js +37 -0
- package/dist/i18n/useLocale.d.ts +11 -0
- package/dist/i18n/useLocale.js +11 -0
- package/dist/i18n/useTranslation.d.ts +12 -0
- package/dist/i18n/useTranslation.js +12 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +101 -0
- package/dist/pages/ResourceListPage.d.ts +8 -0
- package/dist/pages/ResourceListPage.js +139 -0
- package/dist/plugin.d.ts +79 -0
- package/dist/plugin.js +34 -0
- package/dist/runtime/conditions.d.ts +35 -0
- package/dist/runtime/conditions.js +97 -0
- package/dist/runtime/formTraversal.d.ts +25 -0
- package/dist/runtime/formTraversal.js +37 -0
- package/dist/runtime/serializedFunctions.d.ts +41 -0
- package/dist/runtime/serializedFunctions.js +264 -0
- package/dist/slots/Slot.d.ts +24 -0
- package/dist/slots/Slot.js +29 -0
- package/dist/slots/SlotCluster.d.ts +22 -0
- package/dist/slots/SlotCluster.js +49 -0
- package/dist/slots/index.d.ts +7 -0
- package/dist/slots/index.js +4 -0
- package/dist/slots/mergeSlots.d.ts +18 -0
- package/dist/slots/mergeSlots.js +35 -0
- package/dist/slots/types.d.ts +87 -0
- package/dist/slots/types.js +30 -0
- package/dist/styles.css +1 -0
- package/dist/table/TableContext.d.ts +36 -0
- package/dist/table/TableContext.js +13 -0
- package/dist/table/TableRenderer.d.ts +29 -0
- package/dist/table/TableRenderer.js +159 -0
- package/dist/table/components/FiltersPanel.d.ts +11 -0
- package/dist/table/components/FiltersPanel.js +52 -0
- package/dist/table/components/TableToolbar.d.ts +28 -0
- package/dist/table/components/TableToolbar.js +27 -0
- package/dist/table/components/TableToolbarButton.d.ts +6 -0
- package/dist/table/components/TableToolbarButton.js +9 -0
- package/dist/table/components/TableView.d.ts +12 -0
- package/dist/table/components/TableView.js +21 -0
- package/dist/table/defaultRowActions.d.ts +21 -0
- package/dist/table/defaultRowActions.js +37 -0
- package/dist/table/hooks/useColumnVisibility.d.ts +13 -0
- package/dist/table/hooks/useColumnVisibility.js +59 -0
- package/dist/table/hooks/useEditableRows.d.ts +22 -0
- package/dist/table/hooks/useEditableRows.js +63 -0
- package/dist/table/hooks/useTableActions.d.ts +54 -0
- package/dist/table/hooks/useTableActions.js +313 -0
- package/dist/table/hooks/useTableData.d.ts +28 -0
- package/dist/table/hooks/useTableData.js +63 -0
- package/dist/table/hooks/useTableLayout.d.ts +12 -0
- package/dist/table/hooks/useTableLayout.js +31 -0
- package/dist/table/hooks/useTableQuery.d.ts +29 -0
- package/dist/table/hooks/useTableQuery.js +135 -0
- package/dist/types/index.d.ts +224 -0
- package/dist/types/index.js +6 -0
- package/dist/utils/classNames.d.ts +7 -0
- package/dist/utils/classNames.js +9 -0
- package/dist/utils/columnMediaDimensions.d.ts +13 -0
- package/dist/utils/columnMediaDimensions.js +29 -0
- package/dist/utils/columnVisibilityStorage.d.ts +22 -0
- package/dist/utils/columnVisibilityStorage.js +56 -0
- package/dist/utils/fieldErrors.d.ts +13 -0
- package/dist/utils/fieldErrors.js +25 -0
- package/dist/utils/formatValue.d.ts +28 -0
- package/dist/utils/formatValue.js +109 -0
- package/dist/utils/layoutStorage.d.ts +23 -0
- package/dist/utils/layoutStorage.js +53 -0
- package/dist/utils/redirectHandler.d.ts +7 -0
- package/dist/utils/redirectHandler.js +25 -0
- package/dist/utils/tableFormatters.d.ts +14 -0
- package/dist/utils/tableFormatters.js +93 -0
- package/dist/utils/widgetVisibilityStorage.d.ts +11 -0
- package/dist/utils/widgetVisibilityStorage.js +39 -0
- package/package.json +101 -0
- package/src/FieldRenderer.test.tsx +44 -0
- package/src/FieldRenderer.tsx +104 -0
- package/src/FormRenderer.containers.test.tsx +121 -0
- package/src/FormRenderer.test.tsx +174 -0
- package/src/FormRenderer.tsx +140 -0
- package/src/TableRenderer.tsx +2 -0
- package/src/api/actionsApi.ts +76 -0
- package/src/api/authenticatedFetch.ts +40 -0
- package/src/api/exportApi.ts +66 -0
- package/src/api/http.test.ts +58 -0
- package/src/api/http.ts +68 -0
- package/src/api/resourceApi.ts +88 -0
- package/src/api/tableApi.test.ts +108 -0
- package/src/api/tableApi.ts +107 -0
- package/src/api/urls.ts +50 -0
- package/src/app.test.tsx +67 -0
- package/src/app.tsx +181 -0
- package/src/auth/AuthContext.tsx +188 -0
- package/src/auth/LoginPage.tsx +380 -0
- package/src/auth/ProtectedRoute.tsx +39 -0
- package/src/auth/authApiClient.ts +109 -0
- package/src/auth/authFlow.test.tsx +168 -0
- package/src/auth/types.ts +104 -0
- package/src/components/ActionFormModal.tsx +45 -0
- package/src/components/AdminPanel.tsx +368 -0
- package/src/components/Checkbox.tsx +59 -0
- package/src/components/CheckboxField.tsx +88 -0
- package/src/components/ColorPickerField.tsx +93 -0
- package/src/components/DateTimePickerField.tsx +112 -0
- package/src/components/FileUploadField.tsx +841 -0
- package/src/components/GlobalSearch.tsx +436 -0
- package/src/components/GroupField.tsx +85 -0
- package/src/components/HiddenField.tsx +14 -0
- package/src/components/ModalBreadcrumb.tsx +74 -0
- package/src/components/ModalDrawer.tsx +137 -0
- package/src/components/RadioField.tsx +80 -0
- package/src/components/RepeaterField.tsx +546 -0
- package/src/components/ResourceModalRenderer.tsx +144 -0
- package/src/components/RichEditorField.tsx +942 -0
- package/src/components/SectionField.tsx +242 -0
- package/src/components/SelectField.tsx +843 -0
- package/src/components/TabsField.test.tsx +151 -0
- package/src/components/TabsField.tsx +386 -0
- package/src/components/TagsInputField.tsx +411 -0
- package/src/components/TextInputField.tsx +91 -0
- package/src/components/TextareaField.tsx +110 -0
- package/src/components/ToggleField.tsx +126 -0
- package/src/components/ViewModal.tsx +353 -0
- package/src/components/blocks/BlockRenderer.tsx +56 -0
- package/src/components/blocks/FormBlockRenderer.tsx +160 -0
- package/src/components/blocks/TableBlockRenderer.tsx +33 -0
- package/src/components/blocks/TabsBlockRenderer.tsx +49 -0
- package/src/components/blocks/WidgetBlockRenderer.tsx +19 -0
- package/src/components/columns/CheckboxColumnComponent.tsx +38 -0
- package/src/components/columns/ColorColumnComponent.tsx +23 -0
- package/src/components/columns/CustomColumn.test.tsx +55 -0
- package/src/components/columns/DeeplinkWrapper.tsx +103 -0
- package/src/components/columns/IconColumnComponent.tsx +55 -0
- package/src/components/columns/ImageColumnComponent.tsx +220 -0
- package/src/components/columns/MediaColumnComponent.tsx +294 -0
- package/src/components/columns/SelectColumnComponent.tsx +49 -0
- package/src/components/columns/TagsColumnComponent.tsx +46 -0
- package/src/components/columns/TextColumnComponent.tsx +191 -0
- package/src/components/columns/TextInputColumnComponent.tsx +35 -0
- package/src/components/columns/ToggleColumnComponent.tsx +56 -0
- package/src/components/columns/VideoColumnComponent.tsx +236 -0
- package/src/components/columns/ViewColumnComponent.tsx +9 -0
- package/src/components/errors/ErrorBoundary.tsx +58 -0
- package/src/components/filters/CustomFilterComponent.tsx +130 -0
- package/src/components/filters/DateFilterComponent.tsx +272 -0
- package/src/components/filters/QueryBuilderFilterComponent.tsx +502 -0
- package/src/components/layout/Header.tsx +212 -0
- package/src/components/layout/PanelBrandMark.tsx +61 -0
- package/src/components/layout/Sidebar.tsx +283 -0
- package/src/components/modals/RelationCreateModal.tsx +107 -0
- package/src/components/modals/ResourceFormModal.test.tsx +119 -0
- package/src/components/modals/ResourceFormModal.tsx +128 -0
- package/src/components/modals/useResourceForm.ts +207 -0
- package/src/components/modals/view/RecordActions.tsx +69 -0
- package/src/components/modals/view/RecordDetails.tsx +60 -0
- package/src/components/modals/view/RelationPanel.tsx +76 -0
- package/src/components/modals/view/RelationTabs.tsx +145 -0
- package/src/components/modals/view/useRecordView.ts +134 -0
- package/src/components/pages/PageRenderer.tsx +173 -0
- package/src/components/table/ColumnTogglePopup.tsx +85 -0
- package/src/components/table/GridCard.tsx +155 -0
- package/src/components/table/GridView.tsx +138 -0
- package/src/components/table/LayoutToggle.tsx +24 -0
- package/src/components/table/TableActionsDropdown.tsx +114 -0
- package/src/components/table/TableBulkActions.tsx +65 -0
- package/src/components/table/TableHeader.tsx +96 -0
- package/src/components/table/TablePagination.tsx +169 -0
- package/src/components/table/TableRow.tsx +155 -0
- package/src/components/table/TableSearchBar.tsx +66 -0
- package/src/components/table/TableTabs.tsx +49 -0
- package/src/components/ui/Badge.tsx +30 -0
- package/src/components/ui/Button.test.tsx +78 -0
- package/src/components/ui/Button.tsx +102 -0
- package/src/components/ui/Card.tsx +23 -0
- package/src/components/ui/ConfirmDialog.tsx +112 -0
- package/src/components/ui/EmptyState.tsx +24 -0
- package/src/components/ui/ErrorAlert.tsx +37 -0
- package/src/components/ui/Input.tsx +48 -0
- package/src/components/ui/Label.tsx +15 -0
- package/src/components/ui/PillButton.tsx +72 -0
- package/src/components/ui/Select.tsx +33 -0
- package/src/components/ui/Spinner.tsx +39 -0
- package/src/components/ui/Toast.tsx +105 -0
- package/src/components/ui/index.ts +24 -0
- package/src/components/utils/HintDisplay.tsx +26 -0
- package/src/components/utils/Icon.tsx +36 -0
- package/src/components/utils/MediaPreviewModal.tsx +114 -0
- package/src/components/utils/ViewFieldWrapper.tsx +23 -0
- package/src/components/utils/layoutHelpers.ts +267 -0
- package/src/components/widgets/ChartWidget.tsx +247 -0
- package/src/components/widgets/StatsWidget.tsx +72 -0
- package/src/components/widgets/WidgetRenderer.tsx +108 -0
- package/src/components/widgets/WidgetShell.tsx +37 -0
- package/src/contexts/AuthChallengeRegistryContext.tsx +29 -0
- package/src/contexts/BlockRegistryContext.tsx +28 -0
- package/src/contexts/ColumnRegistryContext.tsx +38 -0
- package/src/contexts/FieldRegistryContext.tsx +56 -0
- package/src/contexts/PanelMetadataContext.tsx +60 -0
- package/src/contexts/PanelProviders.tsx +85 -0
- package/src/contexts/ResourceModalContext.tsx +137 -0
- package/src/contexts/SlotRegistryContext.tsx +35 -0
- package/src/contexts/TableRefreshContext.tsx +44 -0
- package/src/contexts/WidgetRegistryContext.tsx +34 -0
- package/src/contexts/createRegistryContext.tsx +29 -0
- package/src/hooks/useAfterStateUpdated.ts +70 -0
- package/src/hooks/useValidation.test.ts +59 -0
- package/src/hooks/useValidation.ts +95 -0
- package/src/i18n/I18nProvider.tsx +128 -0
- package/src/i18n/LocaleSwitcher.tsx +50 -0
- package/src/i18n/activeLocale.ts +39 -0
- package/src/i18n/buildClientI18n.ts +101 -0
- package/src/i18n/i18n.test.tsx +140 -0
- package/src/i18n/index.ts +12 -0
- package/src/i18n/locales/core/en.ts +274 -0
- package/src/i18n/locales/core/index.ts +5 -0
- package/src/i18n/locales/core/sq.ts +275 -0
- package/src/i18n/useFormatter.ts +42 -0
- package/src/i18n/useLocale.ts +16 -0
- package/src/i18n/useTranslation.ts +17 -0
- package/src/index.ts +244 -0
- package/src/pages/ResourceListPage.tsx +205 -0
- package/src/plugin.ts +110 -0
- package/src/runtime/conditions.test.ts +99 -0
- package/src/runtime/conditions.ts +148 -0
- package/src/runtime/formTraversal.ts +41 -0
- package/src/runtime/serializedFunctions.test.ts +59 -0
- package/src/runtime/serializedFunctions.ts +284 -0
- package/src/slots/Slot.test.tsx +89 -0
- package/src/slots/Slot.tsx +47 -0
- package/src/slots/SlotCluster.test.tsx +95 -0
- package/src/slots/SlotCluster.tsx +107 -0
- package/src/slots/index.ts +15 -0
- package/src/slots/mergeSlots.test.ts +71 -0
- package/src/slots/mergeSlots.ts +40 -0
- package/src/slots/slotNames.test.ts +21 -0
- package/src/slots/types.ts +119 -0
- package/src/styles.css +437 -0
- package/src/table/TableContext.tsx +41 -0
- package/src/table/TableRenderer.test.tsx +197 -0
- package/src/table/TableRenderer.tsx +390 -0
- package/src/table/components/FiltersPanel.tsx +193 -0
- package/src/table/components/TableToolbar.tsx +153 -0
- package/src/table/components/TableToolbarButton.tsx +14 -0
- package/src/table/components/TableView.tsx +106 -0
- package/src/table/defaultRowActions.ts +43 -0
- package/src/table/hooks/useColumnVisibility.test.ts +51 -0
- package/src/table/hooks/useColumnVisibility.ts +71 -0
- package/src/table/hooks/useEditableRows.test.ts +69 -0
- package/src/table/hooks/useEditableRows.ts +89 -0
- package/src/table/hooks/useTableActions.ts +393 -0
- package/src/table/hooks/useTableData.ts +89 -0
- package/src/table/hooks/useTableLayout.ts +45 -0
- package/src/table/hooks/useTableQuery.test.ts +116 -0
- package/src/table/hooks/useTableQuery.ts +172 -0
- package/src/test/mockFetch.ts +67 -0
- package/src/test/setup.ts +25 -0
- package/src/types/index.ts +228 -0
- package/src/utils/classNames.ts +10 -0
- package/src/utils/columnMediaDimensions.ts +45 -0
- package/src/utils/columnVisibilityStorage.ts +55 -0
- package/src/utils/fieldErrors.test.ts +35 -0
- package/src/utils/fieldErrors.ts +27 -0
- package/src/utils/formatValue.test.tsx +65 -0
- package/src/utils/formatValue.tsx +117 -0
- package/src/utils/layoutStorage.ts +52 -0
- package/src/utils/redirectHandler.ts +29 -0
- package/src/utils/tableFormatters.test.ts +54 -0
- package/src/utils/tableFormatters.ts +104 -0
- package/src/utils/widgetVisibilityStorage.ts +38 -0
- package/tailwind.config.js +9 -0
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect, KeyboardEvent } from 'react';
|
|
2
|
+
import { useFormContext, useWatch } from 'react-hook-form';
|
|
3
|
+
import { getFieldError } from '../utils/fieldErrors';
|
|
4
|
+
import { FieldProps } from '../types';
|
|
5
|
+
import { useValidation } from '../hooks/useValidation';
|
|
6
|
+
import { cn } from '../utils/classNames';
|
|
7
|
+
import { HintDisplay } from './utils/HintDisplay';
|
|
8
|
+
import { Icon } from './utils/Icon';
|
|
9
|
+
import { ResourceFormModal } from './modals/ResourceFormModal';
|
|
10
|
+
import { executeSerializedFunction } from '../runtime/serializedFunctions';
|
|
11
|
+
import { ViewFieldWrapper } from './utils/ViewFieldWrapper';
|
|
12
|
+
import { formatSelectLabel } from '../utils/formatValue';
|
|
13
|
+
import { authenticatedFetch } from '../api/authenticatedFetch';
|
|
14
|
+
import { translate } from '../i18n/activeLocale';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Component for rendering SelectField in view mode with relationship data fetching
|
|
18
|
+
*/
|
|
19
|
+
function SelectFieldViewMode({
|
|
20
|
+
value,
|
|
21
|
+
isMultiple,
|
|
22
|
+
relationship,
|
|
23
|
+
label,
|
|
24
|
+
optionLabelFormatter,
|
|
25
|
+
apiBaseUrl,
|
|
26
|
+
}: {
|
|
27
|
+
value: any;
|
|
28
|
+
isMultiple: boolean;
|
|
29
|
+
relationship: NonNullable<FieldProps['relationship']>;
|
|
30
|
+
label?: string;
|
|
31
|
+
optionLabelFormatter?: any;
|
|
32
|
+
apiBaseUrl?: string;
|
|
33
|
+
}) {
|
|
34
|
+
const [displayValue, setDisplayValue] = useState<string>('-');
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const fetchRelationshipData = async () => {
|
|
39
|
+
if (!value || (isMultiple && (!Array.isArray(value) || value.length === 0))) {
|
|
40
|
+
setDisplayValue('-');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Extract IDs from value
|
|
45
|
+
const valueIds = isMultiple
|
|
46
|
+
? (Array.isArray(value) ? value : []).map((v: any) => {
|
|
47
|
+
if (typeof v === 'object' && v !== null) {
|
|
48
|
+
return v._id || v.id || String(v);
|
|
49
|
+
}
|
|
50
|
+
return String(v);
|
|
51
|
+
})
|
|
52
|
+
: typeof value === 'object' && value !== null
|
|
53
|
+
? [value._id || value.id || String(value)]
|
|
54
|
+
: [String(value)];
|
|
55
|
+
|
|
56
|
+
if (valueIds.length === 0) {
|
|
57
|
+
setDisplayValue('-');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// If all values are already objects with titleAttribute, use them directly
|
|
62
|
+
if (isMultiple && Array.isArray(value)) {
|
|
63
|
+
const allHaveTitle = value.every(
|
|
64
|
+
(v: any) => typeof v === 'object' && v !== null && v[relationship.titleAttribute],
|
|
65
|
+
);
|
|
66
|
+
if (allHaveTitle) {
|
|
67
|
+
const formatted = value
|
|
68
|
+
.map((v: any) => {
|
|
69
|
+
const title = v[relationship.titleAttribute] || String(v);
|
|
70
|
+
if (optionLabelFormatter) {
|
|
71
|
+
const formattedTitle = executeSerializedFunction(
|
|
72
|
+
optionLabelFormatter,
|
|
73
|
+
v._id || v.id || String(v),
|
|
74
|
+
v,
|
|
75
|
+
);
|
|
76
|
+
return formattedTitle !== null && formattedTitle !== undefined
|
|
77
|
+
? String(formattedTitle)
|
|
78
|
+
: title;
|
|
79
|
+
}
|
|
80
|
+
return title;
|
|
81
|
+
})
|
|
82
|
+
.join(', ');
|
|
83
|
+
setDisplayValue(formatted);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
} else if (
|
|
87
|
+
!isMultiple &&
|
|
88
|
+
typeof value === 'object' &&
|
|
89
|
+
value !== null &&
|
|
90
|
+
value[relationship.titleAttribute]
|
|
91
|
+
) {
|
|
92
|
+
const title = value[relationship.titleAttribute];
|
|
93
|
+
if (optionLabelFormatter) {
|
|
94
|
+
const formattedTitle = executeSerializedFunction(
|
|
95
|
+
optionLabelFormatter,
|
|
96
|
+
value._id || value.id || String(value),
|
|
97
|
+
value,
|
|
98
|
+
);
|
|
99
|
+
setDisplayValue(
|
|
100
|
+
formattedTitle !== null && formattedTitle !== undefined ? String(formattedTitle) : title,
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
setDisplayValue(title);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Need to fetch records
|
|
109
|
+
setIsLoading(true);
|
|
110
|
+
try {
|
|
111
|
+
const resourceSlug = relationship.resource || relationship.name;
|
|
112
|
+
const apiUrl = apiBaseUrl;
|
|
113
|
+
const fetchPromises = valueIds.map(async (id: string) => {
|
|
114
|
+
try {
|
|
115
|
+
const response = await authenticatedFetch(
|
|
116
|
+
`${apiUrl}/${resourceSlug}/${id}`,
|
|
117
|
+
{
|
|
118
|
+
headers: {},
|
|
119
|
+
},
|
|
120
|
+
apiUrl.substring(0, apiUrl.lastIndexOf('/')),
|
|
121
|
+
);
|
|
122
|
+
if (response.ok) {
|
|
123
|
+
const result = await response.json();
|
|
124
|
+
const record = result.data || result;
|
|
125
|
+
return { id, record };
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Error fetching record ${id}:`, error);
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const results = await Promise.all(fetchPromises);
|
|
134
|
+
const validResults = results.filter((r): r is { id: string; record: any } => r !== null);
|
|
135
|
+
|
|
136
|
+
if (validResults.length === 0) {
|
|
137
|
+
setDisplayValue(valueIds.join(', '));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Format the display value
|
|
142
|
+
const formatted = validResults
|
|
143
|
+
.map(({ id, record }) => {
|
|
144
|
+
const title = record[relationship.titleAttribute] || id;
|
|
145
|
+
if (optionLabelFormatter) {
|
|
146
|
+
const formattedTitle = executeSerializedFunction(optionLabelFormatter, id, record);
|
|
147
|
+
return formattedTitle !== null && formattedTitle !== undefined
|
|
148
|
+
? String(formattedTitle)
|
|
149
|
+
: title;
|
|
150
|
+
}
|
|
151
|
+
return title;
|
|
152
|
+
})
|
|
153
|
+
.join(', ');
|
|
154
|
+
|
|
155
|
+
setDisplayValue(formatted);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('Error fetching relationship data:', error);
|
|
158
|
+
// Fallback to showing IDs
|
|
159
|
+
setDisplayValue(valueIds.join(', '));
|
|
160
|
+
} finally {
|
|
161
|
+
setIsLoading(false);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
fetchRelationshipData();
|
|
166
|
+
}, [value, isMultiple, relationship, optionLabelFormatter, apiBaseUrl]);
|
|
167
|
+
|
|
168
|
+
if (isLoading) {
|
|
169
|
+
return (
|
|
170
|
+
<ViewFieldWrapper label={label}>
|
|
171
|
+
<span className="text-fg-secondary">{translate('core:select.loading')}</span>
|
|
172
|
+
</ViewFieldWrapper>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<ViewFieldWrapper label={label}>
|
|
178
|
+
<span dangerouslySetInnerHTML={{ __html: displayValue || '-' }} />
|
|
179
|
+
</ViewFieldWrapper>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Select field component
|
|
185
|
+
* Renders single or multi-select dropdowns with optional creatable functionality
|
|
186
|
+
* Supports relationship-based data fetching from API
|
|
187
|
+
*/
|
|
188
|
+
export function SelectField(props: FieldProps) {
|
|
189
|
+
// View mode: render formatted display
|
|
190
|
+
if (props.mode === 'view') {
|
|
191
|
+
const value = props.value;
|
|
192
|
+
const isMultiple = props.isMultiple || props.multiple;
|
|
193
|
+
const hasRelationship = !!props.relationship;
|
|
194
|
+
|
|
195
|
+
// For relationship-based selects, we need to fetch the related records if value is just an ID
|
|
196
|
+
if (hasRelationship && props.relationship) {
|
|
197
|
+
// Use a component that fetches relationship data
|
|
198
|
+
return (
|
|
199
|
+
<SelectFieldViewMode
|
|
200
|
+
value={value}
|
|
201
|
+
isMultiple={isMultiple}
|
|
202
|
+
relationship={props.relationship}
|
|
203
|
+
label={props.label}
|
|
204
|
+
optionLabelFormatter={props.optionLabelFormatter}
|
|
205
|
+
apiBaseUrl={(props as any).apiBaseUrl}
|
|
206
|
+
/>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// For non-relationship selects, use static options
|
|
211
|
+
const displayValue = formatSelectLabel(value, props.options, isMultiple);
|
|
212
|
+
return <ViewFieldWrapper label={props.label}>{displayValue}</ViewFieldWrapper>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const {
|
|
216
|
+
register,
|
|
217
|
+
setValue,
|
|
218
|
+
control,
|
|
219
|
+
formState: { errors },
|
|
220
|
+
} = useFormContext();
|
|
221
|
+
|
|
222
|
+
const validation = useValidation(props.validation?.rules || [], props.operation, props.name);
|
|
223
|
+
const error = getFieldError(errors, props.name);
|
|
224
|
+
const hasError = !!error;
|
|
225
|
+
const isMultiple = props.isMultiple || props.multiple;
|
|
226
|
+
const isCreatable = props.creatable;
|
|
227
|
+
const isNative = props.native;
|
|
228
|
+
const hasRelationship = !!props.relationship;
|
|
229
|
+
|
|
230
|
+
// Watch current value for multi-select - let the form handle defaults
|
|
231
|
+
const rawValue = useWatch({ control, name: props.name });
|
|
232
|
+
|
|
233
|
+
// Normalize value: if it's an object (from edit mode), extract the ID
|
|
234
|
+
const normalizeValue = (value: any): string | string[] | '' => {
|
|
235
|
+
if (!value) return isMultiple ? [] : '';
|
|
236
|
+
|
|
237
|
+
if (isMultiple) {
|
|
238
|
+
if (!Array.isArray(value)) return [];
|
|
239
|
+
return value.map((v: any) => {
|
|
240
|
+
if (typeof v === 'object' && v !== null) {
|
|
241
|
+
return v._id || v.id || String(v);
|
|
242
|
+
}
|
|
243
|
+
return String(v);
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
if (typeof value === 'object' && value !== null) {
|
|
247
|
+
return value._id || value.id || String(value);
|
|
248
|
+
}
|
|
249
|
+
return String(value);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const currentValue = normalizeValue(rawValue) || (isMultiple ? [] : '');
|
|
254
|
+
|
|
255
|
+
// Update form value if it was an object (normalize it to ID)
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue)) {
|
|
258
|
+
const normalized = normalizeValue(rawValue);
|
|
259
|
+
if (normalized !== rawValue) {
|
|
260
|
+
setValue(props.name, normalized, { shouldValidate: false });
|
|
261
|
+
}
|
|
262
|
+
} else if (rawValue && Array.isArray(rawValue) && rawValue.some((v: any) => typeof v === 'object')) {
|
|
263
|
+
const normalized = normalizeValue(rawValue);
|
|
264
|
+
setValue(props.name, normalized, { shouldValidate: false });
|
|
265
|
+
}
|
|
266
|
+
}, [rawValue, props.name, setValue]);
|
|
267
|
+
|
|
268
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
269
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
270
|
+
const [inputValue, setInputValue] = useState('');
|
|
271
|
+
const [relationshipOptions, setRelationshipOptions] = useState<Record<string, string>>({});
|
|
272
|
+
const [relationshipRecords, setRelationshipRecords] = useState<Record<string, any>>({});
|
|
273
|
+
// Track which records were individually fetched (for edit mode) - these should be formatted
|
|
274
|
+
const [individuallyFetchedIds, setIndividuallyFetchedIds] = useState<Set<string>>(new Set());
|
|
275
|
+
// Cache formatted labels for individually fetched records to prevent re-rendering
|
|
276
|
+
const [formattedLabels, setFormattedLabels] = useState<Record<string, string>>({});
|
|
277
|
+
const [isLoadingRelationship, setIsLoadingRelationship] = useState(false);
|
|
278
|
+
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
279
|
+
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
280
|
+
|
|
281
|
+
// Format option label using custom formatter if provided
|
|
282
|
+
const formatOptionLabel = (value: string, label: string, record?: any): string => {
|
|
283
|
+
if (props.optionLabelFormatter && record) {
|
|
284
|
+
const result = executeSerializedFunction(props.optionLabelFormatter, value, record);
|
|
285
|
+
return result !== null && result !== undefined ? String(result) : label;
|
|
286
|
+
}
|
|
287
|
+
return label;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Fetch relationship data when component mounts or search term changes
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!hasRelationship || !props.relationship) return;
|
|
293
|
+
|
|
294
|
+
const fetchRelationshipData = async () => {
|
|
295
|
+
setIsLoadingRelationship(true);
|
|
296
|
+
try {
|
|
297
|
+
const { resource, titleAttribute } = props.relationship!;
|
|
298
|
+
const resourceSlug = resource || props.relationship!.name;
|
|
299
|
+
|
|
300
|
+
if (!resourceSlug) {
|
|
301
|
+
throw new Error('Resource slug is required for relationship data fetching');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const apiBaseUrl = (props as any).apiBaseUrl;
|
|
305
|
+
const url = `${apiBaseUrl}/${resourceSlug}/list`;
|
|
306
|
+
|
|
307
|
+
const body: any = { perPage: 50 };
|
|
308
|
+
if (searchTerm) {
|
|
309
|
+
body.search = searchTerm;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const response = await authenticatedFetch(
|
|
313
|
+
url,
|
|
314
|
+
{
|
|
315
|
+
method: 'POST',
|
|
316
|
+
headers: { 'Content-Type': 'application/json' },
|
|
317
|
+
body: JSON.stringify(body),
|
|
318
|
+
},
|
|
319
|
+
apiBaseUrl,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
throw new Error(`Failed to fetch relationship data: ${response.status}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = await response.json();
|
|
327
|
+
const data = result.data || [];
|
|
328
|
+
|
|
329
|
+
const options: Record<string, string> = {};
|
|
330
|
+
const records: Record<string, any> = {};
|
|
331
|
+
data.forEach((item: any) => {
|
|
332
|
+
const id = item._id || item.id;
|
|
333
|
+
const title = item[titleAttribute] || id;
|
|
334
|
+
options[id] = title;
|
|
335
|
+
records[id] = item;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Replace with API results so the list reflects current search; preserve labels for selected value(s) so chips still display
|
|
339
|
+
const valueIds = isMultiple
|
|
340
|
+
? Array.isArray(currentValue)
|
|
341
|
+
? currentValue
|
|
342
|
+
: []
|
|
343
|
+
: currentValue
|
|
344
|
+
? [currentValue]
|
|
345
|
+
: [];
|
|
346
|
+
setRelationshipOptions(prev => {
|
|
347
|
+
const next = { ...options };
|
|
348
|
+
valueIds.forEach((id: string) => {
|
|
349
|
+
if (prev[id]) next[id] = prev[id];
|
|
350
|
+
});
|
|
351
|
+
return next;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Replace records with API results; preserve individually fetched ones so formatted labels are kept
|
|
355
|
+
setRelationshipRecords(prev => {
|
|
356
|
+
const next = { ...records };
|
|
357
|
+
valueIds.forEach((id: string) => {
|
|
358
|
+
if (individuallyFetchedIds.has(id) && prev[id]) next[id] = prev[id];
|
|
359
|
+
});
|
|
360
|
+
return next;
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error('Error fetching relationship data:', error);
|
|
364
|
+
setRelationshipOptions({});
|
|
365
|
+
} finally {
|
|
366
|
+
setIsLoadingRelationship(false);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const timeoutId = setTimeout(fetchRelationshipData, searchTerm ? 300 : 0);
|
|
371
|
+
return () => clearTimeout(timeoutId);
|
|
372
|
+
}, [hasRelationship, searchTerm, props.relationship, individuallyFetchedIds, currentValue, isMultiple]);
|
|
373
|
+
|
|
374
|
+
// Fetch related record(s) when editing (if we have a current value but no record data)
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
if (!hasRelationship || !props.relationship || !currentValue) return;
|
|
377
|
+
|
|
378
|
+
const valueIds = isMultiple ? (Array.isArray(currentValue) ? currentValue : []) : [currentValue as string];
|
|
379
|
+
|
|
380
|
+
const missingIds = valueIds.filter((id: string) => !relationshipRecords[id]);
|
|
381
|
+
|
|
382
|
+
if (missingIds.length > 0) {
|
|
383
|
+
const fetchRelatedRecords = async () => {
|
|
384
|
+
try {
|
|
385
|
+
const { resource, titleAttribute } = props.relationship!;
|
|
386
|
+
const resourceSlug = resource || props.relationship!.name;
|
|
387
|
+
const apiBaseUrl = (props as any).apiBaseUrl;
|
|
388
|
+
|
|
389
|
+
const fetchPromises = missingIds.map(async (id: string) => {
|
|
390
|
+
try {
|
|
391
|
+
const response = await authenticatedFetch(
|
|
392
|
+
`${apiBaseUrl}/${resourceSlug}/${id}`,
|
|
393
|
+
{
|
|
394
|
+
headers: {},
|
|
395
|
+
},
|
|
396
|
+
apiBaseUrl,
|
|
397
|
+
);
|
|
398
|
+
if (response.ok) {
|
|
399
|
+
const result = await response.json();
|
|
400
|
+
// Handle new response structure: { data, metadata }
|
|
401
|
+
const record = result.data || result;
|
|
402
|
+
return { id, record };
|
|
403
|
+
}
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`Error fetching record ${id}:`, error);
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const results = await Promise.all(fetchPromises);
|
|
411
|
+
|
|
412
|
+
const newOptions = { ...relationshipOptions };
|
|
413
|
+
const newRecords = { ...relationshipRecords };
|
|
414
|
+
const newFormattedLabels: Record<string, string> = {};
|
|
415
|
+
|
|
416
|
+
results.forEach(result => {
|
|
417
|
+
if (result) {
|
|
418
|
+
const { id, record } = result;
|
|
419
|
+
const title = record[titleAttribute] || id;
|
|
420
|
+
newOptions[id] = title;
|
|
421
|
+
newRecords[id] = record;
|
|
422
|
+
// Pre-format and cache the label for this individually fetched record
|
|
423
|
+
const formatted = formatOptionLabel(id, title, record);
|
|
424
|
+
newFormattedLabels[id] = formatted;
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
setRelationshipOptions(newOptions);
|
|
429
|
+
setRelationshipRecords(newRecords);
|
|
430
|
+
// Mark these records as individually fetched and cache their formatted labels
|
|
431
|
+
setIndividuallyFetchedIds(prev => {
|
|
432
|
+
const newSet = new Set(prev);
|
|
433
|
+
results.forEach(result => {
|
|
434
|
+
if (result) newSet.add(result.id);
|
|
435
|
+
});
|
|
436
|
+
return newSet;
|
|
437
|
+
});
|
|
438
|
+
setFormattedLabels(prev => ({ ...prev, ...newFormattedLabels }));
|
|
439
|
+
} catch (error) {
|
|
440
|
+
console.error('Error fetching related records:', error);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
fetchRelatedRecords();
|
|
445
|
+
}
|
|
446
|
+
}, [hasRelationship, currentValue, relationshipRecords, props.relationship, isMultiple]);
|
|
447
|
+
|
|
448
|
+
// Close dropdown when clicking outside
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
451
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
452
|
+
setIsOpen(false);
|
|
453
|
+
setSearchTerm('');
|
|
454
|
+
setInputValue('');
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
459
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
460
|
+
}, []);
|
|
461
|
+
|
|
462
|
+
// Use relationship options if available, otherwise use static options
|
|
463
|
+
const effectiveOptions = hasRelationship ? relationshipOptions : props.options || {};
|
|
464
|
+
|
|
465
|
+
// If native or not multiple and not creatable and not relationship, use native select
|
|
466
|
+
if (isNative || (!isMultiple && !isCreatable && !hasRelationship)) {
|
|
467
|
+
return (
|
|
468
|
+
<div className="mb-4">
|
|
469
|
+
{props.label && (
|
|
470
|
+
<label
|
|
471
|
+
htmlFor={props.name}
|
|
472
|
+
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
473
|
+
{props.label}
|
|
474
|
+
{validation.required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
|
|
475
|
+
</label>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
<select
|
|
479
|
+
id={props.name}
|
|
480
|
+
{...register(props.name, validation)}
|
|
481
|
+
disabled={props.disabled}
|
|
482
|
+
multiple={isMultiple}
|
|
483
|
+
className={cn(
|
|
484
|
+
'w-full px-3 py-2 border rounded-lg shadow-sm',
|
|
485
|
+
'k-input',
|
|
486
|
+
'focus:outline-none focus:ring-2 focus:ring-ring',
|
|
487
|
+
'transition duration-150 ease-in-out',
|
|
488
|
+
hasError && 'border-red-500 dark:border-red-400 focus:ring-red-500 dark:focus:ring-red-400',
|
|
489
|
+
props.disabled && 'opacity-60 cursor-not-allowed',
|
|
490
|
+
isMultiple && 'min-h-[120px]',
|
|
491
|
+
)}>
|
|
492
|
+
{props.placeholder && !isMultiple && props.selectablePlaceholder !== false && (
|
|
493
|
+
<option value="">{props.placeholder}</option>
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
{effectiveOptions &&
|
|
497
|
+
Object.entries(effectiveOptions).map(([value, label]) => (
|
|
498
|
+
<option key={value} value={value}>
|
|
499
|
+
{label}
|
|
500
|
+
</option>
|
|
501
|
+
))}
|
|
502
|
+
</select>
|
|
503
|
+
|
|
504
|
+
{hasError && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{error?.message as string}</p>}
|
|
505
|
+
|
|
506
|
+
{props.helperText && (
|
|
507
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{props.helperText}</p>
|
|
508
|
+
)}
|
|
509
|
+
|
|
510
|
+
{!hasError && <HintDisplay hint={props.hint} hintIcon={props.hintIcon} hintColor={props.hintColor} />}
|
|
511
|
+
|
|
512
|
+
{isMultiple && !hasError && (
|
|
513
|
+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
514
|
+
Hold Ctrl/Cmd to select multiple options
|
|
515
|
+
</p>
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Enhanced multi-select with chips
|
|
522
|
+
const selectedValues: string[] = isMultiple
|
|
523
|
+
? Array.isArray(currentValue)
|
|
524
|
+
? currentValue.filter((v): v is string => typeof v === 'string')
|
|
525
|
+
: []
|
|
526
|
+
: currentValue && typeof currentValue === 'string'
|
|
527
|
+
? [currentValue]
|
|
528
|
+
: [];
|
|
529
|
+
|
|
530
|
+
const optionEntries = Object.entries(effectiveOptions);
|
|
531
|
+
|
|
532
|
+
// Filter options based on search term
|
|
533
|
+
// For relationship-based selects, the API already filters the results, so skip client-side filtering
|
|
534
|
+
const filteredOptions = hasRelationship
|
|
535
|
+
? optionEntries
|
|
536
|
+
: optionEntries.filter(([value, label]) => {
|
|
537
|
+
const matchesSearch = !searchTerm || label.toLowerCase().includes(searchTerm.toLowerCase());
|
|
538
|
+
return matchesSearch;
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Check if we can create a new option
|
|
542
|
+
const canCreateNew =
|
|
543
|
+
isCreatable &&
|
|
544
|
+
searchTerm &&
|
|
545
|
+
!optionEntries.some(([value, label]) => label.toLowerCase() === searchTerm.toLowerCase()) &&
|
|
546
|
+
!selectedValues.includes(searchTerm);
|
|
547
|
+
|
|
548
|
+
// Check if we can create a new relationship record
|
|
549
|
+
const canCreateRelationship = hasRelationship && props.createOptionForm;
|
|
550
|
+
|
|
551
|
+
const toggleOption = (value: string) => {
|
|
552
|
+
// When selecting a new option, format and cache its label if we have the record
|
|
553
|
+
const record = relationshipRecords[value];
|
|
554
|
+
if (record && !formattedLabels[value]) {
|
|
555
|
+
const label = relationshipOptions[value] || value;
|
|
556
|
+
const formatted = formatOptionLabel(value, label, record);
|
|
557
|
+
setFormattedLabels(prev => ({ ...prev, [value]: formatted }));
|
|
558
|
+
// Mark as individually fetched so it stays formatted
|
|
559
|
+
setIndividuallyFetchedIds(prev => new Set(prev).add(value));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (isMultiple) {
|
|
563
|
+
const newValues = selectedValues.includes(value)
|
|
564
|
+
? selectedValues.filter((v: string) => v !== value)
|
|
565
|
+
: [...selectedValues, value];
|
|
566
|
+
setValue(props.name, newValues, { shouldValidate: true });
|
|
567
|
+
} else {
|
|
568
|
+
setValue(props.name, value, { shouldValidate: true });
|
|
569
|
+
setIsOpen(false);
|
|
570
|
+
setSearchTerm('');
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const removeValue = (value: string) => {
|
|
575
|
+
if (isMultiple) {
|
|
576
|
+
const newValues = selectedValues.filter((v: string) => v !== value);
|
|
577
|
+
setValue(props.name, newValues, { shouldValidate: true });
|
|
578
|
+
} else {
|
|
579
|
+
setValue(props.name, '', { shouldValidate: true });
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const createNewOption = () => {
|
|
584
|
+
if (!canCreateNew || !searchTerm) return;
|
|
585
|
+
|
|
586
|
+
const trimmedValue = searchTerm.trim();
|
|
587
|
+
if (isMultiple) {
|
|
588
|
+
const newValues = [...selectedValues, trimmedValue];
|
|
589
|
+
setValue(props.name, newValues, { shouldValidate: true });
|
|
590
|
+
} else {
|
|
591
|
+
setValue(props.name, trimmedValue, { shouldValidate: true });
|
|
592
|
+
setIsOpen(false);
|
|
593
|
+
}
|
|
594
|
+
setSearchTerm('');
|
|
595
|
+
setInputValue('');
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
const handleCreateRelationshipSuccess = (newRecord: any) => {
|
|
599
|
+
const id = newRecord._id || newRecord.id;
|
|
600
|
+
const title = newRecord[props.relationship!.titleAttribute] || id;
|
|
601
|
+
|
|
602
|
+
setRelationshipOptions(prev => ({ ...prev, [id]: title }));
|
|
603
|
+
setRelationshipRecords(prev => ({ ...prev, [id]: newRecord }));
|
|
604
|
+
|
|
605
|
+
// Format and cache the label for the newly created record
|
|
606
|
+
const formatted = formatOptionLabel(id, title, newRecord);
|
|
607
|
+
setFormattedLabels(prev => ({ ...prev, [id]: formatted }));
|
|
608
|
+
setIndividuallyFetchedIds(prev => new Set(prev).add(id));
|
|
609
|
+
|
|
610
|
+
if (isMultiple) {
|
|
611
|
+
const newValues = [...selectedValues, id];
|
|
612
|
+
setValue(props.name, newValues, { shouldValidate: true });
|
|
613
|
+
} else {
|
|
614
|
+
setValue(props.name, id, { shouldValidate: true });
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
setShowCreateModal(false);
|
|
618
|
+
setIsOpen(false);
|
|
619
|
+
setSearchTerm('');
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
|
623
|
+
if (e.key === 'Enter') {
|
|
624
|
+
e.preventDefault();
|
|
625
|
+
if (canCreateNew) {
|
|
626
|
+
createNewOption();
|
|
627
|
+
} else if (filteredOptions.length === 1) {
|
|
628
|
+
toggleOption(filteredOptions[0][0]);
|
|
629
|
+
setSearchTerm('');
|
|
630
|
+
}
|
|
631
|
+
} else if (e.key === 'Backspace' && !searchTerm && selectedValues.length > 0 && isMultiple) {
|
|
632
|
+
removeValue(selectedValues[selectedValues.length - 1]);
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
const getDisplayLabel = (value: string) => {
|
|
637
|
+
// If we have a cached formatted label for this individually fetched record, use it
|
|
638
|
+
if (formattedLabels[value]) {
|
|
639
|
+
return formattedLabels[value];
|
|
640
|
+
}
|
|
641
|
+
// Otherwise, return the plain label
|
|
642
|
+
return effectiveOptions[value] || value;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
return (
|
|
646
|
+
<div className="mb-4">
|
|
647
|
+
{props.label && (
|
|
648
|
+
<label htmlFor={props.name} className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
649
|
+
{props.label}
|
|
650
|
+
{validation.required && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
|
|
651
|
+
</label>
|
|
652
|
+
)}
|
|
653
|
+
|
|
654
|
+
{/* Hidden input for form registration */}
|
|
655
|
+
<input type="hidden" {...register(props.name, validation)} />
|
|
656
|
+
|
|
657
|
+
<div ref={dropdownRef} className="relative">
|
|
658
|
+
{/* Selected values display / Input */}
|
|
659
|
+
<div
|
|
660
|
+
onClick={() => !props.disabled && setIsOpen(!isOpen)}
|
|
661
|
+
className={cn(
|
|
662
|
+
'w-full min-h-[42px] px-3 py-2 border rounded-lg shadow-sm cursor-pointer',
|
|
663
|
+
'k-input',
|
|
664
|
+
'focus-within:outline-none focus-within:ring-2 focus-within:ring-ring ',
|
|
665
|
+
'transition duration-150 ease-in-out',
|
|
666
|
+
hasError &&
|
|
667
|
+
'border-red-500 dark:border-red-400 focus-within:ring-red-500 dark:focus-within:ring-red-400',
|
|
668
|
+
props.disabled && 'opacity-60 cursor-not-allowed bg-muted',
|
|
669
|
+
)}>
|
|
670
|
+
<div className="flex flex-wrap gap-2 items-center">
|
|
671
|
+
{/* Display selected values as chips */}
|
|
672
|
+
{selectedValues.map((value: string) => {
|
|
673
|
+
const displayLabel = getDisplayLabel(value);
|
|
674
|
+
const hasHTML = /<[^>]+>/.test(displayLabel);
|
|
675
|
+
|
|
676
|
+
return (
|
|
677
|
+
<span
|
|
678
|
+
key={value}
|
|
679
|
+
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm font-medium bg-accent-soft dark:bg-accent-soft text-accent">
|
|
680
|
+
{hasHTML ? (
|
|
681
|
+
<span dangerouslySetInnerHTML={{ __html: displayLabel }} />
|
|
682
|
+
) : (
|
|
683
|
+
<span>{displayLabel}</span>
|
|
684
|
+
)}
|
|
685
|
+
{!props.disabled && (
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
onClick={e => {
|
|
689
|
+
e.stopPropagation();
|
|
690
|
+
removeValue(value);
|
|
691
|
+
}}
|
|
692
|
+
className="hover:text-accent-hover transition-colors">
|
|
693
|
+
<Icon name="X" size={14} />
|
|
694
|
+
</button>
|
|
695
|
+
)}
|
|
696
|
+
</span>
|
|
697
|
+
);
|
|
698
|
+
})}
|
|
699
|
+
|
|
700
|
+
{/* Search/Input field */}
|
|
701
|
+
{!props.disabled && (
|
|
702
|
+
<input
|
|
703
|
+
type="text"
|
|
704
|
+
value={searchTerm}
|
|
705
|
+
onChange={e => {
|
|
706
|
+
setSearchTerm(e.target.value);
|
|
707
|
+
if (!isOpen) setIsOpen(true);
|
|
708
|
+
}}
|
|
709
|
+
onKeyDown={handleKeyDown}
|
|
710
|
+
onClick={e => {
|
|
711
|
+
e.stopPropagation();
|
|
712
|
+
setIsOpen(true);
|
|
713
|
+
}}
|
|
714
|
+
onFocus={() => setIsOpen(true)}
|
|
715
|
+
placeholder={
|
|
716
|
+
selectedValues.length === 0
|
|
717
|
+
? props.placeholder || translate('core:select.placeholder')
|
|
718
|
+
: ''
|
|
719
|
+
}
|
|
720
|
+
className="flex-1 min-w-[120px] outline-none bg-transparent text-fg"
|
|
721
|
+
disabled={props.disabled}
|
|
722
|
+
/>
|
|
723
|
+
)}
|
|
724
|
+
|
|
725
|
+
{/* Dropdown arrow */}
|
|
726
|
+
<Icon
|
|
727
|
+
name={isOpen ? 'ChevronUp' : 'ChevronDown'}
|
|
728
|
+
size={16}
|
|
729
|
+
className="text-gray-400 shrink-0"
|
|
730
|
+
/>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
{/* Dropdown menu */}
|
|
735
|
+
{isOpen && !props.disabled && (
|
|
736
|
+
<div className="absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-border rounded-lg shadow-lg max-h-60 overflow-auto">
|
|
737
|
+
{isLoadingRelationship && (
|
|
738
|
+
<div className="px-3 py-2 text-sm text-fg-secondary flex items-center gap-2">
|
|
739
|
+
<Icon name="Loader2" size={16} className="animate-spin" />
|
|
740
|
+
<span>{translate('core:select.loading')}</span>
|
|
741
|
+
</div>
|
|
742
|
+
)}
|
|
743
|
+
|
|
744
|
+
{!isLoadingRelationship &&
|
|
745
|
+
filteredOptions.length === 0 &&
|
|
746
|
+
!canCreateNew &&
|
|
747
|
+
!canCreateRelationship && (
|
|
748
|
+
<div className="px-3 py-2 text-sm text-fg-secondary">
|
|
749
|
+
{searchTerm
|
|
750
|
+
? translate('core:select.no_matching')
|
|
751
|
+
: translate('core:filters.no_options')}
|
|
752
|
+
</div>
|
|
753
|
+
)}
|
|
754
|
+
|
|
755
|
+
{!isLoadingRelationship &&
|
|
756
|
+
filteredOptions.map(([value, label]) => {
|
|
757
|
+
const record = relationshipRecords[value];
|
|
758
|
+
const displayLabel = formatOptionLabel(value, label, record);
|
|
759
|
+
const hasHTML = /<[^>]+>/.test(displayLabel);
|
|
760
|
+
|
|
761
|
+
return (
|
|
762
|
+
<div
|
|
763
|
+
key={value}
|
|
764
|
+
onClick={() => {
|
|
765
|
+
toggleOption(value);
|
|
766
|
+
setSearchTerm('');
|
|
767
|
+
}}
|
|
768
|
+
className={cn(
|
|
769
|
+
'px-3 py-2 cursor-pointer text-sm transition-colors text-fg',
|
|
770
|
+
'hover:bg-gray-100 dark:hover:bg-gray-700',
|
|
771
|
+
selectedValues.includes(value) && 'bg-accent-soft text-accent',
|
|
772
|
+
)}>
|
|
773
|
+
<div className="flex items-center justify-between">
|
|
774
|
+
{hasHTML ? (
|
|
775
|
+
<span dangerouslySetInnerHTML={{ __html: displayLabel }} />
|
|
776
|
+
) : (
|
|
777
|
+
<span>{displayLabel}</span>
|
|
778
|
+
)}
|
|
779
|
+
{selectedValues.includes(value) && <Icon name="Check" size={16} />}
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
})}
|
|
784
|
+
|
|
785
|
+
{/* Create new simple option */}
|
|
786
|
+
{canCreateNew && !canCreateRelationship && (
|
|
787
|
+
<div
|
|
788
|
+
onClick={createNewOption}
|
|
789
|
+
className={cn(
|
|
790
|
+
'px-3 py-2 cursor-pointer text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 text-accent font-medium',
|
|
791
|
+
filteredOptions.length > 0 && 'border-t border-border',
|
|
792
|
+
)}>
|
|
793
|
+
<div className="flex items-center gap-2">
|
|
794
|
+
<Icon name="Plus" size={16} />
|
|
795
|
+
<span>{translate('core:select.create', { term: searchTerm })}</span>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
)}
|
|
799
|
+
|
|
800
|
+
{/* Create new relationship record */}
|
|
801
|
+
{canCreateRelationship && (
|
|
802
|
+
<div
|
|
803
|
+
onClick={() => {
|
|
804
|
+
setShowCreateModal(true);
|
|
805
|
+
setIsOpen(false);
|
|
806
|
+
}}
|
|
807
|
+
className={cn(
|
|
808
|
+
'px-3 py-2 cursor-pointer text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 text-accent font-medium',
|
|
809
|
+
filteredOptions.length > 0 && 'border-t border-border',
|
|
810
|
+
)}>
|
|
811
|
+
<div className="flex items-center gap-2">
|
|
812
|
+
<Icon name="Plus" size={16} />
|
|
813
|
+
<span>{props.createOptionModalHeading || translate('core:select.create_new')}</span>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
)}
|
|
817
|
+
</div>
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
{hasError && <p className="mt-1 text-sm text-red-600 dark:text-red-400">{error?.message as string}</p>}
|
|
822
|
+
|
|
823
|
+
{props.helperText && <p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{props.helperText}</p>}
|
|
824
|
+
|
|
825
|
+
{!hasError && <HintDisplay hint={props.hint} hintIcon={props.hintIcon} hintColor={props.hintColor} />}
|
|
826
|
+
|
|
827
|
+
{/* Create relationship modal */}
|
|
828
|
+
{canCreateRelationship && showCreateModal && (
|
|
829
|
+
<ResourceFormModal
|
|
830
|
+
isOpen={showCreateModal}
|
|
831
|
+
onClose={() => setShowCreateModal(false)}
|
|
832
|
+
mode="create"
|
|
833
|
+
resourceName={props.createOptionModalHeading || translate('core:common.record')}
|
|
834
|
+
resourceSlug={props.relationship!.resource || props.relationship!.name}
|
|
835
|
+
apiBaseUrl={(props as any).apiBaseUrl}
|
|
836
|
+
formSchema={{ type: 'form', components: props.createOptionForm || [] } as any}
|
|
837
|
+
onSuccess={handleCreateRelationshipSuccess}
|
|
838
|
+
depth={1}
|
|
839
|
+
/>
|
|
840
|
+
)}
|
|
841
|
+
</div>
|
|
842
|
+
);
|
|
843
|
+
}
|