diff --git a/packages/core/helper-plugin/lib/src/hooks/useSelectionState/index.js b/packages/core/helper-plugin/lib/src/hooks/useSelectionState/index.js new file mode 100644 index 0000000000..40094c7bb5 --- /dev/null +++ b/packages/core/helper-plugin/lib/src/hooks/useSelectionState/index.js @@ -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 }]; +}; diff --git a/packages/core/helper-plugin/lib/src/index.js b/packages/core/helper-plugin/lib/src/index.js index f4a534ca14..1ef3274600 100644 --- a/packages/core/helper-plugin/lib/src/index.js +++ b/packages/core/helper-plugin/lib/src/index.js @@ -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'; diff --git a/packages/core/upload/admin/src/components/AssetList/index.js b/packages/core/upload/admin/src/components/AssetList/index.js index 70838eeec0..18028fb475 100644 --- a/packages/core/upload/admin/src/components/AssetList/index.js +++ b/packages/core/upload/admin/src/components/AssetList/index.js @@ -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 ( - + {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']), }; diff --git a/packages/core/upload/admin/src/components/AssetList/tests/AssetList.test.js b/packages/core/upload/admin/src/components/AssetList/tests/AssetList.test.js index ec3f94d749..05d8e4170d 100644 --- a/packages/core/upload/admin/src/components/AssetList/tests/AssetList.test.js +++ b/packages/core/upload/admin/src/components/AssetList/tests/AssetList.test.js @@ -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; + } +
{ + const { formatMessage } = useIntl(); -export const BrowseStep = ({ assets, onEditAsset, onSelectAsset, selectedAssets }) => { return ( - + + {onSelectAllAsset && ( + + + 0 && selectedAssets.length === assets?.length} + onChange={onSelectAllAsset} + /> + + + )} + + + ); }; @@ -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, }; diff --git a/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/SelectedStep/index.js b/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/SelectedStep/index.js index 1dd18b891b..ec5c952be3 100644 --- a/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/SelectedStep/index.js +++ b/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/SelectedStep/index.js @@ -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 ( - - - {formatMessage( - { - id: getTrad('list.assets.selected'), - defaultMessage: - '{number, plural, =0 {No asset} one {1 asset} other {# assets}} selected', - }, - { number: 10 } - )} - - - {formatMessage({ - id: getTrad('modal.upload-list.sub-header-subtitle'), - defaultMessage: 'Manage the assets before adding them to the Media Library', - })} - + + + + {formatMessage( + { + id: getTrad('list.assets.selected'), + defaultMessage: + '{number, plural, =0 {No asset} one {1 asset} other {# assets}} selected', + }, + { number: selectedAssets.length } + )} + + + {formatMessage({ + id: getTrad('modal.upload-list.sub-header-subtitle'), + defaultMessage: 'Manage the assets before adding them to the Media Library', + })} + + + + {}} + /> ); }; + +SelectedStep.propTypes = { + onSelectAsset: PropTypes.func.isRequired, + selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired, +}; diff --git a/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/index.js b/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/index.js index 9b09dc5b53..2260f840d7 100644 --- a/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/index.js +++ b/packages/core/upload/admin/src/components/MediaLibraryInput/AssetDialog/index.js @@ -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', })} - 6 + {selectedAssets.length} @@ -114,15 +116,16 @@ export const AssetDialog = ({ onClose, multiple }) => { {}} - selectedAssets={[]} + onSelectAsset={selectOne} + selectedAssets={selectedAssets} + onSelectAllAsset={() => selectAll(assets)} onEditAsset={() => {}} /> - + diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary.js b/packages/core/upload/admin/src/pages/App/MediaLibrary.js index 71415432f9..67387512f5 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary.js +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary.js @@ -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 (
@@ -129,7 +109,7 @@ export const MediaLibrary = () => { defaultMessage: 'Select all assets', })} value={assets?.length > 0 && selected.length === assets?.length} - onChange={selectAllAssets} + onChange={() => selectAll(assets)} /> )} @@ -149,7 +129,7 @@ export const MediaLibrary = () => { {selected.length > 0 && ( - setSelected([])} /> + )} {loading && } @@ -191,7 +171,7 @@ export const MediaLibrary = () => { {data?.pagination && } diff --git a/packages/core/upload/admin/src/pages/App/components/BulkDeleteButton.js b/packages/core/upload/admin/src/pages/App/components/BulkDeleteButton.js index da2deeea17..660d57c1f8 100644 --- a/packages/core/upload/admin/src/pages/App/components/BulkDeleteButton.js +++ b/packages/core/upload/admin/src/pages/App/components/BulkDeleteButton.js @@ -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, } )} @@ -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, };