Skip to content

Commit

Permalink
Saving datamodel along with list of changed fields and previous values (
Browse files Browse the repository at this point in the history
#899)

Co-authored-by: Ole Martin Handeland <[email protected]>
  • Loading branch information
olemartinorg and Ole Martin Handeland authored Feb 14, 2023
1 parent 0d13eb1 commit fc5994b
Show file tree
Hide file tree
Showing 13 changed files with 441 additions and 344 deletions.
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

0 comments on commit fc5994b

Please sign in to comment.