From 5822b6aef720d30e710e84b84a179814b3c1aeb0 Mon Sep 17 00:00:00 2001 From: Santiago <71732018+Zasa-san@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:30:19 -0300 Subject: [PATCH] recoil to jotai (#6668) * initial setup * replace recoil in components, stories and tests * update in SSR and CSR * change redux legacy updates method * fixed test --- .eslintrc.js | 7 +-- app/react/App.tsx | 36 +++++++++---- app/react/App/App.js | 5 +- .../Templates/components/MetadataTemplate.tsx | 4 +- .../Forms/ConfirmNavigationModal.tsx | 3 +- .../Components/UI/NotificationsContainer.tsx | 17 ++++--- .../UI/specs/NotificationsContainer.cy.tsx | 14 +++--- .../CustomHooks/specs/useApiCaller.spec.tsx | 28 ++++++----- app/react/V2/CustomHooks/useApiCaller.tsx | 4 +- .../V2/Routes/Settings/Account/Account.tsx | 10 ++-- .../Account/Components/TwoFactorSetup.tsx | 8 +-- .../Settings/ActivityLog/ActivityLog.tsx | 10 ++-- .../Routes/Settings/Collection/Collection.tsx | 17 +++---- .../Settings/CustomUploads/CustomUploads.tsx | 4 +- .../components/DropzoneModal.tsx | 4 +- .../components/EditFileSidepanel.tsx | 4 +- .../components/UploadProgress.tsx | 4 +- .../components/uploadProgressAtom.ts | 8 +-- .../Settings/Customization/Customization.tsx | 4 +- .../V2/Routes/Settings/IX/IXDashboard.tsx | 4 +- .../V2/Routes/Settings/IX/IXSuggestions.tsx | 8 +-- .../Settings/IX/components/PDFSidepanel.tsx | 14 +++--- .../Settings/Languages/LanguagesList.tsx | 5 +- .../components/InstallLanguagesModal.tsx | 7 ++- .../Routes/Settings/MenuConfig/MenuConfig.tsx | 8 +-- .../V2/Routes/Settings/Pages/PageEditor.tsx | 4 +- .../V2/Routes/Settings/Pages/PagesList.tsx | 4 +- .../RelationshipTypes/RelationshipTypes.tsx | 18 +++---- .../Routes/Settings/Thesauri/ThesauriList.tsx | 14 +++--- .../Settings/Thesauri/ThesaurusForm.tsx | 50 ++++++++++--------- .../Translations/EditTranslations.tsx | 8 +-- .../Settings/Users/useHandleNotifications.tsx | 4 +- app/react/V2/atoms/notificationAtom.ts | 7 +-- app/react/V2/atoms/relationshipTypes.ts | 16 +----- app/react/V2/atoms/settingsAtom.ts | 16 +----- app/react/V2/atoms/templatesAtom.tsx | 16 +----- app/react/V2/atoms/translationsAtom.tsx | 7 +-- app/react/V2/shared/testingHelpers.ts | 18 +++---- app/react/entry-server.tsx | 31 ++++++------ .../stories/Forms/DatePicker.stories.tsx | 14 +++--- package.json | 2 +- yarn.lock | 48 ++++++++++++------ 42 files changed, 247 insertions(+), 267 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b027f79017..1d8746721f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -221,12 +221,9 @@ module.exports = { 'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/mouse-events-have-key-events': 'off', 'jsx-a11y/label-has-associated-control': 'off', - //react-hooks && recoil + //react-hooks 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': [ - 'warn', - { additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)' }, - ], + 'react-hooks/exhaustive-deps': ['warn'], }, overrides: [ { files: ['app/**/*spec.js'], rules: { 'max-lines-per-function': 'off' } }, diff --git a/app/react/App.tsx b/app/react/App.tsx index a9c0a99664..62816afd06 100644 --- a/app/react/App.tsx +++ b/app/react/App.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; -import { MutableSnapshot, RecoilRoot } from 'recoil'; -import { Provider } from 'react-redux'; +import { createStore, Provider } from 'jotai'; +import { Provider as ReduxProvider } from 'react-redux'; import { getRoutes } from './Routes'; import CustomProvider from './App/Provider'; import { settingsAtom, templatesAtom, translationsAtom } from './V2/atoms'; +import { relationshipTypesAtom } from './V2/atoms/relationshipTypes'; import { store } from './store'; const reduxState = store?.getState(); @@ -14,20 +15,33 @@ const templates = reduxState?.templates.toJS() || []; const router = createBrowserRouter(getRoutes(settings, reduxState?.user.get('_id'))); -const recoilGlobalState = ({ set }: MutableSnapshot) => { - set(settingsAtom, settings); - set(templatesAtom, templates); - set(translationsAtom, { locale: reduxState?.locale || 'en' }); -}; +const atomStore = createStore(); +atomStore.set(settingsAtom, settings); +atomStore.set(templatesAtom, templates); +atomStore.set(translationsAtom, { locale: reduxState?.locale || 'en' }); + +//sync deprecated redux store +atomStore.sub(settingsAtom, () => { + const value = atomStore.get(settingsAtom); + store?.dispatch({ type: 'settings/collection/SET', value }); +}); +atomStore.sub(templatesAtom, () => { + const value = atomStore.get(templatesAtom); + store?.dispatch({ type: 'templates/SET', value }); +}); +atomStore.sub(relationshipTypesAtom, () => { + const value = atomStore.get(relationshipTypesAtom); + store?.dispatch({ type: 'relationTypes/SET', value }); +}); const App = () => ( - + - + - + - + ); export { App }; diff --git a/app/react/App/App.js b/app/react/App/App.js index edcfdc6e47..65ca9082ee 100644 --- a/app/react/App/App.js +++ b/app/react/App/App.js @@ -1,12 +1,11 @@ import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Outlet, useLocation, useParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import Notifications from 'app/Notifications'; import Cookiepopup from 'app/App/Cookiepopup'; import { TranslateForm, t } from 'app/I18N'; import { Icon } from 'UI'; - import { socket } from 'app/socket'; import { NotificationsContainer } from 'V2/Components/UI'; import { settingsAtom } from 'app/V2/atoms/settingsAtom'; @@ -27,7 +26,7 @@ import 'flowbite'; const App = ({ customParams }) => { const [showMenu, setShowMenu] = useState(false); const [confirmOptions, setConfirmOptions] = useState({}); - const setSettings = useSetRecoilState(settingsAtom); + const setSettings = useSetAtom(settingsAtom); const location = useLocation(); const params = useParams(); diff --git a/app/react/Templates/components/MetadataTemplate.tsx b/app/react/Templates/components/MetadataTemplate.tsx index e7af2bfe08..ad8d73a7ff 100644 --- a/app/react/Templates/components/MetadataTemplate.tsx +++ b/app/react/Templates/components/MetadataTemplate.tsx @@ -8,6 +8,7 @@ import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { actions as formActions, Control, Field } from 'react-redux-form'; +import { useSetAtom } from 'jotai'; import { Icon } from 'UI'; import { withContext } from 'app/componentWrappers'; import { FormGroup } from 'app/Forms'; @@ -16,7 +17,6 @@ import { I18NLink, t, Translate } from 'app/I18N'; import { notificationActions } from 'app/Notifications'; import { notify } from 'app/Notifications/actions/notificationsActions'; import { templatesAtom } from 'app/V2/atoms'; -import { useSetRecoilState } from 'recoil'; import api from 'app/Templates/TemplatesAPI'; import { addProperty, @@ -329,7 +329,7 @@ const target = { const withTemplatesAtom = (Comp: React.ComponentClass) => (props: T) => { - const updateTemplatesAtom = useSetRecoilState(templatesAtom); + const updateTemplatesAtom = useSetAtom(templatesAtom); return ; }; diff --git a/app/react/V2/Components/Forms/ConfirmNavigationModal.tsx b/app/react/V2/Components/Forms/ConfirmNavigationModal.tsx index bcf68fa095..5004af6fea 100644 --- a/app/react/V2/Components/Forms/ConfirmNavigationModal.tsx +++ b/app/react/V2/Components/Forms/ConfirmNavigationModal.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { SetterOrUpdater } from 'recoil'; import { Translate } from 'app/I18N'; import { Button, Modal } from '../UI'; type confirmationModalType = { - setShowModal: SetterOrUpdater; + setShowModal: React.Dispatch>; onConfirm?: () => void; }; diff --git a/app/react/V2/Components/UI/NotificationsContainer.tsx b/app/react/V2/Components/UI/NotificationsContainer.tsx index 9f0176973a..9756438e25 100644 --- a/app/react/V2/Components/UI/NotificationsContainer.tsx +++ b/app/react/V2/Components/UI/NotificationsContainer.tsx @@ -1,19 +1,20 @@ import React, { useEffect, useState } from 'react'; -import { useRecoilValue, useResetRecoilState } from 'recoil'; +import { useAtomValue } from 'jotai'; +import { useResetAtom } from 'jotai/utils'; import { notificationAtom } from 'V2/atoms'; import { Notification } from 'V2/Components/UI/Notification'; const NotificationsContainer = () => { const timeout = 6000; const [timerId, setTimerId] = useState(); - const notification = useRecoilValue(notificationAtom); - const resetRecoilAtom = useResetRecoilState(notificationAtom); + const notification = useAtomValue(notificationAtom); + const resetAtom = useResetAtom(notificationAtom); const notificationIsSet = Boolean(notification.text && notification.type); useEffect(() => { if (notificationIsSet && !timerId) { const timer = setTimeout(() => { - resetRecoilAtom(); + resetAtom(); }, timeout); setTimerId(timer); @@ -25,10 +26,10 @@ const NotificationsContainer = () => { setTimerId(undefined); } }; - }, [resetRecoilAtom, notificationIsSet, timerId]); + }, [resetAtom, notificationIsSet, timerId]); const onClickHandler = () => { - resetRecoilAtom(); + resetAtom(); }; const handleMouseEnter = () => timerId && clearTimeout(timerId); @@ -37,7 +38,7 @@ const NotificationsContainer = () => { if (timerId) clearTimeout(timerId); const timer = setTimeout(() => { - resetRecoilAtom(); + resetAtom(); }, timeout); setTimerId(timer); @@ -52,7 +53,7 @@ const NotificationsContainer = () => {
{ }; const Component = () => { - const setNotification = useSetRecoilState(notificationAtom); + const setNotification = useSetAtom(notificationAtom); const onClick = () => { setNotification(notification); }; return ( - + <> - + ); }; @@ -38,9 +38,9 @@ describe('Notifications container', () => { beforeEach(() => { mount( - + - + ); }); diff --git a/app/react/V2/CustomHooks/specs/useApiCaller.spec.tsx b/app/react/V2/CustomHooks/specs/useApiCaller.spec.tsx index ac18a0dc72..dda9c03096 100644 --- a/app/react/V2/CustomHooks/specs/useApiCaller.spec.tsx +++ b/app/react/V2/CustomHooks/specs/useApiCaller.spec.tsx @@ -2,13 +2,19 @@ * @jest-environment jsdom */ import { act, renderHook } from '@testing-library/react'; -import * as recoil from 'recoil'; -import { RecoilRoot, RecoilState } from 'recoil'; +import { Provider } from 'jotai'; import { RequestParams } from 'app/utils/RequestParams'; import { Translate } from 'app/I18N'; import React from 'react'; import { useApiCaller } from '../useApiCaller'; +const mockSetNotification = jest.fn(); + +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useSetAtom: () => mockSetNotification, +})); + describe('describe useApiCaller', () => { let apiCallerHook: { current: { @@ -19,14 +25,9 @@ describe('describe useApiCaller', () => { ) => any; }; }; - const setNotificationMock = jest.fn(); beforeEach(() => { - jest - .spyOn(recoil, 'useSetRecoilState') - .mockImplementation((_state: RecoilState) => setNotificationMock); - - ({ result: apiCallerHook } = renderHook(() => useApiCaller(), { wrapper: RecoilRoot })); + ({ result: apiCallerHook } = renderHook(() => useApiCaller(), { wrapper: Provider })); }); afterEach(() => { @@ -41,20 +42,21 @@ describe('describe useApiCaller', () => { new RequestParams({ data: 'paramid' }), successful action ); - expect(setNotificationMock).toHaveBeenCalled(); + + expect(mockSetNotification).toHaveBeenCalled(); if (success) { expect(await apiResult.data).toEqual({ data: 'result' }); expect(await apiResult.error).toBeUndefined(); - expect(setNotificationMock.mock.calls[0][0].type).toEqual('success'); - expect(setNotificationMock.mock.calls[0][0].text.props.children).toEqual( + expect(mockSetNotification.mock.calls[0][0].type).toEqual('success'); + expect(mockSetNotification.mock.calls[0][0].text.props.children).toEqual( 'successful action' ); } else { expect(await apiResult.data).toBeUndefined(); expect(await apiResult.error).toEqual('An error occurred'); - expect(setNotificationMock.mock.calls[0][0].type).toEqual('error'); - expect(setNotificationMock.mock.calls[0][0].text.props.children).toEqual( + expect(mockSetNotification.mock.calls[0][0].type).toEqual('error'); + expect(mockSetNotification.mock.calls[0][0].text.props.children).toEqual( 'An error occurred' ); } diff --git a/app/react/V2/CustomHooks/useApiCaller.tsx b/app/react/V2/CustomHooks/useApiCaller.tsx index 1857e16ae1..00a539ae95 100644 --- a/app/react/V2/CustomHooks/useApiCaller.tsx +++ b/app/react/V2/CustomHooks/useApiCaller.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { Translate } from 'app/I18N'; import { notificationAtom } from 'V2/atoms'; import { RequestParams } from 'app/utils/RequestParams'; @@ -16,7 +16,7 @@ const getError = async (res: Response) => { }; const useApiCaller = () => { - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const handleSuccess = async (res: Response, successMessageComponent: React.ReactNode) => { setNotifications({ diff --git a/app/react/V2/Routes/Settings/Account/Account.tsx b/app/react/V2/Routes/Settings/Account/Account.tsx index 41bdf99b95..32ff69be27 100644 --- a/app/react/V2/Routes/Settings/Account/Account.tsx +++ b/app/react/V2/Routes/Settings/Account/Account.tsx @@ -7,7 +7,7 @@ import { UserSchema } from 'shared/types/userType'; import { LoaderFunction, useLoaderData, useRevalidator } from 'react-router-dom'; import { useForm } from 'react-hook-form'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { notificationAtom } from 'app/V2/atoms'; import { InputField } from 'app/V2/Components/Forms'; @@ -24,7 +24,7 @@ const accountLoader = const Account = () => { const userAccount = useLoaderData() as UserSchema; const [isSidepanelOpen, setIsSidepanelOpen] = useState(false); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const revalidator = useRevalidator(); type AccountForm = UserSchema & { passwordConfirm?: string }; @@ -125,7 +125,7 @@ const Account = () => { title={Two-Factor Authentication} color="default" > -
+
@@ -142,7 +142,7 @@ const Account = () => { title={Two-Factor Authentication} color="yellow" > -
+
-
+
diff --git a/app/react/V2/Routes/Settings/ActivityLog/ActivityLog.tsx b/app/react/V2/Routes/Settings/ActivityLog/ActivityLog.tsx index 844cc014f0..b79355bdc5 100644 --- a/app/react/V2/Routes/Settings/ActivityLog/ActivityLog.tsx +++ b/app/react/V2/Routes/Settings/ActivityLog/ActivityLog.tsx @@ -22,7 +22,7 @@ import * as activityLogAPI from 'V2/api/activityLog'; import type { ActivityLogResponse } from 'V2/api/activityLog'; import { useIsFirstRender } from 'app/V2/CustomHooks/useIsFirstRender'; import { ActivityLogEntryType } from 'shared/types/activityLogEntryType'; -import { useRecoilValue } from 'recoil'; +import { useAtomValue } from 'jotai'; import { ClientSettings } from 'app/apiResponseTypes'; import { settingsAtom, translationsAtom } from 'app/V2/atoms'; import { getActivityLogColumns } from './components/TableElements'; @@ -123,8 +123,8 @@ interface ActivityLogSearch { const ActivityLog = () => { const [selectedEntry, setSelectedEntry] = useState | null>(null); - const { dateFormat = 'yyyy-mm-dd' } = useRecoilValue(settingsAtom); - const { locale } = useRecoilValue<{ locale: string }>(translationsAtom); + const { dateFormat = 'yyyy-mm-dd' } = useAtomValue(settingsAtom); + const { locale } = useAtomValue<{ locale: string }>(translationsAtom); const [sorting, setSorting] = useState([]); const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); @@ -230,8 +230,8 @@ const ActivityLog = () => { id="activity-filters-form mr-10" onSubmit={handleSubmit(async data => onSubmit(data))} > -
-

+
+

Activity Log

@@ -78,8 +75,8 @@ const Collection = () => { }; const { links, custom, ...formData } = settings; - const setNotifications = useSetRecoilState(notificationAtom); - const setSettings = useSetRecoilState(settingsAtom); + const setNotifications = useSetAtom(notificationAtom); + const setSettings = useSetAtom(settingsAtom); const revalidator = useRevalidator(); formData.private = !formData.private; const { diff --git a/app/react/V2/Routes/Settings/CustomUploads/CustomUploads.tsx b/app/react/V2/Routes/Settings/CustomUploads/CustomUploads.tsx index 35f0825a74..28ebd8b317 100644 --- a/app/react/V2/Routes/Settings/CustomUploads/CustomUploads.tsx +++ b/app/react/V2/Routes/Settings/CustomUploads/CustomUploads.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; import { LoaderFunction, useBlocker, useLoaderData, useRevalidator } from 'react-router-dom'; import { IncomingHttpHeaders } from 'http'; import { Row } from '@tanstack/react-table'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { Translate } from 'app/I18N'; import { FetchResponseError } from 'shared/JSONRequest'; import { FileType } from 'shared/types/fileType'; @@ -33,7 +33,7 @@ const uploadService = new UploadService('custom'); const CustomUploads = () => { const files = useLoaderData() as FileType[]; - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const revalidator = useRevalidator(); const [selectedRows, setSelectedRows] = useState[]>([]); const [fileToEdit, setFileToEdit] = useState(); diff --git a/app/react/V2/Routes/Settings/CustomUploads/components/DropzoneModal.tsx b/app/react/V2/Routes/Settings/CustomUploads/components/DropzoneModal.tsx index e1b078c76d..258665d1f2 100644 --- a/app/react/V2/Routes/Settings/CustomUploads/components/DropzoneModal.tsx +++ b/app/react/V2/Routes/Settings/CustomUploads/components/DropzoneModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useRevalidator } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { FetchResponseError } from 'shared/JSONRequest'; import { Translate } from 'app/I18N'; import { FileType } from 'shared/types/fileType'; @@ -19,7 +19,7 @@ type DropzoneModalProps = { const DropzoneModal = ({ notify, isOpen, setIsOpen, uploadService }: DropzoneModalProps) => { const revalidator = useRevalidator(); const [filesToUpload, setFilesToUpload] = useState([]); - const updateProgress = useSetRecoilState(uploadProgressAtom); + const updateProgress = useSetAtom(uploadProgressAtom); const handleCancel = () => { setIsOpen(false); diff --git a/app/react/V2/Routes/Settings/CustomUploads/components/EditFileSidepanel.tsx b/app/react/V2/Routes/Settings/CustomUploads/components/EditFileSidepanel.tsx index da3489e42f..c6475a5624 100644 --- a/app/react/V2/Routes/Settings/CustomUploads/components/EditFileSidepanel.tsx +++ b/app/react/V2/Routes/Settings/CustomUploads/components/EditFileSidepanel.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useForm } from 'react-hook-form'; import { useRevalidator } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { Translate } from 'app/I18N'; import { FileType } from 'shared/types/fileType'; import { FetchResponseError } from 'shared/JSONRequest'; @@ -21,7 +21,7 @@ type EditFileSidepanelProps = { const EditFileSidepanel = ({ showSidepanel, closeSidepanel, file }: EditFileSidepanelProps) => { const { name, extension } = getFileNameAndExtension(file?.originalname); const revalidator = useRevalidator(); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const notify = (response: FileType | FetchResponseError) => { const hasErrors = response instanceof FetchResponseError; diff --git a/app/react/V2/Routes/Settings/CustomUploads/components/UploadProgress.tsx b/app/react/V2/Routes/Settings/CustomUploads/components/UploadProgress.tsx index 74e50583ac..891bcb6b71 100644 --- a/app/react/V2/Routes/Settings/CustomUploads/components/UploadProgress.tsx +++ b/app/react/V2/Routes/Settings/CustomUploads/components/UploadProgress.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Translate } from 'app/I18N'; -import { useRecoilValue } from 'recoil'; +import { useAtomValue } from 'jotai'; import { uploadProgressAtom } from './uploadProgressAtom'; type UploadProgressProps = { @@ -8,7 +8,7 @@ type UploadProgressProps = { }; const UploadProgress = ({ queueLength }: UploadProgressProps) => { - const { filename, progress } = useRecoilValue(uploadProgressAtom); + const { filename, progress } = useAtomValue(uploadProgressAtom); return filename ? (
diff --git a/app/react/V2/Routes/Settings/CustomUploads/components/uploadProgressAtom.ts b/app/react/V2/Routes/Settings/CustomUploads/components/uploadProgressAtom.ts index 8e2be0b511..1b8e64ef88 100644 --- a/app/react/V2/Routes/Settings/CustomUploads/components/uploadProgressAtom.ts +++ b/app/react/V2/Routes/Settings/CustomUploads/components/uploadProgressAtom.ts @@ -1,8 +1,8 @@ -import { atom } from 'recoil'; +import { atom } from 'jotai'; -const uploadProgressAtom = atom({ - key: 'uploadProgress', - default: { filename: '', progress: undefined } as { filename?: string; progress?: number }, +const uploadProgressAtom = atom({ filename: '', progress: undefined } as { + filename?: string; + progress?: number; }); export { uploadProgressAtom }; diff --git a/app/react/V2/Routes/Settings/Customization/Customization.tsx b/app/react/V2/Routes/Settings/Customization/Customization.tsx index 08287c50d4..7d816ca927 100644 --- a/app/react/V2/Routes/Settings/Customization/Customization.tsx +++ b/app/react/V2/Routes/Settings/Customization/Customization.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { LoaderFunction, useBlocker, useLoaderData } from 'react-router-dom'; import { IncomingHttpHeaders } from 'http'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { FetchResponseError } from 'shared/JSONRequest'; import { ClientSettings } from 'app/apiResponseTypes'; import { Translate } from 'app/I18N'; @@ -28,7 +28,7 @@ const Customisation = () => { const [showModal, setShowModal] = useState(false); const [hasChanges, setHasChanges] = useState(false); const blocker = useBlocker(hasChanges); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); useEffect(() => { if (blocker.state === 'blocked') { diff --git a/app/react/V2/Routes/Settings/IX/IXDashboard.tsx b/app/react/V2/Routes/Settings/IX/IXDashboard.tsx index 45618458b0..ed5277bb6c 100644 --- a/app/react/V2/Routes/Settings/IX/IXDashboard.tsx +++ b/app/react/V2/Routes/Settings/IX/IXDashboard.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useState } from 'react'; import { IncomingHttpHeaders } from 'http'; import { LoaderFunction, useLoaderData, useRevalidator } from 'react-router-dom'; import { Row } from '@tanstack/react-table'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import * as extractorsAPI from 'app/V2/api/ix/extractors'; import * as templatesAPI from 'V2/api/templates'; import { SettingsContent } from 'V2/Components/Layouts/SettingsContent'; @@ -61,7 +61,7 @@ const IXDashboard = () => { const [selected, setSelected] = useState[]>([]); const [confirmModal, setConfirmModal] = useState(false); const [extractorModal, setExtractorModal] = useState(false); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const formmatedExtractors = useMemo( () => formatExtractors(extractors, templates), diff --git a/app/react/V2/Routes/Settings/IX/IXSuggestions.tsx b/app/react/V2/Routes/Settings/IX/IXSuggestions.tsx index 304732a035..8f27b6f092 100644 --- a/app/react/V2/Routes/Settings/IX/IXSuggestions.tsx +++ b/app/react/V2/Routes/Settings/IX/IXSuggestions.tsx @@ -11,7 +11,7 @@ import { useSearchParams, } from 'react-router-dom'; import { Row, SortingState } from '@tanstack/react-table'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import * as extractorsAPI from 'app/V2/api/ix/extractors'; import * as suggestionsAPI from 'app/V2/api/ix/suggestions'; import * as templatesAPI from 'V2/api/templates'; @@ -72,7 +72,7 @@ const IXSuggestions = () => { const [selected, setSelected] = useState[]>([]); const [sorting, setSorting] = useState([]); const { revalidate } = useRevalidator(); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const [status, setStatus] = useState<{ status: ixStatus; message?: string; @@ -231,7 +231,7 @@ const IXSuggestions = () => { {selected.length ? ( -
+
) : ( -
+
diff --git a/app/react/V2/Routes/Settings/Translations/EditTranslations.tsx b/app/react/V2/Routes/Settings/Translations/EditTranslations.tsx index 21521b461f..513a0604cb 100644 --- a/app/react/V2/Routes/Settings/Translations/EditTranslations.tsx +++ b/app/react/V2/Routes/Settings/Translations/EditTranslations.tsx @@ -12,7 +12,7 @@ import { import { InformationCircleIcon } from '@heroicons/react/20/solid'; import { IncomingHttpHeaders } from 'http'; import { useForm } from 'react-hook-form'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { Translate } from 'app/I18N'; import { advancedSort } from 'app/utils/advancedSort'; import { ClientTranslationSchema } from 'app/istore'; @@ -180,7 +180,7 @@ const EditTranslations = () => { const [hideTranslated, setHideTranslated] = useState(false); const fetcher = useFetcher(); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const [showModal, setShowModal] = useState(false); const fileInputRef: React.MutableRefObject = useRef(null); @@ -302,7 +302,7 @@ const EditTranslations = () => { /> ) : ( -
+
There are no untranslated terms @@ -313,7 +313,7 @@ const EditTranslations = () => { -
+
{contextId === 'System' && ( <> diff --git a/app/react/V2/Routes/Settings/Users/useHandleNotifications.tsx b/app/react/V2/Routes/Settings/Users/useHandleNotifications.tsx index cb98795e56..f33a680876 100644 --- a/app/react/V2/Routes/Settings/Users/useHandleNotifications.tsx +++ b/app/react/V2/Routes/Settings/Users/useHandleNotifications.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useFetchers } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; +import { useSetAtom } from 'jotai'; import { last } from 'lodash'; import { Translate } from 'app/I18N'; import { FetchResponseError } from 'shared/JSONRequest'; @@ -9,7 +9,7 @@ import { FormIntent } from './types'; const useHandleNotifications = () => { const fetchers = useFetchers(); - const setNotifications = useSetRecoilState(notificationAtom); + const setNotifications = useSetAtom(notificationAtom); const lastFetcherCall = last(fetchers) || fetchers[0]; const intent = lastFetcherCall?.formData?.get('intent') as FormIntent; diff --git a/app/react/V2/atoms/notificationAtom.ts b/app/react/V2/atoms/notificationAtom.ts index 6dd40af9ed..01ad02ce4f 100644 --- a/app/react/V2/atoms/notificationAtom.ts +++ b/app/react/V2/atoms/notificationAtom.ts @@ -1,12 +1,9 @@ -import { atom } from 'recoil'; +import { atomWithReset } from 'jotai/utils'; import { NotificationProps } from '../Components/UI/Notification'; type notificationAtomType = Omit; -const notificationAtom = atom({ - key: 'Notification', - default: {} as notificationAtomType, -}); +const notificationAtom = atomWithReset({} as notificationAtomType); export type { notificationAtomType }; export { notificationAtom }; diff --git a/app/react/V2/atoms/relationshipTypes.ts b/app/react/V2/atoms/relationshipTypes.ts index 013e1ea5ad..a69e091fd5 100644 --- a/app/react/V2/atoms/relationshipTypes.ts +++ b/app/react/V2/atoms/relationshipTypes.ts @@ -1,18 +1,6 @@ -import { atom } from 'recoil'; +import { atom } from 'jotai'; import { ClientRelationshipType } from 'app/apiResponseTypes'; -import { store } from 'app/store'; -const relationshipTypesAtom = atom({ - key: 'relationshipTypes', - default: [] as ClientRelationshipType[], - //sync deprecated redux store - effects: [ - ({ onSet }) => { - onSet(newValue => { - store?.dispatch({ type: 'relationTypes/SET', value: newValue }); - }); - }, - ], -}); +const relationshipTypesAtom = atom([] as ClientRelationshipType[]); export { relationshipTypesAtom }; diff --git a/app/react/V2/atoms/settingsAtom.ts b/app/react/V2/atoms/settingsAtom.ts index d929583975..a56cf2c5fc 100644 --- a/app/react/V2/atoms/settingsAtom.ts +++ b/app/react/V2/atoms/settingsAtom.ts @@ -1,18 +1,6 @@ -import { atom } from 'recoil'; +import { atom } from 'jotai'; import { ClientSettings } from 'app/apiResponseTypes'; -import { store } from 'app/store'; -const settingsAtom = atom({ - key: 'settings', - default: {} as ClientSettings, - //sync deprecated redux store - effects: [ - ({ onSet }) => { - onSet(newValue => { - store?.dispatch({ type: 'settings/collection/SET', value: newValue }); - }); - }, - ], -}); +const settingsAtom = atom({} as ClientSettings); export { settingsAtom }; diff --git a/app/react/V2/atoms/templatesAtom.tsx b/app/react/V2/atoms/templatesAtom.tsx index 8e646591b1..381b97efcf 100644 --- a/app/react/V2/atoms/templatesAtom.tsx +++ b/app/react/V2/atoms/templatesAtom.tsx @@ -1,18 +1,6 @@ -import { atom } from 'recoil'; +import { atom } from 'jotai'; import { Template } from 'app/apiResponseTypes'; -import { store } from 'app/store'; -const templatesAtom = atom({ - key: 'templates', - default: [] as Template[], - //sync deprecated redux store - effects: [ - ({ onSet }) => { - onSet(newValue => { - store?.dispatch({ type: 'templates/SET', value: newValue }); - }); - }, - ], -}); +const templatesAtom = atom([] as Template[]); export { templatesAtom }; diff --git a/app/react/V2/atoms/translationsAtom.tsx b/app/react/V2/atoms/translationsAtom.tsx index f688ab31b8..40e8f13386 100644 --- a/app/react/V2/atoms/translationsAtom.tsx +++ b/app/react/V2/atoms/translationsAtom.tsx @@ -1,8 +1,5 @@ -import { atom } from 'recoil'; +import { atom } from 'jotai'; -const translationsAtom = atom({ - key: 'translations', - default: { locale: '' }, -}); +const translationsAtom = atom({ locale: '' }); export { translationsAtom }; diff --git a/app/react/V2/shared/testingHelpers.ts b/app/react/V2/shared/testingHelpers.ts index ef9376db2f..8689dd6be2 100644 --- a/app/react/V2/shared/testingHelpers.ts +++ b/app/react/V2/shared/testingHelpers.ts @@ -1,10 +1,10 @@ /* eslint-disable camelcase */ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { createStore } from 'jotai'; import { fromJS } from 'immutable'; import { IStore } from 'app/istore'; import { merge } from 'lodash'; -import { MutableSnapshot } from 'recoil'; import { ClientSettings } from 'app/apiResponseTypes'; import { settingsAtom } from '../atoms'; @@ -26,15 +26,15 @@ const middlewares = [thunk]; const LEGACY_createStore = (state?: Partial) => configureStore(middlewares)(() => ({ ...defaultState, ...state })); -const defaultRecoilState: { settings: ClientSettings } = { +const defaultAtomsState: { settings: ClientSettings } = { settings: { dateFormat: 'dd-mm-yyyy' }, }; -const recoilGlobalState = - (initialState: { settings?: ClientSettings } = {}) => - ({ set }: MutableSnapshot) => { - const defaultRecoilValues = merge(defaultRecoilState, initialState); - set(settingsAtom, defaultRecoilValues.settings); - }; +const atomsGlobalState = (initialState: { settings?: ClientSettings } = {}) => { + const myStore = createStore(); + const values = merge(defaultAtomsState, initialState); + myStore.set(settingsAtom, values.settings); + return myStore; +}; -export { LEGACY_createStore, recoilGlobalState }; +export { LEGACY_createStore, atomsGlobalState }; diff --git a/app/react/entry-server.tsx b/app/react/entry-server.tsx index fca29288ab..412e0a0acd 100644 --- a/app/react/entry-server.tsx +++ b/app/react/entry-server.tsx @@ -4,16 +4,16 @@ import { Request as ExpressRequest, Response } from 'express'; // eslint-disable-next-line node/no-restricted-import import fs from 'fs'; import { AgnosticDataRouteObject, createStaticHandler } from '@remix-run/router'; -import api from 'app/utils/api'; -import { RequestParams } from 'app/utils/RequestParams'; -import { omit, isEmpty } from 'lodash'; +import { matchRoutes, RouteObject } from 'react-router-dom'; import React from 'react'; import ReactDOMServer from 'react-dom/server'; import { Helmet } from 'react-helmet'; -import { Provider } from 'react-redux'; -import { matchRoutes, RouteObject } from 'react-router-dom'; +import { Provider, createStore } from 'jotai'; +import { omit, isEmpty } from 'lodash'; +import { Provider as ReduxProvider } from 'react-redux'; +import api from 'app/utils/api'; +import { RequestParams } from 'app/utils/RequestParams'; import { createStaticRouter, StaticRouterProvider } from 'react-router-dom/server'; -import { MutableSnapshot, RecoilRoot } from 'recoil'; import { FetchResponseError } from 'shared/JSONRequest'; import { ClientSettings } from 'app/apiResponseTypes'; import translationsApi, { IndexedTranslations } from '../api/i18n/translations'; @@ -25,7 +25,7 @@ import { settingsAtom } from './V2/atoms/settingsAtom'; import { I18NUtils, t, Translate } from './I18N'; import { IStore } from './istore'; import { getRoutes } from './Routes'; -import createStore from './store'; +import createReduxStore from './store'; class ServerRenderingFetchError extends Error { constructor(message: string) { @@ -149,7 +149,7 @@ const prepareStore = async (req: ExpressRequest, settings: ClientSettings, langu }, }; - const reduxStore = createStore({ + const reduxStore = createReduxStore({ ...globalResources, locale, }); @@ -181,7 +181,7 @@ const setReduxState = async ( return null; }) .filter(v => v); - const initialStore = createStore(reduxState); + const initialStore = createReduxStore(reduxState); if (dataLoaders && dataLoaders.length > 0) { const headers = { 'Content-Language': reduxState.locale, @@ -272,14 +272,13 @@ const EntryServer = async (req: ExpressRequest, res: Response) => { resetTranslations(); - const recoilGlobalState = ({ set }: MutableSnapshot) => { - set(settingsAtom, settings); - }; + const atomStore = createStore(); + atomStore.set(settingsAtom, settings); const componentHtml = ReactDOMServer.renderToString( - + - + { nonce="the-nonce" /> - + - + ); const html = ReactDOMServer.renderToString( diff --git a/app/react/stories/Forms/DatePicker.stories.tsx b/app/react/stories/Forms/DatePicker.stories.tsx index fec47ab639..c4298faa07 100644 --- a/app/react/stories/Forms/DatePicker.stories.tsx +++ b/app/react/stories/Forms/DatePicker.stories.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { Provider } from 'react-redux'; -import { RecoilRoot } from 'recoil'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Provider } from 'jotai'; import { Meta, StoryObj } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { DatePicker } from 'app/V2/Components/Forms'; -import { LEGACY_createStore as createStore, recoilGlobalState } from 'V2/shared/testingHelpers'; +import { LEGACY_createStore as createStore, atomsGlobalState } from 'V2/shared/testingHelpers'; const meta: Meta = { title: 'Forms/DatePicker', @@ -25,8 +25,8 @@ type Story = StoryObj; const Primary: Story = { render: args => ( - - + + - - + + ), }; diff --git a/package.json b/package.json index 9f958bd6dc..10ae5fdf00 100644 --- a/package.json +++ b/package.json @@ -156,6 +156,7 @@ "immutable": "^3.7.6", "is-reachable": "^5.2.1", "isomorphic-fetch": "3.0.0", + "jotai": "^2.8.0", "json-schema": "^0.4.0", "json-schema-to-typescript": "^13.1.2", "jvent": "1.0.2", @@ -215,7 +216,6 @@ "react-text-selection-handler": "0.1.0", "react-widgets": "v4.6.1", "recharts": "2.12.3", - "recoil": "^0.7.7", "redis": "^3.1.2", "redlock": "^4.2.0", "redux": "^3.7.2", diff --git a/yarn.lock b/yarn.lock index 18e5ef5c74..64dddf3c52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11389,11 +11389,6 @@ gzip-size@^6.0.0: dependencies: duplexer "^0.1.2" -hamt_plus@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" - integrity sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA== - handlebars@^4.7.7, handlebars@^4.7.8: version "4.7.8" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" @@ -13232,6 +13227,11 @@ joi@^17.6.0: "@sideway/formula" "^3.0.0" "@sideway/pinpoint" "^2.0.0" +jotai@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.8.0.tgz#5a6585cd5576c400c2c5f8e157b83ad2ba70b2ab" + integrity sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g== + jpeg-js@^0.3.4: version "0.3.7" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.3.7.tgz#471a89d06011640592d314158608690172b1028d" @@ -17385,13 +17385,6 @@ rechoir@^0.8.0: dependencies: resolve "^1.20.0" -recoil@^0.7.7: - version "0.7.7" - resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.7.tgz#c5f2c843224384c9c09e4a62c060fb4c1454dc8e" - integrity sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ== - dependencies: - hamt_plus "1.0.2" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -18615,7 +18608,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18710,7 +18712,14 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20394,7 +20403,7 @@ world-countries@5.0.0: resolved "https://registry.yarnpkg.com/world-countries/-/world-countries-5.0.0.tgz#6f75ebcce3d5224d84e9117eaf0d75a7726b6501" integrity sha512-wAfOT9Y5i/xnxNOdKJKXdOCw9Q3yQLahBUeuRol+s+o20F6h2a4tLEbJ1lBCYwEQ30Sf9Meqeipk1gib3YwF5w== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -20412,6 +20421,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"