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.
984 lines
35 KiB
TypeScript
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,
|
|
})
|
|
);
|