Files
frontend-app-authoring/src/library-authoring/data/apiHooks.ts
Navin Karkera e2f1aedf9a feat: import analysis step (#2657)
Shows course analysis information in review import details step in course import stepper page. Also handles alerts based on the import status, like, reimport or unsupported number of blocks.
2025-11-18 11:41:27 -05:00

984 lines
35 KiB
TypeScript

import { camelCaseObject } from '@edx/frontend-platform';
import {
useQuery,
useMutation,
useQueryClient,
type Query,
type QueryClient,
replaceEqualDeep,
keepPreviousData,
skipToken,
} from '@tanstack/react-query';
import { useCallback } from 'react';
import { type MeiliSearch } from 'meilisearch';
import { getBlockType, getLibraryId } from '../../generic/key-utils';
import * as api from './api';
import { VersionSpec } from '../LibraryBlock';
import { useContentSearchConnection, useContentSearchResults, buildSearchQueryKey } from '../../search-manager';
export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => {
// Invalidate all content queries related to this library.
// If we allow searching "all courses and libraries" in the future,
// then we'd have to invalidate all `["content_search", "results"]`
// queries, and not just the ones for this library, because items from
// this library could be included in an "all courses and libraries"
// search. For now we only allow searching individual libraries.
const extraFilter = query.queryKey[5]; // extraFilter contains library id
if (!(Array.isArray(extraFilter) || typeof extraFilter === 'string')) {
return false;
}
return query.queryKey[0] === 'content_search' && extraFilter?.includes(`context_key = "${libraryId}"`);
};
export const libraryAuthoringQueryKeys = {
all: ['contentLibrary'],
/**
* Base key for data specific to a contentLibrary
*/
contentLibrary: (contentLibraryId?: string) => [...libraryAuthoringQueryKeys.all, contentLibraryId],
/** All keys for content within the library should be below this key */
contentLibraryContent: (contentLibraryId?: string) => [
...libraryAuthoringQueryKeys.contentLibrary(contentLibraryId),
'content',
],
/** Keys for the list of all libraries */
contentLibraryList: (customParams?: api.GetLibrariesV2CustomParams) => [
...libraryAuthoringQueryKeys.all,
'list',
...(customParams ? [customParams] : []),
],
libraryTeam: (libraryId?: string) => [
...libraryAuthoringQueryKeys.all,
'list',
libraryId,
],
collection: (libraryId?: string, collectionId?: string) => [
...libraryAuthoringQueryKeys.contentLibraryContent(libraryId),
'collection',
collectionId,
],
blockTypes: (libraryId?: string) => [
...libraryAuthoringQueryKeys.all,
'blockTypes',
libraryId,
],
allContainers: (libraryId?: string) => [
...libraryAuthoringQueryKeys.contentLibraryContent(libraryId),
'container',
],
container: (containerId?: string) => {
const baseKey = containerId
? libraryAuthoringQueryKeys.allContainers(getLibraryId(containerId))
: [...libraryAuthoringQueryKeys.all, 'container'];
return [
...baseKey,
containerId,
];
},
containerChildren: (containerId: string) => [
...libraryAuthoringQueryKeys.container(containerId),
'children',
],
containerHierarchy: (containerId?: string) => {
if (containerId) {
return [
'hierarchy',
...libraryAuthoringQueryKeys.container(containerId),
];
}
return ['hierarchy'];
},
courseImports: (libraryId: string) => [
...libraryAuthoringQueryKeys.contentLibrary(libraryId),
'courseImports',
],
allMigrationInfo: () => [...libraryAuthoringQueryKeys.all, 'migrationInfo'],
migrationInfo: (sourceKeys: string[]) => [
...libraryAuthoringQueryKeys.allMigrationInfo(),
...sourceKeys,
],
};
export const xblockQueryKeys = {
all: ['xblock'],
/**
* Base key for data specific to a xblock
*/
xblock: (usageKey?: string) => [...xblockQueryKeys.all, usageKey],
/** Fields (i.e. the content, display name, etc.) of an XBlock */
xblockFields: (usageKey: string, version: VersionSpec = 'draft') => [...xblockQueryKeys.xblock(usageKey), 'fields', version],
/** OLX (XML representation of the fields/content) */
xblockOLX: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'OLX'],
/** assets (static files) */
xblockAssets: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'assets'],
componentMetadata: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'componentMetadata'],
componentDownstreamLinks: (usageKey: string) => [...xblockQueryKeys.xblock(usageKey), 'downstreamLinks'],
/**
* Predicate used to invalidate all metadata only (not OLX, fields, assets, etc.).
* Affects all libraries; we could do a more complex version that affects only one library, but it would require
* introspecting the usage keys.
*/
allComponentMetadata: (query: Query) => query.queryKey[0] === 'xblock' && query.queryKey[2] === 'componentMetadata',
componentHierarchy: (usageKey?: string) => {
if (usageKey) {
return [
'hierarchy',
...xblockQueryKeys.xblock(usageKey),
];
}
return ['hierarchy'];
},
};
/**
* Tell react-query to refresh its cache of any data related to the given
* component (XBlock).
*
* Note that technically it's possible to derive the library key from the
* usageKey, so we could refactor this to only require the usageKey.
*
* @param queryClient The query client - get it via useQueryClient()
* @param contentLibraryId The ID of library that holds the XBlock ("lib:...")
* @param usageKey The usage ID of the XBlock ("lb:...")
*/
export function invalidateComponentData(queryClient: QueryClient, contentLibraryId: string, usageKey: string) {
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockFields(usageKey) });
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
// The description and display name etc. may have changed, so refresh everything in the library too:
// This might fail in case this helper is called after deleting the block.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(contentLibraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
}
/**
* Hook to fetch a content library by its ID.
*/
export const useContentLibrary = (libraryId: string | undefined) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId),
queryFn: () => api.getContentLibrary(libraryId!),
enabled: libraryId !== undefined,
})
);
/**
* Use this mutation to create a block in a library
*/
export const useCreateLibraryBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.createLibraryBlock,
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, variables.libraryId) });
},
});
};
/**
* Use this mutation to delete a block in a library
*/
export const useDeleteLibraryBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.deleteLibraryBlock,
onSettled: (_data, _error, variables) => {
const libraryId = getLibraryId(variables.usageKey);
invalidateComponentData(queryClient, libraryId, variables.usageKey);
},
});
};
/**
* Use this mutation to restore a deleted block in a library
*/
export const useRestoreLibraryBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.restoreLibraryBlock,
onSettled: (_data, _error, variables) => {
const libraryId = getLibraryId(variables.usageKey);
invalidateComponentData(queryClient, libraryId, variables.usageKey);
},
});
};
export const useUpdateLibraryMetadata = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.updateLibraryMetadata,
onMutate: async (data) => {
const queryKey = libraryAuthoringQueryKeys.contentLibrary(data.id);
const previousLibraryData = queryClient.getQueriesData({ queryKey })[0][1] as api.ContentLibrary;
const newLibraryData = {
...previousLibraryData,
...camelCaseObject(data),
};
queryClient.setQueryData(queryKey, newLibraryData);
return { previousLibraryData, newLibraryData };
},
onError: (_err, data, context) => {
queryClient.setQueryData(
libraryAuthoringQueryKeys.contentLibrary(data.id),
context?.previousLibraryData,
);
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.id) });
},
});
};
/**
* Builds the query to fetch list of V2 Libraries
*/
export const useContentLibraryV2List = (customParams: api.GetLibrariesV2CustomParams) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.contentLibraryList(customParams),
queryFn: () => api.getContentLibraryV2List(customParams),
placeholderData: keepPreviousData,
})
);
/** Publish all changes in the library. */
export const useCommitLibraryChanges = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.commitLibraryChanges,
onSettled: (_data, _error, libraryId) => {
// Invalidate all content-related metadata and search results for the whole library.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
// For XBlocks, the only thing we need to invalidate is the metadata which includes "has unpublished changes"
queryClient.invalidateQueries({ predicate: xblockQueryKeys.allComponentMetadata });
},
});
};
/** Discard all un-published changes in the library */
export const useRevertLibraryChanges = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.revertLibraryChanges,
onSettled: (_data, _error, libraryId) => {
// Invalidate all content-related metadata and search results for the whole library.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
// For XBlocks, the only thing we need to invalidate is the metadata which includes "has unpublished changes"
queryClient.invalidateQueries({ predicate: xblockQueryKeys.allComponentMetadata });
},
});
};
/**
* Hook to fetch a content library's team members
*/
export const useLibraryTeam = (libraryId?: string) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.libraryTeam(libraryId),
queryFn: () => api.getLibraryTeam(libraryId!),
enabled: libraryId !== undefined,
})
);
/**
* Hook to fetch the list of XBlock types that can be added to this library.
*/
export const useBlockTypesMetadata = (libraryId?: string) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.blockTypes(libraryId),
queryFn: () => api.getBlockTypes(libraryId!),
enabled: libraryId !== undefined,
})
);
/**
* Hook to add a new member to a content library's team
*/
export const useAddLibraryTeamMember = (libraryId: string | undefined) => {
const queryClient = useQueryClient();
const queryKey = libraryAuthoringQueryKeys.libraryTeam(libraryId);
return useMutation({
mutationFn: api.addLibraryTeamMember,
onSettled: () => {
queryClient.invalidateQueries({ queryKey });
},
});
};
/**
* Hook to delete an existing member from a content library's team
*/
export const useDeleteLibraryTeamMember = (libraryId: string | undefined) => {
const queryClient = useQueryClient();
const queryKey = libraryAuthoringQueryKeys.libraryTeam(libraryId);
return useMutation({
mutationFn: api.deleteLibraryTeamMember,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
},
});
};
/**
* Hook to update an existing member's access in a content library's team
*/
export const useUpdateLibraryTeamMember = (libraryId: string | undefined) => {
const queryClient = useQueryClient();
const queryKey = libraryAuthoringQueryKeys.libraryTeam(libraryId);
return useMutation({
mutationFn: api.updateLibraryTeamMember,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
},
});
};
export const useLibraryPasteClipboard = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: api.libraryPasteClipboard,
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(variables.libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, variables.libraryId) });
},
});
};
export const useLibraryBlockMetadata = (usageId: string | undefined) => (
useQuery({
queryKey: xblockQueryKeys.componentMetadata(usageId!),
queryFn: () => api.getLibraryBlockMetadata(usageId!),
enabled: !!usageId,
})
);
export const useXBlockFields = (usageKey: string, version: VersionSpec = 'draft') => (
useQuery({
queryKey: xblockQueryKeys.xblockFields(usageKey, version),
queryFn: () => api.getXBlockFields(usageKey, version),
enabled: !!usageKey,
})
);
export const useUpdateXBlockFields = (usageKey: string) => {
const contentLibraryId = getLibraryId(usageKey);
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: api.UpdateXBlockFieldsRequest) => api.updateXBlockFields(usageKey, data),
onMutate: async (data) => {
const queryKey = xblockQueryKeys.xblockFields(usageKey);
const previousBlockData = queryClient.getQueriesData({ queryKey })?.[0]?.[1] as api.XBlockFields | undefined;
const formatedData = camelCaseObject(data);
if (!previousBlockData) {
return { previousBlockData };
}
const newBlockData = {
...previousBlockData,
...(formatedData.metadata?.displayName && { displayName: formatedData.metadata.displayName }),
metadata: {
...previousBlockData.metadata,
...formatedData.metadata,
},
};
queryClient.setQueryData(queryKey, newBlockData);
return { previousBlockData };
},
onError: (_err, _data, context) => {
queryClient.setQueryData(
xblockQueryKeys.xblockFields(usageKey),
context?.previousBlockData,
);
},
onSettled: () => {
invalidateComponentData(queryClient, contentLibraryId, usageKey);
},
});
};
/**
* Use this mutation to create a library collection
*/
export const useCreateLibraryCollection = (libraryId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: api.CreateLibraryCollectionDataRequest) => api.createCollection(libraryId, data),
onSettled: () => {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/** Get the OLX source of a library component */
export const useXBlockOLX = (usageKey: string, version: VersionSpec) => (
useQuery({
queryKey: xblockQueryKeys.xblockOLX(usageKey),
queryFn: () => api.getXBlockOLX(usageKey, version),
enabled: !!usageKey,
})
);
/**
* Update the OLX of a library component (advanced feature)
*/
export const useUpdateXBlockOLX = (usageKey: string) => {
const contentLibraryId = getLibraryId(usageKey);
const queryClient = useQueryClient();
return useMutation({
mutationFn: (newOLX: string) => api.setXBlockOLX(usageKey, newOLX),
onSuccess: (olxFromServer) => {
queryClient.setQueryData(xblockQueryKeys.xblockOLX(usageKey), olxFromServer);
invalidateComponentData(queryClient, contentLibraryId, usageKey);
},
});
};
/**
* Publish changes to a library component
*/
export const usePublishComponent = (usageKey: string) => {
const queryClient = useQueryClient();
const contentLibraryId = getLibraryId(usageKey);
return useMutation({
mutationFn: () => api.publishXBlock(usageKey),
onSettled: () => {
invalidateComponentData(queryClient, contentLibraryId, usageKey);
},
});
};
/** Get the full hierarchy of the given library item (component/container) */
export const useLibraryItemHierarchy = (key: string) => {
let queryKey: (string | undefined)[];
let queryFn: () => Promise<api.ItemHierarchyData>;
if (key.startsWith('lb:')) {
queryKey = xblockQueryKeys.componentHierarchy(key);
queryFn = () => api.getBlockHierarchy(key);
} else {
queryKey = libraryAuthoringQueryKeys.containerHierarchy(key!);
queryFn = () => api.getLibraryContainerHierarchy(key!);
}
return useQuery({
queryKey,
queryFn,
enabled: !!key,
});
};
/** Get the list of assets (static files) attached to a library component */
export const useXBlockAssets = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockAssets(usageKey),
queryFn: () => api.getXBlockAssets(usageKey),
enabled: !!usageKey,
})
);
/** Refresh the list of assets (static files) attached to a library component */
export const useInvalidateXBlockAssets = (usageKey: string) => {
const client = useQueryClient();
return useCallback(() => {
client.invalidateQueries({ queryKey: xblockQueryKeys.xblockAssets(usageKey) });
}, [usageKey]);
};
/**
* Use this mutation to delete an asset file from a library
*/
export const useDeleteXBlockAsset = (usageKey: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (path: string) => api.deleteXBlockAsset(usageKey, path),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockAssets(usageKey) });
},
});
};
/**
* Get the metadata for a collection in a library
*/
export const useCollection = (libraryId: string, collectionId?: string) => (
useQuery({
enabled: !!libraryId && !!collectionId,
queryKey: libraryAuthoringQueryKeys.collection(libraryId, collectionId),
queryFn: () => api.getCollectionMetadata(libraryId!, collectionId!),
})
);
/**
* Use this mutation to update the fields of a collection in a library
*/
export const useUpdateCollection = (libraryId: string, collectionId: string) => {
const queryClient = useQueryClient();
const collectionQueryKey = libraryAuthoringQueryKeys.collection(libraryId, collectionId);
return useMutation({
mutationFn: (data: api.UpdateCollectionComponentsRequest) => (
api.updateCollectionMetadata(libraryId, collectionId, data)
),
onMutate: (data) => {
const previousData = queryClient.getQueryData(collectionQueryKey) as api.CollectionMetadata;
queryClient.setQueryData(collectionQueryKey, {
...previousData,
...data,
});
return { previousData };
},
onError: (_err, _data, context) => {
queryClient.setQueryData(collectionQueryKey, context?.previousData);
},
onSettled: () => {
// NOTE: We invalidate the library query here because we need to update the library's
// collection list.
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
queryClient.invalidateQueries({ queryKey: collectionQueryKey });
},
});
};
/**
* Use this mutation to add items to a collection in a library
*/
export const useAddItemsToCollection = (libraryId?: string, collectionId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (usageKeys: string[]) => {
if (libraryId !== undefined && collectionId !== undefined) {
return api.addItemsToCollection(libraryId, collectionId, usageKeys);
}
return undefined;
},
onSettled: () => {
if (libraryId !== undefined && collectionId !== undefined) {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
}
},
});
};
/**
* Use this mutation to remove items from a collection in a library
*/
export const useRemoveItemsFromCollection = (libraryId?: string, collectionId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (usageKeys: string[]) => {
if (libraryId !== undefined && collectionId !== undefined) {
return api.removeItemsFromCollection(libraryId, collectionId, usageKeys);
}
return undefined;
},
onSettled: () => {
if (libraryId !== undefined && collectionId !== undefined) {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
}
},
});
};
/**
* Use this mutation to soft delete collections in a library
*/
export const useDeleteCollection = (libraryId: string, collectionId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => api.deleteCollection(libraryId, collectionId),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Use this mutation to restore soft deleted collections in a library
*/
export const useRestoreCollection = (libraryId: string, collectionId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => api.restoreCollection(libraryId, collectionId),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Use this mutation to update collections related a component in a library
*/
export const useUpdateComponentCollections = (usageKey: string) => {
const queryClient = useQueryClient();
const libraryId = getLibraryId(usageKey);
return useMutation({
mutationFn: async (collectionKeys: string[]) => api.updateComponentCollections(usageKey, collectionKeys),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Use this mutation to create a library container
*/
export const useCreateLibraryContainer = (libraryId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: api.CreateLibraryContainerDataRequest) => api.createLibraryContainer(libraryId, data),
onSettled: () => {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Get the metadata for a container in a library
*/
export const useContainer = (containerId?: string) => (
useQuery({
enabled: !!containerId,
queryKey: libraryAuthoringQueryKeys.container(containerId!),
queryFn: () => api.getContainerMetadata(containerId!),
})
);
/**
* Use this mutation to update the fields of a container in a library.
*
* Use `affectedParentContainerId` to enable the optimistic update when the container
* is updated from a children list of a container
*/
export const useUpdateContainer = (containerId: string, affectedParentContainerId?: string) => {
const libraryId = getLibraryId(containerId);
const queryClient = useQueryClient();
const containerQueryKey = libraryAuthoringQueryKeys.container(containerId);
return useMutation({
mutationFn: (data: api.UpdateContainerDataRequest) => api.updateContainerMetadata(containerId, data),
onMutate: (data) => {
const previousData = queryClient.getQueryData(containerQueryKey) as api.Container;
if (previousData) {
queryClient.setQueryData(containerQueryKey, {
...previousData,
...data,
});
}
let childrenPreviousData;
if (affectedParentContainerId) {
const childrenQueryKey = libraryAuthoringQueryKeys.containerChildren(affectedParentContainerId);
childrenPreviousData = queryClient.getQueryData(childrenQueryKey) as api.Container[];
if (childrenPreviousData) {
queryClient.setQueryData(childrenQueryKey, childrenPreviousData.map(item => (
item.id === containerId ? { ...item, ...data } : item
)));
}
}
return { previousData, childrenPreviousData };
},
onError: (_err, _data, context) => {
if (context?.previousData) {
queryClient.setQueryData(containerQueryKey, context?.previousData);
}
if (affectedParentContainerId && context?.childrenPreviousData) {
const childrenQueryKey = libraryAuthoringQueryKeys.containerChildren(affectedParentContainerId);
queryClient.setQueryData(childrenQueryKey, context?.childrenPreviousData);
}
},
onSettled: () => {
// NOTE: We invalidate the library query here because we need to update the library's
// container list.
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
queryClient.invalidateQueries({ queryKey: containerQueryKey });
// NOTE: We invalidate all container query to update names in children list of containers
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.allContainers(libraryId) });
},
});
};
/**
* Use this mutation to soft delete containers in a library
*/
export const useDeleteContainer = (containerId: string) => {
const libraryId = getLibraryId(containerId);
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => api.deleteContainer(containerId),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Use this mutation to restore a container
*/
export const useRestoreContainer = (containerId: string) => {
const libraryId = getLibraryId(containerId);
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => api.restoreContainer(containerId),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Get the metadata and children for a container in a library
*/
export const useContainerChildren = <ChildType extends {
id: string;
isNew?: boolean;
} = api.LibraryBlockMetadata | api.Container>(
containerId?: string,
published: boolean = false,
) => (
useQuery({
enabled: !!containerId,
queryKey: libraryAuthoringQueryKeys.containerChildren(containerId!),
queryFn: () => api.getLibraryContainerChildren<ChildType>(containerId!, published),
structuralSharing: (oldData: ChildType[], newData: ChildType[]) => {
// This just sets `isNew` flag to new children components
if (oldData) {
const oldDataIds = oldData.map((obj) => obj.id);
// eslint-disable-next-line no-param-reassign
newData = newData.map((newObj) => {
if (!oldDataIds.includes(newObj.id)) {
// Set isNew = true if we have new child on refetch
// eslint-disable-next-line no-param-reassign
newObj.isNew = true;
}
return newObj;
});
}
return replaceEqualDeep(oldData, newData);
},
})
);
/**
* If you work with `useContentFromSearchIndex`, you can use this
* function to get the query key, usually to invalidate the query.
*/
const getSearchQueryKeyFromContent = (
contentIds: string[],
client?: MeiliSearch,
indexName?: string,
) => (
buildSearchQueryKey({
client,
indexName,
extraFilter: [`usage_key IN ["${contentIds.join('","')}"]`],
searchKeywords: '',
blockTypesFilter: [],
problemTypesFilter: [],
publishStatusFilter: [],
tagsFilter: [],
sort: [],
})
);
/**
* Use this mutation to add items to a container
*/
export const useAddItemsToContainer = (containerId?: string) => {
const queryClient = useQueryClient();
const { client, indexName } = useContentSearchConnection();
return useMutation({
mutationFn: async (itemIds: string[]) => {
// istanbul ignore if: this should never happen
if (!containerId) {
return undefined;
}
return api.addComponentsToContainer(containerId, itemIds);
},
onSettled: (_data, _error, variables) => {
// istanbul ignore if: this should never happen
if (!containerId) {
return;
}
// NOTE: We invalidate the library query here because we need to update the library's
// container list.
const libraryId = getLibraryId(containerId);
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerChildren(containerId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
// Invalidate all hierarchies to update grandparents and grandchildren
// It would be complex to bring the entire hierarchy and only update the items within that hierarchy.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerHierarchy(undefined) });
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentHierarchy(undefined) });
// Invalidate the container to update its publish status
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) });
const containerType = getBlockType(containerId);
if (containerType === 'section') {
// We invalidate the search query of the each itemId if the container is a section.
// This because the subsection page calls this query individually.
variables.forEach((itemId) => {
queryClient.invalidateQueries({
queryKey: getSearchQueryKeyFromContent([itemId], client, indexName),
});
});
}
},
});
};
/**
* Use this mutation to update collections related a container in a library
*/
export const useUpdateContainerCollections = (containerId: string) => {
const queryClient = useQueryClient();
const libraryId = getLibraryId(containerId);
return useMutation({
mutationFn: async (collectionKeys: string[]) => api.updateContainerCollections(containerId, collectionKeys),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Update container children
*/
export const useUpdateContainerChildren = (containerId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (usageKeys: string[]) => {
if (!containerId) {
return undefined;
}
return api.updateLibraryContainerChildren(containerId, usageKeys);
},
onSettled: () => {
if (!containerId) {
return;
}
// NOTE: We invalidate the library query here because we need to update the library's
// container list.
const libraryId = getLibraryId(containerId);
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) });
},
});
};
/**
* Remove components from container
*/
export const useRemoveContainerChildren = (containerId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (itemIds: string[]) => {
if (!containerId) {
return undefined;
}
return api.removeLibraryContainerChildren(containerId, itemIds);
},
onSettled: () => {
if (!containerId) {
return;
}
// NOTE: We invalidate the library query here because we need to update the container
// count in the library
const libraryId = getLibraryId(containerId);
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.container(containerId) });
// Invalidate all hierarchies to update grandparents and grandchildren
// It would be complex to bring the entire hierarchy and only update the items within that hierarchy.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerHierarchy(undefined) });
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentHierarchy(undefined) });
},
});
};
/**
* Use this mutation to publish changes to a container and any children within it
*/
export const usePublishContainer = (containerId: string) => {
const queryClient = useQueryClient();
const libraryId = getLibraryId(containerId);
return useMutation({
mutationFn: () => api.publishContainer(containerId),
onSettled: () => {
// Invalidate all content-related metadata and search results for the whole library.
// The child components/xblocks could and even the container itself could appear in many different collections
// or other containers, so it's best to just invalidate everything.
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibraryContent(libraryId) });
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.containerHierarchy(containerId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
// For XBlocks, the only thing we need to invalidate is the metadata which includes "has unpublished changes"
queryClient.invalidateQueries({ predicate: xblockQueryKeys.allComponentMetadata });
},
});
};
/**
* Use this mutations to get a list of objects from the search index
*/
export const useContentFromSearchIndex = (contentIds: string[]) => {
const { client, indexName } = useContentSearchConnection();
const extraFilter = [`usage_key IN ["${contentIds.join('","')}"]`];
// NOTE: assuming that all contentIds are part of a single libraryId as we don't have a usecase
// of passing multiple contentIds from different libraries.
if (contentIds.length > 0) {
try {
const libraryId = getLibraryId(contentIds?.[0]);
extraFilter.push(`context_key = "${libraryId}"`);
} catch {
// Ignore as the contentIds could be part of course instead of a library.
}
}
return useContentSearchResults({
client,
indexName,
searchKeywords: '',
extraFilter,
limit: contentIds.length,
enabled: !!contentIds.length,
skipBlockTypeFetch: true,
});
};
/**
* Returns the course imports which had this library as destination.
*/
export const useCourseImports = (libraryId: string) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.courseImports(libraryId),
queryFn: () => api.getCourseImports(libraryId),
})
);
/**
* Returns the migration info of a given source list
*/
export const useMigrationInfo = (sourcesKeys: string[], enabled: boolean = true) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.migrationInfo(sourcesKeys),
queryFn: enabled ? () => api.getMigrationInfo(sourcesKeys) : skipToken,
})
);