From 19b6f47606e8a526670b447e5f2938df45899884 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Thu, 19 Sep 2024 12:59:32 +0100 Subject: [PATCH 01/14] Temporarily disable progress and report filtering tests --- .../testProgressReportFiltering.spec.cy.js | 42 ++++++------ .../testScoreReportFiltering.cy.js | 65 ++++++++++--------- 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/cypress/e2e/partner-admin/default-tests/testProgressReportFiltering.spec.cy.js b/cypress/e2e/partner-admin/default-tests/testProgressReportFiltering.spec.cy.js index 8d6cad969..97deffc24 100644 --- a/cypress/e2e/partner-admin/default-tests/testProgressReportFiltering.spec.cy.js +++ b/cypress/e2e/partner-admin/default-tests/testProgressReportFiltering.spec.cy.js @@ -121,26 +121,28 @@ describe('The partner admin can view progress reports for a given administration }); }); -describe('The partner admin can view progress reports for a given administration and filter by progress status', () => { - it('Selects an administration, views its score report, then accesses the column filter to filter by progress status', () => { - checkUrl(); - cy.getAdministrationCard(roarTestAdministrationName, 'descending'); - clickProgressButton(roarTestAdministrationId); - setFilterByProgressCategory('ROAR - Word', 'completed'); - checkTableColumn(['Username'], 'CypressTestStudent0'); - }); -}); - -describe('The partner admin can view progress reports for a given administration and filter by grade and progress status', () => { - it('Selects an administration, views its score report, then accesses the column filter to filter by grade and support level', () => { - checkUrl(); - cy.getAdministrationCard(roarTestAdministrationName, 'descending'); - clickProgressButton(roarTestAdministrationId); - setFilterByGrade('1'); - setFilterByProgressCategory('ROAR - Word', 'completed'); - checkTableColumn(['Username'], 'CypressTestStudent0'); - }); -}); +// @NOTE: Temporarily disabled as instructed by the ROAR maintainers team. +// describe('The partner admin can view progress reports for a given administration and filter by progress status', () => { +// it('Selects an administration, views its score report, then accesses the column filter to filter by progress status', () => { +// checkUrl(); +// cy.getAdministrationCard(roarTestAdministrationName, 'descending'); +// clickProgressButton(roarTestAdministrationId); +// setFilterByProgressCategory('ROAR - Word', 'completed'); +// checkTableColumn(['Username'], 'CypressTestStudent0'); +// }); +// }); + +// @NOTE: Temporarily disabled as instructed by the ROAR maintainers team. +// describe('The partner admin can view progress reports for a given administration and filter by grade and progress status', () => { +// it('Selects an administration, views its score report, then accesses the column filter to filter by grade and support level', () => { +// checkUrl(); +// cy.getAdministrationCard(roarTestAdministrationName, 'descending'); +// clickProgressButton(roarTestAdministrationId); +// setFilterByGrade('1'); +// setFilterByProgressCategory('ROAR - Word', 'completed'); +// checkTableColumn(['Username'], 'CypressTestStudent0'); +// }); +// }); describe('The partner admin can view progress reports for a given administration and a not applicable filter returns an empty message', () => { it('Selects an administration, views its score report, then accesses the column filter to filter by a non-returnable filter', () => { diff --git a/cypress/e2e/partner-admin/default-tests/testScoreReportFiltering.cy.js b/cypress/e2e/partner-admin/default-tests/testScoreReportFiltering.cy.js index 0b7851a9a..dcaca0308 100644 --- a/cypress/e2e/partner-admin/default-tests/testScoreReportFiltering.cy.js +++ b/cypress/e2e/partner-admin/default-tests/testScoreReportFiltering.cy.js @@ -96,37 +96,40 @@ describe('The partner admin can view score reports for a given administration an }); }); -describe('The partner admin can view score reports for a given administration and filter by support level', () => { - it('Selects an administration, views its score report, then accesses the column filter to filter by support level', () => { - checkUrl(); - cy.getAdministrationCard(roarTestAdministrationName, 'descending'); - clickScoreButton(roarTestAdministrationId); - setFilterByScoreCategory('ROAR - Word', 'Pink'); - checkTableColumn(['Username'], 'CypressTestStudent0'); - }); -}); - -describe('The partner admin can view score reports for a given administration filter by school, grade, and progress status: completed', () => { - it('Selects an administration, views its score report, then accesses the column filter to filter by school, grade, and completed', () => { - checkUrl(); - cy.getAdministrationCard(roarTestAdministrationName, 'descending'); - clickScoreButton(roarTestAdministrationId); - setFilterByGrade('1'); - setFilterBySchool('Cypress Test School'); - setFilterByProgressCategory('ROAR - Morphology', 'completed'); - checkTableColumn(['Username'], 'CypressTestStudent0'); - }); -}); - -describe('The partner admin can view score reports for a given administration and filter by Assessed', () => { - it('Selects an administration, views its score report, then accesses the column filter to filter by assessed', () => { - checkUrl(); - cy.getAdministrationCard(roarTestAdministrationName, 'descending'); - clickScoreButton(roarTestAdministrationId); - setFilterByScoreCategory('ROAR - Morphology', 'Assessed'); - checkTableColumn(['Username'], 'CypressTestStudent0'); - }); -}); +// @NOTE: Temporarily disabled as instructed by the ROAR maintainers team. +// describe('The partner admin can view score reports for a given administration and filter by support level', () => { +// it('Selects an administration, views its score report, then accesses the column filter to filter by support level', () => { +// checkUrl(); +// cy.getAdministrationCard(roarTestAdministrationName, 'descending'); +// clickScoreButton(roarTestAdministrationId); +// setFilterByScoreCategory('ROAR - Word', 'Pink'); +// checkTableColumn(['Username'], 'CypressTestStudent0'); +// }); +// }); + +// @NOTE: Temporarily disabled as instructed by the ROAR maintainers team. +// describe('The partner admin can view score reports for a given administration filter by school, grade, and progress status: completed', () => { +// it('Selects an administration, views its score report, then accesses the column filter to filter by school, grade, and completed', () => { +// checkUrl(); +// cy.getAdministrationCard(roarTestAdministrationName, 'descending'); +// clickScoreButton(roarTestAdministrationId); +// setFilterByGrade('1'); +// setFilterBySchool('Cypress Test School'); +// setFilterByProgressCategory('ROAR - Morphology', 'completed'); +// checkTableColumn(['Username'], 'CypressTestStudent0'); +// }); +// }); + +// @NOTE: Temporarily disabled as instructed by the ROAR maintainers team. +// describe('The partner admin can view score reports for a given administration and filter by Assessed', () => { +// it('Selects an administration, views its score report, then accesses the column filter to filter by assessed', () => { +// checkUrl(); +// cy.getAdministrationCard(roarTestAdministrationName, 'descending'); +// clickScoreButton(roarTestAdministrationId); +// setFilterByScoreCategory('ROAR - Morphology', 'Assessed'); +// checkTableColumn(['Username'], 'CypressTestStudent0'); +// }); +// }); describe('The partner admin can view score reports for a given administration and a not applicable filter returns an empty message', () => { it('Selects an administration, views its score report, then accesses the column filter to filter by a non-returnable filter', () => { From a4b75a2e844288f9f3c80c80f6df12b7d561f640 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Thu, 19 Sep 2024 16:30:30 +0100 Subject: [PATCH 02/14] Replace administrations query with useAdministrationsQuery composable --- src/components/CreateAdministration.vue | 76 +++++++++---------- .../queries/useAdministrationsQuery.js | 1 - 2 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index fdbb72eff..dd1949fb1 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -186,8 +186,9 @@ import _groupBy from 'lodash/groupBy'; import _values from 'lodash/values'; import { useVuelidate } from '@vuelidate/core'; import { required, requiredIf } from '@vuelidate/validators'; -import { fetchDocById, fetchDocsById } from '@/helpers/query/utils'; +import { fetchDocsById } from '@/helpers/query/utils'; import { useAuthStore } from '@/store/auth'; +import useAdministrationsQuery from '@/composables/queries/useAdministrationsQuery'; import useTaskVariantsQuery from '@/composables/queries/useTaskVariantsQuery'; import TaskPicker from './TaskPicker.vue'; import ConsentPicker from './ConsentPicker.vue'; @@ -246,10 +247,12 @@ const { data: allVariants } = useTaskVariantsQuery(true, { enabled: initialized, }); -// +------------------------------------------+ -// -----| Queries for grabbing pre-existing admins |----- -// +------------------------------------------+ +// +------------------------------------------------------+ +// | Queries for grabbing pre-existing adminitration data | +// +------------------------------------------------------+ +// @TODO: Remove the following queryKeys array and reset/invalidate functions in favour of query mutations that +// automatically invalidate the necessary relevant queries. const queryKeys = [ ['administration', props.adminId], ['variants', 'all'], @@ -260,17 +263,30 @@ const queryKeys = [ ['families', props.adminId], ]; -const shouldGrabAdminInfo = computed(() => { - return initialized.value && Boolean(props.adminId); +const resetAllQueries = async () => { + for (const key of queryKeys) { + await queryClient.resetQueries(key); + } +}; + +const invalidateAllQueries = async () => { + for (const key of queryKeys) { + await queryClient.invalidateQueries(key); + } +}; + +const { data: existingAdministrationData } = useAdministrationsQuery([props.adminId], { + enabled: initialized && props.adminId, + select: (data) => data[0], }); const preExistingAssessmentInfo = computed(() => { - return _get(preExistingAdminInfo.value, 'assessments', []); + return _get(existingAdministrationData.value, 'assessments', []); }); -// Grab districts from preExistingAdminInfo.minimalOrgs.districts +// Grab districts from existingAdministrationData.minimalOrgs.districts const districtsToGrab = computed(() => { - const districtIds = _get(preExistingAdminInfo.value, 'minimalOrgs.districts', []); + const districtIds = _get(existingAdministrationData.value, 'minimalOrgs.districts', []); return districtIds.map((districtId) => { return { collection: 'districts', @@ -284,9 +300,9 @@ const shouldGrabDistricts = computed(() => { return initialized.value && districtsToGrab.value.length > 0; }); -// grab schools from preExistingAdminInfo.minimalOrgs.schools +// grab schools from existingAdministrationData.minimalOrgs.schools const schoolsToGrab = computed(() => { - const schoolIds = _get(preExistingAdminInfo.value, 'minimalOrgs.schools', []); + const schoolIds = _get(existingAdministrationData.value, 'minimalOrgs.schools', []); return schoolIds.map((schoolId) => { return { collection: 'schools', @@ -300,9 +316,9 @@ const shouldGrabSchools = computed(() => { return initialized.value && schoolsToGrab.value.length > 0; }); -// Grab classes from preExistingAdminInfo.minimalOrgs.classes +// Grab classes from existingAdministrationData.minimalOrgs.classes const classesToGrab = computed(() => { - const classIds = _get(preExistingAdminInfo.value, 'minimalOrgs.classes', []); + const classIds = _get(existingAdministrationData.value, 'minimalOrgs.classes', []); return classIds.map((classId) => { return { collection: 'classes', @@ -316,9 +332,9 @@ const shouldGrabClasses = computed(() => { return initialized.value && classesToGrab.value.length > 0; }); -// Grab groups from preExistingAdminInfo.minimalOrgs.groups +// Grab groups from existingAdministrationData.minimalOrgs.groups const groupsToGrab = computed(() => { - const groupIds = _get(preExistingAdminInfo.value, 'minimalOrgs.groups', []); + const groupIds = _get(existingAdministrationData.value, 'minimalOrgs.groups', []); return groupIds.map((id) => { return { collection: 'groups', @@ -332,9 +348,9 @@ const shouldGrabGroups = computed(() => { return initialized.value && groupsToGrab.value.length > 0; }); -// Grab families from preExistingAdminInfo.families +// Grab families from existingAdministrationData.families const familiesToGrab = computed(() => { - const familyIds = _get(preExistingAdminInfo.value, 'minimalOrgs.families', []); + const familyIds = _get(existingAdministrationData.value, 'minimalOrgs.families', []); return familyIds.map((id) => { return { collection: 'families', @@ -348,26 +364,6 @@ const shouldGrabFamilies = computed(() => { return initialized.value && familiesToGrab.value.length > 0; }); -const resetAllQueries = async () => { - for (const key of queryKeys) { - await queryClient.resetQueries(key); - } -}; - -const invalidateAllQueries = async () => { - for (const key of queryKeys) { - await queryClient.invalidateQueries(key); - } -}; - -const { data: preExistingAdminInfo } = useQuery({ - queryKey: ['administration', props.adminId], - queryFn: () => fetchDocById('administrations', props.adminId), - keepPreviousData: true, - enabled: shouldGrabAdminInfo, - staleTime: 5 * 60 * 1000, // 5 minutes -}); - const { data: preDistricts } = useQuery({ queryKey: ['districts', props.adminId], queryFn: () => fetchDocsById(districtsToGrab.value), @@ -449,8 +445,8 @@ const rules = { const v$ = useVuelidate(rules, state); const minStartDate = computed(() => { - if (props.adminId && preExistingAdminInfo.value?.dateOpened) { - return new Date(preExistingAdminInfo.value.dateOpened); + if (props.adminId && existingAdministrationData.value?.dateOpened) { + return new Date(existingAdministrationData.value.dateOpened); } return new Date(); }); @@ -651,7 +647,7 @@ onUnmounted(async () => { } }); -watch([preExistingAdminInfo, allVariants], ([adminInfo, allVariantInfo]) => { +watch([existingAdministrationData, allVariants], ([adminInfo, allVariantInfo]) => { if (adminInfo && !_isEmpty(allVariantInfo)) { state.administrationName = adminInfo.name; state.administrationPublicName = adminInfo.publicName; diff --git a/src/composables/queries/useAdministrationsQuery.js b/src/composables/queries/useAdministrationsQuery.js index 12e812792..e9a7e59f4 100644 --- a/src/composables/queries/useAdministrationsQuery.js +++ b/src/composables/queries/useAdministrationsQuery.js @@ -20,7 +20,6 @@ const useAdministrationsQuery = (administrationIds, queryOptions = undefined) => return { collection: FIRESTORE_COLLECTIONS.ADMINISTRATIONS, docId: administrationId, - select: ['name', 'publicName', 'sequential', 'assessments', 'legal'], }; }), ), From 1b56180e2adecb6884379458ee5e3530441a1898 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Thu, 19 Sep 2024 16:31:01 +0100 Subject: [PATCH 03/14] Replace districts and schools queries with composables --- src/components/CreateAdministration.vue | 82 ++++++++----------------- 1 file changed, 26 insertions(+), 56 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index dd1949fb1..53b2ead8d 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -109,7 +109,7 @@
@@ -189,6 +189,8 @@ import { required, requiredIf } from '@vuelidate/validators'; import { fetchDocsById } from '@/helpers/query/utils'; import { useAuthStore } from '@/store/auth'; import useAdministrationsQuery from '@/composables/queries/useAdministrationsQuery'; +import useDistrictsQuery from '@/composables/queries/useDistrictsQuery'; +import useSchoolsQuery from '@/composables/queries/useSchoolsQuery'; import useTaskVariantsQuery from '@/composables/queries/useTaskVariantsQuery'; import TaskPicker from './TaskPicker.vue'; import ConsentPicker from './ConsentPicker.vue'; @@ -231,9 +233,9 @@ const submitLabel = computed(() => { return 'Create Administration'; }); -// +------------------------------------------+ -// -----| Queries for grabbing variants |----- -// +------------------------------------------+ +// +------------------------------------------------------------------------------------------------------------------+ +// | Fetch Variants with Params +// +------------------------------------------------------------------------------------------------------------------+ const findVariantWithParams = (variants, params) => { // TODO: implement tie breakers if found.length > 1 return _find(variants, (variant) => { @@ -247,9 +249,9 @@ const { data: allVariants } = useTaskVariantsQuery(true, { enabled: initialized, }); -// +------------------------------------------------------+ -// | Queries for grabbing pre-existing adminitration data | -// +------------------------------------------------------+ +// +------------------------------------------------------------------------------------------------------------------+ +// | Fetch pre-existing administration data when editing an administration +// +------------------------------------------------------------------------------------------------------------------+ // @TODO: Remove the following queryKeys array and reset/invalidate functions in favour of query mutations that // automatically invalidate the necessary relevant queries. @@ -275,48 +277,32 @@ const invalidateAllQueries = async () => { } }; +// Fetch the data of the currently being edited administration, incl. its assigned assessments. const { data: existingAdministrationData } = useAdministrationsQuery([props.adminId], { - enabled: initialized && props.adminId, + enabled: initialized && !!props.adminId, select: (data) => data[0], }); -const preExistingAssessmentInfo = computed(() => { - return _get(existingAdministrationData.value, 'assessments', []); +const existingAssessments = computed(() => { + return existingAdministrationData?.value?.assessments ?? []; }); -// Grab districts from existingAdministrationData.minimalOrgs.districts -const districtsToGrab = computed(() => { - const districtIds = _get(existingAdministrationData.value, 'minimalOrgs.districts', []); - return districtIds.map((districtId) => { - return { - collection: 'districts', - docId: districtId, - select: ['name'], - }; - }); -}); +// Fetch the districts assigned to the administration. +const districtIds = computed(() => existingAdministrationData?.value?.minimalOrgs?.districts ?? []); -const shouldGrabDistricts = computed(() => { - return initialized.value && districtsToGrab.value.length > 0; +const { data: existingDistrictsData } = useDistrictsQuery(districtIds, { + enabled: initialized && districtIds.value.length > 0, }); -// grab schools from existingAdministrationData.minimalOrgs.schools -const schoolsToGrab = computed(() => { - const schoolIds = _get(existingAdministrationData.value, 'minimalOrgs.schools', []); - return schoolIds.map((schoolId) => { - return { - collection: 'schools', - docId: schoolId, - select: ['name'], - }; - }); -}); +// Fetch the schools assigned to the administration. +const schoolIds = computed(() => existingAdministrationData.value?.minimalOrgs?.schools ?? []); -const shouldGrabSchools = computed(() => { - return initialized.value && schoolsToGrab.value.length > 0; +const { data: existingSchoolsData } = useSchoolsQuery(schoolIds, { + enabled: initialized && schoolIds.value.length > 0, }); // Grab classes from existingAdministrationData.minimalOrgs.classes +// Fetch classes assigned to the administration. const classesToGrab = computed(() => { const classIds = _get(existingAdministrationData.value, 'minimalOrgs.classes', []); return classIds.map((classId) => { @@ -364,22 +350,6 @@ const shouldGrabFamilies = computed(() => { return initialized.value && familiesToGrab.value.length > 0; }); -const { data: preDistricts } = useQuery({ - queryKey: ['districts', props.adminId], - queryFn: () => fetchDocsById(districtsToGrab.value), - keepPreviousData: true, - enabled: shouldGrabDistricts, - staleTime: 5 * 60 * 1000, // 5 minutes -}); - -const { data: preSchools } = useQuery({ - queryKey: ['schools', 'minimalOrgs', props.adminId], - queryFn: () => fetchDocsById(schoolsToGrab.value), - keepPreviousData: true, - enabled: shouldGrabSchools, - staleTime: 5 * 60 * 1000, // 5 minutes -}); - const { data: preClasses } = useQuery({ queryKey: ['classes', 'minimal', props.adminId], queryFn: () => fetchDocsById(classesToGrab.value), @@ -463,8 +433,8 @@ const minEndDate = computed(() => { // +---------------------------------+ const orgsList = computed(() => { return { - districts: preDistricts.value, - schools: preSchools.value, + districts: existingDistrictsData.value, + schools: existingSchoolsData.value, classes: preClasses.value, groups: preGroups.value, families: preFamilies.value, @@ -675,7 +645,7 @@ watch([existingAdministrationData, allVariants], ([adminInfo, allVariantInfo]) = }); // Set up watchers for changes to orgs as a result of editing an administration -watch(districtsToGrab, async (updatedValue) => { +watch(districtIds, async (updatedValue) => { if (updatedValue.length !== 0) { // Invalidate the districts query and re-fetch the data based on the updated value await queryClient.invalidateQueries(['districts', props.adminId]); @@ -686,7 +656,7 @@ watch(districtsToGrab, async (updatedValue) => { } }); -watch(schoolsToGrab, async (updatedValue) => { +watch(schoolIds, async (updatedValue) => { if (updatedValue.length !== 0) { // Invalidate the schools query and re-fetch the data based on the updated value await queryClient.invalidateQueries(['schools', 'minimalOrgs', props.adminId]); From 17b7796c1c23b19c2cd864b4f781b38aa4e84052 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 17:01:51 +0100 Subject: [PATCH 04/14] Align query omposables and fix reactivity issues --- .../queries/useAdministrationsQuery.js | 24 +++---- .../queries/useAdministrationsQuery.test.js | 71 +++++++++++++++--- .../queries/useAdministrationsStatsQuery.js | 11 ++- .../useAdministrationsStatsQuery.test.js | 64 +++++++++++++++-- src/composables/queries/useClassesQuery.js | 6 +- .../queries/useDistrictsListQuery.test.js | 10 +-- src/composables/queries/useDistrictsQuery.js | 9 ++- .../queries/useDistrictsQuery.test.js | 37 ++++++++-- src/composables/queries/useDsgfOrgQuery.js | 12 ++-- .../queries/useDsgfOrgQuery.test.js | 15 ++-- src/composables/queries/useFamiliesQuery.js | 9 ++- .../queries/useFamiliesQuery.test.js | 70 +++++++++++++++--- .../queries/useGroupsListQuery.test.js | 16 ++++- src/composables/queries/useGroupsQuery.js | 7 +- .../queries/useGroupsQuery.test.js | 49 ++++++++++--- .../queries/useLegalDocsQuery.test.js | 18 +++++ src/composables/queries/useOrgQuery.test.js | 13 ++-- src/composables/queries/useOrgUsersQuery.js | 3 +- .../queries/useOrgUsersQuery.test.js | 50 ++++++++----- src/composables/queries/useSchoolsQuery.js | 6 +- .../queries/useSchoolsQuery.test.js | 49 ++++++++++--- .../queries/useUserAssignmentsQuery.js | 4 +- .../queries/useUserAssignmentsQuery.test.js | 24 ++++--- src/composables/queries/useUserClaimsQuery.js | 4 +- .../queries/useUserClaimsQuery.test.js | 36 ++++++---- src/composables/queries/useUserDataQuery.js | 4 +- .../queries/useUserDataQuery.test.js | 72 +++++++++++-------- .../queries/useUserRunPageQuery.js | 2 +- .../queries/useUserStudentDataQuery.js | 6 +- .../queries/useUserStudentDataQuery.test.js | 41 ++++++----- src/helpers/hasArrayEntries.js | 11 +++ src/helpers/hasArrayEntries.test.js | 41 +++++++++++ src/helpers/query/runs.js | 72 ++++++++++++++----- src/helpers/query/users.js | 69 ++++++++++++++---- src/helpers/query/utils.js | 27 +++++-- 35 files changed, 708 insertions(+), 254 deletions(-) create mode 100644 src/helpers/hasArrayEntries.js create mode 100644 src/helpers/hasArrayEntries.test.js diff --git a/src/composables/queries/useAdministrationsQuery.js b/src/composables/queries/useAdministrationsQuery.js index e9a7e59f4..5c9879a20 100644 --- a/src/composables/queries/useAdministrationsQuery.js +++ b/src/composables/queries/useAdministrationsQuery.js @@ -1,6 +1,7 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; -import { fetchDocsById } from '@/helpers/query/utils'; +import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; +import { fetchDocumentsById } from '@/helpers/query/utils'; import { ADMINISTRATIONS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -12,18 +13,15 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; * @returns {UseQueryResult} The TanStack query result. */ const useAdministrationsQuery = (administrationIds, queryOptions = undefined) => { + // Ensure all necessary data is available before enabling the query. + const conditions = [() => hasArrayEntries(administrationIds)]; + const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); + return useQuery({ - queryKey: [ADMINISTRATIONS_QUERY_KEY, toValue(administrationIds)], - queryFn: () => - fetchDocsById( - toValue(administrationIds)?.map((administrationId) => { - return { - collection: FIRESTORE_COLLECTIONS.ADMINISTRATIONS, - docId: administrationId, - }; - }), - ), - ...queryOptions, + queryKey: [ADMINISTRATIONS_QUERY_KEY, administrationIds], + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.ADMINISTRATIONS, administrationIds), + enabled: isQueryEnabled, + ...options, }); }; diff --git a/src/composables/queries/useAdministrationsQuery.test.js b/src/composables/queries/useAdministrationsQuery.test.js index 209910620..497ab61e0 100644 --- a/src/composables/queries/useAdministrationsQuery.test.js +++ b/src/composables/queries/useAdministrationsQuery.test.js @@ -1,13 +1,13 @@ -import { ref, toValue } from 'vue'; +import { ref, toValue, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; import { withSetup } from '@/test-support/withSetup.js'; -import { fetchDocsById } from '@/helpers/query/utils'; +import { fetchDocumentsById } from '@/helpers/query/utils'; import useAdministrationsQuery from './useAdministrationsQuery'; vi.mock('@/helpers/query/utils', () => ({ - fetchDocsById: vi.fn().mockImplementation(() => []), + fetchDocumentsById: vi.fn().mockImplementation(() => []), })); vi.mock('@tanstack/vue-query', async (getModule) => { @@ -22,7 +22,6 @@ const buildCollectionRequestPayload = (id) => { return { collection: 'administrations', docId: id, - select: ['name', 'publicName', 'sequential', 'assessments', 'legal'], }; }; @@ -46,13 +45,14 @@ describe('useAdministrationsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['administrations', toValue(mockAdministrationIds)], + queryKey: ['administrations', mockAdministrationIds], queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: true, + }), }); - const expectedPayload = mockAdministrationIds.value.map((id) => buildCollectionRequestPayload(id)); - - expect(fetchDocsById).toHaveBeenCalledWith(expectedPayload); + expect(fetchDocumentsById).toHaveBeenCalledWith('administrations', mockAdministrationIds); }); it('should allow the query to be disabled via the passed query options', () => { @@ -66,9 +66,60 @@ describe('useAdministrationsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['administrations', toValue(mockAdministrationIds)], + queryKey: ['administrations', mockAdministrationIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + }); + + it('should only fetch data if the administration IDs are available', async () => { + const mockAdministrationIds = ref(null); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useAdministrationsQuery(mockAdministrationIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['administrations', mockAdministrationIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + + mockAdministrationIds.value = [nanoid(), nanoid()]; + await nextTick(); + + expect(fetchDocumentsById).toHaveBeenCalledWith('administrations', mockAdministrationIds); + }); + + it('should not let queryOptions override the internally computed value', async () => { + const mockAdministrationIds = ref(null); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useAdministrationsQuery(mockAdministrationIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['administrations', mockAdministrationIds], queryFn: expect.any(Function), - enabled: false, + enabled: expect.objectContaining({ + _value: false, + }), }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); }); }); diff --git a/src/composables/queries/useAdministrationsStatsQuery.js b/src/composables/queries/useAdministrationsStatsQuery.js index ca3e8b39e..f67f76aeb 100644 --- a/src/composables/queries/useAdministrationsStatsQuery.js +++ b/src/composables/queries/useAdministrationsStatsQuery.js @@ -1,5 +1,7 @@ import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; +import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocsById } from '@/helpers/query/utils'; import { ADMINISTRATIONS_STATS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -12,8 +14,12 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; * @returns {UseQueryResult} The TanStack query result. */ const useAdministrationsStatsQuery = (administrationIds, queryOptions = undefined) => { + // Ensure all necessary data is available before enabling the query. + const conditions = [() => hasArrayEntries(administrationIds)]; + const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); + return useQuery({ - queryKey: [ADMINISTRATIONS_STATS_QUERY_KEY, toValue(administrationIds)], + queryKey: [ADMINISTRATIONS_STATS_QUERY_KEY, administrationIds], queryFn: () => fetchDocsById( toValue(administrationIds)?.map((administrationId) => { @@ -23,7 +29,8 @@ const useAdministrationsStatsQuery = (administrationIds, queryOptions = undefine }; }), ), - ...queryOptions, + enabled: isQueryEnabled, + ...options, }); }; diff --git a/src/composables/queries/useAdministrationsStatsQuery.test.js b/src/composables/queries/useAdministrationsStatsQuery.test.js index 74d41bc2f..0d779334f 100644 --- a/src/composables/queries/useAdministrationsStatsQuery.test.js +++ b/src/composables/queries/useAdministrationsStatsQuery.test.js @@ -1,4 +1,4 @@ -import { ref, toValue } from 'vue'; +import { ref, toValue, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -45,8 +45,11 @@ describe('useAdministrationsStatsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['administrations-stats', toValue(mockAdministrationIds)], + queryKey: ['administrations-stats', mockAdministrationIds], queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: true, + }), }); const expectedPayload = mockAdministrationIds.value.map((id) => buildCollectionRequestPayload(id)); @@ -65,9 +68,62 @@ describe('useAdministrationsStatsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['administrations-stats', toValue(mockAdministrationIds)], + queryKey: ['administrations-stats', mockAdministrationIds], queryFn: expect.any(Function), - enabled: false, + enabled: expect.objectContaining({ + _value: false, + }), }); + + expect(fetchDocsById).not.toHaveBeenCalled(); + }); + + it('should only fetch data if the administration IDs are available', async () => { + const mockAdministrationIds = ref(null); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useAdministrationsStatsQuery(mockAdministrationIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['administrations-stats', mockAdministrationIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocsById).not.toHaveBeenCalled(); + + mockAdministrationIds.value = [nanoid(), nanoid()]; + await nextTick(); + + const expectedPayload = mockAdministrationIds.value.map((id) => buildCollectionRequestPayload(id)); + + expect(fetchDocsById).toHaveBeenCalledWith(expectedPayload); + }); + + it('should not let queryOptions override the internally computed value', async () => { + const mockAdministrationIds = ref(null); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useAdministrationsStatsQuery(mockAdministrationIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['administrations-stats', mockAdministrationIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocsById).not.toHaveBeenCalled(); }); }); diff --git a/src/composables/queries/useClassesQuery.js b/src/composables/queries/useClassesQuery.js index 51e01e82b..ddeb68acc 100644 --- a/src/composables/queries/useClassesQuery.js +++ b/src/composables/queries/useClassesQuery.js @@ -1,8 +1,8 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; import { fetchDocumentsById } from '@/helpers/query/utils'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { CLASSES_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -15,12 +15,12 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; */ const useClassesQuery = (classIds, queryOptions = undefined) => { // Ensure all necessary data is loaded before enabling the query. - const conditions = [() => !_isEmpty(classIds)]; + const conditions = [() => hasArrayEntries(classIds)]; const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [CLASSES_QUERY_KEY, classIds], - queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.CLASSES, toValue(classIds)), + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.CLASSES, classIds), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useDistrictsListQuery.test.js b/src/composables/queries/useDistrictsListQuery.test.js index 9c5c9a6d2..ac29119be 100644 --- a/src/composables/queries/useDistrictsListQuery.test.js +++ b/src/composables/queries/useDistrictsListQuery.test.js @@ -71,10 +71,10 @@ describe('useDistrictsListQuery', () => { }); it('should only fetch districts only once user claims are loaded', async () => { - const mockData = ref({}); - const mockIsLoading = ref(true); + const mockClaimsData = ref({}); + const mockClaimsLoading = ref(true); - vi.mocked(useUserClaimsQuery).mockReturnValue({ data: mockData, isLoading: mockIsLoading }); + vi.mocked(useUserClaimsQuery).mockReturnValue({ data: mockClaimsData, isLoading: mockClaimsLoading }); vi.spyOn(VueQuery, 'useQuery'); vi.spyOn(orgFetcher, 'mockImplementation'); @@ -93,8 +93,8 @@ describe('useDistrictsListQuery', () => { expect(orgFetcher).not.toHaveBeenCalled(); - mockData.value = mockSuperAdminUserClaims.value; - mockIsLoading.value = false; + mockClaimsData.value = mockSuperAdminUserClaims.value; + mockClaimsLoading.value = false; await nextTick(); diff --git a/src/composables/queries/useDistrictsQuery.js b/src/composables/queries/useDistrictsQuery.js index 6c597534e..3cd38ee7e 100644 --- a/src/composables/queries/useDistrictsQuery.js +++ b/src/composables/queries/useDistrictsQuery.js @@ -1,7 +1,6 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; -import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocumentsById } from '@/helpers/query/utils'; import { DISTRICTS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -14,13 +13,13 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; * @returns {UseQueryResult} The TanStack query result. */ const useDistrictsQuery = (districtIds, queryOptions = undefined) => { - // Ensure all necessary data is loaded before enabling the query. - const conditions = [() => !_isEmpty(districtIds)]; + // Ensure all necessary data is available before enabling the query. + const conditions = [() => hasArrayEntries(districtIds)]; const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [DISTRICTS_QUERY_KEY, districtIds], - queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.DISTRICTS, toValue(districtIds)), + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.DISTRICTS, districtIds), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useDistrictsQuery.test.js b/src/composables/queries/useDistrictsQuery.test.js index ba489a4d1..ee0038905 100644 --- a/src/composables/queries/useDistrictsQuery.test.js +++ b/src/composables/queries/useDistrictsQuery.test.js @@ -1,3 +1,4 @@ +import { ref, nextTick, toValue } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -29,7 +30,7 @@ describe('useDistrictsQuery', () => { }); it('should call query with correct parameters', () => { - const districtIds = nanoid(); + const districtIds = ref([nanoid()]); vi.spyOn(VueQuery, 'useQuery'); @@ -49,7 +50,7 @@ describe('useDistrictsQuery', () => { }); it('should allow the query to be disabled via the passed query options', () => { - const districtIds = nanoid(); + const districtIds = ref([nanoid()]); const queryOptions = { enabled: false }; vi.spyOn(VueQuery, 'useQuery'); @@ -65,10 +66,38 @@ describe('useDistrictsQuery', () => { _value: false, }), }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + }); + + it('should only fetch data if the administration ID is available', async () => { + const districtIds = ref([]); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useDistrictsQuery(districtIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['districts', districtIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + + districtIds.value = [nanoid()]; + await nextTick(); + + expect(fetchDocumentsById).toHaveBeenCalledWith('districts', districtIds); }); - it('should keep the query disabled if not district IDs are specified', () => { - const districtIds = []; + it('should not let queryOptions override the internally computed value', async () => { + const districtIds = ref([]); const queryOptions = { enabled: true }; vi.spyOn(VueQuery, 'useQuery'); diff --git a/src/composables/queries/useDsgfOrgQuery.js b/src/composables/queries/useDsgfOrgQuery.js index fa06b33d1..e788d275b 100644 --- a/src/composables/queries/useDsgfOrgQuery.js +++ b/src/composables/queries/useDsgfOrgQuery.js @@ -1,7 +1,6 @@ +import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; import { DSGF_ORGS_QUERY_KEY } from '@/constants/queryKeys'; -import { storeToRefs } from 'pinia'; -import { useAuthStore } from '@/store/auth'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; import { fetchTreeOrgs } from '@/helpers/query/orgs'; @@ -17,12 +16,9 @@ import { fetchTreeOrgs } from '@/helpers/query/orgs'; * @returns {UseQueryResult} The TanStack query result. */ const useDsgfOrgQuery = (administrationId, assignedOrgs, queryOptions = undefined) => { - const authStore = useAuthStore(); - const { uid } = storeToRefs(authStore); - - // Ensure the User ID is available and the query is enabled. - const queryConditions = [() => !!uid.value]; - const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); + // Ensure all necessary data is available before enabling the query. + const conditions = [() => !!toValue(administrationId)]; + const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [DSGF_ORGS_QUERY_KEY, administrationId], diff --git a/src/composables/queries/useDsgfOrgQuery.test.js b/src/composables/queries/useDsgfOrgQuery.test.js index b05cbaa42..bd1fa89e8 100644 --- a/src/composables/queries/useDsgfOrgQuery.test.js +++ b/src/composables/queries/useDsgfOrgQuery.test.js @@ -93,14 +93,10 @@ describe('useDsgfOrgQuery', () => { expect(fetchTreeOrgs).toHaveBeenCalledWith(mockAdministrationId, mockAssignedOrgs); }); - it('should only fetch data if the user ID is available', async () => { - const mockUserId = nanoid(); - const mockAdministrationId = nanoid(); + it('should only fetch data if the administration ID is available', async () => { + const mockAdministrationId = ref(); const mockAssignedOrgs = [nanoid()]; - const authStore = useAuthStore(piniaInstance); - authStore.uid = null; - const queryOptions = { enabled: true }; withSetup(() => useDsgfOrgQuery(mockAdministrationId, mockAssignedOrgs, queryOptions), { @@ -118,19 +114,16 @@ describe('useDsgfOrgQuery', () => { expect(fetchTreeOrgs).not.toHaveBeenCalled(); - authStore.uid = mockUserId; + mockAdministrationId.value = nanoid(); await nextTick(); expect(fetchTreeOrgs).toHaveBeenCalledWith(mockAdministrationId, mockAssignedOrgs); }); it('should not let queryOptions override the internally computed value', async () => { - const mockAdministrationId = nanoid(); + const mockAdministrationId = null; const mockAssignedOrgs = [nanoid()]; - const authStore = useAuthStore(piniaInstance); - authStore.uid = null; - const queryOptions = { enabled: true }; withSetup(() => useDsgfOrgQuery(mockAdministrationId, mockAssignedOrgs, queryOptions), { diff --git a/src/composables/queries/useFamiliesQuery.js b/src/composables/queries/useFamiliesQuery.js index 3348a6f07..a481ae4ac 100644 --- a/src/composables/queries/useFamiliesQuery.js +++ b/src/composables/queries/useFamiliesQuery.js @@ -1,7 +1,6 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; -import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocumentsById } from '@/helpers/query/utils'; import { FAMILIES_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -14,13 +13,13 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; * @returns {UseQueryResult} The TanStack query result. */ const useFamiliesQuery = (familyIds, queryOptions = undefined) => { - // Ensure all necessary data is loaded before enabling the query. - const conditions = [() => !_isEmpty(familyIds)]; + // Ensure all necessary data is available before enabling the query. + const conditions = [() => hasArrayEntries(familyIds)]; const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [FAMILIES_QUERY_KEY, familyIds], - queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.FAMILIES, toValue(familyIds)), + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.FAMILIES, familyIds), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useFamiliesQuery.test.js b/src/composables/queries/useFamiliesQuery.test.js index 3ff769409..9855c781c 100644 --- a/src/composables/queries/useFamiliesQuery.test.js +++ b/src/composables/queries/useFamiliesQuery.test.js @@ -1,3 +1,4 @@ +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -29,56 +30,105 @@ describe('useFamiliesQuery', () => { }); it('should call query with correct parameters', () => { - const familyIds = [nanoid()]; + const mockFamilyIds = ref([nanoid()]); vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useFamiliesQuery(familyIds), { + withSetup(() => useFamiliesQuery(mockFamilyIds), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['families', familyIds], + queryKey: ['families', mockFamilyIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, }), }); - expect(fetchDocumentsById).toHaveBeenCalledWith('families', familyIds); + expect(fetchDocumentsById).toHaveBeenCalledWith('families', mockFamilyIds); }); it('should allow the query to be disabled via the passed query options', () => { - const familyIds = [nanoid()]; + const mockFamilyIds = ref([nanoid()]); const queryOptions = { enabled: false }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useFamiliesQuery(familyIds, queryOptions), { + withSetup(() => useFamiliesQuery(mockFamilyIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['families', familyIds], + queryKey: ['families', mockFamilyIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, }), }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); }); it('should keep the query disabled if not family IDs are specified', () => { - const familyIds = []; + const mockFamilyIds = ref([]); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useFamiliesQuery(mockFamilyIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['families', mockFamilyIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + }); + + it('should only fetch data if the administration ID is available', async () => { + const mockFamilyIds = ref([]); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useFamiliesQuery(mockFamilyIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['families', mockFamilyIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + + mockFamilyIds.value = [nanoid()]; + await nextTick(); + + expect(fetchDocumentsById).toHaveBeenCalledWith('families', mockFamilyIds); + }); + + it('should not let queryOptions override the internally computed value', async () => { + const mockFamilyIds = ref([]); const queryOptions = { enabled: true }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useFamiliesQuery(familyIds, queryOptions), { + withSetup(() => useFamiliesQuery(mockFamilyIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['families', familyIds], + queryKey: ['families', mockFamilyIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useGroupsListQuery.test.js b/src/composables/queries/useGroupsListQuery.test.js index ec3dfd6c9..abb645533 100644 --- a/src/composables/queries/useGroupsListQuery.test.js +++ b/src/composables/queries/useGroupsListQuery.test.js @@ -1,4 +1,4 @@ -import { ref } from 'vue'; +import { nextTick, ref } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { withSetup } from '@/test-support/withSetup.js'; @@ -63,8 +63,11 @@ describe('useGroupsListQuery', () => { ); }); - it('should only fetch groups only once user claims are loaded', async () => { - vi.mocked(useUserClaimsQuery).mockReturnValue({ data: {}, isLoading: ref(true) }); + it('should only fetch data once user claims are available', async () => { + const mockClaimsData = ref({}); + const mockClaimsLoading = ref(true); + + vi.mocked(useUserClaimsQuery).mockReturnValue({ data: mockClaimsData, isLoading: mockClaimsLoading }); vi.spyOn(VueQuery, 'useQuery'); @@ -81,6 +84,13 @@ describe('useGroupsListQuery', () => { }); expect(orgFetcher).not.toHaveBeenCalled(); + + mockClaimsData.value = mockUserClaims.value; + mockClaimsLoading.value = false; + + await nextTick(); + + expect(orgFetcher).toHaveBeenCalled(); }); it('should allow the query to be disabled via the passed query options', () => { diff --git a/src/composables/queries/useGroupsQuery.js b/src/composables/queries/useGroupsQuery.js index 4e49e3784..ff28fd95c 100644 --- a/src/composables/queries/useGroupsQuery.js +++ b/src/composables/queries/useGroupsQuery.js @@ -1,7 +1,6 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; -import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocumentsById } from '@/helpers/query/utils'; import { GROUPS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -15,12 +14,12 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; */ const useGroupsQuery = (groupIds, queryOptions = undefined) => { // Ensure all necessary data is loaded before enabling the query. - const conditions = [() => !_isEmpty(groupIds)]; + const conditions = [() => hasArrayEntries(groupIds)]; const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [GROUPS_QUERY_KEY, groupIds], - queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.GROUPS, toValue(groupIds)), + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.GROUPS, groupIds), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useGroupsQuery.test.js b/src/composables/queries/useGroupsQuery.test.js index e142424a5..b9f248c7d 100644 --- a/src/composables/queries/useGroupsQuery.test.js +++ b/src/composables/queries/useGroupsQuery.test.js @@ -1,3 +1,4 @@ +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -29,37 +30,37 @@ describe('useGroupsQuery', () => { }); it('should call query with correct parameters', () => { - const groupIds = [nanoid(), nanoid()]; + const mockGroupIds = ref([nanoid(), nanoid()]); vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useGroupsQuery(groupIds), { + withSetup(() => useGroupsQuery(mockGroupIds), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['groups', groupIds], + queryKey: ['groups', mockGroupIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, }), }); - expect(fetchDocumentsById).toHaveBeenCalledWith('groups', groupIds); + expect(fetchDocumentsById).toHaveBeenCalledWith('groups', mockGroupIds); }); it('should allow the query to be disabled via the passed query options', () => { - const groupIds = [nanoid(), nanoid()]; + const mockGroupIds = ref([nanoid(), nanoid()]); const queryOptions = { enabled: false }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useGroupsQuery(groupIds, queryOptions), { + withSetup(() => useGroupsQuery(mockGroupIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['groups', groupIds], + queryKey: ['groups', mockGroupIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -69,18 +70,44 @@ describe('useGroupsQuery', () => { expect(fetchDocumentsById).not.toHaveBeenCalled(); }); - it('should keep the query disabled if not group IDs are specified', () => { - const groupIds = []; + it('should only fetch data if the administration ID is available', async () => { + const mockGroupIds = ref([]); const queryOptions = { enabled: true }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useGroupsQuery(groupIds, queryOptions), { + withSetup(() => useGroupsQuery(mockGroupIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['groups', groupIds], + queryKey: ['groups', mockGroupIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + + mockGroupIds.value = [nanoid()]; + await nextTick(); + + expect(fetchDocumentsById).toHaveBeenCalledWith('groups', mockGroupIds); + }); + + it('should not let queryOptions override the internally computed value', async () => { + const mockGroupIds = ref([]); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useGroupsQuery(mockGroupIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['groups', mockGroupIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useLegalDocsQuery.test.js b/src/composables/queries/useLegalDocsQuery.test.js index 3289d7ab7..c7d02033c 100644 --- a/src/composables/queries/useLegalDocsQuery.test.js +++ b/src/composables/queries/useLegalDocsQuery.test.js @@ -41,4 +41,22 @@ describe('useLegalDocsQuery', () => { expect(fetchLegalDocs).toHaveBeenCalledWith(); }); + + it('should allow the query to be disabled via the passed query options', () => { + const queryOptions = { enabled: false }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useLegalDocsQuery(queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['legal-docs'], + queryFn: expect.any(Function), + enabled: false, + }); + + expect(fetchLegalDocs).not.toHaveBeenCalled(); + }); }); diff --git a/src/composables/queries/useOrgQuery.test.js b/src/composables/queries/useOrgQuery.test.js index 76d4e3428..3ed7eb1d1 100644 --- a/src/composables/queries/useOrgQuery.test.js +++ b/src/composables/queries/useOrgQuery.test.js @@ -1,3 +1,4 @@ +import { ref } from 'vue'; import { describe, it, expect, vi } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -33,42 +34,42 @@ describe('useOrgQuery', () => { it('should return useDistrictsQuery for districts as org type', () => { const mockOrgType = SINGULAR_ORG_TYPES.DISTRICTS; - const mockOrgIds = [nanoid(), nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); const result = useOrgQuery(mockOrgType, mockOrgIds); expect(result).toBe('useDistrictsQuery'); }); it('should return useSchoolsQuery for schools as org type', () => { const mockOrgType = SINGULAR_ORG_TYPES.SCHOOLS; - const mockOrgIds = [nanoid(), nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); const result = useOrgQuery(mockOrgType, mockOrgIds); expect(result).toBe('useSchoolsQuery'); }); it('should return useClassesQuery for classes as org type', () => { const mockOrgType = SINGULAR_ORG_TYPES.CLASSES; - const mockOrgIds = [nanoid(), nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); const result = useOrgQuery(mockOrgType, mockOrgIds); expect(result).toBe('useClassesQuery'); }); it('should return useGroupsQuery for groups as org type', () => { const mockOrgType = SINGULAR_ORG_TYPES.GROUPS; - const mockOrgIds = [nanoid(), nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); const result = useOrgQuery(mockOrgType, mockOrgIds); expect(result).toBe('useGroupsQuery'); }); it('should return useFamiliesQuery for families as org type', () => { const mockOrgType = SINGULAR_ORG_TYPES.FAMILIES; - const mockOrgIds = [nanoid(), nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); const result = useOrgQuery(mockOrgType, mockOrgIds); expect(result).toBe('useFamiliesQuery'); }); it('should throw an error for unsupported org type', () => { const mockOrgType = 'UNSUPPORTED'; - const mockOrgIds = [nanoid()]; + const mockOrgIds = ref([nanoid(), nanoid()]); expect(() => useOrgQuery(mockOrgType, mockOrgIds)).toThrow('Unsupported org type: UNSUPPORTED'); }); }); diff --git a/src/composables/queries/useOrgUsersQuery.js b/src/composables/queries/useOrgUsersQuery.js index 5be0ee57f..4fd8e40d1 100644 --- a/src/composables/queries/useOrgUsersQuery.js +++ b/src/composables/queries/useOrgUsersQuery.js @@ -1,4 +1,3 @@ -import { ref } from 'vue'; import { useQuery } from '@tanstack/vue-query'; import { fetchUsersByOrg } from '@/helpers/query/users'; import { ORG_USERS_QUERY_KEY } from '@/constants/queryKeys'; @@ -9,7 +8,7 @@ import { ORG_USERS_QUERY_KEY } from '@/constants/queryKeys'; * @returns {UseQueryResult} The TanStack query result. */ const useOrgUsersQuery = (orgType, orgId, page, orderBy, queryOptions = undefined) => { - const itemsPerPage = ref(1000000); // @TODO: Replace with a more reasonable value. + const itemsPerPage = 1000000; // @TODO: Replace with a more reasonable value. return useQuery({ queryKey: [ORG_USERS_QUERY_KEY, orgType, orgId, page, orderBy], diff --git a/src/composables/queries/useOrgUsersQuery.test.js b/src/composables/queries/useOrgUsersQuery.test.js index ad05f7925..b928bf353 100644 --- a/src/composables/queries/useOrgUsersQuery.test.js +++ b/src/composables/queries/useOrgUsersQuery.test.js @@ -1,6 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { withSetup } from '@/test-support/withSetup.js'; import * as VueQuery from '@tanstack/vue-query'; +import { nanoid } from 'nanoid'; import { fetchUsersByOrg } from '@/helpers/query/users'; import useOrgUsersQuery from './useOrgUsersQuery'; @@ -27,36 +28,53 @@ describe('useOrgUsersQuery', () => { queryClient?.clear(); }); - it('should call useQuery with correct parameters', () => { - const orgType = 'org'; - const orgId = '1'; - const page = 1; - const orderBy = 'name'; - const queryOptions = { enabled: false }; + it('should call query with correct parameters', () => { + const mockOrgType = 'org'; + const mockOrgId = nanoid(); + const mockPageNumber = 1; + const mockOrderBy = 'name'; + const queryOptions = { enabled: true }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useOrgUsersQuery(orgType, orgId, page, orderBy, queryOptions), { + withSetup(() => useOrgUsersQuery(mockOrgType, mockOrgId, mockPageNumber, mockOrderBy, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['org-users', orgType, orgId, page, orderBy], + queryKey: ['org-users', mockOrgType, mockOrgId, mockPageNumber, mockOrderBy], queryFn: expect.any(Function), - enabled: false, + enabled: true, }); + + expect(fetchUsersByOrg).toHaveBeenCalledWith( + mockOrgType, + mockOrgId, + expect.anything(), + mockPageNumber, + mockOrderBy, + ); }); - it('should call fetchUsersByOrg with correct parameters', async () => { - const orgType = 'school'; - const orgId = 'mock-school-uid'; - const page = 1; - const orderBy = 'name'; + it('should allow the query to be disabled via the passed query options', () => { + const mockOrgType = 'org'; + const mockOrgId = nanoid(); + const mockPageNumber = 1; + const mockOrderBy = 'name'; + const queryOptions = { enabled: false }; + + vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useOrgUsersQuery(orgType, orgId, page, orderBy), { + withSetup(() => useOrgUsersQuery(mockOrgType, mockOrgId, mockPageNumber, mockOrderBy, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); - expect(fetchUsersByOrg).toHaveBeenCalledWith(orgType, orgId, expect.anything(), page, orderBy); + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['org-users', mockOrgType, mockOrgId, mockPageNumber, mockOrderBy], + queryFn: expect.any(Function), + enabled: false, + }); + + expect(fetchUsersByOrg).not.toHaveBeenCalled(); }); }); diff --git a/src/composables/queries/useSchoolsQuery.js b/src/composables/queries/useSchoolsQuery.js index dd6a4f6d5..803a2640d 100644 --- a/src/composables/queries/useSchoolsQuery.js +++ b/src/composables/queries/useSchoolsQuery.js @@ -1,7 +1,7 @@ -import { toValue } from 'vue'; import { useQuery } from '@tanstack/vue-query'; import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; +import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocumentsById } from '@/helpers/query/utils'; import { SCHOOLS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -15,12 +15,12 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; */ const useSchoolsQuery = (schoolIds, queryOptions = undefined) => { // Ensure all necessary data is loaded before enabling the query. - const conditions = [() => !_isEmpty(schoolIds)]; + const conditions = [() => hasArrayEntries(schoolIds)]; const { isQueryEnabled, options } = computeQueryOverrides(conditions, queryOptions); return useQuery({ queryKey: [SCHOOLS_QUERY_KEY, schoolIds], - queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.SCHOOLS, toValue(schoolIds)), + queryFn: () => fetchDocumentsById(FIRESTORE_COLLECTIONS.SCHOOLS, schoolIds), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useSchoolsQuery.test.js b/src/composables/queries/useSchoolsQuery.test.js index 76064c4bd..8fea5c776 100644 --- a/src/composables/queries/useSchoolsQuery.test.js +++ b/src/composables/queries/useSchoolsQuery.test.js @@ -1,3 +1,4 @@ +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -29,37 +30,37 @@ describe('useSchoolsQuery', () => { }); it('should call query with correct parameters when fetching a specific school', () => { - const schoolIds = [nanoid(), nanoid()]; + const mockSchoolIds = ref([nanoid(), nanoid()]); vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useSchoolsQuery(schoolIds), { + withSetup(() => useSchoolsQuery(mockSchoolIds), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['schools', schoolIds], + queryKey: ['schools', mockSchoolIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, }), }); - expect(fetchDocumentsById).toHaveBeenCalledWith('schools', schoolIds); + expect(fetchDocumentsById).toHaveBeenCalledWith('schools', mockSchoolIds); }); it('should allow the query to be disabled via the passed query options', () => { - const schoolIds = [nanoid()]; + const mockSchoolIds = ref([nanoid()]); const queryOptions = { enabled: false }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useSchoolsQuery(schoolIds, queryOptions), { + withSetup(() => useSchoolsQuery(mockSchoolIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['schools', schoolIds], + queryKey: ['schools', mockSchoolIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -69,18 +70,44 @@ describe('useSchoolsQuery', () => { expect(fetchDocumentsById).not.toHaveBeenCalled(); }); - it('should keep the query disabled if not school IDs are specified', () => { - const schoolIds = []; + it('should only fetch data once the school IDs are available', async () => { + const mockSchoolIds = ref([]); const queryOptions = { enabled: true }; vi.spyOn(VueQuery, 'useQuery'); - withSetup(() => useSchoolsQuery(schoolIds, queryOptions), { + withSetup(() => useSchoolsQuery(mockSchoolIds, queryOptions), { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['schools', schoolIds], + queryKey: ['schools', mockSchoolIds], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: false, + }), + }); + + expect(fetchDocumentsById).not.toHaveBeenCalled(); + + mockSchoolIds.value = [nanoid(), nanoid()]; + await nextTick(); + + expect(fetchDocumentsById).toHaveBeenCalledWith('schools', mockSchoolIds); + }); + + it('should not let queryOptions override the internally computed value', async () => { + const mockSchoolIds = ref([]); + const queryOptions = { enabled: true }; + + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useSchoolsQuery(mockSchoolIds, queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith({ + queryKey: ['schools', mockSchoolIds], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useUserAssignmentsQuery.js b/src/composables/queries/useUserAssignmentsQuery.js index d67d82824..1b3c17e9f 100644 --- a/src/composables/queries/useUserAssignmentsQuery.js +++ b/src/composables/queries/useUserAssignmentsQuery.js @@ -19,8 +19,8 @@ const useUserAssignmentsQuery = (queryOptions = undefined) => { const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); return useQuery({ - queryKey: [USER_ASSIGNMENTS_QUERY_KEY], - queryFn: () => getUserAssignments(roarUid.value), + queryKey: [USER_ASSIGNMENTS_QUERY_KEY, roarUid], + queryFn: () => getUserAssignments(roarUid), // Refetch on window focus for MEFS assessments as those are opened in a separate tab. refetchOnWindowFocus: 'always', enabled: isQueryEnabled, diff --git a/src/composables/queries/useUserAssignmentsQuery.test.js b/src/composables/queries/useUserAssignmentsQuery.test.js index 8e0c665f4..581a447b1 100644 --- a/src/composables/queries/useUserAssignmentsQuery.test.js +++ b/src/composables/queries/useUserAssignmentsQuery.test.js @@ -34,7 +34,7 @@ describe('useUserAssignmentsQuery', () => { }); it('should call query with correct parameters', () => { - const mockUserId = nanoid(); + const mockUserId = ref(nanoid()); const authStore = useAuthStore(piniaInstance); authStore.roarUid = mockUserId; @@ -47,7 +47,7 @@ describe('useUserAssignmentsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-assignments'], + queryKey: ['user-assignments', mockUserId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, @@ -59,7 +59,7 @@ describe('useUserAssignmentsQuery', () => { }); it('should correctly control the enabled state of the query', async () => { - const mockUserId = nanoid(); + const mockUserId = ref(nanoid()); const authStore = useAuthStore(piniaInstance); authStore.roarUid = mockUserId; @@ -76,7 +76,7 @@ describe('useUserAssignmentsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-assignments'], + queryKey: ['user-assignments', mockUserId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -93,11 +93,11 @@ describe('useUserAssignmentsQuery', () => { expect(getUserAssignments).toHaveBeenCalledWith(mockUserId); }); - it('should only fetch data if the uid is available', async () => { - const mockUserId = nanoid(); + it('should only fetch data once the uid is available', async () => { + const mockUserId = ref(null); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; + authStore.roarUid = mockUserId; authStore.userQueryKeyIndex = 1; const queryOptions = { enabled: true }; @@ -107,7 +107,7 @@ describe('useUserAssignmentsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-assignments'], + queryKey: ['user-assignments', mockUserId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -118,15 +118,17 @@ describe('useUserAssignmentsQuery', () => { expect(getUserAssignments).not.toHaveBeenCalled(); - authStore.roarUid = mockUserId; + mockUserId.value = nanoid(); await nextTick(); expect(getUserAssignments).toHaveBeenCalledWith(mockUserId); }); it('should not let queryOptions override the internally computed value', async () => { + const mockUserId = ref(null); + const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; + authStore.roarUid = mockUserId; authStore.userQueryKeyIndex = 1; const queryOptions = { enabled: true }; @@ -136,7 +138,7 @@ describe('useUserAssignmentsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-assignments'], + queryKey: ['user-assignments', mockUserId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useUserClaimsQuery.js b/src/composables/queries/useUserClaimsQuery.js index fb36506c8..214715053 100644 --- a/src/composables/queries/useUserClaimsQuery.js +++ b/src/composables/queries/useUserClaimsQuery.js @@ -19,8 +19,8 @@ const useUserClaimsQuery = (queryOptions = undefined) => { const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); return useQuery({ - queryKey: [USER_CLAIMS_QUERY_KEY, uid.value, userQueryKeyIndex.value], - queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USER_CLAIMS, uid.value), + queryKey: [USER_CLAIMS_QUERY_KEY, uid, userQueryKeyIndex], + queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USER_CLAIMS, uid), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useUserClaimsQuery.test.js b/src/composables/queries/useUserClaimsQuery.test.js index 03b14ef4a..57d57a11c 100644 --- a/src/composables/queries/useUserClaimsQuery.test.js +++ b/src/composables/queries/useUserClaimsQuery.test.js @@ -34,11 +34,12 @@ describe('useUserClaimsQuery', () => { }); it('should call query with correct parameters', () => { - const mockUserId = nanoid(); + const mockUserId = ref(nanoid()); + const mockUserQueryKeyIndex = ref(1); const authStore = useAuthStore(piniaInstance); authStore.uid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; vi.spyOn(VueQuery, 'useQuery'); @@ -47,7 +48,7 @@ describe('useUserClaimsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-claims', mockUserId, 1], + queryKey: ['user-claims', mockUserId, mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, @@ -58,11 +59,12 @@ describe('useUserClaimsQuery', () => { }); it('should correctly control the enabled state of the query', async () => { - const mockUserId = nanoid(); + const mockUserId = ref(nanoid()); + const mockUserQueryKeyIndex = ref(5); const authStore = useAuthStore(piniaInstance); authStore.uid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const enableQuery = ref(false); @@ -75,7 +77,7 @@ describe('useUserClaimsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-claims', mockUserId, 1], + queryKey: ['user-claims', mockUserId, mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -91,12 +93,13 @@ describe('useUserClaimsQuery', () => { expect(fetchDocById).toHaveBeenCalledWith('userClaims', mockUserId); }); - it('should only fetch data if the uid is available', async () => { - const mockUserId = nanoid(); + it('should only fetch data if once uid is available', async () => { + const mockUserId = ref(null); + const mockUserQueryKeyIndex = ref(5); const authStore = useAuthStore(piniaInstance); - authStore.uid = null; - authStore.userQueryKeyIndex = 1; + authStore.uid = mockUserId; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const queryOptions = { enabled: true }; @@ -105,7 +108,7 @@ describe('useUserClaimsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-claims', null, 1], + queryKey: ['user-claims', mockUserId, mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -115,16 +118,19 @@ describe('useUserClaimsQuery', () => { expect(fetchDocById).not.toHaveBeenCalled(); - authStore.uid = mockUserId; + mockUserId.value = nanoid(); await nextTick(); expect(fetchDocById).toHaveBeenCalledWith('userClaims', mockUserId); }); it('should not let queryOptions override the internally computed value', async () => { + const mockUserId = ref(null); + const mockUserQueryKeyIndex = ref(5); + const authStore = useAuthStore(piniaInstance); - authStore.uid = null; - authStore.userQueryKeyIndex = 1; + authStore.uid = mockUserId; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const queryOptions = { enabled: true }; @@ -133,7 +139,7 @@ describe('useUserClaimsQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-claims', null, 1], + queryKey: ['user-claims', mockUserId, mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useUserDataQuery.js b/src/composables/queries/useUserDataQuery.js index a91e36df2..ec801e7f0 100644 --- a/src/composables/queries/useUserDataQuery.js +++ b/src/composables/queries/useUserDataQuery.js @@ -23,8 +23,8 @@ const useUserDataQuery = (userId = undefined, queryOptions = undefined) => { const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); return useQuery({ - queryKey: [USER_DATA_QUERY_KEY, uid.value, userQueryKeyIndex.value], - queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, uid.value), + queryKey: [USER_DATA_QUERY_KEY, uid, userQueryKeyIndex], + queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, uid), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useUserDataQuery.test.js b/src/composables/queries/useUserDataQuery.test.js index 667dde6b1..e4752ec99 100644 --- a/src/composables/queries/useUserDataQuery.test.js +++ b/src/composables/queries/useUserDataQuery.test.js @@ -34,11 +34,12 @@ describe('useUserDataQuery', () => { }); it('should call query with correct parameters', () => { - const mockUserId = nanoid(); + const mockUserRoarUid = ref(nanoid()); + const mockUserQueryKeyIndex = ref(1); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarUid; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; vi.spyOn(VueQuery, 'useQuery'); @@ -46,24 +47,28 @@ describe('useUserDataQuery', () => { plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], }); - expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user', mockUserId, 1], - queryFn: expect.any(Function), - enabled: expect.objectContaining({ - _value: true, + expect(VueQuery.useQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['user', expect.objectContaining({ _value: authStore.roarUid }), mockUserQueryKeyIndex], + queryFn: expect.any(Function), + enabled: expect.objectContaining({ + _value: true, + }), }), - }); + ); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId); + expect(fetchDocById).toHaveBeenCalledWith('users', expect.objectContaining({ _value: authStore.roarUid })); }); it('should allow the use of a manual user ID', async () => { - const mockUserId = nanoid(); - const mockStudentUserId = nanoid(); + const mockUserRoarUid = ref(nanoid()); + const mockUserQueryKeyIndex = ref(1); + + const mockStudentUserId = ref(nanoid()); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarUid; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; vi.spyOn(VueQuery, 'useQuery'); @@ -72,22 +77,23 @@ describe('useUserDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user', mockStudentUserId, 1], + queryKey: ['user', expect.objectContaining({ _value: mockStudentUserId }), mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, }), }); - expect(fetchDocById).toHaveBeenCalledWith('users', mockStudentUserId); + expect(fetchDocById).toHaveBeenCalledWith('users', expect.objectContaining({ _value: mockStudentUserId })); }); it('should correctly control the enabled state of the query', async () => { - const mockUserId = nanoid(); + const mockUserRoarUid = ref(nanoid()); + const mockUserQueryKeyIndex = ref(1); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarUid; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const enableQuery = ref(false); @@ -100,7 +106,7 @@ describe('useUserDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user', mockUserId, 1], + queryKey: ['user', expect.objectContaining({ _value: authStore.roarUid }), mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -113,15 +119,16 @@ describe('useUserDataQuery', () => { enableQuery.value = true; await nextTick(); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId); + expect(fetchDocById).toHaveBeenCalledWith('users', expect.objectContaining({ _value: authStore.roarUid })); }); - it('should only fetch data if the roarUid is available', async () => { - const mockUserId = nanoid(); + it('should only fetch data once the roarUid is available', async () => { + const mockUserRoarUid = ref(null); + const mockUserQueryKeyIndex = ref(1); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarUid; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const queryOptions = { enabled: true }; @@ -130,7 +137,7 @@ describe('useUserDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user', null, 1], + queryKey: ['user', expect.objectContaining({ _value: authStore.roarUid }), mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -140,16 +147,19 @@ describe('useUserDataQuery', () => { expect(fetchDocById).not.toHaveBeenCalled(); - authStore.roarUid = mockUserId; + mockUserRoarUid.value = nanoid(); await nextTick(); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId); + expect(fetchDocById).toHaveBeenCalledWith('users', expect.objectContaining({ _value: authStore.roarUid })); }); it('should not let queryOptions override the internally computed value', async () => { + const mockUserRoarUid = ref(null); + const mockUserQueryKeyIndex = ref(1); + const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarUid; + authStore.userQueryKeyIndex = mockUserQueryKeyIndex; const queryOptions = { enabled: true }; @@ -158,7 +168,7 @@ describe('useUserDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user', null, 1], + queryKey: ['user', expect.objectContaining({ _value: authStore.roarUid }), mockUserQueryKeyIndex], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/composables/queries/useUserRunPageQuery.js b/src/composables/queries/useUserRunPageQuery.js index f264c5b45..bb3a4671d 100644 --- a/src/composables/queries/useUserRunPageQuery.js +++ b/src/composables/queries/useUserRunPageQuery.js @@ -36,7 +36,7 @@ const useUserRunPageQuery = (userId, administrationId, orgType, orgId, queryOpti const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); return useQuery({ - queryKey: [USER_RUN_PAGE_QUERY_KEY, toValue(userId), toValue(administrationId), toValue(orgType), toValue(orgId)], + queryKey: [USER_RUN_PAGE_QUERY_KEY, userId, administrationId, orgType, orgId], queryFn: async () => { const runPageData = await runPageFetcher({ administrationId: administrationId, diff --git a/src/composables/queries/useUserStudentDataQuery.js b/src/composables/queries/useUserStudentDataQuery.js index 32550a4b1..34d19b7f4 100644 --- a/src/composables/queries/useUserStudentDataQuery.js +++ b/src/composables/queries/useUserStudentDataQuery.js @@ -9,6 +9,8 @@ import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; /** * User student data query. * + * @TODO: Evaluate wether this query can be replaced by the existing useUserDataQuery composable. + * * @param {QueryOptions|undefined} queryOptions – Optional TanStack query options. * @returns {UseQueryResult} The TanStack query result. */ @@ -21,8 +23,8 @@ const useUserStudentDataQuery = (queryOptions = undefined) => { const { isQueryEnabled, options } = computeQueryOverrides(queryConditions, queryOptions); return useQuery({ - queryKey: [USER_STUDENT_DATA_QUERY_KEY], - queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, roarUid.value, ['studentData']), + queryKey: [USER_STUDENT_DATA_QUERY_KEY, roarUid], + queryFn: () => fetchDocById(FIRESTORE_COLLECTIONS.USERS, roarUid, ['studentData']), enabled: isQueryEnabled, ...options, }); diff --git a/src/composables/queries/useUserStudentDataQuery.test.js b/src/composables/queries/useUserStudentDataQuery.test.js index cdbbc50b1..771ed4404 100644 --- a/src/composables/queries/useUserStudentDataQuery.test.js +++ b/src/composables/queries/useUserStudentDataQuery.test.js @@ -34,11 +34,10 @@ describe('useUserStudentDataQuery', () => { }); it('should call query with correct parameters', () => { - const mockUserId = nanoid(); + const mockUserRoarId = ref(nanoid()); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = mockUserId; - authStore.userQueryKeyIndex = 1; + authStore.roarUid = mockUserRoarId; vi.spyOn(VueQuery, 'useQuery'); @@ -47,21 +46,21 @@ describe('useUserStudentDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-student'], + queryKey: ['user-student', mockUserRoarId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: true, }), }); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId, ['studentData']); + expect(fetchDocById).toHaveBeenCalledWith('users', mockUserRoarId, ['studentData']); }); it('should correctly control the enabled state of the query', async () => { - const mockUserId = nanoid(); + const mockUserRoarId = ref(nanoid()); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = mockUserId; + authStore.roarUid = mockUserRoarId; const enableQuery = ref(false); @@ -74,7 +73,7 @@ describe('useUserStudentDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-student'], + queryKey: ['user-student', mockUserRoarId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -87,14 +86,14 @@ describe('useUserStudentDataQuery', () => { enableQuery.value = true; await nextTick(); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId, ['studentData']); + expect(fetchDocById).toHaveBeenCalledWith('users', mockUserRoarId, ['studentData']); }); - it('should only fetch data if the roarUid is available', async () => { - const mockUserId = nanoid(); + it('should only fetch data once the roarUid is available', async () => { + const mockUserRoarId = ref(null); const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; + authStore.roarUid = mockUserRoarId; const queryOptions = { enabled: true }; @@ -103,7 +102,7 @@ describe('useUserStudentDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-student'], + queryKey: ['user-student', mockUserRoarId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, @@ -113,15 +112,23 @@ describe('useUserStudentDataQuery', () => { expect(fetchDocById).not.toHaveBeenCalled(); - authStore.roarUid = mockUserId; + mockUserRoarId.value = nanoid(); await nextTick(); - expect(fetchDocById).toHaveBeenCalledWith('users', mockUserId, ['studentData']); + expect(VueQuery.useQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryKey: ['user-student', mockUserRoarId], + }), + ); + + expect(fetchDocById).toHaveBeenCalledWith('users', mockUserRoarId, ['studentData']); }); it('should not let queryOptions override the internally computed value', async () => { + const mockUserRoarId = ref(null); + const authStore = useAuthStore(piniaInstance); - authStore.roarUid = null; + authStore.roarUid = mockUserRoarId; const queryOptions = { enabled: true }; @@ -130,7 +137,7 @@ describe('useUserStudentDataQuery', () => { }); expect(VueQuery.useQuery).toHaveBeenCalledWith({ - queryKey: ['user-student'], + queryKey: ['user-student', mockUserRoarId], queryFn: expect.any(Function), enabled: expect.objectContaining({ _value: false, diff --git a/src/helpers/hasArrayEntries.js b/src/helpers/hasArrayEntries.js new file mode 100644 index 000000000..2b06e90de --- /dev/null +++ b/src/helpers/hasArrayEntries.js @@ -0,0 +1,11 @@ +import { toValue } from 'vue'; + +/** + * Test if an array has entries. + * + * @param {Array} array – The array to check for entries. + * @returns {boolean} Whether the array has entries. + */ +export const hasArrayEntries = (array) => { + return Array.isArray(toValue(array)) && toValue(array).length > 0; +}; diff --git a/src/helpers/hasArrayEntries.test.js b/src/helpers/hasArrayEntries.test.js new file mode 100644 index 000000000..398cd8940 --- /dev/null +++ b/src/helpers/hasArrayEntries.test.js @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { ref } from 'vue'; +import { hasArrayEntries } from './hasArrayEntries'; + +describe('hasArrayEntries', () => { + it('should return true for non-empty arrays', () => { + expect(hasArrayEntries([1, 2, 3])).toBe(true); + }); + + it('should return false for empty arrays', () => { + expect(hasArrayEntries([])).toBe(false); + }); + + it('should return false for null', () => { + expect(hasArrayEntries(null)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(hasArrayEntries(undefined)).toBe(false); + }); + + it('should return true for non-empty Vue ref with arrays', () => { + const refWithArray = ref([1, 2, 3]); + expect(hasArrayEntries(refWithArray)).toBe(true); + }); + + it('should return false for empty Vue ref with arrays', () => { + const emptyRefWithArray = ref([]); + expect(hasArrayEntries(emptyRefWithArray)).toBe(false); + }); + + it('should return false for Vue ref with null', () => { + const refWithNull = ref(null); + expect(hasArrayEntries(refWithNull)).toBe(false); + }); + + it('should return false for Vue ref with undefined', () => { + const refWithUndefined = ref(undefined); + expect(hasArrayEntries(refWithUndefined)).toBe(false); + }); +}); diff --git a/src/helpers/query/runs.js b/src/helpers/query/runs.js index 839042ebb..f41c46a33 100644 --- a/src/helpers/query/runs.js +++ b/src/helpers/query/runs.js @@ -1,3 +1,4 @@ +import { toValue } from 'vue'; import _pick from 'lodash/pick'; import _get from 'lodash/get'; import _mapValues from 'lodash/mapValues'; @@ -6,6 +7,23 @@ import _without from 'lodash/without'; import { convertValues, getAxiosInstance, mapFields } from './utils'; import { pluralizeFirestoreCollection } from '@/helpers'; +/** + * Constructs the request body for fetching runs based on the provided parameters. + * + * @param {Object} params - The parameters for constructing the request body. + * @param {string} params.administrationId - The administration ID. + * @param {string} params.orgType - The type of the organization. + * @param {string} params.orgId - The ID of the organization. + * @param {string} [params.taskId] - The task ID. + * @param {boolean} [params.aggregationQuery] - Whether to use aggregation query. + * @param {number} [params.pageLimit] - The page limit for pagination. + * @param {number} [params.page] - The page number for pagination. + * @param {boolean} [params.paginate=true] - Whether to paginate the results. + * @param {Array} [params.select=['scores.computed.composite']] - The fields to select. + * @param {boolean} [params.allDescendants=true] - Whether to include all descendants. + * @param {boolean} [params.requireCompleted=false] - Whether to require completed runs. + * @returns {Object} The constructed request body. + */ export const getRunsRequestBody = ({ administrationId, orgType, @@ -44,7 +62,6 @@ export const getRunsRequestBody = ({ ]; if (administrationId && (orgId || !allDescendants)) { - console.log('adding assignmentId and bestRun to structuredQuery'); requestBody.structuredQuery.where = { compositeFilter: { op: 'AND', @@ -68,7 +85,6 @@ export const getRunsRequestBody = ({ }; if (orgId) { - console.log('adding orgId to structuredQuery'); requestBody.structuredQuery.where.compositeFilter.filters.push({ fieldFilter: { field: { fieldPath: `readOrgs.${pluralizeFirestoreCollection(orgType)}` }, @@ -131,12 +147,20 @@ export const getRunsRequestBody = ({ return requestBody; }; -export const runCounter = (administrationId, orgType, orgId) => { +/** + * Counts the number of runs for a given administration and organization. + * + * @param {string} administrationId - The administration ID. + * @param {string} orgType - The type of the organization. + * @param {string} orgId - The ID of the organization. + * @returns {Promise} The count of runs. + */ +export const runCounter = async (administrationId, orgType, orgId) => { const axiosInstance = getAxiosInstance('app'); const requestBody = getRunsRequestBody({ - administrationId, - orgType, - orgId, + administrationId: toValue(administrationId), + orgType: toValue(orgType), + orgId: toValue(orgId), aggregationQuery: true, }); return axiosInstance.post(':runAggregationQuery', requestBody).then(({ data }) => { @@ -144,6 +168,22 @@ export const runCounter = (administrationId, orgType, orgId) => { }); }; +/** + * Fetches run page data for a given set of parameters. + * + * @param {Object} params - The parameters for fetching run page data. + * @param {string} params.administrationId - The administration ID. + * @param {string} [params.userId] - The user ID. + * @param {string} params.orgType - The organization type. + * @param {string} params.orgId - The organization ID. + * @param {string} [params.taskId] - The task ID. + * @param {number} [params.pageLimit] - The page limit for pagination. + * @param {number} [params.page] - The page number for pagination. + * @param {Array} [params.select=['scores.computed.composite']] - The fields to select. + * @param {string} [params.scoreKey='scores.computed.composite'] - The key for scores. + * @param {boolean} [params.paginate=true] - Whether to paginate the results. + * @returns {Promise>} The fetched run page data. + */ export const runPageFetcher = async ({ administrationId, userId, @@ -158,18 +198,18 @@ export const runPageFetcher = async ({ }) => { const appAxiosInstance = getAxiosInstance('app'); const requestBody = getRunsRequestBody({ - administrationId, - orgType, - orgId, - taskId, - allDescendants: userId === undefined, + administrationId: toValue(administrationId), + orgType: toValue(orgType), + orgId: toValue(orgId), + taskId: toValue(taskId), + allDescendants: toValue(userId) === undefined, aggregationQuery: false, - pageLimit: paginate ? pageLimit.value : undefined, - page: paginate ? page.value : undefined, - paginate: paginate, - select: select, + pageLimit: paginate ? toValue(pageLimit) : undefined, + page: paginate ? toValue(page) : undefined, + paginate: toValue(paginate), + select: toValue(select), }); - const runQuery = userId === undefined ? ':runQuery' : `/users/${userId}:runQuery`; + const runQuery = toValue(userId) === undefined ? ':runQuery' : `/users/${toValue(userId)}:runQuery`; return appAxiosInstance.post(runQuery, requestBody).then(async ({ data }) => { const runData = mapFields(data, true); diff --git a/src/helpers/query/users.js b/src/helpers/query/users.js index 5ff0837d9..afeb62491 100644 --- a/src/helpers/query/users.js +++ b/src/helpers/query/users.js @@ -1,5 +1,22 @@ +import { toValue } from 'vue'; import { convertValues, getAxiosInstance, mapFields } from './utils'; +/** + * Constructs the request body for fetching users. + * + * @param {Object} params - The parameters for constructing the request body. + * @param {Array} [params.userIds=[]] - The IDs of the users to fetch. + * @param {string} params.orgType - The type of the organization (e.g., 'districts', 'schools'). + * @param {string} params.orgId - The ID of the organization. + * @param {boolean} params.aggregationQuery - Whether to perform an aggregation query. + * @param {number} params.pageLimit - The maximum number of users to fetch per page. + * @param {number} params.page - The page number to fetch. + * @param {boolean} [params.paginate=true] - Whether to paginate the results. + * @param {Array} [params.select=['name']] - The fields to select in the query. + * @param {string} params.orderBy - The field to order the results by. + * @returns {Object} The constructed request body. + * @throws {Error} If neither userIds nor orgType and orgId are provided. + */ export const getUsersRequestBody = ({ userIds = [], orgType, @@ -81,45 +98,71 @@ export const getUsersRequestBody = ({ return requestBody; }; +/** + * Fetches a page of users based on the provided user IDs. + * + * @param {Array} userIds - The IDs of the users to fetch. + * @param {number} pageLimit - The maximum number of users to fetch per page. + * @param {number} page - The page number to fetch. + * @returns {Promise} The fetched users data. + */ export const usersPageFetcher = async (userIds, pageLimit, page) => { const axiosInstance = getAxiosInstance(); const requestBody = getUsersRequestBody({ - userIds, + userIds: toValue(userIds), aggregationQuery: false, - pageLimit: pageLimit.value, - page: page.value, + pageLimit: toValue(pageLimit), + page: toValue(page), paginate: true, }); - console.log(`Fetching page ${page.value} for ${userIds}`); + console.log(`Fetching users page ${toValue(page)} for ${toValue(userIds)}`); return axiosInstance.post(':runQuery', requestBody).then(({ data }) => mapFields(data)); }; +/** + * Fetches a page of users based on the provided organization type and ID. + * + * @param {string} orgType - The type of the organization (e.g., 'districts', 'schools'). + * @param {string} orgId - The ID of the organization. + * @param {number} pageLimit - The maximum number of users to fetch per page. + * @param {number} page - The page number to fetch. + * @param {string} orderBy - The field to order the results by. + * @returns {Promise} The fetched users data. + */ export const fetchUsersByOrg = async (orgType, orgId, pageLimit, page, orderBy) => { const axiosInstance = getAxiosInstance(); const requestBody = getUsersRequestBody({ - orgType, - orgId, + orgType: toValue(orgType), + orgId: toValue(orgId), aggregationQuery: false, - pageLimit: pageLimit.value, - page: page.value, + pageLimit: toValue(pageLimit), + page: toValue(page), paginate: true, select: ['username', 'name', 'studentData', 'userType'], - orderBy: orderBy.value, + orderBy: toValue(orderBy), }); - console.log(`Fetching users page ${page.value} for ${orgType} ${orgId}`); + console.log(`Fetching users page ${toValue(page)} for ${toValue(orgType)} ${toValue(orgId)}`); return axiosInstance.post(':runQuery', requestBody).then(({ data }) => mapFields(data)); }; +/** + * Counts the number of users based on the provided organization type and ID. + * + * @param {string} orgType - The type of the organization (e.g., 'districts', 'schools'). + * @param {string} orgId - The ID of the organization. + * @param {string} orderBy - The field to order the results by. + * @returns {Promise} The count of users. + */ export const countUsersByOrg = async (orgType, orgId, orderBy) => { const axiosInstance = getAxiosInstance(); const requestBody = getUsersRequestBody({ - orgType, - orgId, + orgType: toValue(orgType), + orgId: toValue(orgId), aggregationQuery: true, paginate: false, - orderBy: orderBy.value, + orderBy: toValue(orderBy), }); return axiosInstance.post(':runAggregationQuery', requestBody).then(({ data }) => { diff --git a/src/helpers/query/utils.js b/src/helpers/query/utils.js index b727d4df8..ca06fa87c 100644 --- a/src/helpers/query/utils.js +++ b/src/helpers/query/utils.js @@ -1,3 +1,4 @@ +import { toValue } from 'vue'; import axios from 'axios'; import Papa from 'papaparse'; import _get from 'lodash/get'; @@ -119,6 +120,17 @@ export const exportCsv = (data, filename) => { document.body.removeChild(a); }; +/** + * Fetch a document by its ID + * + * @param {String} collection - The Firestore collection name. + * @param {String} docId - The ID of the document to fetch. + * @param {Array} [select] - Optional array of fields to select from the document. + * @param {String} [db=FIRESTORE_DATABASES.ADMIN] - The Firestore database to query. + * @param {Boolean} [unauthenticated=false] - Whether to use an unauthenticated request. + * @param {Boolean} [swallowErrors=false] - Whether to suppress error logging. + * @returns {Promise} The document data or an error message. + */ export const fetchDocById = async ( collection, docId, @@ -127,13 +139,16 @@ export const fetchDocById = async ( unauthenticated = false, swallowErrors = false, ) => { - if (!collection || !docId) { + const collectionValue = toValue(collection); + const docIdValue = toValue(docId); + + if (!toValue(collectionValue) || !toValue(docId)) { console.warn( - `fetchDocById: Collection or docId not provided. Called with collection "${collection}" and docId "${docId}"`, + `fetchDocById: Collection or docId not provided. Called with collection "${collectionValue}" and docId "${docIdValue}"`, ); return {}; } - const docPath = `/${collection}/${docId}`; + const docPath = `/${collectionValue}/${docIdValue}`; const axiosInstance = getAxiosInstance(db, unauthenticated); const queryParams = (select ?? []).map((field) => `mask.fieldPaths=${field}`); const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : ''; @@ -141,8 +156,8 @@ export const fetchDocById = async ( .get(docPath + queryString) .then(({ data }) => { return { - id: docId, - collection, + id: docIdValue, + collectionValue, ..._mapValues(data.fields, (value) => convertValues(value)), }; }) @@ -170,7 +185,7 @@ export const fetchDocById = async ( export const fetchDocumentsById = async (collection, docIds, select = [], db = FIRESTORE_DATABASES.ADMIN) => { const axiosInstance = getAxiosInstance(db); const baseURL = axiosInstance.defaults.baseURL.split('googleapis.com/v1/')[1]; - const documents = docIds.map((docId) => `${baseURL}/${collection}/${docId}`); + const documents = toValue(docIds).map((docId) => `${baseURL}/${collection}/${docId}`); const requestBody = { documents, From 94b6eae6159e015581219519f2bad52cd292231d Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 17:03:01 +0100 Subject: [PATCH 05/14] Replace classes query with useClassesQuery composable --- src/components/CreateAdministration.vue | 38 ++++++++----------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 53b2ead8d..044eb3655 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -191,6 +191,7 @@ import { useAuthStore } from '@/store/auth'; import useAdministrationsQuery from '@/composables/queries/useAdministrationsQuery'; import useDistrictsQuery from '@/composables/queries/useDistrictsQuery'; import useSchoolsQuery from '@/composables/queries/useSchoolsQuery'; +import useClassesQuery from '@/composables/queries/useClassesQuery'; import useTaskVariantsQuery from '@/composables/queries/useTaskVariantsQuery'; import TaskPicker from './TaskPicker.vue'; import ConsentPicker from './ConsentPicker.vue'; @@ -291,31 +292,21 @@ const existingAssessments = computed(() => { const districtIds = computed(() => existingAdministrationData?.value?.minimalOrgs?.districts ?? []); const { data: existingDistrictsData } = useDistrictsQuery(districtIds, { - enabled: initialized && districtIds.value.length > 0, + enabled: initialized, }); // Fetch the schools assigned to the administration. const schoolIds = computed(() => existingAdministrationData.value?.minimalOrgs?.schools ?? []); const { data: existingSchoolsData } = useSchoolsQuery(schoolIds, { - enabled: initialized && schoolIds.value.length > 0, + enabled: initialized, }); -// Grab classes from existingAdministrationData.minimalOrgs.classes -// Fetch classes assigned to the administration. -const classesToGrab = computed(() => { - const classIds = _get(existingAdministrationData.value, 'minimalOrgs.classes', []); - return classIds.map((classId) => { - return { - collection: 'classes', - docId: classId, - select: ['name'], - }; - }); -}); +// Fetch the classes assigned to the administration. +const classIds = computed(() => existingAdministrationData.value?.minimalOrgs?.classes ?? []); -const shouldGrabClasses = computed(() => { - return initialized.value && classesToGrab.value.length > 0; +const { data: existingClassesData } = useClassesQuery(classIds, { + enabled: initialized && classIds.value.length > 0, }); // Grab groups from existingAdministrationData.minimalOrgs.groups @@ -350,14 +341,6 @@ const shouldGrabFamilies = computed(() => { return initialized.value && familiesToGrab.value.length > 0; }); -const { data: preClasses } = useQuery({ - queryKey: ['classes', 'minimal', props.adminId], - queryFn: () => fetchDocsById(classesToGrab.value), - keepPreviousData: true, - enabled: shouldGrabClasses, - staleTime: 5 * 60 * 1000, // 5 minutes -}); - const { data: preGroups } = useQuery({ queryKey: ['groups', props.adminId], queryFn: () => fetchDocsById(groupsToGrab.value), @@ -435,7 +418,7 @@ const orgsList = computed(() => { return { districts: existingDistrictsData.value, schools: existingSchoolsData.value, - classes: preClasses.value, + classes: existingClassesData.value, groups: preGroups.value, families: preFamilies.value, }; @@ -512,6 +495,7 @@ const submit = async () => { pickListError.value = ''; submitted.value = true; const isFormValid = await v$.value.$validate(); + if (isFormValid) { const submittedAssessments = variants.value.map((assessment) => removeUndefined({ @@ -556,6 +540,8 @@ const submit = async () => { }; if (props.adminId) args.administrationId = props.adminId; + console.log('[debug] submitting with args:', args); + await roarfirekit.value .createAdministration(args) .then(() => { @@ -667,7 +653,7 @@ watch(schoolIds, async (updatedValue) => { } }); -watch(classesToGrab, async (updatedValue) => { +watch(classIds, async (updatedValue) => { if (updatedValue.length !== 0) { // Invalidate the classes query and re-fetch the data based on the updated value await queryClient.invalidateQueries(['classes', 'minimal', props.adminId]); From 1146d385d7fd58170f9dca0701bcabd2688f5821 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 17:04:25 +0100 Subject: [PATCH 06/14] Fix linter offenses --- src/components/CreateAdministration.vue | 4 ++-- src/composables/queries/useAdministrationsQuery.test.js | 9 +-------- .../queries/useAdministrationsStatsQuery.test.js | 2 +- src/composables/queries/useClassesQuery.js | 1 - src/composables/queries/useDistrictsQuery.test.js | 2 +- src/composables/queries/useSchoolsQuery.js | 1 - 6 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 044eb3655..47ae94b25 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -280,7 +280,7 @@ const invalidateAllQueries = async () => { // Fetch the data of the currently being edited administration, incl. its assigned assessments. const { data: existingAdministrationData } = useAdministrationsQuery([props.adminId], { - enabled: initialized && !!props.adminId, + enabled: initialized.value && !!props.adminId, select: (data) => data[0], }); @@ -306,7 +306,7 @@ const { data: existingSchoolsData } = useSchoolsQuery(schoolIds, { const classIds = computed(() => existingAdministrationData.value?.minimalOrgs?.classes ?? []); const { data: existingClassesData } = useClassesQuery(classIds, { - enabled: initialized && classIds.value.length > 0, + enabled: initialized.value && classIds.value.length > 0, }); // Grab groups from existingAdministrationData.minimalOrgs.groups diff --git a/src/composables/queries/useAdministrationsQuery.test.js b/src/composables/queries/useAdministrationsQuery.test.js index 497ab61e0..5ac1eec34 100644 --- a/src/composables/queries/useAdministrationsQuery.test.js +++ b/src/composables/queries/useAdministrationsQuery.test.js @@ -1,4 +1,4 @@ -import { ref, toValue, nextTick } from 'vue'; +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; @@ -18,13 +18,6 @@ vi.mock('@tanstack/vue-query', async (getModule) => { }; }); -const buildCollectionRequestPayload = (id) => { - return { - collection: 'administrations', - docId: id, - }; -}; - describe('useAdministrationsQuery', () => { let queryClient; diff --git a/src/composables/queries/useAdministrationsStatsQuery.test.js b/src/composables/queries/useAdministrationsStatsQuery.test.js index 0d779334f..5184d7f6c 100644 --- a/src/composables/queries/useAdministrationsStatsQuery.test.js +++ b/src/composables/queries/useAdministrationsStatsQuery.test.js @@ -1,4 +1,4 @@ -import { ref, toValue, nextTick } from 'vue'; +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; diff --git a/src/composables/queries/useClassesQuery.js b/src/composables/queries/useClassesQuery.js index ddeb68acc..90f01bb59 100644 --- a/src/composables/queries/useClassesQuery.js +++ b/src/composables/queries/useClassesQuery.js @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/vue-query'; -import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; import { fetchDocumentsById } from '@/helpers/query/utils'; import { hasArrayEntries } from '@/helpers/hasArrayEntries'; diff --git a/src/composables/queries/useDistrictsQuery.test.js b/src/composables/queries/useDistrictsQuery.test.js index ee0038905..81ac3ea11 100644 --- a/src/composables/queries/useDistrictsQuery.test.js +++ b/src/composables/queries/useDistrictsQuery.test.js @@ -1,4 +1,4 @@ -import { ref, nextTick, toValue } from 'vue'; +import { ref, nextTick } from 'vue'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as VueQuery from '@tanstack/vue-query'; import { nanoid } from 'nanoid'; diff --git a/src/composables/queries/useSchoolsQuery.js b/src/composables/queries/useSchoolsQuery.js index 803a2640d..8efbe2a53 100644 --- a/src/composables/queries/useSchoolsQuery.js +++ b/src/composables/queries/useSchoolsQuery.js @@ -1,5 +1,4 @@ import { useQuery } from '@tanstack/vue-query'; -import _isEmpty from 'lodash/isEmpty'; import { computeQueryOverrides } from '@/helpers/computeQueryOverrides'; import { hasArrayEntries } from '@/helpers/hasArrayEntries'; import { fetchDocumentsById } from '@/helpers/query/utils'; From ee620d9c87b6682cd5d4b395ffa6c7ffc8dacf81 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 17:31:06 +0100 Subject: [PATCH 07/14] Fix query state reactivity --- src/components/CreateAdministration.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 47ae94b25..044eb3655 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -280,7 +280,7 @@ const invalidateAllQueries = async () => { // Fetch the data of the currently being edited administration, incl. its assigned assessments. const { data: existingAdministrationData } = useAdministrationsQuery([props.adminId], { - enabled: initialized.value && !!props.adminId, + enabled: initialized && !!props.adminId, select: (data) => data[0], }); @@ -306,7 +306,7 @@ const { data: existingSchoolsData } = useSchoolsQuery(schoolIds, { const classIds = computed(() => existingAdministrationData.value?.minimalOrgs?.classes ?? []); const { data: existingClassesData } = useClassesQuery(classIds, { - enabled: initialized.value && classIds.value.length > 0, + enabled: initialized && classIds.value.length > 0, }); // Grab groups from existingAdministrationData.minimalOrgs.groups From d6dff84ea39b6438c71c66ebab39e798f8951ab5 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 21:30:33 +0100 Subject: [PATCH 08/14] Replace groups query with useGroupsQuery composable --- src/components/CreateAdministration.vue | 28 ++++++------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 044eb3655..94e0f4663 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -192,6 +192,7 @@ import useAdministrationsQuery from '@/composables/queries/useAdministrationsQue import useDistrictsQuery from '@/composables/queries/useDistrictsQuery'; import useSchoolsQuery from '@/composables/queries/useSchoolsQuery'; import useClassesQuery from '@/composables/queries/useClassesQuery'; +import useGroupsQuery from '@/composables/queries/useGroupsQuery'; import useTaskVariantsQuery from '@/composables/queries/useTaskVariantsQuery'; import TaskPicker from './TaskPicker.vue'; import ConsentPicker from './ConsentPicker.vue'; @@ -309,20 +310,11 @@ const { data: existingClassesData } = useClassesQuery(classIds, { enabled: initialized && classIds.value.length > 0, }); -// Grab groups from existingAdministrationData.minimalOrgs.groups -const groupsToGrab = computed(() => { - const groupIds = _get(existingAdministrationData.value, 'minimalOrgs.groups', []); - return groupIds.map((id) => { - return { - collection: 'groups', - docId: id, - select: ['name'], - }; - }); -}); +// Fetch the groups assigned to the administration. +const groupIds = computed(() => existingAdministrationData.value?.minimalOrgs?.groups ?? []); -const shouldGrabGroups = computed(() => { - return initialized.value && groupsToGrab.value.length > 0; +const { data: existingGroupData } = useGroupsQuery(groupIds, { + enabled: initialized, }); // Grab families from existingAdministrationData.families @@ -341,14 +333,6 @@ const shouldGrabFamilies = computed(() => { return initialized.value && familiesToGrab.value.length > 0; }); -const { data: preGroups } = useQuery({ - queryKey: ['groups', props.adminId], - queryFn: () => fetchDocsById(groupsToGrab.value), - keepPreviousData: true, - enabled: shouldGrabGroups, - staleTime: 5 * 60 * 1000, // 5 minutes -}); - const { data: preFamilies } = useQuery({ queryKey: ['families', props.adminId], queryFn: () => fetchDocsById(familiesToGrab.value), @@ -419,7 +403,7 @@ const orgsList = computed(() => { districts: existingDistrictsData.value, schools: existingSchoolsData.value, classes: existingClassesData.value, - groups: preGroups.value, + groups: existingGroupData.value, families: preFamilies.value, }; }); From deca7f509869c140dbfcaac866885017570d8a06 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 24 Sep 2024 21:35:03 +0100 Subject: [PATCH 09/14] Replace families query with useFamiliesQuery composable --- src/components/CreateAdministration.vue | 28 ++++++------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 94e0f4663..68f334beb 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -193,6 +193,7 @@ import useDistrictsQuery from '@/composables/queries/useDistrictsQuery'; import useSchoolsQuery from '@/composables/queries/useSchoolsQuery'; import useClassesQuery from '@/composables/queries/useClassesQuery'; import useGroupsQuery from '@/composables/queries/useGroupsQuery'; +import useFamiliesQuery from '@/composables/queries/useFamiliesQuery'; import useTaskVariantsQuery from '@/composables/queries/useTaskVariantsQuery'; import TaskPicker from './TaskPicker.vue'; import ConsentPicker from './ConsentPicker.vue'; @@ -317,28 +318,11 @@ const { data: existingGroupData } = useGroupsQuery(groupIds, { enabled: initialized, }); -// Grab families from existingAdministrationData.families -const familiesToGrab = computed(() => { - const familyIds = _get(existingAdministrationData.value, 'minimalOrgs.families', []); - return familyIds.map((id) => { - return { - collection: 'families', - docId: id, - select: ['name'], - }; - }); -}); +// Fetch the families assigned to the administration. +const familyIds = computed(() => existingAdministrationData.value?.minimalOrgs?.families ?? []); -const shouldGrabFamilies = computed(() => { - return initialized.value && familiesToGrab.value.length > 0; -}); - -const { data: preFamilies } = useQuery({ - queryKey: ['families', props.adminId], - queryFn: () => fetchDocsById(familiesToGrab.value), - keepPreviousData: true, - enabled: shouldGrabFamilies, - staleTime: 5 * 60 * 1000, // 5 minutes +const { data: existingFamiliesData } = useFamiliesQuery(familyIds, { + enabled: initialized, }); // +---------------------------------+ @@ -404,7 +388,7 @@ const orgsList = computed(() => { schools: existingSchoolsData.value, classes: existingClassesData.value, groups: existingGroupData.value, - families: preFamilies.value, + families: existingFamiliesData.value, }; }); From b3c25b6163fdf16f8b7cec4eb1f4205d3ab35a42 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Wed, 25 Sep 2024 00:09:22 +0100 Subject: [PATCH 10/14] Refactor CreateAdminitration page data handling --- src/components/CreateAdministration.vue | 319 +++++++----------- src/components/TaskPicker.vue | 6 +- .../useUpsertAdministrationMutation.js | 40 +++ src/constants/mutationKeys.js | 1 + src/constants/routes.js | 1 + 5 files changed, 169 insertions(+), 198 deletions(-) create mode 100644 src/composables/mutations/useUpsertAdministrationMutation.js diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 68f334beb..1dfb41290 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -91,10 +91,9 @@ - + + +
+ > + {{ submitLabel }} +
@@ -167,26 +171,23 @@ diff --git a/src/components/TaskPicker.vue b/src/components/TaskPicker.vue index 2419ea8e3..6f664b61f 100644 --- a/src/components/TaskPicker.vue +++ b/src/components/TaskPicker.vue @@ -182,12 +182,10 @@ const taskOptions = computed(() => { watch( () => props.inputVariants, (newVariants) => { - // @TODO: Fix this as it's not working as expected. When updating the data set, the data is shown twice. - console.log('debug: inputVariants changed', JSON.parse(JSON.stringify(newVariants))); + // @TODO: Fix this as it's not working as expected. When updating the data set in the parent component, the data is + // added twice to the selectedVariants array, despite the _union call. selectedVariants.value = _union(selectedVariants.value, newVariants); - console.log('debug: selectedVariants', JSON.parse(JSON.stringify(selectedVariants.value))); - // Update the conditions for the variants that were pre-existing selectedVariants.value = selectedVariants.value.map((variant) => { const preExistingInfo = props.preExistingAssessmentInfo.find((info) => info?.variantId === variant?.id); From ca3702b4a68d8c5166831752f728752c93867daf Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Wed, 25 Sep 2024 21:33:06 +0100 Subject: [PATCH 13/14] Fix query reactivity --- src/components/CreateAdministration.vue | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index 6e803a956..e8e36f43b 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -262,8 +262,9 @@ const { data: allVariants } = useTaskVariantsQuery(true, { // | Fetch pre-existing administration data when editing an administration // +------------------------------------------------------------------------------------------------------------------+ // Fetch the data of the currently being edited administration, incl. its assigned assessments. +const fetchAdminitrations = computed(() => initialized.value && !!props.adminId); const { data: existingAdministrationData } = useAdministrationsQuery([props.adminId], { - enabled: initialized && !!props.adminId, + enabled: fetchAdminitrations, select: (data) => data[0], }); @@ -271,10 +272,6 @@ const existingAssessments = computed(() => { return existingAdministrationData?.value?.assessments ?? []; }); -watch(existingAssessments, (existingAssessments) => { - console.log('debug: existingAssessments', existingAssessments.value); -}); - // Fetch the districts assigned to the administration. const districtIds = computed(() => existingAdministrationData?.value?.minimalOrgs?.districts ?? []); From a5bcabf5bd27bc6f13c22a4c3712df408c464ad3 Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Wed, 25 Sep 2024 22:09:47 +0100 Subject: [PATCH 14/14] Fix document path destructuring --- src/helpers/query/utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/helpers/query/utils.js b/src/helpers/query/utils.js index ca06fa87c..96b661004 100644 --- a/src/helpers/query/utils.js +++ b/src/helpers/query/utils.js @@ -200,9 +200,13 @@ export const fetchDocumentsById = async (collection, docIds, select = [], db = F return response.data .filter(({ found }) => found) .map(({ found }) => { - const [, , collectionName, docId] = found.name.split('/'); + // Deconstruct the document path as Firebase conveniently doesn't return basic information like the record ID as + // part of the documentation data. Whilst this is a bit hacky, it works. + const pathParts = found.name.split('/'); + const documentId = pathParts.pop(); + const collectionName = pathParts.pop(); return { - id: docId, + id: documentId, collection: collectionName, ..._mapValues(found.fields, (value) => convertValues(value)), };