mirror of
https://github.com/strapi/strapi.git
synced 2025-09-25 16:29:34 +00:00
Asset selection in ML CM (#11371)
This commit is contained in:
parent
28cb34cfc7
commit
c581e4406a
@ -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 }];
|
||||
};
|
@ -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';
|
||||
|
@ -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']),
|
||||
};
|
||||
|
@ -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=""
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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} />}
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user