From 025aa709657dcd98f7c2e73c31db85b6c2cd6aa1 Mon Sep 17 00:00:00 2001 From: Siarhei Karol Date: Thu, 5 Dec 2024 17:56:12 +0300 Subject: [PATCH] migrate UI controls store --- src/common/hooks/useComplexLookup.ts | 6 +-- src/common/hooks/useProfileSchema.ts | 6 +-- src/common/hooks/useRecordControls.ts | 9 ++-- .../AdvancedSearchModal.tsx | 5 +- .../MarcPreviewComplexLookup.tsx | 6 +-- .../ComplexLookupField/ModalComplexLookup.tsx | 4 +- .../DuplicateGroupContainer.tsx | 10 ++-- .../EditControlPane/EditControlPane.tsx | 6 +-- src/components/EditPreview/EditPreview.tsx | 6 +-- src/components/EditSection/EditSection.tsx | 8 +--- src/components/Fields/Fields.tsx | 6 +-- src/components/ItemSearch/ItemSearch.tsx | 5 +- .../ModalDuplicateImportedResource.tsx | 7 +-- src/components/Preview/Fields.tsx | 6 +-- .../SearchControls/SearchControls.tsx | 10 ++-- src/state/index.ts | 2 - src/state/ui.ts | 46 ------------------- src/store/index.ts | 1 + src/store/selectors.ts | 2 + src/store/ui.ts | 26 +++++++++++ src/store/utils/slice.ts | 14 ++++-- .../components/AdvancedSearchModal.test.tsx | 20 +++++--- .../components/EditControlPane.test.tsx | 14 ++++-- .../__tests__/components/EditPreview.test.tsx | 13 +++--- .../__tests__/components/EditSection.test.tsx | 13 +++--- .../MarcPreviewComplexLookup.test.tsx | 13 +++--- .../__tests__/components/Preview.test.tsx | 13 +++--- src/test/__tests__/store/utils/slice.test.ts | 36 +++++++++++++-- src/types/store.d.ts | 3 ++ src/views/Edit/Edit.tsx | 7 +-- 30 files changed, 160 insertions(+), 163 deletions(-) delete mode 100644 src/state/ui.ts create mode 100644 src/types/store.d.ts diff --git a/src/common/hooks/useComplexLookup.ts b/src/common/hooks/useComplexLookup.ts index 1edb2f36..a64e51b3 100644 --- a/src/common/hooks/useComplexLookup.ts +++ b/src/common/hooks/useComplexLookup.ts @@ -1,5 +1,4 @@ import { ChangeEvent, useCallback, useState } from 'react'; -import { useResetRecoilState } from 'recoil'; import { generateEmptyValueUuid, getLinkedField, @@ -8,11 +7,10 @@ import { } from '@common/helpers/complexLookup.helper'; import { __MOCK_URI_CHANGE_WHEN_IMPLEMENTING } from '@common/constants/complexLookup.constants'; import { AdvancedFieldType } from '@common/constants/uiControls.constants'; -import state from '@state'; import { useModalControls } from './useModalControls'; import { useMarcData } from './useMarcData'; import { useServicesContext } from './useServicesContext'; -import { useInputsState, useMarcPreviewState, useProfileState } from '@src/store'; +import { useInputsState, useMarcPreviewState, useProfileState, useUIState } from '@src/store'; export const useComplexLookup = ({ entry, @@ -35,7 +33,7 @@ export const useComplexLookup = ({ metaData: marcPreviewMetadata, resetMetaData: resetMarcPreviewMetadata, } = useMarcPreviewState(); - const resetIsMarcPreviewOpen = useResetRecoilState(state.ui.isMarcPreviewOpen); + const { resetIsMarcPreviewOpen } = useUIState(); const { isModalOpen, setIsModalOpen, openModal } = useModalControls(); const { fetchMarcData } = useMarcData(setComplexValue); const { uuid, linkedEntry } = entry; diff --git a/src/common/hooks/useProfileSchema.ts b/src/common/hooks/useProfileSchema.ts index f6d7e763..f24514f5 100644 --- a/src/common/hooks/useProfileSchema.ts +++ b/src/common/hooks/useProfileSchema.ts @@ -1,12 +1,10 @@ -import { useSetRecoilState } from 'recoil'; -import state from '@state'; import { useServicesContext } from './useServicesContext'; import { deleteFromSetImmutable } from '@common/helpers/common.helper'; -import { useInputsState, useProfileState, useStatusState } from '@src/store'; +import { useInputsState, useProfileState, useStatusState, useUIState } from '@src/store'; export const useProfileSchema = () => { const { selectedEntriesService, schemaWithDuplicatesService } = useServicesContext() as Required; - const setCollapsibleEntries = useSetRecoilState(state.ui.collapsibleEntries); + const { setCollapsibleEntries } = useUIState(); const { userValues, setUserValues, setSelectedEntries } = useInputsState(); const { setIsEditedRecord: setIsEdited } = useStatusState(); const { schema, setSchema } = useProfileState(); diff --git a/src/common/hooks/useRecordControls.ts b/src/common/hooks/useRecordControls.ts index 02cd577d..bcf24309 100644 --- a/src/common/hooks/useRecordControls.ts +++ b/src/common/hooks/useRecordControls.ts @@ -1,6 +1,5 @@ import { flushSync } from 'react-dom'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; import { postRecord, putRecord, @@ -28,8 +27,7 @@ import { RecordStatus, ResourceType } from '@common/constants/record.constants'; import { generateEditResourceUrl } from '@common/helpers/navigation.helper'; import { ApiErrorCodes, ExternalResourceIdType } from '@common/constants/api.constants'; import { checkHasErrorOfCodeType } from '@common/helpers/api.helper'; -import { useLoadingState, useStatusState, useProfileState, useInputsState } from '@src/store'; -import state from '@state'; +import { useLoadingState, useStatusState, useProfileState, useInputsState, useUIState } from '@src/store'; import { useRecordGeneration } from './useRecordGeneration'; import { useBackToSearchUri } from './useBackToSearchUri'; import { useContainerEvents } from './useContainerEvents'; @@ -52,10 +50,9 @@ export const useRecordControls = () => { const { setIsLoading } = useLoadingState(); const { resetUserValues, selectedRecordBlocks, setSelectedRecordBlocks, record, setRecord } = useInputsState(); const { setSelectedProfile } = useProfileState(); + const { setIsDuplicateImportedResourceModalOpen, setCurrentlyEditedEntityBfid, setCurrentlyPreviewedEntityBfid } = + useUIState(); const { setRecordStatus, setLastSavedRecordId, setIsEditedRecord: setIsEdited, addStatusMessage } = useStatusState(); - const setCurrentlyEditedEntityBfid = useSetRecoilState(state.ui.currentlyEditedEntityBfid); - const setCurrentlyPreviewedEntityBfid = useSetRecoilState(state.ui.currentlyPreviewedEntityBfid); - const setIsDuplicateImportedResourceModalOpen = useSetRecoilState(state.ui.isDuplicateImportedResourceModalOpen); const profile = PROFILE_BFIDS.MONOGRAPH; const currentRecordId = getRecordId(record); const { getProfiles } = useConfig(); diff --git a/src/components/AdvancedSearchModal/AdvancedSearchModal.tsx b/src/components/AdvancedSearchModal/AdvancedSearchModal.tsx index cf3ed735..040f9c33 100644 --- a/src/components/AdvancedSearchModal/AdvancedSearchModal.tsx +++ b/src/components/AdvancedSearchModal/AdvancedSearchModal.tsx @@ -1,6 +1,6 @@ import { FC, memo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { FormattedMessage, useIntl } from 'react-intl'; import { Modal } from '@components/Modal'; import { Input } from '@components/Input'; @@ -12,6 +12,7 @@ import { } from '@common/constants/search.constants'; import { formatRawQuery, generateSearchParamsState } from '@common/helpers/search.helper'; import { Select } from '@components/Select'; +import { useUIState } from '@src/store'; import state from '@state'; import './AdvancedSearchModal.scss'; @@ -29,7 +30,7 @@ type Props = { export const AdvancedSearchModal: FC = memo(({ clearValues }) => { const [, setSearchParams] = useSearchParams(); const { formatMessage } = useIntl(); - const [isOpen, setIsOpen] = useRecoilState(state.ui.isAdvancedSearchOpen); + const { isAdvancedSearchOpen: isOpen, setIsAdvancedSearchOpen: setIsOpen } = useUIState(); const setForceRefreshSearch = useSetRecoilState(state.search.forceRefresh); const [rawQuery, setRawQuery] = useState(DEFAULT_ADVANCED_SEARCH_QUERY); diff --git a/src/components/ComplexLookupField/MarcPreviewComplexLookup.tsx b/src/components/ComplexLookupField/MarcPreviewComplexLookup.tsx index 7ef8a8db..89add550 100644 --- a/src/components/ComplexLookupField/MarcPreviewComplexLookup.tsx +++ b/src/components/ComplexLookupField/MarcPreviewComplexLookup.tsx @@ -1,13 +1,11 @@ import { FC } from 'react'; -import { useRecoilValue } from 'recoil'; import { FormattedDate, FormattedMessage } from 'react-intl'; import { useSearchContext } from '@common/hooks/useSearchContext'; -import { useMarcPreviewState } from '@src/store'; +import { useMarcPreviewState, useUIState } from '@src/store'; import { SearchControlPane } from '@components/SearchControlPane'; import { MarcContent } from '@components/MarcContent'; import { Button, ButtonType } from '@components/Button'; import Times16 from '@src/assets/times-16.svg?react'; -import state from '@state'; import './MarcPreviewComplexLookup.scss'; type MarcPreviewComplexLookupProps = { @@ -16,7 +14,7 @@ type MarcPreviewComplexLookupProps = { export const MarcPreviewComplexLookup: FC = ({ onClose }) => { const { onAssignRecord } = useSearchContext(); - const isMarcPreviewOpen = useRecoilValue(state.ui.isMarcPreviewOpen); + const { isMarcPreviewOpen } = useUIState(); const { complexValue: marcPreviewData, metaData: marcPreviewMetadata } = useMarcPreviewState(); const renderCloseButton = () => ( diff --git a/src/components/ComplexLookupField/ModalComplexLookup.tsx b/src/components/ComplexLookupField/ModalComplexLookup.tsx index 09dc39b6..7991c26c 100644 --- a/src/components/ComplexLookupField/ModalComplexLookup.tsx +++ b/src/components/ComplexLookupField/ModalComplexLookup.tsx @@ -19,7 +19,7 @@ import { ComplexLookupSearchResults } from './ComplexLookupSearchResults'; import { MarcPreviewComplexLookup } from './MarcPreviewComplexLookup'; import { SEARCH_RESULTS_TABLE_CONFIG } from './configs'; import './ModalComplexLookup.scss'; -import { useMarcPreviewState } from '@src/store'; +import { useMarcPreviewState, useUIState } from '@src/store'; interface ModalComplexLookupProps { isOpen: boolean; @@ -51,10 +51,10 @@ export const ModalComplexLookup: FC = memo( const searchResultsFormatter = SEARCH_RESULTS_FORMATTER[assignEntityName] || SEARCH_RESULTS_FORMATTER.default; const buildSearchQuery = SEARCH_QUERY_BUILDER[assignEntityName] || SEARCH_QUERY_BUILDER.default; - const setIsMarcPreviewOpen = useSetRecoilState(state.ui.isMarcPreviewOpen); const setSearchQuery = useSetRecoilState(state.search.query); const clearSearchQuery = useResetRecoilState(state.search.query); const { getFacetsData, getSourceData } = useComplexLookupApi(api, filters); + const { setIsMarcPreviewOpen } = useUIState(); const { setComplexValue, resetComplexValue: resetMarcPreviewValue, diff --git a/src/components/DuplicateGroupContainer/DuplicateGroupContainer.tsx b/src/components/DuplicateGroupContainer/DuplicateGroupContainer.tsx index afc55052..01d5432a 100644 --- a/src/components/DuplicateGroupContainer/DuplicateGroupContainer.tsx +++ b/src/components/DuplicateGroupContainer/DuplicateGroupContainer.tsx @@ -1,12 +1,11 @@ import { FC, ReactNode } from 'react'; -import { Button } from '@components/Button'; -import state from '@state'; -import { useRecoilState } from 'recoil'; -import { IFields } from '@components/Fields'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { Button } from '@components/Button'; +import { IFields } from '@components/Fields'; import ArrowChevronUp from '@src/assets/arrow-chevron-up.svg?react'; import { deleteFromSetImmutable } from '@common/helpers/common.helper'; +import { useUIState } from '@src/store'; import './DuplicateGroupContainer.scss'; interface IDuplicateGroupContainer { @@ -22,7 +21,7 @@ export const DuplicateGroupContainer: FC = ({ generateComponent, groupClassName, }) => { - const [collapsedEntries, setCollapsedEntries] = useRecoilState(state.ui.collapsedEntries); + const { collapsedEntries, setCollapsedEntries } = useUIState(); const twinsAmount = twins.length; const visibleTwins = twins.filter(twinUuid => !collapsedEntries.has(twinUuid)); const isCollapsed = visibleTwins.length === 0; @@ -31,7 +30,6 @@ export const DuplicateGroupContainer: FC = ({ setCollapsedEntries(prev => { const twinsAndPrevCombined = new Set([...(twins ?? []), ...prev]); - // Can use .difference method of Set() once it's been available for some time return twinsAndPrevCombined.size === prev.size ? deleteFromSetImmutable(prev, twins) : twinsAndPrevCombined; }); diff --git a/src/components/EditControlPane/EditControlPane.tsx b/src/components/EditControlPane/EditControlPane.tsx index cec5b68e..93d362a8 100644 --- a/src/components/EditControlPane/EditControlPane.tsx +++ b/src/components/EditControlPane/EditControlPane.tsx @@ -1,5 +1,4 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; import { FormattedMessage } from 'react-intl'; import { Dropdown } from '@components/Dropdown'; import { DropdownItemType } from '@common/constants/uiElements.constants'; @@ -13,8 +12,7 @@ import { useRoutePathPattern } from '@common/hooks/useRoutePathPattern'; import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; import { useMarcData } from '@common/hooks/useMarcData'; import { getEditActionPrefix } from '@common/helpers/bibframe.helper'; -import { useLoadingState, useMarcPreviewState, useStatusState } from '@src/store'; -import state from '@state'; +import { useLoadingState, useMarcPreviewState, useStatusState, useUIState } from '@src/store'; import EyeOpen16 from '@src/assets/eye-open-16.svg?react'; import ExternalLink16 from '@src/assets/external-link-16.svg?react'; import Duplicate16 from '@src/assets/duplicate-16.svg?react'; @@ -24,7 +22,7 @@ import './EditControlPane.scss'; export const EditControlPane = () => { const isInCreateMode = useRoutePathPattern(RESOURCE_CREATE_URLS); const { isLoading } = useLoadingState(); - const currentlyEditedEntityBfid = useRecoilValue(state.ui.currentlyEditedEntityBfid); + const { currentlyEditedEntityBfid } = useUIState(); const { setRecordStatus } = useStatusState(); const { setBasicValue } = useMarcPreviewState(); const navigate = useNavigate(); diff --git a/src/components/EditPreview/EditPreview.tsx b/src/components/EditPreview/EditPreview.tsx index d4ecf269..50bba5ba 100644 --- a/src/components/EditPreview/EditPreview.tsx +++ b/src/components/EditPreview/EditPreview.tsx @@ -1,10 +1,8 @@ -import { useRecoilValue } from 'recoil'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Preview } from '@components/Preview'; import { Button, ButtonType } from '@components/Button'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; -import state from '@state'; import { useParams, useSearchParams } from 'react-router-dom'; import { QueryParams, RESOURCE_CREATE_URLS, ROUTES } from '@common/constants/routes.constants'; import { ResourceType } from '@common/constants/record.constants'; @@ -12,11 +10,11 @@ import { InstancesList } from '@components/InstancesList'; import { useRoutePathPattern } from '@common/hooks/useRoutePathPattern'; import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; import { checkIfRecordHasDependencies } from '@common/helpers/record.helper'; -import { useInputsState, useStatusState } from '@src/store'; +import { useInputsState, useStatusState, useUIState } from '@src/store'; import './EditPreview.scss'; export const EditPreview = () => { - const currentlyPreviewedEntityBfid = useRecoilValue(state.ui.currentlyPreviewedEntityBfid); + const { currentlyPreviewedEntityBfid } = useUIState(); const { isEditedRecord: isEdited } = useStatusState(); const { record } = useInputsState(); const isPositionedSecond = diff --git a/src/components/EditSection/EditSection.tsx b/src/components/EditSection/EditSection.tsx index dc5be928..ccbd4c84 100644 --- a/src/components/EditSection/EditSection.tsx +++ b/src/components/EditSection/EditSection.tsx @@ -1,7 +1,5 @@ import { useEffect, memo } from 'react'; -import { useRecoilValue, useRecoilState } from 'recoil'; import classNames from 'classnames'; -import state from '@state'; import { saveRecordLocally } from '@common/helpers/record.helper'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; import { AUTOSAVE_INTERVAL } from '@common/constants/storage.constants'; @@ -13,7 +11,7 @@ import { useServicesContext } from '@common/hooks/useServicesContext'; import { renderDrawComponent } from './renderDrawComponent'; import './EditSection.scss'; import { useRecordGeneration } from '@common/hooks/useRecordGeneration'; -import { useInputsState, useProfileState, useStatusState } from '@src/store'; +import { useInputsState, useProfileState, useStatusState, useUIState } from '@src/store'; export const EditSection = memo(() => { const { selectedEntriesService } = useServicesContext() as Required; @@ -22,9 +20,7 @@ export const EditSection = memo(() => { const { userValues, addUserValues, selectedRecordBlocks, record, selectedEntries, setSelectedEntries } = useInputsState(); const { isEditedRecord: isEdited, setIsEditedRecord: setIsEdited } = useStatusState(); - const [collapsedEntries, setCollapsedEntries] = useRecoilState(state.ui.collapsedEntries); - const collapsibleEntries = useRecoilValue(state.ui.collapsibleEntries); - const currentlyEditedEntityBfid = useRecoilValue(state.ui.currentlyEditedEntityBfid); + const { collapsedEntries, setCollapsedEntries, collapsibleEntries, currentlyEditedEntityBfid } = useUIState(); const { generateRecord } = useRecordGeneration(); useContainerEvents({ watchEditedState: true }); diff --git a/src/components/Fields/Fields.tsx b/src/components/Fields/Fields.tsx index e2f76df1..16ad63da 100644 --- a/src/components/Fields/Fields.tsx +++ b/src/components/Fields/Fields.tsx @@ -1,14 +1,12 @@ import classNames from 'classnames'; import { FC, memo, ReactElement, ReactNode } from 'react'; -import { useRecoilValue } from 'recoil'; import { AdvancedFieldType } from '@common/constants/uiControls.constants'; -import state from '@state'; import { IDrawComponent } from '@components/EditSection'; import { ENTITY_LEVEL } from '@common/constants/bibframe.constants'; import { DuplicateGroupContainer } from '@components/DuplicateGroupContainer'; import { ConditionalWrapper } from '@components/ConditionalWrapper'; import { DuplicateSubcomponentContainer } from '@components/DuplicateSubcomponentContainer'; -import { useInputsState, useProfileState } from '@src/store'; +import { useInputsState, useProfileState, useUIState } from '@src/store'; export type IFields = { uuid: string | null; @@ -38,7 +36,7 @@ export const Fields: FC = memo( scrollToEnabled = false, groupingDisabled = false, }) => { - const currentlyEditedEntityBfid = useRecoilValue(state.ui.currentlyEditedEntityBfid); + const { currentlyEditedEntityBfid } = useUIState(); const { schema } = useProfileState(); const { selectedEntries } = useInputsState(); diff --git a/src/components/ItemSearch/ItemSearch.tsx b/src/components/ItemSearch/ItemSearch.tsx index 12b38b70..25fbc52d 100644 --- a/src/components/ItemSearch/ItemSearch.tsx +++ b/src/components/ItemSearch/ItemSearch.tsx @@ -1,4 +1,3 @@ -import { useRecoilValue } from 'recoil'; import { FormattedMessage } from 'react-intl'; import { AdvancedSearchModal } from '@components/AdvancedSearchModal'; import { SEARCH_RESULTS_LIMIT } from '@common/constants/search.constants'; @@ -11,7 +10,7 @@ import { useLoadSearchResults } from '@common/hooks/useLoadSearchResults'; import { useSearchContext } from '@common/hooks/useSearchContext'; import { EmptyPlaceholder } from './SearchEmptyPlaceholder'; import './ItemSearch.scss'; -import state from '@state'; +import { useUIState } from '@src/store'; export const ItemSearch = () => { const { @@ -40,7 +39,7 @@ export const ItemSearch = () => { fetchData, onChangeSegment, } = useSearch(); - const isMarcPreviewOpen = useRecoilValue(state.ui.isMarcPreviewOpen); + const { isMarcPreviewOpen } = useUIState(); useLoadSearchResults(fetchData); diff --git a/src/components/ModalDuplicateImportedResource/ModalDuplicateImportedResource.tsx b/src/components/ModalDuplicateImportedResource/ModalDuplicateImportedResource.tsx index 593a40d3..850f5a6e 100644 --- a/src/components/ModalDuplicateImportedResource/ModalDuplicateImportedResource.tsx +++ b/src/components/ModalDuplicateImportedResource/ModalDuplicateImportedResource.tsx @@ -1,15 +1,12 @@ import { memo } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { Modal } from '@components/Modal'; -import { useRecoilState } from 'recoil'; -import state from '@state'; import { useContainerEvents } from '@common/hooks/useContainerEvents'; +import { useUIState } from '@src/store'; import './ModalDuplicateImportedResource.scss'; export const ModalDuplicateImportedResource = memo(() => { - const [isDuplicateImportedResourceModalOpen, setIsDuplicateImportedResourceModalOpen] = useRecoilState( - state.ui.isDuplicateImportedResourceModalOpen, - ); + const { isDuplicateImportedResourceModalOpen, setIsDuplicateImportedResourceModalOpen } = useUIState(); const { formatMessage } = useIntl(); const { dispatchNavigateToOriginEventWithFallback } = useContainerEvents(); diff --git a/src/components/Preview/Fields.tsx b/src/components/Preview/Fields.tsx index 042c128f..1592a63d 100644 --- a/src/components/Preview/Fields.tsx +++ b/src/components/Preview/Fields.tsx @@ -1,5 +1,4 @@ import { ReactNode } from 'react'; -import { useRecoilValue } from 'recoil'; import classNames from 'classnames'; import { ENTITY_LEVEL, GROUP_BY_LEVEL } from '@common/constants/bibframe.constants'; import { BFLITE_BFID_TO_BLOCK } from '@common/constants/bibframeMapping.constants'; @@ -10,8 +9,7 @@ import { getRecordId, getPreviewFieldsConditions } from '@common/helpers/record. import { getParentEntryUuid } from '@common/helpers/schema.helper'; import { useNavigateToEditPage } from '@common/hooks/useNavigateToEditPage'; import { ConditionalWrapper } from '@components/ConditionalWrapper'; -import { useInputsState, useProfileState, useStatusState } from '@src/store'; -import state from '@state'; +import { useInputsState, useProfileState, useStatusState, useUIState } from '@src/store'; import { Labels } from './Labels'; import { Values } from './Values'; @@ -76,7 +74,7 @@ export const Fields = ({ hideActions, forceRenderAllTopLevelEntities, }: FieldsProps) => { - const currentlyPreviewedEntityBfid = useRecoilValue(state.ui.currentlyPreviewedEntityBfid); + const { currentlyPreviewedEntityBfid } = useUIState(); const { userValues: userValuesFromState, record, selectedEntries } = useInputsState(); const { schema: schemaFromState } = useProfileState(); const { isEditedRecord: isEdited, setRecordStatus } = useStatusState(); diff --git a/src/components/SearchControls/SearchControls.tsx b/src/components/SearchControls/SearchControls.tsx index 97a547c3..cb01574f 100644 --- a/src/components/SearchControls/SearchControls.tsx +++ b/src/components/SearchControls/SearchControls.tsx @@ -17,6 +17,7 @@ import CaretDown from '@src/assets/caret-down.svg?react'; import XInCircle from '@src/assets/x-in-circle.svg?react'; import './SearchControls.scss'; import { Announcement } from '@components/Announcement/Announcement'; +import { useUIState } from '@src/store'; type Props = { submitSearch: VoidFunction; @@ -42,8 +43,8 @@ export const SearchControls: FC = ({ submitSearch, changeSegment, clearVa const setMessage = useSetRecoilState(state.search.message); const setNavigationState = useSetRecoilState(state.search.navigationState); const resetControls = useResetRecoilState(state.search.limiters); - const setIsAdvancedSearchOpen = useSetRecoilState(state.ui.isAdvancedSearchOpen); const setFacetsBySegments = useSetRecoilState(state.search.facetsBySegments); + const { isAdvancedSearchOpen, setIsAdvancedSearchOpen } = useUIState(); const [searchParams, setSearchParams] = useSearchParams(); const [announcementMessage, setAnnouncementMessage] = useState(''); const searchQueryParam = searchParams.get(SearchQueryParams.Query); @@ -144,15 +145,12 @@ export const SearchControls: FC = ({ submitSearch, changeSegment, clearVa > - setAnnouncementMessage('')} - /> + setAnnouncementMessage('')} /> {isVisibleAdvancedSearch && ( diff --git a/src/state/index.ts b/src/state/index.ts index e13e40fb..82f3c699 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,7 +1,5 @@ -import ui from './ui'; import search from './search'; export default { - ui, search, }; diff --git a/src/state/ui.ts b/src/state/ui.ts deleted file mode 100644 index c5f30411..00000000 --- a/src/state/ui.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { atom } from 'recoil'; - -const isAdvancedSearchOpen = atom({ - key: 'ui.isAdvancedSearchOpen', - default: false, -}); - -const isMarcPreviewOpen = atom({ - key: 'ui.isMarcPreviewOpen', - default: false, -}); - -const isDuplicateImportedResourceModalOpen = atom({ - key: 'ui.isDuplicateImportedResourceModalOpen', - default: false, -}); - -const collapsedEntries = atom>({ - key: 'ui.collapsedEntries', - default: new Set(), -}); - -const collapsibleEntries = atom>({ - key: 'ui.collapsibleEntries', - default: new Set(), -}); - -const currentlyEditedEntityBfid = atom>({ - key: 'ui.currentlyEditedEntityBfid', - default: new Set(), -}); - -const currentlyPreviewedEntityBfid = atom>({ - key: 'ui.currentlyPreviewedEntityBfid', - default: new Set(), -}); - -export default { - isAdvancedSearchOpen, - isMarcPreviewOpen, - collapsedEntries, - currentlyEditedEntityBfid, - currentlyPreviewedEntityBfid, - isDuplicateImportedResourceModalOpen, - collapsibleEntries, -}; diff --git a/src/store/index.ts b/src/store/index.ts index 33942ea7..ac9314b3 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,6 +5,7 @@ export * from './marcPreview'; export * from './profile'; export * from './inputs'; export * from './config'; +export * from './ui'; // Selector hooks export * from './selectors'; diff --git a/src/store/selectors.ts b/src/store/selectors.ts index 61896f82..bae8dac5 100644 --- a/src/store/selectors.ts +++ b/src/store/selectors.ts @@ -5,6 +5,7 @@ import { useMarcPreviewStore } from './marcPreview'; import { useProfileStore } from './profile'; import { useInputsStore } from './inputs'; import { useConfigStore } from './config'; +import { useUIStore } from './ui'; export const useStatusState = () => createSelectors(useStatusStore).use; export const useLoadingState = () => createSelectors(useLoadingStateStore).use; @@ -12,3 +13,4 @@ export const useMarcPreviewState = () => createSelectors(useMarcPreviewStore).us export const useProfileState = () => createSelectors(useProfileStore).use; export const useInputsState = () => createSelectors(useInputsStore).use; export const useConfigState = () => createSelectors(useConfigStore).use; +export const useUIState = () => createSelectors(useUIStore).use; diff --git a/src/store/ui.ts b/src/store/ui.ts index e69de29b..ef74edc8 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -0,0 +1,26 @@ +import { createBaseSlice, SliceState } from './utils/slice'; +import { generateStore, type StateCreatorTyped } from './utils/storeCreator'; + +export type UIEntries = Set; + +export type uiState = SliceState<'isAdvancedSearchOpen', boolean> & + SliceState<'isMarcPreviewOpen', boolean> & + SliceState<'isDuplicateImportedResourceModalOpen', boolean> & + SliceState<'collapsedEntries', UIEntries> & + SliceState<'collapsibleEntries', UIEntries> & + SliceState<'currentlyEditedEntityBfid', UIEntries> & + SliceState<'currentlyPreviewedEntityBfid', UIEntries>; + +const STORE_NAME = 'UI'; + +const uiStore: StateCreatorTyped = (...args) => ({ + ...createBaseSlice({ basic: 'isAdvancedSearchOpen' }, false)(...args), + ...createBaseSlice({ basic: 'isMarcPreviewOpen' }, false)(...args), + ...createBaseSlice({ basic: 'isDuplicateImportedResourceModalOpen' }, false)(...args), + ...createBaseSlice({ basic: 'collapsedEntries' }, new Set() as UIEntries)(...args), + ...createBaseSlice({ basic: 'collapsibleEntries' }, new Set() as UIEntries)(...args), + ...createBaseSlice({ basic: 'currentlyEditedEntityBfid' }, new Set() as UIEntries)(...args), + ...createBaseSlice({ basic: 'currentlyPreviewedEntityBfid' }, new Set() as UIEntries)(...args), +}); + +export const useUIStore = generateStore(uiStore, STORE_NAME); diff --git a/src/store/utils/slice.ts b/src/store/utils/slice.ts index 1e7732d7..e49edcc5 100644 --- a/src/store/utils/slice.ts +++ b/src/store/utils/slice.ts @@ -5,7 +5,7 @@ type Capitalize = S extends `${infer F}${infer R}` ? `${Upperc export type SliceState = { [P in K]: V; } & { - [P in `set${Capitalize}`]: (value: V) => void; + [P in `set${Capitalize}`]: (value: V | SetState) => void; } & { [P in `reset${Capitalize}`]: () => void; } & Partial<{ @@ -54,8 +54,16 @@ export const createBaseSlice = - set({ [keys.basic]: updatedValue } as any, false, `set${capitalizedTitle}`), + [`set${capitalizedTitle}`]: (updatedValue: V | SetState) => + set( + state => + ({ + [keys.basic]: + typeof updatedValue === 'function' ? (updatedValue as SetState)(state[keys.basic]) : updatedValue, + }) as any, + false, + `set${capitalizedTitle}`, + ), [`reset${capitalizedTitle}`]: () => set({ [keys.basic]: initialValue } as any, false, `reset${capitalizedTitle}`), } as SliceState; diff --git a/src/test/__tests__/components/AdvancedSearchModal.test.tsx b/src/test/__tests__/components/AdvancedSearchModal.test.tsx index d9ec7664..a344d4ab 100644 --- a/src/test/__tests__/components/AdvancedSearchModal.test.tsx +++ b/src/test/__tests__/components/AdvancedSearchModal.test.tsx @@ -5,7 +5,8 @@ import { AdvancedSearchModal } from '@components/AdvancedSearchModal'; import { createModalContainer } from '@src/test/__mocks__/common/misc/createModalContainer.mock'; import * as SearchHelper from '@common/helpers/search.helper'; import { SearchQueryParams } from '@common/constants/routes.constants'; -import state from '@state'; +import { setInitialGlobalState } from '@src/test/__mocks__/store'; +import { useUIStore } from '@src/store'; const setSearchParams = jest.fn(); const clearValues = jest.fn(); @@ -20,9 +21,16 @@ describe('AdvancedSearchModal', () => { createModalContainer(); }); - beforeEach(() => - render( - snapshot.set(state.ui.isAdvancedSearchOpen, true)}> + beforeEach(() => { + setInitialGlobalState([ + { + store: useUIStore, + state: { isAdvancedSearchOpen: true }, + }, + ]); + + return render( + { ])} /> , - ), - ); + ); + }); test('toggles isOpen', () => { fireEvent.click(screen.getByTestId('modal-button-cancel')); diff --git a/src/test/__tests__/components/EditControlPane.test.tsx b/src/test/__tests__/components/EditControlPane.test.tsx index b6e21e07..71b251c7 100644 --- a/src/test/__tests__/components/EditControlPane.test.tsx +++ b/src/test/__tests__/components/EditControlPane.test.tsx @@ -5,16 +5,22 @@ import { RouterProvider, createMemoryRouter } from 'react-router'; import { RecoilRoot } from 'recoil'; import * as recordsApi from '@common/api/records.api'; import { ROUTES } from '@common/constants/routes.constants'; -import state from '@state'; import { PROFILE_BFIDS } from '@common/constants/bibframe.constants'; +import { setInitialGlobalState } from '@src/test/__mocks__/store'; +import { useUIStore } from '@src/store'; const renderWrapper = (withDropdown = true) => { const path = withDropdown ? ROUTES.RESOURCE_EDIT.uri : ROUTES.RESOURCE_CREATE.uri; + setInitialGlobalState([ + { + store: useUIStore, + state: { currentlyEditedEntityBfid: new Set([PROFILE_BFIDS.INSTANCE]) }, + }, + ]); + return render( - snapshot.set(state.ui.currentlyEditedEntityBfid, new Set([PROFILE_BFIDS.INSTANCE]))} - > + { store: useInputsStore, state: { record: {} }, }, + { + store: useUIStore, + state: { currentlyPreviewedEntityBfid: new Set([PROFILE_BFIDS.INSTANCE]) }, + }, ]); render( - { - snapshot.set(state.ui.currentlyPreviewedEntityBfid, new Set([PROFILE_BFIDS.INSTANCE])); - }} - > + }], { initialEntries: ['/resources/create?type=work'], diff --git a/src/test/__tests__/components/EditSection.test.tsx b/src/test/__tests__/components/EditSection.test.tsx index 81403c8d..34925818 100644 --- a/src/test/__tests__/components/EditSection.test.tsx +++ b/src/test/__tests__/components/EditSection.test.tsx @@ -8,8 +8,7 @@ import * as RecordHelper from '@common/helpers/record.helper'; import { AdvancedFieldType } from '@common/constants/uiControls.constants'; import { ServicesProvider } from '@src/providers'; import { routes } from '@src/App'; -import { useInputsStore, useProfileStore } from '@src/store'; -import state from '@state'; +import { useInputsStore, useProfileStore, useUIStore } from '@src/store'; const userValues = { uuid3: { @@ -201,14 +200,14 @@ describe('EditSection', () => { store: useInputsStore, state: { userValues, selectedEntries: ['uuid7'] }, }, + { + store: useUIStore, + state: { currentlyEditedEntityBfid: new Set(['uuid2Bfid']) }, + }, ]); return render( - { - snapshot.set(state.ui.currentlyEditedEntityBfid, new Set(['uuid2Bfid'])); - }} - > + ({ IS_EMBEDDED_MODE: false })); @@ -40,14 +39,14 @@ describe('MarcPreviewComplexLookup', () => { store: useMarcPreviewStore, state: { complexValue: marcPreviewData, metaData: marcPreviewMetadata }, }, + { + store: useUIStore, + state: { isMarcPreviewOpen }, + }, ]); return render( - { - set(state.ui.isMarcPreviewOpen, isMarcPreviewOpen); - }} - > + , ); diff --git a/src/test/__tests__/components/Preview.test.tsx b/src/test/__tests__/components/Preview.test.tsx index 664636c5..e3ecf52b 100644 --- a/src/test/__tests__/components/Preview.test.tsx +++ b/src/test/__tests__/components/Preview.test.tsx @@ -1,7 +1,6 @@ import { Preview } from '@components/Preview'; -import { useInputsStore, useProfileStore } from '@src/store'; +import { useInputsStore, useProfileStore, useUIStore } from '@src/store'; import { setInitialGlobalState } from '@src/test/__mocks__/store'; -import state from '@state'; import { render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import { RecoilRoot } from 'recoil'; @@ -50,14 +49,14 @@ describe('Preview', () => { store: useInputsStore, state: { userValues }, }, + { + store: useUIStore, + state: { currentlyPreviewedEntityBfid: new Set(['uuid1Bfid']) }, + }, ]); return render( - { - snapshot.set(state.ui.currentlyPreviewedEntityBfid, new Set(['uuid1Bfid'])); - }} - > + diff --git a/src/test/__tests__/store/utils/slice.test.ts b/src/test/__tests__/store/utils/slice.test.ts index d4fac5d0..6957ac7f 100644 --- a/src/test/__tests__/store/utils/slice.test.ts +++ b/src/test/__tests__/store/utils/slice.test.ts @@ -1,4 +1,4 @@ -import { createBaseSlice } from '@src/store/utils/slice'; +import { createBaseSlice, SliceState } from '@src/store/utils/slice'; describe('createBaseSlice', () => { type KeyBasic = 'testKey'; @@ -14,11 +14,37 @@ describe('createBaseSlice', () => { store = {}; }); - test('"set" method updates the state', () => { - const baseSlice = createBaseSlice(keys, initialValue)(set, get, store); + describe('"set" method', () => { + let baseSlice: SliceState; + const initialState = { [keys.basic]: initialValue }; + + beforeEach(() => { + baseSlice = createBaseSlice(keys, initialValue)(set, get, store); + }); + + test('updates state with direct value', () => { + const newValue = 'newValue'; + + baseSlice.setTestKey(newValue); + + const stateUpdater = set.mock.calls[0][0]; + const newState = stateUpdater(initialState); - baseSlice.setTestKey('newValue'); - expect(set).toHaveBeenCalledWith({ testKey: 'newValue' }, false, 'setTestKey'); + expect(newState).toEqual({ [keys.basic]: newValue }); + expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'setTestKey'); + }); + + test('updates state with updater function', () => { + baseSlice.setTestKey((prevValue: string) => `${prevValue}_updated`); + + const stateUpdater = set.mock.calls[0][0]; + const newState = stateUpdater(initialState); + + expect(newState).toEqual({ + [keys.basic]: `${initialValue}_updated`, + }); + expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'setTestKey'); + }); }); test('"reset" method resets the state to initial value', () => { diff --git a/src/types/store.d.ts b/src/types/store.d.ts new file mode 100644 index 00000000..4f08b9b5 --- /dev/null +++ b/src/types/store.d.ts @@ -0,0 +1,3 @@ +type UIEntries = import('@src/store').UIEntries; + +type SetState = (value: V) => V; diff --git a/src/views/Edit/Edit.tsx b/src/views/Edit/Edit.tsx index e8da6ca8..a4e36378 100644 --- a/src/views/Edit/Edit.tsx +++ b/src/views/Edit/Edit.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; import { EditSection } from '@components/EditSection'; import { BibframeEntities, PROFILE_BFIDS } from '@common/constants/bibframe.constants'; import { DEFAULT_RECORD_ID } from '@common/constants/storage.constants'; @@ -15,8 +14,7 @@ import { RecordStatus, ResourceType } from '@common/constants/record.constants'; import { EditPreview } from '@components/EditPreview'; import { QueryParams } from '@common/constants/routes.constants'; import { ViewMarcModal } from '@components/ViewMarcModal'; -import { useLoadingState, useMarcPreviewState, useStatusState } from '@src/store'; -import state from '@state'; +import { useLoadingState, useMarcPreviewState, useStatusState, useUIState } from '@src/store'; import './Edit.scss'; const ignoreLoadingStatuses = [RecordStatus.saveAndClose, RecordStatus.saveAndKeepEditing]; @@ -29,8 +27,7 @@ export const Edit = () => { const { basicValue: marcPreviewData, resetBasicValue: resetMarcPreviewData } = useMarcPreviewState(); const recordStatusType = recordStatus?.type; const { setIsLoading } = useLoadingState(); - const setCurrentlyEditedEntityBfid = useSetRecoilState(state.ui.currentlyEditedEntityBfid); - const setCurrentlyPreviewedEntityBfid = useSetRecoilState(state.ui.currentlyPreviewedEntityBfid); + const { setCurrentlyEditedEntityBfid, setCurrentlyPreviewedEntityBfid } = useUIState(); const [queryParams] = useSearchParams(); useResetRecordStatus();