Skip to content
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

Feat: multiple project managers #304

Merged
merged 25 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b874a2d
chore: create_project() does not create a project_manager form change
dleard Feb 25, 2022
a9b55bf
chore: start updating front-end
dleard Feb 25, 2022
1a4318a
feat: multiple project managers can be added to a revision
dleard Feb 26, 2022
34cfe98
chore: add a custom delete mutation
dleard Feb 28, 2022
64bb2ad
chore: remove old project_manager_form_change computed column
dleard Feb 28, 2022
7774164
chore: fix copy-paste error
dleard Feb 28, 2022
43ec23b
test: update projectRevision jest test
dleard Feb 28, 2022
d5fb237
test: add unit test for ProjectManagerForm
dleard Feb 28, 2022
68af6c6
chore: linting fixes
dleard Feb 28, 2022
ce8e527
chore: computed column should ignore changes with an archive operation
dleard Mar 4, 2022
6a48b3e
refactor: move each individual form into a single fragment component
dleard Mar 4, 2022
604b619
chore: fix validation refs
dleard Mar 4, 2022
38ecd71
chore: properly type typescript 'any' types
dleard Mar 7, 2022
d84b2ff
test: update revision page unit test
dleard Mar 8, 2022
a9f4579
test: add ProjectManager unit tests
dleard Mar 8, 2022
9127dab
test: add test for update mutation
dleard Mar 8, 2022
4f70271
chore: fix incorrect interface types
dleard Mar 8, 2022
6e68125
test: update cypress test
dleard Mar 8, 2022
295fa62
chore: project manager fields are optional
dleard Mar 9, 2022
c82b5c0
test: update cypress test
dleard Mar 9, 2022
a2607ae
refactor: move allCifUsers fragment into component that uses it
dleard Mar 9, 2022
db173cf
chore: fix typo
dleard Mar 9, 2022
43cbccc
chore: remove unnecessary fragment
dleard Mar 9, 2022
6c0b00f
chore: remove unnecessary React.Fragment
dleard Mar 9, 2022
60f8e08
chore: remove unnecessary React import
dleard Mar 9, 2022
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
246 changes: 227 additions & 19 deletions app/components/Form/ProjectManagerForm.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
import type { JSONSchema7 } from "json-schema";
import React, { forwardRef, useMemo } from "react";
import { JSONSchema7, JSONSchema7Definition } from "json-schema";
import React, { useMemo, MutableRefObject } from "react";
import { graphql, useFragment } from "react-relay";
import { ProjectManagerForm_allUsers$key } from "__generated__/ProjectManagerForm_allUsers.graphql";
import { ProjectManagerForm_managerFormChange$key } from "__generated__/ProjectManagerForm_managerFormChange.graphql";
import { ProjectManagerForm_query$key } from "__generated__/ProjectManagerForm_query.graphql";
import FormBase from "./FormBase";
import projectManagerSchema from "data/jsonSchemaForm/projectManagerSchema";
import FormComponentProps from "./Interfaces/FormComponentProps";
import Grid from "@button-inc/bcgov-theme/Grid";
import { Button } from "@button-inc/bcgov-theme";
import useAddManagerToRevisionMutation from "mutations/Manager/addManagerToRevision";
import useDeleteManagerFromRevisionMutation from "mutations/Manager/deleteManagerFromRevision";
import { mutation as updateFormChangeMutation } from "mutations/FormChange/updateFormChange";
import useDebouncedMutation from "mutations/useDebouncedMutation";
import EmptyObjectFieldTemplate from "lib/theme/EmptyObjectFieldTemplate";
import FieldLabel from "lib/theme/widgets/FieldLabel";

interface Props extends FormComponentProps {
allUsers: ProjectManagerForm_allUsers$key;
managerFormChange: ProjectManagerForm_managerFormChange$key;
query: ProjectManagerForm_query$key;
projectId: number;
projectRevisionId: string;
projectRevisionRowId: number;
formRefs: MutableRefObject<{}>;
}

const uiSchema = {
"ui:title": "Project Manager",
cifUserId: {
"ui:placeholder": "Select a Project Manager",
"ui:col-md": 12,
"bcgov:size": "small",
"ui:widget": "SearchWidget",
"ui:options": {
label: false,
},
},
};

