Files
frontend-app-authoring/src/library-authoring/data/apiHooks.ts
2024-10-22 17:31:17 +00:00

508 lines
17 KiB
TypeScript

import { camelCaseObject } from '@edx/frontend-platform';
import {
useQuery,
useMutation,
useQueryClient,
type Query,
type QueryClient,
} from '@tanstack/react-query';
import { getLibraryId } from '../../generic/key-utils';
import {
type GetLibrariesV2CustomParams,
type ContentLibrary,
type XBlockFields,
type UpdateXBlockFieldsRequest,
getContentLibrary,
createLibraryBlock,
deleteLibraryBlock,
getContentLibraryV2List,
commitLibraryChanges,
revertLibraryChanges,
updateLibraryMetadata,
getLibraryTeam,
addLibraryTeamMember,
deleteLibraryTeamMember,
updateLibraryTeamMember,
libraryPasteClipboard,
getLibraryBlockMetadata,
getXBlockFields,
updateXBlockFields,
createCollection,
getXBlockOLX,
updateCollectionMetadata,
type UpdateCollectionComponentsRequest,
addComponentsToCollection,
type CreateLibraryCollectionDataRequest,
getCollectionMetadata,
deleteCollection,
restoreCollection,
setXBlockOLX,
getXBlockAssets,
updateComponentCollections,
removeComponentsFromCollection,
publishXBlock,
} from './api';
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],
contentLibraryList: (customParams?: GetLibrariesV2CustomParams) => [
...libraryAuthoringQueryKeys.all,
'list',
...(customParams ? [customParams] : []),
],
libraryTeam: (libraryId?: string) => [
...libraryAuthoringQueryKeys.all,
'list',
libraryId,
],
collection: (libraryId?: string, collectionId?: string) => [
...libraryAuthoringQueryKeys.all,
libraryId,
collectionId,
],
};
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) => [...xblockQueryKeys.xblock(usageKey), 'fields'],
/** 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'],
};
/**
* 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) });
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: () => getContentLibrary(libraryId!),
enabled: libraryId !== undefined,
})
);
/**
* Use this mutation to create a block in a library
*/
export const useCreateLibraryBlock = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: 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: deleteLibraryBlock,
onSettled: (_data, _error, variables) => {
const libraryId = getLibraryId(variables.usageKey);
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
invalidateComponentData(queryClient, libraryId, variables.usageKey);
},
});
};
export const useUpdateLibraryMetadata = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: updateLibraryMetadata,
onMutate: async (data) => {
const queryKey = libraryAuthoringQueryKeys.contentLibrary(data.id);
const previousLibraryData = queryClient.getQueriesData(queryKey)[0][1] as 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: GetLibrariesV2CustomParams) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.contentLibraryList(customParams),
queryFn: () => getContentLibraryV2List(customParams),
keepPreviousData: true,
})
);
export const useCommitLibraryChanges = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: commitLibraryChanges,
onSettled: (_data, _error, libraryId) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
},
});
};
export const useRevertLibraryChanges = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: revertLibraryChanges,
onSettled: (_data, _error, libraryId) => {
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(libraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/**
* Hook to fetch a content library's team members
*/
export const useLibraryTeam = (libraryId: string | undefined) => (
useQuery({
queryKey: libraryAuthoringQueryKeys.libraryTeam(libraryId),
queryFn: () => getLibraryTeam(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: 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: 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: updateLibraryTeamMember,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey });
},
});
};
export const useLibraryPasteClipboard = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: 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) => (
useQuery({
queryKey: xblockQueryKeys.componentMetadata(usageId),
queryFn: () => getLibraryBlockMetadata(usageId),
})
);
export const useXBlockFields = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockFields(usageKey),
queryFn: () => getXBlockFields(usageKey),
enabled: !!usageKey,
})
);
export const useUpdateXBlockFields = (usageKey: string) => {
const contentLibraryId = getLibraryId(usageKey);
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: UpdateXBlockFieldsRequest) => updateXBlockFields(usageKey, data),
onMutate: async (data) => {
const queryKey = xblockQueryKeys.xblockFields(usageKey);
const previousBlockData = queryClient.getQueriesData(queryKey)[0][1] as XBlockFields;
const formatedData = camelCaseObject(data);
const newBlockData = {
...previousBlockData,
...(formatedData.metadata?.displayName && { displayName: formatedData.metadata.displayName }),
metadata: {
...previousBlockData.metadata,
...formatedData.metadata,
},
};
queryClient.setQueryData(queryKey, newBlockData);
return { previousBlockData, newBlockData };
},
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: CreateLibraryCollectionDataRequest) => createCollection(libraryId, data),
onSettled: () => {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};
/** Get the OLX source of a library component */
export const useXBlockOLX = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockOLX(usageKey),
queryFn: () => getXBlockOLX(usageKey),
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) => setXBlockOLX(usageKey, newOLX),
onSuccess: (olxFromServer) => {
queryClient.setQueryData(xblockQueryKeys.xblockOLX(usageKey), olxFromServer);
// Reload the other data for this component:
invalidateComponentData(queryClient, contentLibraryId, usageKey);
// And the description and display name etc. may have changed, so refresh everything in the library too:
queryClient.invalidateQueries({ queryKey: libraryAuthoringQueryKeys.contentLibrary(contentLibraryId) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, contentLibraryId) });
},
});
};
/**
* Publish changes to a library component
*/
export const usePublishComponent = (usageKey: string) => {
const queryClient = useQueryClient();
const contentLibraryId = getLibraryId(usageKey);
return useMutation({
mutationFn: () => publishXBlock(usageKey),
onSettled: () => {
invalidateComponentData(queryClient, contentLibraryId, usageKey);
},
});
};
/** Get the list of assets (static files) attached to a library component */
export const useXBlockAssets = (usageKey: string) => (
useQuery({
queryKey: xblockQueryKeys.xblockAssets(usageKey),
queryFn: () => getXBlockAssets(usageKey),
enabled: !!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: () => 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();
return useMutation({
mutationFn: (data: UpdateCollectionComponentsRequest) => updateCollectionMetadata(libraryId, collectionId, data),
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: libraryAuthoringQueryKeys.collection(libraryId, collectionId) });
},
});
};
/**
* Use this mutation to add components to a collection in a library
*/
export const useAddComponentsToCollection = (libraryId?: string, collectionId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (usageKeys: string[]) => {
if (libraryId !== undefined && collectionId !== undefined) {
return addComponentsToCollection(libraryId, collectionId, usageKeys);
}
return undefined;
},
onSettled: () => {
if (libraryId !== undefined && collectionId !== undefined) {
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
}
},
});
};
/**
* Use this mutation to remove components from a collection in a library
*/
export const useRemoveComponentsFromCollection = (libraryId?: string, collectionId?: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (usageKeys: string[]) => {
if (libraryId !== undefined && collectionId !== undefined) {
return removeComponentsFromCollection(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 () => 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 () => 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 = (libraryId: string, usageKey: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (collectionKeys: string[]) => updateComponentCollections(usageKey, collectionKeys),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: xblockQueryKeys.componentMetadata(usageKey) });
queryClient.invalidateQueries({ predicate: (query) => libraryQueryPredicate(query, libraryId) });
},
});
};