/********************************************************************************************************************* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of * * this software and associated documentation files (the "Software"), to deal in * * the Software without restriction, including without limitation the rights to * * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * * the Software, and to permit persons to whom the Software is furnished to do so. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * *********************************************************************************************************************/ import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef } from 'react' import { castDraft } from 'immer' import { useImmer } from 'use-immer' import { sortBy } from 'lodash' import { IService } from '../../services/base/crudService' interface State { readonly items: TData[] readonly isLoading: boolean } interface Updater { readonly setItems: (items: TData[]) => void readonly refreshItems: () => void readonly getItem: (id: IdType) => Promise readonly createItem: (item: TData) => void readonly updateItem: (item: TData, persist?: boolean) => void readonly deleteItem: (id: IdType) => Promise } export type ContextInterface = [State, Updater] export type IdSelector = (item: TData) => IdType const contextStore: Record = {} export function createDataContext(key: string): React.Context | null> { if (contextStore[key] == null) { contextStore[key] = createContext | null>(null) } return contextStore[key] } const dataProviderStore: Record = {} export function createDataProvider>( key: string, service: TService, idSelector?: IdSelector, ): any { const idSelect: IdSelector = idSelector || ((item: TData) => { return (item as any).Id as IdType }) if (dataProviderStore[key] == null) { const _dataProvider = ({ children }: PropsWithChildren): any => { const [state, updateState] = useImmer>({ items: [], isLoading: false, }) const stateRef = useRef>(state) stateRef.current = state const fetchItems = useCallback(async () => { updateState((draft) => { draft.isLoading = true }) const items = await service.list() updateState((draft) => { draft.items = castDraft(items) draft.isLoading = false }) }, [updateState]) const fetchItem = useCallback( async (id: IdType) => { updateState((draft) => { draft.isLoading = true }) const item = await service.getItem(id) updateState((draft) => { const index = draft.items.findIndex((x) => idSelect(x as TData) === idSelect(item)) if (index < 0) { draft.items.push(castDraft(item)) } else { draft.items[index] = castDraft(item) } }) }, [updateState], ) useEffect(() => { fetchItems() }, [fetchItems]) const updater = useMemo>((): Updater => { return { setItems: (items: TData[]): void => { updateState((draft) => { draft.items = castDraft(items) }) }, refreshItems: (): void => { ;(async () => { await fetchItems() })() }, getItem: async (id: IdType): Promise => { await fetchItem(id) }, createItem: async (item: TData): Promise => { updateState((draft) => { draft.isLoading = true }) const newItem = await service.create(item) updateState((draft) => { draft.items.push(castDraft(newItem)) draft.isLoading = false }) }, updateItem: (item: TData, persist?: boolean): void => { updateState((draft) => { const index = draft.items.findIndex((a) => idSelect(a as TData) === idSelect(item)) if (index < 0) { throw new Error(`Failed to find item with id ${idSelect(item)}`) } draft.items[index] = castDraft(item) if (persist) { ;(async () => { const updated = await service.update(item) updateState((draft_) => { draft_.items[index] = castDraft(updated) }) })() } }) }, deleteItem: async (id: IdType): Promise => { updateState((draft) => { draft.isLoading = true }) await service.deleteItem(id) updateState((draft) => { const newDataItems = draft.items.filter((a) => idSelect(a as TData) !== id) draft.items = newDataItems draft.isLoading = false }) }, } }, [updateState, fetchItems, fetchItem]) const contextValue = useMemo>(() => [state, updater], [state, updater]) const DataContext = createDataContext(key) return {children} } dataProviderStore[key] = _dataProvider } return dataProviderStore[key] } export function useDataContext(key: string): ContextInterface { const dataContext = createDataContext(key) const context = useContext(dataContext) if (context == null) { throw new Error(`DataContext<${key}> is null`) } return context }