// @ts-check /** * This is a file used especially in this `taxonomy` module. * * We are using a new approach, using `useQuery` to build and execute the queries to the APIs. * This approach accelerates the development. * * In this file you will find two types of hooks: * - Hooks that builds the query with `useQuery`. These hooks are not used outside of this file. * Ex. useTaxonomyListData. * - Hooks that calls the query hook, prepare and return the data. * Ex. useTaxonomyListDataResponse & useIsTaxonomyListDataLoaded. */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { camelCaseObject } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { apiUrls, ALL_TAXONOMIES } from './api'; import * as api from './api'; // Query key patterns. Allows an easy way to clear all data related to a given taxonomy. // https://github.com/openedx/frontend-app-admin-portal/blob/2ba315d/docs/decisions/0006-tanstack-react-query.rst // Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories. export const taxonomyQueryKeys = { all: ['taxonomies'], /** * Key for the list of taxonomies, optionally filtered by org. * @param {string} [org] Which org we fetched the taxonomy list for (optional) */ taxonomyList: (org) => [ ...taxonomyQueryKeys.all, 'taxonomyList', ...(org && org !== ALL_TAXONOMIES ? [org] : []), ], /** * Base key for data specific to a single taxonomy. No data is stored directly in this key. * @param {number} taxonomyId ID of the taxonomy */ taxonomy: (taxonomyId) => [...taxonomyQueryKeys.all, 'taxonomy', taxonomyId], /** * @param {number} taxonomyId ID of the taxonomy */ taxonomyMetadata: (taxonomyId) => [...taxonomyQueryKeys.taxonomy(taxonomyId), 'metadata'], /** * @param {number} taxonomyId ID of the taxonomy */ taxonomyTagList: (taxonomyId) => [...taxonomyQueryKeys.taxonomy(taxonomyId), 'tags'], /** * @param {number} taxonomyId ID of the taxonomy * @param {number} pageIndex Which page of tags to load (zero-based) * @param {number} pageSize */ taxonomyTagListPage: (taxonomyId, pageIndex, pageSize) => [ ...taxonomyQueryKeys.taxonomyTagList(taxonomyId), 'page', pageIndex, pageSize, ], /** * Query for loading _all_ the subtags of a particular parent tag * @param {number} taxonomyId ID of the taxonomy * @param {string} parentTagValue */ taxonomyTagSubtagsList: (taxonomyId, parentTagValue) => [ ...taxonomyQueryKeys.taxonomyTagList(taxonomyId), 'subtags', parentTagValue, ], /** * @param {number} taxonomyId ID of the taxonomy * @param {string} fileId Some string to uniquely identify the file we want to upload */ importPlan: (taxonomyId, fileId) => [...taxonomyQueryKeys.all, 'importPlan', taxonomyId, fileId], }; /** * Builds the query to get the taxonomy list * @param {string} [org] Filter the list to only show taxonomies assigned to this org */ export const useTaxonomyList = (org) => ( useQuery({ queryKey: taxonomyQueryKeys.taxonomyList(org), queryFn: () => api.getTaxonomyListData(org), }) ); /** * Builds the mutation to delete a taxonomy. * @returns A function that can be used to delete the taxonomy. */ export const useDeleteTaxonomy = () => { const queryClient = useQueryClient(); const { mutateAsync } = useMutation({ /** @type {import("@tanstack/react-query").MutateFunction} */ mutationFn: async ({ pk }) => api.deleteTaxonomy(pk), onSettled: (_d, _e, args) => { queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyList() }); queryClient.removeQueries({ queryKey: taxonomyQueryKeys.taxonomy(args.pk) }); }, }); return mutateAsync; }; /** Builds the query to get the taxonomy detail * @param {number} taxonomyId */ export const useTaxonomyDetails = (taxonomyId) => useQuery({ queryKey: taxonomyQueryKeys.taxonomyMetadata(taxonomyId), queryFn: () => api.getTaxonomy(taxonomyId), }); /** * Use this mutation to import a new taxonomy. */ export const useImportNewTaxonomy = () => { const queryClient = useQueryClient(); return useMutation({ /** * @type {import("@tanstack/react-query").MutateFunction< * import("./types.js").TaxonomyData, * any, * { * name: string, * description: string, * file: File, * } * >} */ mutationFn: async ({ name, description, file, }) => { const formData = new FormData(); formData.append('taxonomy_name', name); formData.append('taxonomy_description', description); formData.append('file', file); const { data } = await getAuthenticatedHttpClient().post(apiUrls.createTaxonomyFromImport(), formData); return camelCaseObject(data); }, onSuccess: (data) => { // There's a new taxonomy, so the list of taxonomies needs to be refreshed: queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyList(), }); queryClient.setQueryData(taxonomyQueryKeys.taxonomyMetadata(data.id), data); }, }); }; /** * Build the mutation to import tags to an existing taxonomy */ export const useImportTags = () => { const queryClient = useQueryClient(); return useMutation({ /** * @type {import("@tanstack/react-query").MutateFunction< * import("./types.js").TaxonomyData, * any, * { * taxonomyId: number, * file: File, * } * >} */ mutationFn: async ({ taxonomyId, file }) => { const formData = new FormData(); formData.append('file', file); try { const { data } = await getAuthenticatedHttpClient().put(apiUrls.tagsImport(taxonomyId), formData); return camelCaseObject(data); } catch (/** @type {any} */ err) { throw new Error(err.response?.data?.error || err.message); } }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: taxonomyQueryKeys.taxonomyTagList(data.id), }); // In the metadata, 'tagsCount' (and possibly other fields) will have changed: queryClient.setQueryData(taxonomyQueryKeys.taxonomyMetadata(data.id), data); }, }); }; /** * Preview the results of importing the given file into an existing taxonomy. * @param {number} taxonomyId The ID of the taxonomy whose tags we're updating. * @param {File|null} file The file that we want to import */ export const useImportPlan = (taxonomyId, file) => useQuery({ queryKey: taxonomyQueryKeys.importPlan(taxonomyId, file ? `${file.name}${file.lastModified}${file.size}` : ''), /** * @type {import("@tanstack/react-query").QueryFunction} */ queryFn: async () => { if (!taxonomyId || file === null) { return null; } const formData = new FormData(); formData.append('file', file); try { const { data } = await getAuthenticatedHttpClient().put(apiUrls.tagsPlanImport(taxonomyId), formData); return /** @type {string} */(data.plan); } catch (/** @type {any} */ err) { throw new Error(err.response?.data?.error || err.message); } }, retry: false, // If there's an error, it's probably a real problem with the file. Don't try again several times! }); /** * @param {number} taxonomyId * @param {import('./types.js').QueryOptions} options * @returns {import('@tanstack/react-query').UseQueryResult} */ export const useTagListData = (taxonomyId, options) => { const { pageIndex, pageSize } = options; return useQuery({ queryKey: taxonomyQueryKeys.taxonomyTagListPage(taxonomyId, pageIndex, pageSize), queryFn: async () => { const { data } = await getAuthenticatedHttpClient().get(apiUrls.tagList(taxonomyId, pageIndex, pageSize)); return camelCaseObject(data); }, }); }; /** * Temporary hook to load *all* the subtags of a given tag in a taxonomy. * Doesn't handle pagination or anything. This is meant to be replaced by * something more sophisticated later, as we improve the "taxonomy details" page. * @param {number} taxonomyId * @param {string} parentTagValue * @returns {import('@tanstack/react-query').UseQueryResult} */ export const useSubTags = (taxonomyId, parentTagValue) => useQuery({ queryKey: taxonomyQueryKeys.taxonomyTagSubtagsList(taxonomyId, parentTagValue), queryFn: async () => { const response = await getAuthenticatedHttpClient().get(apiUrls.allSubtagsOf(taxonomyId, parentTagValue)); return camelCaseObject(response.data); }, });