const ProjecManagerForm: React.ForwardRefRenderFunction<any, Props> = (
props,
ref
) => {
const ProjectManagerForm: React.FC<Props> = (props) => {
const {
query,
projectId,
projectRevisionId,
projectRevisionRowId,
formRefs,
} = props;

const { allCifUsers } = useFragment(
graphql`
fragment ProjectManagerForm_allUsers on Query {
fragment ProjectManagerForm_query on Query {
allCifUsers {
edges {
node {
Expand All @@ -38,27 +59,214 @@ const ProjecManagerForm: React.ForwardRefRenderFunction<any, Props> = (
}
}
`,
props.allUsers
query
);

const schema: JSONSchema7 = useMemo(() => {
const initialSchema = projectManagerSchema;
const change = useFragment(
graphql`
fragment ProjectManagerForm_managerFormChange on ManagerFormChangesByLabelCompositeReturn {
projectManagerLabel {
id
rowId
label
}
formChange {
id
operation
newFormData
}
}
`,
props.managerFormChange
);

initialSchema.properties.cifUserId = {
...initialSchema.properties.cifUserId,
// Dynamically build the schema from the list of cif_users
const managerSchema = useMemo(() => {
const schema = projectManagerSchema;
schema.properties.cifUserId = {
...schema.properties.cifUserId,
anyOf: allCifUsers.edges.map(({ node }) => {
return {
type: "number",
title: `${node.firstName} ${node.lastName}`,
enum: [node.rowId],
value: node.rowId,
};
} as JSONSchema7Definition;
}),
};
return initialSchema as JSONSchema7;

return schema as JSONSchema7;
}, [allCifUsers]);

return <FormBase {...props} ref={ref} schema={schema} uiSchema={uiSchema} />;
// Add a manager to the project revision
const [applyAddManagerToRevision] = useAddManagerToRevisionMutation();
const addManager = (data: {
cifUserId: number;
projectManagerLabelId: number;
projectId: number;
}) => {
applyAddManagerToRevision({
variables: {
projectRevision: projectRevisionId,
projectRevisionId: projectRevisionRowId,
newFormData: data,
},
matthieu-foucault marked this conversation as resolved.
Show resolved Hide resolved
optimisticResponse: {
createFormChange: {
query: {
projectRevision: {
managerFormChanges: {
edges: {
change: {
projectManagerLabel: {},
formChange: {
id: "new",
newFormData: data,
},
},
},
},
},
},
},
},
});
};

const [applyUpdateFormChangeMutation] = useDebouncedMutation(
updateFormChangeMutation
);

// Delete a manager from the project revision
const [discardFormChange, discardInFlight] =
useDeleteManagerFromRevisionMutation();
const deleteManager = (id: string) => {
if (change.formChange.operation === "CREATE")
discardFormChange({
variables: {
input: {
id: id,
},
projectRevision: projectRevisionId,
},
onError: (error) => {
console.log(error);
},
});
else
applyUpdateFormChangeMutation({
variables: {
input: {
id: id,
formChangePatch: {
operation: "ARCHIVE",
},
},
},
optimisticResponse: {
updateFormChange: {
formChange: {
id: id,
newFormData: {},
},
},
},
onError: (error) => {
console.log(error);
},
debounceKey: id,
});
};

// Update an existing project_manager form change if it exists, otherwise create one
const createOrUpdateFormChange = (
formChangeId: string,
labelId: number,
formChange: { cifUserId: number }
) => {
const data = {
...formChange,
projectManagerLabelId: labelId,
projectId: projectId,
};

// If a form_change already exists, and the payload contains a cifUserId update it
if (formChangeId && formChange?.cifUserId) {
applyUpdateFormChangeMutation({
variables: {
input: {
id: formChangeId,
formChangePatch: {
newFormData: data,
},
},
},
optimisticResponse: {
updateFormChange: {
formChange: {
id: formChangeId,
newFormData: data,
},
},
},
debounceKey: formChangeId,
});
// If a form_change does not exist, and the payload contains a cifUserId create a form_change record
} else if (formChange?.cifUserId) {
addManager(data);
}
// If a form_change exists, and the payload does not contain a cifUserId delete it
else if (formChangeId && !formChange.cifUserId && !discardInFlight) {
deleteManager(formChangeId);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel this entire pattern of creating / updating / deleting form changes depending on whether we have a certain field or not will be reused over and over, just a noting a thought for later on how we could reuse that logic

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, this may be refactored into a hook. We can do that after we have two examples of this pattern (there will be a second one in #280 )

};

return (
<>
<React.Fragment key={change.projectManagerLabel.id}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need those two fragments

<Grid.Row>
<FieldLabel
label={change.projectManagerLabel.label}
required={false}
htmlFor={change.projectManagerLabel.id}
uiSchema={uiSchema}
/>
</Grid.Row>
<Grid.Row>
<Grid.Col span={6}>
<FormBase
id={`form-manager-${change.projectManagerLabel.label}`}
idPrefix={`form-${change.projectManagerLabel.id}`}
ref={(el) =>
(formRefs.current[change.projectManagerLabel.id] = el)
}
formData={change.formChange?.newFormData}
onChange={(data) => {
createOrUpdateFormChange(
change.formChange?.id,
change.projectManagerLabel.rowId,
data.formData
);
}}
schema={managerSchema}
uiSchema={uiSchema}
ObjectFieldTemplate={EmptyObjectFieldTemplate}
/>
</Grid.Col>
<Grid.Col span={4}>
<Button
disabled={discardInFlight || !change.formChange?.id}
variant="secondary"
size="small"
onClick={() => deleteManager(change.formChange?.id)}
>
Clear
</Button>
</Grid.Col>
</Grid.Row>
</React.Fragment>
</>
);
};

export default forwardRef(ProjecManagerForm);
export default ProjectManagerForm;
96 changes: 96 additions & 0 deletions app/components/Form/ProjectManagerFormGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useRef, MutableRefObject } from "react";
import { graphql, useFragment } from "react-relay";
import { ProjectManagerFormGroup_query$key } from "__generated__/ProjectManagerFormGroup_query.graphql";
import { ProjectManagerFormGroup_revision$key } from "__generated__/ProjectManagerFormGroup_revision.graphql";
import { ValidatingFormProps } from "./Interfaces/FormValidationTypes";
import validateFormWithErrors from "lib/helpers/validateFormWithErrors";
import Grid from "@button-inc/bcgov-theme/Grid";
import FormBorder from "lib/theme/components/FormBorder";
import ProjectManagerForm from "./ProjectManagerForm";

interface Props extends ValidatingFormProps {
query: ProjectManagerFormGroup_query$key;
revision: ProjectManagerFormGroup_revision$key;
projectManagerFormRef: MutableRefObject<{}>;
}

const ProjectManagerFormGroup: React.FC<Props> = (props) => {
const formRefs = useRef({});
const query = useFragment(
graphql`
fragment ProjectManagerFormGroup_query on Query {
...ProjectManagerForm_query
}
`,
props.query
);

const projectRevision = useFragment(
graphql`
fragment ProjectManagerFormGroup_revision on ProjectRevision {
id
rowId
managerFormChanges: projectManagerFormChangesByLabel {
edges {
node {
projectManagerLabel {
id
}
...ProjectManagerForm_managerFormChange
}
}
}
projectFormChange {
formDataRecordId
}
}
`,
props.revision
);

props.setValidatingForm({
selfValidate: () => {
return Object.keys(formRefs.current).reduce((agg, formId) => {
const formObject = formRefs.current[formId];
return [...agg, ...validateFormWithErrors(formObject)];
}, []);
},
});

return (
<div>
<Grid cols={10} align="center">
<Grid.Row>
<Grid.Col span={10}>
<FormBorder title="Project Managers">
{projectRevision.managerFormChanges.edges.map(({ node }) => (
<ProjectManagerForm
key={node.projectManagerLabel.id}
managerFormChange={node}
query={query}
projectId={projectRevision.projectFormChange.formDataRecordId}
projectRevisionId={projectRevision.id}
projectRevisionRowId={projectRevision.rowId}
formRefs={formRefs}
/>
))}
</FormBorder>
</Grid.Col>
</Grid.Row>
</Grid>
<style jsx>{`
div :global(button.pg-button) {
margin-left: 0.4em;
margin-right: 0em;
}
div :global(.right-aligned-column) {
display: flex;
justify-content: flex-end;
align-items: flex-start;
}
`}</style>
</div>
);
};

export default ProjectManagerFormGroup;
2 changes: 1 addition & 1 deletion app/cypress/integration/cif/project-revision/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("the new project page", () => {
component: "Project Page with errors",
variant: "empty",
});
cy.get(".error-detail").should("have.length", 8);
cy.get(".error-detail").should("have.length", 7);
// Renders a custom error message for a custom format validation error
cy.get(".error-detail")
.first()
Expand Down
Loading