Skip to content

Commit

Permalink
Merge pull request #2027 from bcgov/1212-no-multiple-tab-save
Browse files Browse the repository at this point in the history
feat: cannot save outdated data
  • Loading branch information
AntBush authored Aug 8, 2023
2 parents 82a2404 + 5ff5986 commit 33f90e7
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 26 deletions.
11 changes: 9 additions & 2 deletions app/components/Form/ApplicationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getSectionNameFromPageNumber,
schemaToSubschemasArray,
} from '../../utils/schemaUtils';
import ConflictModal from './ConflictModal';

const verifyAllSubmissionsFilled = (formData?: SubmissionFieldsJSON) => {
const isSubmissionCompletedByFilled =
Expand Down Expand Up @@ -146,6 +147,7 @@ const ApplicationForm: React.FC<Props> = ({
rowId
jsonData
isEditable
updatedAt
formByFormSchemaId {
jsonSchema
}
Expand Down Expand Up @@ -178,6 +180,7 @@ const ApplicationForm: React.FC<Props> = ({
id: formDataId,
isEditable,
formByFormSchemaId: { jsonSchema },
updatedAt,
},
status,
} = application;
Expand Down Expand Up @@ -326,6 +329,7 @@ const ApplicationForm: React.FC<Props> = ({
jsonData: newFormData,
lastEditedPage: isSaveAsDraftBtn ? 'review' : lastEditedPage,
formDataRowId,
clientUpdatedAt: updatedAt,
},
},
optimisticResponse: {
Expand All @@ -340,7 +344,10 @@ const ApplicationForm: React.FC<Props> = ({
},
},
debounceKey: formDataId,
onError: () => {
onError: (error) => {
if (error.message.includes('Data is Out of Sync')) {
window.location.hash = 'data-out-of-sync';
}
setSavingError(
<>
There was an error saving your response.
Expand Down Expand Up @@ -398,7 +405,6 @@ const ApplicationForm: React.FC<Props> = ({
!isEditable
);
};

return (
<>
<Flex>
Expand Down Expand Up @@ -434,6 +440,7 @@ const ApplicationForm: React.FC<Props> = ({
status={status}
/>
</FormBase>
<ConflictModal id="data-out-of-sync" />
</>
);
};
Expand Down
43 changes: 43 additions & 0 deletions app/components/Form/ConflictModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Modal from '@button-inc/bcgov-theme/Modal';
import Button from '@button-inc/bcgov-theme/Button';
import { useRouter } from 'next/router';
import styled from 'styled-components';

const StyledText = styled.p`
text-align: center;
`;

const ButtonContainer = styled.div`
display: flex;
justify-content: center;
`;

const ConflictModal = ({ id }) => {
const router = useRouter();
return (
<Modal id={id}>
<Modal.Header>Error</Modal.Header>
<Modal.Content>
<StyledText>
The form could not save. This sometimes happens when your application
is open in multiple tabs.
</StyledText>
<StyledText>
Unfortunately any recent work on this page has been lost
</StyledText>
<ButtonContainer>
<Button
onClick={() => {
window.location.hash = '';
router.reload();
}}
>
Refresh & Continue
</Button>
</ButtonContainer>
</Modal.Content>
</Modal>
);
};

export default ConflictModal;
25 changes: 25 additions & 0 deletions app/cypress/integration/applicantportal/dashboard.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@ describe('The applicant dashboard', () => {
cy.findByRole('heading', { name: /^Project information/i }).should('exist');
cy.get('[id="root_projectTitle"]');

cy.intercept(
{
url: '/graphql',
method: 'POST',
},
(req) => {
req.on('before:response', (res) => {
console.log(res);
// Check if the response contains the specific error message
if (
res.body &&
res.body.errors &&
res.body.errors.some(
(error) => error.message === 'Data is Out of Sync'
)
) {
// Erase the window location hash
// cy.window().location.hash = '';
}
delete res.body.errors;
return res;
});
}
).as('graphql');

cy.get('[id="root_geographicAreaDescription"]').type('test');

cy.get('[id="root_projectDescription"]').type('test');
Expand Down
2 changes: 1 addition & 1 deletion app/lib/relay/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function createClientNetwork() {
urlMiddleware({
url: async () => Promise.resolve('/graphql'),
}),
debounceMutationMiddleware(),
debounceMutationMiddleware(500),
uploadMiddleware(),
batchMiddleware({
batchUrl: async () => Promise.resolve('/graphql'),
Expand Down
1 change: 1 addition & 0 deletions app/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -56825,6 +56825,7 @@ input UpdateApplicationFormInput {
formDataRowId: Int!
jsonData: JSON!
lastEditedPage: String!
clientUpdatedAt: Datetime!
}

"""The output of our `updateRfi` mutation."""
Expand Down
37 changes: 37 additions & 0 deletions app/tests/components/Form/ApplicationForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('The application form', () => {
'updateApplicationFormMutation',
{
input: {
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
formDataRowId: 123,
jsonData: {
projectInformation: {
Expand All @@ -108,6 +109,39 @@ describe('The application form', () => {
);
});

it('Results in error if data is out of sync', () => {
componentTestingHelper.loadQuery();
componentTestingHelper.renderComponent();

fireEvent.change(screen.getByLabelText(/project title/i), {
target: { value: 'test title' },
});

componentTestingHelper.expectMutationToBeCalled(
'updateApplicationFormMutation',
{
input: {
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
formDataRowId: 123,
jsonData: {
projectInformation: {
projectTitle: 'test title',
},
},
lastEditedPage: 'projectInformation',
},
}
);

act(() => {
componentTestingHelper.environment.mock.rejectMostRecentOperation(
new Error('Data is Out of Sync')
);
});

expect(window.location.hash).toBe('#data-out-of-sync');
});

it('sets lastEditedPage to the next page when the user clicks on "continue"', async () => {
componentTestingHelper.loadQuery();
componentTestingHelper.renderComponent();
Expand All @@ -120,6 +154,7 @@ describe('The application form', () => {
'updateApplicationFormMutation',
{
input: {
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
formDataRowId: 123,
jsonData: {
projectInformation: {},
Expand Down Expand Up @@ -299,6 +334,7 @@ describe('The application form', () => {
'updateApplicationFormMutation',
{
input: {
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
formDataRowId: 123,
jsonData: {
estimatedProjectEmployment: {
Expand Down Expand Up @@ -337,6 +373,7 @@ describe('The application form', () => {
'updateApplicationFormMutation',
{
input: {
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
formDataRowId: 123,
jsonData: {
projectFunding: {
Expand Down
35 changes: 35 additions & 0 deletions app/tests/components/Form/ConflictModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { useRouter } from 'next/router';
import ConflictModal from 'components/Form/ConflictModal';

jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));

describe('Conflict Modal tests', () => {
it('renders the modal with error header and text content', () => {
const { getByText } = render(<ConflictModal id="test-id" />);
expect(getByText('Error')).toBeInTheDocument();
expect(
getByText(
'The form could not save. This sometimes happens when your application is open in multiple tabs.'
)
).toBeInTheDocument();
expect(
getByText('Unfortunately any recent work on this page has been lost')
).toBeInTheDocument();
});

it('calls router.reload() when the button is clicked', () => {
const mockReload = jest.fn();
useRouter.mockImplementation(() => ({
reload: mockReload,
}));

const { getByText } = render(<ConflictModal id="test-id" />);
fireEvent.click(getByText('Refresh & Continue'));
expect(window.location.hash).toBe('');
expect(mockReload).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ describe('The submission form page', () => {
acknowledgements: { acknowledgementsList: acknowledgementsEnum },
},
lastEditedPage: 'review',
clientUpdatedAt: '2022-09-12T14:04:10.790848-07:00',
},
}
);
Expand Down
49 changes: 32 additions & 17 deletions db/deploy/mutations/update_application_form.sql
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
-- Deploy ccbc:mutations/update_application_form to pg

begin;
-- Since we're updating the function definition, need to manually drop
drop function ccbc_public.update_application_form;

create or replace function ccbc_public.update_application_form(form_data_row_id int, json_data jsonb, last_edited_page varchar)
create or replace function ccbc_public.update_application_form(form_data_row_id int, json_data jsonb, last_edited_page varchar, client_updated_at timestamp with time zone)
returns ccbc_public.form_data as
$$

update ccbc_public.form_data
set
-- use json concatenation operator to merge the provided json_data with the dynamic submission values
json_data = $2 || jsonb_build_object(
'submission', coalesce($2->'submission', jsonb_build_object()) || jsonb_build_object(
'submissionCompletedFor', $2->'organizationProfile'->'organizationName',
'submissionDate', (date_trunc('day', now(), 'America/Vancouver')::date)
)
),
last_edited_page = $3
where id = form_data_row_id
returning *;

$$ language sql;
$func$
declare
current_updated_at timestamp with time zone;
updated_form_data ccbc_public.form_data;
begin

select updated_at into current_updated_at from ccbc_public.form_data where id = form_data_row_id;
-- Adding a buffer, can be used to update if someone happens to have a version of the form that was opened <1 second from the last save
-- Risk is that there can still be overwritten data.
if client_updated_at < current_updated_at - interval '1 second' then
raise exception 'Data is Out of Sync';
end if;

update ccbc_public.form_data
set
-- use json concatenation operator to merge the provided json_data with the dynamic submission values
json_data = $2 || jsonb_build_object(
'submission', coalesce($2->'submission', jsonb_build_object()) || jsonb_build_object(
'submissionCompletedFor', $2->'organizationProfile'->'organizationName',
'submissionDate', (date_trunc('day', now(), 'America/Vancouver')::date)
)
),
last_edited_page = $3
where id = form_data_row_id
returning * into updated_form_data;

return updated_form_data;
end;
$func$ language plpgsql;

grant execute on function ccbc_public.update_application_form to ccbc_auth_user;

Expand Down
33 changes: 33 additions & 0 deletions db/deploy/mutations/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-- Deploy ccbc:mutations/update_application_form to pg

begin;

create or replace function ccbc_public.update_application_form(form_data_row_id int, json_data jsonb, last_edited_page varchar)
returns ccbc_public.form_data as
$$

update ccbc_public.form_data
set
-- use json concatenation operator to merge the provided json_data with the dynamic submission values
json_data = $2 || jsonb_build_object(
'submission', coalesce($2->'submission', jsonb_build_object()) || jsonb_build_object(
'submissionCompletedFor', $2->'organizationProfile'->'organizationName',
'submissionDate', (date_trunc('day', now(), 'America/Vancouver')::date)
)
),
last_edited_page = $3
where id = form_data_row_id
returning *;

$$ language sql;

grant execute on function ccbc_public.update_application_form to ccbc_auth_user;

comment on function ccbc_public.update_application_form is
$$
Mutation to update the "application" form.
This mutation should only be used by applicants as it sets the submission page data
$$;


commit;
30 changes: 29 additions & 1 deletion db/revert/mutations/update_application_form.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
-- Revert ccbc:mutations/update_application_form_data from pg
-- Deploy ccbc:mutations/update_application_form to pg

begin;

drop function ccbc_public.update_application_form;

create or replace function ccbc_public.update_application_form(form_data_row_id int, json_data jsonb, last_edited_page varchar)
returns ccbc_public.form_data as
$$

update ccbc_public.form_data
set
-- use json concatenation operator to merge the provided json_data with the dynamic submission values
json_data = $2 || jsonb_build_object(
'submission', coalesce($2->'submission', jsonb_build_object()) || jsonb_build_object(
'submissionCompletedFor', $2->'organizationProfile'->'organizationName',
'submissionDate', (date_trunc('day', now(), 'America/Vancouver')::date)
)
),
last_edited_page = $3
where id = form_data_row_id
returning *;

$$ language sql;

grant execute on function ccbc_public.update_application_form to ccbc_auth_user;

comment on function ccbc_public.update_application_form is
$$
Mutation to update the "application" form.
This mutation should only be used by applicants as it sets the submission page data
$$;


commit;
Loading

0 comments on commit 33f90e7

Please sign in to comment.