From fed4161d08eb50d608a08c1e05255356c2fe86ce Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 1 Oct 2024 11:09:03 +0100 Subject: [PATCH 1/4] Introduce useTasksDictionaryQuery composable --- .../queries/useTasksDictionaryQuery.js | 28 +++++ .../queries/useTasksDictionaryQuery.test.js | 114 ++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/composables/queries/useTasksDictionaryQuery.js create mode 100644 src/composables/queries/useTasksDictionaryQuery.test.js diff --git a/src/composables/queries/useTasksDictionaryQuery.js b/src/composables/queries/useTasksDictionaryQuery.js new file mode 100644 index 000000000..8eccccec7 --- /dev/null +++ b/src/composables/queries/useTasksDictionaryQuery.js @@ -0,0 +1,28 @@ +import { computed } from 'vue'; +import useTasksQuery from '@/composables/queries/useTasksQuery'; + +/** + * Tasks dictionary query. + * + * Leverage the useTasksQuery composable to fetch tasks and reduce them into a dictionary. This is useful for quickly + * accessing tasks by ID without having to iterate over the potentially large tasks array. + * + * @param {QueryOptions|undefined} queryOptions – Optional TanStack query options. + * @returns {UseQueryResult} The TanStack query result with the tasks dictionary. + */ +const useTasksDictionaryQuery = (queryOptions = undefined) => { + const { data, ...queryState } = useTasksQuery(true, undefined, queryOptions); + + const tasksDictionary = computed(() => { + return Array.isArray(data.value) + ? data.value.reduce((acc, doc) => { + acc[doc.id] = doc; + return acc; + }, {}) + : {}; + }); + + return { data: tasksDictionary, ...queryState }; +}; + +export default useTasksDictionaryQuery; diff --git a/src/composables/queries/useTasksDictionaryQuery.test.js b/src/composables/queries/useTasksDictionaryQuery.test.js new file mode 100644 index 000000000..c73671ffb --- /dev/null +++ b/src/composables/queries/useTasksDictionaryQuery.test.js @@ -0,0 +1,114 @@ +import { nextTick, ref } from 'vue'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { withSetup } from '@/test-support/withSetup.js'; +import * as VueQuery from '@tanstack/vue-query'; +import useTasksQuery from './useTasksQuery'; +import useTasksDictionaryQuery from './useTasksDictionaryQuery'; + +vi.mock('@/helpers/query/tasks', () => ({ + taskFetcher: vi.fn().mockImplementation(() => []), + fetchByTaskId: vi.fn().mockImplementation(() => []), +})); + +vi.mock('@tanstack/vue-query', async (getModule) => { + const original = await getModule(); + return { + ...original, + useQuery: vi.fn().mockImplementation(original.useQuery), + }; +}); + +// vi.mock('./useTasksQuery', () => ({ +// default: vi.fn(), +// })) + +describe('useTasksQuery', () => { + let queryClient; + + beforeEach(() => { + queryClient = new VueQuery.QueryClient(); + }); + + afterEach(() => { + queryClient?.clear(); + }); + + describe('useTasksDictionaryQuery', () => { + it('should return an empty dictionary when data is undefined', () => { + VueQuery.useQuery.mockReturnValue({ + data: ref(undefined), + isLoading: ref(false), + error: ref(null), + }); + + const [result] = withSetup(() => useTasksDictionaryQuery(), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + const { data: tasksDictionary } = result; + + expect(tasksDictionary.value).toEqual({}); + }); + + it('should return a dictionary of tasks when data is an array', () => { + const mockData = [ + { id: '1', name: 'Task 1' }, + { id: '2', name: 'Task 2' }, + ]; + + VueQuery.useQuery.mockReturnValue({ + data: ref(mockData), + isLoading: ref(false), + error: ref(null), + }); + + const [result] = withSetup(() => useTasksDictionaryQuery(), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + const { data: tasksDictionary } = result; + + expect(tasksDictionary.value).toEqual({ + 1: { id: '1', name: 'Task 1' }, + 2: { id: '2', name: 'Task 2' }, + }); + }); + + it('should return the query state properties', () => { + const mockData = [ + { id: '1', name: 'Task 1' }, + { id: '2', name: 'Task 2' }, + ]; + + VueQuery.useQuery.mockReturnValue({ + data: ref(mockData), + isLoading: ref(true), + error: ref(null), + }); + + const [result] = withSetup(() => useTasksDictionaryQuery(), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + const { data: tasksDictionary, isLoading, error } = result; + + expect(tasksDictionary.value).toEqual({ + 1: { id: '1', name: 'Task 1' }, + 2: { id: '2', name: 'Task 2' }, + }); + expect(isLoading.value).toBe(true); + expect(error.value).toBe(null); + }); + + it('should pass queryOptions to the dependent tasks query', () => { + const queryOptions = { enabled: false, refetchOnWindowFocus: false }; + vi.spyOn(VueQuery, 'useQuery'); + + withSetup(() => useTasksDictionaryQuery(queryOptions), { + plugins: [[VueQuery.VueQueryPlugin, { queryClient }]], + }); + + expect(VueQuery.useQuery).toHaveBeenCalledWith(expect.objectContaining(queryOptions)); + }); + }); +}); From b99405467ec514e5a8c1ca5e47f6d2d87311f9ab Mon Sep 17 00:00:00 2001 From: Maximilian Oertel Date: Tue, 1 Oct 2024 14:08:17 +0100 Subject: [PATCH 2/4] Replace authStore tasks dictionary in favour of query composable --- src/components/CardAdministration.vue | 32 ++- .../reports/DistributionChartFacet.vue | 21 +- .../reports/DistributionChartOverview.vue | 21 +- .../reports/DistributionChartSupport.vue | 11 +- .../reports/IndividualScoreReportTask.vue | 256 ++++++++--------- src/components/reports/tasks/TaskReport.vue | 18 +- .../queries/useTasksDictionaryQuery.test.js | 8 +- src/pages/ProgressReport.vue | 266 +++++++++--------- src/pages/ScoreReport.vue | 18 +- src/pages/StudentReport.vue | 18 +- src/store/auth.js | 13 - 11 files changed, 347 insertions(+), 335 deletions(-) diff --git a/src/components/CardAdministration.vue b/src/components/CardAdministration.vue index 09b08919e..31e386db3 100644 --- a/src/components/CardAdministration.vue +++ b/src/components/CardAdministration.vue @@ -24,24 +24,29 @@ +
Dates: {{ processedDates.start.toLocaleDateString() }} — {{ processedDates.end.toLocaleDateString() }}
+
Assessments: - - {{ tasksDictionary[assessmentId]?.publicName ?? assessmentId }} - - + +
@@ -62,6 +67,7 @@
+
+ { return window.innerWidth > 768; }); +const { data: tasksDictionary, isLoading: isLoadingTasksDictionary } = useTasksDictionaryQuery(); + const { data: orgs, isLoading: isLoadingDsgfOrgs } = useDsgfOrgQuery(props.id, props.assignees, { enabled: enableQueries, }); diff --git a/src/components/reports/DistributionChartFacet.vue b/src/components/reports/DistributionChartFacet.vue index 2f936424b..c96264b95 100644 --- a/src/components/reports/DistributionChartFacet.vue +++ b/src/components/reports/DistributionChartFacet.vue @@ -18,11 +18,7 @@