Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 102 additions & 28 deletions src/apps/draft-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -92,6 +96,7 @@ function generateLocalFormSubmissionDraftsFromDraftSubmissions(
taskActionId: draftSubmission.taskCompletion?.taskAction.taskActionId,
draftSubmission,
versions: undefined,
downloadStatus: 'SUCCESS',
})
}
}
Expand Down Expand Up @@ -125,7 +130,9 @@ async function generatePublicLocalFormSubmissionDraftsFromStorage(
)

return _orderBy(localFormSubmissionDrafts, (localFormSubmissionDraft) => {
return localFormSubmissionDraft.draftSubmission?.createdAt
return localFormSubmissionDraft.downloadStatus === 'SUCCESS'
? localFormSubmissionDraft.draftSubmission?.createdAt
: undefined
})
}

Expand All @@ -144,6 +151,22 @@ async function generateLocalFormSubmissionDraftsFromStorage(
deletedDraftIds,
)

async function broadcastUpdate() {
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
const draftsToDownload: SubmissionTypes.FormSubmissionDraft[] = []

for (const formSubmissionDraft of localDraftsStorage.syncedFormSubmissionDrafts) {
if (
// Unsycned version of draft takes priority over the synced version
Expand All @@ -153,33 +176,79 @@ async function generateLocalFormSubmissionDraftsFromStorage(
// Remove any drafts deleted while offline
!deletedDraftIds.has(formSubmissionDraft.id)
) {
draftsToDownload.push(formSubmissionDraft)
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'PENDING',
})
}
}

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) => {
console.warn(
`Could not fetch draft submission for draft: ${formSubmissionDraft.id}`,
err,
)

if (
err instanceof OneBlinkAppsError &&
err.title === DRAFT_DATA_UNAVAILABLE_ERROR_TITLE
) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'NOT_AVAILABLE',
})
return
}
Comment on lines 203 to +220
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could change getDraftSubmission() to return undefined for missing data and change the other places its being called to throw the same error if it returns undefined, instead of relying on a string as the title. Please disregard if you like this implementation ❤️

Copy link
Contributor Author

@divporter divporter Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'ERROR',
downloadError: err.message,
})

return undefined
})
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
draftSubmission,
})
if (draftSubmission) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
draftSubmission: draftSubmission,
downloadStatus: 'SUCCESS',
})
}

await broadcastUpdate()
}
}

const localFormSubmissionDrafts = Array.from(
localFormSubmissionDraftsMap.values(),
)

return _orderBy(localFormSubmissionDrafts, (localFormSubmissionDraft) => {
return (
localFormSubmissionDraft.draftSubmission?.createdAt ||
return _orderBy(
localFormSubmissionDrafts,
(localFormSubmissionDraft) =>
getLatestFormSubmissionDraftVersion(localFormSubmissionDraft.versions)
?.createdAt
)
})
?.createdAt,
)
}

function errorHandler(error: Error): Error {
Expand Down Expand Up @@ -800,24 +869,29 @@ 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('Downloading drafts in the background')
setAndBroadcastDrafts(localDraftsStorage)
.then(async () => {
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
})

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) {
Expand Down
4 changes: 3 additions & 1 deletion src/apps/services/api/drafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
)
}
Expand Down
19 changes: 13 additions & 6 deletions src/apps/types/submissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +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
}
} & (
| {
downloadStatus: 'PENDING' | 'DOWNLOADING' | 'NOT_AVAILABLE'
}
| {
downloadStatus: 'ERROR'
downloadError: string
}
| {
downloadStatus: 'SUCCESS'
draftSubmission: DraftSubmission
}
)

export type FormSubmission = NewFormSubmission &
BaseFormSubmission & {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useDrafts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down