diff --git a/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/index.js b/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/index.js index 876241a35a..00d06a5949 100644 --- a/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/index.js +++ b/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/index.js @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { useIntl } from 'react-intl'; import { Button } from '@strapi/design-system/Button'; import { Flex } from '@strapi/design-system/Flex'; -import { Stack } from '@strapi/design-system/Stack'; import { Box } from '@strapi/design-system/Box'; import { Divider } from '@strapi/design-system/Divider'; import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox'; @@ -17,11 +16,14 @@ import PlusIcon from '@strapi/icons/Plus'; import { FolderDefinition, AssetDefinition } from '../../../constants'; import getTrad from '../../../utils/getTrad'; +import { getBreadcrumbDataCM } from '../../../utils'; import getAllowedFiles from '../../../utils/getAllowedFiles'; import { AssetList } from '../../AssetList'; import { FolderList } from '../../FolderList'; import { EmptyAssets } from '../../EmptyAssets'; +import { Breadcrumbs } from '../../Breadcrumbs'; import SortPicker from '../../SortPicker'; +import { useFolder } from '../../../hooks/useFolder'; import { FolderCard, FolderCardBody, FolderCardBodyAction } from '../../FolderCard'; import { Filters } from './Filters'; import PaginationFooter from './PaginationFooter'; @@ -32,7 +34,6 @@ const StartBlockActions = styled(Flex)` & > * + * { margin-left: ${({ theme }) => theme.spaces[2]}; } - margin-left: ${({ pullRight }) => (pullRight ? 'auto' : undefined)}; `; @@ -48,6 +49,7 @@ export const BrowseStep = ({ allowedTypes, assets, canCreate, + canRead, folders, multiple, onAddAsset, @@ -66,6 +68,16 @@ export const BrowseStep = ({ selectedAssets, }) => { const { formatMessage } = useIntl(); + + const { data: currentFolder, isLoading: isCurrentFolderLoading } = useFolder( + queryObject?.folder, + { + enabled: canRead && !!queryObject?.folder, + } + ); + + const breadcrumbs = !isCurrentFolderLoading && getBreadcrumbDataCM(currentFolder); + const allAllowedAsset = getAllowedFiles(allowedTypes, assets); const areAllAssetSelected = allAllowedAsset.every( @@ -79,7 +91,6 @@ export const BrowseStep = ({ const isSearchingOrFiltering = isSearching || isFiltering; const assetCount = assets.length; const folderCount = folders.length; - const handleClickFolderCard = (...args) => { // Search query will always fetch the same results // we remove it here to allow navigating in a folder and see the result of this navigation @@ -88,48 +99,61 @@ export const BrowseStep = ({ }; return ( - + {onSelectAllAsset && ( - - - {(assetCount > 0 || folderCount > 0 || isFiltering) && ( - - {multiple && ( - - - - )} - - - - )} + + {(assetCount > 0 || folderCount > 0 || isFiltering) && ( + + {multiple && ( + + + + )} + + + + )} - {(assetCount > 0 || folderCount > 0 || isSearching) && ( - - - - )} - - + {(assetCount > 0 || folderCount > 0 || isSearching) && ( + + + + )} + + + )} + + {canRead && breadcrumbs?.length > 0 && currentFolder && ( + + )} @@ -175,10 +199,13 @@ export const BrowseStep = ({ 0) || !isSearchingOrFiltering) && - formatMessage({ - id: getTrad('list.folders.title'), - defaultMessage: 'Folders', - })) || + formatMessage( + { + id: getTrad('list.folders.title'), + defaultMessage: 'Folders ({count})', + }, + { count: folderCount } + )) || '' } > @@ -209,7 +236,6 @@ export const BrowseStep = ({ {folder.name} : - {formatMessage( { @@ -234,33 +260,38 @@ export const BrowseStep = ({ )} {assetCount > 0 && folderCount > 0 && ( - + )} {assetCount > 0 && ( - 0)) && - queryObject.page === 1 && - formatMessage({ - id: getTrad('list.assets.title'), - defaultMessage: 'Assets', - })) || - '' - } - /> + + 0)) && + queryObject.page === 1 && + formatMessage( + { + id: getTrad('list.assets.title'), + defaultMessage: 'Assets ({count})', + }, + { count: assetCount } + )) || + '' + } + /> + )} {pagination.pageCount > 0 && ( - + )} - + ); }; @@ -281,11 +312,11 @@ BrowseStep.defaultProps = { onEditAsset: undefined, onEditFolder: undefined, }; - BrowseStep.propTypes = { allowedTypes: PropTypes.arrayOf(PropTypes.string), assets: PropTypes.arrayOf(AssetDefinition).isRequired, canCreate: PropTypes.bool.isRequired, + canRead: PropTypes.bool.isRequired, folders: PropTypes.arrayOf(FolderDefinition), multiple: PropTypes.bool, onAddAsset: PropTypes.func.isRequired, @@ -304,6 +335,7 @@ BrowseStep.propTypes = { page: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, _q: PropTypes.string, + folder: PropTypes.number, }).isRequired, pagination: PropTypes.shape({ pageCount: PropTypes.number.isRequired }).isRequired, selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired, diff --git a/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/tests/__snapshots__/index.test.js.snap b/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/tests/__snapshots__/index.test.js.snap index 43028d8766..e508a4201b 100644 --- a/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/tests/__snapshots__/index.test.js.snap +++ b/packages/core/upload/admin/src/components/AssetDialog/BrowseStep/tests/__snapshots__/index.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`BrowseStep renders and match snapshot 1`] = ` -.c74 { +.c81 { border: 0; -webkit-clip: rect(0 0 0 0); clip: rect(0 0 0 0); @@ -13,18 +13,50 @@ exports[`BrowseStep renders and match snapshot 1`] = ` width: 1px; } -.c19 { +.c22 { + padding-top: 12px; +} + +.c31 { + padding-top: 8px; + padding-bottom: 8px; +} + +.c35 { + position: relative; +} + +.c40 { + background: #eaf5ff; + color: #66b7f1; + padding-top: 8px; + padding-right: 12px; + padding-bottom: 8px; + padding-left: 12px; + border-radius: 4px; +} + +.c45 { + padding: 4px; + max-width: 100%; +} + +.c72 { + padding-left: 8px; +} + +.c16 { font-weight: 600; color: #32324d; font-size: 0.75rem; line-height: 1.33; } -.c16 { +.c13 { padding-right: 8px; } -.c13 { +.c10 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -38,21 +70,21 @@ exports[`BrowseStep renders and match snapshot 1`] = ` outline: none; } -.c13 svg { +.c10 svg { height: 12px; width: 12px; } -.c13 svg > g, -.c13 svg path { +.c10 svg > g, +.c10 svg path { fill: #ffffff; } -.c13[aria-disabled='true'] { +.c10[aria-disabled='true'] { pointer-events: none; } -.c13:after { +.c10:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -67,11 +99,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid transparent; } -.c13:focus-visible { +.c10:focus-visible { outline: none; } -.c13:focus-visible:after { +.c10:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -82,11 +114,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid #4945ff; } -.c17 { +.c14 { height: 100%; } -.c14 { +.c11 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -98,7 +130,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` background: #ffffff; } -.c14 .c15 { +.c11 .c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -109,66 +141,70 @@ exports[`BrowseStep renders and match snapshot 1`] = ` align-items: center; } -.c14 .c18 { +.c11 .c15 { color: #ffffff; } -.c14[aria-disabled='true'] { +.c11[aria-disabled='true'] { border: 1px solid #dcdce4; background: #eaeaef; } -.c14[aria-disabled='true'] .c18 { +.c11[aria-disabled='true'] .c15 { color: #666687; } -.c14[aria-disabled='true'] svg > g, -.c14[aria-disabled='true'] svg path { +.c11[aria-disabled='true'] svg > g, +.c11[aria-disabled='true'] svg path { fill: #666687; } -.c14[aria-disabled='true']:active { +.c11[aria-disabled='true']:active { border: 1px solid #dcdce4; background: #eaeaef; } -.c14[aria-disabled='true']:active .c18 { +.c11[aria-disabled='true']:active .c15 { color: #666687; } -.c14[aria-disabled='true']:active svg > g, -.c14[aria-disabled='true']:active svg path { +.c11[aria-disabled='true']:active svg > g, +.c11[aria-disabled='true']:active svg path { fill: #666687; } -.c14:hover { +.c11:hover { background-color: #f6f6f9; } -.c14:active { +.c11:active { background-color: #eaeaef; } -.c14 .c18 { +.c11 .c15 { color: #32324d; } -.c14 svg > g, -.c14 svg path { +.c11 svg > g, +.c11 svg path { fill: #32324d; } -.c36 { +.c42 { position: relative; overflow: hidden; max-width: 100%; } -.c41 { +.c47 { max-width: 100%; } -.c3 { +.c58 { + padding-top: 16px; +} + +.c0 { -webkit-align-items: flex-start; -webkit-box-align: flex-start; -ms-flex-align: flex-start; @@ -186,7 +222,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` justify-content: space-between; } -.c4 { +.c1 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -203,7 +239,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-wrap: wrap; } -.c20 { +.c17 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -217,7 +253,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: row; } -.c37 { +.c43 { -webkit-align-items: flex-start; -webkit-box-align: flex-start; -ms-flex-align: flex-start; @@ -231,7 +267,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: column; } -.c42 { +.c48 { -webkit-align-items: start; -webkit-box-align: start; -ms-flex-align: start; @@ -245,7 +281,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: column; } -.c52 { +.c59 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -263,162 +299,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` justify-content: space-between; } -.c31 { - background: #ffffff; - padding: 12px; - border-radius: 4px; - border-style: solid; - border-width: 1px; - border-color: #eaeaef; - box-shadow: 0px 1px 4px rgba(33,33,52,0.1); - cursor: pointer; - cursor: pointer; -} - -.c0 { - -webkit-align-items: stretch; - -webkit-box-align: stretch; - -ms-flex-align: stretch; - align-items: stretch; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; -} - -.c32 { - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: row; - -ms-flex-direction: row; - flex-direction: row; -} - -.c1 > * { - margin-top: 0; - margin-bottom: 0; -} - -.c1 > * + * { - margin-top: 16px; -} - -.c33 > * { - margin-left: 0; - margin-right: 0; -} - -.c33 > * + * { - margin-left: 8px; -} - -.c2 { - padding-bottom: 16px; -} - -.c25 { - padding-top: 8px; - padding-bottom: 8px; -} - -.c29 { - position: relative; -} - -.c34 { - background: #eaf5ff; - color: #66b7f1; - padding-top: 8px; - padding-right: 12px; - padding-bottom: 8px; - padding-left: 12px; - border-radius: 4px; -} - -.c39 { - padding: 4px; - max-width: 100%; -} - -.c65 { - padding-left: 8px; -} - -.c27 { - display: grid; - grid-template-columns: repeat(12,1fr); - gap: 16px; -} - -.c28 { - grid-column: span 3; - max-width: 100%; -} - -.c26 { - font-weight: 500; - color: #32324d; - font-weight: 500; - font-size: 1rem; - line-height: 1.25; -} - -.c43 { - font-weight: 500; - color: #32324d; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.875rem; - line-height: 1.43; -} - -.c46 { - color: #666687; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.75rem; - line-height: 1.33; -} - -.c66 { - color: #666687; - font-size: 0.875rem; - line-height: 1.43; -} - -.c72 { - font-weight: 600; - color: #32324d; - font-size: 0.75rem; - line-height: 1.33; -} - -.c45 { - border: 0; - -webkit-clip: rect(0 0 0 0); - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} - -.c23 { +.c20 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -432,21 +313,21 @@ exports[`BrowseStep renders and match snapshot 1`] = ` outline: none; } -.c23 svg { +.c20 svg { height: 12px; width: 12px; } -.c23 svg > g, -.c23 svg path { +.c20 svg > g, +.c20 svg path { fill: #ffffff; } -.c23[aria-disabled='true'] { +.c20[aria-disabled='true'] { pointer-events: none; } -.c23:after { +.c20:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -461,11 +342,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid transparent; } -.c23:focus-visible { +.c20:focus-visible { outline: none; } -.c23:focus-visible:after { +.c20:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -476,7 +357,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid #4945ff; } -.c24 { +.c21 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -493,30 +374,30 @@ exports[`BrowseStep renders and match snapshot 1`] = ` width: 2rem; } -.c24 svg > g, -.c24 svg path { +.c21 svg > g, +.c21 svg path { fill: #8e8ea9; } -.c24:hover svg > g, -.c24:hover svg path { +.c21:hover svg > g, +.c21:hover svg path { fill: #666687; } -.c24:active svg > g, -.c24:active svg path { +.c21:active svg > g, +.c21:active svg path { fill: #a5a5ba; } -.c24[aria-disabled='true'] { +.c21[aria-disabled='true'] { background-color: #eaeaef; } -.c24[aria-disabled='true'] svg path { +.c21[aria-disabled='true'] svg path { fill: #666687; } -.c57 { +.c64 { position: absolute; left: 0; right: 0; @@ -527,24 +408,24 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: none; } -.c57:focus { +.c64:focus { outline: none; } -.c57[aria-disabled='true'] { +.c64[aria-disabled='true'] { cursor: not-allowed; } -.c60 { +.c67 { padding-right: 16px; padding-left: 16px; } -.c62 { +.c69 { padding-left: 12px; } -.c53 { +.c60 { -webkit-align-items: stretch; -webkit-box-align: stretch; -ms-flex-align: stretch; @@ -558,7 +439,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: column; } -.c55 { +.c62 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -572,7 +453,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: row; } -.c58 { +.c65 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -590,7 +471,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` justify-content: space-between; } -.c61 { +.c68 { color: #32324d; display: block; white-space: nowrap; @@ -600,12 +481,12 @@ exports[`BrowseStep renders and match snapshot 1`] = ` line-height: 1.43; } -.c54 > * { +.c61 > * { margin-top: 0; margin-bottom: 0; } -.c56 { +.c63 { position: relative; border: 1px solid #dcdce4; padding-right: 12px; @@ -621,28 +502,28 @@ exports[`BrowseStep renders and match snapshot 1`] = ` transition-duration: 0.2s; } -.c56:focus-within { +.c63:focus-within { border: 1px solid #4945ff; box-shadow: #4945ff 0px 0px 0px 2px; } -.c63 { +.c70 { background: transparent; border: none; position: relative; z-index: 1; } -.c63 svg { +.c70 svg { height: 0.6875rem; width: 0.6875rem; } -.c63 svg path { +.c70 svg path { fill: #666687; } -.c64 { +.c71 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -651,19 +532,27 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: none; } -.c64 svg { +.c71 svg { width: 0.375rem; } -.c59 { +.c66 { width: 100%; } -.c48 { - right: 16px; +.c37 { + background: #ffffff; + padding: 12px; + border-radius: 4px; + border-style: solid; + border-width: 1px; + border-color: #eaeaef; + box-shadow: 0px 1px 4px rgba(33,33,52,0.1); + cursor: pointer; + cursor: pointer; } -.c49 { +.c38 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -677,32 +566,196 @@ exports[`BrowseStep renders and match snapshot 1`] = ` flex-direction: row; } -.c50 > * { +.c39 > * { margin-left: 0; margin-right: 0; } -.c50 > * + * { +.c39 > * + * { margin-left: 8px; } -.c51 { - position: absolute; - top: 12px; +.c32 { + font-weight: 500; + color: #32324d; + font-weight: 500; + font-size: 1rem; + line-height: 1.25; } -.c9 { +.c49 { + font-weight: 500; + color: #32324d; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.875rem; + line-height: 1.43; +} + +.c52 { + color: #666687; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 0.75rem; + line-height: 1.33; +} + +.c73 { + color: #666687; + font-size: 0.875rem; + line-height: 1.43; +} + +.c79 { font-weight: 600; color: #32324d; font-size: 0.75rem; line-height: 1.33; } -.c11 { +.c51 { + border: 0; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.c33 { + display: grid; + grid-template-columns: repeat(12,1fr); + gap: 16px; +} + +.c34 { + grid-column: span 3; + max-width: 100%; +} + +.c54 { + right: 16px; +} + +.c55 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c56 > * { + margin-left: 0; + margin-right: 0; +} + +.c56 > * + * { + margin-left: 8px; +} + +.c57 { + position: absolute; + top: 12px; +} + +.c26 { + padding-top: 4px; + padding-right: 8px; + padding-bottom: 4px; padding-left: 8px; } +.c28 { + padding-right: 4px; + padding-left: 4px; +} + +.c27 { + color: #32324d; + font-size: 0.75rem; + line-height: 1.33; +} + +.c29 { + color: #8e8ea9; + font-size: 0.75rem; + line-height: 1.33; +} + +.c23 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c25 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; +} + +.c24:first-child { + margin-left: calc(-1*8px); +} + +.c30 { + border-radius: 4px; + color: #666687; + font-size: 0.75rem; + line-height: 1.43; + padding: 4px 8px; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c30:hover, +.c30:focus { + background-color: #dcdce4; + color: #4a4a6a; +} + .c6 { + font-weight: 600; + color: #32324d; + font-size: 0.75rem; + line-height: 1.33; +} + +.c8 { + padding-left: 8px; +} + +.c3 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -716,21 +769,21 @@ exports[`BrowseStep renders and match snapshot 1`] = ` outline: none; } -.c6 svg { +.c3 svg { height: 12px; width: 12px; } -.c6 svg > g, -.c6 svg path { +.c3 svg > g, +.c3 svg path { fill: #ffffff; } -.c6[aria-disabled='true'] { +.c3[aria-disabled='true'] { pointer-events: none; } -.c6:after { +.c3:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -745,11 +798,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid transparent; } -.c6:focus-visible { +.c3:focus-visible { outline: none; } -.c6:focus-visible:after { +.c3:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -760,7 +813,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid #4945ff; } -.c7 { +.c4 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -772,7 +825,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` background: #ffffff; } -.c7 .c10 { +.c4 .c7 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -783,56 +836,56 @@ exports[`BrowseStep renders and match snapshot 1`] = ` align-items: center; } -.c7 .c8 { +.c4 .c5 { color: #ffffff; } -.c7[aria-disabled='true'] { +.c4[aria-disabled='true'] { border: 1px solid #dcdce4; background: #eaeaef; } -.c7[aria-disabled='true'] .c8 { +.c4[aria-disabled='true'] .c5 { color: #666687; } -.c7[aria-disabled='true'] svg > g, -.c7[aria-disabled='true'] svg path { +.c4[aria-disabled='true'] svg > g, +.c4[aria-disabled='true'] svg path { fill: #666687; } -.c7[aria-disabled='true']:active { +.c4[aria-disabled='true']:active { border: 1px solid #dcdce4; background: #eaeaef; } -.c7[aria-disabled='true']:active .c8 { +.c4[aria-disabled='true']:active .c5 { color: #666687; } -.c7[aria-disabled='true']:active svg > g, -.c7[aria-disabled='true']:active svg path { +.c4[aria-disabled='true']:active svg > g, +.c4[aria-disabled='true']:active svg path { fill: #666687; } -.c7:hover { +.c4:hover { background-color: #f6f6f9; } -.c7:active { +.c4:active { background-color: #eaeaef; } -.c7 .c8 { +.c4 .c5 { color: #32324d; } -.c7 svg > g, -.c7 svg path { +.c4 svg > g, +.c4 svg path { fill: #32324d; } -.c12 { +.c9 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -843,12 +896,12 @@ exports[`BrowseStep renders and match snapshot 1`] = ` align-items: center; } -.c12 svg { +.c9 svg { height: 4px; width: 6px; } -.c30 { +.c36 { height: 100%; left: 0; position: absolute; @@ -857,41 +910,41 @@ exports[`BrowseStep renders and match snapshot 1`] = ` width: 100%; } -.c30:hover, -.c30:focus { +.c36:hover, +.c36:focus { -webkit-text-decoration: none; text-decoration: none; } -.c35 path { +.c41 path { fill: currentColor; } -.c47 { +.c53 { display: none; } -.c38 { +.c44 { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } -.c40:focus { +.c46:focus { outline: 2px solid #4945ff; outline-offset: -2px; } -.c67 > * + * { +.c74 > * + * { margin-left: 4px; } -.c73 { +.c80 { line-height: revert; } -.c68 { +.c75 { padding: 12px; border-radius: 4px; -webkit-text-decoration: none; @@ -904,7 +957,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` outline: none; } -.c68:after { +.c75:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -919,11 +972,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid transparent; } -.c68:focus-visible { +.c75:focus-visible { outline: none; } -.c68:focus-visible:after { +.c75:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -934,7 +987,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid #4945ff; } -.c70 { +.c77 { padding: 12px; border-radius: 4px; box-shadow: 0px 1px 4px rgba(33,33,52,0.1); @@ -948,7 +1001,7 @@ exports[`BrowseStep renders and match snapshot 1`] = ` outline: none; } -.c70:after { +.c77:after { -webkit-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; @@ -963,11 +1016,11 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid transparent; } -.c70:focus-visible { +.c77:focus-visible { outline: none; } -.c70:focus-visible:after { +.c77:focus-visible:after { border-radius: 8px; content: ''; position: absolute; @@ -978,226 +1031,288 @@ exports[`BrowseStep renders and match snapshot 1`] = ` border: 2px solid #4945ff; } -.c71 { +.c78 { color: #271fe0; background: #ffffff; } -.c71:hover { +.c78:hover { box-shadow: 0px 1px 4px rgba(33,33,52,0.1); } -.c69 { +.c76 { font-size: 0.7rem; pointer-events: none; } -.c69 svg path { +.c76 svg path { fill: #c0c0cf; } -.c69:focus svg path, -.c69:hover svg path { +.c76:focus svg path, +.c76:hover svg path { fill: #c0c0cf; } -.c5 > * + * { +.c2 > * + * { margin-left: 8px; } -.c21 { +.c18 { margin-left: auto; } -.c21 > * + * { +.c18 > * + * { margin-left: 8px; } -.c22 { +.c19 { -webkit-flex-shrink: 0; -ms-flex-negative: 0; flex-shrink: 0; } -.c44 { +.c50 { max-width: 100%; } @media (max-width:68.75rem) { - .c28 { + .c34 { grid-column: span; } } @media (max-width:34.375rem) { - .c28 { + .c34 { grid-column: span; } }
-
-
- -
+
-
- - + + +
+ + Filters -
+ +
+
+ + +
+
+ +

