From 822a8d209900bdb587ba2f81c67f4b6b3958d865 Mon Sep 17 00:00:00 2001 From: Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> Date: Thu, 27 Jul 2023 13:37:09 -0700 Subject: [PATCH 1/8] feat: initial archive draft mutation and ui --- app/components/Dashboard/Row.tsx | 10 ++++ .../application/archiveApplication.ts | 24 +++++++++ app/schema/schema.graphql | 51 +++++++++++++++++++ db/deploy/mutations/archive_application.sql | 36 +++++++++++++ db/revert/mutations/archive_application.sql | 7 +++ db/sqitch.plan | 1 + 6 files changed, 129 insertions(+) create mode 100644 app/schema/mutations/application/archiveApplication.ts create mode 100644 db/deploy/mutations/archive_application.sql create mode 100644 db/revert/mutations/archive_application.sql diff --git a/app/components/Dashboard/Row.tsx b/app/components/Dashboard/Row.tsx index 3ad9253115..0e25c75b30 100644 --- a/app/components/Dashboard/Row.tsx +++ b/app/components/Dashboard/Row.tsx @@ -44,6 +44,7 @@ const Row = ({ application, formPages, reviewPage, setWithdrawId }) => { const isWithdrawn = application.status === 'withdrawn'; const isSubmitted = application.status === 'submitted'; + const isDraft = application.status === 'draft'; const getApplicationUrl = () => { if (isWithdrawn) { @@ -85,6 +86,15 @@ const Row = ({ application, formPages, reviewPage, setWithdrawId }) => { )} + {!ccbcNumber && isDraft && ( + + )} {application.hasRfiOpen && ( + useMutationWithErrorMessage( + mutation, + () => 'An error occurred while archiveing the application.' + ); + +export { mutation, useArchiveApplicationMutation }; diff --git a/app/schema/schema.graphql b/app/schema/schema.graphql index dad4289619..62eb927a9d 100644 --- a/app/schema/schema.graphql +++ b/app/schema/schema.graphql @@ -45839,6 +45839,14 @@ type Mutation { """ input: DeleteSowTab8ByRowIdInput! ): DeleteSowTab8Payload + + """Mutation to archive an application and its related data""" + archiveApplication( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: ArchiveApplicationInput! + ): ArchiveApplicationPayload archiveApplicationSow( """ The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. @@ -53443,6 +53451,49 @@ input DeleteSowTab8ByRowIdInput { rowId: Int! } +"""The output of our `archiveApplication` mutation.""" +type ArchiveApplicationPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + application: Application + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """Reads a single `Intake` that is related to this `Application`.""" + intakeByIntakeId: Intake + + """Reads a single `CcbcUser` that is related to this `Application`.""" + ccbcUserByCreatedBy: CcbcUser + + """Reads a single `CcbcUser` that is related to this `Application`.""" + ccbcUserByUpdatedBy: CcbcUser + + """Reads a single `CcbcUser` that is related to this `Application`.""" + ccbcUserByArchivedBy: CcbcUser + + """An edge for our `Application`. May be used by Relay 1.""" + applicationEdge( + """The method to use when ordering `Application`.""" + orderBy: [ApplicationsOrderBy!] = [PRIMARY_KEY_ASC] + ): ApplicationsEdge +} + +"""All input for the `archiveApplication` mutation.""" +input ArchiveApplicationInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + applicationRowId: Int! +} + """The output of our `archiveApplicationSow` mutation.""" type ArchiveApplicationSowPayload { """ diff --git a/db/deploy/mutations/archive_application.sql b/db/deploy/mutations/archive_application.sql new file mode 100644 index 0000000000..defd3fe8f1 --- /dev/null +++ b/db/deploy/mutations/archive_application.sql @@ -0,0 +1,36 @@ +-- Deploy ccbc:mutations/archive_application to pg + +begin; + +create or replace function ccbc_public.archive_application(application_row_id int) +returns ccbc_public.application as $$ +declare +application_status varchar; +form_data_id int; +form_data_status varchar(1000); +begin + +select ccbc_public.application_status(ccbc_public.application.*) into application_status from ccbc_public.application where id = application_row_id; + +select id, form_data_status_type_id from +ccbc_public.application_form_data((select row(ccbc_public.application.*)::ccbc_public.application from ccbc_public.application where id = application_row_id)) +into form_data_id, form_data_status; + + +-- Just one more check at the DB level making sure form is in draft +if application_status === 'draft' then + update ccbc_public.form_data set archived_at = now() where id = form_data_id; + update ccbc_public.application set archived_at = now() where id = application_row_id; +end if; + +return (select row(application.*)::ccbc_public.application from ccbc_public.application where id = application_row_id); + +end + +$$ language plpgsql; + +grant execute on function ccbc_public.archive_application to ccbc_auth_user; + +comment on function ccbc_public.archive_application is 'Mutation to archive an application and its related data'; + +commit; diff --git a/db/revert/mutations/archive_application.sql b/db/revert/mutations/archive_application.sql new file mode 100644 index 0000000000..a0903dce2c --- /dev/null +++ b/db/revert/mutations/archive_application.sql @@ -0,0 +1,7 @@ +-- Revert ccbc:mutations/archive_application from pg + +BEGIN; + +drop function if exists ccbc_public.archive_application; + +COMMIT; diff --git a/db/sqitch.plan b/db/sqitch.plan index 643fd0d07c..8484fe9bc8 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -372,3 +372,4 @@ mutations/create_change_request [mutations/create_change_request@1.85.0] 2023-07 tables/intake_005_add_description 2023-07-19T23:20:24Z Marcel Mueller # add description column functions/next_intake [functions/next_intake@1.85.0] 2023-07-24T20:08:07Z Marcel Mueller # grant analyst permissions mutations/create_intake [mutations/create_intake@1.88.0] 2023-07-24T21:51:46Z Marcel Mueller # add description input and verify intake number +mutations/archive_application 2023-07-26T22:48:25Z Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> # functionality to archive application and its form data From ca93da5be9963d1de902e5b93f2381a507e5cec5 Mon Sep 17 00:00:00 2001 From: Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:35:46 -0700 Subject: [PATCH 2/8] feat: only show non archived applications --- app/pages/applicantportal/dashboard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/applicantportal/dashboard.tsx b/app/pages/applicantportal/dashboard.tsx index 28e77134d3..f2996a8626 100644 --- a/app/pages/applicantportal/dashboard.tsx +++ b/app/pages/applicantportal/dashboard.tsx @@ -167,7 +167,7 @@ export const withRelayOptions = { const sub: string = ctx?.req?.claims?.sub; return { - formOwner: { owner: sub }, + formOwner: { owner: sub, archivedAt: null, archivedBy: null }, }; }, }; From 348c9e864de00d0d1f46621c929b90c19b39c641 Mon Sep 17 00:00:00 2001 From: Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:02:43 -0700 Subject: [PATCH 3/8] fix: application status check --- db/deploy/mutations/archive_application.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/deploy/mutations/archive_application.sql b/db/deploy/mutations/archive_application.sql index defd3fe8f1..266c21e007 100644 --- a/db/deploy/mutations/archive_application.sql +++ b/db/deploy/mutations/archive_application.sql @@ -18,7 +18,7 @@ into form_data_id, form_data_status; -- Just one more check at the DB level making sure form is in draft -if application_status === 'draft' then +if application_status = 'draft' then update ccbc_public.form_data set archived_at = now() where id = form_data_id; update ccbc_public.application set archived_at = now() where id = application_row_id; end if; From 19a35be67b9df27ec3f7cf95c0dcb97bbd09d470 Mon Sep 17 00:00:00 2001 From: Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:04:07 -0700 Subject: [PATCH 4/8] feat: delete draft application --- app/components/Dashboard/ArchiveModal.tsx | 92 +++++++++++++++++++++++ app/components/Dashboard/Row.tsx | 15 +++- app/components/Dashboard/Table.tsx | 5 +- 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 app/components/Dashboard/ArchiveModal.tsx diff --git a/app/components/Dashboard/ArchiveModal.tsx b/app/components/Dashboard/ArchiveModal.tsx new file mode 100644 index 0000000000..858989d416 --- /dev/null +++ b/app/components/Dashboard/ArchiveModal.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; +import Button from '@button-inc/bcgov-theme/Button'; +import Modal from '@button-inc/bcgov-theme/Modal'; +import styled from 'styled-components'; + +import { useArchiveApplicationMutation } from 'schema/mutations/application/archiveApplication'; +import X from './XIcon'; + +const StyledModal = styled(Modal)` + display: flex; + align-items: center; + z-index: 2; +`; + +const ModalButtons = styled('div')` + & button { + margin-right: 1em; + } +`; + +const StyledConfirmBox = styled('div')` + position: absolute; + left: 40px; + bottom: 3.5em; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + background: #1a5a96; + border-radius: 4px; + color: #ffffff; + padding: 16px 24px; + + & div:first-child { + margin-right: 1em; + } +`; + +const ArchiveModal = ({ id }) => { + const [successModal, setSuccessModal] = useState(false); + + const [archiveApplication] = useArchiveApplicationMutation(); + + const handleWithdraw = async () => { + archiveApplication({ + variables: { + input: { + applicationRowId: id, + }, + }, + onCompleted: () => setSuccessModal(true), + }); + }; + + return ( + <> + + + Delete draft + + + + + +

Are you sure you want to delete this draft application?

+ + + + + + + + + +
+
+ {successModal && ( + +
Application deleted
+ +
+ )} + + ); +}; + +export default ArchiveModal; diff --git a/app/components/Dashboard/Row.tsx b/app/components/Dashboard/Row.tsx index 0e25c75b30..937fc13df0 100644 --- a/app/components/Dashboard/Row.tsx +++ b/app/components/Dashboard/Row.tsx @@ -31,7 +31,13 @@ const StyledBtns = styled('div')` } `; -const Row = ({ application, formPages, reviewPage, setWithdrawId }) => { +const Row = ({ + application, + formPages, + reviewPage, + setWithdrawId, + setArchiveId, +}) => { const { ccbcNumber, intakeByIntakeId, formData, projectName, rowId, status } = application; @@ -88,8 +94,11 @@ const Row = ({ application, formPages, reviewPage, setWithdrawId }) => { )} {!ccbcNumber && isDraft && ( )} {application.hasRfiOpen && ( From b36f500585664e9354264bbf1cb4aa3bf74135cd Mon Sep 17 00:00:00 2001 From: Rafael Solorzano <61289255+rafasdc@users.noreply.github.com> Date: Mon, 31 Jul 2023 09:27:43 -0700 Subject: [PATCH 8/8] test: add delete tests --- .../components/Dashboard/Dashboard.test.tsx | 54 +++++++++++++++++++ .../pages/applicantportal/dashboard.test.tsx | 27 +++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/app/tests/components/Dashboard/Dashboard.test.tsx b/app/tests/components/Dashboard/Dashboard.test.tsx index c66ca62071..43b84a4619 100644 --- a/app/tests/components/Dashboard/Dashboard.test.tsx +++ b/app/tests/components/Dashboard/Dashboard.test.tsx @@ -333,4 +333,58 @@ describe('The Dashboard', () => { expect(screen.getByText('View')).toBeInTheDocument(); expect(screen.queryByTestId('withdraw-btn-test')).toBeNull(); }); + + it('Calls the correct mutation when the delete button is clicked', async () => { + const payload = { + Query() { + return { + allApplications: { + edges: [ + { + node: { + id: 'WyJhcHBsaWNhdGlvbnMiLDJd', + rowId: 2, + owner: '4e0ac88c-bf05-49ac-948f-7fd53c7a9fd6', + status: 'draft', + projectName: null, + ccbcNumber: null, + formData: { + lastEditedPage: '', + isEditable: true, + }, + intakeByIntakeId: { + ccbcIntakeNumber: 1, + closeTimestamp: '2024-09-09T13:49:23.513427-07:00', + openTimestamp: '2022-07-25T00:00:00-07:00', + }, + }, + }, + ], + }, + }; + }, + }; + componentTestingHelper.loadQuery(payload); + const user = userEvent.setup(); + + componentTestingHelper.renderComponent(); + expect(screen.getByText('Draft')).toBeInTheDocument(); + expect(screen.getByText('Delete')).toBeInTheDocument(); + expect(screen.getByTestId('archive-btn-test')).toBeInTheDocument(); + + const archiveBtn = screen.getByTestId('archive-btn-test'); + await user.click(archiveBtn); + + const archiveModalBtn = screen.getByTestId('archive-yes-btn'); + await user.click(archiveModalBtn); + + componentTestingHelper.expectMutationToBeCalled( + 'archiveApplicationMutation', + { + input: { + applicationRowId: 2, + }, + } + ); + }); }); diff --git a/app/tests/pages/applicantportal/dashboard.test.tsx b/app/tests/pages/applicantportal/dashboard.test.tsx index d4b48ca7f6..0b2929b801 100644 --- a/app/tests/pages/applicantportal/dashboard.test.tsx +++ b/app/tests/pages/applicantportal/dashboard.test.tsx @@ -88,6 +88,23 @@ const mockQueryPayload = { }, }, }, + { + node: { + id: 'WyJhcHBsaWNhdGlvbnMiLDJF', + rowId: 4, + owner: '4e0ac88c-bf05-49ac-948f-7fd53c7a9fd6', + status: 'draft', + projectName: 'test', + ccbcNumber: null, + formData: { + lastEditedPage: '', + isEditable: false, + }, + intakeByIntakeId: { + ccbcIntakeNumber: 3, + }, + }, + }, ], }, session: { @@ -236,12 +253,20 @@ describe('The index page', () => { expect(screen.getAllByText(`View`)[0]).toBeInTheDocument(); }); - it('displays the intake numbers for 2 applications', async () => { + it('displays the intake numbers for 3 applications', async () => { pageTestingHelper.loadQuery(); pageTestingHelper.renderPage(); expect(screen.getByText('1')).toBeInTheDocument(); expect(screen.getByText('2')).toBeInTheDocument(); + expect(screen.getByText('3')).toBeInTheDocument(); + }); + + it('should show the delete button for draft applications', async () => { + pageTestingHelper.loadQuery(); + pageTestingHelper.renderPage(); + + expect(screen.getByText('Delete')).toBeInTheDocument(); }); afterEach(() => {