Asset selection in ML CM (#11371)

This commit is contained in:
Marvin Frachet 2021-10-27 11:23:46 +02:00 committed by GitHub
parent 28cb34cfc7
commit c581e4406a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 176 additions and 76 deletions

View File

@ -0,0 +1,30 @@
import { useState } from 'react';
export const useSelectionState = (key, initialValue) => {
const [selections, setSelections] = useState(initialValue);
const selectOne = selection => {
const index = selections.findIndex(
currentSelection => currentSelection[key] === selection[key]
);
if (index > -1) {
setSelections(prevSelected => [
...prevSelected.slice(0, index),
...prevSelected.slice(index + 1),
]);
} else {
setSelections(prevSelected => [...prevSelected, selection]);
}
};
const selectAll = nextSelections => {
if (selections.length > 0) {
setSelections(initialValue);
} else {
setSelections(nextSelections);
}
};
return [selections, { selectOne, selectAll }];
};

View File

@ -103,7 +103,9 @@ export { default as PopUpWarningModal } from './old/components/PopUpWarning/Styl
// Contexts
export { default as AppInfosContext } from './contexts/AppInfosContext';
export { default as AutoReloadOverlayBockerContext } from './contexts/AutoReloadOverlayBockerContext';
export {
default as AutoReloadOverlayBockerContext,
} from './contexts/AutoReloadOverlayBockerContext';
export { default as NotificationsContext } from './contexts/NotificationsContext';
export { default as OverlayBlockerContext } from './contexts/OverlayBlockerContext';
@ -118,6 +120,7 @@ export { default as useLibrary } from './hooks/useLibrary';
export { default as useNotification } from './hooks/useNotification';
export { default as useStrapiApp } from './hooks/useStrapiApp';
export { default as useTracking } from './hooks/useTracking';
export { useSelectionState } from './hooks/useSelectionState';
export { default as useQueryParams } from './hooks/useQueryParams';
export { default as useOverlayBlocker } from './hooks/useOverlayBlocker';
@ -192,15 +195,21 @@ export { default as SortIcon } from './icons/SortIcon';
export { default as RemoveRoundedButton } from './icons/RemoveRoundedButton';
// content-manager
export { default as ContentManagerEditViewDataManagerContext } from './content-manager/contexts/ContentManagerEditViewDataManagerContext';
export { default as useCMEditViewDataManager } from './content-manager/hooks/useCMEditViewDataManager';
export {
default as ContentManagerEditViewDataManagerContext,
} from './content-manager/contexts/ContentManagerEditViewDataManagerContext';
export {
default as useCMEditViewDataManager,
} from './content-manager/hooks/useCMEditViewDataManager';
export { getType };
export { getOtherInfos };
// Utils
export { default as auth } from './utils/auth';
export { default as hasPermissions } from './utils/hasPermissions';
export { default as prefixFileUrlWithBackendUrl } from './utils/prefixFileUrlWithBackendUrl/prefixFileUrlWithBackendUrl';
export {
default as prefixFileUrlWithBackendUrl,
} from './utils/prefixFileUrlWithBackendUrl/prefixFileUrlWithBackendUrl';
export { default as prefixPluginTranslations } from './utils/prefixPluginTranslations';
export { default as pxToRem } from './utils/pxToRem';
export { default as to } from './utils/await-to-js';
@ -209,6 +218,8 @@ export { default as customEllipsis } from './utils/customEllipsis';
export { default as translatedErrors } from './utils/translatedErrors';
export { default as formatComponentData } from './content-manager/utils/formatComponentData';
export { findMatchingPermissions } from './utils/hasPermissions';
export { default as contentManagementUtilRemoveFieldsFromData } from './content-manager/utils/contentManagementUtilRemoveFieldsFromData';
export {
default as contentManagementUtilRemoveFieldsFromData,
} from './content-manager/utils/contentManagementUtilRemoveFieldsFromData';
export { default as getFileExtension } from './utils/getFileExtension/getFileExtension';
export * from './utils/stopPropagation';

View File

@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { GridLayout } from '@strapi/parts/Layout';
import styled from 'styled-components';
import { Box } from '@strapi/parts/Box';
import { KeyboardNavigable } from '@strapi/parts/KeyboardNavigable';
import { prefixFileUrlWithBackendUrl, getFileExtension } from '@strapi/helper-plugin';
import { ImageAssetCard } from '../AssetCard/ImageAssetCard';
@ -8,12 +9,23 @@ import { VideoAssetCard } from '../AssetCard/VideoAssetCard';
import { DocAssetCard } from '../AssetCard/DocAssetCard';
import { AssetType } from '../../constants';
export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }) => {
const GridColSize = {
S: 180,
M: 250,
};
const GridLayout = styled(Box)`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(${({ size }) => `${GridColSize[size]}px`}, 1fr));
grid-gap: ${({ theme }) => theme.spaces[4]};
`;
export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets, size }) => {
return (
<KeyboardNavigable tagName="article">
<GridLayout>
<GridLayout size={size}>
{assets.map(asset => {
const isSelected = selectedAssets.indexOf(asset.id) > -1;
const isSelected = selectedAssets.indexOf(asset) > -1;
if (asset.mime.includes(AssetType.Video)) {
return (
@ -27,6 +39,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
size={size}
/>
);
}
@ -45,6 +58,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
size={size}
/>
);
}
@ -58,6 +72,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)}
selected={isSelected}
size={size}
/>
);
})}
@ -74,9 +89,14 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
);
};
AssetList.defaultProps = {
size: 'M',
};
AssetList.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onEditAsset: PropTypes.func.isRequired,
onSelectAsset: PropTypes.func.isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
size: PropTypes.oneOf(['S', 'M']),
};

