From 3abc5431950157c1a17acba558375cb55451714c Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 10:29:11 +1100 Subject: [PATCH 1/6] AP-7184b # `draftService.syncDrafts` to download draft data in the background --- CHANGELOG.md | 4 ++ src/apps/draft-service.ts | 82 ++++++++++++++++++++++++++++------- src/apps/types/submissions.ts | 2 + src/hooks/useDrafts.tsx | 1 + 4 files changed, 73 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb13b801..19f60b4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **[BREAKING]** `draftService.syncDrafts` to download draft data in the background + ### Fixed - Lot / DP Numbers display for NSW Point V3 form element diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index 460c5c39b..aeecc5b93 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -144,6 +144,25 @@ async function generateLocalFormSubmissionDraftsFromStorage( deletedDraftIds, ) + async function broadcastUpdate() { + await executeDraftsListeners( + Array.from(localFormSubmissionDraftsMap.values()), + ) + } + + // At this point we need to store the state of the drafts in localForage + const username = getUsername() + if (!username) { + throw new OneBlinkAppsError( + 'You cannot download drafts until you have logged in. Please login and try again.', + { + requiresLogin: true, + }, + ) + } + + const draftsToDownload: SubmissionTypes.FormSubmissionDraft[] = [] + for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { if ( // Unsycned version of draft takes priority over the synced version @@ -153,6 +172,33 @@ async function generateLocalFormSubmissionDraftsFromStorage( // Remove any drafts deleted while offline !deletedDraftIds.has(formSubmissionDraft.id) ) { + const localDraftSubmission = await getLocalDraftSubmission( + formSubmissionDraft.id, + ) + draftsToDownload.push(formSubmissionDraft) + localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { + ...formSubmissionDraft, + downloadStatus: 'PENDING', + draftSubmission: localDraftSubmission ?? undefined, + }) + } + } + + await broadcastUpdate() + + // TODO Batch the downloads instead of sequentially + if (draftsToDownload.length) { + for (const formSubmissionDraft of draftsToDownload) { + const currentValue = localFormSubmissionDraftsMap.get( + formSubmissionDraft.id, + ) + if (currentValue) { + localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { + ...currentValue, + downloadStatus: 'DOWNLOADING', + }) + } + await broadcastUpdate() const draftSubmission = await getDraftSubmission( formSubmissionDraft, ).catch((err) => { @@ -164,8 +210,10 @@ async function generateLocalFormSubmissionDraftsFromStorage( }) localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { ...formSubmissionDraft, - draftSubmission, + draftSubmission: draftSubmission, + downloadStatus: draftSubmission ? 'SUCCESS' : 'ERROR', }) + await broadcastUpdate() } } @@ -800,24 +848,26 @@ async function syncDrafts({ localDraftsStorage.syncedFormSubmissionDrafts = formSubmissionDrafts } - await setAndBroadcastDrafts(localDraftsStorage) - - if (localDraftsStorage.syncedFormSubmissionDrafts.length) { - console.log( - 'Ensuring all draft data is available for offline use for synced drafts', - localDraftsStorage.syncedFormSubmissionDrafts, - ) - for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { - await getDraftSubmission(formSubmissionDraft, abortSignal).catch( - (error) => { - console.warn('Could not download Draft Data as JSON', error) - }, + console.log('Ddownloading drafts in the background') + setAndBroadcastDrafts(localDraftsStorage).then(async () => { + if (localDraftsStorage.syncedFormSubmissionDrafts.length) { + console.log( + 'Ensuring all draft data is available for offline use for synced drafts', + localDraftsStorage.syncedFormSubmissionDrafts, ) + for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { + await getDraftSubmission(formSubmissionDraft, abortSignal).catch( + (error) => { + console.warn('Could not download Draft Data as JSON', error) + }, + ) + } } - } + console.log('Finished syncing drafts.') + _isSyncingDrafts = false + }) - console.log('Finished syncing drafts.') - _isSyncingDrafts = false + // broadcast the drafts and download the draft data in the background } catch (error) { _isSyncingDrafts = false if (abortSignal?.aborted) { diff --git a/src/apps/types/submissions.ts b/src/apps/types/submissions.ts index 0cf2feaa6..88a656cb1 100644 --- a/src/apps/types/submissions.ts +++ b/src/apps/types/submissions.ts @@ -111,6 +111,8 @@ export type LocalFormSubmissionDraft = Omit< draftSubmission: DraftSubmission | undefined /** `true` if the draft was created by a public user (not logged in). */ isPublic?: boolean + /** The status of the draft download */ + downloadStatus?: 'PENDING' | 'DOWNLOADING' | 'ERROR' | 'SUCCESS' } export type FormSubmission = NewFormSubmission & diff --git a/src/hooks/useDrafts.tsx b/src/hooks/useDrafts.tsx index 730055d04..f972fea72 100644 --- a/src/hooks/useDrafts.tsx +++ b/src/hooks/useDrafts.tsx @@ -100,6 +100,7 @@ export function DraftsContextProvider({ syncError: null, })) }, []) + // TODO add formId to prioritize downloads for specific forms const syncDrafts = React.useCallback( async (abortSignal: AbortSignal | undefined) => { if (!isDraftsEnabled || isUsingFormsKey) { From 2432d6c67c8140526c9d081ec39146d8cd6e6fb5 Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 11:54:55 +1100 Subject: [PATCH 2/6] AP-7184 # apply feedback --- src/apps/draft-service.ts | 63 +++++++++++++++++++++++---------- src/apps/services/api/drafts.ts | 4 ++- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index aeecc5b93..a4e0ca750 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -5,7 +5,11 @@ import OneBlinkAppsError from './services/errors/oneBlinkAppsError' import { isOffline } from './offline-service' import { getUsername } from './services/cognito' import { getFormsKeyId, getCurrentFormsAppUser } from './auth-service' -import { getFormSubmissionDrafts, uploadDraftData } from './services/api/drafts' +import { + DRAFT_DATA_UNAVAILABLE_ERROR_TITLE, + getFormSubmissionDrafts, + uploadDraftData, +} from './services/api/drafts' import { getPendingQueueSubmissions, deletePendingQueueSubmission, @@ -199,6 +203,7 @@ async function generateLocalFormSubmissionDraftsFromStorage( }) } await broadcastUpdate() + let errorOrNotAvaialable: 'ERROR' | 'NOT_AVAILABLE' = 'ERROR' const draftSubmission = await getDraftSubmission( formSubmissionDraft, ).catch((err) => { @@ -206,12 +211,18 @@ async function generateLocalFormSubmissionDraftsFromStorage( `Could not fetch draft submission for draft: ${formSubmissionDraft.id}`, err, ) + if ( + err instanceof OneBlinkAppsError && + err.title === DRAFT_DATA_UNAVAILABLE_ERROR_TITLE + ) { + errorOrNotAvaialable = 'NOT_AVAILABLE' + } return undefined }) localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { ...formSubmissionDraft, draftSubmission: draftSubmission, - downloadStatus: draftSubmission ? 'SUCCESS' : 'ERROR', + downloadStatus: draftSubmission ? 'SUCCESS' : errorOrNotAvaialable, }) await broadcastUpdate() } @@ -848,24 +859,40 @@ async function syncDrafts({ localDraftsStorage.syncedFormSubmissionDrafts = formSubmissionDrafts } - console.log('Ddownloading drafts in the background') - setAndBroadcastDrafts(localDraftsStorage).then(async () => { - if (localDraftsStorage.syncedFormSubmissionDrafts.length) { - console.log( - 'Ensuring all draft data is available for offline use for synced drafts', - localDraftsStorage.syncedFormSubmissionDrafts, - ) - for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { - await getDraftSubmission(formSubmissionDraft, abortSignal).catch( - (error) => { - console.warn('Could not download Draft Data as JSON', error) - }, + console.log('Downloading drafts in the background') + setAndBroadcastDrafts(localDraftsStorage) + .then(async () => { + if (localDraftsStorage.syncedFormSubmissionDrafts.length) { + console.log( + 'Ensuring all draft data is available for offline use for synced drafts', + localDraftsStorage.syncedFormSubmissionDrafts, ) + for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { + await getDraftSubmission(formSubmissionDraft, abortSignal).catch( + (error) => { + console.warn('Could not download Draft Data as JSON', error) + }, + ) + } } - } - console.log('Finished syncing drafts.') - _isSyncingDrafts = false - }) + console.log('Finished syncing drafts.') + }) + .catch((error) => { + if (abortSignal?.aborted) { + console.log('Syncing drafts has been aborted') + return + } + console.warn( + 'Error while attempting download drafts in the background', + error, + ) + if (!(error instanceof OneBlinkAppsError)) { + Sentry.captureException(error) + } + }) + .finally(() => { + _isSyncingDrafts = false + }) // broadcast the drafts and download the draft data in the background } catch (error) { diff --git a/src/apps/services/api/drafts.ts b/src/apps/services/api/drafts.ts index 86edd8d4c..40ca9e40d 100644 --- a/src/apps/services/api/drafts.ts +++ b/src/apps/services/api/drafts.ts @@ -12,6 +12,8 @@ import generateOneBlinkUploader from '../generateOneBlinkUploader' import { OneBlinkStorageError } from '@oneblink/storage' import generateOneBlinkDownloader from '../generateOneBlinkDownloader' +export const DRAFT_DATA_UNAVAILABLE_ERROR_TITLE = 'Draft Data Unavailable' + async function uploadDraftData( draftSubmission: DraftSubmission, onProgress?: ProgressListener, @@ -186,7 +188,7 @@ async function downloadDraftData( throw new OneBlinkAppsError( "Data has been removed based on your administrator's draft data retention policy.", { - title: 'Draft Data Unavailable', + title: DRAFT_DATA_UNAVAILABLE_ERROR_TITLE, }, ) } From ffdcbed803c32c907810ed2c4f9658f4a2ea8386 Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 12:45:14 +1100 Subject: [PATCH 3/6] AP-1784 # convert download status to union type --- src/apps/draft-service.ts | 47 ++++++++++++++++++++++------------- src/apps/types/submissions.ts | 21 ++++++++++------ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index a4e0ca750..51f4593ab 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -96,6 +96,7 @@ function generateLocalFormSubmissionDraftsFromDraftSubmissions( taskActionId: draftSubmission.taskCompletion?.taskAction.taskActionId, draftSubmission, versions: undefined, + downloadStatus: 'SUCCESS', }) } } @@ -129,7 +130,9 @@ async function generatePublicLocalFormSubmissionDraftsFromStorage( ) return _orderBy(localFormSubmissionDrafts, (localFormSubmissionDraft) => { - return localFormSubmissionDraft.draftSubmission?.createdAt + return localFormSubmissionDraft.downloadStatus === 'SUCCESS' + ? localFormSubmissionDraft.draftSubmission?.createdAt + : undefined }) } @@ -176,14 +179,10 @@ async function generateLocalFormSubmissionDraftsFromStorage( // Remove any drafts deleted while offline !deletedDraftIds.has(formSubmissionDraft.id) ) { - const localDraftSubmission = await getLocalDraftSubmission( - formSubmissionDraft.id, - ) draftsToDownload.push(formSubmissionDraft) localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { ...formSubmissionDraft, downloadStatus: 'PENDING', - draftSubmission: localDraftSubmission ?? undefined, }) } } @@ -203,7 +202,7 @@ async function generateLocalFormSubmissionDraftsFromStorage( }) } await broadcastUpdate() - let errorOrNotAvaialable: 'ERROR' | 'NOT_AVAILABLE' = 'ERROR' + const draftSubmission = await getDraftSubmission( formSubmissionDraft, ).catch((err) => { @@ -211,19 +210,34 @@ async function generateLocalFormSubmissionDraftsFromStorage( `Could not fetch draft submission for draft: ${formSubmissionDraft.id}`, err, ) + if ( err instanceof OneBlinkAppsError && err.title === DRAFT_DATA_UNAVAILABLE_ERROR_TITLE ) { - errorOrNotAvaialable = 'NOT_AVAILABLE' + localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { + ...formSubmissionDraft, + downloadStatus: 'NOT_AVAILABLE', + }) + return } + + localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { + ...formSubmissionDraft, + downloadStatus: 'ERROR', + downloadError: err.message, + }) + return undefined }) - localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { - ...formSubmissionDraft, - draftSubmission: draftSubmission, - downloadStatus: draftSubmission ? 'SUCCESS' : errorOrNotAvaialable, - }) + if (draftSubmission) { + localFormSubmissionDraftsMap.set(formSubmissionDraft.id, { + ...formSubmissionDraft, + draftSubmission: draftSubmission, + downloadStatus: 'SUCCESS', + }) + } + await broadcastUpdate() } } @@ -233,11 +247,10 @@ async function generateLocalFormSubmissionDraftsFromStorage( ) return _orderBy(localFormSubmissionDrafts, (localFormSubmissionDraft) => { - return ( - localFormSubmissionDraft.draftSubmission?.createdAt || - getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions) - ?.createdAt - ) + return localFormSubmissionDraft.downloadStatus === 'SUCCESS' + ? localFormSubmissionDraft.draftSubmission?.createdAt + : getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions) + ?.createdAt }) } diff --git a/src/apps/types/submissions.ts b/src/apps/types/submissions.ts index 88a656cb1..e27242bf2 100644 --- a/src/apps/types/submissions.ts +++ b/src/apps/types/submissions.ts @@ -104,16 +104,21 @@ export type LocalFormSubmissionDraft = Omit< * remotely yet. */ versions: SubmissionTypes.FormSubmissionDraftVersion[] | undefined - /** - * The draft submission data. `undefined` if it has not been downloaded - * locally yet. - */ - draftSubmission: DraftSubmission | undefined /** `true` if the draft was created by a public user (not logged in). */ isPublic?: boolean - /** The status of the draft download */ - downloadStatus?: 'PENDING' | 'DOWNLOADING' | 'ERROR' | 'SUCCESS' -} +} & ( + | { + downloadStatus: 'PENDING' | 'DOWNLOADING' | 'NOT_AVAILABLE' + } + | { + downloadStatus: 'ERROR' + downloadError: string + } + | { + downloadStatus: 'SUCCESS' + draftSubmission: DraftSubmission + } + ) export type FormSubmission = NewFormSubmission & BaseFormSubmission & { From f2499e74076a74f61d3a0c81ec497a1466dc3666 Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 14:59:53 +1100 Subject: [PATCH 4/6] AP-7184 # keep draft order consistent --- src/apps/draft-service.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index 51f4593ab..a1c957fdd 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -152,9 +152,16 @@ async function generateLocalFormSubmissionDraftsFromStorage( ) async function broadcastUpdate() { - await executeDraftsListeners( - Array.from(localFormSubmissionDraftsMap.values()), + const draftsToBroadcast = Array.from(localFormSubmissionDraftsMap.values()) + + const orderedDrafts = _orderBy( + draftsToBroadcast, + (localFormSubmissionDraft) => + getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions) + ?.createdAt, ) + + await executeDraftsListeners(orderedDrafts) } // At this point we need to store the state of the drafts in localForage @@ -246,12 +253,12 @@ async function generateLocalFormSubmissionDraftsFromStorage( localFormSubmissionDraftsMap.values(), ) - return _orderBy(localFormSubmissionDrafts, (localFormSubmissionDraft) => { - return localFormSubmissionDraft.downloadStatus === 'SUCCESS' - ? localFormSubmissionDraft.draftSubmission?.createdAt - : getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions) - ?.createdAt - }) + return _orderBy( + localFormSubmissionDrafts, + (localFormSubmissionDraft) => + getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions) + ?.createdAt, + ) } function errorHandler(error: Error): Error { From d1b846f6472845888cb6136a1aa54e9d1c1400e2 Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 15:18:33 +1100 Subject: [PATCH 5/6] AP-7184 # removed user check --- src/apps/draft-service.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index a1c957fdd..78a9be465 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -165,16 +165,6 @@ async function generateLocalFormSubmissionDraftsFromStorage( } // At this point we need to store the state of the drafts in localForage - const username = getUsername() - if (!username) { - throw new OneBlinkAppsError( - 'You cannot download drafts until you have logged in. Please login and try again.', - { - requiresLogin: true, - }, - ) - } - const draftsToDownload: SubmissionTypes.FormSubmissionDraft[] = [] for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { From 4613e2558882695574a4db966025afd1aa82586c Mon Sep 17 00:00:00 2001 From: David Porter Date: Wed, 25 Feb 2026 16:05:00 +1100 Subject: [PATCH 6/6] AP-7184 # remove unnecessary code --- src/apps/draft-service.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/apps/draft-service.ts b/src/apps/draft-service.ts index 78a9be465..f15b3460a 100644 --- a/src/apps/draft-service.ts +++ b/src/apps/draft-service.ts @@ -872,19 +872,6 @@ async function syncDrafts({ console.log('Downloading drafts in the background') setAndBroadcastDrafts(localDraftsStorage) .then(async () => { - if (localDraftsStorage.syncedFormSubmissionDrafts.length) { - console.log( - 'Ensuring all draft data is available for offline use for synced drafts', - localDraftsStorage.syncedFormSubmissionDrafts, - ) - for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) { - await getDraftSubmission(formSubmissionDraft, abortSignal).catch( - (error) => { - console.warn('Could not download Draft Data as JSON', error) - }, - ) - } - } console.log('Finished syncing drafts.') }) .catch((error) => {