Skip to content

Saving datamodel along with list of changed fields and previous values #899

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Feb 14, 2023
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"cypress": "12.5.1",
"cypress-axe": "1.3.0",
"cypress-plugin-tab": "1.0.5",
"cypress-wait-until": "^1.7.2",
"dotenv": "16.0.3",
"eslint": "8.33.0",
"eslint-config-prettier": "8.6.0",
Expand Down
4 changes: 2 additions & 2 deletions src/__mocks__/formDataStateMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ export function getFormDataStateMock(customState?: Partial<IFormDataState>) {
'referencedGroup[1].inputField': 'Value from input field [1]',
'referencedGroup[2].inputField': 'Value from input field [2]',
},
hasSubmitted: false,
lastSavedFormData: {},
savingId: '',
submittingId: '',
responseInstance: false,
unsavedChanges: false,
saving: false,
ignoreWarnings: true,
};

Expand Down
39 changes: 22 additions & 17 deletions src/features/form/data/formDataSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import type { AnyAction } from 'redux';
import { fetchFormDataSaga, watchFetchFormDataInitialSaga } from 'src/features/form/data/fetch/fetchFormDataSagas';
import { autoSaveSaga, saveFormDataSaga, submitFormSaga } from 'src/features/form/data/submit/submitFormDataSagas';
import { deleteAttachmentReferenceSaga, updateFormDataSaga } from 'src/features/form/data/update/updateFormDataSagas';
import { FormLayoutActions } from 'src/features/form/layout/formLayoutSlice';
import { checkIfRuleShouldRunSaga } from 'src/features/form/rules/check/checkRulesSagas';
import { checkIfDataListShouldRefetchSaga } from 'src/shared/resources/dataLists/fetchDataListsSaga';
import { checkIfOptionsShouldRefetchSaga } from 'src/shared/resources/options/fetch/fetchOptionsSagas';
import { ProcessActions } from 'src/shared/resources/process/processSlice';
import { createSagaSlice } from 'src/shared/resources/utils/sagaSlice';
import type { IFormDataState } from 'src/features/form/data';
import type { IFormData, IFormDataState } from 'src/features/form/data';
import type {
IDeleteAttachmentReference,
IFetchFormData,
Expand All @@ -24,12 +23,12 @@ import type { MkActionType } from 'src/shared/resources/utils/sagaSlice';

export const initialState: IFormDataState = {
formData: {},
error: null,
responseInstance: null,
lastSavedFormData: {},
unsavedChanges: false,
saving: false,
submittingId: '',
savingId: '',
hasSubmitted: false,
error: null,
ignoreWarnings: false,
};

Expand All @@ -51,6 +50,7 @@ const formDataSlice = createSagaSlice((mkAction: MkActionType<IFormDataState>) =
reducer: (state, action) => {
const { formData } = action.payload;
state.formData = formData;
state.lastSavedFormData = formData;
},
}),
fetchRejected: mkAction<IFormDataRejected>({
Expand All @@ -66,12 +66,22 @@ const formDataSlice = createSagaSlice((mkAction: MkActionType<IFormDataState>) =
},
}),
submit: mkAction<ISubmitDataAction>({
takeLatest: submitFormSaga,
takeEvery: submitFormSaga,
reducer: (state, action) => {
const { apiMode, componentId } = action.payload;
state.savingId = apiMode !== 'Complete' ? componentId : state.savingId;
state.submittingId = apiMode === 'Complete' ? componentId : state.submittingId;
state.hasSubmitted = apiMode === 'Complete';
},
}),
savingStarted: mkAction<void>({
reducer: (state) => {
state.saving = true;
},
}),
savingEnded: mkAction<{ model: IFormData }>({
reducer: (state, action) => {
state.saving = false;
state.lastSavedFormData = action.payload.model;
},
}),
submitFulfilled: mkAction<void>({
Expand All @@ -92,22 +102,23 @@ const formDataSlice = createSagaSlice((mkAction: MkActionType<IFormDataState>) =
update: mkAction<IUpdateFormData>({
takeEvery: updateFormDataSaga,
reducer: (state) => {
state.hasSubmitted = false;
state.ignoreWarnings = false;
},
}),
updateFulfilled: mkAction<IUpdateFormDataFulfilled>({
takeLatest: [checkIfRuleShouldRunSaga, autoSaveSaga],
takeEvery: [checkIfOptionsShouldRefetchSaga, checkIfDataListShouldRefetchSaga],
reducer: (state, action) => {
const { field, data } = action.payload;
const { field, data, skipAutoSave } = action.payload;
// Remove if data is null, undefined or empty string
if (data === undefined || data === null || data === '') {
delete state.formData[field];
} else {
state.formData[field] = data;
}
state.unsavedChanges = true;
if (!skipAutoSave) {
state.unsavedChanges = true;
}
},
}),
updateRejected: mkAction<IFormDataRejected>({
Expand All @@ -117,20 +128,14 @@ const formDataSlice = createSagaSlice((mkAction: MkActionType<IFormDataState>) =
},
}),
save: mkAction<ISaveAction>({
takeLatest: saveFormDataSaga,
takeEvery: saveFormDataSaga,
}),
deleteAttachmentReference: mkAction<IDeleteAttachmentReference>({
takeLatest: deleteAttachmentReferenceSaga,
}),
},
extraReducers: (builder) => {
builder
.addCase(FormLayoutActions.updateCurrentView, (state) => {
state.hasSubmitted = true;
})
.addCase(FormLayoutActions.updateCurrentViewFulfilled, (state) => {
state.hasSubmitted = false;
})
.addMatcher(isProcessAction, (state) => {
state.submittingId = '';
})
Expand Down
22 changes: 19 additions & 3 deletions src/features/form/data/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
export interface IFormDataState {
// This is the constantly mutated object containing the current form data/data model. In key-value form.
formData: IFormData;
error: Error | null;
responseInstance: any;

// Last saved form data. This one is a copy of the above, and will be copied from there after each save. This means
// we can remember everything that changed, along with previous values, and pass them to the backend when we save
// values. Do not change this unless you know what you're doing.
lastSavedFormData: IFormData;

// These two control the state machine for saving data. We should only perform one save/PUT request at a time, because
// we might get back data that we have to merge into our data model (from `ProcessDataWrite`), and running multiple
// save requests at the same time may overwrite data or cause non-atomic saves the backend does not expect. If
// `unsavedChanges` is set it means we currently have data in `formData` that has not yet been PUT - and if `saving`
// is set it means we're currently sending a request (so the next one, if triggered, will wait until the last save
// has completed). At last, the saved `formData` is set into `lastSavedFormData` (after potential changes from
// `ProcessDataWrite` on the server).
unsavedChanges: boolean;
saving: boolean;

// The component IDs which triggered submitting/saving last time
submittingId: string;
savingId: string;
hasSubmitted: boolean;

error: Error | null;
ignoreWarnings: boolean;
}

Expand Down
Loading