diff --git a/src/components/CreateAdministration.vue b/src/components/CreateAdministration.vue index fdbb72eff..e8e36f43b 100644 --- a/src/components/CreateAdministration.vue +++ b/src/components/CreateAdministration.vue @@ -91,10 +91,9 @@ - + + +
+ > + {{ submitLabel }} +
@@ -167,41 +171,49 @@ diff --git a/src/components/TaskPicker.vue b/src/components/TaskPicker.vue index cfc524843..6f664b61f 100644 --- a/src/components/TaskPicker.vue +++ b/src/components/TaskPicker.vue @@ -182,11 +182,14 @@ const taskOptions = computed(() => { watch( () => props.inputVariants, (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); // 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); + if (preExistingInfo) { return { ...variant, variant: { ...variant?.variant, conditions: preExistingInfo.conditions } }; } @@ -205,7 +208,6 @@ const updateVariant = (variantId, conditions) => { }); selectedVariants.value = updatedVariants; return; - // props.selectedVariant[] }; const selectedVariants = ref([]); diff --git a/src/composables/mutations/useUpsertAdministrationMutation.js b/src/composables/mutations/useUpsertAdministrationMutation.js new file mode 100644 index 000000000..8691ceefc --- /dev/null +++ b/src/composables/mutations/useUpsertAdministrationMutation.js @@ -0,0 +1,40 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; +import { useAuthStore } from '@/store/auth'; +import { ADMINISTRATION_UPSERT_MUTATION_KEY } from '@/constants/mutationKeys'; +import { + ADMINISTRATIONS_QUERY_KEY, + ADMINISTRATIONS_LIST_QUERY_KEY, + ADMINISTRATION_ASSIGNMENTS_QUERY_KEY, +} from '@/constants/queryKeys'; + +/** + * Upsert Administration mutation. + * + * TanStack mutation to update or insert an administration and automatically invalidate the corresponding queries. + * + * @returns {Object} The mutation object returned by `useMutation`. + */ +const useUpsertAdministrationMutation = () => { + const authStore = useAuthStore(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ADMINISTRATION_UPSERT_MUTATION_KEY, + mutationFn: async (data) => { + await authStore.roarfirekit.createAdministration(data); + }, + onSuccess: () => { + // Invalidate the queries to refetch the administration data. + // @NOTE: Usually we would apply a more granular invalidation strategy including updating the specific + // adminitration record in the cache. However, unfortunately, given the nature of the data model and the data that + // is updated in the application, we would have to manually map the updated data, which could cause issues when + // the data model changes. Therefore, we invalidate the entire query to ensure the data is up-to-date. + queryClient.invalidateQueries({ queryKey: [ADMINISTRATIONS_QUERY_KEY] }); + queryClient.invalidateQueries({ queryKey: [ADMINISTRATIONS_LIST_QUERY_KEY] }); + queryClient.invalidateQueries({ queryKey: [ADMINISTRATION_ASSIGNMENTS_QUERY_KEY] }); + console.warn('Administration upserted successfully.'); + }, + }); +}; + +export default useUpsertAdministrationMutation; diff --git a/src/composables/queries/useAdministrationsQuery.js b/src/composables/queries/useAdministrationsQuery.js index 12e812792..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,19 +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, - select: ['name', 'publicName', 'sequential', 'assessments', 'legal'], - }; - }), - ), - ...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..5ac1eec34 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, 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) => { @@ -18,14 +18,6 @@ vi.mock('@tanstack/vue-query', async (getModule) => { }; }); -const buildCollectionRequestPayload = (id) => { - return { - collection: 'administrations', - docId: id, - select: ['name', 'publicName', 'sequential', 'assessments', 'legal'], - }; -}; - describe('useAdministrationsQuery', () => { let queryClient; @@ -46,13 +38,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 +59,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: false, + 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: 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..5184d7f6c 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, 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..90f01bb59 100644 --- a/src/composables/queries/useClassesQuery.js +++ b/src/composables/queries/useClassesQuery.js @@ -1,8 +1,7 @@ -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 +14,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..81ac3ea11 100644 --- a/src/composables/queries/useDistrictsQuery.test.js +++ b/src/composables/queries/useDistrictsQuery.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,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..8efbe2a53 100644 --- a/src/composables/queries/useSchoolsQuery.js +++ b/src/composables/queries/useSchoolsQuery.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 { SCHOOLS_QUERY_KEY } from '@/constants/queryKeys'; import { FIRESTORE_COLLECTIONS } from '@/constants/firebase'; @@ -15,12 +14,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/constants/mutationKeys.js b/src/constants/mutationKeys.js index 00648609c..2bd7308cb 100644 --- a/src/constants/mutationKeys.js +++ b/src/constants/mutationKeys.js @@ -1,3 +1,4 @@ +export const ADMINISTRATION_UPSERT_MUTATION_KEY = 'administration-upsert'; export const TASK_ADD_MUTATION_KEY = 'task-add'; export const TASK_UPDATE_MUTATION_KEY = 'task-update'; export const TASK_VARIANT_ADD_MUTATION_KEY = 'task-variant-add'; diff --git a/src/constants/routes.js b/src/constants/routes.js index 3577c905e..19d1deeb4 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -7,6 +7,7 @@ * @constant {Object} APP_ROUTES – The individual routes of the application. */ export const APP_ROUTES = { + HOME: '/', SIGN_IN: '/signin', PROGRESS_REPORT: '/administration/:administrationId/:orgType/:orgId', SCORE_REPORT: '/scores/:administrationId/:orgType/:orgId', 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..96b661004 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, @@ -185,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)), };