@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,841 @@
|
|
|
1
|
+
import React, { useCallback, useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import { useFormContext } from 'react-hook-form';
|
|
3
|
+
import { getFieldError } from '../utils/fieldErrors';
|
|
4
|
+
import { useDropzone } from 'react-dropzone';
|
|
5
|
+
import { Check, Upload, File, Trash2, Loader2 } from 'lucide-react';
|
|
6
|
+
import { FieldProps } from '../types';
|
|
7
|
+
import { HintDisplay } from './utils/HintDisplay';
|
|
8
|
+
import { ViewFieldWrapper } from './utils/ViewFieldWrapper';
|
|
9
|
+
import { MediaPreviewModal } from './utils/MediaPreviewModal';
|
|
10
|
+
import { authenticatedFetch } from '../api/authenticatedFetch';
|
|
11
|
+
import { useValidation } from '../hooks/useValidation';
|
|
12
|
+
import { translate } from '../i18n/activeLocale';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents a file in the upload field
|
|
16
|
+
*/
|
|
17
|
+
interface FileItem {
|
|
18
|
+
/** Unique identifier for this file item */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Storage key (from server after upload) */
|
|
21
|
+
key?: string;
|
|
22
|
+
/** Storage adapter name */
|
|
23
|
+
bucket?: string;
|
|
24
|
+
/** Display URL (server URL or local blob) */
|
|
25
|
+
url?: string;
|
|
26
|
+
/** Local file object (for new uploads) */
|
|
27
|
+
file?: File;
|
|
28
|
+
/** Local blob URL for preview */
|
|
29
|
+
blobUrl?: string;
|
|
30
|
+
/** Upload status */
|
|
31
|
+
status: 'pending' | 'uploading' | 'uploaded' | 'existing' | 'error';
|
|
32
|
+
/** Error message if status is 'error' */
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Server response for media upload
|
|
38
|
+
*/
|
|
39
|
+
interface UploadResponse {
|
|
40
|
+
data: {
|
|
41
|
+
url: string;
|
|
42
|
+
key: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate a unique ID for file tracking
|
|
48
|
+
*/
|
|
49
|
+
const generateId = () => `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Convert File to base64 string
|
|
53
|
+
*/
|
|
54
|
+
const fileToBase64 = (file: File): Promise<string> => {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const reader = new FileReader();
|
|
57
|
+
reader.onload = () => {
|
|
58
|
+
const result = reader.result as string;
|
|
59
|
+
resolve(result.split(',')[1]);
|
|
60
|
+
};
|
|
61
|
+
reader.onerror = reject;
|
|
62
|
+
reader.readAsDataURL(file);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Format bytes to human readable size
|
|
68
|
+
*/
|
|
69
|
+
const formatFileSize = (bytes: number): string => {
|
|
70
|
+
if (bytes === 0) return '0 Bytes';
|
|
71
|
+
const k = 1024;
|
|
72
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
73
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
74
|
+
return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if a URL points to an image
|
|
79
|
+
*/
|
|
80
|
+
const isImageUrl = (url?: string): boolean => {
|
|
81
|
+
if (!url) return false;
|
|
82
|
+
if (url.startsWith('blob:')) return true;
|
|
83
|
+
return /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(url);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if a URL points to a video
|
|
88
|
+
*/
|
|
89
|
+
const isVideoUrl = (url?: string): boolean => {
|
|
90
|
+
if (!url) return false;
|
|
91
|
+
return /\.(mp4|webm|ogg|mov|m4v)$/i.test(url);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a URL points to an audio file
|
|
96
|
+
*/
|
|
97
|
+
const isAudioUrl = (url?: string): boolean => {
|
|
98
|
+
if (!url) return false;
|
|
99
|
+
return /\.(mp3|wav|ogg|m4a)$/i.test(url);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
type ViewMediaType = 'image' | 'video' | 'audio';
|
|
103
|
+
|
|
104
|
+
function FileUploadViewField({ label, value }: { label?: string; value: any }) {
|
|
105
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
106
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
107
|
+
const [previewType, setPreviewType] = useState<ViewMediaType>('image');
|
|
108
|
+
|
|
109
|
+
const displayValue = value || null;
|
|
110
|
+
const filesArray = Array.isArray(displayValue) ? displayValue : displayValue ? [displayValue] : [];
|
|
111
|
+
|
|
112
|
+
if (filesArray.length === 0) {
|
|
113
|
+
return <ViewFieldWrapper label={label}>-</ViewFieldWrapper>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const openPreview = (url: string, type: ViewMediaType) => {
|
|
117
|
+
setPreviewUrl(url);
|
|
118
|
+
setPreviewType(type);
|
|
119
|
+
setPreviewOpen(true);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ViewFieldWrapper label={label}>
|
|
124
|
+
<div className="flex flex-wrap gap-4">
|
|
125
|
+
{filesArray.map((file: any, index: number) => {
|
|
126
|
+
const fileUrl = typeof file === 'object' && file !== null ? file.url || file.key : file;
|
|
127
|
+
if (!fileUrl) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const image = isImageUrl(fileUrl);
|
|
132
|
+
const video = isVideoUrl(fileUrl);
|
|
133
|
+
|
|
134
|
+
if (video) {
|
|
135
|
+
return (
|
|
136
|
+
<div key={index} className="relative">
|
|
137
|
+
<video
|
|
138
|
+
src={fileUrl}
|
|
139
|
+
controls
|
|
140
|
+
className="w-72 h-44 rounded-lg border border-border object-cover bg-black"
|
|
141
|
+
onClick={e => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
openPreview(fileUrl, 'video');
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (image) {
|
|
151
|
+
return (
|
|
152
|
+
<div key={index} className="relative">
|
|
153
|
+
<img
|
|
154
|
+
src={fileUrl}
|
|
155
|
+
alt={`${label || translate('core:common.image')} ${index + 1}`}
|
|
156
|
+
className="w-40 h-40 object-cover rounded-lg border border-border cursor-pointer hover:opacity-80 transition-opacity"
|
|
157
|
+
onClick={() => openPreview(fileUrl, 'image')}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div
|
|
165
|
+
key={index}
|
|
166
|
+
className="flex items-center gap-2 p-2 border border-border rounded-lg bg-surface">
|
|
167
|
+
<File size={20} className="text-fg-secondary" />
|
|
168
|
+
<span className="text-sm text-fg">
|
|
169
|
+
{typeof file === 'object' && file !== null
|
|
170
|
+
? file.key || translate('core:common.file')
|
|
171
|
+
: translate('core:common.file')}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{previewUrl && (
|
|
179
|
+
<MediaPreviewModal
|
|
180
|
+
isOpen={previewOpen}
|
|
181
|
+
onClose={() => setPreviewOpen(false)}
|
|
182
|
+
mediaUrl={previewUrl}
|
|
183
|
+
mediaType={previewType}
|
|
184
|
+
title={label}
|
|
185
|
+
autoplay={previewType === 'video'}
|
|
186
|
+
controls
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
</ViewFieldWrapper>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function FileUploadField({
|
|
194
|
+
name,
|
|
195
|
+
label,
|
|
196
|
+
helperText,
|
|
197
|
+
hint,
|
|
198
|
+
hintIcon,
|
|
199
|
+
hintColor,
|
|
200
|
+
acceptedFileTypes = [],
|
|
201
|
+
maxSize,
|
|
202
|
+
minSize,
|
|
203
|
+
multiple = false,
|
|
204
|
+
maxFiles,
|
|
205
|
+
disabled,
|
|
206
|
+
required,
|
|
207
|
+
visibility,
|
|
208
|
+
directory,
|
|
209
|
+
bucket,
|
|
210
|
+
apiBaseUrl,
|
|
211
|
+
resource,
|
|
212
|
+
mode,
|
|
213
|
+
value,
|
|
214
|
+
validation,
|
|
215
|
+
operation,
|
|
216
|
+
}: FieldProps & {
|
|
217
|
+
visibility?: 'public' | 'private';
|
|
218
|
+
directory?: string;
|
|
219
|
+
bucket?: string;
|
|
220
|
+
apiBaseUrl?: string;
|
|
221
|
+
resource?: string;
|
|
222
|
+
}) {
|
|
223
|
+
// View mode: render formatted display (with image/video preview)
|
|
224
|
+
if (mode === 'view') {
|
|
225
|
+
return <FileUploadViewField label={label} value={value} />;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const {
|
|
229
|
+
setValue,
|
|
230
|
+
watch,
|
|
231
|
+
register,
|
|
232
|
+
formState: { errors: formErrors },
|
|
233
|
+
} = useFormContext();
|
|
234
|
+
const [files, setFiles] = useState<FileItem[]>([]);
|
|
235
|
+
const [errors, setErrors] = useState<string[]>([]);
|
|
236
|
+
const initializedRef = useRef(false);
|
|
237
|
+
|
|
238
|
+
// Preview state for edit mode
|
|
239
|
+
const [editPreviewOpen, setEditPreviewOpen] = useState(false);
|
|
240
|
+
const [editPreviewUrl, setEditPreviewUrl] = useState<string | null>(null);
|
|
241
|
+
const [editPreviewType, setEditPreviewType] = useState<ViewMediaType>('image');
|
|
242
|
+
|
|
243
|
+
const currentValue = watch(name);
|
|
244
|
+
|
|
245
|
+
// Evaluate validation conditions with form context
|
|
246
|
+
const validationResult = useValidation(validation?.rules || [], operation, name);
|
|
247
|
+
const isRequired = validationResult.required !== undefined;
|
|
248
|
+
const fieldError = getFieldError(formErrors, name);
|
|
249
|
+
|
|
250
|
+
// Register field with React Hook Form for validation
|
|
251
|
+
React.useEffect(() => {
|
|
252
|
+
register(name, validationResult);
|
|
253
|
+
}, [register, name, validationResult]);
|
|
254
|
+
|
|
255
|
+
// Compute endpoints
|
|
256
|
+
// Use generic media endpoint if no resource is provided, otherwise use resource-specific endpoint
|
|
257
|
+
const uploadEndpoint = useMemo(() => {
|
|
258
|
+
if (!apiBaseUrl) return null;
|
|
259
|
+
if (resource) {
|
|
260
|
+
return `${apiBaseUrl}/${resource}/media/upload`;
|
|
261
|
+
}
|
|
262
|
+
return `${apiBaseUrl}/media/upload`;
|
|
263
|
+
}, [apiBaseUrl, resource]);
|
|
264
|
+
|
|
265
|
+
const deleteEndpoint = useMemo(() => {
|
|
266
|
+
if (!apiBaseUrl) return null;
|
|
267
|
+
if (resource) {
|
|
268
|
+
return `${apiBaseUrl}/${resource}/media/delete`;
|
|
269
|
+
}
|
|
270
|
+
return `${apiBaseUrl}/media/delete`;
|
|
271
|
+
}, [apiBaseUrl, resource]);
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Initialize from existing value (server sends { url, key } objects)
|
|
275
|
+
* Only runs once on mount if there's an initial value
|
|
276
|
+
*/
|
|
277
|
+
useEffect(() => {
|
|
278
|
+
if (initializedRef.current) return;
|
|
279
|
+
if (!currentValue) return;
|
|
280
|
+
|
|
281
|
+
initializedRef.current = true;
|
|
282
|
+
const existingFiles: FileItem[] = [];
|
|
283
|
+
const keys: string[] = [];
|
|
284
|
+
|
|
285
|
+
const processItem = (item: any): FileItem | null => {
|
|
286
|
+
if (!item) return null;
|
|
287
|
+
|
|
288
|
+
// New format: { key, bucket, url }
|
|
289
|
+
if (typeof item === 'object' && item.key) {
|
|
290
|
+
keys.push(item.key);
|
|
291
|
+
return {
|
|
292
|
+
id: generateId(),
|
|
293
|
+
key: item.key,
|
|
294
|
+
bucket: item.bucket,
|
|
295
|
+
url: item.url || item.key, // Use url if available, fallback to key
|
|
296
|
+
status: 'existing',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// String form: treat the value as a storage key
|
|
301
|
+
if (typeof item === 'string') {
|
|
302
|
+
keys.push(item);
|
|
303
|
+
return {
|
|
304
|
+
id: generateId(),
|
|
305
|
+
key: item,
|
|
306
|
+
url: item,
|
|
307
|
+
status: 'existing',
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (Array.isArray(currentValue)) {
|
|
315
|
+
currentValue.forEach(item => {
|
|
316
|
+
const fileItem = processItem(item);
|
|
317
|
+
if (fileItem) existingFiles.push(fileItem);
|
|
318
|
+
});
|
|
319
|
+
} else {
|
|
320
|
+
const fileItem = processItem(currentValue);
|
|
321
|
+
if (fileItem) existingFiles.push(fileItem);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
setFiles(existingFiles);
|
|
325
|
+
// Set form value to just keys (don't validate on initial load)
|
|
326
|
+
setValue(name, multiple ? keys : keys[0] || null, { shouldValidate: false });
|
|
327
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
328
|
+
}, []); // Only run once on mount
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Upload a single file to the server
|
|
332
|
+
*/
|
|
333
|
+
const uploadFile = useCallback(
|
|
334
|
+
async (file: File): Promise<UploadResponse | null> => {
|
|
335
|
+
if (!uploadEndpoint) {
|
|
336
|
+
console.warn('No upload endpoint configured');
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const base64 = await fileToBase64(file);
|
|
342
|
+
const response = await authenticatedFetch(
|
|
343
|
+
uploadEndpoint,
|
|
344
|
+
{
|
|
345
|
+
method: 'POST',
|
|
346
|
+
headers: { 'Content-Type': 'application/json' },
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
file: base64,
|
|
349
|
+
filename: file.name,
|
|
350
|
+
contentType: file.type,
|
|
351
|
+
fieldName: name,
|
|
352
|
+
isArray: multiple,
|
|
353
|
+
path: directory,
|
|
354
|
+
visibility,
|
|
355
|
+
bucket, // Pass bucket adapter name
|
|
356
|
+
}),
|
|
357
|
+
},
|
|
358
|
+
apiBaseUrl,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const error = await response.json();
|
|
363
|
+
throw new Error(`Upload failed: ${error.message || response.statusText}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return await response.json();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error('Upload error:', error);
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[uploadEndpoint, name, multiple, directory, visibility],
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Delete a file from bucket
|
|
377
|
+
*/
|
|
378
|
+
const deleteFromStorage = useCallback(
|
|
379
|
+
async (payload: string | { key: string; bucket?: string }): Promise<boolean> => {
|
|
380
|
+
if (!deleteEndpoint) return true;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const body = typeof payload === 'string' ? { key: payload } : payload;
|
|
384
|
+
const response = await authenticatedFetch(
|
|
385
|
+
deleteEndpoint,
|
|
386
|
+
{
|
|
387
|
+
method: 'POST',
|
|
388
|
+
headers: { 'Content-Type': 'application/json' },
|
|
389
|
+
body: JSON.stringify(body),
|
|
390
|
+
},
|
|
391
|
+
apiBaseUrl,
|
|
392
|
+
);
|
|
393
|
+
return response.ok;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('Delete error:', error);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
[deleteEndpoint],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Update form value with current keys
|
|
404
|
+
* Backend will automatically detect deletions by comparing with existing record
|
|
405
|
+
*/
|
|
406
|
+
const updateFormValue = useCallback(
|
|
407
|
+
(fileList: FileItem[]) => {
|
|
408
|
+
// Get keys from uploaded/existing files
|
|
409
|
+
const keys = fileList
|
|
410
|
+
.filter(f => f.key && (f.status === 'uploaded' || f.status === 'existing'))
|
|
411
|
+
.map(f => f.key!);
|
|
412
|
+
|
|
413
|
+
// Set main field value - backend will detect deletions automatically
|
|
414
|
+
// Trigger validation when value changes
|
|
415
|
+
if (multiple) {
|
|
416
|
+
setValue(name, keys.length > 0 ? keys : [], { shouldValidate: true });
|
|
417
|
+
} else {
|
|
418
|
+
setValue(name, keys[0] || null, { shouldValidate: true });
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
[multiple, name, setValue],
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handle file drop/selection
|
|
426
|
+
*/
|
|
427
|
+
const onDrop = useCallback(
|
|
428
|
+
async (acceptedFiles: File[], rejectedFiles: any[]) => {
|
|
429
|
+
setErrors([]);
|
|
430
|
+
|
|
431
|
+
// Handle rejected files
|
|
432
|
+
const newErrors: string[] = [];
|
|
433
|
+
rejectedFiles.forEach(({ file, errors: fileErrors }) => {
|
|
434
|
+
fileErrors.forEach((error: any) => {
|
|
435
|
+
switch (error.code) {
|
|
436
|
+
case 'file-too-large':
|
|
437
|
+
newErrors.push(`${file.name}: File is too large`);
|
|
438
|
+
break;
|
|
439
|
+
case 'file-too-small':
|
|
440
|
+
newErrors.push(`${file.name}: File is too small`);
|
|
441
|
+
break;
|
|
442
|
+
case 'file-invalid-type':
|
|
443
|
+
newErrors.push(`${file.name}: Invalid file type`);
|
|
444
|
+
break;
|
|
445
|
+
case 'too-many-files':
|
|
446
|
+
newErrors.push(`Too many files. Maximum is ${maxFiles}`);
|
|
447
|
+
break;
|
|
448
|
+
default:
|
|
449
|
+
newErrors.push(`${file.name}: ${error.message}`);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (newErrors.length > 0) {
|
|
455
|
+
setErrors(newErrors);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (acceptedFiles.length === 0) return;
|
|
460
|
+
|
|
461
|
+
// Create file items with pending status
|
|
462
|
+
const newFileItems: FileItem[] = acceptedFiles.map(file => ({
|
|
463
|
+
id: generateId(),
|
|
464
|
+
file,
|
|
465
|
+
blobUrl: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,
|
|
466
|
+
status: 'pending' as const,
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
// Handle single file replacement
|
|
470
|
+
let updatedFiles: FileItem[];
|
|
471
|
+
|
|
472
|
+
if (!multiple) {
|
|
473
|
+
// Clean up existing file
|
|
474
|
+
const existingFile = files[0];
|
|
475
|
+
if (existingFile) {
|
|
476
|
+
// Revoke blob URL
|
|
477
|
+
if (existingFile.blobUrl) {
|
|
478
|
+
URL.revokeObjectURL(existingFile.blobUrl);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (existingFile.key && existingFile.status === 'uploaded') {
|
|
482
|
+
// Delete immediately (not saved yet) - backend will handle existing files
|
|
483
|
+
const deletePayload = existingFile.bucket
|
|
484
|
+
? { key: existingFile.key, bucket: existingFile.bucket }
|
|
485
|
+
: { key: existingFile.key };
|
|
486
|
+
deleteFromStorage(deletePayload);
|
|
487
|
+
}
|
|
488
|
+
// Note: For existing files, backend will detect deletion automatically
|
|
489
|
+
}
|
|
490
|
+
updatedFiles = newFileItems;
|
|
491
|
+
} else {
|
|
492
|
+
updatedFiles = [...files, ...newFileItems];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Update state with pending files
|
|
496
|
+
setFiles(updatedFiles);
|
|
497
|
+
|
|
498
|
+
// Upload files
|
|
499
|
+
if (uploadEndpoint) {
|
|
500
|
+
// Mark files as uploading
|
|
501
|
+
setFiles(prev =>
|
|
502
|
+
prev.map(f =>
|
|
503
|
+
newFileItems.find(nf => nf.id === f.id) ? { ...f, status: 'uploading' as const } : f,
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// Upload all new files and collect results
|
|
508
|
+
const uploadResults: { id: string; result: UploadResponse | null }[] = [];
|
|
509
|
+
for (const fileItem of newFileItems) {
|
|
510
|
+
const result = await uploadFile(fileItem.file!);
|
|
511
|
+
uploadResults.push({ id: fileItem.id, result });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Build the final file list
|
|
515
|
+
const finalFiles: FileItem[] = [];
|
|
516
|
+
|
|
517
|
+
// Start with existing/previously uploaded files (for multiple mode)
|
|
518
|
+
if (multiple) {
|
|
519
|
+
for (const f of updatedFiles) {
|
|
520
|
+
// Keep files that aren't part of this upload batch
|
|
521
|
+
if (!newFileItems.find(nf => nf.id === f.id)) {
|
|
522
|
+
finalFiles.push(f);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Add newly uploaded files with their results
|
|
528
|
+
for (const fileItem of newFileItems) {
|
|
529
|
+
const uploadResult = uploadResults.find(r => r.id === fileItem.id);
|
|
530
|
+
|
|
531
|
+
if (uploadResult?.result?.data?.url && uploadResult?.result?.data?.key) {
|
|
532
|
+
// Revoke blob URL since we have server URL now
|
|
533
|
+
if (fileItem.blobUrl) {
|
|
534
|
+
URL.revokeObjectURL(fileItem.blobUrl);
|
|
535
|
+
}
|
|
536
|
+
// Create new object explicitly to avoid any issues with spread
|
|
537
|
+
// Extract bucket from formatted data if available
|
|
538
|
+
const bucketName = bucket;
|
|
539
|
+
finalFiles.push({
|
|
540
|
+
id: fileItem.id,
|
|
541
|
+
file: fileItem.file,
|
|
542
|
+
key: uploadResult.result.data.key,
|
|
543
|
+
bucket: bucketName,
|
|
544
|
+
url: uploadResult.result.data.url,
|
|
545
|
+
status: 'uploaded' as const,
|
|
546
|
+
});
|
|
547
|
+
} else {
|
|
548
|
+
finalFiles.push({
|
|
549
|
+
id: fileItem.id,
|
|
550
|
+
file: fileItem.file,
|
|
551
|
+
blobUrl: fileItem.blobUrl,
|
|
552
|
+
status: 'error' as const,
|
|
553
|
+
error: 'Upload failed',
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Update state and form value separately (not inside setFiles callback)
|
|
559
|
+
setFiles(finalFiles);
|
|
560
|
+
updateFormValue(finalFiles);
|
|
561
|
+
} else {
|
|
562
|
+
// No upload endpoint - just update state
|
|
563
|
+
setFiles(updatedFiles);
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
[files, multiple, maxFiles, uploadEndpoint, uploadFile, deleteFromStorage, updateFormValue],
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Remove a file
|
|
571
|
+
* Backend will automatically detect deletions by comparing with existing record
|
|
572
|
+
*/
|
|
573
|
+
const removeFile = useCallback(
|
|
574
|
+
async (fileId: string) => {
|
|
575
|
+
const fileToRemove = files.find(f => f.id === fileId);
|
|
576
|
+
if (!fileToRemove) return;
|
|
577
|
+
|
|
578
|
+
// Revoke blob URL
|
|
579
|
+
if (fileToRemove.blobUrl) {
|
|
580
|
+
URL.revokeObjectURL(fileToRemove.blobUrl);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (fileToRemove.key && fileToRemove.status === 'uploaded') {
|
|
584
|
+
// Delete immediately (not saved to DB yet) - backend will handle existing files
|
|
585
|
+
const deletePayload = fileToRemove.bucket
|
|
586
|
+
? { key: fileToRemove.key, bucket: fileToRemove.bucket }
|
|
587
|
+
: { key: fileToRemove.key };
|
|
588
|
+
const deleted = await deleteFromStorage(deletePayload);
|
|
589
|
+
if (!deleted) {
|
|
590
|
+
setErrors(['Failed to delete file from bucket']);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// Note: For existing files, backend will detect deletion automatically
|
|
595
|
+
|
|
596
|
+
// Update files state
|
|
597
|
+
const updatedFiles = files.filter(f => f.id !== fileId);
|
|
598
|
+
setFiles(updatedFiles);
|
|
599
|
+
|
|
600
|
+
// Update form value - backend will detect deletions
|
|
601
|
+
updateFormValue(updatedFiles);
|
|
602
|
+
},
|
|
603
|
+
[files, deleteFromStorage, updateFormValue],
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// Calculate upload limits
|
|
607
|
+
const validFileCount = files.filter(f => f.status !== 'error').length;
|
|
608
|
+
const maxAllowed = multiple ? maxFiles || Infinity : 1;
|
|
609
|
+
const remainingSlots = Math.max(0, maxAllowed - validFileCount);
|
|
610
|
+
const isAtLimit = remainingSlots <= 0;
|
|
611
|
+
|
|
612
|
+
// Dropzone config
|
|
613
|
+
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
614
|
+
onDrop,
|
|
615
|
+
accept:
|
|
616
|
+
acceptedFileTypes.length > 0
|
|
617
|
+
? acceptedFileTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {})
|
|
618
|
+
: undefined,
|
|
619
|
+
maxSize: maxSize ? maxSize * 1024 : undefined,
|
|
620
|
+
minSize: minSize ? minSize * 1024 : undefined,
|
|
621
|
+
multiple,
|
|
622
|
+
maxFiles: remainingSlots,
|
|
623
|
+
disabled: disabled || isAtLimit,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Get display URL for a file
|
|
627
|
+
const getDisplayUrl = (file: FileItem): string | undefined => file.url || file.blobUrl;
|
|
628
|
+
|
|
629
|
+
// Get display name for a file
|
|
630
|
+
const getDisplayName = (file: FileItem): string => {
|
|
631
|
+
if (file.file?.name) return file.file.name;
|
|
632
|
+
if (file.url) return file.url.split('/').pop() || 'File';
|
|
633
|
+
if (file.key) return file.key.split('/').pop() || 'File';
|
|
634
|
+
return 'File';
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const openEditPreview = (url: string, type: ViewMediaType) => {
|
|
638
|
+
setEditPreviewUrl(url);
|
|
639
|
+
setEditPreviewType(type);
|
|
640
|
+
setEditPreviewOpen(true);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div className="space-y-2">
|
|
645
|
+
{/* Label */}
|
|
646
|
+
{label && (
|
|
647
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
648
|
+
{label}
|
|
649
|
+
{isRequired && <span className="text-red-500 dark:text-red-400 ml-1">*</span>}
|
|
650
|
+
</label>
|
|
651
|
+
)}
|
|
652
|
+
|
|
653
|
+
{/* Helper text */}
|
|
654
|
+
{helperText && <p className="text-sm text-gray-500 dark:text-gray-400">{helperText}</p>}
|
|
655
|
+
|
|
656
|
+
{/* Hint */}
|
|
657
|
+
<HintDisplay hint={hint} hintIcon={hintIcon} hintColor={hintColor} />
|
|
658
|
+
|
|
659
|
+
{/* Dropzone */}
|
|
660
|
+
{isAtLimit ? (
|
|
661
|
+
<div className="border-2 border-dashed rounded-lg p-6 text-center border-border bg-gray-50 dark:bg-gray-800/50">
|
|
662
|
+
<Check className="mx-auto h-12 w-12 text-green-500 dark:text-green-400" strokeWidth={1.5} />
|
|
663
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
664
|
+
{multiple ? (
|
|
665
|
+
<>
|
|
666
|
+
Maximum of <span className="font-semibold">{maxFiles}</span> files reached
|
|
667
|
+
</>
|
|
668
|
+
) : (
|
|
669
|
+
<>{translate('core:file.uploaded')}</>
|
|
670
|
+
)}
|
|
671
|
+
</p>
|
|
672
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
673
|
+
Remove {multiple ? 'a file' : 'the file'} to upload a new one
|
|
674
|
+
</p>
|
|
675
|
+
</div>
|
|
676
|
+
) : (
|
|
677
|
+
<div
|
|
678
|
+
{...getRootProps()}
|
|
679
|
+
className={`
|
|
680
|
+
border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors
|
|
681
|
+
${isDragActive ? 'border-accent bg-accent-soft dark:bg-accent-soft' : 'border-border hover:border-gray-400'}
|
|
682
|
+
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
683
|
+
`}>
|
|
684
|
+
<input {...getInputProps()} />
|
|
685
|
+
<Upload className="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" strokeWidth={1.5} />
|
|
686
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
687
|
+
{isDragActive ? (
|
|
688
|
+
<span className="font-semibold text-accent">{translate('core:file.drop_here')}</span>
|
|
689
|
+
) : (
|
|
690
|
+
<>
|
|
691
|
+
<span className="font-semibold text-accent">
|
|
692
|
+
{translate('core:file.click_to_upload')}
|
|
693
|
+
</span>{' '}
|
|
694
|
+
or drag and drop
|
|
695
|
+
</>
|
|
696
|
+
)}
|
|
697
|
+
</p>
|
|
698
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
699
|
+
{acceptedFileTypes.length > 0 ? acceptedFileTypes.join(', ') : 'Any file type'}
|
|
700
|
+
{maxSize && ` • Max ${maxSize}KB`}
|
|
701
|
+
{multiple && maxFiles && ` • ${validFileCount}/${maxFiles} files`}
|
|
702
|
+
</p>
|
|
703
|
+
</div>
|
|
704
|
+
)}
|
|
705
|
+
|
|
706
|
+
{/* Errors */}
|
|
707
|
+
{(errors.length > 0 || fieldError) && (
|
|
708
|
+
<div className="space-y-1">
|
|
709
|
+
{errors.map((error, index) => (
|
|
710
|
+
<p key={index} className="text-sm text-red-600 dark:text-red-400">
|
|
711
|
+
{error}
|
|
712
|
+
</p>
|
|
713
|
+
))}
|
|
714
|
+
{fieldError && (
|
|
715
|
+
<p className="text-sm text-red-600 dark:text-red-400">{fieldError.message as string}</p>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
)}
|
|
719
|
+
|
|
720
|
+
{/* File list */}
|
|
721
|
+
{files.length > 0 && (
|
|
722
|
+
<div className="space-y-2 mt-4">
|
|
723
|
+
{files.map(file => {
|
|
724
|
+
const displayUrl = getDisplayUrl(file);
|
|
725
|
+
const displayName = getDisplayName(file);
|
|
726
|
+
const isImage = (displayUrl && isImageUrl(displayUrl)) || file.file?.type?.startsWith('image/');
|
|
727
|
+
const isVideo = (displayUrl && isVideoUrl(displayUrl)) || file.file?.type?.startsWith('video/');
|
|
728
|
+
const isAudio = (displayUrl && isAudioUrl(displayUrl)) || file.file?.type?.startsWith('audio/');
|
|
729
|
+
const isLoading = file.status === 'pending' || file.status === 'uploading';
|
|
730
|
+
const hasError = file.status === 'error';
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<div
|
|
734
|
+
key={file.id}
|
|
735
|
+
className={`flex items-center justify-between p-3 bg-muted rounded-lg border border-border
|
|
736
|
+
${isLoading ? 'opacity-60' : ''}
|
|
737
|
+
${hasError ? 'border-red-500 dark:border-red-400' : ''}
|
|
738
|
+
`}>
|
|
739
|
+
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
740
|
+
{/* Thumbnail */}
|
|
741
|
+
{displayUrl && (isImage || isVideo || isAudio) ? (
|
|
742
|
+
<button
|
|
743
|
+
type="button"
|
|
744
|
+
onClick={() =>
|
|
745
|
+
openEditPreview(
|
|
746
|
+
displayUrl,
|
|
747
|
+
isVideo ? 'video' : isAudio ? 'audio' : 'image',
|
|
748
|
+
)
|
|
749
|
+
}
|
|
750
|
+
className="h-14 w-14 rounded overflow-hidden flex items-center justify-center bg-muted hover:opacity-80 transition-opacity">
|
|
751
|
+
{isImage ? (
|
|
752
|
+
<img
|
|
753
|
+
src={displayUrl}
|
|
754
|
+
alt={displayName}
|
|
755
|
+
className="h-full w-full object-cover"
|
|
756
|
+
/>
|
|
757
|
+
) : isVideo ? (
|
|
758
|
+
<div className="h-full w-full flex items-center justify-center bg-black text-white text-xs">
|
|
759
|
+
<span className="px-1">{translate('core:file.video')}</span>
|
|
760
|
+
</div>
|
|
761
|
+
) : (
|
|
762
|
+
<div className="h-full w-full flex items-center justify-center bg-linear-to-r from-blue-600 to-purple-600 text-white text-xs">
|
|
763
|
+
<span className="px-1">{translate('core:file.audio')}</span>
|
|
764
|
+
</div>
|
|
765
|
+
)}
|
|
766
|
+
</button>
|
|
767
|
+
) : (
|
|
768
|
+
<div className="h-10 w-10 bg-muted rounded flex items-center justify-center">
|
|
769
|
+
<File className="h-6 w-6 text-gray-400 dark:text-gray-500" />
|
|
770
|
+
</div>
|
|
771
|
+
)}
|
|
772
|
+
|
|
773
|
+
{/* File info */}
|
|
774
|
+
<div className="flex-1 min-w-0">
|
|
775
|
+
<p className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
|
|
776
|
+
{displayName}
|
|
777
|
+
</p>
|
|
778
|
+
{file.status === 'pending' && (
|
|
779
|
+
<p className="text-xs text-gray-500 flex items-center gap-1">
|
|
780
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
781
|
+
Waiting...
|
|
782
|
+
</p>
|
|
783
|
+
)}
|
|
784
|
+
{file.status === 'uploading' && (
|
|
785
|
+
<p className="text-xs text-accent flex items-center gap-1">
|
|
786
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
787
|
+
Uploading...
|
|
788
|
+
</p>
|
|
789
|
+
)}
|
|
790
|
+
{file.status === 'error' && (
|
|
791
|
+
<p className="text-xs text-red-500 dark:text-red-400">
|
|
792
|
+
{file.error || translate('core:common.error')}
|
|
793
|
+
</p>
|
|
794
|
+
)}
|
|
795
|
+
{file.status === 'uploaded' && (
|
|
796
|
+
<p className="text-xs text-green-500 dark:text-green-400">
|
|
797
|
+
{translate('core:file.saved')}
|
|
798
|
+
</p>
|
|
799
|
+
)}
|
|
800
|
+
{file.status === 'existing' && (
|
|
801
|
+
<p className="text-xs text-green-500 dark:text-green-400">
|
|
802
|
+
{translate('core:file.saved')}
|
|
803
|
+
</p>
|
|
804
|
+
)}
|
|
805
|
+
{file.file && file.status !== 'error' && (
|
|
806
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
807
|
+
{formatFileSize(file.file.size)}
|
|
808
|
+
</p>
|
|
809
|
+
)}
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
|
|
813
|
+
{/* Remove button */}
|
|
814
|
+
<button
|
|
815
|
+
type="button"
|
|
816
|
+
onClick={() => removeFile(file.id)}
|
|
817
|
+
className="ml-3 text-red-500 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 focus:outline-none"
|
|
818
|
+
disabled={disabled || isLoading}>
|
|
819
|
+
<Trash2 className="h-5 w-5" />
|
|
820
|
+
</button>
|
|
821
|
+
</div>
|
|
822
|
+
);
|
|
823
|
+
})}
|
|
824
|
+
</div>
|
|
825
|
+
)}
|
|
826
|
+
|
|
827
|
+
{/* Edit mode media preview */}
|
|
828
|
+
{editPreviewUrl && (
|
|
829
|
+
<MediaPreviewModal
|
|
830
|
+
isOpen={editPreviewOpen}
|
|
831
|
+
onClose={() => setEditPreviewOpen(false)}
|
|
832
|
+
mediaUrl={editPreviewUrl}
|
|
833
|
+
mediaType={editPreviewType}
|
|
834
|
+
title={typeof label === 'string' ? label : undefined}
|
|
835
|
+
autoplay={editPreviewType === 'video' || editPreviewType === 'audio'}
|
|
836
|
+
controls
|
|
837
|
+
/>
|
|
838
|
+
)}
|
|
839
|
+
</div>
|
|
840
|
+
);
|
|
841
|
+
}
|