@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,70 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useWatch, useFormContext } from 'react-hook-form';
|
|
3
|
+
import { executeSerializedFunction } from '../runtime/serializedFunctions';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to execute afterStateUpdated callback when a field's value changes
|
|
7
|
+
* @param fieldName - Name of the field to watch
|
|
8
|
+
* @param callbackString - Serialized callback function as string
|
|
9
|
+
*/
|
|
10
|
+
export function useAfterStateUpdated(fieldName: string, callbackString?: string) {
|
|
11
|
+
const { control, getValues, setValue } = useFormContext();
|
|
12
|
+
const currentValue = useWatch({ control, name: fieldName });
|
|
13
|
+
const previousValueRef = useRef<any>(currentValue);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
// Skip if no callback is provided
|
|
17
|
+
if (!callbackString) return;
|
|
18
|
+
|
|
19
|
+
// Skip on initial mount (when previousValue is undefined)
|
|
20
|
+
if (previousValueRef.current === undefined && currentValue === undefined) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if value actually changed
|
|
25
|
+
const hasChanged = previousValueRef.current !== currentValue;
|
|
26
|
+
|
|
27
|
+
if (hasChanged) {
|
|
28
|
+
try {
|
|
29
|
+
// Create get function to access form values
|
|
30
|
+
const get = (field: string) => {
|
|
31
|
+
// Support dot notation for nested fields
|
|
32
|
+
const keys = field.split('.');
|
|
33
|
+
let value = getValues();
|
|
34
|
+
|
|
35
|
+
for (const key of keys) {
|
|
36
|
+
if (value === undefined || value === null) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
value = value[key];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Create set function to update form values
|
|
46
|
+
const set = (field: string, value: any) => {
|
|
47
|
+
setValue(field, value, {
|
|
48
|
+
shouldValidate: true,
|
|
49
|
+
shouldDirty: true,
|
|
50
|
+
shouldTouch: true,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Evaluate the callback function using the shared utility
|
|
55
|
+
// The function should be in the format: (get, set, state, old) => { ... }
|
|
56
|
+
// Reconstruct the function first, then call it with the parameters
|
|
57
|
+
const callback = executeSerializedFunction(callbackString);
|
|
58
|
+
if (typeof callback === 'function') {
|
|
59
|
+
// Call it with the required parameters
|
|
60
|
+
callback(get, set, currentValue, previousValueRef.current);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Error executing afterStateUpdated callback:', error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update previous value for next comparison
|
|
68
|
+
previousValueRef.current = currentValue;
|
|
69
|
+
}, [currentValue, callbackString, getValues, setValue]);
|
|
70
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { renderHook } from '@testing-library/react';
|
|
4
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
5
|
+
import { useValidation } from './useValidation';
|
|
6
|
+
|
|
7
|
+
// The hook no longer re-implements rule semantics: it delegates to the shared
|
|
8
|
+
// ValidationEngine via a single RHF `validate` function. These tests confirm
|
|
9
|
+
// the wiring (required → native RHF, everything else → engine) and that the
|
|
10
|
+
// verdicts match the engine the backend also uses.
|
|
11
|
+
|
|
12
|
+
function wrapper({ children }: { children: React.ReactNode }) {
|
|
13
|
+
function Inner() {
|
|
14
|
+
const methods = useForm();
|
|
15
|
+
return React.createElement(FormProvider, methods as any, children);
|
|
16
|
+
}
|
|
17
|
+
return React.createElement(Inner);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runValidate(result: ReturnType<typeof useValidation>, value: any, formValues: Record<string, any> = {}) {
|
|
21
|
+
return result.validate?.kratos(value, formValues);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('useValidation', () => {
|
|
25
|
+
it('maps required to RHF native (drives the asterisk)', () => {
|
|
26
|
+
const { result } = renderHook(() => useValidation(['required'], 'create', 'name'), { wrapper });
|
|
27
|
+
expect(result.current.required).toBe('This field is required');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('delegates email validation to the engine', () => {
|
|
31
|
+
const { result } = renderHook(() => useValidation(['email'], 'create', 'email'), { wrapper });
|
|
32
|
+
expect(runValidate(result.current, 'a@b.com')).toBe(true);
|
|
33
|
+
expect(runValidate(result.current, 'nope')).toMatch(/valid email/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('delegates min length to the engine', () => {
|
|
37
|
+
const { result } = renderHook(() => useValidation(['min:3'], 'create', 'title'), { wrapper });
|
|
38
|
+
expect(runValidate(result.current, 'abc')).toBe(true);
|
|
39
|
+
expect(runValidate(result.current, 'ab')).toMatch(/at least 3 characters/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('enforces alpha (parity with the backend)', () => {
|
|
43
|
+
const { result } = renderHook(() => useValidation(['alpha'], 'create', 'code'), { wrapper });
|
|
44
|
+
expect(runValidate(result.current, 'abc')).toBe(true);
|
|
45
|
+
expect(runValidate(result.current, 'ab1')).toMatch(/letters/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('resolves cross-field rules against form values', () => {
|
|
49
|
+
const { result } = renderHook(() => useValidation(['same:password'], 'create', 'confirm'), { wrapper });
|
|
50
|
+
expect(runValidate(result.current, 'x', { password: 'x' })).toBe(true);
|
|
51
|
+
expect(runValidate(result.current, 'x', { password: 'y' })).toMatch(/must match password/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('skips rules whose condition is false', () => {
|
|
55
|
+
const rules = [{ rule: 'required', condition: false }];
|
|
56
|
+
const { result } = renderHook(() => useValidation(rules, 'create', 'name'), { wrapper });
|
|
57
|
+
expect(result.current.required).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useWatch, useFormContext } from 'react-hook-form';
|
|
3
|
+
import { RHFValidationRules } from '../types';
|
|
4
|
+
import type { ValidationRule } from '@maxal_studio/kratosjs';
|
|
5
|
+
// Runtime engine is imported from the pure /dist/validation subpath (no server
|
|
6
|
+
// code), so it never drags Panel/MikroORM/Express into the browser bundle.
|
|
7
|
+
import { ValidationEngine } from '@maxal_studio/kratosjs/dist/validation';
|
|
8
|
+
import { evaluateCondition } from '../runtime/conditions';
|
|
9
|
+
import { useI18nContext } from '../i18n/I18nProvider';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validation rule with optional condition
|
|
13
|
+
*/
|
|
14
|
+
interface ValidationRuleWithCondition {
|
|
15
|
+
rule: ValidationRule;
|
|
16
|
+
condition?: boolean | string; // Serialized function string or boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook to convert KratosJs validation rules to React Hook Form format.
|
|
21
|
+
*
|
|
22
|
+
* Rule semantics are NOT re-implemented here: after resolving each rule's
|
|
23
|
+
* condition against the live form state, the surviving rules are handed to the
|
|
24
|
+
* shared `ValidationEngine` — the SAME engine the backend runs — via a single
|
|
25
|
+
* RHF `validate` function. This guarantees the client and server agree on every
|
|
26
|
+
* rule and message. `required` is additionally mapped to RHF's native
|
|
27
|
+
* `required` so the required-asterisk UI keeps working.
|
|
28
|
+
*
|
|
29
|
+
* @param rules Validation rules (strings, or objects with rule + condition)
|
|
30
|
+
* @param operation Current operation ('create' | 'edit' | 'view')
|
|
31
|
+
* @param fieldName Name of the field being validated (for cross-field rules and messages)
|
|
32
|
+
* @returns React Hook Form validation object
|
|
33
|
+
*/
|
|
34
|
+
export function useValidation(
|
|
35
|
+
rules: (ValidationRule | ValidationRuleWithCondition)[] = [],
|
|
36
|
+
operation?: 'create' | 'edit' | 'view',
|
|
37
|
+
fieldName = '',
|
|
38
|
+
): RHFValidationRules {
|
|
39
|
+
const { control } = useFormContext();
|
|
40
|
+
const formState = useWatch({ control }) || {};
|
|
41
|
+
const { t } = useI18nContext();
|
|
42
|
+
|
|
43
|
+
return useMemo(() => {
|
|
44
|
+
const validation: RHFValidationRules = {};
|
|
45
|
+
|
|
46
|
+
// Resolve conditions against the live form state, keeping only the
|
|
47
|
+
// rule strings that currently apply.
|
|
48
|
+
const activeRules: string[] = [];
|
|
49
|
+
rules.forEach(ruleOrObj => {
|
|
50
|
+
let rule: ValidationRule;
|
|
51
|
+
let condition: boolean | string | undefined;
|
|
52
|
+
|
|
53
|
+
if (typeof ruleOrObj === 'object' && ruleOrObj !== null && 'rule' in ruleOrObj) {
|
|
54
|
+
rule = ruleOrObj.rule;
|
|
55
|
+
condition = ruleOrObj.condition;
|
|
56
|
+
} else {
|
|
57
|
+
rule = ruleOrObj as ValidationRule;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (condition !== undefined) {
|
|
61
|
+
const shouldApply = evaluateCondition(condition, formState, operation);
|
|
62
|
+
if (!shouldApply) return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof rule === 'string') activeRules.push(rule);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// `required` → RHF native (drives the asterisk + empty-value enforcement).
|
|
69
|
+
if (activeRules.includes('required')) {
|
|
70
|
+
validation.required = t('core:validation.required_generic');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Everything else is delegated to the shared engine. `required` is handled
|
|
74
|
+
// natively above, so it's excluded here to avoid a duplicate message.
|
|
75
|
+
const engineRules = activeRules.filter(r => r !== 'required');
|
|
76
|
+
if (engineRules.length > 0) {
|
|
77
|
+
validation.validate = {
|
|
78
|
+
...((validation.validate as any) || {}),
|
|
79
|
+
kratos: (value: any, formValues: any) => {
|
|
80
|
+
const violations = ValidationEngine.validateValue(value, engineRules, {
|
|
81
|
+
allValues: formValues || {},
|
|
82
|
+
field: fieldName,
|
|
83
|
+
});
|
|
84
|
+
if (violations.length === 0) return true;
|
|
85
|
+
const v = violations[0];
|
|
86
|
+
// Render in the active locale when the rule provides an i18n key;
|
|
87
|
+
// otherwise use the already-rendered message (overrides/inline).
|
|
88
|
+
return v.messageKey ? t(`core:${v.messageKey}`, v.params) : v.message;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return validation;
|
|
94
|
+
}, [rules, formState, operation, fieldName, t]);
|
|
95
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import type { KratosI18n } from '@maxal_studio/kratosjs/dist/i18n';
|
|
3
|
+
import { buildClientI18n, type ClientI18nConfig } from './buildClientI18n';
|
|
4
|
+
import { setActiveI18n } from './activeLocale';
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = 'kratosjs-locale';
|
|
7
|
+
|
|
8
|
+
export interface I18nContextValue {
|
|
9
|
+
/** The underlying engine (escape hatch). */
|
|
10
|
+
engine: KratosI18n;
|
|
11
|
+
/** Active UI locale. */
|
|
12
|
+
locale: string;
|
|
13
|
+
/** Switch the active locale (persists + updates `<html lang/dir>` + re-fetches). */
|
|
14
|
+
setLocale: (locale: string) => void;
|
|
15
|
+
/** Supported locales. */
|
|
16
|
+
locales: string[];
|
|
17
|
+
/** Text direction of the active locale. */
|
|
18
|
+
dir: 'ltr' | 'rtl';
|
|
19
|
+
/** Translate a (possibly `ns:`-prefixed) key against the active locale. */
|
|
20
|
+
t: (key: string, params?: Record<string, unknown>) => string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const I18nContext = createContext<I18nContextValue | null>(null);
|
|
24
|
+
|
|
25
|
+
// Lazy default engine so components used outside a provider (isolated tests, the
|
|
26
|
+
// pre-mount window) still resolve `core` chrome instead of crashing.
|
|
27
|
+
let defaultEngine: KratosI18n | null = null;
|
|
28
|
+
function getDefaultEngine(): KratosI18n {
|
|
29
|
+
if (!defaultEngine) defaultEngine = buildClientI18n();
|
|
30
|
+
return defaultEngine;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveInitialLocale(engine: KratosI18n, configDefault?: string): string {
|
|
34
|
+
const locales = engine.getLocales();
|
|
35
|
+
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
|
|
36
|
+
if (stored && locales.includes(stored)) return stored;
|
|
37
|
+
if (configDefault && locales.includes(configDefault)) return configDefault;
|
|
38
|
+
const nav = typeof navigator !== 'undefined' ? navigator.language : '';
|
|
39
|
+
if (nav) {
|
|
40
|
+
if (locales.includes(nav)) return nav;
|
|
41
|
+
const base = nav.split('-')[0];
|
|
42
|
+
if (locales.includes(base)) return base;
|
|
43
|
+
}
|
|
44
|
+
return engine.getDefaultLocale();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface I18nProviderProps {
|
|
48
|
+
config?: ClientI18nConfig;
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function I18nProvider({ config, children }: I18nProviderProps) {
|
|
53
|
+
const engine = useMemo(() => buildClientI18n(config), [config]);
|
|
54
|
+
const [locale, setLocaleState] = useState(() => resolveInitialLocale(engine, config?.defaultLocale));
|
|
55
|
+
|
|
56
|
+
// Set the module mirror SYNCHRONOUSLY during render (not only in the effect) so
|
|
57
|
+
// non-hook `translate()` calls in children resolve the correct locale on the
|
|
58
|
+
// very first paint — important for a persisted non-default locale.
|
|
59
|
+
useMemo(() => setActiveI18n(engine, locale), [engine, locale]);
|
|
60
|
+
|
|
61
|
+
// Keep the <html> attributes + storage in sync with the locale.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setActiveI18n(engine, locale);
|
|
64
|
+
if (typeof document !== 'undefined') {
|
|
65
|
+
document.documentElement.lang = locale;
|
|
66
|
+
document.documentElement.dir = engine.getDir(locale);
|
|
67
|
+
}
|
|
68
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, locale);
|
|
69
|
+
}, [engine, locale]);
|
|
70
|
+
|
|
71
|
+
const setLocale = useCallback(
|
|
72
|
+
(next: string) => {
|
|
73
|
+
if (next === locale) return;
|
|
74
|
+
// Persist + apply the document attributes immediately, then do a full
|
|
75
|
+
// refresh so EVERY view — metadata, already-open table/form schemas, and
|
|
76
|
+
// server-rendered labels — re-fetches and re-renders in the new locale.
|
|
77
|
+
// (Schemas are cached per-mount, so a targeted re-fetch would miss them.)
|
|
78
|
+
if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, next);
|
|
79
|
+
if (typeof document !== 'undefined') {
|
|
80
|
+
document.documentElement.lang = next;
|
|
81
|
+
document.documentElement.dir = engine.getDir(next);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
if (typeof window !== 'undefined' && typeof window.location?.reload === 'function') {
|
|
85
|
+
window.location.reload();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// jsdom/SSR: reload isn't implemented — fall through to in-place update.
|
|
90
|
+
}
|
|
91
|
+
setLocaleState(next);
|
|
92
|
+
},
|
|
93
|
+
[locale, engine],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const value = useMemo<I18nContextValue>(
|
|
97
|
+
() => ({
|
|
98
|
+
engine,
|
|
99
|
+
locale,
|
|
100
|
+
setLocale,
|
|
101
|
+
locales: engine.getLocales(),
|
|
102
|
+
dir: engine.getDir(locale),
|
|
103
|
+
t: (key, params) => engine.t(key, { locale, ...params }),
|
|
104
|
+
}),
|
|
105
|
+
[engine, locale, setLocale],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Read the i18n context. Degrades gracefully to a default `core`-only engine when
|
|
113
|
+
* no `<I18nProvider>` is mounted (so isolated component tests don't all need wrapping).
|
|
114
|
+
*/
|
|
115
|
+
export function useI18nContext(): I18nContextValue {
|
|
116
|
+
const ctx = useContext(I18nContext);
|
|
117
|
+
if (ctx) return ctx;
|
|
118
|
+
const engine = getDefaultEngine();
|
|
119
|
+
const locale = engine.getDefaultLocale();
|
|
120
|
+
return {
|
|
121
|
+
engine,
|
|
122
|
+
locale,
|
|
123
|
+
setLocale: () => {},
|
|
124
|
+
locales: engine.getLocales(),
|
|
125
|
+
dir: engine.getDir(locale),
|
|
126
|
+
t: (key, params) => engine.t(key, { locale, ...params }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useI18nContext } from './I18nProvider';
|
|
2
|
+
|
|
3
|
+
/** Human label for a locale code, in the locale's own language (falls back to the code). */
|
|
4
|
+
function localeLabel(code: string): string {
|
|
5
|
+
try {
|
|
6
|
+
const dn = new Intl.DisplayNames([code], { type: 'language' });
|
|
7
|
+
const name = dn.of(code);
|
|
8
|
+
if (name) return name.charAt(0).toUpperCase() + name.slice(1);
|
|
9
|
+
} catch {
|
|
10
|
+
// Intl.DisplayNames unavailable — fall through.
|
|
11
|
+
}
|
|
12
|
+
return code;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LocaleSwitcherProps {
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Accessible label (defaults to the translated `core:panel.language`). */
|
|
18
|
+
'aria-label'?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Native `<select>` locale switcher. Renders nothing when the panel has a single
|
|
23
|
+
* locale. Used in the Header account menu and on the login screen.
|
|
24
|
+
*/
|
|
25
|
+
export function LocaleSwitcher({ className, ...rest }: LocaleSwitcherProps) {
|
|
26
|
+
const { locale, setLocale, locales, t } = useI18nContext();
|
|
27
|
+
if (locales.length <= 1) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<select
|
|
31
|
+
value={locale}
|
|
32
|
+
onChange={e => setLocale(e.target.value)}
|
|
33
|
+
aria-label={rest['aria-label'] ?? t('core:panel.language')}
|
|
34
|
+
className={
|
|
35
|
+
className ??
|
|
36
|
+
'k-input text-sm py-1.5 px-3 pr-8 rounded-md border border-border focus:outline-none focus:ring-2 focus:ring-ring focus:border-accent disabled:opacity-50 disabled:cursor-not-allowed transition-colors appearance-none bg-no-repeat bg-right'
|
|
37
|
+
}
|
|
38
|
+
style={{
|
|
39
|
+
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E")`,
|
|
40
|
+
backgroundPosition: 'right 0.5rem center',
|
|
41
|
+
backgroundSize: '1.5em 1.5em',
|
|
42
|
+
}}>
|
|
43
|
+
{locales.map(code => (
|
|
44
|
+
<option key={code} value={code}>
|
|
45
|
+
{localeLabel(code)}
|
|
46
|
+
</option>
|
|
47
|
+
))}
|
|
48
|
+
</select>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Module-level mirror of the active client locale + engine.
|
|
2
|
+
//
|
|
3
|
+
// Lets non-hook utilities (formatValue, tableFormatters) localize via `translate`
|
|
4
|
+
// / `getActiveLocale` without threading a locale argument through every call.
|
|
5
|
+
// The I18nProvider keeps this in sync with React state on every locale change.
|
|
6
|
+
|
|
7
|
+
import type { KratosI18n } from '@maxal_studio/kratosjs/dist/i18n';
|
|
8
|
+
import { buildClientI18n } from './buildClientI18n';
|
|
9
|
+
|
|
10
|
+
let activeEngine: KratosI18n | null = null;
|
|
11
|
+
let activeLocale = 'en';
|
|
12
|
+
let defaultEngine: KratosI18n | null = null;
|
|
13
|
+
|
|
14
|
+
/** The active engine, or a lazily-built `core`-only default (no provider mounted). */
|
|
15
|
+
function engine(): KratosI18n {
|
|
16
|
+
if (activeEngine) return activeEngine;
|
|
17
|
+
if (!defaultEngine) defaultEngine = buildClientI18n();
|
|
18
|
+
return defaultEngine;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Update the active engine + locale (called by I18nProvider). */
|
|
22
|
+
export function setActiveI18n(next: KratosI18n, locale: string): void {
|
|
23
|
+
activeEngine = next;
|
|
24
|
+
activeLocale = locale;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The active locale, e.g. for `Intl` formatters. Defaults to `'en'`. */
|
|
28
|
+
export function getActiveLocale(): string {
|
|
29
|
+
return activeLocale;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Translate `key` against the active locale. Falls back to a `core`-only default
|
|
34
|
+
* engine when no provider has mounted (so isolated tests + pre-mount utilities
|
|
35
|
+
* still resolve chrome). Prefer `useTranslation` inside components.
|
|
36
|
+
*/
|
|
37
|
+
export function translate(key: string, params?: Record<string, unknown>): string {
|
|
38
|
+
return engine().t(key, { locale: activeLocale, ...params });
|
|
39
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Build the client KratosI18n from the server-injected config + optional overrides.
|
|
2
|
+
//
|
|
3
|
+
// The backend is the single source of truth: it injects the plugin + app catalogs
|
|
4
|
+
// (and the locale config) into the admin HTML, and the client consumes them here.
|
|
5
|
+
//
|
|
6
|
+
// Catalog precedence (later wins): package chrome (`core`) → server-injected
|
|
7
|
+
// resources (plugin + app namespaces) → optional mount-time overrides. App override
|
|
8
|
+
// catalogs may target any namespace via a `ns:` key prefix (e.g. `core:common.save`
|
|
9
|
+
// overrides a built-in chrome string); unprefixed keys go to the `app` namespace.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createI18n,
|
|
13
|
+
type KratosI18n,
|
|
14
|
+
type I18nResources,
|
|
15
|
+
type Catalog,
|
|
16
|
+
type Direction,
|
|
17
|
+
} from '@maxal_studio/kratosjs/dist/i18n';
|
|
18
|
+
import { clientCoreResources } from './locales/core';
|
|
19
|
+
|
|
20
|
+
/** Per-locale catalog map, e.g. `{ en: {...}, sq: {...} }`. */
|
|
21
|
+
export type ClientTranslations = Record<string, Catalog>;
|
|
22
|
+
|
|
23
|
+
export interface ClientI18nConfig {
|
|
24
|
+
defaultLocale?: string;
|
|
25
|
+
fallbackLocale?: string;
|
|
26
|
+
locales?: string[];
|
|
27
|
+
/** Text direction per locale, forwarded from the server for app-added RTL locales. */
|
|
28
|
+
directions?: Record<string, Direction>;
|
|
29
|
+
/**
|
|
30
|
+
* Server-injected catalogs (`namespace -> locale -> catalog`) for the plugin and
|
|
31
|
+
* app namespaces. Populated from `window.__VALAJS_I18N__`.
|
|
32
|
+
*/
|
|
33
|
+
resources?: I18nResources;
|
|
34
|
+
/**
|
|
35
|
+
* Optional mount-time override catalogs by locale. Keys may be `ns:key`
|
|
36
|
+
* (defaults to the `app` namespace) — mainly to override built-in `core:` chrome.
|
|
37
|
+
*/
|
|
38
|
+
translations?: ClientTranslations;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mergeInto(target: I18nResources, namespace: string, locale: string, catalog: Catalog): void {
|
|
42
|
+
const ns = (target[namespace] ??= {});
|
|
43
|
+
ns[locale] = { ...(ns[locale] ?? {}), ...catalog };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Assemble the `namespace -> locale -> catalog` resources for the client engine.
|
|
48
|
+
*/
|
|
49
|
+
export function buildClientResources(config: ClientI18nConfig = {}): I18nResources {
|
|
50
|
+
const resources: I18nResources = {};
|
|
51
|
+
|
|
52
|
+
// 1. Package chrome (core namespace) — bundled with the React package.
|
|
53
|
+
for (const [locale, catalog] of Object.entries(clientCoreResources)) {
|
|
54
|
+
mergeInto(resources, 'core', locale, catalog);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Server-injected catalogs (plugin + app namespaces, already in the right
|
|
58
|
+
// app-wins precedence from the backend merge).
|
|
59
|
+
for (const [namespace, byLocale] of Object.entries(config.resources ?? {})) {
|
|
60
|
+
for (const [locale, catalog] of Object.entries(byLocale)) {
|
|
61
|
+
mergeInto(resources, namespace, locale, catalog);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Mount-time overrides (last → win). A `ns:` prefix routes the key to that
|
|
66
|
+
// namespace, so the app can override core/plugin strings; otherwise `app`.
|
|
67
|
+
for (const [locale, catalog] of Object.entries(config.translations ?? {})) {
|
|
68
|
+
for (const [key, value] of Object.entries(catalog)) {
|
|
69
|
+
const idx = key.indexOf(':');
|
|
70
|
+
const namespace = idx === -1 ? 'app' : key.slice(0, idx);
|
|
71
|
+
const realKey = idx === -1 ? key : key.slice(idx + 1);
|
|
72
|
+
mergeInto(resources, namespace, locale, { [realKey]: value });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return resources;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Locales discovered across all registered catalogs (plus any declared). */
|
|
80
|
+
export function discoverClientLocales(resources: I18nResources, declared?: string[]): string[] {
|
|
81
|
+
if (declared && declared.length > 0) return declared;
|
|
82
|
+
const set = new Set<string>();
|
|
83
|
+
for (const byLocale of Object.values(resources)) {
|
|
84
|
+
for (const locale of Object.keys(byLocale)) set.add(locale);
|
|
85
|
+
}
|
|
86
|
+
return set.size > 0 ? [...set] : ['en'];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Create the client engine from the server-injected config + optional overrides. */
|
|
90
|
+
export function buildClientI18n(config: ClientI18nConfig = {}): KratosI18n {
|
|
91
|
+
const resources = buildClientResources(config);
|
|
92
|
+
const locales = discoverClientLocales(resources, config.locales);
|
|
93
|
+
const defaultLocale = config.defaultLocale ?? (locales.includes('en') ? 'en' : locales[0]);
|
|
94
|
+
return createI18n({
|
|
95
|
+
locales,
|
|
96
|
+
defaultLocale,
|
|
97
|
+
fallbackLocale: config.fallbackLocale ?? defaultLocale,
|
|
98
|
+
directions: config.directions,
|
|
99
|
+
resources,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, screen, renderHook, act } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { I18nProvider } from './I18nProvider';
|
|
6
|
+
import { useTranslation } from './useTranslation';
|
|
7
|
+
import { useLocale } from './useLocale';
|
|
8
|
+
import { useFormatter } from './useFormatter';
|
|
9
|
+
import { LocaleSwitcher } from './LocaleSwitcher';
|
|
10
|
+
import { buildClientResources, buildClientI18n } from './buildClientI18n';
|
|
11
|
+
|
|
12
|
+
const config = {
|
|
13
|
+
defaultLocale: 'en',
|
|
14
|
+
translations: {
|
|
15
|
+
en: { 'home.title': 'Home', 'core:common.save': 'Save it' },
|
|
16
|
+
sq: { 'home.title': 'Ballina', 'core:common.save': 'Ruaje' },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function wrapper({ children }: { children: React.ReactNode }) {
|
|
21
|
+
return <I18nProvider config={config}>{children}</I18nProvider>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => localStorage.clear());
|
|
25
|
+
|
|
26
|
+
describe('buildClientResources', () => {
|
|
27
|
+
it('routes app keys: bare → app namespace, ns: prefix → that namespace', () => {
|
|
28
|
+
const res = buildClientResources(config);
|
|
29
|
+
expect(res.app.en['home.title']).toBe('Home');
|
|
30
|
+
// `core:common.save` overrides the package chrome default.
|
|
31
|
+
expect(res.core.en['common.save']).toBe('Save it');
|
|
32
|
+
// Built-in chrome still present for non-overridden keys.
|
|
33
|
+
expect(res.core.en['common.cancel']).toBe('Cancel');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('merges server-injected resources (plugin + app namespaces)', () => {
|
|
37
|
+
const res = buildClientResources({
|
|
38
|
+
resources: {
|
|
39
|
+
twofa: { en: { title: 'Verify' } },
|
|
40
|
+
app: { en: { 'users.label': 'Users' } },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(res.twofa.en['title']).toBe('Verify');
|
|
44
|
+
expect(res.app.en['users.label']).toBe('Users');
|
|
45
|
+
// Built-in chrome is still present alongside injected catalogs.
|
|
46
|
+
expect(res.core.en['common.cancel']).toBe('Cancel');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('lets mount-time translations override server-injected resources', () => {
|
|
50
|
+
const res = buildClientResources({
|
|
51
|
+
resources: { app: { en: { 'users.label': 'Users' } } },
|
|
52
|
+
translations: { en: { 'users.label': 'People' } },
|
|
53
|
+
});
|
|
54
|
+
expect(res.app.en['users.label']).toBe('People');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('localizes built-in chrome for a custom locale via injected `core` resources', () => {
|
|
58
|
+
// Backend registered `core` chrome for French (a locale the package does not bundle)
|
|
59
|
+
// plus an English override — both arrive via the injected resources.
|
|
60
|
+
const res = buildClientResources({
|
|
61
|
+
locales: ['en', 'fr'],
|
|
62
|
+
resources: { core: { fr: { 'common.save': 'Enregistrer' }, en: { 'common.save': 'Store' } } },
|
|
63
|
+
});
|
|
64
|
+
// New locale gets chrome from the injected catalog.
|
|
65
|
+
expect(res.core.fr['common.save']).toBe('Enregistrer');
|
|
66
|
+
// Injected `core` overrides the bundled default for existing locales...
|
|
67
|
+
expect(res.core.en['common.save']).toBe('Store');
|
|
68
|
+
// ...while non-overridden bundled chrome keys stay intact.
|
|
69
|
+
expect(res.core.en['common.cancel']).toBe('Cancel');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('resolves chrome in a custom locale end-to-end through the engine', () => {
|
|
73
|
+
const engine = buildClientI18n({
|
|
74
|
+
locales: ['en', 'fr'],
|
|
75
|
+
defaultLocale: 'en',
|
|
76
|
+
resources: { core: { fr: { 'common.save': 'Enregistrer' } } },
|
|
77
|
+
});
|
|
78
|
+
expect(engine.t('core:common.save', { locale: 'fr' })).toBe('Enregistrer');
|
|
79
|
+
expect(engine.getLocales()).toContain('fr');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('useTranslation', () => {
|
|
84
|
+
it('translates chrome + app keys (app overrides core)', () => {
|
|
85
|
+
const { result } = renderHook(() => useTranslation(), { wrapper });
|
|
86
|
+
expect(result.current.t('app:home.title')).toBe('Home');
|
|
87
|
+
expect(result.current.t('core:common.save')).toBe('Save it'); // app override
|
|
88
|
+
expect(result.current.t('core:common.cancel')).toBe('Cancel'); // package default
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('switching locale persists the choice and triggers a full refresh', () => {
|
|
92
|
+
const reload = vi.fn();
|
|
93
|
+
Object.defineProperty(window, 'location', { value: { reload }, writable: true });
|
|
94
|
+
const { result } = renderHook(() => useLocale(), { wrapper });
|
|
95
|
+
act(() => result.current.setLocale('sq'));
|
|
96
|
+
expect(localStorage.getItem('kratosjs-locale')).toBe('sq');
|
|
97
|
+
expect(document.documentElement.lang).toBe('sq');
|
|
98
|
+
expect(reload).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('falls back to a default engine outside any provider', () => {
|
|
102
|
+
const { result } = renderHook(() => useTranslation());
|
|
103
|
+
expect(result.current.t('core:common.save')).toBe('Save'); // built-in default
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('LocaleSwitcher', () => {
|
|
108
|
+
it('renders the registered locales and switches on change', async () => {
|
|
109
|
+
const reload = vi.fn();
|
|
110
|
+
Object.defineProperty(window, 'location', { value: { reload }, writable: true });
|
|
111
|
+
render(
|
|
112
|
+
<I18nProvider config={config}>
|
|
113
|
+
<LocaleSwitcher />
|
|
114
|
+
</I18nProvider>,
|
|
115
|
+
);
|
|
116
|
+
const select = screen.getByRole('combobox');
|
|
117
|
+
expect(select).toBeInTheDocument();
|
|
118
|
+
await userEvent.selectOptions(select, 'sq');
|
|
119
|
+
expect(localStorage.getItem('kratosjs-locale')).toBe('sq');
|
|
120
|
+
expect(document.documentElement.lang).toBe('sq');
|
|
121
|
+
expect(reload).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('renders nothing for a single-locale panel', () => {
|
|
125
|
+
render(
|
|
126
|
+
<I18nProvider config={{ defaultLocale: 'en', locales: ['en'], translations: { en: {} } }}>
|
|
127
|
+
<LocaleSwitcher />
|
|
128
|
+
</I18nProvider>,
|
|
129
|
+
);
|
|
130
|
+
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('useFormatter', () => {
|
|
135
|
+
it('formats currency and relative time in the active locale', () => {
|
|
136
|
+
const { result } = renderHook(() => useFormatter(), { wrapper });
|
|
137
|
+
expect(result.current.currency(1234.5, 'USD')).toContain('1,234.50');
|
|
138
|
+
expect(result.current.relativeTime(-2, 'hour')).toBe('2 hours ago');
|
|
139
|
+
});
|
|
140
|
+
});
|