Files
frontend-app-authoring/src/taxonomy/data/apiHooks.js
Braden MacDonald abe68ac599 refactor: Convert more Taxonomy code to TypeScript (#1532)
* Converts some files from .js or .mjs to .ts
* Moves the API code from src/taxonomy/tag-list/data into src/taxonomy/data
* Cleans up and improves some type definitions
* No user-visible changes / functionality changes.
2024-11-27 15:31:55 -05:00

237 lines
8.5 KiB
JavaScript

// @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<any, any, {pk: number}>} */
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<string|null>}
*/
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<import('./types.js').TagListData>}
*/
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<import('./types.js').TagListData>}
*/
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);
},
});