Files
frontend-app-authoring/src/library-authoring/component-info/ComponentAdvancedAssets.tsx
Braden MacDonald 3d8d248599 feat: arbitrary asset upload/deletion for Library Components [FC-0062] (#1430)
Allow users to upload and delete assets associated with Content Library
components via the sidebar panel, under the "Advanced Details" section
of the "Details" tab. This is intended as a debug tool and power-user
feature, similar to the OLX editor provided there. It's also serving as
our interim image-upload solution, because it was easier to implement
than the full modal that integrates with TinyMCE.

---------

Co-authored-by: XnpioChV <xnpiochv@gmail.com>
2024-10-24 09:46:27 -04:00

101 lines
4.1 KiB
TypeScript

/* eslint-disable no-nested-ternary */
/* eslint-disable import/prefer-default-export */
import React from 'react';
import {
Button,
Dropzone,
} from '@openedx/paragon';
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 { useDeleteXBlockAsset, useInvalidateXBlockAssets, useXBlockAssets } from '../data/apiHooks';
import messages from './messages';
export const ComponentAdvancedAssets: React.FC<Record<never, never>> = () => {
const intl = useIntl();
const { readOnly, sidebarComponentInfo } = useLibraryContext();
const usageKey = sidebarComponentInfo?.id;
// istanbul ignore if: this should never happen in production
if (!usageKey) {
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 }) => {
const uploadData = new FormData();
const file = fileData.get('file') as File;
uploadData.set('content', file); // Paragon calls this 'file' but our API needs it called 'content'
// TODO: We may wish to warn the user (and prompt to confirm?) if they are
// about to overwite an existing file by uploading a file with the same
// name as an existing file. That is a workflow we want to support, but only
// if it's intentional.
// Note: we follow the convention that files meant to be seen/downloaded by
// learners should be prefixed with 'static/'
const uploadUrl = `${getXBlockAssetsApiUrl(usageKey)}static/${encodeURI(file.name)}`;
const client = getAuthenticatedHttpClient();
try {
await client.put(uploadUrl, uploadData, requestConfig);
} catch (error) {
handleError(error);
return;
}
refreshAssets();
}, [usageKey]);
// For deleting assets:
const deleter = useDeleteXBlockAsset(usageKey);
const [filePathToDelete, setConfirmDeleteAsset] = React.useState<string>('');
const deleteFile = React.useCallback(() => {
deleter.mutateAsync(filePathToDelete); // Don't wait for this before clearing the modal on the next line
setConfirmDeleteAsset('');
}, [filePathToDelete, usageKey]);
return (
<>
<ul>
{ areAssetsLoading ? <li><LoadingSpinner /></li> : null }
{ assets?.map(a => (
<li key={a.path}>
<a href={a.url}>{a.path}</a>{' '}
(<FormattedNumber value={a.size} notation="compact" unit="byte" unitDisplay="narrow" />)
<Button variant="link" size="sm" iconBefore={Delete} onClick={() => { setConfirmDeleteAsset(a.path); }} title={intl.formatMessage(messages.advancedDetailsAssetsDeleteButton)}>
<span className="sr-only"><FormattedMessage {...messages.advancedDetailsAssetsDeleteButton} /></span>
</Button>
</li>
)) }
</ul>
{ assets !== undefined && !readOnly // Wait until assets have loaded before displaying add button:
? (
<Dropzone
style={{ height: '200px' }}
onProcessUpload={handleProcessUpload}
onUploadProgress={() => {}}
/>
)
: null }
<DeleteModal
isOpen={filePathToDelete !== ''}
close={() => { setConfirmDeleteAsset(''); }}
variant="warning"
title={intl.formatMessage(messages.advancedDetailsAssetsDeleteFileTitle)}
description={`Are you sure you want to delete ${filePathToDelete}?`}
onDeleteSubmit={deleteFile}
btnState="default"
/>
</>
);
};