Bulk delete

This commit is contained in:
mfrachet 2021-10-18 15:52:46 +02:00
parent 698ece4396
commit c879a50e95
7 changed files with 339 additions and 98 deletions

View File

@ -0,0 +1,37 @@
import { useMutation, useQueryClient } from 'react-query';
import { useNotification } from '@strapi/helper-plugin';
import { removeAssetRequest } from '../utils/removeAssetQuery';
const bulkRemoveQuery = assetIds => {
const promises = assetIds.map(assetId => removeAssetRequest(assetId));
return Promise.all(promises);
};
export const useBulkRemoveAsset = () => {
const toggleNotification = useNotification();
const queryClient = useQueryClient();
const mutation = useMutation(bulkRemoveQuery, {
onSuccess: () => {
// Coupled with the cache of useAssets
queryClient.refetchQueries(['assets'], { active: true });
queryClient.refetchQueries(['asset-count'], { active: true });
toggleNotification({
type: 'success',
message: {
id: 'modal.remove.success-label',
defaultMessage: 'The asset has been successfully removed.',
},
});
},
onError: error => {
toggleNotification({ type: 'warning', message: error.message });
},
});
const removeAssets = assetIds => mutation.mutateAsync(assetIds);
return { ...mutation, removeAssets };
};

View File

