From beb5d9a1a5b634e52e82ea4522cf59ea238e8793 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:53:31 +0100 Subject: [PATCH 1/2] Simplify ra-data-local-forage setup --- packages/ra-data-localforage/README.md | 21 +-- .../ra-data-localforage/src/index.stories.tsx | 25 ++++ packages/ra-data-localforage/src/index.ts | 135 ++++++++++++++---- 3 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 packages/ra-data-localforage/src/index.stories.tsx diff --git a/packages/ra-data-localforage/README.md b/packages/ra-data-localforage/README.md index 095e9d1f210..43296a9818a 100644 --- a/packages/ra-data-localforage/README.md +++ b/packages/ra-data-localforage/README.md @@ -19,24 +19,9 @@ import { Admin, Resource } from 'react-admin'; import localForageDataProvider from 'ra-data-local-forage'; import { PostList } from './posts'; +const dataProvider = localForageDataProvider(); const App = () => { - const [dataProvider, setDataProvider] = React.useState(null); - - React.useEffect(() => { - async function startDataProvider() { - const localForageProvider = await localForageDataProvider(); - setDataProvider(localForageProvider); - } - - if (dataProvider === null) { - startDataProvider(); - } - }, [dataProvider]); - - // hide the admin until the data provider is ready - if (!dataProvider) return

Loading...

; - return ( @@ -52,7 +37,7 @@ export default App; By default, the data provider starts with no resource. To set default data if the IndexedDB is empty, pass a JSON object as the `defaultData` argument: ```js -const dataProvider = await localForageDataProvider({ +const dataProvider = localForageDataProvider({ defaultData: { posts: [ { id: 0, title: 'Hello, world!' }, @@ -75,7 +60,7 @@ Foreign keys are also supported: just name the field `{related_resource_name}_id As this data provider doesn't use the network, you can't debug it using the network tab of your browser developer tools. However, it can log all calls (input and output) in the console, provided you set the `loggingEnabled` parameter: ```js -const dataProvider = await localForageDataProvider({ +const dataProvider = localForageDataProvider({ loggingEnabled: true }); ``` diff --git a/packages/ra-data-localforage/src/index.stories.tsx b/packages/ra-data-localforage/src/index.stories.tsx new file mode 100644 index 00000000000..8a3de91513b --- /dev/null +++ b/packages/ra-data-localforage/src/index.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Admin, EditGuesser, ListGuesser, Resource } from 'react-admin'; +import localforageDataProvider from './index'; + +export default { + title: 'ra-data-local-forage', +}; + +export const Basic = () => { + const dataProvider = localforageDataProvider({ + prefixLocalForageKey: 'story-app-', + defaultData: { + posts: [ + { id: 1, title: 'Hello, world!' }, + { id: 2, title: 'FooBar' }, + ], + }, + }); + + return ( + + + + ); +}; diff --git a/packages/ra-data-localforage/src/index.ts b/packages/ra-data-localforage/src/index.ts index bb7e76c9f72..49df3b25e60 100644 --- a/packages/ra-data-localforage/src/index.ts +++ b/packages/ra-data-localforage/src/index.ts @@ -25,12 +25,12 @@ import localforage from 'localforage'; * @example // initialize with no data * * import localForageDataProvider from 'ra-data-local-forage'; - * const dataProvider = await localForageDataProvider(); + * const dataProvider = localForageDataProvider(); * * @example // initialize with default data (will be ignored if data has been modified by user) * * import localForageDataProvider from 'ra-data-local-forage'; - * const dataProvider = await localForageDataProvider({ + * const dataProvider = localForageDataProvider({ * defaultData: { * posts: [ * { id: 0, title: 'Hello, world!' }, @@ -43,15 +43,17 @@ import localforage from 'localforage'; * } * }); */ -export default async ( - params?: LocalForageDataProviderParams -): Promise => { +export default (params?: LocalForageDataProviderParams): DataProvider => { const { defaultData = {}, prefixLocalForageKey = 'ra-data-local-forage-', loggingEnabled = false, } = params || {}; + let data: Record | undefined; + let baseDataProvider: DataProvider | undefined; + let initializePromise: Promise | undefined; + const getLocalForageData = async (): Promise => { const keys = await localforage.keys(); const keyFiltered = keys.filter(key => { @@ -71,28 +73,44 @@ export default async ( return localForageData; }; - const localForageData = await getLocalForageData(); - const data = localForageData ?? defaultData; + const initialize = async () => { + if (!initializePromise) { + initializePromise = initializeProvider(); + } + return initializePromise; + }; + + const initializeProvider = async () => { + const localForageData = await getLocalForageData(); + data = localForageData ?? defaultData; + + baseDataProvider = fakeRestProvider( + data, + loggingEnabled + ) as DataProvider; + }; // Persist in localForage const updateLocalForage = (resource: string) => { + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } localforage.setItem( `${prefixLocalForageKey}${resource}`, data[resource] ); }; - const baseDataProvider = fakeRestProvider( - data, - loggingEnabled - ) as DataProvider; - return { // read methods are just proxies to FakeRest - getList: ( + getList: async ( resource: string, params: GetListParams ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } return baseDataProvider .getList(resource, params) .catch(error => { @@ -104,19 +122,35 @@ export default async ( } }); }, - getOne: ( + getOne: async ( resource: string, params: GetOneParams - ) => baseDataProvider.getOne(resource, params), - getMany: ( + ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + return baseDataProvider.getOne(resource, params); + }, + getMany: async ( resource: string, params: GetManyParams - ) => baseDataProvider.getMany(resource, params), - getManyReference: ( + ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + return baseDataProvider.getMany(resource, params); + }, + getManyReference: async ( resource: string, params: GetManyReferenceParams - ) => - baseDataProvider + ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + return baseDataProvider .getManyReference(resource, params) .catch(error => { if (error.code === 1) { @@ -125,13 +159,22 @@ export default async ( } else { throw error; } - }), + }); + }, // update methods need to persist changes in localForage - update: ( + update: async ( resource: string, params: UpdateParams ) => { + await initialize(); + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + const index = data[resource].findIndex( (record: { id: any }) => record.id === params.id ); @@ -142,8 +185,16 @@ export default async ( updateLocalForage(resource); return baseDataProvider.update(resource, params); }, - updateMany: (resource: string, params: UpdateManyParams) => { + updateMany: async (resource: string, params: UpdateManyParams) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + params.ids.forEach((id: Identifier) => { + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } const index = data[resource].findIndex( (record: { id: Identifier }) => record.id === id ); @@ -155,14 +206,21 @@ export default async ( updateLocalForage(resource); return baseDataProvider.updateMany(resource, params); }, - create: = any>( + create: async = any>( resource: string, params: CreateParams ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } // we need to call the fakerest provider first to get the generated id return baseDataProvider .create(resource, params) .then(response => { + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } if (!data.hasOwnProperty(resource)) { data[resource] = []; } @@ -171,10 +229,17 @@ export default async ( return response; }); }, - delete: ( + delete: async ( resource: string, params: DeleteParams ) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } const index = data[resource].findIndex( (record: { id: any }) => record.id === params.id ); @@ -182,10 +247,22 @@ export default async ( updateLocalForage(resource); return baseDataProvider.delete(resource, params); }, - deleteMany: (resource: string, params: DeleteManyParams) => { - const indexes = params.ids.map((id: any) => - data[resource].findIndex((record: any) => record.id === id) - ); + deleteMany: async (resource: string, params: DeleteManyParams) => { + await initialize(); + if (!baseDataProvider) { + throw new Error('The dataProvider is not initialized.'); + } + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } + const indexes = params.ids.map((id: any) => { + if (!data) { + throw new Error('The dataProvider is not initialized.'); + } + return data[resource].findIndex( + (record: any) => record.id === id + ); + }); pullAt(data[resource], indexes); updateLocalForage(resource); return baseDataProvider.deleteMany(resource, params); From 69fd3e9cde2194b6f25217ef90e762bf1df858f9 Mon Sep 17 00:00:00 2001 From: Gildas <1122076+djhi@users.noreply.github.com> Date: Mon, 20 Jan 2025 19:01:52 +0100 Subject: [PATCH 2/2] Fix build --- .../ra-data-localforage/src/index.stories.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/ra-data-localforage/src/index.stories.tsx b/packages/ra-data-localforage/src/index.stories.tsx index 8a3de91513b..5b3430fb1cb 100644 --- a/packages/ra-data-localforage/src/index.stories.tsx +++ b/packages/ra-data-localforage/src/index.stories.tsx @@ -1,5 +1,11 @@ import React from 'react'; -import { Admin, EditGuesser, ListGuesser, Resource } from 'react-admin'; +import { Resource } from 'ra-core'; +import { + AdminContext, + AdminUI, + EditGuesser, + ListGuesser, +} from 'ra-ui-materialui'; import localforageDataProvider from './index'; export default { @@ -18,8 +24,10 @@ export const Basic = () => { }); return ( - - - + + + + + ); };