diff --git a/src/common/hooks/useCommonStatus.ts b/src/common/hooks/useCommonStatus.ts index 59865ec8..c67cd44c 100644 --- a/src/common/hooks/useCommonStatus.ts +++ b/src/common/hooks/useCommonStatus.ts @@ -2,11 +2,11 @@ import { UserNotificationFactory } from '@common/services/userNotification'; import { useStatusState } from '@src/store'; export const useCommonStatus = () => { - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); return { set: (l10nId: string, type: StatusType) => { - addStatusMessage?.(UserNotificationFactory.createMessage(type, l10nId)); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(type, l10nId)); }, }; }; diff --git a/src/common/hooks/useFetchSearchData.ts b/src/common/hooks/useFetchSearchData.ts index cc445039..7f0712a2 100644 --- a/src/common/hooks/useFetchSearchData.ts +++ b/src/common/hooks/useFetchSearchData.ts @@ -25,7 +25,7 @@ export const useFetchSearchData = () => { } = useSearchContext(); const { setIsLoading } = useLoadingState(); const { setMessage, data, setData, resetData, setPageMetadata } = useSearchState(); - const { addStatusMessage, resetStatusMessages } = useStatusState(); + const { addStatusMessagesItem, resetStatusMessages } = useStatusState(); const validateAndNormalizeQuery = useCallback( (type: SearchIdentifiers, query: string) => { @@ -177,7 +177,7 @@ export const useFetchSearchData = () => { setData(content); setPageMetadata({ totalPages, totalElements: totalRecords, prev, next }); } catch { - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); } finally { setIsLoading(false); } diff --git a/src/common/hooks/useMarcData.ts b/src/common/hooks/useMarcData.ts index 18aba913..5207dca2 100644 --- a/src/common/hooks/useMarcData.ts +++ b/src/common/hooks/useMarcData.ts @@ -5,7 +5,7 @@ import { useLoadingState, useStatusState } from '@src/store'; export const useMarcData = (setMarcPreviewData: (value: any) => void) => { const { setIsLoading } = useLoadingState(); - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); const fetchMarcData = async (recordId?: string, endpointUrl?: string): Promise => { if (!recordId) return undefined; @@ -19,7 +19,7 @@ export const useMarcData = (setMarcPreviewData: (value: any) => void) => { setMarcPreviewData(marcData); } catch (error) { - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantLoadMarc')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantLoadMarc')); } finally { setIsLoading(false); } diff --git a/src/common/hooks/useProcessedRecordAndSchema.hook.ts b/src/common/hooks/useProcessedRecordAndSchema.hook.ts index f23d01c3..deabe30b 100644 --- a/src/common/hooks/useProcessedRecordAndSchema.hook.ts +++ b/src/common/hooks/useProcessedRecordAndSchema.hook.ts @@ -21,7 +21,7 @@ type IGetProcessedRecordAndSchema = { }; export const useProcessedRecordAndSchema = () => { - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); const { setRecord } = useInputsState(); const { formatMessage } = useIntl(); const { userValuesService, schemaWithDuplicatesService, recordNormalizingService, recordToSchemaMappingService } = @@ -72,7 +72,7 @@ export const useProcessedRecordAndSchema = () => { } catch (error) { console.error(error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorLoadingResource')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorLoadingResource')); } return { @@ -87,7 +87,7 @@ export const useProcessedRecordAndSchema = () => { recordToSchemaMappingService, schemaWithDuplicatesService, setRecord, - addStatusMessage, + addStatusMessagesItem, userValuesService, ], ); diff --git a/src/common/hooks/useRecordControls.ts b/src/common/hooks/useRecordControls.ts index e903c332..75071152 100644 --- a/src/common/hooks/useRecordControls.ts +++ b/src/common/hooks/useRecordControls.ts @@ -53,7 +53,7 @@ export const useRecordControls = () => { const { setSelectedProfile } = useProfileState(); const { setIsDuplicateImportedResourceModalOpen, setCurrentlyEditedEntityBfid, setCurrentlyPreviewedEntityBfid } = useUIState(); - const { setRecordStatus, setLastSavedRecordId, setIsEditedRecord: setIsEdited, addStatusMessage } = useStatusState(); + const { setRecordStatus, setLastSavedRecordId, setIsEditedRecord: setIsEdited, addStatusMessagesItem } = useStatusState(); const profile = PROFILE_BFIDS.MONOGRAPH; const currentRecordId = getRecordId(record); const { getProfiles } = useConfig(); @@ -124,7 +124,7 @@ export const useRecordControls = () => { dispatchUnblockEvent(); !asRefToNewRecord && setRecord(parsedResponse); - addStatusMessage?.( + addStatusMessagesItem?.( UserNotificationFactory.createMessage(StatusType.success, recordId ? 'ld.rdUpdateSuccess' : 'ld.rdSaveSuccess'), ); @@ -171,7 +171,7 @@ export const useRecordControls = () => { } catch (error) { console.error('Cannot save the resource description', error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSaveRd')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSaveRd')); } finally { setIsLoading(false); } @@ -207,13 +207,13 @@ export const useRecordControls = () => { await deleteRecordRequest(currentRecordId as unknown as string); deleteRecordLocally(profile, currentRecordId as unknown as string); discardRecord(); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.success, 'ld.rdDeleted')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.success, 'ld.rdDeleted')); navigate(ROUTES.SEARCH.uri); } catch (error) { console.error('Cannot delete the resource description', error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantDeleteRd')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantDeleteRd')); } }; @@ -224,7 +224,7 @@ export const useRecordControls = () => { const contents = record?.resource?.[uriSelector]; if (!contents) { - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSelectReferenceContents')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantSelectReferenceContents')); return navigate(ROUTES.RESOURCE_CREATE.uri); } @@ -244,7 +244,7 @@ export const useRecordControls = () => { } catch (e) { console.error('Error fetching record and selecting entity values: ', e); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); } }; @@ -262,7 +262,7 @@ export const useRecordControls = () => { return recordData; } catch (_err) { - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, errorMessage ?? 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, errorMessage ?? 'ld.errorFetching')); } }; @@ -289,7 +289,7 @@ export const useRecordControls = () => { if (checkHasErrorOfCodeType(err as ApiError, ApiErrorCodes.AlreadyExists)) { setIsDuplicateImportedResourceModalOpen(true); } else { - addStatusMessage?.( + addStatusMessagesItem?.( UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetchingExternalResourceForEditing'), ); } diff --git a/src/common/hooks/useSearchFiltersData.ts b/src/common/hooks/useSearchFiltersData.ts index a147b1be..1ba88b08 100644 --- a/src/common/hooks/useSearchFiltersData.ts +++ b/src/common/hooks/useSearchFiltersData.ts @@ -10,7 +10,7 @@ const DEFAULT_SEARCH_FACETS_QUERY = 'id=*'; export const useSearchFiltersData = () => { const { selectedFacetsGroups, setSelectedFacetsGroups, resetSelectedFacetsGroups, setFacetsData, setSourceData } = useSearchState(); - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); useEffect(() => { return resetSelectedFacetsGroups(); @@ -37,7 +37,7 @@ export const useSearchFiltersData = () => { } catch (error) { console.error(error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); } }; @@ -56,7 +56,7 @@ export const useSearchFiltersData = () => { } catch (error) { console.error(error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); } }; diff --git a/src/components/EditSection/EditSection.tsx b/src/components/EditSection/EditSection.tsx index da41ece8..b704c47e 100644 --- a/src/components/EditSection/EditSection.tsx +++ b/src/components/EditSection/EditSection.tsx @@ -20,7 +20,7 @@ export const EditSection = memo(() => { const { selectedEntriesService } = useServicesContext() as Required; const { selectedProfile, initialSchemaKey } = useProfileState(); const resourceTemplates = selectedProfile?.json.Profile.resourceTemplates; - const { userValues, addUserValues, selectedRecordBlocks, record, selectedEntries, setSelectedEntries } = + const { userValues, addUserValuesItem, selectedRecordBlocks, record, selectedEntries, setSelectedEntries } = useInputsState(); const { isEditedRecord: isEdited, setIsEditedRecord: setIsEdited } = useStatusState(); const { collapsedEntries, setCollapsedEntries, collapsibleEntries, currentlyEditedEntityBfid } = useUIState(); @@ -50,7 +50,7 @@ export const EditSection = memo(() => { const debouncedAddUserValues = useRef( debounce((value: UserValues) => { - addUserValues?.(value); + addUserValuesItem?.(value); }, USER_INPUT_DELAY), ).current; diff --git a/src/components/SearchResultEntry/SearchResultEntry.tsx b/src/components/SearchResultEntry/SearchResultEntry.tsx index fb1e60ee..a7808f75 100644 --- a/src/components/SearchResultEntry/SearchResultEntry.tsx +++ b/src/components/SearchResultEntry/SearchResultEntry.tsx @@ -60,7 +60,7 @@ export const SearchResultEntry: FC = ({ instances, ...restOfW const { navigationState } = useSearchState(); const [isOpen, setIsOpen] = useState(true); const { setIsLoading } = useLoadingState(); - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); const { previewContent } = useInputsState(); const toggleIsOpen = () => setIsOpen(!isOpen); const { fetchRecord } = useRecordControls(); @@ -72,7 +72,7 @@ export const SearchResultEntry: FC = ({ instances, ...restOfW } catch (error) { console.error(error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorFetching')); } finally { setIsLoading(false); } diff --git a/src/components/SimpleLookupField/SimpleLookupField.tsx b/src/components/SimpleLookupField/SimpleLookupField.tsx index c56947f5..b461db19 100644 --- a/src/components/SimpleLookupField/SimpleLookupField.tsx +++ b/src/components/SimpleLookupField/SimpleLookupField.tsx @@ -43,7 +43,7 @@ export const SimpleLookupField: FC = ({ const { getLookupData, loadLookupData } = useSimpleLookupData(); const loadedOptions = getLookupData()?.[uri] || []; const options = filterLookupOptionsByParentBlock(loadedOptions, propertyUri, parentBlockUri); - const { addStatusMessage } = useStatusState(); + const { addStatusMessagesItem } = useStatusState(); const { simpleLookupRef, forceDisplayOptionsAtTheTop } = useSimpleLookupObserver(); const [localValue, setLocalValue] = useState( @@ -65,7 +65,7 @@ export const SimpleLookupField: FC = ({ } catch (error) { console.error('Cannot load data for the Lookup:', error); - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantLoadSimpleLookupData')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.cantLoadSimpleLookupData')); } finally { setIsLoading(false); } diff --git a/src/store/stores/config.ts b/src/store/stores/config.ts index d20bc613..ae33cad2 100644 --- a/src/store/stores/config.ts +++ b/src/store/stores/config.ts @@ -1,8 +1,8 @@ import { LOCALES } from '@common/i18n/locales'; import { OKAPI_CONFIG } from '@common/constants/api.constants'; import { localStorageService } from '@common/services/storage'; -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { type SliceState } from '../utils/slice'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; type Locale = (typeof LOCALES)[keyof typeof LOCALES]; type CustomEvents = Record | null; @@ -13,13 +13,16 @@ export type ConfigState = SliceState<'locale', Locale> & const STORE_NAME = 'Config'; -const configStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice( - { basic: 'locale' }, - localStorageService.deserialize(OKAPI_CONFIG)?.locale || LOCALES.ENGLISH_US, - )(...args), - ...createBaseSlice({ basic: 'customEvents' }, null as CustomEvents)(...args), - ...createBaseSlice({ basic: 'hasNavigationOrigin' }, false)(...args), -}); +const sliceConfigs: SliceConfigs = { + locale: { + initialValue: localStorageService.deserialize(OKAPI_CONFIG)?.locale || LOCALES.ENGLISH_US, + }, + customEvents: { + initialValue: null as CustomEvents, + }, + hasNavigationOrigin: { + initialValue: false, + }, +}; -export const useConfigStore = generateStore(configStore, STORE_NAME); +export const useConfigStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/inputs.ts b/src/store/stores/inputs.ts index 9e92d67d..6a706933 100644 --- a/src/store/stores/inputs.ts +++ b/src/store/stores/inputs.ts @@ -1,5 +1,5 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; export type RecordState = RecordEntry | null; export type SelectedRecordBlocksState = SelectedRecordBlocks | undefined; @@ -13,12 +13,24 @@ export type InputsState = SliceState<'userValues', UserValues> & const STORE_NAME = 'Inputs'; -const inputsStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'userValues' }, {} as UserValues, true)(...args), - ...createBaseSlice({ basic: 'previewContent' }, [] as PreviewContent[])(...args), - ...createBaseSlice({ basic: 'record' }, null as RecordState)(...args), - ...createBaseSlice({ basic: 'selectedRecordBlocks' }, undefined as SelectedRecordBlocksState)(...args), - ...createBaseSlice({ basic: 'selectedEntries' }, [] as SelectedEntriesState)(...args), -}); +const sliceConfigs: SliceConfigs = { + userValues: { + initialValue: {}, + canAddSingleItem: true, + singleItem: { type: {} as UserValue }, + }, + previewContent: { + initialValue: [], + }, + record: { + initialValue: null, + }, + selectedRecordBlocks: { + initialValue: undefined, + }, + selectedEntries: { + initialValue: [], + }, +}; -export const useInputsStore = generateStore(inputsStore, STORE_NAME); +export const useInputsStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/loadingState.ts b/src/store/stores/loadingState.ts index c3ac0ec1..a7ab0ac9 100644 --- a/src/store/stores/loadingState.ts +++ b/src/store/stores/loadingState.ts @@ -1,12 +1,14 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; export type LoadingState = SliceState<'isLoading', boolean>; const STORE_NAME = 'Loading'; -const loadingStateStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'isLoading' }, false)(...args), -}); +const sliceConfigs: SliceConfigs = { + isLoading: { + initialValue: false, + }, +}; -export const useLoadingStateStore = generateStore(loadingStateStore, STORE_NAME); +export const useLoadingStateStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/marcPreview.ts b/src/store/stores/marcPreview.ts index 1e85bc08..87fafd1e 100644 --- a/src/store/stores/marcPreview.ts +++ b/src/store/stores/marcPreview.ts @@ -1,5 +1,5 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; type MarcPreviewData = MarcDTO | null; type MarcPreviewMetaData = MarcPreviewMetadata | null; @@ -10,10 +10,16 @@ export type MarcPreviewState = SliceState<'basicValue', any> & const STORE_NAME = 'MarcPreview'; -const marcPreviewStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'basicValue' }, null)(...args), - ...createBaseSlice({ basic: 'complexValue' }, null as MarcPreviewData)(...args), - ...createBaseSlice({ basic: 'metaData' }, null as MarcPreviewMetaData)(...args), -}); +const sliceConfigs: SliceConfigs = { + basicValue: { + initialValue: null, + }, + complexValue: { + initialValue: null, + }, + metaData: { + initialValue: null, + }, +}; -export const useMarcPreviewStore = generateStore(marcPreviewStore, STORE_NAME); +export const useMarcPreviewStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/profile.ts b/src/store/stores/profile.ts index 59629eff..791b62d7 100644 --- a/src/store/stores/profile.ts +++ b/src/store/stores/profile.ts @@ -1,5 +1,5 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; type SelectedProfileType = ProfileEntry | null; type PreparedFieldsType = ResourceTemplates | null; @@ -13,12 +13,22 @@ export type ProfileState = SliceState<'profiles', ProfileEntry[]> & const STORE_NAME = 'Profile'; -const profileStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'profiles' }, [] as ProfileEntry[])(...args), - ...createBaseSlice({ basic: 'selectedProfile' }, null as SelectedProfileType)(...args), - ...createBaseSlice({ basic: 'preparedFields' }, null as PreparedFieldsType)(...args), - ...createBaseSlice({ basic: 'initialSchemaKey' }, null as InitialSchemaKeyType)(...args), - ...createBaseSlice({ basic: 'schema' }, new Map())(...args), -}); +const sliceConfigs: SliceConfigs = { + profiles: { + initialValue: [], + }, + selectedProfile: { + initialValue: null, + }, + preparedFields: { + initialValue: null, + }, + initialSchemaKey: { + initialValue: null, + }, + schema: { + initialValue: new Map(), + }, +}; -export const useProfileStore = generateStore(profileStore, STORE_NAME); +export const useProfileStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/search.ts b/src/store/stores/search.ts index 848b5e3a..121e9e99 100644 --- a/src/store/stores/search.ts +++ b/src/store/stores/search.ts @@ -3,8 +3,8 @@ import { DEFAULT_SEARCH_BY, DEFAULT_SEARCH_LIMITERS, } from '@common/constants/search.constants'; -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { type SliceState } from '../utils/slice'; +import { createStoreFactory, SliceConfigs } from '../utils/createStoreFactory'; type Data = null | WorkAsSearchResultDTO[]; type SourceData = SourceDataDTO | null; @@ -24,19 +24,43 @@ export type SearchState = SliceState<'query', string> & const STORE_NAME = 'Search'; -const searchStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'query' }, '')(...args), - ...createBaseSlice({ basic: 'message' }, '')(...args), - ...createBaseSlice({ basic: 'searchBy' }, DEFAULT_SEARCH_BY as SearchIdentifiers)(...args), - ...createBaseSlice({ basic: 'data' }, null as Data)(...args), - ...createBaseSlice({ basic: 'facets' }, DEFAULT_SEARCH_LIMITERS as Limiters)(...args), - ...createBaseSlice({ basic: 'navigationState' }, {} as SearchParamsState)(...args), - ...createBaseSlice({ basic: 'forceRefresh' }, false)(...args), - ...createBaseSlice({ basic: 'pageMetadata' }, { totalElements: 0, totalPages: 0 } as PageMetadata)(...args), - ...createBaseSlice({ basic: 'facetsBySegments' }, DEFAULT_FACET_BY_SEGMENT as FacetsBySegments)(...args), - ...createBaseSlice({ basic: 'sourceData' }, null as SourceData)(...args), - ...createBaseSlice({ basic: 'selectedFacetsGroups' }, [] as string[])(...args), - ...createBaseSlice({ basic: 'facetsData' }, {} as FacetsDTO)(...args), -}); +const sliceConfigs: SliceConfigs = { + query: { + initialValue: '', + }, + message: { + initialValue: '', + }, + searchBy: { + initialValue: DEFAULT_SEARCH_BY, + }, + data: { + initialValue: null, + }, + facets: { + initialValue: DEFAULT_SEARCH_LIMITERS, + }, + navigationState: { + initialValue: {}, + }, + forceRefresh: { + initialValue: false, + }, + pageMetadata: { + initialValue: { totalElements: 0, totalPages: 0 }, + }, + facetsBySegments: { + initialValue: DEFAULT_FACET_BY_SEGMENT, + }, + sourceData: { + initialValue: null, + }, + selectedFacetsGroups: { + initialValue: [], + }, + facetsData: { + initialValue: {}, + }, +}; -export const useSearchStore = generateStore(searchStore, STORE_NAME); +export const useSearchStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/status.ts b/src/store/stores/status.ts index 7b7ded5b..dd01c2e6 100644 --- a/src/store/stores/status.ts +++ b/src/store/stores/status.ts @@ -1,24 +1,30 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; type LastSavedRecordId = string | null; export type StatusState = SliceState<'lastSavedRecordId', LastSavedRecordId> & SliceState<'isEditedRecord', boolean> & SliceState<'recordStatus', RecordStatus> & - SliceState<'statusMessages', StatusEntry[], 'statusMessage', StatusEntry>; + SliceState<'statusMessages', StatusEntry[], StatusEntry>; const STORE_NAME = 'Status'; -const statusStore: StateCreatorTyped = (...args) => ({ - ...createBaseSlice({ basic: 'lastSavedRecordId' }, null as LastSavedRecordId)(...args), - ...createBaseSlice({ basic: 'isEditedRecord' }, false)(...args), - ...createBaseSlice({ basic: 'recordStatus' }, { type: undefined } as RecordStatus)(...args), - ...createBaseSlice<'statusMessages', StatusEntry[], 'statusMessage', StatusEntry>( - { basic: 'statusMessages', singleItem: 'statusMessage' }, - [] as StatusEntry[], - true, - )(...args), -}); +const sliceConfigs: SliceConfigs = { + lastSavedRecordId: { + initialValue: null, + }, + isEditedRecord: { + initialValue: false, + }, + recordStatus: { + initialValue: { type: undefined }, + }, + statusMessages: { + initialValue: [], + singleItem: { type: {} }, + canAddSingleItem: true, + }, +}; -export const useStatusStore = generateStore(statusStore, STORE_NAME); +export const useStatusStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/stores/ui.ts b/src/store/stores/ui.ts index 5268f286..6e0b99ff 100644 --- a/src/store/stores/ui.ts +++ b/src/store/stores/ui.ts @@ -1,9 +1,9 @@ -import { createBaseSlice, SliceState } from '../utils/slice'; -import { generateStore, type StateCreatorTyped } from '../utils/storeCreator'; +import { createStoreFactory, type SliceConfigs } from '../utils/createStoreFactory'; +import { type SliceState } from '../utils/slice'; export type UIEntries = Set; -export type uiState = SliceState<'isAdvancedSearchOpen', boolean> & +export type UIState = SliceState<'isAdvancedSearchOpen', boolean> & SliceState<'isMarcPreviewOpen', boolean> & SliceState<'isDuplicateImportedResourceModalOpen', boolean> & SliceState<'collapsedEntries', UIEntries> & @@ -13,14 +13,28 @@ export type uiState = SliceState<'isAdvancedSearchOpen', boolean> & 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), -}); +const sliceConfigs: SliceConfigs = { + isAdvancedSearchOpen: { + initialValue: false, + }, + isMarcPreviewOpen: { + initialValue: false, + }, + isDuplicateImportedResourceModalOpen: { + initialValue: false, + }, + collapsedEntries: { + initialValue: new Set(), + }, + collapsibleEntries: { + initialValue: new Set(), + }, + currentlyEditedEntityBfid: { + initialValue: new Set(), + }, + currentlyPreviewedEntityBfid: { + initialValue: new Set(), + }, +}; -export const useUIStore = generateStore(uiStore, STORE_NAME); +export const useUIStore = createStoreFactory(sliceConfigs, STORE_NAME); diff --git a/src/store/utils/createStoreFactory.ts b/src/store/utils/createStoreFactory.ts new file mode 100644 index 00000000..6f3311f8 --- /dev/null +++ b/src/store/utils/createStoreFactory.ts @@ -0,0 +1,41 @@ +import { createBaseSlice, type SliceState } from '../utils/slice'; +import { generateStore, type StateCreatorTyped } from './storeCreator'; + +type SliceConfig = { + initialValue: Value; + singleItem?: { + type: SingleItemType; + }; + canAddSingleItem?: boolean; +}; + +export type SliceConfigs = Record; + +type InferSliceState = { + [K in keyof T]: T[K] extends SliceConfig + ? T[K]['singleItem'] extends { type: any } + ? SliceState + : SliceState + : never; +}[keyof T]; + +export function createStoreFactory(configs: T, storeName: string) { + type State = InferSliceState; + + const storeCreator: StateCreatorTyped = (set, get, api) => + Object.entries(configs).reduce( + (acc, [key, { initialValue, canAddSingleItem }]) => ({ + ...acc, + ...createBaseSlice( + { + basic: key as keyof T & string, + }, + initialValue, + canAddSingleItem, + )(set as any, get as any, api as any), + }), + {} as State, + ); + + return generateStore(storeCreator as StateCreatorTyped, storeName); +} diff --git a/src/store/utils/slice.ts b/src/store/utils/slice.ts index e49edcc5..45dcd3e2 100644 --- a/src/store/utils/slice.ts +++ b/src/store/utils/slice.ts @@ -2,14 +2,14 @@ import { StateCreator } from 'zustand'; type Capitalize = S extends `${infer F}${infer R}` ? `${Uppercase}${R}` : S; -export type SliceState = { +export type SliceState = { [P in K]: V; } & { [P in `set${Capitalize}`]: (value: V | SetState) => void; } & { [P in `reset${Capitalize}`]: () => void; } & Partial<{ - [P in `add${Capitalize}`]: (value: T) => void; + [P in `add${Capitalize}Item`]: (value: T) => void; }>; const capitalize = (value: string) => { @@ -18,7 +18,7 @@ const capitalize = (value: string) => { const updateValue = (value: V, updatedValue: T): V => { if (Array.isArray(value)) { - return [...value, updatedValue] as any; + return [...value, updatedValue] as V; } else if (value instanceof Map) { const newMap = new Map(value); @@ -28,9 +28,9 @@ const updateValue = (value: V, updatedValue: T): V => { Object.entries(updatedValue).forEach(([key, value]) => newMap.set(key, value)); } - return newMap as any; + return newMap as V; } else if (value instanceof Set) { - return new Set([...value, updatedValue]) as any; + return new Set([...value, updatedValue]) as V; } else if (typeof value === 'object' && value !== null) { if (typeof updatedValue === 'object' && updatedValue !== null) { return { ...value, ...updatedValue } as any; @@ -44,11 +44,11 @@ const updateValue = (value: V, updatedValue: T): V => { return updatedValue as any; }; -export const createBaseSlice = ( - keys: { basic: K; singleItem?: S }, +export const createBaseSlice = ( + keys: { basic: K }, initialValue: V, canAddSingleItem = false, -): StateCreator, [['zustand/devtools', never]], [], SliceState> => { +): StateCreator, [['zustand/devtools', never]], [], SliceState> => { return set => { const capitalizedTitle = capitalize(keys.basic); @@ -65,10 +65,10 @@ export const createBaseSlice = set({ [keys.basic]: initialValue } as any, false, `reset${capitalizedTitle}`), - } as SliceState; + } as SliceState; if (canAddSingleItem) { - (baseSlice as any)[`add${capitalize(keys.singleItem ?? keys.basic)}`] = (updatedValue: T) => + (baseSlice as any)[`add${capitalize(keys.basic)}Item`] = (updatedValue: T) => set( state => { const value = state[keys.basic] as any; @@ -76,7 +76,7 @@ export const createBaseSlice = = ReturnType>; + export type StateCreatorTyped = StateCreator; export const generateStore = (store: StateCreatorTyped, name: string) => diff --git a/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts b/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts index 509f074f..c333bab7 100644 --- a/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts +++ b/src/test/__tests__/common/hooks/useProcessedRecordAndSchema.test.ts @@ -16,7 +16,7 @@ describe('useProcessedRecordAndSchema', () => { setInitialGlobalState([ { store: useStatusStore, - state: { addStatusMessage: jest.fn() }, + state: { addStatusMessagesItem: jest.fn() }, }, { store: useInputsStore, diff --git a/src/test/__tests__/common/hooks/useSearchFiltersData.test.ts b/src/test/__tests__/common/hooks/useSearchFiltersData.test.ts index 33ed5515..24d9928a 100644 --- a/src/test/__tests__/common/hooks/useSearchFiltersData.test.ts +++ b/src/test/__tests__/common/hooks/useSearchFiltersData.test.ts @@ -13,7 +13,7 @@ describe('useSearchFiltersData', () => { const resetSelectedFacetsGroups = jest.fn(); const setFacetsData = jest.fn(); const setSourceData = jest.fn(); - const addStatusMessage = jest.fn(); + const addStatusMessagesItem = jest.fn(); const DEFAULT_SEARCH_SOURCE_LIMIT = '50'; const DEFAULT_SEARCH_FACETS_QUERY = 'id=*'; @@ -30,7 +30,7 @@ describe('useSearchFiltersData', () => { setSourceData, }, }, - { store: useStatusStore, state: { addStatusMessage } }, + { store: useStatusStore, state: { addStatusMessagesItem } }, ]); }); diff --git a/src/test/__tests__/store/utils/slice.test.ts b/src/test/__tests__/store/utils/slice.test.ts index 6957ac7f..bc18a8ec 100644 --- a/src/test/__tests__/store/utils/slice.test.ts +++ b/src/test/__tests__/store/utils/slice.test.ts @@ -2,7 +2,6 @@ import { createBaseSlice, SliceState } from '@src/store/utils/slice'; describe('createBaseSlice', () => { type KeyBasic = 'testKey'; - type KeySingleItem = 'item'; const keys = { basic: 'testKey' }; const initialValue = 'initialValue'; @@ -15,7 +14,7 @@ describe('createBaseSlice', () => { }); describe('"set" method', () => { - let baseSlice: SliceState; + let baseSlice: SliceState; const initialState = { [keys.basic]: initialValue }; beforeEach(() => { @@ -55,14 +54,14 @@ describe('createBaseSlice', () => { }); describe('"add" method', () => { - const keys = { basic: 'testKey' as KeyBasic, singleItem: 'item' as KeySingleItem }; + const keys = { basic: 'testKey' as KeyBasic }; test('updates the state when canAddSingleItem is true', () => { const initialValue = ['initialValue']; - const baseSlice = createBaseSlice<'testKey', string[], 'item', string>(keys, initialValue, true)(set, get, store); + const baseSlice = createBaseSlice<'testKey', string[], string>(keys, initialValue, true)(set, get, store); - baseSlice.addItem?.('newItem'); - expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKey'); + baseSlice.addTestKeyItem?.('newItem'); + expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKeyItem'); const state = { testKey: ['initialValue'] }; const updater = set.mock.calls[0][0]; @@ -71,33 +70,25 @@ describe('createBaseSlice', () => { test('updates the state when value is an object', () => { type StateEntry = Record; - const initialValue = { key1: 'value_1' } as StateEntry; - const baseSlice = createBaseSlice(keys, initialValue, true)( - set, - get, - store, - ); + const initialValue = { key_1: 'value_1' } as StateEntry; + const baseSlice = createBaseSlice(keys, initialValue, true)(set, get, store); - baseSlice.addItem?.({ key2: 'value_2' }); - expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKey'); + baseSlice.addTestKeyItem?.({ key_2: 'value_2' }); + expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKeyItem'); - const state = { testKey: { key1: 'value_1' } }; + const state = { testKey: { key_1: 'value_1' } }; const updater = set.mock.calls[0][0]; expect(updater(state)).toEqual({ - testKey: { key1: 'value_1', key2: 'value_2' }, + testKey: { key_1: 'value_1', key_2: 'value_2' }, }); }); test('updates the state when value is a Set', () => { const initialValue = new Set(['value_1']); - const baseSlice = createBaseSlice, KeySingleItem, string>(keys, initialValue, true)( - set, - get, - store, - ); - - baseSlice.addItem?.('value_2'); - expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKey'); + const baseSlice = createBaseSlice, string>(keys, initialValue, true)(set, get, store); + + baseSlice.addTestKeyItem?.('value_2'); + expect(set).toHaveBeenCalledWith(expect.any(Function), false, 'addTestKeyItem'); const state = { testKey: new Set(['value_1']) }; const updater = set.mock.calls[0][0]; diff --git a/src/views/Edit/Edit.tsx b/src/views/Edit/Edit.tsx index a4e36378..a80be729 100644 --- a/src/views/Edit/Edit.tsx +++ b/src/views/Edit/Edit.tsx @@ -23,7 +23,7 @@ export const Edit = () => { const { getProfiles } = useConfig(); const { fetchRecord, clearRecordState, fetchRecordAndSelectEntityValues } = useRecordControls(); const { resourceId } = useParams(); - const { recordStatus, addStatusMessage } = useStatusState(); + const { recordStatus, addStatusMessagesItem } = useStatusState(); const { basicValue: marcPreviewData, resetBasicValue: resetMarcPreviewData } = useMarcPreviewState(); const recordStatusType = recordStatus?.type; const { setIsLoading } = useLoadingState(); @@ -85,7 +85,7 @@ export const Edit = () => { record: typedRecord, }); } catch { - addStatusMessage?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorLoadingResource')); + addStatusMessagesItem?.(UserNotificationFactory.createMessage(StatusType.error, 'ld.errorLoadingResource')); } finally { setIsLoading(false); }