@ -1,16 +1,6 @@
import { useMutation, useQueryClient } from 'react-query';
import { useNotification } from '@strapi/helper-plugin';
import { axiosInstance } from '../utils';
const removeAssetRequest = assetId => {
const endpoint = `/upload/files/${assetId}`;
return axiosInstance({
method: 'delete',
url: endpoint,
headers: {},
});
};
import { removeAssetRequest } from '../utils/removeAssetQuery';
export const useRemoveAsset = onSuccess => {
const toggleNotification = useNotification();

View File

@ -25,6 +25,7 @@ import { SortPicker } from './components/SortPicker';
import { PaginationFooter } from '../../components/PaginationFooter';
import { useMediaLibraryPermissions } from '../../hooks/useMediaLibraryPermissions';
import { useAssetCount } from '../../hooks/useAssetCount';
import { BulkDeleteButton } from './components/BulkDeleteButton';
const BoxWithHeight = styled(Box)`
height: ${32 / 16}rem;
@ -51,14 +52,35 @@ export const MediaLibrary = () => {
const [showUploadAssetDialog, setShowUploadAssetDialog] = useState(false);
const [assetToEdit, setAssetToEdit] = useState(undefined);
const [selected, setSelected] = useState([]);
const toggleUploadAssetDialog = () => setShowUploadAssetDialog(prev => !prev);
useFocusWhenNavigate();
const loading = isLoadingPermissions || isLoading || isLoadingCount;
const assets = data?.results;
const selectAllAssets = () => {
if (selected.length > 0) {
setSelected([]);
} else {
setSelected((assets || []).map(({ id }) => id));
}
};
const selectAsset = asset => {
const index = selected.indexOf(asset.id);
if (index > -1) {
setSelected(prevSelected => [
...prevSelected.slice(0, index),
...prevSelected.slice(index + 1),
]);
} else {
setSelected(prevSelected => [...prevSelected, asset.id]);
}
};
return (
<Layout>
<Main aria-busy={loading}>
@ -107,6 +129,8 @@ export const MediaLibrary = () => {
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
value={selected.length === assets?.length}
onChange={selectAllAssets}
/>
</BoxWithHeight>
<SortPicker />
@ -124,6 +148,10 @@ export const MediaLibrary = () => {
/>
<ContentLayout>
{selected.length > 0 && (
<BulkDeleteButton assetIds={selected} onSuccess={() => setSelected([])} />
)}
{loading && <LoadingIndicatorPage />}
{error && <AnErrorOccurred />}
{!canRead && <NoPermissions />}
@ -160,7 +188,12 @@ export const MediaLibrary = () => {
)}
{canRead && assets && assets.length > 0 && (
<>
<ListView assets={assets} onEditAsset={setAssetToEdit} />
<ListView
assets={assets}
onEditAsset={setAssetToEdit}
onSelectAsset={selectAsset}
selectedAssets={selected}
/>
{data?.pagination && <PaginationFooter pagination={data.pagination} />}
</>
)}

View File

@ -0,0 +1,60 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Subtitle } from '@strapi/parts/Text';
import { Button } from '@strapi/parts/Button';
import { Stack } from '@strapi/parts/Stack';
import DeleteIcon from '@strapi/icons/DeleteIcon';
import { ConfirmDialog } from '@strapi/helper-plugin';
import { useBulkRemoveAsset } from '../../../hooks/useBulkRemoveAsset';
import getTrad from '../../../utils/getTrad';
export const BulkDeleteButton = ({ assetIds, onSuccess }) => {
const { formatMessage } = useIntl();
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { isLoading, removeAssets } = useBulkRemoveAsset();
const handleConfirmRemove = async () => {
await removeAssets(assetIds);
onSuccess();
};
return (
<>
<Stack horizontal size={2} paddingBottom={5}>
<Subtitle textColor="neutral600">
{formatMessage(
{
id: getTrad('list.assets.selected.plural'),
defaultMessage: '1 asset selected',
},
{
number: assetIds.length,
}
)}
</Subtitle>
<Button
variant="danger-light"
size="S"
startIcon={<DeleteIcon />}
onClick={() => setShowConfirmDialog(true)}
>
{formatMessage({ id: getTrad('control-card.delete'), defaultMessage: 'Delete' })}
</Button>
</Stack>
<ConfirmDialog
isConfirmButtonLoading={isLoading}
isOpen={showConfirmDialog}
onToggleDialog={() => setShowConfirmDialog(false)}
onConfirm={handleConfirmRemove}
/>
</>
);
};
BulkDeleteButton.propTypes = {
assetIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onSuccess: PropTypes.func.isRequired,
};

View File

@ -8,11 +8,13 @@ import { VideoAssetCard } from '../../../components/AssetCard/VideoAssetCard';
import { DocAssetCard } from '../../../components/AssetCard/DocAssetCard';
import { AssetType } from '../../../constants';
export const ListView = ({ assets, onEditAsset }) => {
export const ListView = ({ assets, onEditAsset, onSelectAsset, selectedAssets }) => {
return (
<KeyboardNavigable tagName="article">
<GridLayout>
{assets.map(asset => {
const isSelected = (selectedAssets || []).indexOf(asset.id) > -1;
if (asset.mime.includes(AssetType.Video)) {
return (
<VideoAssetCard
@ -23,6 +25,8 @@ export const ListView = ({ assets, onEditAsset }) => {
url={prefixFileUrlWithBackendUrl(asset.url)}
mime={asset.mime}
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
/>
);
}
@ -38,6 +42,8 @@ export const ListView = ({ assets, onEditAsset }) => {
width={asset.width}
thumbnail={prefixFileUrlWithBackendUrl(asset?.formats?.thumbnail?.url || asset.url)}
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
/>
);
}
@ -49,6 +55,8 @@ export const ListView = ({ assets, onEditAsset }) => {
name={asset.name}
extension={getFileExtension(asset.ext)}
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
/>
);
})}
@ -60,4 +68,6 @@ export const ListView = ({ assets, onEditAsset }) => {
ListView.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onEditAsset: PropTypes.func.isRequired,
onSelectAsset: PropTypes.func.isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.number).isRequired,
};

View File

@ -130,7 +130,7 @@ describe('MediaLibrary / ListView', () => {
);
expect(container).toMatchInlineSnapshot(`
.c30 {
.c32 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
@ -148,7 +148,7 @@ describe('MediaLibrary / ListView', () => {
grid-gap: 16px;
}
.c5 {
.c7 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -162,21 +162,21 @@ describe('MediaLibrary / ListView', () => {
outline: none;
}
.c5 svg {
.c7 svg {
height: 12px;
width: 12px;
}
.c5 svg > g,
.c5 svg path {
.c7 svg > g,
.c7 svg path {
fill: #ffffff;
}
.c5[aria-disabled='true'] {
.c7[aria-disabled='true'] {
pointer-events: none;
}
.c5:after {
.c7:after {
-webkit-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.2s;
@ -191,11 +191,11 @@ describe('MediaLibrary / ListView', () => {
border: 2px solid transparent;
}
.c5:focus-visible {
.c7:focus-visible {
outline: none;
}
.c5:focus-visible:after {
.c7:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
@ -206,7 +206,7 @@ describe('MediaLibrary / ListView', () => {
border: 2px solid #4945ff;
}
.c6 {
.c8 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -223,26 +223,26 @@ describe('MediaLibrary / ListView', () => {
width: 2rem;
}
.c6 svg > g,
.c6 svg path {
.c8 svg > g,
.c8 svg path {
fill: #8e8ea9;
}
.c6:hover svg > g,
.c6:hover svg path {
.c8:hover svg > g,
.c8:hover svg path {
fill: #666687;
}
.c6:active svg > g,
.c6:active svg path {
.c8:active svg > g,
.c8:active svg path {
fill: #a5a5ba;
}
.c6[aria-disabled='true'] {
.c8[aria-disabled='true'] {
background-color: #eaeaef;
}
.c6[aria-disabled='true'] svg path {
.c8[aria-disabled='true'] svg path {
fill: #666687;
}
@ -252,21 +252,21 @@ describe('MediaLibrary / ListView', () => {
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
}
.c9 {
.c11 {
padding-top: 8px;
padding-right: 12px;
padding-bottom: 8px;
padding-left: 12px;
}
.c16 {
.c18 {
background: #f6f6f9;
color: #666687;
padding: 4px;
border-radius: 4px;
}
.c24 {
.c26 {
background: #32324d;
color: #ffffff;
padding: 4px;
@ -274,6 +274,12 @@ describe('MediaLibrary / ListView', () => {
}
.c4 {
position: absolute;
top: 12px;
left: 12px;
}
.c6 {
position: absolute;
top: 12px;
right: 12px;
@ -297,7 +303,7 @@ describe('MediaLibrary / ListView', () => {
align-items: center;
}
.c10 {
.c12 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -311,7 +317,7 @@ describe('MediaLibrary / ListView', () => {
align-items: flex-start;
}
.c22 {
.c24 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -325,14 +331,14 @@ describe('MediaLibrary / ListView', () => {
align-items: center;
}
.c8 {
.c10 {
margin: 0;
padding: 0;
max-height: 100%;
max-width: 100%;
}
.c7 {
.c9 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
@ -346,62 +352,129 @@ describe('MediaLibrary / ListView', () => {
background: repeating-conic-gradient(#f6f6f9 0% 25%,transparent 0% 50%) 50% / 20px 20px;
}
.c12 {
.c14 {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.33;
color: #32324d;
}
.c13 {
.c15 {
font-weight: 400;
font-size: 0.75rem;
line-height: 1.33;
color: #666687;
}
.c19 {
.c21 {
font-weight: 400;
font-size: 0.875rem;
line-height: 1.43;
color: #32324d;
}
.c26 {
.c28 {
font-weight: 400;
font-size: 0.75rem;
line-height: 1.33;
color: #ffffff;
}
.c20 {
.c22 {
font-weight: 600;
line-height: 1.14;
}
.c21 {
.c23 {
font-weight: 600;
font-size: 0.6875rem;
line-height: 1.45;
text-transform: uppercase;
}
.c17 {
.c19 {
display: inline-block;
}
.c15 {
.c17 {
margin-left: auto;
-webkit-flex-shrink: 0;
-ms-flex-negative: 0;
flex-shrink: 0;
}
.c18 {
.c20 {
margin-left: 4px;
}
.c11 {
.c5 {
margin: 0;
height: 18px;
min-width: 18px;
border-radius: 4px;
border: 1px solid #c0c0cf;
-webkit-appearance: none;
background-color: #ffffff;
}
.c5:checked {
background-color: #4945ff;
border: 1px solid #4945ff;
}
.c5:checked:after {
content: '';
display: block;
position: relative;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSJ3aGl0ZSIKICAvPgo8L3N2Zz4=) no-repeat no-repeat center center;
width: 10px;
height: 10px;
left: 50%;
top: 50%;
-webkit-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.c5:checked:disabled:after {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAiIGhlaWdodD0iOCIgdmlld0JveD0iMCAwIDEwIDgiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGgKICAgIGQ9Ik04LjU1MzIzIDAuMzk2OTczQzguNjMxMzUgMC4zMTYzNTUgOC43NjA1MSAwLjMxNTgxMSA4LjgzOTMxIDAuMzk1NzY4TDkuODYyNTYgMS40MzQwN0M5LjkzODkzIDEuNTExNTcgOS45MzkzNSAxLjYzNTkgOS44NjM0OSAxLjcxMzlMNC4wNjQwMSA3LjY3NzI0QzMuOTg1OSA3Ljc1NzU1IDMuODU3MDcgNy43NTgwNSAzLjc3ODM0IDcuNjc4MzRMMC4xMzg2NiAzLjk5MzMzQzAuMDYxNzc5OCAzLjkxNTQ5IDAuMDYxNzEwMiAzLjc5MDMyIDAuMTM4NTA0IDMuNzEyNEwxLjE2MjEzIDIuNjczNzJDMS4yNDAzOCAyLjU5NDMyIDEuMzY4NDMgMi41OTQyMiAxLjQ0NjggMi42NzM0OEwzLjkyMTc0IDUuMTc2NDdMOC41NTMyMyAwLjM5Njk3M1oiCiAgICBmaWxsPSIjOEU4RUE5IgogIC8+Cjwvc3ZnPg==) no-repeat no-repeat center center;
}
.c5:disabled {
background-color: #dcdce4;
border: 1px solid #c0c0cf;
}
.c5:indeterminate {
background-color: #4945ff;
border: 1px solid #4945ff;
}
.c5:indeterminate:after {
content: '';
display: block;
position: relative;
color: white;
height: 2px;
width: 10px;
background-color: #ffffff;
left: 50%;
top: 50%;
-webkit-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
.c5:indeterminate:disabled {
background-color: #dcdce4;
border: 1px solid #c0c0cf;
}
.c5:indeterminate:disabled:after {
background-color: #8e8ea9;
}
.c13 {
word-break: break-all;
}
@ -410,31 +483,31 @@ describe('MediaLibrary / ListView', () => {
border-bottom: 1px solid #eaeaef;
}
.c25 {
.c27 {
position: absolute;
bottom: 4px;
right: 4px;
}
.c14 {
.c16 {
text-transform: uppercase;
}
.c23 canvas {
.c25 canvas {
display: block;
max-width: 100%;
max-height: 10.25rem;
}
.c27 {
text-transform: uppercase;
}
.c29 {
text-transform: uppercase;
}
.c28 svg {
.c31 {
text-transform: uppercase;
}
.c30 svg {
font-size: 3rem;
}
@ -455,12 +528,21 @@ describe('MediaLibrary / ListView', () => {
>
<div
class="c4"
>
<input
aria-labelledby="card-1-title"
class="c5"
type="checkbox"
/>
</div>
<div
class="c6"
>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-1"
class="c5 c6"
class="c7 c8"
tabindex="0"
type="button"
>
@ -482,35 +564,35 @@ describe('MediaLibrary / ListView', () => {
</span>
</div>
<div
class="c7"
class="c9"
>
<img
aria-hidden="true"
class="c8"
class="c10"
src="http://localhost:1337/uploads/thumbnail_strapi_cover_1fabc982ce_5b43615ed5.png?width=1066&height=551"
/>
</div>
</div>
<div
class="c9"
>
<div
class="c10"
>
<div
class="c11"
>
<h2
<div
class="c12"
>
<div
class="c13"
>
<h2
class="c14"
id="card-1-title"
>
strapi-cover_1fabc982ce.png
</h2>
<div
class="c13"
class="c15"
>
<span
class="c14"
class="c16"
>
png
</span>
@ -518,13 +600,13 @@ describe('MediaLibrary / ListView', () => {
</div>
</div>
<div
class="c15"
class="c17"
>
<div
class="c16 c17 c18"
class="c18 c19 c20"
>
<span
class="c19 c20 c21"
class="c21 c22 c23"
>
Image
</span>
@ -543,12 +625,21 @@ describe('MediaLibrary / ListView', () => {
>
<div
class="c4"
>
<input
aria-labelledby="card-2-title"
class="c5"
type="checkbox"
/>
</div>
<div
class="c6"
>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-3"
class="c5 c6"
class="c7 c8"
tabindex="0"
type="button"
>
@ -570,13 +661,13 @@ describe('MediaLibrary / ListView', () => {
</span>
</div>
<div
class="c7"
class="c9"
>
<div
class="c22"
class="c24"
>
<div
class="c23"
class="c25"
>
<video
src="http://localhost:1337/uploads/mov_bbb_2f3907f7aa.mp4"
@ -590,48 +681,48 @@ describe('MediaLibrary / ListView', () => {
</div>
</div>
<time
class="c24 c25"
class="c26 c27"
>
<span
class="c26"
class="c28"
>
...
</span>
</time>
</div>
<div
class="c9"
>
<div
class="c10"
>
<div
class="c11"
>
<h2
<div
class="c12"
>
<div
class="c13"
>
<h2
class="c14"
id="card-2-title"
>
mov_bbb.mp4
</h2>
<div
class="c13"
class="c15"
>
<span
class="c27"
class="c29"
>
mp4
</span>
</div>
</div>
<div
class="c15"
class="c17"
>
<div
class="c16 c17 c18"
class="c18 c19 c20"
>
<span
class="c19 c20 c21"
class="c21 c22 c23"
>
Video
</span>
@ -650,12 +741,21 @@ describe('MediaLibrary / ListView', () => {
>
<div
class="c4"
>
<input
aria-labelledby="card-3-title"
class="c5"
type="checkbox"
/>
</div>
<div
class="c6"
>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-5"
class="c5 c6"
class="c7 c8"
tabindex="0"
type="button"
>
@ -677,13 +777,13 @@ describe('MediaLibrary / ListView', () => {
</span>
</div>
<div
class="c7"
class="c9"
>
<div
class="c22"
class="c24"
>
<span
class="c28"
class="c30"
>
<svg
aria-label="CARTE MARIAGE AVS - Printemps.pdf"
@ -702,39 +802,39 @@ describe('MediaLibrary / ListView', () => {
</div>
</div>
</div>
<div
class="c9"
>
<div
class="c10"
>
<div
class="c11"
>
<h2
<div
class="c12"
>
<div
class="c13"
>
<h2
class="c14"
id="card-3-title"
>
CARTE MARIAGE AVS - Printemps.pdf
</h2>
<div
class="c13"
class="c15"
>
<span
class="c29"
class="c31"
>
pdf
</span>
</div>
</div>
<div
class="c15"
class="c17"
>
<div
class="c16 c17 c18"
class="c18 c19 c20"
>
<span
class="c19 c20 c21"
class="c21 c22 c23"
>
Doc
</span>
@ -746,7 +846,7 @@ describe('MediaLibrary / ListView', () => {
</div>
</div>
<div
class="c30"
class="c32"
>
<p
aria-live="polite"

View File

@ -0,0 +1,11 @@
import axiosInstance from './axiosInstance';
export const removeAssetRequest = assetId => {
const endpoint = `/upload/files/${assetId}`;
return axiosInstance({
method: 'delete',
url: endpoint,
headers: {},
});
};