From b47a558db1b00e6d1d6af394059e5859aae64d83 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 23 Oct 2024 20:53:13 -0400 Subject: [PATCH 01/28] Create standard goals migration, sort goals alphabetically by name in dropdown --- .../ActivityReport/Pages/goalsObjectives.js | 10 ++++ .../20241024002207-insert-standard-goals.js | 55 +++++++++++++++++++ src/services/goalTemplates.ts | 2 +- 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/migrations/20241024002207-insert-standard-goals.js diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js index 57ca8d1494..433e66d1d2 100644 --- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js +++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js @@ -149,6 +149,16 @@ const GoalsObjectives = ({ try { const fetchedGoalTemplates = await getGoalTemplates(grantIds); + fetchedGoalTemplates.sort((a, b) => { + if (a.name < b.name) { + return -1; + } + if (a.name > b.name) { + return 1; + } + return 0; + }); + // format goalTemplates const formattedGoalTemplates = fetchedGoalTemplates.map((gt) => ({ ...gt, diff --git a/src/migrations/20241024002207-insert-standard-goals.js b/src/migrations/20241024002207-insert-standard-goals.js new file mode 100644 index 0000000000..3b738dc991 --- /dev/null +++ b/src/migrations/20241024002207-insert-standard-goals.js @@ -0,0 +1,55 @@ +const md5 = require('md5'); +const { prepMigration } = require('../lib/migration'); +const { CREATION_METHOD } = require('../constants'); + +const standardGoalTexts = [ + '(Teaching Practices) The recipient will implement systems and services that ensure effective adult-child interactions and responsive care using effective teaching and home visiting practice', + '(Child Safety) The recipient will implement systems and services to ensure that everyone promotes a culture of program safety, so children are kept safe at all times.', + '(ERSEA) The recipient will implement systems and services to ensure their Eligibility, Recruitment, Selection, Enrollment, and Attendance procedures meet the needs of their children, families, and community.', + '(Governance) The recipient will establish and maintain a formal structure for program governance that ensures clear roles, responsibilities and procedures, effective training, and representation of families and the community.', + '(Fiscal Management) The recipient will implement strong fiscal management and reporting systems to ensure the safeguarding of federal funds, facilities, and resources.', + '(Development and Learning) The recipient will implement child development and early learning services that are developmentally, culturally, and linguistically appropriate for all children and families.', + '(Mental Health) The recipient will implement systems and services that promote the mental and behavioral health of all children and families.', + '(Physical Health) The recipient will implement systems and services that ensure expectant families and children\'s health, oral health, and nutrition needs are met in developmentally, culturally, and linguistically appropriate ways', + '(DEIA) The recipient will implement comprehensive systems and services that promote diversity, equity, inclusion, accessibility, and belonging.', + '(Family Engagement) The recipient will implement family engagement strategies that are relationship-based and culturally and linguistically appropriate.', + '(Family Support) The recipient will implement collaborative systems and services with families and community partners to support family well-being and the needs of vulnerable families.', + '(Professional Development) The recipient will implement a systematic approach to staff training and professional development that assists staff in acquiring, refining, or increasing the knowledge and skills needed to provide high-quality, comprehensive services.', + '(Workforce Development) The recipient will implement systems to recruit, hire, onboard, support, and retain staff to ensure all program staff have sufficient knowledge, experience, competencies, and resources to fulfill the roles and responsibilities of their positions.', + '(New Leaders) The recipient\'s new leader(s) will identify and use resources, professional development, and access to necessary regulations to meet the needs of their role(s) and responsibilities.', + '(CQI and Data) The recipient will implement data and ongoing monitoring systems to inform continuous quality improvement.', + '(Program Structure) The recipient will implement management and program structures that provide effective oversight and administration and meet the needs of the staff, families, and communities.', + '(Disaster Recovery) The recipient will implement systems and services to support children, families, and staff with recovering from disasters.', + '(RAN investigation) The recipient will implement systems and services to address a reported child incident during the RO investigation.', +]; +const standardGoal = (templateName) => ({ + creationMethod: CREATION_METHOD.CURATED, + hash: md5(templateName), + createdAt: new Date(), + updatedAt: new Date(), +}); + +const standardGoalTemplates = standardGoalTexts.map((templateName) => ({ + ...standardGoal(templateName), + templateName, +})); + +module.exports = { + up: async (queryInterface) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + await queryInterface.bulkInsert('GoalTemplates', standardGoalTemplates, { transaction }); + }, + ), + + down: async (queryInterface) => queryInterface.sequelize.transaction( + async (transaction) => { + await prepMigration(queryInterface, transaction, __filename); + + await queryInterface.sequelize.query(` + DELETE FROM "GoalTemplates" + WHERE "creationMethod" = 'Curated' AND "templateName" IN (${standardGoalTexts.map((templateName) => `'${templateName}'`).join(',')}); + `, { transaction }); + }, + ), +}; diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts index d21e1c0735..a55332372d 100644 --- a/src/services/goalTemplates.ts +++ b/src/services/goalTemplates.ts @@ -125,7 +125,7 @@ export async function getCuratedTemplates( { regionId: null }, ], }, - ORDER: [['name', 'ASC']], + ORDER: [['name', 'ASC']], // not sure why this doesn't work; [[ 'templateName', 'ASC' ]] also doesn't do the trick }); } From ba1208e7899e35ee9bee116b59c3bf1b89750218 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 23 Oct 2024 20:56:57 -0400 Subject: [PATCH 02/28] Add back "templateNameModifiedAt" to migration --- src/migrations/20241024002207-insert-standard-goals.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/migrations/20241024002207-insert-standard-goals.js b/src/migrations/20241024002207-insert-standard-goals.js index 3b738dc991..55db32b43b 100644 --- a/src/migrations/20241024002207-insert-standard-goals.js +++ b/src/migrations/20241024002207-insert-standard-goals.js @@ -27,6 +27,7 @@ const standardGoal = (templateName) => ({ hash: md5(templateName), createdAt: new Date(), updatedAt: new Date(), + templateNameModifiedAt: new Date(), }); const standardGoalTemplates = standardGoalTexts.map((templateName) => ({ From be76a7d33f65043f288a16c90b80f32b57f33bc3 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 23 Oct 2024 20:59:30 -0400 Subject: [PATCH 03/28] Fix seeders --- src/seeders/20210127161802-add-goals.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/seeders/20210127161802-add-goals.js b/src/seeders/20210127161802-add-goals.js index 1e30e7d41c..0e4a1abdc4 100644 --- a/src/seeders/20210127161802-add-goals.js +++ b/src/seeders/20210127161802-add-goals.js @@ -16,7 +16,7 @@ module.exports = { up: async (queryInterface) => { const goalTemplates = [ { - id: 2, + id: 50, // 2, hash: queryInterface.sequelize.fn('md5', queryInterface.sequelize.fn('TRIM', 'Identify strategies to support Professional Development with an emphasis on Staff Wellness and Social Emotional Development.')), templateName: 'Identify strategies to support Professional Development with an emphasis on Staff Wellness and Social Emotional Development.', createdAt: now, @@ -25,7 +25,7 @@ module.exports = { creationMethod: 'Automatic', }, { - id: 3, + id: 51, // 3, hash: queryInterface.sequelize.fn('md5', queryInterface.sequelize.fn('TRIM', 'Recipient supports and sustains comprehensive, integrated and systemic SR, PFCE, and PD processes and services.')), templateName: 'Recipient supports and sustains comprehensive, integrated and systemic SR, PFCE, and PD processes and services.', createdAt: now, @@ -34,7 +34,7 @@ module.exports = { creationMethod: 'Automatic', }, { - id: 4, + id: 52, // 4, hash: queryInterface.sequelize.fn('md5', queryInterface.sequelize.fn('TRIM', bulletedGoal)), templateName: bulletedGoal, createdAt: now, @@ -43,7 +43,7 @@ module.exports = { creationMethod: 'Automatic', }, { - id: 5, + id: 53, // 5, hash: queryInterface.sequelize.fn('md5', queryInterface.sequelize.fn('TRIM', longGoal)), templateName: longGoal, createdAt: now, @@ -60,7 +60,7 @@ module.exports = { status: 'Not Started', createdAt: now, updatedAt: now, - goalTemplateId: 2, + goalTemplateId: 50, grantId: 1, onAR: false, onApprovedAR: false, @@ -72,7 +72,7 @@ module.exports = { status: 'Not Started', createdAt: now, updatedAt: now, - goalTemplateId: 3, + goalTemplateId: 51, grantId: 1, onAR: false, onApprovedAR: false, @@ -83,7 +83,7 @@ module.exports = { status: 'Not Started', createdAt: now, updatedAt: now, - goalTemplateId: 4, + goalTemplateId: 52, grantId: 1, onAR: false, onApprovedAR: false, @@ -94,7 +94,7 @@ module.exports = { status: 'Not Started', createdAt: now, updatedAt: now, - goalTemplateId: 5, + goalTemplateId: 53, grantId: 2, onAR: false, onApprovedAR: false, From a1a03dd6d89a802e6c90b93c8b7468b0ae1cb756 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Thu, 24 Oct 2024 15:48:09 -0400 Subject: [PATCH 04/28] Update language --- src/migrations/20241024002207-insert-standard-goals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/migrations/20241024002207-insert-standard-goals.js b/src/migrations/20241024002207-insert-standard-goals.js index 55db32b43b..7ed64166d9 100644 --- a/src/migrations/20241024002207-insert-standard-goals.js +++ b/src/migrations/20241024002207-insert-standard-goals.js @@ -3,7 +3,7 @@ const { prepMigration } = require('../lib/migration'); const { CREATION_METHOD } = require('../constants'); const standardGoalTexts = [ - '(Teaching Practices) The recipient will implement systems and services that ensure effective adult-child interactions and responsive care using effective teaching and home visiting practice', + '(Teaching Practices) The recipient will implement systems and services that ensure effective adult-child interactions and responsive care using effective teaching and home visiting practices.', '(Child Safety) The recipient will implement systems and services to ensure that everyone promotes a culture of program safety, so children are kept safe at all times.', '(ERSEA) The recipient will implement systems and services to ensure their Eligibility, Recruitment, Selection, Enrollment, and Attendance procedures meet the needs of their children, families, and community.', '(Governance) The recipient will establish and maintain a formal structure for program governance that ensures clear roles, responsibilities and procedures, effective training, and representation of families and the community.', From 05b11a1d8894de466ecc146d6fe9f041c9731b74 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Fri, 25 Oct 2024 11:00:17 -0400 Subject: [PATCH 05/28] Update src/migrations/20241024002207-insert-standard-goals.js Co-authored-by: GarrettEHill --- src/migrations/20241024002207-insert-standard-goals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/migrations/20241024002207-insert-standard-goals.js b/src/migrations/20241024002207-insert-standard-goals.js index 7ed64166d9..1680761d64 100644 --- a/src/migrations/20241024002207-insert-standard-goals.js +++ b/src/migrations/20241024002207-insert-standard-goals.js @@ -49,7 +49,7 @@ module.exports = { await queryInterface.sequelize.query(` DELETE FROM "GoalTemplates" - WHERE "creationMethod" = 'Curated' AND "templateName" IN (${standardGoalTexts.map((templateName) => `'${templateName}'`).join(',')}); + WHERE "creationMethod" = '${CREATION_METHOD.CURATED}' AND "templateName" IN (${standardGoalTexts.map((templateName) => `'${templateName}'`).join(',')}); `, { transaction }); }, ), From 53359a897103e56009be7c50a36455b3f85f763e Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Mon, 28 Oct 2024 15:42:01 -0400 Subject: [PATCH 06/28] Fix case on service and remove sort from FE --- .../src/pages/ActivityReport/Pages/goalsObjectives.js | 10 ---------- src/services/goalTemplates.ts | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js index 433e66d1d2..57ca8d1494 100644 --- a/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js +++ b/frontend/src/pages/ActivityReport/Pages/goalsObjectives.js @@ -149,16 +149,6 @@ const GoalsObjectives = ({ try { const fetchedGoalTemplates = await getGoalTemplates(grantIds); - fetchedGoalTemplates.sort((a, b) => { - if (a.name < b.name) { - return -1; - } - if (a.name > b.name) { - return 1; - } - return 0; - }); - // format goalTemplates const formattedGoalTemplates = fetchedGoalTemplates.map((gt) => ({ ...gt, diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts index a55332372d..1fbd08f5d2 100644 --- a/src/services/goalTemplates.ts +++ b/src/services/goalTemplates.ts @@ -125,7 +125,7 @@ export async function getCuratedTemplates( { regionId: null }, ], }, - ORDER: [['name', 'ASC']], // not sure why this doesn't work; [[ 'templateName', 'ASC' ]] also doesn't do the trick + order: [[ 'templateName', 'ASC' ]], }); } From 29e34230344c3b9ddb694fe7fe6e2ce7b53e6a7e Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Tue, 29 Oct 2024 11:01:08 -0400 Subject: [PATCH 07/28] Fix lint error --- src/services/goalTemplates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts index 1fbd08f5d2..64b5f18d8f 100644 --- a/src/services/goalTemplates.ts +++ b/src/services/goalTemplates.ts @@ -125,7 +125,7 @@ export async function getCuratedTemplates( { regionId: null }, ], }, - order: [[ 'templateName', 'ASC' ]], + order: [['templateName', 'ASC']], }); } From 1aeb88263e7239d15efb71e71940d3c9093caae0 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Tue, 29 Oct 2024 11:26:10 -0400 Subject: [PATCH 08/28] Deploy standard goals to dev --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c376f45636..18ec92d46b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -560,7 +560,7 @@ parameters: type: string dev_git_branch: # change to feature branch to test deployment description: "Name of github branch that will deploy to dev" - default: "al-ttahub-3402-connect-be-overview" + default: "mb/TTAHUB-3484/insert-standard-goals" type: string sandbox_git_branch: # change to feature branch to test deployment default: "mb/TTAHUB-3483/checkbox-to-activity-reports" From 26494f163de9c2d26a166ccb673879de11e54d83 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Tue, 29 Oct 2024 14:29:45 -0400 Subject: [PATCH 09/28] Add some helpful fixes --- .../20241024002207-insert-standard-goals.js | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/migrations/20241024002207-insert-standard-goals.js b/src/migrations/20241024002207-insert-standard-goals.js index 1680761d64..c19bb89b93 100644 --- a/src/migrations/20241024002207-insert-standard-goals.js +++ b/src/migrations/20241024002207-insert-standard-goals.js @@ -22,12 +22,15 @@ const standardGoalTexts = [ '(Disaster Recovery) The recipient will implement systems and services to support children, families, and staff with recovering from disasters.', '(RAN investigation) The recipient will implement systems and services to address a reported child incident during the RO investigation.', ]; + +const now = new Date(); + const standardGoal = (templateName) => ({ creationMethod: CREATION_METHOD.CURATED, hash: md5(templateName), - createdAt: new Date(), - updatedAt: new Date(), - templateNameModifiedAt: new Date(), + createdAt: now, + updatedAt: now, + templateNameModifiedAt: now, }); const standardGoalTemplates = standardGoalTexts.map((templateName) => ({ @@ -47,10 +50,14 @@ module.exports = { async (transaction) => { await prepMigration(queryInterface, transaction, __filename); - await queryInterface.sequelize.query(` - DELETE FROM "GoalTemplates" - WHERE "creationMethod" = '${CREATION_METHOD.CURATED}' AND "templateName" IN (${standardGoalTexts.map((templateName) => `'${templateName}'`).join(',')}); - `, { transaction }); + await queryInterface.bulkDelete( + 'GoalTemplates', + { + creationMethod: CREATION_METHOD.CURATED, + templateName: standardGoalTexts.map((templateName) => templateName), + }, + { transaction }, + ); }, ), }; From 08a4840b8fbc4c1ba1030d4e236d80dd221887cf Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:23:27 -0400 Subject: [PATCH 10/28] Frontend updates to allow setting source on new curated goals --- frontend/src/components/GoalForm/Form.js | 5 +- frontend/src/components/GoalForm/index.js | 5 ++ .../src/fetchers/__tests__/goalTemplates.js | 19 ++++++++ frontend/src/fetchers/goalTemplates.js | 7 +++ .../Pages/components/GoalForm.js | 4 +- .../Pages/components/GoalPicker.js | 47 ++++++++++++------- .../Pages/components/__tests__/GoalPicker.js | 25 ++++++++-- 7 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 frontend/src/fetchers/__tests__/goalTemplates.js diff --git a/frontend/src/components/GoalForm/Form.js b/frontend/src/components/GoalForm/Form.js index 7e1899112d..8b5336bfa1 100644 --- a/frontend/src/components/GoalForm/Form.js +++ b/frontend/src/components/GoalForm/Form.js @@ -48,6 +48,7 @@ export default function Form({ isOnApprovedReport, isOnReport, isCurated, + isSourceEditable, isNew, status, datePickerKey, @@ -185,7 +186,7 @@ export default function Form({ { + describe('getGoalTemplateSource', () => { + afterEach(() => fetchMock.reset()); + it('should fetch goal template source', async () => { + fetchMock.get(join(goalTemplatesUrl, '1', 'source', '?grantIds=1'), { source: 'source' }); + const source = await getGoalTemplateSource(1, [1]); + + expect(source).toEqual({ source: 'source' }); + }); + }); +}); diff --git a/frontend/src/fetchers/goalTemplates.js b/frontend/src/fetchers/goalTemplates.js index 66690c31a0..ab58c43c99 100644 --- a/frontend/src/fetchers/goalTemplates.js +++ b/frontend/src/fetchers/goalTemplates.js @@ -13,6 +13,13 @@ export async function getGoalTemplates(grantIds) { return response.json(); } +export async function getGoalTemplateSource(templateId, grantIds) { + const params = grantIds.map((goalId) => `grantIds=${goalId}`).join('&'); + const url = join(goalTemplatesUrl, String(templateId), 'source', `?${params}`); + const response = await get(url); + return response.json(); +} + export async function getGoalTemplatePrompts(templateId, goalIds = []) { const params = goalIds.map((goalId) => `goalIds=${goalId}`).join('&'); const url = join(goalTemplatesUrl, String(templateId), 'prompts', `?${params}`); diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js index 08fa70571d..3fec298193 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js +++ b/frontend/src/pages/ActivityReport/Pages/components/GoalForm.js @@ -150,6 +150,7 @@ export default function GoalForm({ const prompts = combinePrompts(templatePrompts, goal.prompts); const isCurated = goal.isCurated || false; + const { isSourceEditable } = goal; return ( <> @@ -181,7 +182,7 @@ export default function GoalForm({ { - onChange(goal); - if (goal.isCurated) { - const prompts = await getGoalTemplatePrompts(goal.goalTemplateId, goal.goalIds); - if (prompts) { - setTemplatePrompts(prompts); + try { + if (goal.isCurated) { + const [prompts, source] = await Promise.all([ + getGoalTemplatePrompts(goal.goalTemplateId, goal.goalIds), + // eslint-disable-next-line max-len + getGoalTemplateSource(goal.goalTemplateId, activityRecipients.map((ar) => ar.activityRecipientId)), + ]); + + onChange({ + ...goal, + source: source.source, + }); + + if (prompts) { + setTemplatePrompts(prompts); + } + } else { + onChange(goal); + setTemplatePrompts(false); } - } else { - setTemplatePrompts(false); - } - // update the goal date forcefully - // also update the date picker key to force a re-render - setValue('goalEndDate', goal.endDate || ''); - if (goal.goalIds) { - setDatePickerKey(`DPKEY-${goal.goalIds.join('-')}`); - } + // update the goal date forcefully + // also update the date picker key to force a re-render + setValue('goalEndDate', goal.endDate || ''); + if (goal.goalIds) { + setDatePickerKey(`DPKEY-${goal.goalIds.join('-')}`); + } - setSelectedGoal(null); + setSelectedGoal(null); + } catch (err) { + // handle this + console.log(err); + } }; const onKeep = async () => { diff --git a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js index f64c9903ab..9e4d7924cb 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js +++ b/frontend/src/pages/ActivityReport/Pages/components/__tests__/GoalPicker.js @@ -40,6 +40,7 @@ const GP = ({ availableGoals, selectedGoals, goalForEditing, goalTemplates }) => role: 'central office', }, collaborators: [], + activityRecipients: [{ activityRecipientId: 1 }], }, }); @@ -283,6 +284,10 @@ describe('GoalPicker', () => { prompt: 'WHYYYYYYYY?', }, ]); + fetchMock.get('/api/goal-templates/1/source?grantIds=1', { + source: 'source', + }); + const availableGoals = [{ label: 'Goal 1', value: 1, @@ -291,17 +296,25 @@ describe('GoalPicker', () => { goalTemplateId: 1, }]; - renderGoalPicker(availableGoals, null); + act(() => { + renderGoalPicker(availableGoals, null); + }); const selector = await screen.findByLabelText(/Select recipient's goal*/i); const [availableGoal] = availableGoals; - await selectEvent.select(selector, [availableGoal.label]); + await act(async () => { + await selectEvent.select(selector, [availableGoal.label]); + }); const input = document.querySelector('[name="goalForEditing"]'); expect(input.value).toBe(availableGoal.value.toString()); }); it('with prompts', async () => { + fetchMock.get('/api/goal-templates/1/source?grantIds=1', { + source: 'source', + }); + fetchMock.get('/api/goal-templates/1/prompts?goalIds=1', [ { type: 'multiselect', @@ -321,12 +334,16 @@ describe('GoalPicker', () => { goalTemplateId: 1, }]; - renderGoalPicker(availableGoals, null); + act(() => { + renderGoalPicker(availableGoals, null); + }); const selector = await screen.findByLabelText(/Select recipient's goal*/i); const [availableGoal] = availableGoals; - await selectEvent.select(selector, [availableGoal.label]); + await act(async () => { + await selectEvent.select(selector, [availableGoal.label]); + }); const input = document.querySelector('[name="goalForEditing"]'); expect(input.value).toBe(availableGoal.value.toString()); From 4b17c56bd7f69f887fdc24d239972801f42c1fed Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:24:13 -0400 Subject: [PATCH 11/28] Remove console --- frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js index 8a4f305d34..5a25eae0cb 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js +++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js @@ -162,7 +162,6 @@ const GoalPicker = ({ setSelectedGoal(null); } catch (err) { // handle this - console.log(err); } }; From 9aae15f769fd2f870acbcd19fb9c5f106afce56e Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:24:47 -0400 Subject: [PATCH 12/28] Update models and reducers --- src/goalServices/goals.js | 1 + src/goalServices/goalsByIdAndRecipient.ts | 4 +++- src/goalServices/reduceGoals.ts | 1 + src/goalServices/types.ts | 2 ++ src/models/goal.js | 10 ++++++++++ src/models/goalTemplate.js | 6 ++++++ 6 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js index aa5f1aacd4..1f090a8d27 100644 --- a/src/goalServices/goals.js +++ b/src/goalServices/goals.js @@ -124,6 +124,7 @@ export async function goalsByIdsAndActivityReport(id, activityReportId) { ['name', 'label'], 'id', 'name', + 'isSourceEditable', ], where: { id, diff --git a/src/goalServices/goalsByIdAndRecipient.ts b/src/goalServices/goalsByIdAndRecipient.ts index f335274869..b1f988a353 100644 --- a/src/goalServices/goalsByIdAndRecipient.ts +++ b/src/goalServices/goalsByIdAndRecipient.ts @@ -49,6 +49,7 @@ const OPTIONS_FOR_GOAL_FORM_QUERY = (id: number[] | number, recipientId: number) 'name', 'status', 'source', + 'isSourceEditable', 'onAR', 'onApprovedAR', 'id', @@ -184,7 +185,7 @@ const OPTIONS_FOR_GOAL_FORM_QUERY = (id: number[] | number, recipientId: number) { model: GoalTemplate, as: 'goalTemplate', - attributes: [], + attributes: ['source', 'creationMethod'], required: false, }, { @@ -233,6 +234,7 @@ export default async function goalsByIdAndRecipient(ids: number | number[], reci const reformattedGoals = goals.map((goal) => ({ ...goal, + isSourceEditable: goal.isSourceEditable, isReopenedGoal: wasGoalPreviouslyClosed(goal), objectives: goal.objectives .map((objective: IObjectiveModelInstance) => ({ diff --git a/src/goalServices/reduceGoals.ts b/src/goalServices/reduceGoals.ts index 1e54830242..8e8a0d3dda 100644 --- a/src/goalServices/reduceGoals.ts +++ b/src/goalServices/reduceGoals.ts @@ -552,6 +552,7 @@ export function reduceGoals( const goal = { ...currentValue.dataValues, isCurated: currentValue.dataValues.isCurated, + isSourceEditable: currentValue.isSourceEditable, goalNumber: currentValue.goalNumber || `G-${currentValue.dataValues.id}`, grantId: currentValue.grant.id, id: currentValue.dataValues.id, diff --git a/src/goalServices/types.ts b/src/goalServices/types.ts index b38f105801..8afa675ddb 100644 --- a/src/goalServices/types.ts +++ b/src/goalServices/types.ts @@ -236,6 +236,7 @@ interface IGoal { grantIds: number[]; isNew: boolean; isReopenedGoal: boolean; + isSourceEditable: boolean; collaborators: { goalNumber: string; goalCreator: IGoalCollaborator; @@ -282,6 +283,7 @@ interface IReducedGoal { goalCreatorRoles: string; }[]; activityReportGoals?: IActivityReportGoal[]; + isSourceEditable: boolean; } interface IGoalModelInstance extends IGoal { diff --git a/src/models/goal.js b/src/models/goal.js index 322bae8575..a88e8edcdc 100644 --- a/src/models/goal.js +++ b/src/models/goal.js @@ -146,6 +146,16 @@ export default (sequelize, DataTypes) => { source: { type: DataTypes.ENUM(GOAL_SOURCES), }, + isSourceEditable: { + type: DataTypes.VIRTUAL, + get() { + if (this.goalTemplate && this.goalTemplate.source) { + return this.goalTemplate.source === null; + } + + return true; + }, + }, }, { sequelize, modelName: 'Goal', diff --git a/src/models/goalTemplate.js b/src/models/goalTemplate.js index e1e4453119..9bddc4ed64 100644 --- a/src/models/goalTemplate.js +++ b/src/models/goalTemplate.js @@ -67,6 +67,12 @@ export default (sequelize, DataTypes) => { allowNull: true, type: DataTypes.STRING, }, + isSourceEditable: { + type: DataTypes.VIRTUAL, + get() { + return this.source === null; + }, + }, }, { sequelize, modelName: 'GoalTemplate', From 420e9b606c8a0431d913e1520aeab55215c963ed Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:27:17 -0400 Subject: [PATCH 13/28] Add route, handler, test --- src/routes/goalTemplates/handlers.test.js | 71 +++++++++++++++++++++++ src/routes/goalTemplates/handlers.ts | 25 ++++++++ src/routes/goalTemplates/index.js | 8 ++- 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 src/routes/goalTemplates/handlers.test.js diff --git a/src/routes/goalTemplates/handlers.test.js b/src/routes/goalTemplates/handlers.test.js new file mode 100644 index 0000000000..d4e9d480dd --- /dev/null +++ b/src/routes/goalTemplates/handlers.test.js @@ -0,0 +1,71 @@ +import { + INTERNAL_SERVER_ERROR, +} from 'http-codes'; +import { getSource } from './handlers'; +import { getSourceFromTemplate } from '../../services/goalTemplates'; + +jest.mock('../../services/goalTemplates'); + +const mockResponse = { + attachment: jest.fn(), + json: jest.fn(), + send: jest.fn(), + sendStatus: jest.fn(), + status: jest.fn(() => ({ + end: jest.fn(), + send: jest.fn(), + })), +}; + +describe('goalTemplates handlers', () => { + describe('getSource', () => { + it('handles success', async () => { + const req = { + params: { + goalTemplateId: 1, + }, + query: { + grantIds: [1], + }, + }; + + getSourceFromTemplate.mockResolvedValue('RTTAPA Development'); + + await getSource(req, mockResponse); + + expect(mockResponse.json).toHaveBeenCalledWith({ source: 'RTTAPA Development' }); + }); + it('handles null source', async () => { + const req = { + params: { + goalTemplateId: 1, + }, + query: { + grantIds: [1], + }, + }; + + getSourceFromTemplate.mockResolvedValue(undefined); + + await getSource(req, mockResponse); + + expect(mockResponse.json).toHaveBeenCalledWith({ source: '' }); + }); + }); + it('handles error', async () => { + const req = { + params: { + goalTemplateId: 1, + }, + query: { + grantIds: [1], + }, + }; + + getSourceFromTemplate.mockRejectedValue(new Error('error')); + + await getSource(req, mockResponse); + + expect(mockResponse.status).toHaveBeenCalledWith(INTERNAL_SERVER_ERROR); + }); +}); diff --git a/src/routes/goalTemplates/handlers.ts b/src/routes/goalTemplates/handlers.ts index 9c32782867..cf4d7f16a4 100644 --- a/src/routes/goalTemplates/handlers.ts +++ b/src/routes/goalTemplates/handlers.ts @@ -6,6 +6,7 @@ import { getCuratedTemplates, getFieldPromptsForCuratedTemplate, getOptionsByGoalTemplateFieldPromptName, + getSourceFromTemplate, } from '../../services/goalTemplates'; export async function getGoalTemplates(req: Request, res: Response) { @@ -23,6 +24,30 @@ export async function getGoalTemplates(req: Request, res: Response) { } } +export async function getSource(req: Request, res: Response) { + try { + const { goalTemplateId } = req.params; + const { grantIds } = req.query; + + // this is a single string param in the url, i.e. the "1" in /goalTemplates/1/prompts/ + // this will be verified as "canBeNumber" by some middleware before we get to this point + const numericalGoalTemplateId = parseInt(goalTemplateId, DECIMAL_BASE); + + // this is a query string, i.e. the "goalIds=1&goalIds=2&goalIds=3" + // the query can have one or more goal ids, and its hard to tell + // until we parse it if it's a single value or an array + const parsedGrantIds = [grantIds] + .flat() + .map((id: string) => parseInt(id, DECIMAL_BASE)) + .filter((id: number) => !Number.isNaN(id)); + + const source = await getSourceFromTemplate(numericalGoalTemplateId, parsedGrantIds); + res.json({ source: source || '' }); + } catch (err) { + await handleErrors(req, res, err, 'goalTemplates.getSource'); + } +} + export async function getPrompts(req: Request, res: Response) { try { const { goalTemplateId } = req.params; diff --git a/src/routes/goalTemplates/index.js b/src/routes/goalTemplates/index.js index fa5fcda842..1804959a02 100644 --- a/src/routes/goalTemplates/index.js +++ b/src/routes/goalTemplates/index.js @@ -1,11 +1,17 @@ import express from 'express'; import transactionWrapper from '../transactionWrapper'; import authMiddleware from '../../middleware/authMiddleware'; -import { getGoalTemplates, getPrompts, getOptionsByPromptName } from './handlers'; +import { + getGoalTemplates, + getPrompts, + getOptionsByPromptName, + getSource, +} from './handlers'; import { checkGoalTemplateIdParam } from '../../middleware/checkIdParamMiddleware'; const router = express.Router(); router.get('/', authMiddleware, transactionWrapper(getGoalTemplates)); router.get('/:goalTemplateId/prompts/', authMiddleware, checkGoalTemplateIdParam, transactionWrapper(getPrompts)); +router.get('/:goalTemplateId/source/', authMiddleware, checkGoalTemplateIdParam, transactionWrapper(getSource)); router.get('/options', transactionWrapper(getOptionsByPromptName)); export default router; From ea67389222caa886011cf9ef2c49721bb237d3e8 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:32:19 -0400 Subject: [PATCH 14/28] Add service and test --- src/services/goalTemplates.test.js | 349 ++++++++++++++++++++--------- src/services/goalTemplates.ts | 21 ++ 2 files changed, 267 insertions(+), 103 deletions(-) diff --git a/src/services/goalTemplates.test.js b/src/services/goalTemplates.test.js index b0dfa1a70c..913fe6a63a 100644 --- a/src/services/goalTemplates.test.js +++ b/src/services/goalTemplates.test.js @@ -1,7 +1,8 @@ import faker from '@faker-js/faker'; +import { GOAL_SOURCES } from '@ttahub/common'; import crypto from 'crypto'; import db from '../models'; -import { setFieldPromptsForCuratedTemplate } from './goalTemplates'; +import { setFieldPromptsForCuratedTemplate, getSourceFromTemplate } from './goalTemplates'; import { AUTOMATIC_CREATION } from '../constants'; const { @@ -14,135 +15,277 @@ const { sequelize, } = db; -describe('setFieldPromptsForCuratedTemplate', () => { - let promptResponses; - let template; - let goalIds; - let grant; - let recipient; - let promptId; - let promptTitle; - - beforeAll(async () => { - recipient = await Recipient.create({ - id: faker.datatype.number({ min: 56000 }), - name: faker.datatype.string(20), - }); +describe('goalTemplates services', () => { + afterAll(async () => { + await sequelize.close(); + }); - grant = await Grant.create({ - regionId: 2, - status: 'Active', - id: faker.datatype.number({ min: 56000 }), - number: faker.datatype.string(255), - recipientId: recipient.id, - }); + describe('getSourceFromTemplate', () => { + let template; + let templateTwo; + let grant; + let grantTwo; + let grantThree; + let recipient; + + beforeAll(async () => { + recipient = await Recipient.create({ + id: faker.datatype.number({ min: 56000 }), + name: faker.datatype.string(20), + }); + + grant = await Grant.create({ + regionId: 2, + status: 'Active', + id: faker.datatype.number({ min: 56000 }), + number: faker.datatype.string(255), + recipientId: recipient.id, + }); + + grantTwo = await Grant.create({ + regionId: 2, + status: 'Active', + id: faker.datatype.number({ min: 56000 }), + number: faker.datatype.string(255), + recipientId: recipient.id, + }); + + grantThree = await Grant.create({ + regionId: 2, + status: 'Active', + id: faker.datatype.number({ min: 56000 }), + number: faker.datatype.string(255), + recipientId: recipient.id, + }); + + const n = faker.lorem.sentence(5); + + const secret = 'secret'; + const hash = crypto + .createHmac('md5', secret) + .update(n) + .digest('hex'); - const n = faker.lorem.sentence(5); + template = await GoalTemplate.create({ + hash, + templateName: n, + creationMethod: AUTOMATIC_CREATION, + source: GOAL_SOURCES[1], + }); - const secret = 'secret'; - const hash = crypto - .createHmac('md5', secret) - .update(n) - .digest('hex'); + await Goal.create({ + grantId: grant.id, + goalTemplateId: template.id, + name: n, + source: GOAL_SOURCES[0], + }); - template = await GoalTemplate.create({ - hash, - templateName: n, - creationMethod: AUTOMATIC_CREATION, + await Goal.create({ + grantId: grantTwo.id, + goalTemplateId: template.id, + name: n, + }); + + const n2 = faker.lorem.sentence(5); + + const hash2 = crypto + .createHmac('md5', secret) + .update(n2) + .digest('hex'); + + templateTwo = await GoalTemplate.create({ + hash: hash2, + templateName: n2, + creationMethod: AUTOMATIC_CREATION, + }); + + await Goal.create({ + grantId: grantThree.id, + goalTemplateId: templateTwo.id, + name: n2, + }); }); - promptTitle = faker.datatype.string(255); - - const prompt = await GoalTemplateFieldPrompt.create({ - goalTemplateId: template.id, - ordinal: 1, - title: promptTitle, - prompt: promptTitle, - hint: '', - options: ['option 1', 'option 2', 'option 3'], - fieldType: 'multiselect', - validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] }, + afterAll(async () => { + await Goal.destroy({ + where: { + goalTemplateId: [ + template.id, + templateTwo.id, + ], + }, + force: true, + paranoid: true, + individualHooks: true, + }); + await GoalTemplate.destroy({ + where: { + id: [ + template.id, + templateTwo.id], + }, + individualHooks: true, + }); + await Grant.destroy({ + where: { + id: [ + grant.id, + grantTwo.id, + grantThree.id, + ], + }, + individualHooks: true, + }); + await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true }); }); - promptId = prompt.id; + it('returns source from the goal', async () => { + const source = await getSourceFromTemplate(template.id, [grant.id]); - promptResponses = [ - { promptId: prompt.id, response: ['option 1', 'option 2'] }, - ]; + expect(source).toBe(GOAL_SOURCES[0]); + }); + it('returns source from the template', async () => { + const source = await getSourceFromTemplate(template.id, [grantTwo.id]); - const goal = await Goal.create({ - grantId: grant.id, - goalTemplateId: template.id, - name: n, + expect(source).toBe(GOAL_SOURCES[1]); }); - goalIds = [goal.id]; - }); + it('returns null source', async () => { + const source = await getSourceFromTemplate(template.id, [grantThree.id]); - afterAll(async () => { - await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true }); - // eslint-disable-next-line max-len - await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true }); - await Goal.destroy({ - where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true, + expect(source).toBeFalsy(); }); - await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true }); - await Grant.destroy({ where: { id: grant.id }, individualHooks: true }); - await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true }); - await sequelize.close(); }); - it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => { - // save initial field responses - await setFieldPromptsForCuratedTemplate(goalIds, promptResponses); + describe('setFieldPromptsForCuratedTemplate', () => { + let promptResponses; + let template; + let goalIds; + let grant; + let recipient; + let promptId; + let promptTitle; - // check that the field responses were saved - const fieldResponses = await GoalFieldResponse.findAll({ - where: { goalId: goalIds }, - }); + beforeAll(async () => { + recipient = await Recipient.create({ + id: faker.datatype.number({ min: 56000 }), + name: faker.datatype.string(20), + }); - expect(fieldResponses.length).toBe(promptResponses.length); - expect(fieldResponses[0].response).toEqual(promptResponses[0].response); + grant = await Grant.create({ + regionId: 2, + status: 'Active', + id: faker.datatype.number({ min: 56000 }), + number: faker.datatype.string(255), + recipientId: recipient.id, + }); - // update field responses - await setFieldPromptsForCuratedTemplate(goalIds, [ - { promptId, response: ['option 1'] }, - ]); + const n = faker.lorem.sentence(5); - // check that the field responses were updated - const updatedFieldResponses = await GoalFieldResponse.findAll({ - where: { goalId: goalIds }, - }); + const secret = 'secret'; + const hash = crypto + .createHmac('md5', secret) + .update(n) + .digest('hex'); - expect(updatedFieldResponses.length).toBe(promptResponses.length); - expect(updatedFieldResponses[0].response).toEqual(['option 1']); - }); + template = await GoalTemplate.create({ + hash, + templateName: n, + creationMethod: AUTOMATIC_CREATION, + }); + + promptTitle = faker.datatype.string(255); + + const prompt = await GoalTemplateFieldPrompt.create({ + goalTemplateId: template.id, + ordinal: 1, + title: promptTitle, + prompt: promptTitle, + hint: '', + options: ['option 1', 'option 2', 'option 3'], + fieldType: 'multiselect', + validations: { required: 'Select a root cause', rules: [{ name: 'maxSelections', value: 2, message: 'You can only select 2 options' }] }, + }); + + promptId = prompt.id; - it('should use the provided validations', async () => { - const fieldResponses = await GoalFieldResponse.findAll({ - where: { goalId: goalIds }, - raw: true, + promptResponses = [ + { promptId: prompt.id, response: ['option 1', 'option 2'] }, + ]; + + const goal = await Goal.create({ + grantId: grant.id, + goalTemplateId: template.id, + name: n, + }); + + goalIds = [goal.id]; }); - // test validation error (no more than 2 options can be selected) - await expect(setFieldPromptsForCuratedTemplate(goalIds, [ - { promptId, response: ['option 1', 'option 2', 'option 3'] }, - ])).rejects.toThrow(); + afterAll(async () => { + await GoalFieldResponse.destroy({ where: { goalId: goalIds }, individualHooks: true }); + // eslint-disable-next-line max-len + await GoalTemplateFieldPrompt.destroy({ where: { goalTemplateId: template.id }, individualHooks: true }); + await Goal.destroy({ + where: { goalTemplateId: template.id }, force: true, paranoid: true, individualHooks: true, + }); + await GoalTemplate.destroy({ where: { id: template.id }, individualHooks: true }); + await Grant.destroy({ where: { id: grant.id }, individualHooks: true }); + await Recipient.destroy({ where: { id: recipient.id }, individualHooks: true }); + }); + + it('should call setFieldPromptForCuratedTemplate for each prompt response', async () => { + // save initial field responses + await setFieldPromptsForCuratedTemplate(goalIds, promptResponses); + + // check that the field responses were saved + const fieldResponses = await GoalFieldResponse.findAll({ + where: { goalId: goalIds }, + }); + + expect(fieldResponses.length).toBe(promptResponses.length); + expect(fieldResponses[0].response).toEqual(promptResponses[0].response); + + // update field responses + await setFieldPromptsForCuratedTemplate(goalIds, [ + { promptId, response: ['option 1'] }, + ]); - // check that the field responses were not updated - const notUpdatedFieldResponses = await GoalFieldResponse.findAll({ - where: { goalId: goalIds }, - raw: true, + // check that the field responses were updated + const updatedFieldResponses = await GoalFieldResponse.findAll({ + where: { goalId: goalIds }, + }); + + expect(updatedFieldResponses.length).toBe(promptResponses.length); + expect(updatedFieldResponses[0].response).toEqual(['option 1']); }); - expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length); - expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response); - }); + it('should use the provided validations', async () => { + const fieldResponses = await GoalFieldResponse.findAll({ + where: { goalId: goalIds }, + raw: true, + }); - it('does nothing if the prompt doesn\'t exist', async () => { - const fictionalId = 123454345345; - await expect(setFieldPromptsForCuratedTemplate(goalIds, [ - { promptId: fictionalId, response: ['option 1'] }, - ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`); + // test validation error (no more than 2 options can be selected) + await expect(setFieldPromptsForCuratedTemplate(goalIds, [ + { promptId, response: ['option 1', 'option 2', 'option 3'] }, + ])).rejects.toThrow(); + + // check that the field responses were not updated + const notUpdatedFieldResponses = await GoalFieldResponse.findAll({ + where: { goalId: goalIds }, + raw: true, + }); + + expect(notUpdatedFieldResponses.length).toBe(fieldResponses.length); + expect(notUpdatedFieldResponses[0].response).toStrictEqual(fieldResponses[0].response); + }); + + it('does nothing if the prompt doesn\'t exist', async () => { + const fictionalId = 123454345345; + await expect(setFieldPromptsForCuratedTemplate(goalIds, [ + { promptId: fictionalId, response: ['option 1'] }, + ])).rejects.toThrow(`No prompt found with ID ${fictionalId}`); + }); }); }); diff --git a/src/services/goalTemplates.ts b/src/services/goalTemplates.ts index 64b5f18d8f..8ca63bddb8 100644 --- a/src/services/goalTemplates.ts +++ b/src/services/goalTemplates.ts @@ -67,6 +67,7 @@ export async function getCuratedTemplates( attributes: [ 'id', 'source', + 'isSourceEditable', ['templateName', 'label'], ['id', 'value'], ['templateName', 'name'], @@ -129,6 +130,26 @@ export async function getCuratedTemplates( }); } +export async function getSourceFromTemplate( + goalTemplateId: number, + grantIds: number[], +) { + const goal = await GoalModel.findOne({ + where: { + goalTemplateId, + grantId: grantIds, + }, + attributes: ['source'], + include: { + model: GoalTemplateModel, + as: 'goalTemplate', + attributes: ['source'], + }, + }); + + return goal?.source || goal?.goalTemplate?.source; +} + /** Retrieves field prompts for a curated goal template and associated goals. @param goalTemplateId - The ID of the goal template to retrieve prompts for. From 801c9d74a1be1a03dfa9012824410c89a10037ac Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:34:11 -0400 Subject: [PATCH 15/28] Attempt to handle errors in fetch --- .../Pages/components/GoalPicker.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js index 5a25eae0cb..a6d9a494c9 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js +++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js @@ -152,16 +152,17 @@ const GoalPicker = ({ setTemplatePrompts(false); } - // update the goal date forcefully - // also update the date picker key to force a re-render - setValue('goalEndDate', goal.endDate || ''); - if (goal.goalIds) { - setDatePickerKey(`DPKEY-${goal.goalIds.join('-')}`); - } - setSelectedGoal(null); } catch (err) { - // handle this + onChange(goal); + setTemplatePrompts(false); + } + + // update the goal date forcefully + // also update the date picker key to force a re-render + setValue('goalEndDate', goal.endDate || ''); + if (goal.goalIds) { + setDatePickerKey(`DPKEY-${goal.goalIds.join('-')}`); } }; From 083b00845bd6e7239cf84df61dea25babcfde6ac Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 11:47:33 -0400 Subject: [PATCH 16/28] Remove from an API response --- src/goalServices/goalsByIdAndRecipient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goalServices/goalsByIdAndRecipient.ts b/src/goalServices/goalsByIdAndRecipient.ts index b1f988a353..7a2289fdf5 100644 --- a/src/goalServices/goalsByIdAndRecipient.ts +++ b/src/goalServices/goalsByIdAndRecipient.ts @@ -185,7 +185,7 @@ const OPTIONS_FOR_GOAL_FORM_QUERY = (id: number[] | number, recipientId: number) { model: GoalTemplate, as: 'goalTemplate', - attributes: ['source', 'creationMethod'], + attributes: [], required: false, }, { From c0a9850203da376dab469b5aa8e7a35911b01fa6 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 12:17:23 -0400 Subject: [PATCH 17/28] Fix tests and exclude curated goals from dropdown --- src/goalServices/goals.js | 35 ++++++++++++++++++++++++++++++++-- src/goalServices/goals.test.js | 1 + tests/api/recipient.spec.ts | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/goalServices/goals.js b/src/goalServices/goals.js index 1f090a8d27..844f9d59bf 100644 --- a/src/goalServices/goals.js +++ b/src/goalServices/goals.js @@ -686,6 +686,13 @@ export async function goalsForGrants(grantIds) { group: ['"Grant".id'], }); + const curatedTemplates = await GoalTemplate.findAll({ + attributes: ['id'], + where: { + creationMethod: CREATION_METHOD.CURATED, + }, + }); + /** * we need one big array that includes the old recipient id as well, * removing all the nulls along the way @@ -742,7 +749,15 @@ export async function goalsForGrants(grantIds) { 'source', 'createdVia', ], - group: ['"Goal"."name"', '"Goal"."status"', '"Goal"."endDate"', '"Goal"."onApprovedAR"', '"Goal"."source"', '"Goal"."createdVia"', '"Goal".id'], + group: [ + '"Goal"."name"', + '"Goal"."status"', + '"Goal"."endDate"', + '"Goal"."onApprovedAR"', + '"Goal"."source"', + '"Goal"."createdVia"', + '"Goal".id', + ], where: { name: { [Op.ne]: '', // exclude "blank" goals @@ -751,6 +766,16 @@ export async function goalsForGrants(grantIds) { status: { [Op.notIn]: ['Closed', 'Suspended'], }, + goalTemplateId: { + [Op.or]: [ + { + [Op.notIn]: curatedTemplates.map((ct) => ct.id), + }, + { + [Op.is]: null, + }, + ], + }, }, include: [ { @@ -770,7 +795,13 @@ export async function goalsForGrants(grantIds) { required: false, }, ], - order: [['name', 'asc']], + order: [[sequelize.fn( + 'MAX', + sequelize.fn( + 'DISTINCT', + sequelize.col('"Goal"."createdAt"'), + ), + ), 'desc']], }); } diff --git a/src/goalServices/goals.test.js b/src/goalServices/goals.test.js index 2b8ce180dd..9cb3506776 100644 --- a/src/goalServices/goals.test.js +++ b/src/goalServices/goals.test.js @@ -149,6 +149,7 @@ describe('Goals DB service', () => { ['name', 'label'], 'id', 'name', + 'isSourceEditable', ], where: { id: mockGoalId, diff --git a/tests/api/recipient.spec.ts b/tests/api/recipient.spec.ts index 5e82d07009..c347b957ed 100644 --- a/tests/api/recipient.spec.ts +++ b/tests/api/recipient.spec.ts @@ -185,6 +185,7 @@ test.describe('get /recipient', () => { Joi.object({ id: Joi.number(), isCurated: Joi.boolean(), + isSourceEditable: Joi.boolean(), prompts: Joi.object(), promptsForReview: Joi.array().items(Joi.object({ key: Joi.string(), From 5a23d4fe7113ae928615177e365a1da8c94e3603 Mon Sep 17 00:00:00 2001 From: Matt Bevilacqua Date: Wed, 30 Oct 2024 13:03:43 -0400 Subject: [PATCH 18/28] Change label --- .../src/pages/ActivityReport/Pages/components/GoalPicker.js | 3 +-- .../ActivityReport/Pages/components/__tests__/GoalPicker.js | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js index a6d9a494c9..2906aae9e2 100644 --- a/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js +++ b/frontend/src/pages/ActivityReport/Pages/components/GoalPicker.js @@ -197,7 +197,6 @@ const GoalPicker = ({ onChangeGoal(goal); }; - const pickerLabel = useOhsStandardGoal ? 'Select OHS standard goal' : 'Select recipient\'s goal'; const pickerOptions = useOhsStandardGoal ? goalTemplates : options; return ( @@ -219,7 +218,7 @@ const GoalPicker = ({