View File

@ -142,12 +142,6 @@ describe('MediaLibrary / AssetList', () => {
width: 1px;
}
.c0 {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
grid-gap: 16px;
}
.c7 {
display: -webkit-box;
display: -webkit-flex;
@ -512,6 +506,12 @@ describe('MediaLibrary / AssetList', () => {
font-size: 3rem;
}
.c0 {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
grid-gap: 16px;
}
<div>
<div
class=""

View File

@ -1,15 +1,53 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Flex } from '@strapi/parts/Flex';
import { Stack } from '@strapi/parts/Stack';
import { BaseCheckbox } from '@strapi/parts/BaseCheckbox';
import { AssetList } from '../../../AssetList';
import getTrad from '../../../../utils/getTrad';
export const BrowseStep = ({
assets,
onEditAsset,
onSelectAsset,
onSelectAllAsset,
selectedAssets,
}) => {
const { formatMessage } = useIntl();
export const BrowseStep = ({ assets, onEditAsset, onSelectAsset, selectedAssets }) => {
return (
<AssetList
assets={assets}
onSelectAsset={onSelectAsset}
selectedAssets={selectedAssets}
onEditAsset={onEditAsset}
/>
<Stack size={4}>
{onSelectAllAsset && (
<Flex>
<Flex
paddingLeft={2}
paddingRight={2}
background="neutral0"
hasRadius
borderColor="neutral200"
height={`${32 / 16}rem`}
>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
value={assets?.length > 0 && selectedAssets.length === assets?.length}
onChange={onSelectAllAsset}
/>
</Flex>
</Flex>
)}
<AssetList
size="S"
assets={assets}
onSelectAsset={onSelectAsset}
selectedAssets={selectedAssets}
onEditAsset={onEditAsset}
/>
</Stack>
);
};
@ -17,5 +55,6 @@ BrowseStep.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onEditAsset: PropTypes.func.isRequired,
onSelectAsset: PropTypes.func.isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.number).isRequired,
onSelectAllAsset: PropTypes.func.isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};

View File

@ -1,30 +1,47 @@
import React from 'react';
import { useIntl } from 'react-intl';
import PropTypes from 'prop-types';
import { Stack } from '@strapi/parts/Stack';
import { Text } from '@strapi/parts/Text';
import { AssetList } from '../../../AssetList';
import getTrad from '../../../../utils/getTrad';
export const SelectedStep = () => {
export const SelectedStep = ({ selectedAssets, onSelectAsset }) => {
const { formatMessage } = useIntl();
return (
<Stack size={0}>
<Text small bold textColor="neutral800">
{formatMessage(
{
id: getTrad('list.assets.selected'),
defaultMessage:
'{number, plural, =0 {No asset} one {1 asset} other {# assets}} selected',
},
{ number: 10 }
)}
</Text>
<Text small textColor="neutral600">
{formatMessage({
id: getTrad('modal.upload-list.sub-header-subtitle'),
defaultMessage: 'Manage the assets before adding them to the Media Library',
})}
</Text>
<Stack size={4}>
<Stack size={0}>
<Text small bold textColor="neutral800">
{formatMessage(
{
id: getTrad('list.assets.selected'),
defaultMessage:
'{number, plural, =0 {No asset} one {1 asset} other {# assets}} selected',
},
{ number: selectedAssets.length }
)}
</Text>
<Text small textColor="neutral600">
{formatMessage({
id: getTrad('modal.upload-list.sub-header-subtitle'),
defaultMessage: 'Manage the assets before adding them to the Media Library',
})}
</Text>
</Stack>
<AssetList
size="S"
assets={selectedAssets}
onSelectAsset={onSelectAsset}
selectedAssets={selectedAssets}
onEditAsset={() => {}}
/>
</Stack>
);
};
SelectedStep.propTypes = {
onSelectAsset: PropTypes.func.isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
};

View File

@ -13,6 +13,7 @@ import {
NoPermissions,
NoMedia,
AnErrorOccurred,
useSelectionState,
} from '@strapi/helper-plugin';
import AddIcon from '@strapi/icons/AddIcon';
import getTrad from '../../../utils/getTrad';
@ -25,8 +26,9 @@ import { useAssets } from '../../../hooks/useAssets';
// eslint-disable-next-line no-unused-vars
export const AssetDialog = ({ onClose, multiple }) => {
const { formatMessage } = useIntl();
const { canRead, canCreate, isLoading: isLoadingPermissions } = useMediaLibraryPermissions();
const [selectedAssets, { selectOne, selectAll }] = useSelectionState('id', []);
const { canRead, canCreate, isLoading: isLoadingPermissions } = useMediaLibraryPermissions();
const { data, isLoading, error } = useAssets({
skipWhen: !canRead,
});
@ -97,7 +99,7 @@ export const AssetDialog = ({ onClose, multiple }) => {
id: getTrad('modal.header.select-files'),
defaultMessage: 'Selected files',
})}
<Badge marginLeft={2}>6</Badge>
<Badge marginLeft={2}>{selectedAssets.length}</Badge>
</Tab>
</Tabs>
@ -114,15 +116,16 @@ export const AssetDialog = ({ onClose, multiple }) => {
<ModalBody>
<BrowseStep
assets={assets}
onSelectAsset={() => {}}
selectedAssets={[]}
onSelectAsset={selectOne}
selectedAssets={selectedAssets}
onSelectAllAsset={() => selectAll(assets)}
onEditAsset={() => {}}
/>
</ModalBody>
</TabPanel>
<TabPanel>
<ModalBody>
<SelectedStep />
<SelectedStep selectedAssets={selectedAssets} onSelectAsset={selectOne} />
</ModalBody>
</TabPanel>
</TabPanels>

View File

@ -8,6 +8,7 @@ import {
NoMedia,
AnErrorOccurred,
Search,
useSelectionState,
} from '@strapi/helper-plugin';
import { Layout, HeaderLayout, ContentLayout, ActionLayout } from '@strapi/parts/Layout';
import { Main } from '@strapi/parts/Main';
@ -49,7 +50,7 @@ export const MediaLibrary = () => {
const [showUploadAssetDialog, setShowUploadAssetDialog] = useState(false);
const [assetToEdit, setAssetToEdit] = useState(undefined);
const [selected, setSelected] = useState([]);
const [selected, { selectOne, selectAll }] = useSelectionState('id', []);
const toggleUploadAssetDialog = () => setShowUploadAssetDialog(prev => !prev);
useFocusWhenNavigate();
@ -58,27 +59,6 @@ export const MediaLibrary = () => {
const assets = data?.results;
const assetCount = data?.pagination?.total || 0;
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}>
@ -129,7 +109,7 @@ export const MediaLibrary = () => {
defaultMessage: 'Select all assets',
})}
value={assets?.length > 0 && selected.length === assets?.length}
onChange={selectAllAssets}
onChange={() => selectAll(assets)}
/>
</BoxWithHeight>
)}
@ -149,7 +129,7 @@ export const MediaLibrary = () => {
<ContentLayout>
{selected.length > 0 && (
<BulkDeleteButton assetIds={selected} onSuccess={() => setSelected([])} />
<BulkDeleteButton selectedAssets={selected} onSuccess={selectAll} />
)}
{loading && <LoadingIndicatorPage />}
@ -191,7 +171,7 @@ export const MediaLibrary = () => {
<AssetList
assets={assets}
onEditAsset={setAssetToEdit}
onSelectAsset={selectAsset}
onSelectAsset={selectOne}
selectedAssets={selected}
/>
{data?.pagination && <PaginationFooter pagination={data.pagination} />}

View File

@ -9,14 +9,14 @@ import { ConfirmDialog } from '@strapi/helper-plugin';
import { useBulkRemoveAsset } from '../../../hooks/useBulkRemoveAsset';
import getTrad from '../../../utils/getTrad';
export const BulkDeleteButton = ({ assetIds, onSuccess }) => {
export const BulkDeleteButton = ({ selectedAssets, onSuccess }) => {
const { formatMessage } = useIntl();
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { isLoading, removeAssets } = useBulkRemoveAsset();
const handleConfirmRemove = async () => {
await removeAssets(assetIds);
await removeAssets(selectedAssets.map(({ id }) => id));
onSuccess();
};
@ -31,7 +31,7 @@ export const BulkDeleteButton = ({ assetIds, onSuccess }) => {
'{number, plural, =0 {No asset} one {1 asset} other {# assets}} selected',
},
{
number: assetIds.length,
number: selectedAssets.length,
}
)}
</Subtitle>
@ -56,6 +56,6 @@ export const BulkDeleteButton = ({ assetIds, onSuccess }) => {
};
BulkDeleteButton.propTypes = {
assetIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onSuccess: PropTypes.func.isRequired,
};