- Folders + Folders (1)

@@ -1255,17 +1370,17 @@ exports[`BrowseStep renders and match snapshot 1`] = `

({ ...jest.requireActual('@strapi/helper-plugin'), useTracking: jest.fn(() => ({ trackUsage: jest.fn() })), @@ -62,38 +67,50 @@ const FIXTURE_FOLDERS = [ }, ]; +const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + const ComponentFixture = props => { return ( - - - - - - - + + + + + {}}> + + + + + + ); }; const setup = props => render(); - describe('BrowseStep', () => { afterEach(() => { jest.clearAllMocks(); @@ -101,14 +118,37 @@ describe('BrowseStep', () => { it('renders and match snapshot', () => { const { container } = setup(); - expect(container).toMatchSnapshot(); }); + it('should not fetch folder if the user does not have the permission', () => { + const spy = jest.fn().mockReturnValueOnce({ isLoading: false }); + useFolder.mockImplementationOnce(spy); + + setup({ + canRead: false, + queryObject: { folder: 1, page: 1, pageSize: 10, filters: { $and: [] } }, + }); + + expect(spy).toHaveBeenCalledWith(1, { enabled: false }); + }); + + it('should show breadcrumbs navigation', () => { + setup(); + + expect(screen.queryByLabelText('Folders navigation')).toBeInTheDocument(); + }); + + it('should hide breadcrumbs navigation if in root folder', () => { + useFolder.mockReturnValueOnce({ isLoading: false, data: undefined }); + setup(); + + expect(screen.queryByLabelText('Folders navigation')).not.toBeInTheDocument(); + }); + it('calls onAddAsset callback', () => { const spy = jest.fn(); const { getByText } = setup({ onAddAsset: spy, folders: [] }); - fireEvent.click(getByText('Add new assets')); expect(spy).toHaveBeenCalled(); }); @@ -116,19 +156,16 @@ describe('BrowseStep', () => { it('calls onChangeFolder callback', () => { const spy = jest.fn(); const { getByRole } = setup({ onChangeFolder: spy }); - fireEvent.click( getByRole('button', { name: /folder 1 : 1 folder, 1 asset/i, }) ); - expect(spy).toHaveBeenCalled(); }); it('does display empty state upload first assets if no folder or assets', () => { setup({ folders: [], assets: [] }); - expect(screen.getByText('Upload your first assets...')).toBeInTheDocument(); }); @@ -138,7 +175,6 @@ describe('BrowseStep', () => { assets: [], queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' }, }); - expect(screen.getByText('There are no assets with the applied filters')).toBeInTheDocument(); }); @@ -148,7 +184,6 @@ describe('BrowseStep', () => { assets: [], queryObject: { page: 1, pageSize: 10, filters: { $and: [{ mime: 'audio' }] }, _q: '' }, }); - expect(screen.getByText('Filters')).toBeInTheDocument(); }); @@ -158,7 +193,6 @@ describe('BrowseStep', () => { assets: FIXTURE_ASSETS, queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' }, }); - expect(screen.queryByText('Assets')).not.toBeInTheDocument(); }); @@ -166,7 +200,6 @@ describe('BrowseStep', () => { setup({ queryObject: { page: 1, pageSize: 10, filters: { $and: [] }, _q: 'true' }, }); - expect(screen.queryByText('Folders')).not.toBeInTheDocument(); }); @@ -175,7 +208,7 @@ describe('BrowseStep', () => { assets: FIXTURE_ASSETS, }); - expect(screen.getByText('Folders')).toBeInTheDocument(); - expect(screen.getByText('Assets')).toBeInTheDocument(); + expect(screen.getByText('Folders (1)')).toBeInTheDocument(); + expect(screen.getByText('Assets (1)')).toBeInTheDocument(); }); }); diff --git a/packages/core/upload/admin/src/components/AssetDialog/DialogHeader.js b/packages/core/upload/admin/src/components/AssetDialog/DialogHeader.js deleted file mode 100644 index 2c1726d011..0000000000 --- a/packages/core/upload/admin/src/components/AssetDialog/DialogHeader.js +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; -import { Breadcrumbs, Crumb } from '@strapi/design-system/Breadcrumbs'; -import { ModalHeader } from '@strapi/design-system/ModalLayout'; -import { Stack } from '@strapi/design-system/Stack'; -import { Icon } from '@strapi/design-system/Icon'; -import ArrowLeft from '@strapi/icons/ArrowLeft'; -import { findRecursiveFolderMetadatas, getTrad } from '../../utils'; -import { useFolderStructure } from '../../hooks/useFolderStructure'; - -const BackButton = styled.button` - height: ${({ theme }) => theme.spaces[4]}; - color: ${({ theme }) => theme.colors.neutral500}; - - &:hover, - &:focus { - color: ${({ theme }) => theme.colors.neutral600}; - } -`; - -const BackIcon = styled(Icon)` - path { - fill: currentColor; - } -`; - -export const DialogHeader = ({ currentFolder, onChangeFolder, canRead }) => { - const { formatMessage } = useIntl(); - - const { data, isLoading } = useFolderStructure({ - enabled: canRead, - }); - - const folderMetadatas = - !isLoading && Array.isArray(data) && findRecursiveFolderMetadatas(data[0], currentFolder); - const folderLabel = - folderMetadatas?.currentFolderLabel && - (folderMetadatas.currentFolderLabel.length > 60 - ? `${folderMetadatas.currentFolderLabel.slice(0, 60)}...` - : folderMetadatas.currentFolderLabel); - - return ( - - - {currentFolder && ( - onChangeFolder(folderMetadatas?.parentId)} - > - - - )} - - - {formatMessage({ - id: getTrad('header.actions.add-assets'), - defaultMessage: 'Add new assets', - })} - - {folderLabel && {folderLabel}} - - - - ); -}; - -DialogHeader.defaultProps = { - currentFolder: undefined, - onChangeFolder: undefined, -}; - -DialogHeader.propTypes = { - canRead: PropTypes.bool.isRequired, - currentFolder: PropTypes.number, - onChangeFolder: PropTypes.func, -}; diff --git a/packages/core/upload/admin/src/components/AssetDialog/index.js b/packages/core/upload/admin/src/components/AssetDialog/index.js index b11567c659..d27c7cadae 100644 --- a/packages/core/upload/admin/src/components/AssetDialog/index.js +++ b/packages/core/upload/admin/src/components/AssetDialog/index.js @@ -1,17 +1,17 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { ModalLayout, ModalBody } from '@strapi/design-system/ModalLayout'; +import { useIntl } from 'react-intl'; +import { ModalLayout, ModalBody, ModalHeader } from '@strapi/design-system/ModalLayout'; import { Flex } from '@strapi/design-system/Flex'; import { Button } from '@strapi/design-system/Button'; import { Divider } from '@strapi/design-system/Divider'; -import { useIntl } from 'react-intl'; +import { Typography } from '@strapi/design-system/Typography'; import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs'; import { Badge } from '@strapi/design-system/Badge'; import { Loader } from '@strapi/design-system/Loader'; import { Stack } from '@strapi/design-system/Stack'; import { NoPermissions, AnErrorOccurred, useSelectionState, pxToRem } from '@strapi/helper-plugin'; - import { getTrad, containsAssetFilter } from '../../utils'; import { SelectedStep } from './SelectedStep'; import { BrowseStep } from './BrowseStep'; @@ -21,7 +21,6 @@ import { useFolders } from '../../hooks/useFolders'; import useModalQueryParams from '../../hooks/useModalQueryParams'; import { AssetDefinition } from '../../constants'; import getAllowedFiles from '../../utils/getAllowedFiles'; -import { DialogHeader } from './DialogHeader'; import { DialogFooter } from './DialogFooter'; import { EditAssetDialog } from '../EditAssetDialog'; import { moveElement } from '../../utils/moveElement'; @@ -55,6 +54,7 @@ export const AssetDialog = ({ canCopyLink, canDownload, } = useMediaLibraryPermissions(); + const [ { queryObject }, { @@ -66,11 +66,13 @@ export const AssetDialog = ({ onChangeFolder: onChangeFolderParam, }, ] = useModalQueryParams({ folder: folderId }); + const { data: { pagination, results: assets } = {}, isLoading: isLoadingAssets, error: errorAssets, } = useAssets({ skipWhen: !canRead, query: queryObject }); + const { data: folders, isLoading: isLoadingFolders, error: errorFolders } = useFolders({ enabled: canRead && !containsAssetFilter(queryObject) && pagination?.page === 1, query: queryObject, @@ -84,6 +86,7 @@ export const AssetDialog = ({ const [initialSelectedTabIndex, setInitialSelectedTabIndex] = useState( selectedAssets.length > 0 ? 1 : 0 ); + const handleSelectAllAssets = () => { const hasAllAssets = assets.every( asset => selectedAssets.findIndex(curr => curr.id === asset.id) !== -1 @@ -97,6 +100,7 @@ export const AssetDialog = ({ return multiple ? selectAll(allowedAssets) : undefined; }; + const handleSelectAsset = asset => { return multiple ? selectOne(asset) : selectOnly(asset); }; @@ -107,11 +111,18 @@ export const AssetDialog = ({ if (isLoading) { return ( - + + + {formatMessage({ + id: getTrad('header.actions.add-assets'), + defaultMessage: 'Add new assets', + })} + + {formatMessage({ - id: getTrad('list.asset.load'), + id: getTrad('content.isLoading'), defaultMessage: 'Content is loading.', })} @@ -124,7 +135,14 @@ export const AssetDialog = ({ if (hasError) { return ( - + + + {formatMessage({ + id: getTrad('header.actions.add-assets'), + defaultMessage: 'Add new assets', + })} + + @@ -134,7 +152,14 @@ export const AssetDialog = ({ if (!canRead) { return ( - + + + {formatMessage({ + id: getTrad('header.actions.add-assets'), + defaultMessage: 'Add new assets', + })} + + @@ -168,7 +193,6 @@ export const AssetDialog = ({ const offset = destIndex - hoverIndex; const orderedAssetsClone = selectedAssets.slice(); const nextAssets = moveElement(orderedAssetsClone, hoverIndex, offset); - setSelections(nextAssets); }; @@ -179,11 +203,14 @@ export const AssetDialog = ({ return ( - + + + {formatMessage({ + id: getTrad('header.actions.add-assets'), + defaultMessage: 'Add new assets', + })} + + {selectedAssets.length} - +

+ + +
  • + + parent folder + + +
  • +
  • +
    + + current folder + +
    +
  • + + +
    +

    +

    +

    +
    +`; diff --git a/packages/core/upload/admin/src/components/Breadcrumbs/tests/index.test.js b/packages/core/upload/admin/src/components/Breadcrumbs/tests/index.test.js new file mode 100644 index 0000000000..32ce79bfef --- /dev/null +++ b/packages/core/upload/admin/src/components/Breadcrumbs/tests/index.test.js @@ -0,0 +1,71 @@ +import React from 'react'; +import { render as renderTL, screen, fireEvent, waitFor } from '@testing-library/react'; +import { QueryClientProvider, QueryClient } from 'react-query'; +import { MemoryRouter } from 'react-router-dom'; +import { IntlProvider } from 'react-intl'; + +import { ThemeProvider, lightTheme } from '@strapi/design-system'; +import { Breadcrumbs } from '../index'; + +jest.mock('../../../hooks/useFolderStructure'); + +jest.mock('@strapi/helper-plugin', () => ({ + ...jest.requireActual('@strapi/helper-plugin'), + useQueryParams: jest.fn().mockReturnValue([{ query: { folder: 22 } }]), +})); + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + +const defaultBreadcrumbs = [ + { + href: '/', + id: null, + label: 'Media Library', + }, + [], + { href: '/', id: 21, label: 'parent folder' }, + { id: 22, label: 'current folder' }, +]; + +const setup = props => + renderTL( + + + + + + + + + + ); + +describe('Media Library | Breadcrumbs', () => { + test('should render and match snapshot', () => { + const { container } = setup({ currentFolderId: 22 }); + + expect(container.querySelector('nav')).toBeInTheDocument(); + expect(screen.getByText('parent folder')).toBeInTheDocument(); + expect(screen.getByText('current folder')).toBeInTheDocument(); + expect(screen.getByText('Media Library')).toBeInTheDocument(); + expect(container).toMatchSnapshot(); + }); + + test('should store other ascendants in simple menu', async () => { + const { getByRole } = setup({ currentFolderId: 22 }); + + const simpleMenuButton = getByRole('button', { name: /get more ascendants folders/i }); + fireEvent.mouseDown(simpleMenuButton); + + await waitFor(() => { + expect(screen.getByText('second child')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.js b/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.js index 5f8a183f07..223f99e7f8 100644 --- a/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.js +++ b/packages/core/upload/admin/src/components/BulkMoveDialog/BulkMoveDialog.js @@ -62,7 +62,7 @@ export const BulkMoveDialog = ({ onClose, selected, currentFolder }) => { {formatMessage({ - id: getTrad('list.asset.load'), + id: getTrad('content.isLoading'), defaultMessage: 'Content is loading.', })} diff --git a/packages/core/upload/admin/src/components/EditAssetDialog/index.js b/packages/core/upload/admin/src/components/EditAssetDialog/index.js index adf343895e..babb1a8807 100644 --- a/packages/core/upload/admin/src/components/EditAssetDialog/index.js +++ b/packages/core/upload/admin/src/components/EditAssetDialog/index.js @@ -146,7 +146,7 @@ export const EditAssetDialog = ({ {formatMessage({ - id: getTrad('list.asset.load'), + id: getTrad('content.isLoading'), defaultMessage: 'Content is loading.', })} diff --git a/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.js b/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.js index edacc10c2f..4d7622ca37 100644 --- a/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.js +++ b/packages/core/upload/admin/src/components/EditFolderDialog/EditFolderDialog.js @@ -130,7 +130,7 @@ export const EditFolderDialog = ({ onClose, folder, location, parentFolderId }) {formatMessage({ - id: getTrad('list.asset.load'), + id: getTrad('content.isLoading'), defaultMessage: 'Content is loading.', })} diff --git a/packages/core/upload/admin/src/constants.js b/packages/core/upload/admin/src/constants.js index 175926c550..50e661495b 100644 --- a/packages/core/upload/admin/src/constants.js +++ b/packages/core/upload/admin/src/constants.js @@ -21,6 +21,8 @@ const ParentFolderDefinition = PropTypes.shape({ path: PropTypes.string.isRequired, }); +ParentFolderDefinition.parent = PropTypes.shape(ParentFolderDefinition); + export const FolderDefinition = PropTypes.shape({ id: PropTypes.number.isRequired, children: PropTypes.shape({ @@ -32,7 +34,7 @@ export const FolderDefinition = PropTypes.shape({ count: PropTypes.number.isRequired, }), name: PropTypes.string.isRequired, - parent: PropTypes.oneOf([ParentFolderDefinition, PropTypes.number]), + parent: PropTypes.oneOfType([ParentFolderDefinition, PropTypes.number]), updatedAt: PropTypes.string.isRequired, updatedBy: PropTypes.shape(), pathId: PropTypes.number.isRequired, @@ -72,3 +74,15 @@ export const AssetDefinition = PropTypes.shape({ }), }), }); + +export const CrumbDefinition = PropTypes.shape({ + id: PropTypes.number, + label: PropTypes.string.isRequired, + href: PropTypes.string, +}); + +export const CrumbMenuDefinition = PropTypes.arrayOf(CrumbDefinition); + +export const BreadcrumbsDefinition = PropTypes.arrayOf( + PropTypes.oneOfType([CrumbDefinition, CrumbMenuDefinition]) +); diff --git a/packages/core/upload/admin/src/hooks/__mocks__/useFolderStructure.js b/packages/core/upload/admin/src/hooks/__mocks__/useFolderStructure.js index b3f60e89c6..3b016ffdf1 100644 --- a/packages/core/upload/admin/src/hooks/__mocks__/useFolderStructure.js +++ b/packages/core/upload/admin/src/hooks/__mocks__/useFolderStructure.js @@ -19,7 +19,13 @@ export const useFolderStructure = jest.fn().mockReturnValue({ { value: 21, name: 'first child of the second child', - children: [], + children: [ + { + value: 22, + name: 'another child', + children: [], + }, + ], }, ], }, diff --git a/packages/core/upload/admin/src/hooks/tests/useFolder.test.js b/packages/core/upload/admin/src/hooks/tests/useFolder.test.js index 719e092cf3..cf7090c45e 100644 --- a/packages/core/upload/admin/src/hooks/tests/useFolder.test.js +++ b/packages/core/upload/admin/src/hooks/tests/useFolder.test.js @@ -81,7 +81,9 @@ describe('useFolder', () => { await waitFor(() => result.current.isSuccess); await waitForNextUpdate(); - expect(axiosInstance.get).toBeCalledWith(`/upload/folders/1?populate=parent`); + expect(axiosInstance.get).toBeCalledWith( + '/upload/folders/1?populate[parent][populate][parent]=*' + ); }); test('it does not fetch, if enabled is set to false', async () => { diff --git a/packages/core/upload/admin/src/hooks/useFolder.js b/packages/core/upload/admin/src/hooks/useFolder.js index 3cce2b7134..753e408c05 100644 --- a/packages/core/upload/admin/src/hooks/useFolder.js +++ b/packages/core/upload/admin/src/hooks/useFolder.js @@ -10,7 +10,9 @@ export const useFolder = (id, { enabled = true }) => { const fetchFolder = async () => { try { - const { data } = await axiosInstance.get(`${dataRequestURL}/${id}?populate=parent`); + const { data } = await axiosInstance.get( + `${dataRequestURL}/${id}?populate[parent][populate][parent]=*` + ); return data.data; } catch (err) { diff --git a/packages/core/upload/admin/src/pages/App/MediaLibrary.js b/packages/core/upload/admin/src/pages/App/MediaLibrary.js index d07a852eab..554aec0cd0 100644 --- a/packages/core/upload/admin/src/pages/App/MediaLibrary.js +++ b/packages/core/upload/admin/src/pages/App/MediaLibrary.js @@ -2,7 +2,6 @@ import React, { useState, useRef } from 'react'; // useState import { useIntl } from 'react-intl'; import styled from 'styled-components'; import { useLocation, useHistory } from 'react-router-dom'; -import { stringify } from 'qs'; import { LoadingIndicatorPage, useFocusWhenNavigate, @@ -31,7 +30,7 @@ import { FolderList } from '../../components/FolderList'; import SortPicker from '../../components/SortPicker'; import { useAssets } from '../../hooks/useAssets'; import { useFolders } from '../../hooks/useFolders'; -import { getTrad, containsAssetFilter } from '../../utils'; +import { getTrad, containsAssetFilter, getBreadcrumbDataML, getFolderURL } from '../../utils'; import { PaginationFooter } from '../../components/PaginationFooter'; import { useMediaLibraryPermissions } from '../../hooks/useMediaLibraryPermissions'; import { useFolder } from '../../hooks/useFolder'; @@ -148,8 +147,9 @@ export const MediaLibrary = () => {
    { 0) || !isFiltering) && - formatMessage({ - id: getTrad('list.folders.title'), - defaultMessage: 'Folders', - })) || + formatMessage( + { + id: getTrad('list.folders.title'), + defaultMessage: 'Folders ({count})', + }, + { count: folderCount } + )) || '' } > @@ -243,13 +246,7 @@ export const MediaLibrary = () => { currentFolder => currentFolder.id === folder.id ); - // Search query will always fetch the same results - // we remove it here to allow navigating in a folder and see the result of this navigation - const { _q, ...queryParamsWithoutQ } = query; - const url = `${pathname}?${stringify({ - ...queryParamsWithoutQ, - folder: folder.id, - })}`; + const url = getFolderURL(pathname, query, folder); return ( @@ -334,10 +331,13 @@ export const MediaLibrary = () => { title={ ((!isFiltering || (isFiltering && folderCount > 0)) && assetsData?.pagination?.page === 1 && - formatMessage({ - id: getTrad('list.assets.title'), - defaultMessage: 'Assets', - })) || + formatMessage( + { + id: getTrad('list.assets.title'), + defaultMessage: 'Assets ({count})', + }, + { count: assetCount } + )) || '' } /> diff --git a/packages/core/upload/admin/src/pages/App/components/Header.js b/packages/core/upload/admin/src/pages/App/components/Header.js index 6c975aff69..b1ea8736f3 100644 --- a/packages/core/upload/admin/src/pages/App/components/Header.js +++ b/packages/core/upload/admin/src/pages/App/components/Header.js @@ -11,15 +11,15 @@ import { Link } from '@strapi/design-system/Link'; import ArrowLeft from '@strapi/icons/ArrowLeft'; import Plus from '@strapi/icons/Plus'; import { getTrad } from '../../../utils'; -import { FolderDefinition } from '../../../constants'; +import { FolderDefinition, BreadcrumbsDefinition } from '../../../constants'; +import { Breadcrumbs } from '../../../components/Breadcrumbs'; export const Header = ({ + breadcrumbs, canCreate, + folder, onToggleEditFolderDialog, onToggleUploadAssetDialog, - folder, - assetCount, - folderCount, }) => { const { formatMessage } = useIntl(); const { pathname } = useLocation(); @@ -28,22 +28,27 @@ export const Header = ({ ...query, folder: folder?.parent?.id ?? undefined, }; - const name = folder?.name?.length > 30 ? `${folder.name.slice(0, 30)}...` : folder?.name; return ( + ) + } navigationAction={ folder && ( { expect(container).toMatchSnapshot(); }); - test('truncates long folder lavels', () => { - useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { folder: 2 } }, jest.fn()]); - - const { queryByText } = setup({ - folder: { ...FIXTURE_FOLDER, name: 'The length of this label exceeds the maximum length' }, - }); - expect(queryByText('Media Library - The length of this label excee...')).toBeInTheDocument(); - }); - test('does not render a back button at the root level of the media library', () => { const { queryByText } = setup({ folder: null }); diff --git a/packages/core/upload/admin/src/pages/App/tests/MediaLibrary.test.js b/packages/core/upload/admin/src/pages/App/tests/MediaLibrary.test.js index 8b130c205e..e8680e4cc5 100644 --- a/packages/core/upload/admin/src/pages/App/tests/MediaLibrary.test.js +++ b/packages/core/upload/admin/src/pages/App/tests/MediaLibrary.test.js @@ -4,12 +4,13 @@ import { QueryClientProvider, QueryClient } from 'react-query'; import { render as renderTL, screen, waitFor, fireEvent } from '@testing-library/react'; import { useSelectionState, useQueryParams, TrackingContext } from '@strapi/helper-plugin'; import { MemoryRouter } from 'react-router-dom'; +import { IntlProvider } from 'react-intl'; import { useMediaLibraryPermissions } from '../../../hooks/useMediaLibraryPermissions'; import { useFolders } from '../../../hooks/useFolders'; import { useAssets } from '../../../hooks/useAssets'; +import { useFolder } from '../../../hooks/useFolder'; import { MediaLibrary } from '../MediaLibrary'; -import en from '../../../translations/en.json'; const FIXTURE_ASSET_PAGINATION = { pageCount: 1, @@ -56,7 +57,6 @@ jest.mock('../../../hooks/useMediaLibraryPermissions'); jest.mock('../../../hooks/useFolders'); jest.mock('../../../hooks/useFolder'); jest.mock('../../../hooks/useAssets'); - jest.mock('@strapi/helper-plugin', () => ({ ...jest.requireActual('@strapi/helper-plugin'), useRBAC: jest.fn(), @@ -66,17 +66,11 @@ jest.mock('@strapi/helper-plugin', () => ({ .fn() .mockReturnValue([[], { selectOne: jest.fn(), selectAll: jest.fn() }]), })); - jest.mock('../../../utils', () => ({ ...jest.requireActual('../../../utils'), getTrad: x => x, })); -jest.mock('react-intl', () => ({ - FormattedMessage: ({ id }) => id, - useIntl: () => ({ formatMessage: jest.fn(({ id }) => en[id] || id) }), -})); - const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -89,13 +83,15 @@ const queryClient = new QueryClient({ const renderML = () => renderTL( - - - - - - - + + + + + + + + + ); @@ -107,7 +103,6 @@ describe('Media library homepage', () => { describe('navigation', () => { it('focuses the title when mounting the component', () => { renderML(); - expect(screen.getByRole('main')).toHaveFocus(); }); }); @@ -119,27 +114,21 @@ describe('Media library homepage', () => { canCreate: false, canRead: false, }); - renderML(); - expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true'); expect(screen.getByText('Loading content.')).toBeInTheDocument(); }); it('shows a loader while resolving assets', () => { useAssets.mockReturnValueOnce({ isLoading: true }); - renderML(); - expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true'); expect(screen.getByText('Loading content.')).toBeInTheDocument(); }); it('shows a loader while resolving folders', () => { useFolders.mockReturnValueOnce({ isLoading: true }); - renderML(); - expect(screen.getByRole('main').getAttribute('aria-busy')).toBe('true'); expect(screen.getByText('Loading content.')).toBeInTheDocument(); }); @@ -150,7 +139,7 @@ describe('Media library homepage', () => { it('shows the filters dropdown when the user is allowed to read', () => { renderML(); - expect(screen.getByText('app.utils.filters')).toBeInTheDocument(); + expect(screen.getByText('Filters')).toBeInTheDocument(); }); it('hides the filters dropdown when the user is not allowed to read', () => { @@ -159,9 +148,7 @@ describe('Media library homepage', () => { canRead: false, canCreate: false, }); - renderML(); - expect(screen.queryByText('app.utils.filters')).not.toBeInTheDocument(); }); }); @@ -169,7 +156,6 @@ describe('Media library homepage', () => { describe('sort by', () => { it('shows the sort by dropdown when the user is allowed to read', () => { renderML(); - expect(screen.getByText('Sort by')).toBeInTheDocument(); }); @@ -179,30 +165,28 @@ describe('Media library homepage', () => { canRead: false, canCreate: false, }); - renderML(); - expect(screen.queryByText('Sort by')).not.toBeInTheDocument(); }); [ - ['Most recent uploads', 'createdAt:DESC'], - ['Oldest uploads', 'createdAt:ASC'], - ['Alphabetical order (A to Z)', 'name:ASC'], - ['Reverse alphabetical order (Z to A)', 'name:DESC'], - ['Most recent updates', 'updatedAt:DESC'], - ['Oldest updates', 'updatedAt:ASC'], - ].forEach(([label, sortKey]) => { - it(`modifies the URL with the according params: ${label} ${sortKey}`, async () => { + 'createdAt:DESC', + 'createdAt:ASC', + 'name:ASC', + 'name:DESC', + 'updatedAt:DESC', + 'updatedAt:ASC', + ].forEach(sortKey => { + it(`modifies the URL with the according params: ${sortKey}`, async () => { const setQueryMock = jest.fn(); useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: {} }, setQueryMock]); renderML(); fireEvent.mouseDown(screen.getByText('Sort by')); - await waitFor(() => expect(screen.getByText(label)).toBeInTheDocument()); - fireEvent.mouseDown(screen.getByText(label)); - await waitFor(() => expect(screen.queryByText(label)).not.toBeInTheDocument()); + await waitFor(() => expect(screen.getByText(sortKey)).toBeInTheDocument()); + fireEvent.mouseDown(screen.getByText(sortKey)); + await waitFor(() => expect(screen.queryByText(sortKey)).not.toBeInTheDocument()); expect(setQueryMock).toBeCalledWith({ sort: sortKey }); }); @@ -210,7 +194,13 @@ describe('Media library homepage', () => { }); describe('select all', () => { - it('is not visible if there are not folders and assets', () => { + it('shows the select all button when the user is allowed to update', () => { + renderML(); + + expect(screen.getByLabelText('Select all folders & assets')).toBeInTheDocument(); + }); + + it('hides the select all if there are not folders and assets', () => { useAssets.mockReturnValueOnce({ isLoading: false, error: null, @@ -221,17 +211,10 @@ describe('Media library homepage', () => { isLoading: false, error: null, }); + renderML(); - expect( - screen.queryByText('There are no elements with the applied filters') - ).not.toBeInTheDocument(); - }); - - it('shows the select all button when the user is allowed to update', () => { - renderML(); - - expect(screen.getByLabelText('Select all assets')).toBeInTheDocument(); + expect(screen.queryByLabelText('Select all assets')).not.toBeInTheDocument(); }); it('hides the select all button when the user is not allowed to update', () => { @@ -241,9 +224,7 @@ describe('Media library homepage', () => { canCreate: true, canUpdate: false, }); - renderML(); - expect(screen.queryByLabelText('Select all assets')).not.toBeInTheDocument(); }); }); @@ -255,9 +236,7 @@ describe('Media library homepage', () => { canRead: false, canCreate: false, }); - renderML(); - await waitFor(() => expect(screen.queryByText(`Add new assets`)).not.toBeInTheDocument()); }); @@ -267,9 +246,7 @@ describe('Media library homepage', () => { canRead: true, canCreate: true, }); - renderML(); - await waitFor(() => expect(screen.getByText(`Add new assets`)).toBeInTheDocument()); }); }); @@ -277,7 +254,6 @@ describe('Media library homepage', () => { describe('create folder', () => { it('shows the create button if the user has create permissions', () => { renderML(); - expect(screen.getByText('Add new folder')).toBeInTheDocument(); }); @@ -286,28 +262,39 @@ describe('Media library homepage', () => { isLoading: false, canCreate: false, }); - renderML(); - expect(screen.queryByText('Add new folder')).not.toBeInTheDocument(); }); }); }); describe('content', () => { + it('should show breadcrumbs navigation', () => { + renderML(); + + expect(screen.queryByLabelText('Folders navigation')).toBeInTheDocument(); + }); + + it('should hide breadcrumbs navigation if in root folder', () => { + useFolder.mockReturnValueOnce({ isLoading: false, data: undefined }); + renderML(); + + expect(screen.queryByLabelText('Folders navigation')).not.toBeInTheDocument(); + }); + it('does display empty state upload first assets if no folder or assets', () => { useFolders.mockReturnValueOnce({ data: [], isLoading: false, error: null, }); + useAssets.mockReturnValueOnce({ isLoading: false, error: null, data: {}, }); renderML(); - expect(screen.queryByText('Upload your first assets...')).toBeInTheDocument(); }); @@ -324,7 +311,6 @@ describe('Media library homepage', () => { }); useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]); renderML(); - expect( screen.queryByText('There are no elements with the applied filters') ).toBeInTheDocument(); @@ -338,7 +324,6 @@ describe('Media library homepage', () => { }); useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]); renderML(); - expect(screen.queryByText('Assets')).not.toBeInTheDocument(); }); @@ -350,15 +335,14 @@ describe('Media library homepage', () => { }); useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]); renderML(); - expect(screen.queryByText('Folders')).not.toBeInTheDocument(); }); it('displays folders and folders title', () => { renderML(); - expect(screen.queryByText('Folders')).toBeInTheDocument(); - expect(screen.getByText('Folder 1')).toBeInTheDocument(); + expect(screen.getByText('Folders (1)')).toBeInTheDocument(); + expect(screen.getByText('1 folder, 1 asset')).toBeInTheDocument(); }); it('displays folder with checked checkbox when is selected', () => { @@ -379,13 +363,11 @@ describe('Media library homepage', () => { { selectOne: jest.fn(), selectAll: jest.fn() }, ]); renderML(); - expect(screen.getByTestId('folder-checkbox-1')).toBeChecked(); }); it('doest not displays folder with checked checkbox when is not selected', () => { renderML(); - expect(screen.getByTestId('folder-checkbox-1')).not.toBeChecked(); }); @@ -398,8 +380,8 @@ describe('Media library homepage', () => { renderML(); - expect(screen.queryByText('list.folders.title')).not.toBeInTheDocument(); - expect(screen.queryByText('Folder 1')).not.toBeInTheDocument(); + expect(screen.queryByText('1 folder, 1 asset')).not.toBeInTheDocument(); + expect(screen.queryByText('Folders (1)')).not.toBeInTheDocument(); }); it('does display folders if a search is performed', () => { @@ -407,17 +389,17 @@ describe('Media library homepage', () => { renderML(); - expect(screen.queryByText('Folders')).toBeInTheDocument(); - expect(screen.queryByText('Folder 1')).toBeInTheDocument(); + expect(screen.queryByText('1 folder, 1 asset')).toBeInTheDocument(); + expect(screen.queryByText('Folders (1)')).toBeInTheDocument(); }); - it('does not display folders if the media library is being filtered', () => { + it('does display folders if the media library is being filtered', () => { useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { filters: 'true' } }, jest.fn()]); renderML(); - expect(screen.queryByText('Folders')).toBeInTheDocument(); - expect(screen.queryByText('Folder 1')).toBeInTheDocument(); + expect(screen.queryByText('1 folder, 1 asset')).toBeInTheDocument(); + expect(screen.queryByText('Folders (1)')).toBeInTheDocument(); }); it('does not fetch folders if the current page !== 1', () => { @@ -434,9 +416,7 @@ describe('Media library homepage', () => { }, }); useQueryParams.mockReturnValueOnce([{ rawQuery: '', query: { _q: 'true' } }, jest.fn()]); - renderML(); - expect(useFolders).toHaveBeenCalledWith(expect.objectContaining({ enabled: false })); }); @@ -457,15 +437,12 @@ describe('Media library homepage', () => { { rawQuery: '', query: { _q: '', filters: { $and: { mime: 'audio' } } } }, jest.fn(), ]); - renderML(); - expect(useFolders).toHaveBeenCalledWith(expect.objectContaining({ enabled: false })); }); it('displays assets', () => { renderML(); - expect(screen.getByText('3874873.jpg')).toBeInTheDocument(); }); @@ -475,9 +452,7 @@ describe('Media library homepage', () => { canRead: false, canCreate: false, }); - renderML(); - expect(screen.queryByText('3874873.jpg')).not.toBeInTheDocument(); }); @@ -489,14 +464,11 @@ describe('Media library homepage', () => { results: [], }, }); - useFolders.mockReturnValueOnce({ isLoading: false, data: [], }); - renderML(); - expect(screen.queryByText('Upload your first assets...')).toBeInTheDocument(); }); @@ -510,15 +482,12 @@ describe('Media library homepage', () => { results: [], }, }); - useFolders.mockReturnValueOnce({ isLoading: false, error: null, data: [], }); - renderML(); - expect( screen.queryByText('There are no elements with the applied filters') ).toBeInTheDocument(); diff --git a/packages/core/upload/admin/src/pages/App/tests/__snapshots__/Header.test.js.snap b/packages/core/upload/admin/src/pages/App/tests/__snapshots__/Header.test.js.snap index 1d3d517e9a..c61d161f2b 100644 --- a/packages/core/upload/admin/src/pages/App/tests/__snapshots__/Header.test.js.snap +++ b/packages/core/upload/admin/src/pages/App/tests/__snapshots__/Header.test.js.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header renders 1`] = ` -.c20 { +.c19 { border: 0; -webkit-clip: rect(0 0 0 0); clip: rect(0 0 0 0); @@ -310,12 +310,6 @@ exports[`Header renders 1`] = ` line-height: 1.25; } -.c19 { - color: #666687; - font-size: 1rem; - line-height: 1.5; -} - .c5 { color: #4945ff; font-size: 0.75rem; @@ -435,7 +429,7 @@ exports[`Header renders 1`] = `

    - Media Library - Folder 1 + Media Library

    -

    - 2 folders - 2 assets -

    { + let data = [ + { + id: null, + label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' }, + }, + ]; + + if (folder?.parent?.parent) { + data.push([]); + } + + if (folder?.parent) { + data.push({ + id: folder.parent.id, + label: folder.parent.name, + }); + } + + if (folder) { + data.push({ + id: folder.id, + label: folder.name, + }); + } + + return data; +}; + +export default getBreadcrumbDataML; diff --git a/packages/core/upload/admin/src/utils/getBreadcrumbDataML.js b/packages/core/upload/admin/src/utils/getBreadcrumbDataML.js new file mode 100644 index 0000000000..507261dae0 --- /dev/null +++ b/packages/core/upload/admin/src/utils/getBreadcrumbDataML.js @@ -0,0 +1,35 @@ +import getTrad from './getTrad'; +import getFolderURL from './getFolderURL'; + +const getBreadcrumbDataML = (folder, { pathname, query }) => { + let data = [ + { + id: null, + label: { id: getTrad('plugin.name'), defaultMessage: 'Media Library' }, + href: folder ? getFolderURL(pathname, query) : undefined, + }, + ]; + + if (folder?.parent?.parent) { + data.push([]); + } + + if (folder?.parent) { + data.push({ + id: folder.parent.id, + label: folder.parent.name, + href: getFolderURL(pathname, query, folder.parent), + }); + } + + if (folder) { + data.push({ + id: folder.id, + label: folder.name, + }); + } + + return data; +}; + +export default getBreadcrumbDataML; diff --git a/packages/core/upload/admin/src/utils/getFolderParents.js b/packages/core/upload/admin/src/utils/getFolderParents.js new file mode 100644 index 0000000000..273206e7fb --- /dev/null +++ b/packages/core/upload/admin/src/utils/getFolderParents.js @@ -0,0 +1,24 @@ +import flattenTree from '../components/SelectTree/utils/flattenTree'; + +const getFolderParents = (folders, currentFolderId) => { + const parents = []; + const flatFolders = flattenTree(folders); + const currentFolder = flatFolders.find(folder => folder.value === currentFolderId); + + if (!currentFolder) { + return []; + } + + let { parent } = currentFolder; + + while (parent !== undefined) { + // eslint-disable-next-line no-loop-func + let parentToStore = flatFolders.find(({ value }) => value === parent); + parents.push({ id: parentToStore.value, label: parentToStore.label }); + parent = parentToStore.parent; + } + + return parents.reverse(); +}; + +export default getFolderParents; diff --git a/packages/core/upload/admin/src/utils/getFolderURL.js b/packages/core/upload/admin/src/utils/getFolderURL.js new file mode 100644 index 0000000000..0f5f4c10b9 --- /dev/null +++ b/packages/core/upload/admin/src/utils/getFolderURL.js @@ -0,0 +1,18 @@ +import { stringify } from 'qs'; + +const getFolderURL = (pathname, query, folder) => { + const { _q, ...queryParamsWithoutQ } = query; + const queryParamsString = stringify( + { + ...queryParamsWithoutQ, + folder: folder?.id, + }, + { encode: false } + ); + + // Search query will always fetch the same results + // we remove it here to allow navigating in a folder and see the result of this navigation + return `${pathname}${queryParamsString ? `?${queryParamsString}` : ''}`; +}; + +export default getFolderURL; diff --git a/packages/core/upload/admin/src/utils/index.js b/packages/core/upload/admin/src/utils/index.js index 8b45a3d87f..94fe1b40ec 100644 --- a/packages/core/upload/admin/src/utils/index.js +++ b/packages/core/upload/admin/src/utils/index.js @@ -5,4 +5,8 @@ export { default as getTrad } from './getTrad'; export { default as findRecursiveFolderByValue } from './findRecursiveFolderByValue'; export { default as findRecursiveFolderMetadatas } from './findRecursiveFolderMetadatas'; export { default as containsAssetFilter } from './containsAssetFilter'; +export { default as getBreadcrumbDataML } from './getBreadcrumbDataML'; +export { default as getBreadcrumbDataCM } from './getBreadcrumbDataCM'; +export { default as getFolderURL } from './getFolderURL'; +export { default as getFolderParents } from './getFolderParents'; export * from './formatDuration'; diff --git a/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataCM.test.js b/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataCM.test.js new file mode 100644 index 0000000000..28cc719c39 --- /dev/null +++ b/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataCM.test.js @@ -0,0 +1,86 @@ +import { getBreadcrumbDataCM } from '..'; + +const FIXTURE_FOLDER = { + id: 1, + name: 'first-level', +}; + +describe('getBreadcrumbDataCM', () => { + test('return one item at the root of the media library', () => { + expect(getBreadcrumbDataCM(null)).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + ]); + }); + + test('returns two items for the first level of the media library', () => { + expect(getBreadcrumbDataCM(FIXTURE_FOLDER)).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + { + id: 1, + label: 'first-level', + }, + ]); + }); + + test('returns three items for the second level of the media library', () => { + expect( + getBreadcrumbDataCM({ ...FIXTURE_FOLDER, parent: { id: 2, name: 'second-level' } }) + ).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + + { + id: 2, + label: 'second-level', + }, + + { + id: 1, + label: 'first-level', + }, + ]); + }); + + test('returns four items for the third level of the media library', () => { + expect( + getBreadcrumbDataCM({ + ...FIXTURE_FOLDER, + parent: { id: 2, name: 'second-level', parent: { id: 3, name: 'third-level' } }, + }) + ).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + [], + { + id: 2, + label: 'second-level', + }, + { + id: 1, + label: 'first-level', + }, + ]); + }); +}); diff --git a/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataML.test.js b/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataML.test.js new file mode 100644 index 0000000000..20b6ee534e --- /dev/null +++ b/packages/core/upload/admin/src/utils/tests/getBreadcrumbDataML.test.js @@ -0,0 +1,111 @@ +import { getBreadcrumbDataML } from '..'; + +const FIXTURE_PATHNAME = '/media-library'; +const FIXTURE_QUERY = { + _q: 'search', + some: 'thing', +}; +const FIXTURE_FOLDER = { + id: 1, + name: 'first-level', +}; + +describe('getBreadcrumbDataML', () => { + test('return one item at the root of the media library', () => { + expect( + getBreadcrumbDataML(null, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY }) + ).toStrictEqual([ + { + href: undefined, + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + ]); + }); + + test('returns two items for the first level of the media library', () => { + expect( + getBreadcrumbDataML(FIXTURE_FOLDER, { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY }) + ).toStrictEqual([ + { + href: '/media-library?some=thing', + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + }, + + { + id: 1, + label: 'first-level', + }, + ]); + }); + + test('returns three items for the second level of the media library', () => { + expect( + getBreadcrumbDataML( + { ...FIXTURE_FOLDER, parent: { id: 2, name: 'second-level' } }, + { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY } + ) + ).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + href: '/media-library?some=thing', + }, + + { + id: 2, + label: 'second-level', + href: '/media-library?some=thing&folder=2', + }, + + { + id: 1, + label: 'first-level', + }, + ]); + }); + + test('returns four items for the third level of the media library', () => { + expect( + getBreadcrumbDataML( + { + ...FIXTURE_FOLDER, + parent: { id: 2, name: 'second-level', parent: { id: 3, name: 'third-level' } }, + }, + { pathname: FIXTURE_PATHNAME, query: FIXTURE_QUERY } + ) + ).toStrictEqual([ + { + id: null, + label: { + id: 'upload.plugin.name', + defaultMessage: 'Media Library', + }, + href: '/media-library?some=thing', + }, + + [], + + { + id: 2, + label: 'second-level', + href: '/media-library?some=thing&folder=2', + }, + + { + id: 1, + label: 'first-level', + }, + ]); + }); +}); diff --git a/packages/core/upload/admin/src/utils/tests/getFolderParents.test.js b/packages/core/upload/admin/src/utils/tests/getFolderParents.test.js new file mode 100644 index 0000000000..d8f17cc2bf --- /dev/null +++ b/packages/core/upload/admin/src/utils/tests/getFolderParents.test.js @@ -0,0 +1,69 @@ +import getFolderParents from '../getFolderParents'; + +const FIXTURE_FOLDER_STRUCTURE = [ + { + value: null, + label: 'Media Library', + children: [ + { + value: 1, + label: 'First folder', + children: [ + { + value: 2, + label: 'Second folder', + children: [ + { + value: 3, + label: 'Third folder', + children: [ + { + value: 5, + label: 'Fourth folder', + children: [], + }, + ], + }, + { + value: 4, + label: 'Second folder sibling', + children: [], + }, + ], + }, + ], + }, + ], + }, +]; + +describe('getFolderParents', () => { + test('should return ascendants', () => { + const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 3); + const expected = [ + { id: null, label: 'Media Library' }, + { id: 1, label: 'First folder' }, + { id: 2, label: 'Second folder' }, + ]; + + expect(result).toEqual(expected); + }); + + test('should not return parent siblings', () => { + const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 5); + const expected = [ + { id: null, label: 'Media Library' }, + { id: 1, label: 'First folder' }, + { id: 2, label: 'Second folder' }, + { id: 3, label: 'Third folder' }, + ]; + + expect(result).toEqual(expected); + }); + + test('should return array if current folder id not found', () => { + const result = getFolderParents(FIXTURE_FOLDER_STRUCTURE, 8); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/core/upload/admin/src/utils/tests/getFolderURL.test.js b/packages/core/upload/admin/src/utils/tests/getFolderURL.test.js new file mode 100644 index 0000000000..4c464c37bf --- /dev/null +++ b/packages/core/upload/admin/src/utils/tests/getFolderURL.test.js @@ -0,0 +1,31 @@ +import { getFolderURL } from '..'; + +const FIXTURE_PATHNAME = '/media-library'; +const FIXTURE_QUERY = {}; +const FIXTURE_FOLDER = { + id: 1, +}; + +describe('getFolderURL', () => { + test('returns a path for the root of the media library', () => { + expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY)).toStrictEqual(FIXTURE_PATHNAME); + }); + + test('returns a path for a folder', () => { + expect(getFolderURL(FIXTURE_PATHNAME, FIXTURE_QUERY, FIXTURE_FOLDER)).toStrictEqual( + `${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER.id}` + ); + }); + + test('removes _q query parameter', () => { + expect( + getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, _q: 'search' }, FIXTURE_FOLDER) + ).toStrictEqual(`${FIXTURE_PATHNAME}?folder=${FIXTURE_FOLDER.id}`); + }); + + test('keeps and stringifies query parameter', () => { + expect( + getFolderURL(FIXTURE_PATHNAME, { ...FIXTURE_QUERY, some: 'thing' }, FIXTURE_FOLDER) + ).toStrictEqual(`${FIXTURE_PATHNAME}?some=thing&folder=${FIXTURE_FOLDER.id}`); + }); +}); diff --git a/packages/plugins/graphql/server/bootstrap.js b/packages/plugins/graphql/server/bootstrap.js index 3e233638cc..cf10468464 100644 --- a/packages/plugins/graphql/server/bootstrap.js +++ b/packages/plugins/graphql/server/bootstrap.js @@ -62,7 +62,7 @@ module.exports = async ({ strapi }) => { : ApolloServerPluginLandingPageGraphQLPlayground(), ], - cache: 'bounded' + cache: 'bounded', }; const serverConfig = merge(defaultServerConfig, config('apolloServer')); @@ -114,7 +114,7 @@ module.exports = async ({ strapi }) => { // allow graphql playground to load without authentication if (ctx.request.method === 'GET') return next(); - + return strapi.auth.authenticate(ctx, next); },