From 9659733fb0c083017ad2d93f23dbcc0d1e1042ab Mon Sep 17 00:00:00 2001 From: Luna McNulty Date: Wed, 27 Sep 2023 11:17:21 -0400 Subject: [PATCH 1/4] wip --- .../components/checklists/CheckListForm.js | 22 ++++++++++++++---- .../components/checklists/ChecklistsIndex.js | 23 ++++++++++++++++--- site/gatsby-site/src/pages/apps/checklists.js | 4 +++- .../aiidprod/checklists/rules.json | 15 ++++++++++-- .../aiidprod/checklists/schema.json | 1 + 5 files changed, 54 insertions(+), 11 deletions(-) diff --git a/site/gatsby-site/src/components/checklists/CheckListForm.js b/site/gatsby-site/src/components/checklists/CheckListForm.js index 7abc9a6d6f..d288170d89 100644 --- a/site/gatsby-site/src/components/checklists/CheckListForm.js +++ b/site/gatsby-site/src/components/checklists/CheckListForm.js @@ -28,6 +28,9 @@ export default function CheckListForm({ tags, isSubmitting, }) { + + console.log(`values`, values); + const [deleteChecklist] = useMutation(DELETE_CHECKLIST); const confirmDeleteChecklist = async (id) => { @@ -48,7 +51,11 @@ export default function CheckListForm({ const [allPrecedents, setAllPrecedents] = useState([]); - const searchTags = [...values['tags_goals'], ...values['tags_methods'], ...values['tags_other']]; + const searchTags = [ + ...(values['tags_goals'] || []), + ...(values['tags_methods'] || []), + ...(values['tags_other'] || []) + ]; useEffect(() => { searchRisks({ values, setFieldValue, setRisksLoading, setAllPrecedents, setSaveStatus }); @@ -92,6 +99,7 @@ export default function CheckListForm({ submitForm(); }; + return (
@@ -182,7 +190,7 @@ export default function CheckListForm({ */} - {!risksLoading && values.risks.length == 0 && ( + {!risksLoading && values.risks?.length == 0 && ( No risks yet. Try adding some system tags. )} { - const queryTags = [...values['tags_goals'], ...values['tags_methods'], ...values['tags_other']]; + const queryTags = [ + ...(values['tags_goals'] || []), + ...(values['tags_methods'] || []), + ...(values['tags_other'] || []) + ]; if (queryTags.length == 0) return; @@ -427,7 +439,7 @@ const searchRisks = async ({ startClosed: true, }; - const notDuplicate = [...risksToAdd, ...values.risks].every( + const notDuplicate = [...risksToAdd, ...(values.risks || [])].every( (existingRisk) => !areDuplicates(existingRisk, newRisk) ); @@ -440,7 +452,7 @@ const searchRisks = async ({ } } } - setFieldValue('risks', values.risks.concat(risksToAdd)); + setFieldValue('risks', (values.risks || []).concat(risksToAdd)); setAllPrecedents(allPrecedents); // Example result: diff --git a/site/gatsby-site/src/components/checklists/ChecklistsIndex.js b/site/gatsby-site/src/components/checklists/ChecklistsIndex.js index 3551ea3b4a..7943334055 100644 --- a/site/gatsby-site/src/components/checklists/ChecklistsIndex.js +++ b/site/gatsby-site/src/components/checklists/ChecklistsIndex.js @@ -3,8 +3,10 @@ import { Button, Spinner } from 'flowbite-react'; import Card from 'elements/Card'; import { Trans, useTranslation } from 'react-i18next'; import { LocalizedLink } from 'plugins/gatsby-theme-i18n'; -import { useQuery, useMutation } from '@apollo/client'; +import { useQuery, useMutation, useApolloClient } from '@apollo/client'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useUserContext } from '../../contexts/userContext'; + import ExportDropdown from 'components/checklists/ExportDropdown'; import { DeleteButton, removeTypename, statusIcon, statusColor } from 'utils/checklists'; import { FIND_CHECKLISTS, INSERT_CHECKLIST, DELETE_CHECKLIST } from '../../graphql/checklists'; @@ -18,6 +20,8 @@ export default function ChecklistsIndex() { const { data: checklistsData, loading: checklistsLoading } = useQuery(FIND_CHECKLISTS); + const { user } = useUserContext(); + const [checklists, setChecklists] = useState([]); useEffect(() => { @@ -35,8 +39,21 @@ export default function ChecklistsIndex() { Risk Checklists - {userIsOwner && ( - confirmDeleteChecklist(values.id)}> - Delete - - )} - - - +
+ {owner.first_name} {owner.last_name} +
+ + + + + {userIsOwner && ( + confirmDeleteChecklist(values.id)}> + Delete + + )} + +
This feature is in development. Data entered will not be retained. @@ -312,50 +318,43 @@ const OtherTagInput = ({ values, tags, setFieldValue, userIsOwner }) => ( /> ); -const Header = (props) => ( -
{props.children}
-); +const Header = (props) => { + const className = ` + border-[rgb(230,236,241)] border-b-[1px] + lg:min-h-[5.5rem] + flex justify-between flex-wrap gap-4 + pb-2 -mt-2 + ${props.className} + `; -const HeaderRow = (props) => ( -
- {props.children} -
-); + return
{props.children}
; +}; -const HeaderControls = (props) => ( -
- {props.children} -
-); +const HeaderInfo = (props) => { + const className = `flex flex-col justify-center ${props.className}`; -const SideBySide = (props) => ( -
*]:w-full [&>*]:md:w-1/2 [&>*]:h-full ${props.className}`, - }} - > - {props.children} -
-); + return
{props.children}
; +}; + +const HeaderControls = (props) => { + const className = `flex flex-wrap md:flex-nowrap shrink-0 gap-2 items-center max-w-full ${props.className}`; + + return
{props.children}
; +}; + +const SideBySide = (props) => { + const className = ` + flex flex-col md:flex-row gap-2 + [&>*]:w-full [&>*]:md:w-1/2 + [&>*]:h-full + ${props.className} + `; + + return
{props.children}
; +}; -// TODO: Unless the network connection is fairly slow -// (check with throttling in browser devtools), -// submissions happen fast enough that this never rerenders with -// isSubmitting == true. -// This needs to be better optimized for the render cycle. -function SavingIndicator({ isSubmitting, submissionError }) { - const className = 'text-lg text-gray-500 inline-block mx-4'; +function SavingIndicator({ isSubmitting, submissionError, className }) { + className = `text-lg text-gray-500 inline-block ${className}`; if (isSubmitting) { return ( diff --git a/site/gatsby-site/src/components/checklists/ChecklistsIndex.js b/site/gatsby-site/src/components/checklists/ChecklistsIndex.js index 546b6f3485..6143903381 100644 --- a/site/gatsby-site/src/components/checklists/ChecklistsIndex.js +++ b/site/gatsby-site/src/components/checklists/ChecklistsIndex.js @@ -18,7 +18,7 @@ import { import { FIND_CHECKLISTS, INSERT_CHECKLIST, DELETE_CHECKLIST } from '../../graphql/checklists'; import useToastContext, { SEVERITY } from '../../hooks/useToast'; -const ChecklistsIndex = () => { +const ChecklistsIndex = ({ users }) => { const { t } = useTranslation(); const { user } = useUserContext(); @@ -79,14 +79,18 @@ const ChecklistsIndex = () => {
{checklists.map((checklist) => ( - + u.userId == checklist.owner_id)} + {...{ checklist, setChecklists }} + /> ))}
); }; -const CheckListCard = ({ checklist, setChecklists }) => { +const CheckListCard = ({ checklist, setChecklists, owner }) => { const { t } = useTranslation(); const { user } = useUserContext(); @@ -158,20 +162,27 @@ const CheckListCard = ({ checklist, setChecklists }) => { )} -
    - {(checklist.risks || []) - .filter((r) => r.risk_status != 'Not Applicable') - .map((risk) => ( -
  • - - {risk.title} -
  • - ))} -
+
+ {owner && ( + + by {owner.first_name} {owner.last_name} + + )} +
    + {(checklist.risks || []) + .filter((r) => r.risk_status != 'Not Applicable') + .map((risk) => ( +
  • + + {risk.title} +
  • + ))} +
+
); }; diff --git a/site/gatsby-site/src/pages/apps/checklists.js b/site/gatsby-site/src/pages/apps/checklists.js index 94cabc2b0a..5de91d0a80 100644 --- a/site/gatsby-site/src/pages/apps/checklists.js +++ b/site/gatsby-site/src/pages/apps/checklists.js @@ -16,9 +16,13 @@ import { FIND_CHECKLIST, UPDATE_CHECKLIST } from '../../graphql/checklists'; const ChecklistsPage = (props) => { const { location: { pathname }, - data: { taxa, classifications }, + data, } = props; + const [taxa, classifications, users] = ['taxa', 'classifications', 'users'].map( + (e) => data[e]?.nodes + ); + const { t } = useTranslation(); return ( @@ -26,12 +30,12 @@ const ChecklistsPage = (props) => { {t('Risk Checklists')} - + ); }; -const ChecklistsPageBody = ({ taxa, classifications, t }) => { +const ChecklistsPageBody = ({ taxa, classifications, users }) => { const [query] = useQueryParams({ id: StringParam, }); @@ -95,7 +99,7 @@ const ChecklistsPageBody = ({ taxa, classifications, t }) => { if (!query.id) { return ( <> - + ); } @@ -117,7 +121,7 @@ const ChecklistsPageBody = ({ taxa, classifications, t }) => { } */ } > - {(FormProps) => } + {(FormProps) => } ); } @@ -126,8 +130,8 @@ const ChecklistsPageBody = ({ taxa, classifications, t }) => { const classificationsToTags = ({ classifications, taxa }) => { const tags = new Set(); - for (const classification of classifications.nodes) { - const taxonomy = taxa.nodes.find((t) => t.namespace == classification.namespace); + for (const classification of classifications) { + const taxonomy = taxa.find((t) => t.namespace == classification.namespace); for (const attribute of classification.attributes) { const field = taxonomy.field_list.find((f) => f.short_name == attribute.short_name); @@ -207,6 +211,13 @@ export const query = graphql` } } } + users: allMongodbCustomDataUsers { + nodes { + userId + first_name + last_name + } + } classifications: allMongodbAiidprodClassifications { nodes { namespace From c5c7fef5af612c48dcd077f4e77aeddbdf97aa6b Mon Sep 17 00:00:00 2001 From: Luna McNulty Date: Fri, 13 Oct 2023 17:03:52 -0400 Subject: [PATCH 3/4] Add tests, remove unused/commented code --- .../e2e/integration/apps/checklistsForm.cy.js | 148 ++++++++++++++++++ .../integration/apps/checklistsIndex.cy.js | 88 +++++++++++ .../components/checklists/CheckListForm.js | 11 +- .../components/checklists/ChecklistsIndex.js | 73 +++++---- site/gatsby-site/src/graphql/checklists.js | 8 - site/gatsby-site/src/pages/apps/checklists.js | 14 +- 6 files changed, 283 insertions(+), 59 deletions(-) create mode 100644 site/gatsby-site/cypress/e2e/integration/apps/checklistsForm.cy.js create mode 100644 site/gatsby-site/cypress/e2e/integration/apps/checklistsIndex.cy.js diff --git a/site/gatsby-site/cypress/e2e/integration/apps/checklistsForm.cy.js b/site/gatsby-site/cypress/e2e/integration/apps/checklistsForm.cy.js new file mode 100644 index 0000000000..9c9f9474d0 --- /dev/null +++ b/site/gatsby-site/cypress/e2e/integration/apps/checklistsForm.cy.js @@ -0,0 +1,148 @@ +import { maybeIt } from '../../../support/utils'; +const { gql } = require('@apollo/client'); + +describe('Checklists App Form', () => { + const url = '/apps/checklists?id=testChecklist'; + + const usersQuery = { + query: gql` + { + users { + userId + roles + adminData { + email + } + } + } + `, + timeout: 60000, // mongodb admin api is extremely slow + }; + + it('Should have read-only access for non-logged-in users', () => { + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'findChecklist', + 'findChecklist', + { + data: { + checklist: { + __typename: 'Checklist', + about: '', + id: 'testChecklist', + name: 'Test Checklist', + owner_id: 'a-fake-user-id-that-does-not-exist', + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + }, + } + ); + + cy.visit(url); + + cy.wait(['@findChecklist']); + + cy.waitForStableDOM(); + + cy.get('[data-cy="checklist-form"] textarea:not([disabled])').should('not.exist'); + + cy.get('[data-cy="checklist-form"] input:not([disabled]):not([readonly])').should('not.exist'); + }); + + maybeIt('Should have read-only access for logged-in non-owners', () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'findChecklist', + 'findChecklist', + { + data: { + checklist: { + __typename: 'Checklist', + about: '', + id: 'testChecklist', + name: 'Test Checklist', + owner_id: 'a-fake-user-id-that-does-not-exist', + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + }, + } + ); + + cy.visit(url); + + cy.wait(['@findChecklist']); + + cy.waitForStableDOM(); + + cy.get('[data-cy="checklist-form"] textarea:not([disabled])').should('not.exist'); + + cy.get('[data-cy="checklist-form"] input:not([disabled]):not([readonly])').should('not.exist'); + }); + + maybeIt('Should allow editing for owner', () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.query(usersQuery).then(({ data: { users } }) => { + const user = users.find((user) => user.adminData.email == Cypress.env('e2eUsername')); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'findChecklist', + 'findChecklist', + { + data: { + checklist: { + __typename: 'Checklist', + about: '', + id: 'testChecklist', + name: 'Test Checklist', + owner_id: user.userId, + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + }, + } + ); + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'upsertChecklist', + 'upsertChecklist', + { + data: { + checklist: { + __typename: 'Checklist', + about: "It's a system that does something probably.", + id: 'testChecklist', + name: 'Test Checklist', + owner_id: user.userId, + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + }, + } + ); + + cy.visit(url); + + cy.wait(['@findChecklist']); + + cy.waitForStableDOM(); + + cy.get('[data-cy="about"]').type("It's a system that does something probably."); + + cy.wait(['@upsertChecklist']); + }); + }); +}); diff --git a/site/gatsby-site/cypress/e2e/integration/apps/checklistsIndex.cy.js b/site/gatsby-site/cypress/e2e/integration/apps/checklistsIndex.cy.js new file mode 100644 index 0000000000..c562946a70 --- /dev/null +++ b/site/gatsby-site/cypress/e2e/integration/apps/checklistsIndex.cy.js @@ -0,0 +1,88 @@ +import { maybeIt } from '../../../support/utils'; + +const { gql } = require('@apollo/client'); + +describe('Checklists App Index', () => { + const url = '/apps/checklists'; + + const newChecklistButtonQuery = '#new-checklist-button'; + + const usersQuery = { + query: gql` + { + users { + userId + roles + adminData { + email + } + } + } + `, + timeout: 60000, // mongodb admin api is extremely slow + }; + + it('Should not display New Checklist button as non-logged-in user', () => { + cy.visit(url); + + cy.get(newChecklistButtonQuery).should('not.exist'); + }); + + maybeIt('Should display New Checklist button as logged-in user', () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.visit(url); + + cy.get(newChecklistButtonQuery).should('exist'); + }); + + maybeIt('Should show delete buttons only for owned checklists', () => { + cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); + + cy.query(usersQuery).then(({ data: { users } }) => { + const user = users.find((user) => user.adminData.email == Cypress.env('e2eUsername')); + + cy.conditionalIntercept( + '**/graphql', + (req) => req.body.operationName == 'findChecklists', + 'findChecklists', + { + data: { + checklists: [ + { + about: '', + id: 'fakeChecklist1', + name: 'My Checklist', + owner_id: user.userId, + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + { + about: '', + id: 'fakeChecklist2', + name: "Somebody Else's Checklist", + owner_id: 'aFakeUserId', + risks: [], + tags_goals: [], + tags_methods: [], + tags_other: [], + }, + ], + }, + } + ); + + cy.visit(url); + + cy.wait(['@findChecklists']); + + cy.waitForStableDOM(); + + cy.get('[data-cy="checklist-card"]:first-child button').contains('Delete').should('exist'); + + cy.get('[data-cy="checklist-card"]:last-child button').contains('Delete').should('not.exist'); + }); + }); +}); diff --git a/site/gatsby-site/src/components/checklists/CheckListForm.js b/site/gatsby-site/src/components/checklists/CheckListForm.js index a74878a71d..132cf91db5 100644 --- a/site/gatsby-site/src/components/checklists/CheckListForm.js +++ b/site/gatsby-site/src/components/checklists/CheckListForm.js @@ -110,7 +110,7 @@ export default function CheckListForm({ }; return ( - +
@@ -125,9 +125,11 @@ export default function CheckListForm({ disabled={!userIsOwner} /> -
- {owner.first_name} {owner.last_name} -
+ {owner && owner.first_name && owner.last_name && ( +
+ {owner.first_name} {owner.last_name} +
+ )}
@@ -226,6 +228,7 @@ const AboutSystem = ({ formAbout, debouncedSetFieldValue, userIsOwner }) => { About System