From d010d5569fda5cb252e3c7de29f545309e18a433 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Wed, 23 Oct 2024 09:15:30 -0700 Subject: [PATCH] feat: support deleting assets --- .../ComponentAdvancedAssets.tsx | 31 +++++++++++++++++-- .../component-info/messages.ts | 10 ++++++ src/library-authoring/data/api.ts | 8 +++++ src/library-authoring/data/apiHooks.ts | 14 +++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/component-info/ComponentAdvancedAssets.tsx b/src/library-authoring/component-info/ComponentAdvancedAssets.tsx index 12fe9617c..72a9018ce 100644 --- a/src/library-authoring/component-info/ComponentAdvancedAssets.tsx +++ b/src/library-authoring/component-info/ComponentAdvancedAssets.tsx @@ -2,16 +2,18 @@ /* eslint-disable import/prefer-default-export */ import React from 'react'; import { + Button, Dropzone, } from '@openedx/paragon'; -import { Plus } from '@openedx/paragon/icons'; +import { Delete } from '@openedx/paragon/icons'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { FormattedMessage, FormattedNumber, useIntl } from '@edx/frontend-platform/i18n'; import { LoadingSpinner } from '../../generic/Loading'; +import DeleteModal from '../../generic/delete-modal/DeleteModal'; import { useLibraryContext } from '../common/context'; import { getXBlockAssetsApiUrl } from '../data/api'; -import { useInvalidateXBlockAssets, useXBlockAssets } from '../data/apiHooks'; +import { useDeleteXBlockAsset, useInvalidateXBlockAssets, useXBlockAssets } from '../data/apiHooks'; import messages from './messages'; export const ComponentAdvancedAssets: React.FC> = () => { @@ -24,9 +26,11 @@ export const ComponentAdvancedAssets: React.FC> = () => { throw new Error('sidebarComponentUsageKey is required to render ComponentAdvancedAssets'); } + // For listing assets: const { data: assets, isLoading: areAssetsLoading } = useXBlockAssets(usageKey); const refreshAssets = useInvalidateXBlockAssets(usageKey); + // For uploading assets: const handleProcessUpload = React.useCallback(async ({ fileData, requestConfig, handleError, }: { fileData: FormData, requestConfig: any, handleError: any }) => { @@ -34,7 +38,7 @@ export const ComponentAdvancedAssets: React.FC> = () => { const file = fileData.get('file') as File; uploadData.set('content', file); // Paragon calls this 'file' but our API needs it called 'content' // TODO: make the filename unique if is already exists in assets list, to avoid overwriting. - const uploadUrl = getXBlockAssetsApiUrl(usageKey) + encodeURI(file.name); + const uploadUrl = `${getXBlockAssetsApiUrl(usageKey)}static/${encodeURI(file.name)}`; const client = getAuthenticatedHttpClient(); try { await client.put(uploadUrl, uploadData, requestConfig); @@ -45,6 +49,14 @@ export const ComponentAdvancedAssets: React.FC> = () => { refreshAssets(); }, [usageKey]); + // For deleting assets: + const deleter = useDeleteXBlockAsset(usageKey); + const [filePathToDelete, setConfirmDeleteAsset] = React.useState(''); + const deleteFile = React.useCallback(() => { + deleter.mutateAsync(filePathToDelete); // Don't wait for this before clearing the modal on the next line + setConfirmDeleteAsset(''); + }, [filePathToDelete, usageKey]); + return ( <>
    @@ -53,6 +65,9 @@ export const ComponentAdvancedAssets: React.FC> = () => {
  • {a.path}{' '} () +
  • )) }
@@ -65,6 +80,16 @@ export const ComponentAdvancedAssets: React.FC> = () => { /> ) : null } + + { setConfirmDeleteAsset(''); }} + variant="warning" + title={intl.formatMessage(messages.advancedDetailsAssetsDeleteFileTitle)} + description={`Are you sure you want to delete ${filePathToDelete}?`} + onDeleteSubmit={deleteFile} + btnState="default" + /> ); }; diff --git a/src/library-authoring/component-info/messages.ts b/src/library-authoring/component-info/messages.ts index 1a77b3c0f..1c02d867f 100644 --- a/src/library-authoring/component-info/messages.ts +++ b/src/library-authoring/component-info/messages.ts @@ -11,6 +11,16 @@ const messages = defineMessages({ defaultMessage: 'Assets (Files)', description: 'Heading for files attached to the component', }, + advancedDetailsAssetsDeleteFileTitle: { + id: 'course-authoring.library-authoring.component.advanced.assets.delete-file-title', + defaultMessage: 'Delete File', + description: 'Title for confirmation dialog when deleting a file', + }, + advancedDetailsAssetsDeleteButton: { + id: 'course-authoring.library-authoring.component.advanced.assets.delete-btn', + defaultMessage: 'Delete this file', + description: 'screen reader description of the delete button for each static asset file', + }, advancedDetailsOLX: { id: 'course-authoring.library-authoring.component.advanced.olx', defaultMessage: 'OLX Source', diff --git a/src/library-authoring/data/api.ts b/src/library-authoring/data/api.ts index c47f4bdc5..06f8c50f0 100644 --- a/src/library-authoring/data/api.ts +++ b/src/library-authoring/data/api.ts @@ -444,6 +444,14 @@ export async function getXBlockAssets(usageKey: string): Promise<{ path: string; return data.files; } +/** + * Delete a single asset file + */ +// istanbul ignore next +export async function deleteXBlockAsset(usageKey: string, path: string): Promise { + await getAuthenticatedHttpClient().delete(getXBlockAssetsApiUrl(usageKey) + encodeURIComponent(path)); +} + /** * Get the collection metadata. */ diff --git a/src/library-authoring/data/apiHooks.ts b/src/library-authoring/data/apiHooks.ts index b6e52124b..159fc8e6d 100644 --- a/src/library-authoring/data/apiHooks.ts +++ b/src/library-authoring/data/apiHooks.ts @@ -43,6 +43,7 @@ import { updateComponentCollections, removeComponentsFromCollection, publishXBlock, + deleteXBlockAsset, } from './api'; export const libraryQueryPredicate = (query: Query, libraryId: string): boolean => { @@ -406,6 +407,19 @@ export const useInvalidateXBlockAssets = (usageKey: string) => { }, [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) => deleteXBlockAsset(usageKey, path), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: xblockQueryKeys.xblockAssets(usageKey) }); + }, + }); +}; + /** * Get the metadata for a collection in a library */