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 // Contexts
export { default as AppInfosContext } from './contexts/AppInfosContext'; 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 NotificationsContext } from './contexts/NotificationsContext';
export { default as OverlayBlockerContext } from './contexts/OverlayBlockerContext'; 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 useNotification } from './hooks/useNotification';
export { default as useStrapiApp } from './hooks/useStrapiApp'; export { default as useStrapiApp } from './hooks/useStrapiApp';
export { default as useTracking } from './hooks/useTracking'; export { default as useTracking } from './hooks/useTracking';
export { useSelectionState } from './hooks/useSelectionState';
export { default as useQueryParams } from './hooks/useQueryParams'; export { default as useQueryParams } from './hooks/useQueryParams';
export { default as useOverlayBlocker } from './hooks/useOverlayBlocker'; 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'; export { default as RemoveRoundedButton } from './icons/RemoveRoundedButton';
// content-manager // content-manager
export { default as ContentManagerEditViewDataManagerContext } from './content-manager/contexts/ContentManagerEditViewDataManagerContext'; export {
export { default as useCMEditViewDataManager } from './content-manager/hooks/useCMEditViewDataManager'; default as ContentManagerEditViewDataManagerContext,
} from './content-manager/contexts/ContentManagerEditViewDataManagerContext';
export {
default as useCMEditViewDataManager,
} from './content-manager/hooks/useCMEditViewDataManager';
export { getType }; export { getType };
export { getOtherInfos }; export { getOtherInfos };
// Utils // Utils
export { default as auth } from './utils/auth'; export { default as auth } from './utils/auth';
export { default as hasPermissions } from './utils/hasPermissions'; 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 prefixPluginTranslations } from './utils/prefixPluginTranslations';
export { default as pxToRem } from './utils/pxToRem'; export { default as pxToRem } from './utils/pxToRem';
export { default as to } from './utils/await-to-js'; 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 translatedErrors } from './utils/translatedErrors';
export { default as formatComponentData } from './content-manager/utils/formatComponentData'; export { default as formatComponentData } from './content-manager/utils/formatComponentData';
export { findMatchingPermissions } from './utils/hasPermissions'; 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 { default as getFileExtension } from './utils/getFileExtension/getFileExtension';
export * from './utils/stopPropagation'; export * from './utils/stopPropagation';

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { KeyboardNavigable } from '@strapi/parts/KeyboardNavigable';
import { prefixFileUrlWithBackendUrl, getFileExtension } from '@strapi/helper-plugin'; import { prefixFileUrlWithBackendUrl, getFileExtension } from '@strapi/helper-plugin';
import { ImageAssetCard } from '../AssetCard/ImageAssetCard'; import { ImageAssetCard } from '../AssetCard/ImageAssetCard';
@ -8,12 +9,23 @@ import { VideoAssetCard } from '../AssetCard/VideoAssetCard';
import { DocAssetCard } from '../AssetCard/DocAssetCard'; import { DocAssetCard } from '../AssetCard/DocAssetCard';
import { AssetType } from '../../constants'; 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 ( return (
<KeyboardNavigable tagName="article"> <KeyboardNavigable tagName="article">
<GridLayout> <GridLayout size={size}>
{assets.map(asset => { {assets.map(asset => {
const isSelected = selectedAssets.indexOf(asset.id) > -1; const isSelected = selectedAssets.indexOf(asset) > -1;
if (asset.mime.includes(AssetType.Video)) { if (asset.mime.includes(AssetType.Video)) {
return ( return (
@ -27,6 +39,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)} onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)} onSelect={() => onSelectAsset(asset)}
selected={isSelected} selected={isSelected}
size={size}
/> />
); );
} }
@ -45,6 +58,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)} onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)} onSelect={() => onSelectAsset(asset)}
selected={isSelected} selected={isSelected}
size={size}
/> />
); );
} }
@ -58,6 +72,7 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
onEdit={() => onEditAsset(asset)} onEdit={() => onEditAsset(asset)}
onSelect={() => onSelectAsset(asset)} onSelect={() => onSelectAsset(asset)}
selected={isSelected} selected={isSelected}
size={size}
/> />
); );
})} })}
@ -74,9 +89,14 @@ export const AssetList = ({ assets, onEditAsset, onSelectAsset, selectedAssets }
); );
}; };
AssetList.defaultProps = {
size: 'M',
};
AssetList.propTypes = { AssetList.propTypes = {
assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired, assets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
onEditAsset: PropTypes.func.isRequired, onEditAsset: PropTypes.func.isRequired,
onSelectAsset: 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; width: 1px;
} }
.c0 {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
grid-gap: 16px;
}
.c7 { .c7 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
@ -512,6 +506,12 @@ describe('MediaLibrary / AssetList', () => {
font-size: 3rem; font-size: 3rem;
} }
.c0 {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(250px,1fr));
grid-gap: 16px;
}
<div> <div>
<div <div
class="" class=""

View File

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

View File

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

View File

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

View File

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