diff --git a/package.json b/package.json index 4d6e0775..9c7d5cfe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "drama-queen", "private": true, - "version": "2.3.2", + "version": "2.3.3-rc.0", "type": "module", "scripts": { "dev": "vite --port 5001 --strictPort", diff --git a/src/core/tools/array.test.ts b/src/core/tools/array.test.ts new file mode 100644 index 00000000..ae7942c4 --- /dev/null +++ b/src/core/tools/array.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' + +import { chunk } from './array' + +describe('chunk function', () => { + it('splits an array into chunks of the specified size', () => { + const array = [1, 2, 3, 4, 5] + const result = chunk(array, 2) + expect(result).toEqual([[1, 2], [3, 4], [5]]) + }) + + it('returns an empty array if input array is empty', () => { + const result = chunk([], 3) + expect(result).toEqual([]) + }) + + it('throws an error if chunkSize is not strictly positive', () => { + const array = [1, 2, 3] + expect(() => chunk(array, 0)).toThrow( + 'chunkSize must be a strictly positive integer', + ) + expect(() => chunk(array, -2)).toThrow( + 'chunkSize must be a strictly positive integer', + ) + expect(() => chunk(array, 0.5)).toThrow( + 'chunkSize must be a strictly positive integer', + ) + }) + + it('handles chunkSize larger than the array length', () => { + const array = [1, 2, 3] + const result = chunk(array, 10) + expect(result).toEqual([[1, 2, 3]]) + }) + + it('works with arrays of objects', () => { + const array = [{ id: 1 }, { id: 2 }, { id: 3 }] + const result = chunk(array, 2) + expect(result).toEqual([[{ id: 1 }, { id: 2 }], [{ id: 3 }]]) + }) + + it('works with mixed-type arrays', () => { + const array = [1, 'a', true, null] + const result = chunk(array, 2) + expect(result).toEqual([ + [1, 'a'], + [true, null], + ]) + }) +}) diff --git a/src/core/tools/array.ts b/src/core/tools/array.ts new file mode 100644 index 00000000..b50f21e0 --- /dev/null +++ b/src/core/tools/array.ts @@ -0,0 +1,12 @@ +export const chunk = (array: T[], chunkSize: number) => { + if (chunkSize <= 0 || !Number.isInteger(chunkSize)) { + throw new Error('chunkSize must be a strictly positive integer') + } + + const res: T[][] = [] + for (let i = 0; i < array.length; i += chunkSize) { + const chunk = array.slice(i, i + chunkSize) + res.push(chunk) + } + return res +} diff --git a/src/core/tools/externalResources.ts b/src/core/tools/externalResources.ts index 5c4ee5d4..29a5872e 100644 --- a/src/core/tools/externalResources.ts +++ b/src/core/tools/externalResources.ts @@ -7,6 +7,7 @@ import type { Manifest, } from '@/core/model' +import { chunk } from './array' import { fetchUrl } from './fetchUrl' const EXTERNAL_QUESTIONNAIRES_KEYWORD = 'gide' @@ -64,9 +65,17 @@ export async function filterTransformedManifest( } // Cache every external resources (not already cached) for a particular questionnaire -export async function getResourcesFromExternalQuestionnaire( - questionnaire: ExternalQuestionnaire, -): Promise { +export async function getResourcesFromExternalQuestionnaire({ + questionnaire, + callBackTotal, + callBackReset, + callBackUnit, +}: { + questionnaire: ExternalQuestionnaire + callBackTotal: (total: number) => void + callBackReset: () => void + callBackUnit: () => void +}): Promise { const transformedManifest = await getTransformedManifest(questionnaire.id) const filteredTransformedManifest = await filterTransformedManifest( @@ -74,8 +83,26 @@ export async function getResourcesFromExternalQuestionnaire( transformedManifest, ) - const manifestCache = await caches.open(questionnaire.cacheName) - return await manifestCache.addAll(filteredTransformedManifest) + const transformManifestFilteredChunked = chunk( + filteredTransformedManifest, + 15, + ) + callBackReset() + callBackTotal(transformManifestFilteredChunked.length) + + return await (transformManifestFilteredChunked || []).reduce( + async (previousPromise, subManifest) => { + await previousPromise + + const putSubManifestInCache = async () => { + const cacheForManifest = await caches.open(questionnaire.cacheName) + await cacheForManifest.addAll(subManifest) + callBackUnit() + } + return putSubManifestInCache() + }, + Promise.resolve(), + ) } // Separate, from the list of external questionnaires, those that are needed and those that are not needed diff --git a/src/core/usecases/synchronizeData/selectors.ts b/src/core/usecases/synchronizeData/selectors.ts index 9e5d04e0..7d4d15b3 100644 --- a/src/core/usecases/synchronizeData/selectors.ts +++ b/src/core/usecases/synchronizeData/selectors.ts @@ -47,17 +47,42 @@ const externalResourcesProgress = createSelector(downloadingState, (state) => { return undefined } // if there is no external resources, we don't show the progress bar - if (state.totalExternalResources === undefined) { + if (state.totalExternalResourcesByQuestionnaire === undefined) { return undefined } if ( - state.externalResourcesCompleted === 0 && - state.totalExternalResources === 0 + state.externalResourcesByQuestionnaireCompleted === 0 && + state.totalExternalResourcesByQuestionnaire === 0 ) return 100 - return (state.externalResourcesCompleted * 100) / state.totalExternalResources + return ( + (state.externalResourcesByQuestionnaireCompleted * 100) / + state.totalExternalResourcesByQuestionnaire + ) }) +const externalResourcesProgressCount = createSelector( + downloadingState, + (state) => { + if (state === undefined) { + return undefined + } + // if there is no external resources, we don't show the progress bar + if (state.totalExternalResources === undefined) { + return undefined + } + if ( + state.totalExternalResources === 0 && + state.externalResourcesCompleted === 0 + ) + return undefined + return { + externalResourcesCompleted: state.externalResourcesCompleted, + totalExternalResources: state.totalExternalResources, + } + }, +) + const uploadProgress = createSelector(state, (state) => { if (state.stateDescription !== 'running') { return undefined @@ -77,6 +102,7 @@ const main = createSelector( nomenclatureProgress, surveyProgress, externalResourcesProgress, + externalResourcesProgressCount, uploadProgress, ( state, @@ -84,6 +110,7 @@ const main = createSelector( nomenclatureProgress, surveyProgress, externalResourcesProgress, + externalResourcesProgressCount, uploadProgress, ) => { switch (state.stateDescription) { @@ -107,6 +134,7 @@ const main = createSelector( nomenclatureProgress, surveyProgress, externalResourcesProgress, + externalResourcesProgressCount, } } } diff --git a/src/core/usecases/synchronizeData/state.ts b/src/core/usecases/synchronizeData/state.ts index 9556dc6a..5722a303 100644 --- a/src/core/usecases/synchronizeData/state.ts +++ b/src/core/usecases/synchronizeData/state.ts @@ -32,6 +32,8 @@ export namespace State { nomenclatureCompleted: number totalSurvey: number surveyCompleted: number + totalExternalResourcesByQuestionnaire?: number + externalResourcesByQuestionnaireCompleted: number totalExternalResources?: number externalResourcesCompleted: number } @@ -61,6 +63,10 @@ export const { reducer, actions } = createUsecaseActions({ // for total external resources, we make difference for displaying progress bar between : // 0 : external synchro is triggered but there is no needed questionnaire so we want a fullfilled progress bar // undefined : external synchro is not triggered so we don't want the progress bar + totalExternalResourcesByQuestionnaire: EXTERNAL_RESOURCES_URL + ? Infinity + : undefined, + externalResourcesByQuestionnaireCompleted: 0, totalExternalResources: EXTERNAL_RESOURCES_URL ? Infinity : undefined, externalResourcesCompleted: 0, }), @@ -136,13 +142,41 @@ export const { reducer, actions } = createUsecaseActions({ totalExternalResources, } }, - downloadExternalResourceCompleted: (state) => { + setDownloadExternalResourcesCompleted: (state) => { assert(state.stateDescription === 'running' && state.type === 'download') return { ...state, externalResourcesCompleted: state.externalResourcesCompleted + 1, } }, + setDownloadTotalExternalResourcesByQuestionnaire: ( + state, + { + payload, + }: { payload: { totalExternalResourcesByQuestionnaire: number } }, + ) => { + const { totalExternalResourcesByQuestionnaire } = payload + assert(state.stateDescription === 'running' && state.type === 'download') + return { + ...state, + totalExternalResourcesByQuestionnaire, + } + }, + downloadExternalResourceByQuestionnaireCompleted: (state) => { + assert(state.stateDescription === 'running' && state.type === 'download') + return { + ...state, + externalResourcesByQuestionnaireCompleted: + state.externalResourcesByQuestionnaireCompleted + 1, + } + }, + downloadExternalResourceReset: (state) => { + assert(state.stateDescription === 'running' && state.type === 'download') + return { + ...state, + externalResourcesByQuestionnaireCompleted: 0, + } + }, setUploadTotal: (state, { payload }: { payload: { total: number } }) => { const { total } = payload assert(state.stateDescription === 'running' && state.type === 'upload') diff --git a/src/core/usecases/synchronizeData/thunks.ts b/src/core/usecases/synchronizeData/thunks.ts index 337124f5..44c85d42 100644 --- a/src/core/usecases/synchronizeData/thunks.ts +++ b/src/core/usecases/synchronizeData/thunks.ts @@ -219,19 +219,36 @@ export const thunks = { ) // add in cache the missing external resources for needed questionnaires - const prGetExternalResources = Promise.all( - neededQuestionnaires.map(async (questionnaire) => { - await getResourcesFromExternalQuestionnaire(questionnaire) - .then(() => - dispatch(actions.downloadExternalResourceCompleted()), - ) + const prGetExternalResources = (neededQuestionnaires || []).reduce( + async (previousPromise, questionnaire) => { + await previousPromise + + return getResourcesFromExternalQuestionnaire({ + questionnaire: questionnaire, + callBackTotal: (total: number) => + dispatch( + actions.setDownloadTotalExternalResourcesByQuestionnaire({ + totalExternalResourcesByQuestionnaire: total, + }), + ), + callBackReset: () => + dispatch(actions.downloadExternalResourceReset()), + callBackUnit: () => + dispatch( + actions.downloadExternalResourceByQuestionnaireCompleted(), + ), + }) + .then(() => { + dispatch(actions.setDownloadExternalResourcesCompleted()) + }) .catch((error) => console.error( `An error occurred while fetching external resources of questionnaire ${questionnaire.id}`, error, ), ) - }), + }, + Promise.resolve(), ) // delete the cache of every not needed external questionnaires diff --git a/src/ui/pages/synchronize/LoadingDisplay.tsx b/src/ui/pages/synchronize/LoadingDisplay.tsx index 49863f66..7abf36ba 100644 --- a/src/ui/pages/synchronize/LoadingDisplay.tsx +++ b/src/ui/pages/synchronize/LoadingDisplay.tsx @@ -11,7 +11,8 @@ type LoadingDisplayProps = { syncStepTitle: string progressBars: { progress: number - label: string | undefined + label?: string + extraTitle?: string }[] } @@ -30,21 +31,22 @@ export function LoadingDisplay(props: LoadingDisplayProps) { - {progressBars.map((bar) => ( - + {progressBars.map(({ label, progress, extraTitle }) => ( + - {bar.label !== undefined && ( + {label !== undefined && ( - {bar.label} + {label} + {extraTitle ? `: ${extraTitle}` : ''} )} diff --git a/src/ui/pages/synchronize/SynchronizeData.tsx b/src/ui/pages/synchronize/SynchronizeData.tsx index f8b29b93..9e8f1c8b 100644 --- a/src/ui/pages/synchronize/SynchronizeData.tsx +++ b/src/ui/pages/synchronize/SynchronizeData.tsx @@ -18,6 +18,7 @@ export function SynchronizeData() { surveyProgress, surveyUnitProgress, externalResourcesProgress, + externalResourcesProgressCount, uploadProgress, } = useCoreState('synchronizeData', 'main') @@ -51,7 +52,6 @@ export function SynchronizeData() { progressBars={[ { progress: uploadProgress, - label: undefined, }, ]} syncStepTitle={t('uploadingData')} @@ -73,11 +73,17 @@ export function SynchronizeData() { label: t('surveyUnitsProgress'), }, // render external resources progress bar only if there are external resources - ...(externalResourcesProgress !== undefined + ...(externalResourcesProgress !== undefined && + externalResourcesProgressCount !== undefined ? [ { progress: externalResourcesProgress, label: t('externalResourcesProgress'), + extraTitle: Number.isFinite( + externalResourcesProgressCount.totalExternalResources, + ) + ? `${externalResourcesProgressCount.externalResourcesCompleted} / ${externalResourcesProgressCount.totalExternalResources}` + : undefined, }, ] : []),