From d9156177ff30b370c234de1132582d8bde37d872 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Wed, 2 Dec 2020 15:33:02 +0200 Subject: [PATCH 01/45] feat(media dir nav): navigation through sub directories and breadcrumbs --- .../netlify-cms-backend-github/src/API.ts | 35 +++++++++ .../src/implementation.tsx | 32 ++++++--- .../src/actions/mediaLibrary.ts | 22 ++++-- packages/netlify-cms-core/src/backend.ts | 8 +++ .../components/MediaLibrary/MediaLibrary.js | 31 ++++++-- .../MediaLibrary/MediaLibraryBreadCrumbs.js | 72 +++++++++++++++++++ .../MediaLibrary/MediaLibraryCard.js | 26 ++++--- .../MediaLibrary/MediaLibraryCardGrid.js | 1 + .../MediaLibrary/MediaLibraryModal.js | 12 +++- .../MediaLibrary/MediaLibraryTop.js | 11 +++ .../netlify-cms-core/src/reducers/entries.ts | 5 +- .../src/reducers/mediaLibrary.ts | 13 +++- packages/netlify-cms-core/src/types/redux.ts | 2 + 13 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index d44b7615ee4b..9a9fd66aba96 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -695,6 +695,41 @@ export default class API { } } + async listDirs( + path: string, + { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, + ): Promise<{ type: string; id: string; name: string; path: string; }[]> { + const folder = trim(path, '/'); + try { + const result: Octokit.GitGetTreeResponse = await this.request( + `${repoURL}/git/trees/${branch}:${folder}`, + { + // GitHub API supports recursive=1 for getting the entire recursive tree + // or omitting it to get the non-recursive tree + params: depth > 1 ? { recursive: 1 } : {}, + }, + ); + return ( + result.tree + // filter only files and up to the required depth + .filter(file => file.type === 'tree' && file.path.split('/').length <= depth) + .map(file => ({ + type: file.type, + id: file.sha, + name: basename(file.path), + path: `${folder}/${file.path}`, + })) + ); + } catch (err) { + if (err && err.status === 404) { + console.log('This 404 was expected and handled appropriately.'); + return []; + } else { + throw err; + } + } + } + filterOpenAuthoringBranches = async (branch: string) => { try { const pullRequest = await this.getBranchPullRequest(branch); diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index 1bc18492d1b4..a9370485ddaa 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -69,7 +69,8 @@ export default class GitHub implements Implementation { useOpenAuthoring?: boolean; branch: string; apiRoot: string; - mediaFolder: string; + defaultMediaFolder: string; + currentMediaFolder: string; previewContext: string; token: string | null; squashMerges: boolean; @@ -115,7 +116,8 @@ export default class GitHub implements Implementation { this.squashMerges = config.backend.squash_merges || false; this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; - this.mediaFolder = config.media_folder; + this.defaultMediaFolder = config.media_folder; + this.currentMediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); } @@ -436,14 +438,25 @@ export default class GitHub implements Implementation { .catch(() => ({ file: { path, id: null }, data: '' })); } - getMedia(mediaFolder = this.mediaFolder) { - return this.api!.listFiles(mediaFolder).then(files => - files.map(({ id, name, size, path }) => { + updateMediaFolder(path: string) { + this.currentMediaFolder = path || this.currentMediaFolder; + return this.currentMediaFolder; + } + + getDefaultMediaFolder() { + return this.defaultMediaFolder; + } + + getMedia(currentMediaFolder = this.currentMediaFolder) { + return Promise.all([this.api!.listDirs(currentMediaFolder), this.api!.listFiles(currentMediaFolder)]).then(result => { + return result[0].map(({ id, name, path, type }) => { + return { id, name, displayURL: { id, path }, path, type }; + }).concat(result[1].map(({ id, name, size, path, type }) => { // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls // for private repositories - return { id, name, size, displayURL: { id, path }, path }; - }), - ); + return { id, name, size, displayURL: { id, path }, path, type }; + })); + }) } async getMediaFile(path: string) { @@ -484,9 +497,12 @@ export default class GitHub implements Implementation { } async persistMedia(mediaFile: AssetProxy, options: PersistOptions) { + console.log({...mediaFile}); try { await this.api!.persistFiles([], [mediaFile], options); const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string }; + console.log(mediaFile); + console.log(path) const displayURL = URL.createObjectURL(fileObj); return { id: sha, diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 4a013a50db7d..3865fc2322f3 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -44,6 +44,8 @@ export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE'; export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST'; export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS'; export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE'; +export const MEDIA_FOLDER_UPDATE = 'MEDIA_FOLDER_UPDATE'; +export const DEFAULT_MEDIA_FOLDER = 'DEFAULT_MEDIA_FOLDER'; export function createMediaLibrary(instance: MediaLibraryInstance) { const api = { @@ -131,9 +133,19 @@ export function removeInsertedMedia(controlID: string) { return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } }; } -export function loadMedia( - opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {}, -) { +export function updateMediaFolder(selectedMediaFolder: string) { + return (dispatch: ThunkDispatch, getState: () => State) => { + const state = getState(); + const backend = currentBackend(state.config); + const currentMediaFolder = backend.updateMediaFolder(selectedMediaFolder); + const defaultMediaFolder = backend.getDefaultMediaFolder(); + dispatch({ type: MEDIA_FOLDER_UPDATE, payload: { currentMediaFolder, defaultMediaFolder } }); + }; +} + +export function loadMedia( + opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {} + ) { const { delay = 0, query = '', page = 1, privateUpload } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); @@ -157,7 +169,6 @@ export function loadMedia( } } dispatch(mediaLoading(page)); - const loadFunction = () => backend .getMedia() @@ -262,6 +273,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); + console.log(state); const path = selectMediaFilePath(state.config, collection, entry, fileName, field); assetProxy = createAssetProxy({ file, @@ -287,6 +299,8 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { }); return dispatch(addDraftEntryMediaFile(mediaFile)); } else { + console.log('backend.persistMedia'); + console.log(state.config); mediaFile = await backend.persistMedia(state.config, assetProxy); } diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 69c02de041ef..15ef1e1d50e3 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -805,6 +805,14 @@ export class Backend { return entryValue; } + updateMediaFolder(path: string) { + return this.implementation.updateMediaFolder(path); + } + + getDefaultMediaFolder() { + return this.implementation.getDefaultMediaFolder(); + } + getMedia() { return this.implementation.getMedia(); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index dc8f0cedce7b..7802f5722550 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -13,6 +13,7 @@ import { insertMedia as insertMediaAction, loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, + updateMediaFolder as updateMediaFolderAction, } from 'Actions/mediaLibrary'; import { selectMediaFiles } from 'Reducers/mediaLibrary'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; @@ -42,6 +43,7 @@ class MediaLibrary extends React.Component { privateUpload: PropTypes.bool, config: ImmutablePropTypes.map, loadMedia: PropTypes.func.isRequired, + updateMediaFolder: PropTypes.func.isRequired, dynamicSearchQuery: PropTypes.string, page: PropTypes.number, persistMedia: PropTypes.func.isRequired, @@ -65,6 +67,7 @@ class MediaLibrary extends React.Component { }; componentDidMount() { + this.props.updateMediaFolder(); this.props.loadMedia(); } @@ -109,7 +112,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, type }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -121,11 +124,12 @@ class MediaLibrary extends React.Component { queryOrder, displayURL, draft, + isDirectory: type === 'tree', isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; }); - + /** * Get the sort order for use with `lodash.orderBy`, and always add the * `queryOrder` sort as the lowest priority sort order. @@ -144,10 +148,20 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - this.setState({ selectedFile }); + if (asset.isViewableImage) { + const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; + this.setState({ selectedFile }); + } else { + this.props.updateMediaFolder(asset.path); + this.props.loadMedia(); + } }; + handleBreadcrumbClick = path => { + this.props.updateMediaFolder(`${path}`); + this.props.loadMedia(); + } + /** * Upload a file. */ @@ -304,9 +318,10 @@ class MediaLibrary extends React.Component { isPaginating, privateUpload, displayURLs, + currentMediaFolder, + defaultMediaFolder, t, } = this.props; - return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} + handleBreadcrumbClick={this.handleBreadcrumbClick} handleLoadMore={this.handleLoadMore} displayURLs={displayURLs} loadDisplayURL={this.loadDisplayURL} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} t={t} /> ); @@ -364,6 +382,8 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), + currentMediaFolder: mediaLibrary.get('currentMediaFolder'), + defaultMediaFolder: mediaLibrary.get('defaultMediaFolder'), field, }; return { ...mediaLibraryProps }; @@ -376,6 +396,7 @@ const mapDispatchToProps = { insertMedia: insertMediaAction, loadMediaDisplayURL: loadMediaDisplayURLAction, closeMediaLibrary: closeMediaLibraryAction, + updateMediaFolder: updateMediaFolderAction, }; export default connect(mapStateToProps, mapDispatchToProps)(translate()(MediaLibrary)); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js new file mode 100644 index 000000000000..0e809453fd87 --- /dev/null +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { trim } from 'lodash'; +import styled from '@emotion/styled'; +import { Icon } from 'netlify-cms-ui-default'; + +const BreadCrumbsContainer = styled.div` + display: flex; + justify-content: flex-start; + margin: 20px 0; +` +const BreadCrumbsItem = styled.div` + display: flex; +` + +const BreadCrumbsItemLabel = styled.div` + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + padding: 6px 8px; + background: #eff0f4; + border-radius: 5px; +` + +const BreadCrumbsItemDivider = styled.div` + display: flex; + justify-content: center; + align-items: center; +` + +class MediaLibraryBreadcrumbs extends React.Component { + render() { + const { + handleBreadcrumbClick, + currentMediaFolder, + defaultMediaFolder, + } = this.props; + var hiddenPath = defaultMediaFolder.split('/').slice(0, -1).join('/'); + var currentMediaFolderParts = trim(currentMediaFolder.replace(hiddenPath, ''), '/').split('/'); + var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { + return { + isDefaultMediaDirectory: index === 0, + path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index+ 1).join('/')}`, + label: part + } + }); + + this.BreadCrumbsContent = breadcrumbsArray.map((item, index) => { + return ( + + handleBreadcrumbClick(item.path)}> + {item.isDefaultMediaDirectory ? : item.label} + + / + + ) + }); + return ( + + {this.BreadCrumbsContent} + + ); + } +} + +MediaLibraryBreadcrumbs.propTypes = { + handleBreadcrumbClick: PropTypes.func.isRequired, + currentMediaFolder: PropTypes.string, +}; + +export default MediaLibraryBreadcrumbs; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 6d61119ae0d9..819a2ff88d22 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import styled from '@emotion/styled'; -import { colors, borders, lengths, shadows, effects } from 'netlify-cms-ui-default'; +import { colors, borders, lengths, shadows, effects, Icon } from 'netlify-cms-ui-default'; const IMAGE_HEIGHT = 160; @@ -77,8 +77,19 @@ class MediaLibraryCard extends React.Component { type, isViewableImage, isDraft, + isDirectory, } = this.props; const url = displayURL.get('url'); + var cardImageWrapper = ( + {isDraft ? {draftText} : null} + {url && isViewableImage ? ( + + ) : ( + {type} + )} + ); + var cardDirectoryWrapper = ; + var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; return ( - - {isDraft ? {draftText} : null} - {url && isViewableImage ? ( - - ) : ( - {type} - )} - + {previewElement} {text} ); } componentDidMount() { - const { displayURL, loadDisplayURL } = this.props; - if (!displayURL.get('url')) { + const { displayURL, loadDisplayURL, isViewableImage } = this.props; + if (!displayURL.get('url') && isViewableImage) { loadDisplayURL(); } } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index aa8ceefe01a8..5a42d68b463d 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -58,6 +58,7 @@ const CardWrapper = props => { loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} + isDirectory={file.isDirectory} /> ); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index 301094850e4b..160867ece906 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -26,7 +26,7 @@ const cardOutsideWidth = `300px`; const StyledModal = styled(Modal)` display: grid; - grid-template-rows: 120px auto; + grid-template-rows: 170px auto; width: calc(${cardOutsideWidth} + 20px); background-color: ${props => props.isPrivate && colors.grayDark}; @@ -87,10 +87,13 @@ const MediaLibraryModal = ({ handleDownload, setScrollContainerRef, handleAssetClick, + handleBreadcrumbClick, handleLoadMore, loadDisplayURL, displayURLs, t, + currentMediaFolder, + defaultMediaFolder, }) => { const filteredFiles = forImage ? handleFilter(files) : files; const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles; @@ -128,7 +131,11 @@ const MediaLibraryModal = ({ hasSelection={hasSelection} isPersisting={isPersisting} isDeleting={isDeleting} - /> + handleBreadcrumbClick={handleBreadcrumbClick} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} + /> + {!shouldShowEmptyMessage ? null : ( )} @@ -189,6 +196,7 @@ MediaLibraryModal.propTypes = { handleInsert: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, + handleBreadcrumbClick: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, loadDisplayURL: PropTypes.func.isRequired, t: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index c2cec3e2006c..9d5f6f04970b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import MediaLibrarySearch from './MediaLibrarySearch'; import MediaLibraryHeader from './MediaLibraryHeader'; +import MediaLibraryBreadcrumbs from './MediaLibraryBreadCrumbs'; import { UploadButton, DeleteButton, DownloadButton, InsertButton } from './MediaLibraryButtons'; const LibraryTop = styled.div` @@ -37,6 +38,9 @@ const MediaLibraryTop = ({ hasSelection, isPersisting, isDeleting, + handleBreadcrumbClick, + currentMediaFolder, + defaultMediaFolder, }) => { const shouldShowButtonLoader = isPersisting || isDeleting; const uploadEnabled = !shouldShowButtonLoader; @@ -100,6 +104,13 @@ const MediaLibraryTop = ({ )} + + + ); }; diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 70e1fa1edaae..e90be2413444 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -762,9 +762,10 @@ export const selectMediaFilePath = ( if (isAbsolutePath(mediaPath)) { return mediaPath; } - + console.log('mediaPath'); + console.log(mediaPath); const mediaFolder = selectMediaFolder(config, collection, entryMap, field); - + console.log(mediaFolder); return join(mediaFolder, basename(mediaPath)); }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index b72196d19055..e289872444d8 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -18,6 +18,8 @@ import { MEDIA_DISPLAY_URL_REQUEST, MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE, + MEDIA_FOLDER_UPDATE, + DEFAULT_MEDIA_FOLDER, } from '../actions/mediaLibrary'; import { selectEditingDraft, selectMediaFolder } from './entries'; import { selectIntegration } from './'; @@ -209,6 +211,16 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => .deleteIn([...displayURLPath, 'url']) ); } + + case MEDIA_FOLDER_UPDATE: { + // zzz not sure what this is doing? + return state.withMutations(map => { + map.set('currentMediaFolder', action.payload.currentMediaFolder); + map.set('defaultMediaFolder', action.payload.defaultMediaFolder); + map.set('isLoading', false); + }); + } + default: return state; } @@ -233,7 +245,6 @@ export function selectMediaFiles(state: State, field?: EntryField) { } else { files = mediaLibrary.get('files') || []; } - return files; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index f4487cd3991c..4efcb45f38ba 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -440,6 +440,8 @@ export interface MediaLibraryAction extends Action { payload: MediaLibraryInstance & { controlID: string; forImage: boolean; + currentMediaFolder: string, + defaultMediaFolder: string, privateUpload: boolean; config: Map; field?: EntryField; From b712a7e8e9747e6e1e17016029e2dfaf82c19a14 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 6 Dec 2020 19:50:26 +0100 Subject: [PATCH 02/45] refactor: simplify code by listing files recursively --- .../netlify-cms-backend-github/src/API.ts | 63 ++++++----------- .../src/GraphQLAPI.ts | 50 ++++++++------ .../src/__tests__/GraphQLAPI.spec.js | 67 ++++++++++++++++++- .../src/implementation.tsx | 48 ++++++------- .../src/actions/mediaLibrary.ts | 22 ++---- packages/netlify-cms-core/src/backend.ts | 8 --- .../components/MediaLibrary/MediaLibrary.js | 42 ++++++------ .../MediaLibrary/MediaLibraryBreadCrumbs.js | 35 +++++----- .../MediaLibrary/MediaLibraryCard.js | 20 +++--- .../MediaLibrary/MediaLibraryModal.js | 4 +- .../netlify-cms-core/src/reducers/entries.ts | 5 +- .../src/reducers/mediaLibrary.ts | 13 +--- packages/netlify-cms-core/src/types/redux.ts | 2 - 13 files changed, 192 insertions(+), 187 deletions(-) diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index 9a9fd66aba96..ffa757b6795c 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -55,8 +55,13 @@ export interface Config { cmsLabelPrefix: string; } +export enum TreeFileType { + TREE = 'tree', + BLOB = 'blob', +} + interface TreeFile { - type: 'blob' | 'tree'; + type: TreeFileType; sha: string; path: string; raw?: string; @@ -383,7 +388,9 @@ export default class API { this.request(`${this.repoURL}/git/trees`, { method: 'POST', body: JSON.stringify({ - tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }], + tree: [ + { path: 'README.md', mode: '100644', type: TreeFileType.BLOB, sha: item.sha }, + ], }), }), ) @@ -661,8 +668,8 @@ export default class API { async listFiles( path: string, - { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, - ): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> { + { repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {}, + ): Promise<{ type: TreeFileType; id: string; name: string; path: string; size: number }[]> { const folder = trim(path, '/'); try { const result: Octokit.GitGetTreeResponse = await this.request( @@ -676,9 +683,12 @@ export default class API { return ( result.tree // filter only files and up to the required depth - .filter(file => file.type === 'blob' && file.path.split('/').length <= depth) + .filter( + file => + types.includes(file.type as TreeFileType) && file.path.split('/').length <= depth, + ) .map(file => ({ - type: file.type, + type: file.type as TreeFileType, id: file.sha, name: basename(file.path), path: `${folder}/${file.path}`, @@ -695,41 +705,6 @@ export default class API { } } - async listDirs( - path: string, - { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, - ): Promise<{ type: string; id: string; name: string; path: string; }[]> { - const folder = trim(path, '/'); - try { - const result: Octokit.GitGetTreeResponse = await this.request( - `${repoURL}/git/trees/${branch}:${folder}`, - { - // GitHub API supports recursive=1 for getting the entire recursive tree - // or omitting it to get the non-recursive tree - params: depth > 1 ? { recursive: 1 } : {}, - }, - ); - return ( - result.tree - // filter only files and up to the required depth - .filter(file => file.type === 'tree' && file.path.split('/').length <= depth) - .map(file => ({ - type: file.type, - id: file.sha, - name: basename(file.path), - path: `${folder}/${file.path}`, - })) - ); - } catch (err) { - if (err && err.status === 404) { - console.log('This 404 was expected and handled appropriately.'); - return []; - } else { - throw err; - } - } - } - filterOpenAuthoringBranches = async (branch: string) => { try { const pullRequest = await this.getBranchPullRequest(branch); @@ -1404,7 +1379,7 @@ export default class API { const entry = { path: trimStart(file.path, '/'), mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: file.sha, } as TreeEntry; @@ -1426,14 +1401,14 @@ export default class API { tree.push({ path: file.path, mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: null, }); // create in new path tree.push({ path: file.path.replace(sourceDir, destDir), mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: file.path === from ? sha : file.id, }); } diff --git a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts index c14048b913dd..8f7e32515336 100644 --- a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts +++ b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts @@ -18,7 +18,14 @@ import { } from 'netlify-cms-lib-util'; import { trim, trimStart } from 'lodash'; import introspectionQueryResultData from './fragmentTypes'; -import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API'; +import API, { + Config, + BlobArgs, + API_NAME, + PullRequestState, + MOCK_PULL_REQUEST, + TreeFileType, +} from './API'; import * as queries from './queries'; import * as mutations from './mutations'; import { GraphQLError } from 'graphql'; @@ -35,7 +42,7 @@ interface TreeEntry { object?: { entries: TreeEntry[]; }; - type: 'blob' | 'tree'; + type: TreeFileType; name: string; sha: string; blob?: { @@ -47,7 +54,7 @@ interface TreeFile { path: string; id: string; size: number; - type: string; + type: TreeFileType; name: string; } @@ -337,22 +344,24 @@ export default class GraphQLAPI extends API { } } - getAllFiles(entries: TreeEntry[], path: string) { + getAllFiles(entries: TreeEntry[], path: string, types: TreeFileType[]) { const allFiles: TreeFile[] = entries.reduce((acc, item) => { - if (item.type === 'tree') { + const itemToAdd = { + name: item.name, + type: item.type, + id: item.sha, + path: `${path}/${item.name}`, + size: item.blob ? item.blob.size : 0, + }; + if (item.type === TreeFileType.BLOB) { + return [...acc, itemToAdd]; + } + + if (item.type === TreeFileType.TREE) { const entries = item.object?.entries || []; - return [...acc, ...this.getAllFiles(entries, `${path}/${item.name}`)]; - } else if (item.type === 'blob') { - return [ - ...acc, - { - name: item.name, - type: item.type, - id: item.sha, - path: `${path}/${item.name}`, - size: item.blob ? item.blob.size : 0, - }, - ]; + const currentDir = types.includes(TreeFileType.TREE) ? [itemToAdd] : []; + const subFiles = this.getAllFiles(entries, `${path}/${item.name}`, types); + return [...acc, ...currentDir, ...subFiles]; } return acc; @@ -360,7 +369,10 @@ export default class GraphQLAPI extends API { return allFiles; } - async listFiles(path: string, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}) { + async listFiles( + path: string, + { repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {}, + ) { const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL); const folder = trim(path, '/'); const { data } = await this.query({ @@ -369,7 +381,7 @@ export default class GraphQLAPI extends API { }); if (data.repository.object) { - const allFiles = this.getAllFiles(data.repository.object.entries, folder); + const allFiles = this.getAllFiles(data.repository.object.entries, folder, types); return allFiles; } else { return []; diff --git a/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js b/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js index bb1e77ae1ac6..ae5cbdcc987f 100644 --- a/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js +++ b/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js @@ -41,7 +41,7 @@ describe('github GraphQL API', () => { ]; const path = 'posts'; - expect(api.getAllFiles(entries, path)).toEqual([ + expect(api.getAllFiles(entries, path, ['blob'])).toEqual([ { name: 'post-1.md', id: 'sha-1', @@ -65,5 +65,70 @@ describe('github GraphQL API', () => { }, ]); }); + + it('should should return directories when types includes `tree`', () => { + const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' }); + const entries = [ + { + name: 'post-1.md', + sha: 'sha-1', + type: 'blob', + blob: { size: 1 }, + }, + { + name: 'post-2.md', + sha: 'sha-2', + type: 'blob', + blob: { size: 2 }, + }, + { + name: '2019', + sha: 'dir-sha', + type: 'tree', + object: { + entries: [ + { + name: 'nested-post.md', + sha: 'nested-post-sha', + type: 'blob', + blob: { size: 3 }, + }, + ], + }, + }, + ]; + const path = 'posts'; + + expect(api.getAllFiles(entries, path, ['blob', 'tree'])).toEqual([ + { + name: 'post-1.md', + id: 'sha-1', + type: 'blob', + size: 1, + path: 'posts/post-1.md', + }, + { + name: 'post-2.md', + id: 'sha-2', + type: 'blob', + size: 2, + path: 'posts/post-2.md', + }, + { + name: '2019', + id: 'dir-sha', + type: 'tree', + size: 0, + path: 'posts/2019', + }, + { + name: 'nested-post.md', + id: 'nested-post-sha', + type: 'blob', + size: 3, + path: 'posts/2019/nested-post.md', + }, + ]); + }); }); }); diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index a9370485ddaa..c28f464b5c2a 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -34,7 +34,7 @@ import { } from 'netlify-cms-lib-util'; import AuthenticationPage from './AuthenticationPage'; import { Octokit } from '@octokit/rest'; -import API, { API_NAME } from './API'; +import API, { API_NAME, TreeFileType } from './API'; import GraphQLAPI from './GraphQLAPI'; type GitHubUser = Octokit.UsersGetAuthenticatedResponse; @@ -69,8 +69,7 @@ export default class GitHub implements Implementation { useOpenAuthoring?: boolean; branch: string; apiRoot: string; - defaultMediaFolder: string; - currentMediaFolder: string; + mediaFolder: string; previewContext: string; token: string | null; squashMerges: boolean; @@ -116,8 +115,7 @@ export default class GitHub implements Implementation { this.squashMerges = config.backend.squash_merges || false; this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; - this.defaultMediaFolder = config.media_folder; - this.currentMediaFolder = config.media_folder; + this.mediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); } @@ -438,25 +436,24 @@ export default class GitHub implements Implementation { .catch(() => ({ file: { path, id: null }, data: '' })); } - updateMediaFolder(path: string) { - this.currentMediaFolder = path || this.currentMediaFolder; - return this.currentMediaFolder; - } - - getDefaultMediaFolder() { - return this.defaultMediaFolder; - } - - getMedia(currentMediaFolder = this.currentMediaFolder) { - return Promise.all([this.api!.listDirs(currentMediaFolder), this.api!.listFiles(currentMediaFolder)]).then(result => { - return result[0].map(({ id, name, path, type }) => { - return { id, name, displayURL: { id, path }, path, type }; - }).concat(result[1].map(({ id, name, size, path, type }) => { - // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls - // for private repositories - return { id, name, size, displayURL: { id, path }, path, type }; - })); - }) + async getMedia(mediaFolder = this.mediaFolder) { + const files = await this.api!.listFiles(mediaFolder, { + types: [TreeFileType.BLOB, TreeFileType.TREE], + depth: 100, + }); + const withDisplayUrls = files.map(({ id, name, size, path, type }) => { + // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls + // for private repositories + return { + id, + name, + size, + displayURL: { id, path }, + path, + isDirectory: type === TreeFileType.TREE, + }; + }); + return withDisplayUrls; } async getMediaFile(path: string) { @@ -497,12 +494,9 @@ export default class GitHub implements Implementation { } async persistMedia(mediaFile: AssetProxy, options: PersistOptions) { - console.log({...mediaFile}); try { await this.api!.persistFiles([], [mediaFile], options); const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string }; - console.log(mediaFile); - console.log(path) const displayURL = URL.createObjectURL(fileObj); return { id: sha, diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 3865fc2322f3..4a013a50db7d 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -44,8 +44,6 @@ export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE'; export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST'; export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS'; export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE'; -export const MEDIA_FOLDER_UPDATE = 'MEDIA_FOLDER_UPDATE'; -export const DEFAULT_MEDIA_FOLDER = 'DEFAULT_MEDIA_FOLDER'; export function createMediaLibrary(instance: MediaLibraryInstance) { const api = { @@ -133,19 +131,9 @@ export function removeInsertedMedia(controlID: string) { return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } }; } -export function updateMediaFolder(selectedMediaFolder: string) { - return (dispatch: ThunkDispatch, getState: () => State) => { - const state = getState(); - const backend = currentBackend(state.config); - const currentMediaFolder = backend.updateMediaFolder(selectedMediaFolder); - const defaultMediaFolder = backend.getDefaultMediaFolder(); - dispatch({ type: MEDIA_FOLDER_UPDATE, payload: { currentMediaFolder, defaultMediaFolder } }); - }; -} - -export function loadMedia( - opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {} - ) { +export function loadMedia( + opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {}, +) { const { delay = 0, query = '', page = 1, privateUpload } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); @@ -169,6 +157,7 @@ export function loadMedia( } } dispatch(mediaLoading(page)); + const loadFunction = () => backend .getMedia() @@ -273,7 +262,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - console.log(state); const path = selectMediaFilePath(state.config, collection, entry, fileName, field); assetProxy = createAssetProxy({ file, @@ -299,8 +287,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { }); return dispatch(addDraftEntryMediaFile(mediaFile)); } else { - console.log('backend.persistMedia'); - console.log(state.config); mediaFile = await backend.persistMedia(state.config, assetProxy); } diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 15ef1e1d50e3..69c02de041ef 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -805,14 +805,6 @@ export class Backend { return entryValue; } - updateMediaFolder(path: string) { - return this.implementation.updateMediaFolder(path); - } - - getDefaultMediaFolder() { - return this.implementation.getDefaultMediaFolder(); - } - getMedia() { return this.implementation.getMedia(); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 7802f5722550..6c1410633dc2 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -13,9 +13,9 @@ import { insertMedia as insertMediaAction, loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, - updateMediaFolder as updateMediaFolderAction, } from 'Actions/mediaLibrary'; import { selectMediaFiles } from 'Reducers/mediaLibrary'; +import { dirname } from 'path'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; /** @@ -43,13 +43,13 @@ class MediaLibrary extends React.Component { privateUpload: PropTypes.bool, config: ImmutablePropTypes.map, loadMedia: PropTypes.func.isRequired, - updateMediaFolder: PropTypes.func.isRequired, dynamicSearchQuery: PropTypes.string, page: PropTypes.number, persistMedia: PropTypes.func.isRequired, deleteMedia: PropTypes.func.isRequired, insertMedia: PropTypes.func.isRequired, closeMediaLibrary: PropTypes.func.isRequired, + defaultMediaFolder: PropTypes.string.isRequired, t: PropTypes.func.isRequired, }; @@ -67,7 +67,6 @@ class MediaLibrary extends React.Component { }; componentDidMount() { - this.props.updateMediaFolder(); this.props.loadMedia(); } @@ -112,7 +111,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, type }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -124,12 +123,12 @@ class MediaLibrary extends React.Component { queryOrder, displayURL, draft, - isDirectory: type === 'tree', + isDirectory, isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; }); - + /** * Get the sort order for use with `lodash.orderBy`, and always add the * `queryOrder` sort as the lowest priority sort order. @@ -148,19 +147,17 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - if (asset.isViewableImage) { + if (asset.isDirectory) { + this.setState({ currentMediaFolder: asset.path }); + } else { const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; this.setState({ selectedFile }); - } else { - this.props.updateMediaFolder(asset.path); - this.props.loadMedia(); } }; - handleBreadcrumbClick = path => { - this.props.updateMediaFolder(`${path}`); - this.props.loadMedia(); - } + handleBreadcrumbClick = currentMediaFolder => { + this.setState({ currentMediaFolder }); + }; /** * Upload a file. @@ -318,15 +315,18 @@ class MediaLibrary extends React.Component { isPaginating, privateUpload, displayURLs, - currentMediaFolder, defaultMediaFolder, t, } = this.props; + + const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; + const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); + return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} handleBreadcrumbClick={this.handleBreadcrumbClick} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} handleLoadMore={this.handleLoadMore} displayURLs={displayURLs} loadDisplayURL={this.loadDisplayURL} - currentMediaFolder={currentMediaFolder} - defaultMediaFolder={defaultMediaFolder} t={t} /> ); @@ -363,7 +363,7 @@ class MediaLibrary extends React.Component { } const mapStateToProps = state => { - const { mediaLibrary } = state; + const { mediaLibrary, config } = state; const field = mediaLibrary.get('field'); const mediaLibraryProps = { isVisible: mediaLibrary.get('isVisible'), @@ -382,9 +382,8 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), - currentMediaFolder: mediaLibrary.get('currentMediaFolder'), - defaultMediaFolder: mediaLibrary.get('defaultMediaFolder'), field, + defaultMediaFolder: config.get('media_folder'), }; return { ...mediaLibraryProps }; }; @@ -396,7 +395,6 @@ const mapDispatchToProps = { insertMedia: insertMediaAction, loadMediaDisplayURL: loadMediaDisplayURLAction, closeMediaLibrary: closeMediaLibraryAction, - updateMediaFolder: updateMediaFolderAction, }; export default connect(mapStateToProps, mapDispatchToProps)(translate()(MediaLibrary)); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 0e809453fd87..22e488675a4b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -8,10 +8,10 @@ const BreadCrumbsContainer = styled.div` display: flex; justify-content: flex-start; margin: 20px 0; -` +`; const BreadCrumbsItem = styled.div` display: flex; -` +`; const BreadCrumbsItemLabel = styled.div` display: flex; @@ -21,29 +21,28 @@ const BreadCrumbsItemLabel = styled.div` padding: 6px 8px; background: #eff0f4; border-radius: 5px; -` +`; const BreadCrumbsItemDivider = styled.div` display: flex; justify-content: center; align-items: center; -` +`; class MediaLibraryBreadcrumbs extends React.Component { render() { - const { - handleBreadcrumbClick, - currentMediaFolder, - defaultMediaFolder, - } = this.props; - var hiddenPath = defaultMediaFolder.split('/').slice(0, -1).join('/'); + const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder } = this.props; + var hiddenPath = defaultMediaFolder + .split('/') + .slice(0, -1) + .join('/'); var currentMediaFolderParts = trim(currentMediaFolder.replace(hiddenPath, ''), '/').split('/'); var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { return { isDefaultMediaDirectory: index === 0, - path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index+ 1).join('/')}`, - label: part - } + path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index + 1).join('/')}`, + label: part, + }; }); this.BreadCrumbsContent = breadcrumbsArray.map((item, index) => { @@ -51,16 +50,12 @@ class MediaLibraryBreadcrumbs extends React.Component { handleBreadcrumbClick(item.path)}> {item.isDefaultMediaDirectory ? : item.label} - + / - ) + ); }); - return ( - - {this.BreadCrumbsContent} - - ); + return {this.BreadCrumbsContent}; } } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 819a2ff88d22..3addbc730372 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -80,15 +80,17 @@ class MediaLibraryCard extends React.Component { isDirectory, } = this.props; const url = displayURL.get('url'); - var cardImageWrapper = ( - {isDraft ? {draftText} : null} - {url && isViewableImage ? ( - - ) : ( - {type} - )} - ); - var cardDirectoryWrapper = ; + var cardImageWrapper = ( + + {isDraft ? {draftText} : null} + {url && isViewableImage ? ( + + ) : ( + {type} + )} + + ); + var cardDirectoryWrapper = ; var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; return ( - + /> + {!shouldShowEmptyMessage ? null : ( )} diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index e90be2413444..70e1fa1edaae 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -762,10 +762,9 @@ export const selectMediaFilePath = ( if (isAbsolutePath(mediaPath)) { return mediaPath; } - console.log('mediaPath'); - console.log(mediaPath); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field); - console.log(mediaFolder); + return join(mediaFolder, basename(mediaPath)); }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index e289872444d8..b72196d19055 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -18,8 +18,6 @@ import { MEDIA_DISPLAY_URL_REQUEST, MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE, - MEDIA_FOLDER_UPDATE, - DEFAULT_MEDIA_FOLDER, } from '../actions/mediaLibrary'; import { selectEditingDraft, selectMediaFolder } from './entries'; import { selectIntegration } from './'; @@ -211,16 +209,6 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => .deleteIn([...displayURLPath, 'url']) ); } - - case MEDIA_FOLDER_UPDATE: { - // zzz not sure what this is doing? - return state.withMutations(map => { - map.set('currentMediaFolder', action.payload.currentMediaFolder); - map.set('defaultMediaFolder', action.payload.defaultMediaFolder); - map.set('isLoading', false); - }); - } - default: return state; } @@ -245,6 +233,7 @@ export function selectMediaFiles(state: State, field?: EntryField) { } else { files = mediaLibrary.get('files') || []; } + return files; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 4efcb45f38ba..f4487cd3991c 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -440,8 +440,6 @@ export interface MediaLibraryAction extends Action { payload: MediaLibraryInstance & { controlID: string; forImage: boolean; - currentMediaFolder: string, - defaultMediaFolder: string, privateUpload: boolean; config: Map; field?: EntryField; From 1ce6f502a6e9bf58bb6125a177115044f2b5bc2f Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 13:42:37 +0200 Subject: [PATCH 03/45] feat(media dir nav): uploading to nested directories works --- dev-test/config.yml | 9 ++++++--- packages/netlify-cms-core/src/actions/mediaLibrary.ts | 10 ++++++---- .../src/components/MediaLibrary/MediaLibrary.js | 5 +++-- packages/netlify-cms-core/src/reducers/entries.ts | 8 ++++++-- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index 2ffe5041ddb5..1ab34824dffb 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -1,10 +1,11 @@ backend: - name: test-repo + name: github + repo: andrew-paterson/netlify-cms-hugo-test-site site_url: https://example.com publish_mode: editorial_workflow -media_folder: assets/uploads +media_folder: static/images collections: # A list of collections the CMS should be able to edit - name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit @@ -13,7 +14,9 @@ collections: # A list of collections the CMS should be able to edit description: > The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection. - folder: '_posts' + folder: 'content/posts' + media_folder: '/static/images/posts' + public_folder: '/images/posts' slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 4a013a50db7d..ec8ad4ef7d00 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -208,15 +208,17 @@ function createMediaFileFromAsset({ } export function persistMedia(file: File, opts: MediaOptions = {}) { - const { privateUpload, field } = opts; + const { privateUpload, field, currentMediaFolder } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, null, 'assetStore'); const files: MediaFile[] = selectMediaFiles(state, field); const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug')); - const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName); - + const entry = state.entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); + const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); + const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path); const editingDraft = selectEditingDraft(state.entryDraft); /** @@ -262,7 +264,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - const path = selectMediaFilePath(state.config, collection, entry, fileName, field); + const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); assetProxy = createAssetProxy({ file, path, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 6c1410633dc2..fd6e12979fc1 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -171,6 +171,8 @@ class MediaLibrary extends React.Component { event.persist(); event.stopPropagation(); event.preventDefault(); + const { defaultMediaFolder } = this.props; + const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const { persistMedia, privateUpload, config, t, field } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; @@ -184,7 +186,7 @@ class MediaLibrary extends React.Component { }), ); } else { - await persistMedia(file, { privateUpload, field }); + await persistMedia(file, { privateUpload, field, currentMediaFolder }); this.setState({ selectedFile: this.props.files[0] }); @@ -321,7 +323,6 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); - return ( { + if (currentMediaFolder) { + return trim(currentMediaFolder, '/') + } const name = 'media_folder'; let mediaFolder = config.get(name); @@ -748,7 +752,6 @@ export const selectMediaFolder = ( : join(collection!.get('folder') as string, DRAFT_MEDIA_FILES); } } - return trim(mediaFolder, '/'); }; @@ -758,12 +761,13 @@ export const selectMediaFilePath = ( entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, + currentMediaFolder: string, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectMediaFolder(config, collection, entryMap, field); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field, currentMediaFolder); return join(mediaFolder, basename(mediaPath)); }; From 0c6fdef2fc5e2de7bdf4f0560683ea00e95ce59c Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 16:18:28 +0200 Subject: [PATCH 04/45] feat(media dir nav): basic ui for multiple asset selection --- .../components/MediaLibrary/MediaLibrary.js | 28 ++++++-- .../MediaLibrary/MediaLibraryCard.js | 68 +++++++++++++++++-- .../MediaLibrary/MediaLibraryCardGrid.js | 15 +++- .../MediaLibrary/MediaLibraryModal.js | 7 +- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index fd6e12979fc1..3593f3318d5a 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -147,12 +147,27 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - if (asset.isDirectory) { - this.setState({ currentMediaFolder: asset.path }); + // console.log('handleAssetClick'); + // if (asset.isDirectory) { + // this.setState({ currentMediaFolder: asset.path }); + // } else { + // const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; + // this.setState({ selectedFile }); + // } + }; + + handleAssetCheckboxChange = (asset, event) => { + console.log('handleAssetCheckboxChange') + let selectedAssets = this.state.selectedAssets || []; + if (event.target.checked) { + selectedAssets.push(asset) } else { - const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - this.setState({ selectedFile }); + selectedAssets = selectedAssets.filter(selectedAsset => { + return selectedAsset.key !== asset.key; + }) } + this.setState({ selectedAssets }); + event.stopPropagation(); }; handleBreadcrumbClick = currentMediaFolder => { @@ -323,11 +338,12 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); + const currentDirFilesOrderedByTreeType = (currentDirFiles || []).filter(file => file.isDirectory).concat((currentDirFiles || []).filter(file => !file.isDirectory)); return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} + handleAssetCheckboxChange={this.handleAssetCheckboxChange} handleBreadcrumbClick={this.handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 3addbc730372..d245c96cd802 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -33,7 +33,7 @@ const CardImageWrapper = styled.div` const CardImage = styled.img` width: 100%; height: ${IMAGE_HEIGHT}px; - object-fit: contain; + object-fit: cover; border-radius: 2px 2px 0 0; `; @@ -46,12 +46,35 @@ const CardFileIcon = styled.div` font-size: 3em; `; -const CardText = styled.p` +const CardDirctoryWrapper = styled.div` + width: 100%; + height: 160px; + object-fit: cover; + border-radius: 2px 2px 0 0; + padding: 1em; + font-size: 3em; + background: #eee; +`; + +const CardText = styled.div` color: ${colors.text}; padding: 8px; - margin-top: 20px; + font-size: 0.8em; + display: flex; + justify-content: center; + flex-direction: column; +`; + +const ObjectName = styled.div` + color: ${colors.text}; + font-weight: 600; overflow-wrap: break-word; line-height: 1.3 !important; + text-align: left; +`; + +const ImageMeta = styled.div` + text-align: left; `; const DraftText = styled.p` @@ -62,6 +85,26 @@ const DraftText = styled.p` border-radius: ${lengths.borderRadius} 0px ${lengths.borderRadius} 0; `; +function readableFileSize(numberOfBytes) { + let readableSize; + let units; + numberOfBytes = numberOfBytes || 0; + if (numberOfBytes >= 0 && numberOfBytes < 1000) { + readableSize = numberOfBytes; + units = ' bytes'; + } else if (numberOfBytes >= 1000 && numberOfBytes < 1000000) { + readableSize = Math.ceil(numberOfBytes / 1000); + units = ' KB'; + } else if (numberOfBytes >= 1000000 && numberOfBytes < 1000000000) { + readableSize = (numberOfBytes / 1000000).toFixed(2); + units = ' MB'; + } else if (numberOfBytes >= 1000000000) { + readableSize = (numberOfBytes / 1000000000).toFixed(2); + units = ' GB'; + } + return readableSize + units; +} + class MediaLibraryCard extends React.Component { render() { const { @@ -69,6 +112,8 @@ class MediaLibraryCard extends React.Component { displayURL, text, onClick, + onAssetCheckboxClick, + onChecked, draftText, width, height, @@ -78,6 +123,7 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, + size } = this.props; const url = displayURL.get('url'); var cardImageWrapper = ( @@ -90,8 +136,12 @@ class MediaLibraryCard extends React.Component { )} ); - var cardDirectoryWrapper = ; - var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; + var cardDirectoryEl = ( + + + + ); + var previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; return ( + {previewElement} - {text} + + {text} + {isViewableImage ? {type} - {readableFileSize(size)} : null} + ); } @@ -119,7 +173,7 @@ MediaLibraryCard.propTypes = { isSelected: PropTypes.bool, displayURL: ImmutablePropTypes.map.isRequired, text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, draftText: PropTypes.string.isRequired, width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 5a42d68b463d..cd8fe4b3aafa 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -16,7 +16,9 @@ const CardWrapper = props => { data: { mediaItems, isSelectedFile, + isSelectedAsset, onAssetClick, + onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -32,7 +34,6 @@ const CardWrapper = props => { return null; } const file = mediaItems[index]; - return (
{ height: style.height - gutter, }} > + onAssetClick(file)} + onChecked={(event) => {onAssetCheckboxClick(file, event)}} isDraft={file.draft} draftText={cardDraftText} width={cardWidth} @@ -59,6 +63,7 @@ const CardWrapper = props => { type={file.type} isViewableImage={file.isViewableImage} isDirectory={file.isDirectory} + size={file.size} />
); @@ -101,7 +106,9 @@ const PaginatedGrid = ({ setScrollContainerRef, mediaItems, isSelectedFile, + isSelectedAsset, onAssetClick, + onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -120,7 +127,8 @@ const PaginatedGrid = ({ {mediaItems.map(file => ( onAssetClick(file)} isDraft={file.draft} @@ -133,6 +141,7 @@ const PaginatedGrid = ({ loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} + onAssetCheckboxClick={onAssetCheckboxClick} /> ))} {!canLoadMore ? null : } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index d7dd2577315f..a984b3e38a24 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -14,7 +14,7 @@ import { colors } from 'netlify-cms-ui-default'; * Responsive styling needs to be overhauled. Current setup requires specifying * widths per breakpoint. */ -const cardWidth = `280px`; +const cardWidth = `240px`; const cardHeight = `240px`; const cardMargin = `10px`; @@ -75,6 +75,7 @@ const MediaLibraryModal = ({ privateUpload, query, selectedFile, + selectedAssets, handleFilter, handleQuery, toTableData, @@ -87,6 +88,7 @@ const MediaLibraryModal = ({ handleDownload, setScrollContainerRef, handleAssetClick, + handleAssetCheckboxChange, handleBreadcrumbClick, handleLoadMore, loadDisplayURL, @@ -143,7 +145,9 @@ const MediaLibraryModal = ({ setScrollContainerRef={setScrollContainerRef} mediaItems={tableData} isSelectedFile={file => selectedFile.key === file.key} + isSelectedAsset={file => (selectedAssets || []).filter(selectedAsset => selectedAsset.key === file.key).length > 0} onAssetClick={handleAssetClick} + onAssetCheckboxClick={handleAssetCheckboxChange} canLoadMore={hasNextPage} onLoadMore={handleLoadMore} isPaginating={isPaginating} @@ -196,6 +200,7 @@ MediaLibraryModal.propTypes = { handleInsert: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, + handleAssetCheckboxChange: PropTypes.func.isRequired, handleBreadcrumbClick: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, loadDisplayURL: PropTypes.func.isRequired, From b6f6a81982456c53ca09c9f180c0e5b5883b2a7d Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 16:41:59 +0200 Subject: [PATCH 05/45] feat(media dir nav): selected assets array cleared when user changes folders --- .../components/MediaLibrary/MediaLibrary.js | 36 ++++++++++--------- .../MediaLibrary/MediaLibraryCard.js | 3 +- .../MediaLibrary/MediaLibraryCardGrid.js | 1 - 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 3593f3318d5a..3be92aa20cd6 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -143,23 +143,15 @@ class MediaLibrary extends React.Component { this.props.closeMediaLibrary(); }; - /** - * Toggle asset selection on click. - */ - handleAssetClick = asset => { - // console.log('handleAssetClick'); - // if (asset.isDirectory) { - // this.setState({ currentMediaFolder: asset.path }); - // } else { - // const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - // this.setState({ selectedFile }); - // } - }; + isSelectedAsset = asset => { + return (this.state.selectedAssets || []).filter(selectedAsset => { + return selectedAsset.key === asset.key + }).length > 0; + } - handleAssetCheckboxChange = (asset, event) => { - console.log('handleAssetCheckboxChange') + updateSelectedAssets = (asset) => { let selectedAssets = this.state.selectedAssets || []; - if (event.target.checked) { + if (!this.isSelectedAsset(asset)) { selectedAssets.push(asset) } else { selectedAssets = selectedAssets.filter(selectedAsset => { @@ -167,11 +159,23 @@ class MediaLibrary extends React.Component { }) } this.setState({ selectedAssets }); + } + + handleAssetCheckboxChange = (asset, event) => { event.stopPropagation(); + this.updateSelectedAssets(asset); + }; + + handleAssetClick = asset => { + if (asset.isDirectory) { + this.setState({ currentMediaFolder: asset.path, selectedAssets: [] }); + } else { + this.updateSelectedAssets(asset); + } }; handleBreadcrumbClick = currentMediaFolder => { - this.setState({ currentMediaFolder }); + this.setState({ currentMediaFolder, selectedAssets: [] }); }; /** diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index d245c96cd802..fdbbb9f616d3 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -112,7 +112,6 @@ class MediaLibraryCard extends React.Component { displayURL, text, onClick, - onAssetCheckboxClick, onChecked, draftText, width, @@ -152,7 +151,7 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - + {previewElement} {text} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index cd8fe4b3aafa..7d2dec8f7ccc 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -47,7 +47,6 @@ const CardWrapper = props => { onAssetClick(file)} From 4089afaffa923121e2524870d2b7232053edd247 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 22 Dec 2020 10:49:16 +0200 Subject: [PATCH 06/45] feat(media dir nav): delete button enabled when at least one media assets selected --- .../MediaLibrary/MediaLibraryCard.js | 62 ++++++++++++++++++- .../MediaLibrary/MediaLibraryModal.js | 2 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index fdbbb9f616d3..effa7a65d552 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -16,7 +16,7 @@ const Card = styled.div` cursor: pointer; overflow: hidden; background-color: ${props => props.isPrivate && colors.textFieldBorder}; - + position: relative; &:focus { outline: none; } @@ -85,6 +85,61 @@ const DraftText = styled.p` border-radius: ${lengths.borderRadius} 0px ${lengths.borderRadius} 0; `; +const radioButtonCheckboxWidthHeight = '24px'; +const inputBorderColor = '#fff'; +const inputBackgroundColor = 'rgba(0, 0, 0, 0.2)'; +const inputHoverBorderColor = '#33BCB0'; +const radioCheckboxSelectedBackgroundColor = '#33BCB0'; +const svgDataRriIconTick = `url('data:image/svg+xml;utf8,')`; +const radioCheckboxSelectedBackgroundPosition = 'center 6px'; +const radioCheckboxSelectedBackgroundSize = '13px'; +const inputFocusBorderColor = '#fff'; + +const AssetCheckbox = styled.input` + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; + cursor: pointer; + left: 0; + top: 0; + width: ${radioButtonCheckboxWidthHeight}; + height: ${radioButtonCheckboxWidthHeight}; + padding: 0 !important; + background: transparent !important; + border-width: 0 !important; + &:after { + border: 1px solid ${inputBorderColor}; + content: ""; + background-color: ${inputBackgroundColor}; + width: ${radioButtonCheckboxWidthHeight}; + height: ${radioButtonCheckboxWidthHeight}; + border-radius: 50%; + position: absolute; + top: 0; + left: 0px; + + } + &:checked:after, &.checked:after { + background: ${radioCheckboxSelectedBackgroundColor} ${svgDataRriIconTick} no-repeat ${radioCheckboxSelectedBackgroundPosition}; + background-size: ${radioCheckboxSelectedBackgroundSize}; + + } + &:focus:after, &:focus:checked:after { + border-color: ${inputFocusBorderColor}; + } + &:hover:after { + border-color ${inputHoverBorderColor}; + } +`; + +const CheckboxContainer = styled.div` + position: absolute; + top: 0; + left: 0; + z-index: 1; +`; + function readableFileSize(numberOfBytes) { let readableSize; let units; @@ -151,12 +206,15 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - + + + {previewElement} {text} {isViewableImage ? {type} - {readableFileSize(size)} : null} +
); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index a984b3e38a24..2a158f14e7af 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -112,7 +112,7 @@ const MediaLibraryModal = ({ (!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) || (!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults')); - const hasSelection = hasMedia && !isEmpty(selectedFile); + const hasSelection = hasMedia && (selectedAssets || []).length > 0; return ( From 391aeded1b40889e28881ae442d86514aa516699 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 22 Dec 2020 12:02:44 +0200 Subject: [PATCH 07/45] feat(media dir nav): synchronous deletion of multiple files --- .../src/implementation.tsx | 1 + .../components/MediaLibrary/MediaLibrary.js | 18 +++++++++++------- .../MediaLibrary/MediaLibraryCard.js | 15 +++++++++------ .../MediaLibrary/MediaLibraryCardGrid.js | 1 + packages/netlify-cms-ui-default/src/Icon.js | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index c28f464b5c2a..fb7191d7c14a 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -451,6 +451,7 @@ export default class GitHub implements Implementation { displayURL: { id, path }, path, isDirectory: type === TreeFileType.TREE, + hasChildren: files.filter(file => file.path.startsWith(path)).length > 1 }; }); return withDisplayUrls; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 3be92aa20cd6..7d07c9f6ca07 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -111,7 +111,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory, hasChildren }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -124,6 +124,7 @@ class MediaLibrary extends React.Component { displayURL, draft, isDirectory, + hasChildren, isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; @@ -230,16 +231,19 @@ class MediaLibrary extends React.Component { /** * Removes the selected file from the backend. */ - handleDelete = () => { - const { selectedFile } = this.state; + handleDelete = async () => { + const { selectedAssets } = this.state; const { files, deleteMedia, privateUpload, t } = this.props; if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) { return; } - const file = files.find(file => selectedFile.key === file.key); - deleteMedia(file, { privateUpload }).then(() => { - this.setState({ selectedFile: {} }); - }); + const filesToDelete = selectedAssets.map(selectedAsset => + files.find(file => selectedAsset.key === file.key) + ) + for (const file of filesToDelete) { + await deleteMedia(file, { privateUpload }); + } + this.setState({ selectedAssets: [] }); }; /** diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index effa7a65d552..507b7f5ef72c 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -177,7 +177,8 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, - size + size, + hasChildren } = this.props; const url = displayURL.get('url'); var cardImageWrapper = ( @@ -192,7 +193,7 @@ class MediaLibraryCard extends React.Component { ); var cardDirectoryEl = ( - + ); var previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; @@ -206,15 +207,17 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - - - + {!hasChildren ? ( + + + + ) : null} + {previewElement} {text} {isViewableImage ? {type} - {readableFileSize(size)} : null} - ); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 7d2dec8f7ccc..76cc837c05f5 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -62,6 +62,7 @@ const CardWrapper = props => { type={file.type} isViewableImage={file.isViewableImage} isDirectory={file.isDirectory} + hasChildren={file.hasChildren} size={file.size} /> diff --git a/packages/netlify-cms-ui-default/src/Icon.js b/packages/netlify-cms-ui-default/src/Icon.js index cd8a8e363a9b..b4f88644d210 100644 --- a/packages/netlify-cms-ui-default/src/Icon.js +++ b/packages/netlify-cms-ui-default/src/Icon.js @@ -49,6 +49,7 @@ const sizes = { small: '18px', medium: '24px', large: '32px', + max: '100%', }; const Icon = ({ type, direction, size = 'medium', className }) => { From a6499c9b8b7579c01c609740fa09bb7e3bc38919 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Wed, 2 Dec 2020 15:33:02 +0200 Subject: [PATCH 08/45] feat(media dir nav): navigation through sub directories and breadcrumbs --- .../netlify-cms-backend-github/src/API.ts | 35 +++++++++ .../src/implementation.tsx | 32 ++++++--- .../src/actions/mediaLibrary.ts | 22 ++++-- packages/netlify-cms-core/src/backend.ts | 8 +++ .../components/MediaLibrary/MediaLibrary.js | 31 ++++++-- .../MediaLibrary/MediaLibraryBreadCrumbs.js | 72 +++++++++++++++++++ .../MediaLibrary/MediaLibraryCard.js | 26 ++++--- .../MediaLibrary/MediaLibraryCardGrid.js | 1 + .../MediaLibrary/MediaLibraryModal.js | 12 +++- .../MediaLibrary/MediaLibraryTop.js | 11 +++ .../netlify-cms-core/src/reducers/entries.ts | 5 +- .../src/reducers/mediaLibrary.ts | 13 +++- packages/netlify-cms-core/src/types/redux.ts | 2 + 13 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index 30cba911013b..e52e2c50e21f 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -684,6 +684,41 @@ export default class API { } } + async listDirs( + path: string, + { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, + ): Promise<{ type: string; id: string; name: string; path: string; }[]> { + const folder = trim(path, '/'); + try { + const result: Octokit.GitGetTreeResponse = await this.request( + `${repoURL}/git/trees/${branch}:${folder}`, + { + // GitHub API supports recursive=1 for getting the entire recursive tree + // or omitting it to get the non-recursive tree + params: depth > 1 ? { recursive: 1 } : {}, + }, + ); + return ( + result.tree + // filter only files and up to the required depth + .filter(file => file.type === 'tree' && file.path.split('/').length <= depth) + .map(file => ({ + type: file.type, + id: file.sha, + name: basename(file.path), + path: `${folder}/${file.path}`, + })) + ); + } catch (err) { + if (err && err.status === 404) { + console.log('This 404 was expected and handled appropriately.'); + return []; + } else { + throw err; + } + } + } + filterOpenAuthoringBranches = async (branch: string) => { try { const pullRequest = await this.getBranchPullRequest(branch); diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index 1bc18492d1b4..a9370485ddaa 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -69,7 +69,8 @@ export default class GitHub implements Implementation { useOpenAuthoring?: boolean; branch: string; apiRoot: string; - mediaFolder: string; + defaultMediaFolder: string; + currentMediaFolder: string; previewContext: string; token: string | null; squashMerges: boolean; @@ -115,7 +116,8 @@ export default class GitHub implements Implementation { this.squashMerges = config.backend.squash_merges || false; this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; - this.mediaFolder = config.media_folder; + this.defaultMediaFolder = config.media_folder; + this.currentMediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); } @@ -436,14 +438,25 @@ export default class GitHub implements Implementation { .catch(() => ({ file: { path, id: null }, data: '' })); } - getMedia(mediaFolder = this.mediaFolder) { - return this.api!.listFiles(mediaFolder).then(files => - files.map(({ id, name, size, path }) => { + updateMediaFolder(path: string) { + this.currentMediaFolder = path || this.currentMediaFolder; + return this.currentMediaFolder; + } + + getDefaultMediaFolder() { + return this.defaultMediaFolder; + } + + getMedia(currentMediaFolder = this.currentMediaFolder) { + return Promise.all([this.api!.listDirs(currentMediaFolder), this.api!.listFiles(currentMediaFolder)]).then(result => { + return result[0].map(({ id, name, path, type }) => { + return { id, name, displayURL: { id, path }, path, type }; + }).concat(result[1].map(({ id, name, size, path, type }) => { // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls // for private repositories - return { id, name, size, displayURL: { id, path }, path }; - }), - ); + return { id, name, size, displayURL: { id, path }, path, type }; + })); + }) } async getMediaFile(path: string) { @@ -484,9 +497,12 @@ export default class GitHub implements Implementation { } async persistMedia(mediaFile: AssetProxy, options: PersistOptions) { + console.log({...mediaFile}); try { await this.api!.persistFiles([], [mediaFile], options); const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string }; + console.log(mediaFile); + console.log(path) const displayURL = URL.createObjectURL(fileObj); return { id: sha, diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 4a013a50db7d..3865fc2322f3 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -44,6 +44,8 @@ export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE'; export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST'; export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS'; export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE'; +export const MEDIA_FOLDER_UPDATE = 'MEDIA_FOLDER_UPDATE'; +export const DEFAULT_MEDIA_FOLDER = 'DEFAULT_MEDIA_FOLDER'; export function createMediaLibrary(instance: MediaLibraryInstance) { const api = { @@ -131,9 +133,19 @@ export function removeInsertedMedia(controlID: string) { return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } }; } -export function loadMedia( - opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {}, -) { +export function updateMediaFolder(selectedMediaFolder: string) { + return (dispatch: ThunkDispatch, getState: () => State) => { + const state = getState(); + const backend = currentBackend(state.config); + const currentMediaFolder = backend.updateMediaFolder(selectedMediaFolder); + const defaultMediaFolder = backend.getDefaultMediaFolder(); + dispatch({ type: MEDIA_FOLDER_UPDATE, payload: { currentMediaFolder, defaultMediaFolder } }); + }; +} + +export function loadMedia( + opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {} + ) { const { delay = 0, query = '', page = 1, privateUpload } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); @@ -157,7 +169,6 @@ export function loadMedia( } } dispatch(mediaLoading(page)); - const loadFunction = () => backend .getMedia() @@ -262,6 +273,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); + console.log(state); const path = selectMediaFilePath(state.config, collection, entry, fileName, field); assetProxy = createAssetProxy({ file, @@ -287,6 +299,8 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { }); return dispatch(addDraftEntryMediaFile(mediaFile)); } else { + console.log('backend.persistMedia'); + console.log(state.config); mediaFile = await backend.persistMedia(state.config, assetProxy); } diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 0516a2e6cb24..5545682c4a1c 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -805,6 +805,14 @@ export class Backend { return entryValue; } + updateMediaFolder(path: string) { + return this.implementation.updateMediaFolder(path); + } + + getDefaultMediaFolder() { + return this.implementation.getDefaultMediaFolder(); + } + getMedia() { return this.implementation.getMedia(); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index dc8f0cedce7b..7802f5722550 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -13,6 +13,7 @@ import { insertMedia as insertMediaAction, loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, + updateMediaFolder as updateMediaFolderAction, } from 'Actions/mediaLibrary'; import { selectMediaFiles } from 'Reducers/mediaLibrary'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; @@ -42,6 +43,7 @@ class MediaLibrary extends React.Component { privateUpload: PropTypes.bool, config: ImmutablePropTypes.map, loadMedia: PropTypes.func.isRequired, + updateMediaFolder: PropTypes.func.isRequired, dynamicSearchQuery: PropTypes.string, page: PropTypes.number, persistMedia: PropTypes.func.isRequired, @@ -65,6 +67,7 @@ class MediaLibrary extends React.Component { }; componentDidMount() { + this.props.updateMediaFolder(); this.props.loadMedia(); } @@ -109,7 +112,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, type }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -121,11 +124,12 @@ class MediaLibrary extends React.Component { queryOrder, displayURL, draft, + isDirectory: type === 'tree', isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; }); - + /** * Get the sort order for use with `lodash.orderBy`, and always add the * `queryOrder` sort as the lowest priority sort order. @@ -144,10 +148,20 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - this.setState({ selectedFile }); + if (asset.isViewableImage) { + const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; + this.setState({ selectedFile }); + } else { + this.props.updateMediaFolder(asset.path); + this.props.loadMedia(); + } }; + handleBreadcrumbClick = path => { + this.props.updateMediaFolder(`${path}`); + this.props.loadMedia(); + } + /** * Upload a file. */ @@ -304,9 +318,10 @@ class MediaLibrary extends React.Component { isPaginating, privateUpload, displayURLs, + currentMediaFolder, + defaultMediaFolder, t, } = this.props; - return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} + handleBreadcrumbClick={this.handleBreadcrumbClick} handleLoadMore={this.handleLoadMore} displayURLs={displayURLs} loadDisplayURL={this.loadDisplayURL} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} t={t} /> ); @@ -364,6 +382,8 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), + currentMediaFolder: mediaLibrary.get('currentMediaFolder'), + defaultMediaFolder: mediaLibrary.get('defaultMediaFolder'), field, }; return { ...mediaLibraryProps }; @@ -376,6 +396,7 @@ const mapDispatchToProps = { insertMedia: insertMediaAction, loadMediaDisplayURL: loadMediaDisplayURLAction, closeMediaLibrary: closeMediaLibraryAction, + updateMediaFolder: updateMediaFolderAction, }; export default connect(mapStateToProps, mapDispatchToProps)(translate()(MediaLibrary)); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js new file mode 100644 index 000000000000..0e809453fd87 --- /dev/null +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { trim } from 'lodash'; +import styled from '@emotion/styled'; +import { Icon } from 'netlify-cms-ui-default'; + +const BreadCrumbsContainer = styled.div` + display: flex; + justify-content: flex-start; + margin: 20px 0; +` +const BreadCrumbsItem = styled.div` + display: flex; +` + +const BreadCrumbsItemLabel = styled.div` + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + padding: 6px 8px; + background: #eff0f4; + border-radius: 5px; +` + +const BreadCrumbsItemDivider = styled.div` + display: flex; + justify-content: center; + align-items: center; +` + +class MediaLibraryBreadcrumbs extends React.Component { + render() { + const { + handleBreadcrumbClick, + currentMediaFolder, + defaultMediaFolder, + } = this.props; + var hiddenPath = defaultMediaFolder.split('/').slice(0, -1).join('/'); + var currentMediaFolderParts = trim(currentMediaFolder.replace(hiddenPath, ''), '/').split('/'); + var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { + return { + isDefaultMediaDirectory: index === 0, + path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index+ 1).join('/')}`, + label: part + } + }); + + this.BreadCrumbsContent = breadcrumbsArray.map((item, index) => { + return ( + + handleBreadcrumbClick(item.path)}> + {item.isDefaultMediaDirectory ? : item.label} + + / + + ) + }); + return ( + + {this.BreadCrumbsContent} + + ); + } +} + +MediaLibraryBreadcrumbs.propTypes = { + handleBreadcrumbClick: PropTypes.func.isRequired, + currentMediaFolder: PropTypes.string, +}; + +export default MediaLibraryBreadcrumbs; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 6d61119ae0d9..819a2ff88d22 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import styled from '@emotion/styled'; -import { colors, borders, lengths, shadows, effects } from 'netlify-cms-ui-default'; +import { colors, borders, lengths, shadows, effects, Icon } from 'netlify-cms-ui-default'; const IMAGE_HEIGHT = 160; @@ -77,8 +77,19 @@ class MediaLibraryCard extends React.Component { type, isViewableImage, isDraft, + isDirectory, } = this.props; const url = displayURL.get('url'); + var cardImageWrapper = ( + {isDraft ? {draftText} : null} + {url && isViewableImage ? ( + + ) : ( + {type} + )} + ); + var cardDirectoryWrapper = ; + var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; return ( - - {isDraft ? {draftText} : null} - {url && isViewableImage ? ( - - ) : ( - {type} - )} - + {previewElement} {text} ); } componentDidMount() { - const { displayURL, loadDisplayURL } = this.props; - if (!displayURL.get('url')) { + const { displayURL, loadDisplayURL, isViewableImage } = this.props; + if (!displayURL.get('url') && isViewableImage) { loadDisplayURL(); } } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index aa8ceefe01a8..5a42d68b463d 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -58,6 +58,7 @@ const CardWrapper = props => { loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} + isDirectory={file.isDirectory} /> ); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index 301094850e4b..160867ece906 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -26,7 +26,7 @@ const cardOutsideWidth = `300px`; const StyledModal = styled(Modal)` display: grid; - grid-template-rows: 120px auto; + grid-template-rows: 170px auto; width: calc(${cardOutsideWidth} + 20px); background-color: ${props => props.isPrivate && colors.grayDark}; @@ -87,10 +87,13 @@ const MediaLibraryModal = ({ handleDownload, setScrollContainerRef, handleAssetClick, + handleBreadcrumbClick, handleLoadMore, loadDisplayURL, displayURLs, t, + currentMediaFolder, + defaultMediaFolder, }) => { const filteredFiles = forImage ? handleFilter(files) : files; const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles; @@ -128,7 +131,11 @@ const MediaLibraryModal = ({ hasSelection={hasSelection} isPersisting={isPersisting} isDeleting={isDeleting} - /> + handleBreadcrumbClick={handleBreadcrumbClick} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} + /> + {!shouldShowEmptyMessage ? null : ( )} @@ -189,6 +196,7 @@ MediaLibraryModal.propTypes = { handleInsert: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, + handleBreadcrumbClick: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, loadDisplayURL: PropTypes.func.isRequired, t: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index c2cec3e2006c..9d5f6f04970b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import MediaLibrarySearch from './MediaLibrarySearch'; import MediaLibraryHeader from './MediaLibraryHeader'; +import MediaLibraryBreadcrumbs from './MediaLibraryBreadCrumbs'; import { UploadButton, DeleteButton, DownloadButton, InsertButton } from './MediaLibraryButtons'; const LibraryTop = styled.div` @@ -37,6 +38,9 @@ const MediaLibraryTop = ({ hasSelection, isPersisting, isDeleting, + handleBreadcrumbClick, + currentMediaFolder, + defaultMediaFolder, }) => { const shouldShowButtonLoader = isPersisting || isDeleting; const uploadEnabled = !shouldShowButtonLoader; @@ -100,6 +104,13 @@ const MediaLibraryTop = ({ )} + + + ); }; diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 70e1fa1edaae..e90be2413444 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -762,9 +762,10 @@ export const selectMediaFilePath = ( if (isAbsolutePath(mediaPath)) { return mediaPath; } - + console.log('mediaPath'); + console.log(mediaPath); const mediaFolder = selectMediaFolder(config, collection, entryMap, field); - + console.log(mediaFolder); return join(mediaFolder, basename(mediaPath)); }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index b72196d19055..e289872444d8 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -18,6 +18,8 @@ import { MEDIA_DISPLAY_URL_REQUEST, MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE, + MEDIA_FOLDER_UPDATE, + DEFAULT_MEDIA_FOLDER, } from '../actions/mediaLibrary'; import { selectEditingDraft, selectMediaFolder } from './entries'; import { selectIntegration } from './'; @@ -209,6 +211,16 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => .deleteIn([...displayURLPath, 'url']) ); } + + case MEDIA_FOLDER_UPDATE: { + // zzz not sure what this is doing? + return state.withMutations(map => { + map.set('currentMediaFolder', action.payload.currentMediaFolder); + map.set('defaultMediaFolder', action.payload.defaultMediaFolder); + map.set('isLoading', false); + }); + } + default: return state; } @@ -233,7 +245,6 @@ export function selectMediaFiles(state: State, field?: EntryField) { } else { files = mediaLibrary.get('files') || []; } - return files; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index d723b78cdbc9..5d6828bed55e 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -430,6 +430,8 @@ export interface MediaLibraryAction extends Action { payload: MediaLibraryInstance & { controlID: string; forImage: boolean; + currentMediaFolder: string, + defaultMediaFolder: string, privateUpload: boolean; config: Map; field?: EntryField; From 16195ef97cab85576ff87ad838e97bdacaa491a2 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 6 Dec 2020 19:50:26 +0100 Subject: [PATCH 09/45] refactor: simplify code by listing files recursively --- .../netlify-cms-backend-github/src/API.ts | 63 ++++++----------- .../src/GraphQLAPI.ts | 50 ++++++++------ .../src/__tests__/GraphQLAPI.spec.js | 67 ++++++++++++++++++- .../src/implementation.tsx | 48 ++++++------- .../src/actions/mediaLibrary.ts | 22 ++---- packages/netlify-cms-core/src/backend.ts | 8 --- .../components/MediaLibrary/MediaLibrary.js | 42 ++++++------ .../MediaLibrary/MediaLibraryBreadCrumbs.js | 35 +++++----- .../MediaLibrary/MediaLibraryCard.js | 20 +++--- .../MediaLibrary/MediaLibraryModal.js | 4 +- .../netlify-cms-core/src/reducers/entries.ts | 5 +- .../src/reducers/mediaLibrary.ts | 13 +--- packages/netlify-cms-core/src/types/redux.ts | 2 - 13 files changed, 192 insertions(+), 187 deletions(-) diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index e52e2c50e21f..d9896048c3fd 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -55,8 +55,13 @@ export interface Config { cmsLabelPrefix: string; } +export enum TreeFileType { + TREE = 'tree', + BLOB = 'blob', +} + interface TreeFile { - type: 'blob' | 'tree'; + type: TreeFileType; sha: string; path: string; raw?: string; @@ -372,7 +377,9 @@ export default class API { this.request(`${this.repoURL}/git/trees`, { method: 'POST', body: JSON.stringify({ - tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }], + tree: [ + { path: 'README.md', mode: '100644', type: TreeFileType.BLOB, sha: item.sha }, + ], }), }), ) @@ -650,8 +657,8 @@ export default class API { async listFiles( path: string, - { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, - ): Promise<{ type: string; id: string; name: string; path: string; size: number }[]> { + { repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {}, + ): Promise<{ type: TreeFileType; id: string; name: string; path: string; size: number }[]> { const folder = trim(path, '/'); try { const result: Octokit.GitGetTreeResponse = await this.request( @@ -665,9 +672,12 @@ export default class API { return ( result.tree // filter only files and up to the required depth - .filter(file => file.type === 'blob' && file.path.split('/').length <= depth) + .filter( + file => + types.includes(file.type as TreeFileType) && file.path.split('/').length <= depth, + ) .map(file => ({ - type: file.type, + type: file.type as TreeFileType, id: file.sha, name: basename(file.path), path: `${folder}/${file.path}`, @@ -684,41 +694,6 @@ export default class API { } } - async listDirs( - path: string, - { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}, - ): Promise<{ type: string; id: string; name: string; path: string; }[]> { - const folder = trim(path, '/'); - try { - const result: Octokit.GitGetTreeResponse = await this.request( - `${repoURL}/git/trees/${branch}:${folder}`, - { - // GitHub API supports recursive=1 for getting the entire recursive tree - // or omitting it to get the non-recursive tree - params: depth > 1 ? { recursive: 1 } : {}, - }, - ); - return ( - result.tree - // filter only files and up to the required depth - .filter(file => file.type === 'tree' && file.path.split('/').length <= depth) - .map(file => ({ - type: file.type, - id: file.sha, - name: basename(file.path), - path: `${folder}/${file.path}`, - })) - ); - } catch (err) { - if (err && err.status === 404) { - console.log('This 404 was expected and handled appropriately.'); - return []; - } else { - throw err; - } - } - } - filterOpenAuthoringBranches = async (branch: string) => { try { const pullRequest = await this.getBranchPullRequest(branch); @@ -1405,7 +1380,7 @@ export default class API { const entry = { path: trimStart(file.path, '/'), mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: file.sha, } as TreeEntry; @@ -1427,14 +1402,14 @@ export default class API { tree.push({ path: file.path, mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: null, }); // create in new path tree.push({ path: file.path.replace(sourceDir, destDir), mode: '100644', - type: 'blob', + type: TreeFileType.BLOB, sha: file.path === from ? sha : file.id, }); } diff --git a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts index c14048b913dd..8f7e32515336 100644 --- a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts +++ b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts @@ -18,7 +18,14 @@ import { } from 'netlify-cms-lib-util'; import { trim, trimStart } from 'lodash'; import introspectionQueryResultData from './fragmentTypes'; -import API, { Config, BlobArgs, API_NAME, PullRequestState, MOCK_PULL_REQUEST } from './API'; +import API, { + Config, + BlobArgs, + API_NAME, + PullRequestState, + MOCK_PULL_REQUEST, + TreeFileType, +} from './API'; import * as queries from './queries'; import * as mutations from './mutations'; import { GraphQLError } from 'graphql'; @@ -35,7 +42,7 @@ interface TreeEntry { object?: { entries: TreeEntry[]; }; - type: 'blob' | 'tree'; + type: TreeFileType; name: string; sha: string; blob?: { @@ -47,7 +54,7 @@ interface TreeFile { path: string; id: string; size: number; - type: string; + type: TreeFileType; name: string; } @@ -337,22 +344,24 @@ export default class GraphQLAPI extends API { } } - getAllFiles(entries: TreeEntry[], path: string) { + getAllFiles(entries: TreeEntry[], path: string, types: TreeFileType[]) { const allFiles: TreeFile[] = entries.reduce((acc, item) => { - if (item.type === 'tree') { + const itemToAdd = { + name: item.name, + type: item.type, + id: item.sha, + path: `${path}/${item.name}`, + size: item.blob ? item.blob.size : 0, + }; + if (item.type === TreeFileType.BLOB) { + return [...acc, itemToAdd]; + } + + if (item.type === TreeFileType.TREE) { const entries = item.object?.entries || []; - return [...acc, ...this.getAllFiles(entries, `${path}/${item.name}`)]; - } else if (item.type === 'blob') { - return [ - ...acc, - { - name: item.name, - type: item.type, - id: item.sha, - path: `${path}/${item.name}`, - size: item.blob ? item.blob.size : 0, - }, - ]; + const currentDir = types.includes(TreeFileType.TREE) ? [itemToAdd] : []; + const subFiles = this.getAllFiles(entries, `${path}/${item.name}`, types); + return [...acc, ...currentDir, ...subFiles]; } return acc; @@ -360,7 +369,10 @@ export default class GraphQLAPI extends API { return allFiles; } - async listFiles(path: string, { repoURL = this.repoURL, branch = this.branch, depth = 1 } = {}) { + async listFiles( + path: string, + { repoURL = this.repoURL, branch = this.branch, depth = 1, types = [TreeFileType.BLOB] } = {}, + ) { const { owner, name } = this.getOwnerAndNameFromRepoUrl(repoURL); const folder = trim(path, '/'); const { data } = await this.query({ @@ -369,7 +381,7 @@ export default class GraphQLAPI extends API { }); if (data.repository.object) { - const allFiles = this.getAllFiles(data.repository.object.entries, folder); + const allFiles = this.getAllFiles(data.repository.object.entries, folder, types); return allFiles; } else { return []; diff --git a/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js b/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js index bb1e77ae1ac6..ae5cbdcc987f 100644 --- a/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js +++ b/packages/netlify-cms-backend-github/src/__tests__/GraphQLAPI.spec.js @@ -41,7 +41,7 @@ describe('github GraphQL API', () => { ]; const path = 'posts'; - expect(api.getAllFiles(entries, path)).toEqual([ + expect(api.getAllFiles(entries, path, ['blob'])).toEqual([ { name: 'post-1.md', id: 'sha-1', @@ -65,5 +65,70 @@ describe('github GraphQL API', () => { }, ]); }); + + it('should should return directories when types includes `tree`', () => { + const api = new GraphQLAPI({ branch: 'gh-pages', repo: 'owner/my-repo' }); + const entries = [ + { + name: 'post-1.md', + sha: 'sha-1', + type: 'blob', + blob: { size: 1 }, + }, + { + name: 'post-2.md', + sha: 'sha-2', + type: 'blob', + blob: { size: 2 }, + }, + { + name: '2019', + sha: 'dir-sha', + type: 'tree', + object: { + entries: [ + { + name: 'nested-post.md', + sha: 'nested-post-sha', + type: 'blob', + blob: { size: 3 }, + }, + ], + }, + }, + ]; + const path = 'posts'; + + expect(api.getAllFiles(entries, path, ['blob', 'tree'])).toEqual([ + { + name: 'post-1.md', + id: 'sha-1', + type: 'blob', + size: 1, + path: 'posts/post-1.md', + }, + { + name: 'post-2.md', + id: 'sha-2', + type: 'blob', + size: 2, + path: 'posts/post-2.md', + }, + { + name: '2019', + id: 'dir-sha', + type: 'tree', + size: 0, + path: 'posts/2019', + }, + { + name: 'nested-post.md', + id: 'nested-post-sha', + type: 'blob', + size: 3, + path: 'posts/2019/nested-post.md', + }, + ]); + }); }); }); diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index a9370485ddaa..c28f464b5c2a 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -34,7 +34,7 @@ import { } from 'netlify-cms-lib-util'; import AuthenticationPage from './AuthenticationPage'; import { Octokit } from '@octokit/rest'; -import API, { API_NAME } from './API'; +import API, { API_NAME, TreeFileType } from './API'; import GraphQLAPI from './GraphQLAPI'; type GitHubUser = Octokit.UsersGetAuthenticatedResponse; @@ -69,8 +69,7 @@ export default class GitHub implements Implementation { useOpenAuthoring?: boolean; branch: string; apiRoot: string; - defaultMediaFolder: string; - currentMediaFolder: string; + mediaFolder: string; previewContext: string; token: string | null; squashMerges: boolean; @@ -116,8 +115,7 @@ export default class GitHub implements Implementation { this.squashMerges = config.backend.squash_merges || false; this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; - this.defaultMediaFolder = config.media_folder; - this.currentMediaFolder = config.media_folder; + this.mediaFolder = config.media_folder; this.previewContext = config.backend.preview_context || ''; this.lock = asyncLock(); } @@ -438,25 +436,24 @@ export default class GitHub implements Implementation { .catch(() => ({ file: { path, id: null }, data: '' })); } - updateMediaFolder(path: string) { - this.currentMediaFolder = path || this.currentMediaFolder; - return this.currentMediaFolder; - } - - getDefaultMediaFolder() { - return this.defaultMediaFolder; - } - - getMedia(currentMediaFolder = this.currentMediaFolder) { - return Promise.all([this.api!.listDirs(currentMediaFolder), this.api!.listFiles(currentMediaFolder)]).then(result => { - return result[0].map(({ id, name, path, type }) => { - return { id, name, displayURL: { id, path }, path, type }; - }).concat(result[1].map(({ id, name, size, path, type }) => { - // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls - // for private repositories - return { id, name, size, displayURL: { id, path }, path, type }; - })); - }) + async getMedia(mediaFolder = this.mediaFolder) { + const files = await this.api!.listFiles(mediaFolder, { + types: [TreeFileType.BLOB, TreeFileType.TREE], + depth: 100, + }); + const withDisplayUrls = files.map(({ id, name, size, path, type }) => { + // load media using getMediaDisplayURL to avoid token expiration with GitHub raw content urls + // for private repositories + return { + id, + name, + size, + displayURL: { id, path }, + path, + isDirectory: type === TreeFileType.TREE, + }; + }); + return withDisplayUrls; } async getMediaFile(path: string) { @@ -497,12 +494,9 @@ export default class GitHub implements Implementation { } async persistMedia(mediaFile: AssetProxy, options: PersistOptions) { - console.log({...mediaFile}); try { await this.api!.persistFiles([], [mediaFile], options); const { sha, path, fileObj } = mediaFile as AssetProxy & { sha: string }; - console.log(mediaFile); - console.log(path) const displayURL = URL.createObjectURL(fileObj); return { id: sha, diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 3865fc2322f3..4a013a50db7d 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -44,8 +44,6 @@ export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE'; export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST'; export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS'; export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE'; -export const MEDIA_FOLDER_UPDATE = 'MEDIA_FOLDER_UPDATE'; -export const DEFAULT_MEDIA_FOLDER = 'DEFAULT_MEDIA_FOLDER'; export function createMediaLibrary(instance: MediaLibraryInstance) { const api = { @@ -133,19 +131,9 @@ export function removeInsertedMedia(controlID: string) { return { type: MEDIA_REMOVE_INSERTED, payload: { controlID } }; } -export function updateMediaFolder(selectedMediaFolder: string) { - return (dispatch: ThunkDispatch, getState: () => State) => { - const state = getState(); - const backend = currentBackend(state.config); - const currentMediaFolder = backend.updateMediaFolder(selectedMediaFolder); - const defaultMediaFolder = backend.getDefaultMediaFolder(); - dispatch({ type: MEDIA_FOLDER_UPDATE, payload: { currentMediaFolder, defaultMediaFolder } }); - }; -} - -export function loadMedia( - opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {} - ) { +export function loadMedia( + opts: { delay?: number; query?: string; page?: number; privateUpload?: boolean } = {}, +) { const { delay = 0, query = '', page = 1, privateUpload } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); @@ -169,6 +157,7 @@ export function loadMedia( } } dispatch(mediaLoading(page)); + const loadFunction = () => backend .getMedia() @@ -273,7 +262,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - console.log(state); const path = selectMediaFilePath(state.config, collection, entry, fileName, field); assetProxy = createAssetProxy({ file, @@ -299,8 +287,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { }); return dispatch(addDraftEntryMediaFile(mediaFile)); } else { - console.log('backend.persistMedia'); - console.log(state.config); mediaFile = await backend.persistMedia(state.config, assetProxy); } diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 5545682c4a1c..0516a2e6cb24 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -805,14 +805,6 @@ export class Backend { return entryValue; } - updateMediaFolder(path: string) { - return this.implementation.updateMediaFolder(path); - } - - getDefaultMediaFolder() { - return this.implementation.getDefaultMediaFolder(); - } - getMedia() { return this.implementation.getMedia(); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 7802f5722550..6c1410633dc2 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -13,9 +13,9 @@ import { insertMedia as insertMediaAction, loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, - updateMediaFolder as updateMediaFolderAction, } from 'Actions/mediaLibrary'; import { selectMediaFiles } from 'Reducers/mediaLibrary'; +import { dirname } from 'path'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; /** @@ -43,13 +43,13 @@ class MediaLibrary extends React.Component { privateUpload: PropTypes.bool, config: ImmutablePropTypes.map, loadMedia: PropTypes.func.isRequired, - updateMediaFolder: PropTypes.func.isRequired, dynamicSearchQuery: PropTypes.string, page: PropTypes.number, persistMedia: PropTypes.func.isRequired, deleteMedia: PropTypes.func.isRequired, insertMedia: PropTypes.func.isRequired, closeMediaLibrary: PropTypes.func.isRequired, + defaultMediaFolder: PropTypes.string.isRequired, t: PropTypes.func.isRequired, }; @@ -67,7 +67,6 @@ class MediaLibrary extends React.Component { }; componentDidMount() { - this.props.updateMediaFolder(); this.props.loadMedia(); } @@ -112,7 +111,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, type }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -124,12 +123,12 @@ class MediaLibrary extends React.Component { queryOrder, displayURL, draft, - isDirectory: type === 'tree', + isDirectory, isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; }); - + /** * Get the sort order for use with `lodash.orderBy`, and always add the * `queryOrder` sort as the lowest priority sort order. @@ -148,19 +147,17 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - if (asset.isViewableImage) { + if (asset.isDirectory) { + this.setState({ currentMediaFolder: asset.path }); + } else { const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; this.setState({ selectedFile }); - } else { - this.props.updateMediaFolder(asset.path); - this.props.loadMedia(); } }; - handleBreadcrumbClick = path => { - this.props.updateMediaFolder(`${path}`); - this.props.loadMedia(); - } + handleBreadcrumbClick = currentMediaFolder => { + this.setState({ currentMediaFolder }); + }; /** * Upload a file. @@ -318,15 +315,18 @@ class MediaLibrary extends React.Component { isPaginating, privateUpload, displayURLs, - currentMediaFolder, defaultMediaFolder, t, } = this.props; + + const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; + const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); + return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} handleBreadcrumbClick={this.handleBreadcrumbClick} + currentMediaFolder={currentMediaFolder} + defaultMediaFolder={defaultMediaFolder} handleLoadMore={this.handleLoadMore} displayURLs={displayURLs} loadDisplayURL={this.loadDisplayURL} - currentMediaFolder={currentMediaFolder} - defaultMediaFolder={defaultMediaFolder} t={t} /> ); @@ -363,7 +363,7 @@ class MediaLibrary extends React.Component { } const mapStateToProps = state => { - const { mediaLibrary } = state; + const { mediaLibrary, config } = state; const field = mediaLibrary.get('field'); const mediaLibraryProps = { isVisible: mediaLibrary.get('isVisible'), @@ -382,9 +382,8 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), - currentMediaFolder: mediaLibrary.get('currentMediaFolder'), - defaultMediaFolder: mediaLibrary.get('defaultMediaFolder'), field, + defaultMediaFolder: config.get('media_folder'), }; return { ...mediaLibraryProps }; }; @@ -396,7 +395,6 @@ const mapDispatchToProps = { insertMedia: insertMediaAction, loadMediaDisplayURL: loadMediaDisplayURLAction, closeMediaLibrary: closeMediaLibraryAction, - updateMediaFolder: updateMediaFolderAction, }; export default connect(mapStateToProps, mapDispatchToProps)(translate()(MediaLibrary)); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 0e809453fd87..22e488675a4b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -8,10 +8,10 @@ const BreadCrumbsContainer = styled.div` display: flex; justify-content: flex-start; margin: 20px 0; -` +`; const BreadCrumbsItem = styled.div` display: flex; -` +`; const BreadCrumbsItemLabel = styled.div` display: flex; @@ -21,29 +21,28 @@ const BreadCrumbsItemLabel = styled.div` padding: 6px 8px; background: #eff0f4; border-radius: 5px; -` +`; const BreadCrumbsItemDivider = styled.div` display: flex; justify-content: center; align-items: center; -` +`; class MediaLibraryBreadcrumbs extends React.Component { render() { - const { - handleBreadcrumbClick, - currentMediaFolder, - defaultMediaFolder, - } = this.props; - var hiddenPath = defaultMediaFolder.split('/').slice(0, -1).join('/'); + const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder } = this.props; + var hiddenPath = defaultMediaFolder + .split('/') + .slice(0, -1) + .join('/'); var currentMediaFolderParts = trim(currentMediaFolder.replace(hiddenPath, ''), '/').split('/'); var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { return { isDefaultMediaDirectory: index === 0, - path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index+ 1).join('/')}`, - label: part - } + path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index + 1).join('/')}`, + label: part, + }; }); this.BreadCrumbsContent = breadcrumbsArray.map((item, index) => { @@ -51,16 +50,12 @@ class MediaLibraryBreadcrumbs extends React.Component { handleBreadcrumbClick(item.path)}> {item.isDefaultMediaDirectory ? : item.label} - + / - ) + ); }); - return ( - - {this.BreadCrumbsContent} - - ); + return {this.BreadCrumbsContent}; } } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 819a2ff88d22..3addbc730372 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -80,15 +80,17 @@ class MediaLibraryCard extends React.Component { isDirectory, } = this.props; const url = displayURL.get('url'); - var cardImageWrapper = ( - {isDraft ? {draftText} : null} - {url && isViewableImage ? ( - - ) : ( - {type} - )} - ); - var cardDirectoryWrapper = ; + var cardImageWrapper = ( + + {isDraft ? {draftText} : null} + {url && isViewableImage ? ( + + ) : ( + {type} + )} + + ); + var cardDirectoryWrapper = ; var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; return ( - + /> + {!shouldShowEmptyMessage ? null : ( )} diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index e90be2413444..70e1fa1edaae 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -762,10 +762,9 @@ export const selectMediaFilePath = ( if (isAbsolutePath(mediaPath)) { return mediaPath; } - console.log('mediaPath'); - console.log(mediaPath); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field); - console.log(mediaFolder); + return join(mediaFolder, basename(mediaPath)); }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index e289872444d8..b72196d19055 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -18,8 +18,6 @@ import { MEDIA_DISPLAY_URL_REQUEST, MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE, - MEDIA_FOLDER_UPDATE, - DEFAULT_MEDIA_FOLDER, } from '../actions/mediaLibrary'; import { selectEditingDraft, selectMediaFolder } from './entries'; import { selectIntegration } from './'; @@ -211,16 +209,6 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => .deleteIn([...displayURLPath, 'url']) ); } - - case MEDIA_FOLDER_UPDATE: { - // zzz not sure what this is doing? - return state.withMutations(map => { - map.set('currentMediaFolder', action.payload.currentMediaFolder); - map.set('defaultMediaFolder', action.payload.defaultMediaFolder); - map.set('isLoading', false); - }); - } - default: return state; } @@ -245,6 +233,7 @@ export function selectMediaFiles(state: State, field?: EntryField) { } else { files = mediaLibrary.get('files') || []; } + return files; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 5d6828bed55e..d723b78cdbc9 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -430,8 +430,6 @@ export interface MediaLibraryAction extends Action { payload: MediaLibraryInstance & { controlID: string; forImage: boolean; - currentMediaFolder: string, - defaultMediaFolder: string, privateUpload: boolean; config: Map; field?: EntryField; From 3c64681ee7d3e431ea643e48894f2914ff989db8 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 13:42:37 +0200 Subject: [PATCH 10/45] feat(media dir nav): uploading to nested directories works --- dev-test/config.yml | 9 ++++++--- packages/netlify-cms-core/src/actions/mediaLibrary.ts | 10 ++++++---- .../src/components/MediaLibrary/MediaLibrary.js | 5 +++-- packages/netlify-cms-core/src/reducers/entries.ts | 8 ++++++-- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index 2ffe5041ddb5..1ab34824dffb 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -1,10 +1,11 @@ backend: - name: test-repo + name: github + repo: andrew-paterson/netlify-cms-hugo-test-site site_url: https://example.com publish_mode: editorial_workflow -media_folder: assets/uploads +media_folder: static/images collections: # A list of collections the CMS should be able to edit - name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit @@ -13,7 +14,9 @@ collections: # A list of collections the CMS should be able to edit description: > The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection. - folder: '_posts' + folder: 'content/posts' + media_folder: '/static/images/posts' + public_folder: '/images/posts' slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 4a013a50db7d..ec8ad4ef7d00 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -208,15 +208,17 @@ function createMediaFileFromAsset({ } export function persistMedia(file: File, opts: MediaOptions = {}) { - const { privateUpload, field } = opts; + const { privateUpload, field, currentMediaFolder } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, null, 'assetStore'); const files: MediaFile[] = selectMediaFiles(state, field); const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug')); - const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName); - + const entry = state.entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); + const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); + const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path); const editingDraft = selectEditingDraft(state.entryDraft); /** @@ -262,7 +264,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - const path = selectMediaFilePath(state.config, collection, entry, fileName, field); + const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); assetProxy = createAssetProxy({ file, path, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 6c1410633dc2..fd6e12979fc1 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -171,6 +171,8 @@ class MediaLibrary extends React.Component { event.persist(); event.stopPropagation(); event.preventDefault(); + const { defaultMediaFolder } = this.props; + const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const { persistMedia, privateUpload, config, t, field } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; @@ -184,7 +186,7 @@ class MediaLibrary extends React.Component { }), ); } else { - await persistMedia(file, { privateUpload, field }); + await persistMedia(file, { privateUpload, field, currentMediaFolder }); this.setState({ selectedFile: this.props.files[0] }); @@ -321,7 +323,6 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); - return ( { + if (currentMediaFolder) { + return trim(currentMediaFolder, '/') + } const name = 'media_folder'; let mediaFolder = config.get(name); @@ -748,7 +752,6 @@ export const selectMediaFolder = ( : join(collection!.get('folder') as string, DRAFT_MEDIA_FILES); } } - return trim(mediaFolder, '/'); }; @@ -758,12 +761,13 @@ export const selectMediaFilePath = ( entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, + currentMediaFolder: string, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectMediaFolder(config, collection, entryMap, field); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field, currentMediaFolder); return join(mediaFolder, basename(mediaPath)); }; From 405e04d28dac31d734af1513b655a1261dc7eb7a Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 16:18:28 +0200 Subject: [PATCH 11/45] feat(media dir nav): basic ui for multiple asset selection --- .../components/MediaLibrary/MediaLibrary.js | 28 ++++++-- .../MediaLibrary/MediaLibraryCard.js | 68 +++++++++++++++++-- .../MediaLibrary/MediaLibraryCardGrid.js | 15 +++- .../MediaLibrary/MediaLibraryModal.js | 7 +- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index fd6e12979fc1..3593f3318d5a 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -147,12 +147,27 @@ class MediaLibrary extends React.Component { * Toggle asset selection on click. */ handleAssetClick = asset => { - if (asset.isDirectory) { - this.setState({ currentMediaFolder: asset.path }); + // console.log('handleAssetClick'); + // if (asset.isDirectory) { + // this.setState({ currentMediaFolder: asset.path }); + // } else { + // const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; + // this.setState({ selectedFile }); + // } + }; + + handleAssetCheckboxChange = (asset, event) => { + console.log('handleAssetCheckboxChange') + let selectedAssets = this.state.selectedAssets || []; + if (event.target.checked) { + selectedAssets.push(asset) } else { - const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - this.setState({ selectedFile }); + selectedAssets = selectedAssets.filter(selectedAsset => { + return selectedAsset.key !== asset.key; + }) } + this.setState({ selectedAssets }); + event.stopPropagation(); }; handleBreadcrumbClick = currentMediaFolder => { @@ -323,11 +338,12 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); + const currentDirFilesOrderedByTreeType = (currentDirFiles || []).filter(file => file.isDirectory).concat((currentDirFiles || []).filter(file => !file.isDirectory)); return ( (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} + handleAssetCheckboxChange={this.handleAssetCheckboxChange} handleBreadcrumbClick={this.handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 3addbc730372..d245c96cd802 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -33,7 +33,7 @@ const CardImageWrapper = styled.div` const CardImage = styled.img` width: 100%; height: ${IMAGE_HEIGHT}px; - object-fit: contain; + object-fit: cover; border-radius: 2px 2px 0 0; `; @@ -46,12 +46,35 @@ const CardFileIcon = styled.div` font-size: 3em; `; -const CardText = styled.p` +const CardDirctoryWrapper = styled.div` + width: 100%; + height: 160px; + object-fit: cover; + border-radius: 2px 2px 0 0; + padding: 1em; + font-size: 3em; + background: #eee; +`; + +const CardText = styled.div` color: ${colors.text}; padding: 8px; - margin-top: 20px; + font-size: 0.8em; + display: flex; + justify-content: center; + flex-direction: column; +`; + +const ObjectName = styled.div` + color: ${colors.text}; + font-weight: 600; overflow-wrap: break-word; line-height: 1.3 !important; + text-align: left; +`; + +const ImageMeta = styled.div` + text-align: left; `; const DraftText = styled.p` @@ -62,6 +85,26 @@ const DraftText = styled.p` border-radius: ${lengths.borderRadius} 0px ${lengths.borderRadius} 0; `; +function readableFileSize(numberOfBytes) { + let readableSize; + let units; + numberOfBytes = numberOfBytes || 0; + if (numberOfBytes >= 0 && numberOfBytes < 1000) { + readableSize = numberOfBytes; + units = ' bytes'; + } else if (numberOfBytes >= 1000 && numberOfBytes < 1000000) { + readableSize = Math.ceil(numberOfBytes / 1000); + units = ' KB'; + } else if (numberOfBytes >= 1000000 && numberOfBytes < 1000000000) { + readableSize = (numberOfBytes / 1000000).toFixed(2); + units = ' MB'; + } else if (numberOfBytes >= 1000000000) { + readableSize = (numberOfBytes / 1000000000).toFixed(2); + units = ' GB'; + } + return readableSize + units; +} + class MediaLibraryCard extends React.Component { render() { const { @@ -69,6 +112,8 @@ class MediaLibraryCard extends React.Component { displayURL, text, onClick, + onAssetCheckboxClick, + onChecked, draftText, width, height, @@ -78,6 +123,7 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, + size } = this.props; const url = displayURL.get('url'); var cardImageWrapper = ( @@ -90,8 +136,12 @@ class MediaLibraryCard extends React.Component { )} ); - var cardDirectoryWrapper = ; - var previewElement = isDirectory ? cardDirectoryWrapper : cardImageWrapper; + var cardDirectoryEl = ( + + + + ); + var previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; return ( + {previewElement} - {text} + + {text} + {isViewableImage ? {type} - {readableFileSize(size)} : null} + ); } @@ -119,7 +173,7 @@ MediaLibraryCard.propTypes = { isSelected: PropTypes.bool, displayURL: ImmutablePropTypes.map.isRequired, text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, draftText: PropTypes.string.isRequired, width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 5a42d68b463d..cd8fe4b3aafa 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -16,7 +16,9 @@ const CardWrapper = props => { data: { mediaItems, isSelectedFile, + isSelectedAsset, onAssetClick, + onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -32,7 +34,6 @@ const CardWrapper = props => { return null; } const file = mediaItems[index]; - return (
{ height: style.height - gutter, }} > + onAssetClick(file)} + onChecked={(event) => {onAssetCheckboxClick(file, event)}} isDraft={file.draft} draftText={cardDraftText} width={cardWidth} @@ -59,6 +63,7 @@ const CardWrapper = props => { type={file.type} isViewableImage={file.isViewableImage} isDirectory={file.isDirectory} + size={file.size} />
); @@ -101,7 +106,9 @@ const PaginatedGrid = ({ setScrollContainerRef, mediaItems, isSelectedFile, + isSelectedAsset, onAssetClick, + onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -120,7 +127,8 @@ const PaginatedGrid = ({ {mediaItems.map(file => ( onAssetClick(file)} isDraft={file.draft} @@ -133,6 +141,7 @@ const PaginatedGrid = ({ loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} + onAssetCheckboxClick={onAssetCheckboxClick} /> ))} {!canLoadMore ? null : } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index d7dd2577315f..a984b3e38a24 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -14,7 +14,7 @@ import { colors } from 'netlify-cms-ui-default'; * Responsive styling needs to be overhauled. Current setup requires specifying * widths per breakpoint. */ -const cardWidth = `280px`; +const cardWidth = `240px`; const cardHeight = `240px`; const cardMargin = `10px`; @@ -75,6 +75,7 @@ const MediaLibraryModal = ({ privateUpload, query, selectedFile, + selectedAssets, handleFilter, handleQuery, toTableData, @@ -87,6 +88,7 @@ const MediaLibraryModal = ({ handleDownload, setScrollContainerRef, handleAssetClick, + handleAssetCheckboxChange, handleBreadcrumbClick, handleLoadMore, loadDisplayURL, @@ -143,7 +145,9 @@ const MediaLibraryModal = ({ setScrollContainerRef={setScrollContainerRef} mediaItems={tableData} isSelectedFile={file => selectedFile.key === file.key} + isSelectedAsset={file => (selectedAssets || []).filter(selectedAsset => selectedAsset.key === file.key).length > 0} onAssetClick={handleAssetClick} + onAssetCheckboxClick={handleAssetCheckboxChange} canLoadMore={hasNextPage} onLoadMore={handleLoadMore} isPaginating={isPaginating} @@ -196,6 +200,7 @@ MediaLibraryModal.propTypes = { handleInsert: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, + handleAssetCheckboxChange: PropTypes.func.isRequired, handleBreadcrumbClick: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, loadDisplayURL: PropTypes.func.isRequired, From 49bc28944dffc08fac770d69a68fa04dff653ffd Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 21 Dec 2020 16:41:59 +0200 Subject: [PATCH 12/45] feat(media dir nav): selected assets array cleared when user changes folders --- .../components/MediaLibrary/MediaLibrary.js | 36 ++++++++++--------- .../MediaLibrary/MediaLibraryCard.js | 3 +- .../MediaLibrary/MediaLibraryCardGrid.js | 1 - 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 3593f3318d5a..3be92aa20cd6 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -143,23 +143,15 @@ class MediaLibrary extends React.Component { this.props.closeMediaLibrary(); }; - /** - * Toggle asset selection on click. - */ - handleAssetClick = asset => { - // console.log('handleAssetClick'); - // if (asset.isDirectory) { - // this.setState({ currentMediaFolder: asset.path }); - // } else { - // const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; - // this.setState({ selectedFile }); - // } - }; + isSelectedAsset = asset => { + return (this.state.selectedAssets || []).filter(selectedAsset => { + return selectedAsset.key === asset.key + }).length > 0; + } - handleAssetCheckboxChange = (asset, event) => { - console.log('handleAssetCheckboxChange') + updateSelectedAssets = (asset) => { let selectedAssets = this.state.selectedAssets || []; - if (event.target.checked) { + if (!this.isSelectedAsset(asset)) { selectedAssets.push(asset) } else { selectedAssets = selectedAssets.filter(selectedAsset => { @@ -167,11 +159,23 @@ class MediaLibrary extends React.Component { }) } this.setState({ selectedAssets }); + } + + handleAssetCheckboxChange = (asset, event) => { event.stopPropagation(); + this.updateSelectedAssets(asset); + }; + + handleAssetClick = asset => { + if (asset.isDirectory) { + this.setState({ currentMediaFolder: asset.path, selectedAssets: [] }); + } else { + this.updateSelectedAssets(asset); + } }; handleBreadcrumbClick = currentMediaFolder => { - this.setState({ currentMediaFolder }); + this.setState({ currentMediaFolder, selectedAssets: [] }); }; /** diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index d245c96cd802..fdbbb9f616d3 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -112,7 +112,6 @@ class MediaLibraryCard extends React.Component { displayURL, text, onClick, - onAssetCheckboxClick, onChecked, draftText, width, @@ -152,7 +151,7 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - + {previewElement} {text} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index cd8fe4b3aafa..7d2dec8f7ccc 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -47,7 +47,6 @@ const CardWrapper = props => { onAssetClick(file)} From 7b0bcc66c385994c4075a576056084afd10beaed Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 22 Dec 2020 10:49:16 +0200 Subject: [PATCH 13/45] feat(media dir nav): delete button enabled when at least one media assets selected --- .../MediaLibrary/MediaLibraryCard.js | 62 ++++++++++++++++++- .../MediaLibrary/MediaLibraryModal.js | 2 +- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index fdbbb9f616d3..effa7a65d552 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -16,7 +16,7 @@ const Card = styled.div` cursor: pointer; overflow: hidden; background-color: ${props => props.isPrivate && colors.textFieldBorder}; - + position: relative; &:focus { outline: none; } @@ -85,6 +85,61 @@ const DraftText = styled.p` border-radius: ${lengths.borderRadius} 0px ${lengths.borderRadius} 0; `; +const radioButtonCheckboxWidthHeight = '24px'; +const inputBorderColor = '#fff'; +const inputBackgroundColor = 'rgba(0, 0, 0, 0.2)'; +const inputHoverBorderColor = '#33BCB0'; +const radioCheckboxSelectedBackgroundColor = '#33BCB0'; +const svgDataRriIconTick = `url('data:image/svg+xml;utf8,')`; +const radioCheckboxSelectedBackgroundPosition = 'center 6px'; +const radioCheckboxSelectedBackgroundSize = '13px'; +const inputFocusBorderColor = '#fff'; + +const AssetCheckbox = styled.input` + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; + cursor: pointer; + left: 0; + top: 0; + width: ${radioButtonCheckboxWidthHeight}; + height: ${radioButtonCheckboxWidthHeight}; + padding: 0 !important; + background: transparent !important; + border-width: 0 !important; + &:after { + border: 1px solid ${inputBorderColor}; + content: ""; + background-color: ${inputBackgroundColor}; + width: ${radioButtonCheckboxWidthHeight}; + height: ${radioButtonCheckboxWidthHeight}; + border-radius: 50%; + position: absolute; + top: 0; + left: 0px; + + } + &:checked:after, &.checked:after { + background: ${radioCheckboxSelectedBackgroundColor} ${svgDataRriIconTick} no-repeat ${radioCheckboxSelectedBackgroundPosition}; + background-size: ${radioCheckboxSelectedBackgroundSize}; + + } + &:focus:after, &:focus:checked:after { + border-color: ${inputFocusBorderColor}; + } + &:hover:after { + border-color ${inputHoverBorderColor}; + } +`; + +const CheckboxContainer = styled.div` + position: absolute; + top: 0; + left: 0; + z-index: 1; +`; + function readableFileSize(numberOfBytes) { let readableSize; let units; @@ -151,12 +206,15 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - + + + {previewElement} {text} {isViewableImage ? {type} - {readableFileSize(size)} : null} +
); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index a984b3e38a24..2a158f14e7af 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -112,7 +112,7 @@ const MediaLibraryModal = ({ (!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) || (!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults')); - const hasSelection = hasMedia && !isEmpty(selectedFile); + const hasSelection = hasMedia && (selectedAssets || []).length > 0; return ( From 97709e34b03fe718fef7962bebd58a65267ee4a3 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 22 Dec 2020 12:02:44 +0200 Subject: [PATCH 14/45] feat(media dir nav): synchronous deletion of multiple files --- .../src/implementation.tsx | 1 + .../components/MediaLibrary/MediaLibrary.js | 18 +++++++++++------- .../MediaLibrary/MediaLibraryCard.js | 15 +++++++++------ .../MediaLibrary/MediaLibraryCardGrid.js | 1 + packages/netlify-cms-ui-default/src/Icon.js | 1 + 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index c28f464b5c2a..fb7191d7c14a 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -451,6 +451,7 @@ export default class GitHub implements Implementation { displayURL: { id, path }, path, isDirectory: type === TreeFileType.TREE, + hasChildren: files.filter(file => file.path.startsWith(path)).length > 1 }; }); return withDisplayUrls; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 3be92aa20cd6..7d07c9f6ca07 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -111,7 +111,7 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory }) => { + files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory, hasChildren }) => { const ext = fileExtension(name).toLowerCase(); return { key, @@ -124,6 +124,7 @@ class MediaLibrary extends React.Component { displayURL, draft, isDirectory, + hasChildren, isImage: IMAGE_EXTENSIONS.includes(ext), isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), }; @@ -230,16 +231,19 @@ class MediaLibrary extends React.Component { /** * Removes the selected file from the backend. */ - handleDelete = () => { - const { selectedFile } = this.state; + handleDelete = async () => { + const { selectedAssets } = this.state; const { files, deleteMedia, privateUpload, t } = this.props; if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) { return; } - const file = files.find(file => selectedFile.key === file.key); - deleteMedia(file, { privateUpload }).then(() => { - this.setState({ selectedFile: {} }); - }); + const filesToDelete = selectedAssets.map(selectedAsset => + files.find(file => selectedAsset.key === file.key) + ) + for (const file of filesToDelete) { + await deleteMedia(file, { privateUpload }); + } + this.setState({ selectedAssets: [] }); }; /** diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index effa7a65d552..507b7f5ef72c 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -177,7 +177,8 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, - size + size, + hasChildren } = this.props; const url = displayURL.get('url'); var cardImageWrapper = ( @@ -192,7 +193,7 @@ class MediaLibraryCard extends React.Component { ); var cardDirectoryEl = ( - + ); var previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; @@ -206,15 +207,17 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - - - + {!hasChildren ? ( + + + + ) : null} + {previewElement} {text} {isViewableImage ? {type} - {readableFileSize(size)} : null} - ); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 7d2dec8f7ccc..76cc837c05f5 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -62,6 +62,7 @@ const CardWrapper = props => { type={file.type} isViewableImage={file.isViewableImage} isDirectory={file.isDirectory} + hasChildren={file.hasChildren} size={file.size} /> diff --git a/packages/netlify-cms-ui-default/src/Icon.js b/packages/netlify-cms-ui-default/src/Icon.js index cd8a8e363a9b..b4f88644d210 100644 --- a/packages/netlify-cms-ui-default/src/Icon.js +++ b/packages/netlify-cms-ui-default/src/Icon.js @@ -49,6 +49,7 @@ const sizes = { small: '18px', medium: '24px', large: '32px', + max: '100%', }; const Icon = ({ type, direction, size = 'medium', className }) => { From cada8c9266f3050648c0294fbdeecc2bc9d71c26 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Tue, 12 Jan 2021 16:07:53 +0100 Subject: [PATCH 15/45] refactor: code cleanup --- .../src/implementation.tsx | 2 +- .../src/actions/mediaLibrary.ts | 19 +- .../components/MediaLibrary/MediaLibrary.js | 61 ++-- .../MediaLibrary/MediaLibraryCard.js | 31 +- .../MediaLibrary/MediaLibraryCardGrid.js | 9 +- .../MediaLibrary/MediaLibraryModal.js | 7 +- .../MediaLibraryCard.spec.js.snap | 336 ++++++++++++++++-- .../netlify-cms-core/src/reducers/entries.ts | 6 +- 8 files changed, 381 insertions(+), 90 deletions(-) diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index fb7191d7c14a..d9c7900c64f9 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -451,7 +451,7 @@ export default class GitHub implements Implementation { displayURL: { id, path }, path, isDirectory: type === TreeFileType.TREE, - hasChildren: files.filter(file => file.path.startsWith(path)).length > 1 + hasChildren: files.filter(file => file.path.startsWith(path)).length > 1, }; }); return withDisplayUrls; diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index ec8ad4ef7d00..a7e00eccc328 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -217,7 +217,14 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug')); const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); + const path = selectMediaFilePath( + state.config, + collection, + entry, + fileName, + field, + currentMediaFolder, + ); const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path); const editingDraft = selectEditingDraft(state.entryDraft); @@ -264,7 +271,14 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - const path = selectMediaFilePath(state.config, collection, entry, fileName, field, currentMediaFolder); + const path = selectMediaFilePath( + state.config, + collection, + entry, + fileName, + field, + currentMediaFolder, + ); assetProxy = createAssetProxy({ file, path, @@ -414,6 +428,7 @@ export function mediaLoading(page: number) { interface MediaOptions { privateUpload?: boolean; field?: EntryField; + currentMediaFolder?: string; } export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) { diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 7d07c9f6ca07..b658793b5666 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -111,24 +111,37 @@ class MediaLibrary extends React.Component { toTableData = files => { const tableData = files && - files.map(({ key, name, id, size, path, queryOrder, displayURL, draft, isDirectory, hasChildren }) => { - const ext = fileExtension(name).toLowerCase(); - return { + files.map( + ({ key, - id, name, - path, - type: ext.toUpperCase(), + id, size, + path, queryOrder, displayURL, draft, isDirectory, hasChildren, - isImage: IMAGE_EXTENSIONS.includes(ext), - isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), - }; - }); + }) => { + const ext = fileExtension(name).toLowerCase(); + return { + key, + id, + name, + path, + type: ext.toUpperCase(), + size, + queryOrder, + displayURL, + draft, + isDirectory, + hasChildren, + isImage: IMAGE_EXTENSIONS.includes(ext), + isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext), + }; + }, + ); /** * Get the sort order for use with `lodash.orderBy`, and always add the @@ -145,22 +158,24 @@ class MediaLibrary extends React.Component { }; isSelectedAsset = asset => { - return (this.state.selectedAssets || []).filter(selectedAsset => { - return selectedAsset.key === asset.key - }).length > 0; - } + return ( + (this.state.selectedAssets || []).filter(selectedAsset => { + return selectedAsset.key === asset.key; + }).length > 0 + ); + }; - updateSelectedAssets = (asset) => { + updateSelectedAssets = asset => { let selectedAssets = this.state.selectedAssets || []; if (!this.isSelectedAsset(asset)) { - selectedAssets.push(asset) + selectedAssets.push(asset); } else { selectedAssets = selectedAssets.filter(selectedAsset => { return selectedAsset.key !== asset.key; - }) + }); } this.setState({ selectedAssets }); - } + }; handleAssetCheckboxChange = (asset, event) => { event.stopPropagation(); @@ -237,9 +252,9 @@ class MediaLibrary extends React.Component { if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) { return; } - const filesToDelete = selectedAssets.map(selectedAsset => - files.find(file => selectedAsset.key === file.key) - ) + const filesToDelete = selectedAssets.map(selectedAsset => + files.find(file => selectedAsset.key === file.key), + ); for (const file of filesToDelete) { await deleteMedia(file, { privateUpload }); } @@ -346,7 +361,9 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); - const currentDirFilesOrderedByTreeType = (currentDirFiles || []).filter(file => file.isDirectory).concat((currentDirFiles || []).filter(file => !file.isDirectory)); + const currentDirFilesOrderedByTreeType = (currentDirFiles || []) + .filter(file => file.isDirectory) + .concat((currentDirFiles || []).filter(file => !file.isDirectory)); return ( ) : null} - + {previewElement} {text} - {isViewableImage ? {type} - {readableFileSize(size)} : null} + {isViewableImage ? ( + + {type} - {readableFileSize(size)} + + ) : null} ); @@ -233,7 +238,7 @@ MediaLibraryCard.propTypes = { isSelected: PropTypes.bool, displayURL: ImmutablePropTypes.map.isRequired, text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, draftText: PropTypes.string.isRequired, width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 76cc837c05f5..0380099b8840 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -15,7 +15,6 @@ const CardWrapper = props => { style, data: { mediaItems, - isSelectedFile, isSelectedAsset, onAssetClick, onAssetCheckboxClick, @@ -44,13 +43,14 @@ const CardWrapper = props => { height: style.height - gutter, }} > - onAssetClick(file)} - onChecked={(event) => {onAssetCheckboxClick(file, event)}} + onChecked={event => { + onAssetCheckboxClick(file, event); + }} isDraft={file.draft} draftText={cardDraftText} width={cardWidth} @@ -105,7 +105,6 @@ const VirtualizedGrid = props => { const PaginatedGrid = ({ setScrollContainerRef, mediaItems, - isSelectedFile, isSelectedAsset, onAssetClick, onAssetCheckboxClick, @@ -127,7 +126,6 @@ const PaginatedGrid = ({ {mediaItems.map(file => ( onAssetClick(file)} @@ -190,7 +188,6 @@ MediaLibraryCardGrid.propTypes = { draft: PropTypes.bool, }), ).isRequired, - isSelectedFile: PropTypes.func.isRequired, onAssetClick: PropTypes.func.isRequired, canLoadMore: PropTypes.bool, onLoadMore: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index 2a158f14e7af..62ba2f7250aa 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { Map } from 'immutable'; -import { isEmpty } from 'lodash'; import { translate } from 'react-polyglot'; import { Modal } from 'UI'; import MediaLibraryTop from './MediaLibraryTop'; @@ -74,7 +73,6 @@ const MediaLibraryModal = ({ isPaginating, privateUpload, query, - selectedFile, selectedAssets, handleFilter, handleQuery, @@ -144,8 +142,9 @@ const MediaLibraryModal = ({ selectedFile.key === file.key} - isSelectedAsset={file => (selectedAssets || []).filter(selectedAsset => selectedAsset.key === file.key).length > 0} + isSelectedAsset={file => + (selectedAssets || []).filter(selectedAsset => selectedAsset.key === file.key).length > 0 + } onAssetClick={handleAssetClick} onAssetCheckboxClick={handleAssetCheckboxChange} canLoadMore={hasNextPage} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap index 452ba837de50..0a641e16891c 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +++ b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap @@ -2,7 +2,7 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` - .emotion-8 { + .emotion-16 { width: 100px; height: 240px; margin: 10px; @@ -10,13 +10,63 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; + position: relative; } -.emotion-8:focus { +.emotion-16:focus { outline: none; } -.emotion-4 { +.emotion-2 { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + +.emotion-0 { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; + cursor: pointer; + left: 0; + top: 0; + width: 24px; + height: 24px; + padding: 0 !important; + background: transparent !important; + border-width: 0 !important; +} + +.emotion-0:after { + border: 1px solid #fff; + content: ''; + background-color: rgba(0,0,0,0.2); + width: 24px; + height: 24px; + border-radius: 50%; + position: absolute; + top: 0; + left: 0px; +} + +.emotion-0:hover:after { + border-color: #33BCB0; +} + +.emotion-0:checked:after, +.emotion-0.checked:after { + background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; + background-size: 13px; +} + +.emotion-0:focus:after, +.emotion-0:focus:checked:after { + border-color: #fff; +} + +.emotion-8 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -27,22 +77,43 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` position: relative; } -.emotion-2 { +.emotion-6 { width: 100%; height: 160px; - object-fit: contain; + object-fit: cover; border-radius: 2px 2px 0 0; } -.emotion-6 { +.emotion-14 { color: #798291; padding: 8px; - margin-top: 20px; + font-size: 0.8em; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} + +.emotion-10 { + color: #798291; + font-weight: 600; overflow-wrap: break-word; line-height: 1.3 !important; + text-align: left; } -.emotion-0 { +.emotion-12 { + text-align: left; +} + +.emotion-4 { color: #70399f; background-color: #f6d8ff; position: absolute; @@ -51,37 +122,55 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` }
+ +
+

Draft

-

- image.png -

+
+ image.png +
+
+ - 0 bytes +
+
`; exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` - .emotion-6 { + .emotion-14 { width: 100px; height: 240px; margin: 10px; @@ -89,13 +178,63 @@ exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; + position: relative; } -.emotion-6:focus { +.emotion-14:focus { outline: none; } .emotion-2 { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + +.emotion-0 { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; + cursor: pointer; + left: 0; + top: 0; + width: 24px; + height: 24px; + padding: 0 !important; + background: transparent !important; + border-width: 0 !important; +} + +.emotion-0:after { + border: 1px solid #fff; + content: ''; + background-color: rgba(0,0,0,0.2); + width: 24px; + height: 24px; + border-radius: 50%; + position: absolute; + top: 0; + left: 0px; +} + +.emotion-0:hover:after { + border-color: #33BCB0; +} + +.emotion-0:checked:after, +.emotion-0.checked:after { + background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; + background-size: 13px; +} + +.emotion-0:focus:after, +.emotion-0:focus:checked:after { + border-color: #fff; +} + +.emotion-6 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -106,23 +245,44 @@ exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` position: relative; } -.emotion-0 { +.emotion-4 { width: 100%; height: 160px; - object-fit: contain; + object-fit: cover; border-radius: 2px 2px 0 0; } -.emotion-4 { +.emotion-12 { color: #798291; padding: 8px; - margin-top: 20px; + font-size: 0.8em; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} + +.emotion-8 { + color: #798291; + font-weight: 600; overflow-wrap: break-word; line-height: 1.3 !important; + text-align: left; +} + +.emotion-10 { + text-align: left; }
- +
+
+
-

- image.png -

+
+ image.png +
+
+ - 0 bytes +
+
`; exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` - .emotion-6 { + .emotion-12 { width: 100px; height: 240px; margin: 10px; @@ -154,13 +332,63 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; + position: relative; } -.emotion-6:focus { +.emotion-12:focus { outline: none; } .emotion-2 { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + +.emotion-0 { + display: inline-block; + -webkit-appearance: none; + -moz-appearance: none; + position: relative; + cursor: pointer; + left: 0; + top: 0; + width: 24px; + height: 24px; + padding: 0 !important; + background: transparent !important; + border-width: 0 !important; +} + +.emotion-0:after { + border: 1px solid #fff; + content: ''; + background-color: rgba(0,0,0,0.2); + width: 24px; + height: 24px; + border-radius: 50%; + position: absolute; + top: 0; + left: 0px; +} + +.emotion-0:hover:after { + border-color: #33BCB0; +} + +.emotion-0:checked:after, +.emotion-0.checked:after { + background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; + background-size: 13px; +} + +.emotion-0:focus:after, +.emotion-0:focus:checked:after { + border-color: #fff; +} + +.emotion-6 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -171,15 +399,32 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` position: relative; } -.emotion-4 { +.emotion-10 { color: #798291; padding: 8px; - margin-top: 20px; + font-size: 0.8em; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} + +.emotion-8 { + color: #798291; + font-weight: 600; overflow-wrap: break-word; line-height: 1.3 !important; + text-align: left; } -.emotion-0 { +.emotion-4 { width: 100%; height: 160px; object-fit: cover; @@ -189,7 +434,7 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` }
-
+
+
+
Not Viewable
-

- image.png -

+
+ image.png +
+
`; diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 40380eab7e56..19658b0031c8 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -730,10 +730,10 @@ export const selectMediaFolder = ( collection: Collection | null, entryMap: EntryMap | undefined, field: EntryField | undefined, - currentMediaFolder: string, + currentMediaFolder?: string, ) => { if (currentMediaFolder) { - return trim(currentMediaFolder, '/') + return trim(currentMediaFolder, '/'); } const name = 'media_folder'; let mediaFolder = config.get(name); @@ -761,7 +761,7 @@ export const selectMediaFilePath = ( entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, - currentMediaFolder: string, + currentMediaFolder?: string, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; From bfec05d6ec3e76d7e79aa7056691ce98d1f8a91f Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 18 Jan 2021 13:46:37 +0200 Subject: [PATCH 16/45] feat(media dir nav): current media folder initially derived from filepaths --- .../src/components/MediaLibrary/MediaLibrary.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index b658793b5666..1ab9f98be187 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -340,6 +340,19 @@ class MediaLibrary extends React.Component { return matchFiles; }; + deriveCurrentMediaFolder(files = []) { + var shortestPath = files.map(file => { + return { + path: file.path, + segments: file.path.split('/') + } + }).sort((a,b) => { + return a.segments.length-b.segments.length; + })[0] || {}; + var length = shortestPath.segments ? shortestPath.segments.length - 1 : 0; + return (shortestPath.segments || []).slice(0,length).join('/'); + } + render() { const { isVisible, @@ -359,7 +372,7 @@ class MediaLibrary extends React.Component { t, } = this.props; - const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; + const currentMediaFolder = this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFilesOrderedByTreeType = (currentDirFiles || []) .filter(file => file.isDirectory) From efdc470d68e5516819cccbe47443899560b41c2b Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 18 Jan 2021 15:42:13 +0200 Subject: [PATCH 17/45] feat(media dir nav): nav with collections works and is optional --- dev-test/config.yml | 3 +-- .../src/components/MediaLibrary/MediaLibrary.js | 2 +- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 10 +++++++--- packages/netlify-cms-core/src/types/redux.ts | 1 + 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index db2ab5683a18..a6772859801d 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -16,6 +16,7 @@ collections: # A list of collections the CMS should be able to edit guidelines that are specific to a collection. folder: 'content/posts' media_folder: '/static/images/one' + disable_media_folder_navigation: false slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection @@ -51,9 +52,7 @@ collections: # A list of collections the CMS should be able to edit widget: 'image' required: false tagname: '' - - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' } - - name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit label: 'FAQ' # Used in the UI folder: '_faqs' diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 1ab9f98be187..c467d8f39762 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -101,7 +101,7 @@ class MediaLibrary extends React.Component { filterImages = files => { return files.filter(file => { const ext = fileExtension(file.name).toLowerCase(); - return IMAGE_EXTENSIONS.includes(ext); + return IMAGE_EXTENSIONS.includes(ext) || file.isDirectory; }); }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index b72196d19055..34afc8a23780 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -227,9 +227,13 @@ export function selectMediaFiles(state: State, field?: EntryField) { const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - files = entryFiles - .filter(f => dirname(f.path) === mediaFolder) - .map(file => ({ key: file.id, ...file })); + if (collection.get('disable_media_folder_navigation')) { + files = entryFiles + .filter(f => { return (dirname(f.path) === mediaFolder) && !f.isDirectory}) + } else { + files = entryFiles; + } + files = files.map(file => ({ key: file.id, ...file })); } else { files = mediaLibrary.get('files') || []; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index d723b78cdbc9..6e9d156d3e06 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -199,6 +199,7 @@ type CollectionObject = { fields: EntryFields; isFetching: boolean; media_folder?: string; + disable_media_folder_navigation?: string; public_folder?: string; preview_path?: string; preview_path_date_field?: string; From 7d26160d4682069764f2e6fef4695c64490ae579 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 18 Jan 2021 16:21:02 +0200 Subject: [PATCH 18/45] feat(media dir nav): disabling dir nav works for fields and collections --- dev-test/config.yml | 8 +++++++- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 9 ++++++++- packages/netlify-cms-core/src/types/redux.ts | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index a6772859801d..e5e503d98601 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -16,7 +16,6 @@ collections: # A list of collections the CMS should be able to edit guidelines that are specific to a collection. folder: 'content/posts' media_folder: '/static/images/one' - disable_media_folder_navigation: false slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection @@ -47,11 +46,18 @@ collections: # A list of collections the CMS should be able to edit time_format: 'HH:mm', format: 'YYYY-MM-DD HH:mm', } + - label: 'Other Image' + name: 'other' + widget: 'image' + required: false + tagname: '' - label: 'Cover Image' name: 'image' widget: 'image' required: false tagname: '' + media_folder: '/static/images/one/one-deeper' + disable_media_folder_navigation: true - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' } - name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit label: 'FAQ' # Used in the UI diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index 34afc8a23780..4d0c8c79a98a 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -227,7 +227,14 @@ export function selectMediaFiles(state: State, field?: EntryField) { const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - if (collection.get('disable_media_folder_navigation')) { + let disableMediaFolderNav; + + if (field && field.has('disable_media_folder_navigation')) { + disableMediaFolderNav = field.get('disable_media_folder_navigation'); + } else if (collection && collection.has('disable_media_folder_navigation')) { + disableMediaFolderNav = collection.get('disable_media_folder_navigation'); + } + if (disableMediaFolderNav) { files = entryFiles .filter(f => { return (dirname(f.path) === mediaFolder) && !f.isDirectory}) } else { diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 6e9d156d3e06..54512d5832ef 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -135,6 +135,7 @@ export type EntryField = StaticallyTypedRecord<{ default: string | null | boolean | List; media_folder?: string; public_folder?: string; + disable_media_folder_navigation?: string; comment?: string; meta?: boolean; i18n: 'translate' | 'duplicate' | 'none'; From 8fe35ca4768ef8b50d9a2da67e42be8188b62e45 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 18 Jan 2021 16:24:58 +0200 Subject: [PATCH 19/45] feat(media dir nav): current media navigation forgotten when user closes media library --- .../netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index c467d8f39762..c63d975dfb3a 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -154,6 +154,7 @@ class MediaLibrary extends React.Component { }; handleClose = () => { + this.setState({ currentMediaFolder: null}); this.props.closeMediaLibrary(); }; From 318c0c0a608582477812c4b0ce33ff2282ca31c6 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Thu, 28 Jan 2021 19:41:01 +0100 Subject: [PATCH 20/45] chore: fix linting, formating, tests --- packages/netlify-cms-core/src/backend.ts | 1 + .../components/MediaLibrary/MediaLibrary.js | 26 +++++++++++-------- .../MediaLibrary/MediaLibraryCard.js | 2 +- .../MediaLibrary/MediaLibraryCardGrid.js | 1 - .../src/reducers/mediaLibrary.ts | 11 ++++---- packages/netlify-cms-core/src/types/redux.ts | 4 +-- .../src/implementation.ts | 1 + 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 0516a2e6cb24..106437fb6258 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -251,6 +251,7 @@ export interface MediaFile { url?: string; file?: File; field?: EntryField; + isDirectory?: boolean; } interface BackupEntry { diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index c63d975dfb3a..caa842989a7e 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -154,7 +154,7 @@ class MediaLibrary extends React.Component { }; handleClose = () => { - this.setState({ currentMediaFolder: null}); + this.setState({ currentMediaFolder: null }); this.props.closeMediaLibrary(); }; @@ -342,16 +342,19 @@ class MediaLibrary extends React.Component { }; deriveCurrentMediaFolder(files = []) { - var shortestPath = files.map(file => { - return { - path: file.path, - segments: file.path.split('/') - } - }).sort((a,b) => { - return a.segments.length-b.segments.length; - })[0] || {}; + var shortestPath = + files + .map(file => { + return { + path: file.path, + segments: file.path.split('/'), + }; + }) + .sort((a, b) => { + return a.segments.length - b.segments.length; + })[0] || {}; var length = shortestPath.segments ? shortestPath.segments.length - 1 : 0; - return (shortestPath.segments || []).slice(0,length).join('/'); + return (shortestPath.segments || []).slice(0, length).join('/'); } render() { @@ -373,7 +376,8 @@ class MediaLibrary extends React.Component { t, } = this.props; - const currentMediaFolder = this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); + const currentMediaFolder = + this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFilesOrderedByTreeType = (currentDirFiles || []) .filter(file => file.isDirectory) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index d738737ba3e3..fbac8992ce08 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -238,7 +238,7 @@ MediaLibraryCard.propTypes = { isSelected: PropTypes.bool, displayURL: ImmutablePropTypes.map.isRequired, text: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, draftText: PropTypes.string.isRequired, width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index bb687e6b2a41..0380099b8840 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -43,7 +43,6 @@ const CardWrapper = props => { height: style.height - gutter, }} > - { return (dirname(f.path) === mediaFolder) && !f.isDirectory}) + files = entryFiles.filter(f => { + return dirname(f.path) === mediaFolder && !f.isDirectory; + }); } else { files = entryFiles; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 54512d5832ef..b32a1c853d43 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -135,7 +135,7 @@ export type EntryField = StaticallyTypedRecord<{ default: string | null | boolean | List; media_folder?: string; public_folder?: string; - disable_media_folder_navigation?: string; + disable_media_folder_navigation: boolean; comment?: string; meta?: boolean; i18n: 'translate' | 'duplicate' | 'none'; @@ -200,7 +200,7 @@ type CollectionObject = { fields: EntryFields; isFetching: boolean; media_folder?: string; - disable_media_folder_navigation?: string; + disable_media_folder_navigation: boolean; public_folder?: string; preview_path?: string; preview_path_date_field?: string; diff --git a/packages/netlify-cms-lib-util/src/implementation.ts b/packages/netlify-cms-lib-util/src/implementation.ts index 350d53a4792b..6c96d4777d7e 100644 --- a/packages/netlify-cms-lib-util/src/implementation.ts +++ b/packages/netlify-cms-lib-util/src/implementation.ts @@ -18,6 +18,7 @@ export interface ImplementationMediaFile { draft?: boolean; url?: string; file?: File; + isDirectory?: boolean; } export interface UnpublishedEntryMediaFile { From 198c4b1fc55ff609bb2adf94bd44a40e4a5ab15b Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 15 Feb 2021 14:25:46 +0200 Subject: [PATCH 21/45] feat(media dir nav): empty folder creation --- .../components/MediaLibrary/MediaLibrary.js | 30 ++++++- .../MediaLibrary/MediaLibraryButtons.js | 5 ++ .../MediaLibraryCreateFolder-old.js | 61 ++++++++++++++ .../MediaLibrary/MediaLibraryCreateFolder.js | 84 +++++++++++++++++++ .../MediaLibrary/MediaLibraryModal.js | 5 ++ .../MediaLibrary/MediaLibraryTop.js | 9 ++ .../src/Icon/images/_index.js | 2 + .../src/Icon/images/not-allowed.svg | 1 + 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js create mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js create mode 100644 packages/netlify-cms-ui-default/src/Icon/images/not-allowed.svg diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index b6f350c0bcfa..f4c73374d28b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -272,6 +272,30 @@ class MediaLibrary extends React.Component { this.setState({ selectedAssets: [] }); }; + handleCreateFolder = dirName => { + this.setState({ isLoading: true}) + function byteToHex(byte) { + return ('0' + byte.toString(16)).slice(-2); + } + function generateId(len = 40) { + var arr = new Uint8Array(len / 2); + window.crypto.getRandomValues(arr); + return Array.from(arr, byteToHex).join(""); + } + const { defaultMediaFolder } = this.props; + const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; + this.props.files.push({ + id: generateId(40), + hasChildren: false, + key: generateId(40), + name: dirName, + path: `${currentMediaFolder}/${dirName}`, + displayURL: {path: `${currentMediaFolder}/${dirName}`}, + isDirectory: true + }); + this.setState({ isLoading: false}) + } + /** * Downloads the selected file. */ @@ -389,14 +413,15 @@ class MediaLibrary extends React.Component { const currentMediaFolder = this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); - const currentDirFilesOrderedByTreeType = (currentDirFiles || []) - .filter(file => file.isDirectory) + const currentDirFolders = (currentDirFiles || []).filter(file => file.isDirectory); + const currentDirFilesOrderedByTreeType = currentDirFolders .concat((currentDirFiles || []).filter(file => !file.isDirectory)); return ( (this.scrollContainerRef = ref)} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js index 80b4f48e53a1..ab75009dd8ab 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js @@ -59,6 +59,11 @@ export const InsertButton = styled.button` ${buttons.green}; `; +export const CreateFolderButton = styled.button` + ${styles.button}; + ${buttons.gray}; +`; + const ActionButton = styled.button` ${styles.button}; ${props => diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js new file mode 100644 index 000000000000..80292f91d0cb --- /dev/null +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js @@ -0,0 +1,61 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Icon, lengths, colors, zIndex } from 'netlify-cms-ui-default'; + +const CreateFolderContainer = styled.div` + height: 37px; + display: flex; + align-items: center; + position: relative; + width: 400px; +`; + +const CreateFolderInput = styled.input` + background-color: #eff0f4; + border-radius: ${lengths.borderRadius}; + + font-size: 14px; + padding: 10px 6px 10px 32px; + width: 100%; + position: relative; + z-index: ${zIndex.zIndex1}; + + &:focus { + outline: none; + box-shadow: inset 0 0 0 2px ${colors.active}; + } +`; + +const CreateFolderIcon = styled(Icon)` + position: absolute; + top: 50%; + left: 6px; + z-index: ${zIndex.zIndex2}; + transform: translate(0, -50%); +`; + +function MediaLibraryCreateFolder({ value, onChange, onKeyDown, placeholder, disabled }) { + return ( + + + + + ); +} + +MediaLibraryCreateFolder.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func.isRequired, + onKeyDown: PropTypes.func.isRequired, + placeholder: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + +export default MediaLibraryCreateFolder; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js new file mode 100644 index 000000000000..28096697b2b9 --- /dev/null +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { Icon, lengths, colors, zIndex } from 'netlify-cms-ui-default'; + +const CreateFolderContainer = styled.div` + height: 37px; + display: flex; + align-items: center; + position: relative; + width: 400px; +`; + +const CreateFolderInput = styled.input` + background-color: #eff0f4; + border-radius: ${lengths.borderRadius}; + + font-size: 14px; + padding: 10px 6px 10px 32px; + width: 100%; + position: relative; + z-index: ${zIndex.zIndex1}; + + &:focus { + outline: none; + box-shadow: inset 0 0 0 2px ${colors.active}; + } +`; + +const CreateFolderIcon = styled(Icon)` + position: absolute; + top: 50%; + left: 6px; + z-index: ${zIndex.zIndex2}; + transform: translate(0, -50%); +`; + +class MediaLibraryCreateFolder extends React.Component { + constructor(props) { + super(props); + this.state = {value: '', icon: 'folder'}; + + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + } + + handleChange(event) { + const { folders } = this.props; + this.setState({value: event.target.value, folderExists: folders.find(folder => folder.name === event.target.value)}); + } + + handleKeyDown(event) { + const { onKeyDown } = this.props; + if (event.key === 'Enter' && !this.state.folderExists) { + onKeyDown(this.state.value); + this.setState({value: ''}); + } + } + + render() { + const { placeholder } = this.props; + return ( + + + + + ); + } +} + +MediaLibraryCreateFolder.propTypes = { + value: PropTypes.string, + onKeyDown: PropTypes.func.isRequired, + placeholder: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + +export default MediaLibraryCreateFolder; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index c96e0b5031c6..e605d8538b53 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -63,6 +63,7 @@ function MediaLibraryModal({ isVisible, canInsert, files, + folders, dynamicSearch, dynamicSearchActive, forImage, @@ -82,6 +83,7 @@ function MediaLibraryModal({ handleSearchKeyDown, handlePersist, handleDelete, + handleCreateFolder, handleInsert, handleDownload, setScrollContainerRef, @@ -127,6 +129,7 @@ function MediaLibraryModal({ onSearchKeyDown={handleSearchKeyDown} searchDisabled={!dynamicSearchActive && !hasFilteredFiles} onDelete={handleDelete} + onCreateFolder={handleCreateFolder} canInsert={canInsert} onInsert={handleInsert} hasSelection={hasSelection} @@ -136,6 +139,7 @@ function MediaLibraryModal({ currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} selectedFile={selectedFile} + folders={folders} /> {!shouldShowEmptyMessage ? null : ( @@ -199,6 +203,7 @@ MediaLibraryModal.propTypes = { handlePersist: PropTypes.func.isRequired, handleDelete: PropTypes.func.isRequired, handleInsert: PropTypes.func.isRequired, + handleCreateFolder: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, handleAssetCheckboxChange: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index 507fb9b05a66..48a1b3935cf9 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import MediaLibrarySearch from './MediaLibrarySearch'; +import MediaLibraryCreateFolder from './MediaLibraryCreateFolder'; import MediaLibraryHeader from './MediaLibraryHeader'; import MediaLibraryBreadcrumbs from './MediaLibraryBreadCrumbs'; import { @@ -38,6 +39,7 @@ function MediaLibraryTop({ onSearchChange, onSearchKeyDown, searchDisabled, + onCreateFolder, onDelete, canInsert, onInsert, @@ -48,6 +50,7 @@ function MediaLibraryTop({ currentMediaFolder, defaultMediaFolder, selectedFile, + folders, }) { const shouldShowButtonLoader = isPersisting || isDeleting; const uploadEnabled = !shouldShowButtonLoader; @@ -101,6 +104,11 @@ function MediaLibraryTop({ placeholder={t('mediaLibrary.mediaLibraryModal.search')} disabled={searchDisabled} /> + {deleteButtonLabel} @@ -133,6 +141,7 @@ MediaLibraryTop.propTypes = { query: PropTypes.string, onSearchChange: PropTypes.func.isRequired, onSearchKeyDown: PropTypes.func.isRequired, + onCreateFolder: PropTypes.func.isRequired, searchDisabled: PropTypes.bool.isRequired, onDelete: PropTypes.func.isRequired, canInsert: PropTypes.bool, diff --git a/packages/netlify-cms-ui-default/src/Icon/images/_index.js b/packages/netlify-cms-ui-default/src/Icon/images/_index.js index f774b2d15460..c06927309da9 100644 --- a/packages/netlify-cms-ui-default/src/Icon/images/_index.js +++ b/packages/netlify-cms-ui-default/src/Icon/images/_index.js @@ -33,6 +33,7 @@ import iconMediaAlt from './media-alt.svg'; import iconNetlify from './netlify.svg'; import iconNetlifyCms from './netlify-cms-logo.svg'; import iconNewTab from './new-tab.svg'; +import iconNotAllowed from './not-allowed.svg'; import iconPage from './page.svg'; import iconPages from './pages.svg'; import iconPagesAlt from './pages-alt.svg'; @@ -83,6 +84,7 @@ const images = { netlify: iconNetlify, 'netlify-cms': iconNetlifyCms, 'new-tab': iconNewTab, + 'not-allowed': iconNotAllowed, page: iconPage, pages: iconPages, 'pages-alt': iconPagesAlt, diff --git a/packages/netlify-cms-ui-default/src/Icon/images/not-allowed.svg b/packages/netlify-cms-ui-default/src/Icon/images/not-allowed.svg new file mode 100644 index 000000000000..d603157e99f4 --- /dev/null +++ b/packages/netlify-cms-ui-default/src/Icon/images/not-allowed.svg @@ -0,0 +1 @@ + \ No newline at end of file From eb41f0ee0cdae3450cadd5dab737e28b41567a03 Mon Sep 17 00:00:00 2001 From: Andrew Paterson Date: Mon, 15 Feb 2021 17:34:44 +0200 Subject: [PATCH 22/45] Update packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js Co-authored-by: Valentin Nourdin --- .../src/components/MediaLibrary/MediaLibraryTop.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index 48a1b3935cf9..5f601f572180 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -106,7 +106,7 @@ function MediaLibraryTop({ /> From 3b822c4bc53d42b9c084929257645776000fa2a5 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 15 Feb 2021 17:38:51 +0200 Subject: [PATCH 23/45] feat(media dir nav): remove junk file --- .../MediaLibraryCreateFolder-old.js | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js deleted file mode 100644 index 80292f91d0cb..000000000000 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder-old.js +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import styled from '@emotion/styled'; -import { Icon, lengths, colors, zIndex } from 'netlify-cms-ui-default'; - -const CreateFolderContainer = styled.div` - height: 37px; - display: flex; - align-items: center; - position: relative; - width: 400px; -`; - -const CreateFolderInput = styled.input` - background-color: #eff0f4; - border-radius: ${lengths.borderRadius}; - - font-size: 14px; - padding: 10px 6px 10px 32px; - width: 100%; - position: relative; - z-index: ${zIndex.zIndex1}; - - &:focus { - outline: none; - box-shadow: inset 0 0 0 2px ${colors.active}; - } -`; - -const CreateFolderIcon = styled(Icon)` - position: absolute; - top: 50%; - left: 6px; - z-index: ${zIndex.zIndex2}; - transform: translate(0, -50%); -`; - -function MediaLibraryCreateFolder({ value, onChange, onKeyDown, placeholder, disabled }) { - return ( - - - - - ); -} - -MediaLibraryCreateFolder.propTypes = { - value: PropTypes.string, - onChange: PropTypes.func.isRequired, - onKeyDown: PropTypes.func.isRequired, - placeholder: PropTypes.string.isRequired, - disabled: PropTypes.bool, -}; - -export default MediaLibraryCreateFolder; From 2aa5ee24d822122b4d7bcd1f22474b96884959ce Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Wed, 24 Feb 2021 11:42:08 +0200 Subject: [PATCH 24/45] feat(media dir nav): revert dev-test config --- dev-test/config.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/dev-test/config.yml b/dev-test/config.yml index e5e503d98601..2ffe5041ddb5 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -1,11 +1,10 @@ backend: - name: github - repo: andrew-paterson/netlify-cms-hugo-test-site + name: test-repo site_url: https://example.com publish_mode: editorial_workflow -media_folder: static/images +media_folder: assets/uploads collections: # A list of collections the CMS should be able to edit - name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit @@ -14,8 +13,7 @@ collections: # A list of collections the CMS should be able to edit description: > The description is a great place for tone setting, high level information, and editing guidelines that are specific to a collection. - folder: 'content/posts' - media_folder: '/static/images/one' + folder: '_posts' slug: '{{year}}-{{month}}-{{day}}-{{slug}}' summary: '{{title}} -- {{year}}/{{month}}/{{day}}' create: true # Allow users to create new documents in this collection @@ -46,19 +44,14 @@ collections: # A list of collections the CMS should be able to edit time_format: 'HH:mm', format: 'YYYY-MM-DD HH:mm', } - - label: 'Other Image' - name: 'other' - widget: 'image' - required: false - tagname: '' - label: 'Cover Image' name: 'image' widget: 'image' required: false tagname: '' - media_folder: '/static/images/one/one-deeper' - disable_media_folder_navigation: true + - { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' } + - name: 'faq' # Used in routes, ie.: /admin/collections/:slug/edit label: 'FAQ' # Used in the UI folder: '_faqs' From 88cdf88a82db9df735e85fa52069008f1ccfcab1 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Thu, 11 Mar 2021 17:54:49 +0100 Subject: [PATCH 25/45] style: run prettier --- .../src/components/MediaLibrary/MediaLibrary.js | 17 +++++++++-------- .../MediaLibrary/MediaLibraryCreateFolder.js | 9 ++++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index f4c73374d28b..96436603f893 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -273,14 +273,14 @@ class MediaLibrary extends React.Component { }; handleCreateFolder = dirName => { - this.setState({ isLoading: true}) + this.setState({ isLoading: true }); function byteToHex(byte) { return ('0' + byte.toString(16)).slice(-2); } function generateId(len = 40) { var arr = new Uint8Array(len / 2); window.crypto.getRandomValues(arr); - return Array.from(arr, byteToHex).join(""); + return Array.from(arr, byteToHex).join(''); } const { defaultMediaFolder } = this.props; const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; @@ -290,11 +290,11 @@ class MediaLibrary extends React.Component { key: generateId(40), name: dirName, path: `${currentMediaFolder}/${dirName}`, - displayURL: {path: `${currentMediaFolder}/${dirName}`}, - isDirectory: true + displayURL: { path: `${currentMediaFolder}/${dirName}` }, + isDirectory: true, }); - this.setState({ isLoading: false}) - } + this.setState({ isLoading: false }); + }; /** * Downloads the selected file. @@ -414,8 +414,9 @@ class MediaLibrary extends React.Component { this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFolders = (currentDirFiles || []).filter(file => file.isDirectory); - const currentDirFilesOrderedByTreeType = currentDirFolders - .concat((currentDirFiles || []).filter(file => !file.isDirectory)); + const currentDirFilesOrderedByTreeType = currentDirFolders.concat( + (currentDirFiles || []).filter(file => !file.isDirectory), + ); return ( folder.name === event.target.value)}); + this.setState({ + value: event.target.value, + folderExists: folders.find(folder => folder.name === event.target.value), + }); } handleKeyDown(event) { const { onKeyDown } = this.props; if (event.key === 'Enter' && !this.state.folderExists) { onKeyDown(this.state.value); - this.setState({value: ''}); + this.setState({ value: '' }); } } From 7540d607329e82b71194dca875ab3f2d4389e002 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 14 Mar 2021 18:22:48 +0100 Subject: [PATCH 26/45] fix: config access --- .../src/components/MediaLibrary/MediaLibrary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 96436603f893..ea4fc4cd7a50 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -482,7 +482,7 @@ function mapStateToProps(state) { hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), field, - defaultMediaFolder: config.get('media_folder'), + defaultMediaFolder: config.media_folder, }; return { ...mediaLibraryProps }; } From 9041de0dd8580a7dbf5a4a55419953dcec18091c Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 14 Mar 2021 18:40:11 +0100 Subject: [PATCH 27/45] fix: add missing translation --- .../src/components/MediaLibrary/MediaLibraryCard.js | 6 +++--- packages/netlify-cms-locales/src/en/index.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index fbac8992ce08..4e76ed8c7bae 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -182,7 +182,7 @@ class MediaLibraryCard extends React.Component { hasChildren, } = this.props; const url = displayURL.get('url'); - var cardImageWrapper = ( + const cardImageWrapper = ( {isDraft ? {draftText} : null} {url && isViewableImage ? ( @@ -192,12 +192,12 @@ class MediaLibraryCard extends React.Component { )} ); - var cardDirectoryEl = ( + const cardDirectoryEl = ( ); - var previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; + const previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; return ( Date: Wed, 17 Mar 2021 09:08:10 +0200 Subject: [PATCH 28/45] feat(media dir nav): revert dev-test config --- .../components/MediaLibrary/MediaLibrary.js | 45 ++----- .../MediaLibrary/MediaLibraryCard.js | 113 +----------------- .../MediaLibrary/MediaLibraryCardGrid.js | 17 ++- .../MediaLibrary/MediaLibraryModal.js | 13 +- packages/netlify-cms-locales/src/en/index.js | 1 + 5 files changed, 26 insertions(+), 163 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index f4c73374d28b..c448c3c6b84a 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -168,36 +168,16 @@ class MediaLibrary extends React.Component { this.props.closeMediaLibrary(); }; - isSelectedAsset = asset => { - return ( - (this.state.selectedAssets || []).filter(selectedAsset => { - return selectedAsset.key === asset.key; - }).length > 0 - ); - }; - - updateSelectedAssets = asset => { - let selectedAssets = this.state.selectedAssets || []; - if (!this.isSelectedAsset(asset)) { - selectedAssets.push(asset); - } else { - selectedAssets = selectedAssets.filter(selectedAsset => { - return selectedAsset.key !== asset.key; - }); - } - this.setState({ selectedAssets }); - }; - - handleAssetCheckboxChange = (asset, event) => { - event.stopPropagation(); - this.updateSelectedAssets(asset); + updateSelectedFile = asset => { + const selectedFile = this.state.selectedFile.key === asset.key ? {} : asset; + this.setState({ selectedFile }); }; handleAssetClick = asset => { if (asset.isDirectory) { this.setState({ currentMediaFolder: asset.path, selectedAssets: [] }); } else { - this.updateSelectedAssets(asset); + this.updateSelectedFile(asset); } }; @@ -257,19 +237,16 @@ class MediaLibrary extends React.Component { /** * Removes the selected file from the backend. */ - handleDelete = async () => { - const { selectedAssets } = this.state; + handleDelete = () => { + const { selectedFile } = this.state; const { files, deleteMedia, privateUpload, t } = this.props; if (!window.confirm(t('mediaLibrary.mediaLibrary.onDelete'))) { return; } - const filesToDelete = selectedAssets.map(selectedAsset => - files.find(file => selectedAsset.key === file.key), - ); - for (const file of filesToDelete) { - await deleteMedia(file, { privateUpload }); - } - this.setState({ selectedAssets: [] }); + const file = files.find(file => selectedFile.key === file.key); + deleteMedia(file, { privateUpload }).then(() => { + this.setState({ selectedFile: {} }); + }); }; handleCreateFolder = dirName => { @@ -433,7 +410,6 @@ class MediaLibrary extends React.Component { privateUpload={privateUpload} query={this.state.query} selectedFile={this.state.selectedFile} - selectedAssets={this.state.selectedAssets} handleFilter={this.filterImages} handleQuery={this.queryFilter} toTableData={this.toTableData} @@ -447,7 +423,6 @@ class MediaLibrary extends React.Component { handleDownload={this.handleDownload} setScrollContainerRef={ref => (this.scrollContainerRef = ref)} handleAssetClick={this.handleAssetClick} - handleAssetCheckboxChange={this.handleAssetCheckboxChange} handleBreadcrumbClick={this.handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index fbac8992ce08..e3e7d09775b0 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -16,7 +16,7 @@ const Card = styled.div` cursor: pointer; overflow: hidden; background-color: ${props => props.isPrivate && colors.textFieldBorder}; - position: relative; + &:focus { outline: none; } @@ -56,25 +56,12 @@ const CardDirctoryWrapper = styled.div` background: #eee; `; -const CardText = styled.div` +const CardText = styled.p` color: ${colors.text}; padding: 8px; - font-size: 0.8em; - display: flex; - justify-content: center; - flex-direction: column; -`; - -const ObjectName = styled.div` - color: ${colors.text}; - font-weight: 600; + margin-top: 20px; overflow-wrap: break-word; line-height: 1.3 !important; - text-align: left; -`; - -const ImageMeta = styled.div` - text-align: left; `; const DraftText = styled.p` @@ -85,82 +72,6 @@ const DraftText = styled.p` border-radius: ${lengths.borderRadius} 0px ${lengths.borderRadius} 0; `; -const radioButtonCheckboxWidthHeight = '24px'; -const inputBorderColor = '#fff'; -const inputBackgroundColor = 'rgba(0, 0, 0, 0.2)'; -const inputHoverBorderColor = '#33BCB0'; -const radioCheckboxSelectedBackgroundColor = '#33BCB0'; -const svgDataRriIconTick = `url('data:image/svg+xml;utf8,')`; -const radioCheckboxSelectedBackgroundPosition = 'center 6px'; -const radioCheckboxSelectedBackgroundSize = '13px'; -const inputFocusBorderColor = '#fff'; - -const AssetCheckbox = styled.input` - display: inline-block; - -webkit-appearance: none; - -moz-appearance: none; - position: relative; - cursor: pointer; - left: 0; - top: 0; - width: ${radioButtonCheckboxWidthHeight}; - height: ${radioButtonCheckboxWidthHeight}; - padding: 0 !important; - background: transparent !important; - border-width: 0 !important; - &:after { - border: 1px solid ${inputBorderColor}; - content: ''; - background-color: ${inputBackgroundColor}; - width: ${radioButtonCheckboxWidthHeight}; - height: ${radioButtonCheckboxWidthHeight}; - border-radius: 50%; - position: absolute; - top: 0; - left: 0px; - } - &:hover:after { - border-color: ${inputHoverBorderColor}; - } - &:checked:after, - &.checked:after { - background: ${radioCheckboxSelectedBackgroundColor} ${svgDataRriIconTick} no-repeat - ${radioCheckboxSelectedBackgroundPosition}; - background-size: ${radioCheckboxSelectedBackgroundSize}; - } - &:focus:after, - &:focus:checked:after { - border-color: ${inputFocusBorderColor}; - } -`; - -const CheckboxContainer = styled.div` - position: absolute; - top: 0; - left: 0; - z-index: 1; -`; - -function readableFileSize(numberOfBytes) { - let readableSize; - let units; - numberOfBytes = numberOfBytes || 0; - if (numberOfBytes >= 0 && numberOfBytes < 1000) { - readableSize = numberOfBytes; - units = ' bytes'; - } else if (numberOfBytes >= 1000 && numberOfBytes < 1000000) { - readableSize = Math.ceil(numberOfBytes / 1000); - units = ' KB'; - } else if (numberOfBytes >= 1000000 && numberOfBytes < 1000000000) { - readableSize = (numberOfBytes / 1000000).toFixed(2); - units = ' MB'; - } else if (numberOfBytes >= 1000000000) { - readableSize = (numberOfBytes / 1000000000).toFixed(2); - units = ' GB'; - } - return readableSize + units; -} - class MediaLibraryCard extends React.Component { render() { const { @@ -168,7 +79,6 @@ class MediaLibraryCard extends React.Component { displayURL, text, onClick, - onChecked, draftText, width, height, @@ -178,8 +88,6 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, - size, - hasChildren, } = this.props; const url = displayURL.get('url'); var cardImageWrapper = ( @@ -208,21 +116,8 @@ class MediaLibraryCard extends React.Component { tabIndex="-1" isPrivate={isPrivate} > - {!hasChildren ? ( - - - - ) : null} - {previewElement} - - {text} - {isViewableImage ? ( - - {type} - {readableFileSize(size)} - - ) : null} - + {text} ); } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 4a6a07736606..1ebfccec5f81 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -15,9 +15,8 @@ function CardWrapper(props) { style, data: { mediaItems, - isSelectedAsset, + isSelectedFile, onAssetClick, - onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -33,6 +32,7 @@ function CardWrapper(props) { return null; } const file = mediaItems[index]; + return (
onAssetClick(file)} - onChecked={event => { - onAssetCheckboxClick(file, event); - }} + isDraft={file.draft} draftText={cardDraftText} width={cardWidth} @@ -105,9 +103,8 @@ function VirtualizedGrid(props) { function PaginatedGrid({ setScrollContainerRef, mediaItems, - isSelectedAsset, + isSelectedFile, onAssetClick, - onAssetCheckboxClick, cardDraftText, cardWidth, cardHeight, @@ -126,7 +123,7 @@ function PaginatedGrid({ {mediaItems.map(file => ( onAssetClick(file)} isDraft={file.draft} @@ -139,7 +136,6 @@ function PaginatedGrid({ loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} - onAssetCheckboxClick={onAssetCheckboxClick} /> ))} {!canLoadMore ? null : } @@ -188,6 +184,7 @@ MediaLibraryCardGrid.propTypes = { draft: PropTypes.bool, }), ).isRequired, + isSelectedFile: PropTypes.func.isRequired, onAssetClick: PropTypes.func.isRequired, canLoadMore: PropTypes.bool, onLoadMore: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index e605d8538b53..d3357662673d 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import { Map } from 'immutable'; +import { isEmpty } from 'lodash'; import { translate } from 'react-polyglot'; import { Modal } from 'UI'; import MediaLibraryTop from './MediaLibraryTop'; @@ -74,7 +75,7 @@ function MediaLibraryModal({ isPaginating, privateUpload, query, - selectedAssets, + selectedFile, handleFilter, handleQuery, toTableData, @@ -88,7 +89,6 @@ function MediaLibraryModal({ handleDownload, setScrollContainerRef, handleAssetClick, - handleAssetCheckboxChange, handleBreadcrumbClick, handleLoadMore, loadDisplayURL, @@ -96,7 +96,6 @@ function MediaLibraryModal({ t, currentMediaFolder, defaultMediaFolder, - selectedFile, }) { const filteredFiles = forImage ? handleFilter(files) : files; const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles; @@ -113,7 +112,7 @@ function MediaLibraryModal({ (!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) || (!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults')); - const hasSelection = hasMedia && (selectedAssets || []).length > 0; + const hasSelection = hasMedia && !isEmpty(selectedFile); return ( @@ -148,11 +147,8 @@ function MediaLibraryModal({ - (selectedAssets || []).filter(selectedAsset => selectedAsset.key === file.key).length > 0 - } + isSelectedFile={file => selectedFile.key === file.key} onAssetClick={handleAssetClick} - onAssetCheckboxClick={handleAssetCheckboxChange} canLoadMore={hasNextPage} onLoadMore={handleLoadMore} isPaginating={isPaginating} @@ -206,7 +202,6 @@ MediaLibraryModal.propTypes = { handleCreateFolder: PropTypes.func.isRequired, setScrollContainerRef: PropTypes.func.isRequired, handleAssetClick: PropTypes.func.isRequired, - handleAssetCheckboxChange: PropTypes.func.isRequired, handleBreadcrumbClick: PropTypes.func.isRequired, handleLoadMore: PropTypes.func.isRequired, loadDisplayURL: PropTypes.func.isRequired, diff --git a/packages/netlify-cms-locales/src/en/index.js b/packages/netlify-cms-locales/src/en/index.js index 6fa827ab2c70..75838cf4a3d5 100644 --- a/packages/netlify-cms-locales/src/en/index.js +++ b/packages/netlify-cms-locales/src/en/index.js @@ -223,6 +223,7 @@ const en = { deleting: 'Deleting...', deleteSelected: 'Delete selected', chooseSelected: 'Choose selected', + createFolder: 'Create folder', }, }, ui: { From f07937f3b6d07b6edf936eadcd4476a19440aa7d Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 23 Mar 2021 11:04:32 +0200 Subject: [PATCH 29/45] feat(media dir nav): fix breadcrumbs bug --- .../src/components/MediaLibrary/MediaLibraryBreadCrumbs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 22e488675a4b..5df9ca82b754 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -32,7 +32,7 @@ const BreadCrumbsItemDivider = styled.div` class MediaLibraryBreadcrumbs extends React.Component { render() { const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder } = this.props; - var hiddenPath = defaultMediaFolder + var hiddenPath = trim(defaultMediaFolder, '/') .split('/') .slice(0, -1) .join('/'); From 39a2e39e46b618c877ce764cf5ccbf00d42688a8 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Wed, 31 Mar 2021 16:51:43 +0200 Subject: [PATCH 30/45] feat(media dir nav): use current media folder for select media file publich path function --- packages/netlify-cms-core/src/reducers/entries.ts | 6 +++--- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 77d56b9893ff..ba31e7169dba 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -741,7 +741,6 @@ export function selectMediaFolder( let mediaFolder = config[name]; const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); - if (customFolder) { const folder = evaluateFolder(name, config, collection!, entryMap, field); if (folder.startsWith('/')) { @@ -784,7 +783,8 @@ export function selectMediaFilePublicPath( if (isAbsolutePath(mediaPath)) { return mediaPath; } - + const mediaFolder = config['media_folder']!; + const relativePath = `/${mediaPath}`.replace(mediaFolder, ''); const name = 'public_folder'; let publicFolder = config[name]!; @@ -794,7 +794,7 @@ export function selectMediaFilePublicPath( publicFolder = evaluateFolder(name, config, collection!, entryMap, field); } - return join(publicFolder, basename(mediaPath)); + return join(publicFolder, relativePath); } export function selectEditingDraft(state: EntryDraft) { diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index e5d0dbfff9ce..2ad23d68ca2a 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -243,7 +243,7 @@ export function selectMediaFiles(state: State, field?: EntryField) { const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - let disableMediaFolderNav = true; + let disableMediaFolderNav = false; if (field && field.has('disable_media_folder_navigation')) { disableMediaFolderNav = field.get('disable_media_folder_navigation', true); From b8b2367cd06e8466f4b86717fbc13a3da3772c0f Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Tue, 6 Apr 2021 11:15:05 +0200 Subject: [PATCH 31/45] feat(media dir nav): accommodate collection and field level media folder setting --- .../components/MediaLibrary/MediaLibrary.js | 25 +++------- .../MediaLibrary/MediaLibraryBreadCrumbs.js | 21 ++++++-- .../MediaLibrary/MediaLibraryCard.js | 15 +++--- .../MediaLibrary/MediaLibraryCardGrid.js | 5 ++ .../MediaLibrary/MediaLibraryModal.js | 3 ++ .../MediaLibrary/MediaLibraryTop.js | 16 ++++-- .../src/reducers/mediaLibrary.ts | 49 +++++++++---------- 7 files changed, 74 insertions(+), 60 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 61f1df5d476e..8042101fd961 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -14,7 +14,7 @@ import { loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, } from 'Actions/mediaLibrary'; -import { selectMediaFiles } from 'Reducers/mediaLibrary'; +import { selectMediaFiles, getStartingMediaFolder, getMediaFolderNavDisabled } from 'Reducers/mediaLibrary'; import { dirname } from 'path'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; @@ -352,22 +352,6 @@ class MediaLibrary extends React.Component { return matchFiles; }; - deriveCurrentMediaFolder(files = []) { - var shortestPath = - files - .map(file => { - return { - path: file.path, - segments: file.path.split('/'), - }; - }) - .sort((a, b) => { - return a.segments.length - b.segments.length; - })[0] || {}; - var length = shortestPath.segments ? shortestPath.segments.length - 1 : 0; - return (shortestPath.segments || []).slice(0, length).join('/'); - } - render() { const { isVisible, @@ -384,11 +368,13 @@ class MediaLibrary extends React.Component { privateUpload, displayURLs, defaultMediaFolder, + startingMediaFolder, + mediaFolderNavDisabled, t, } = this.props; const currentMediaFolder = - this.state.currentMediaFolder || this.deriveCurrentMediaFolder(files); + this.state.currentMediaFolder || startingMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFolders = (currentDirFiles || []).filter(file => file.isDirectory); const currentDirFilesOrderedByTreeType = currentDirFolders.concat( @@ -427,6 +413,7 @@ class MediaLibrary extends React.Component { handleBreadcrumbClick={this.handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} + mediaFolderNavDisabled={mediaFolderNavDisabled} handleLoadMore={this.handleLoadMore} displayURLs={displayURLs} loadDisplayURL={this.loadDisplayURL} @@ -443,6 +430,8 @@ function mapStateToProps(state) { isVisible: mediaLibrary.get('isVisible'), canInsert: mediaLibrary.get('canInsert'), files: selectMediaFiles(state, field), + startingMediaFolder: getStartingMediaFolder(state, field), + mediaFolderNavDisabled: getMediaFolderNavDisabled(state, field), displayURLs: mediaLibrary.get('displayURLs'), dynamicSearch: mediaLibrary.get('dynamicSearch'), dynamicSearchActive: mediaLibrary.get('dynamicSearchActive'), diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 5df9ca82b754..fb636ff502a0 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -14,6 +14,13 @@ const BreadCrumbsItem = styled.div` `; const BreadCrumbsItemLabel = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 6px 8px; +`; + +const BreadCrumbsItemButton = styled.div` display: flex; justify-content: center; align-items: center; @@ -31,7 +38,7 @@ const BreadCrumbsItemDivider = styled.div` class MediaLibraryBreadcrumbs extends React.Component { render() { - const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder } = this.props; + const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder, mediaFolderNavDisabled } = this.props; var hiddenPath = trim(defaultMediaFolder, '/') .split('/') .slice(0, -1) @@ -48,9 +55,15 @@ class MediaLibraryBreadcrumbs extends React.Component { this.BreadCrumbsContent = breadcrumbsArray.map((item, index) => { return ( - handleBreadcrumbClick(item.path)}> - {item.isDefaultMediaDirectory ? : item.label} - + {mediaFolderNavDisabled ? ( + + {item.isDefaultMediaDirectory ? : item.label} + + ) : ( + handleBreadcrumbClick(item.path)}> + {item.isDefaultMediaDirectory ? : item.label} + + )} / ); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js index 33bdf98565ec..44b45def52f0 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCard.js @@ -33,20 +33,20 @@ const CardImageWrapper = styled.div` const CardImage = styled.img` width: 100%; height: ${IMAGE_HEIGHT}px; - object-fit: cover; + object-fit: contain; border-radius: 2px 2px 0 0; `; const CardFileIcon = styled.div` width: 100%; height: 160px; - object-fit: cover; + object-fit: contain; border-radius: 2px 2px 0 0; padding: 1em; font-size: 3em; `; -const CardDirctoryWrapper = styled.div` +const CardDirectoryWrapper = styled.div` width: 100%; height: 160px; object-fit: cover; @@ -88,6 +88,7 @@ class MediaLibraryCard extends React.Component { isViewableImage, isDraft, isDirectory, + mediaFolderNavDisabled, } = this.props; const url = displayURL.get('url'); const cardImageWrapper = ( @@ -101,20 +102,22 @@ class MediaLibraryCard extends React.Component { ); const cardDirectoryEl = ( - + - + ); + const disabled = isDirectory && mediaFolderNavDisabled; const previewElement = isDirectory ? cardDirectoryEl : cardImageWrapper; return ( {previewElement} {text} diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index 1ebfccec5f81..a7e5e76b2286 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -25,6 +25,7 @@ function CardWrapper(props) { loadDisplayURL, columnCount, gutter, + mediaFolderNavDisabled, }, } = props; const index = rowIndex * columnCount + columnIndex; @@ -62,6 +63,7 @@ function CardWrapper(props) { isDirectory={file.isDirectory} hasChildren={file.hasChildren} size={file.size} + mediaFolderNavDisabled={mediaFolderNavDisabled} />
); @@ -116,6 +118,7 @@ function PaginatedGrid({ onLoadMore, isPaginating, paginatingMessage, + mediaFolderNavDisabled, }) { return ( @@ -136,6 +139,8 @@ function PaginatedGrid({ loadDisplayURL={() => loadDisplayURL(file)} type={file.type} isViewableImage={file.isViewableImage} + mediaFolderNavDisabled={mediaFolderNavDisabled} + /> ))} {!canLoadMore ? null : } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index d3357662673d..3f04684a8c79 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -96,6 +96,7 @@ function MediaLibraryModal({ t, currentMediaFolder, defaultMediaFolder, + mediaFolderNavDisabled, }) { const filteredFiles = forImage ? handleFilter(files) : files; const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles; @@ -137,6 +138,7 @@ function MediaLibraryModal({ handleBreadcrumbClick={handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} + mediaFolderNavDisabled={mediaFolderNavDisabled} selectedFile={selectedFile} folders={folders} /> @@ -160,6 +162,7 @@ function MediaLibraryModal({ isPrivate={privateUpload} loadDisplayURL={loadDisplayURL} displayURLs={displayURLs} + mediaFolderNavDisabled={mediaFolderNavDisabled} />
); diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index 5f601f572180..ba3879ca146d 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -49,6 +49,7 @@ function MediaLibraryTop({ handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder, + mediaFolderNavDisabled, selectedFile, folders, }) { @@ -104,11 +105,15 @@ function MediaLibraryTop({ placeholder={t('mediaLibrary.mediaLibraryModal.search')} disabled={searchDisabled} /> - + {!mediaFolderNavDisabled ? ( + + ) : ( + null + )} {deleteButtonLabel} @@ -125,6 +130,7 @@ function MediaLibraryTop({ handleBreadcrumbClick={handleBreadcrumbClick} currentMediaFolder={currentMediaFolder} defaultMediaFolder={defaultMediaFolder} + mediaFolderNavDisabled={mediaFolderNavDisabled} /> diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index 2ad23d68ca2a..aacfdf18bb9b 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -229,40 +229,35 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) { return state; } } - -export function selectMediaFiles(state: State, field?: EntryField) { - const { mediaLibrary, entryDraft } = state; +export function getStartingMediaFolder(state: State, field?: EntryField) { const editingDraft = selectEditingDraft(state.entryDraft); + const { entryDraft } = state; + const entry = entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); const integration = selectIntegration(state, null, 'assetStore'); - - let files; if (editingDraft && !integration) { - const entryFiles = entryDraft - .getIn(['entry', 'mediaFiles'], List()) - .toJS() as MediaFile[]; - const entry = entryDraft.get('entry'); - const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - let disableMediaFolderNav = false; + return mediaFolder; + } + return null; +} - if (field && field.has('disable_media_folder_navigation')) { - disableMediaFolderNav = field.get('disable_media_folder_navigation', true); - } else if (collection && collection.has('disable_media_folder_navigation')) { - disableMediaFolderNav = collection.get('disable_media_folder_navigation', true); - } - if (disableMediaFolderNav) { - files = entryFiles.filter(f => { - return dirname(f.path) === mediaFolder && !f.isDirectory; - }); - } else { - files = entryFiles; - } - files = files.map(file => ({ key: file.id, ...file })); - } else { - files = mediaLibrary.get('files') || []; +export function getMediaFolderNavDisabled(state: State, field?: EntryField) { + let disableMediaFolderNav = false; + const { entryDraft } = state; + const entry = entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); + if (field && field.has('disable_media_folder_navigation')) { + disableMediaFolderNav = field.get('disable_media_folder_navigation', true); + } else if (collection && collection.has('disable_media_folder_navigation')) { + disableMediaFolderNav = collection.get('disable_media_folder_navigation', true); } + return disableMediaFolderNav; +} - return files; +export function selectMediaFiles(state: State) { + const { mediaLibrary } = state; + return mediaLibrary.get('files') || []; } export function selectMediaFileByPath(state: State, path: string) { From e462272ca69e149c069dd3cc61972f439ccf77d8 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Tue, 6 Apr 2021 21:10:02 +0400 Subject: [PATCH 32/45] chore: fix formatting and lint warnings --- .../src/components/MediaLibrary/MediaLibrary.js | 9 ++++++--- .../components/MediaLibrary/MediaLibraryBreadCrumbs.js | 7 ++++++- .../src/components/MediaLibrary/MediaLibraryCardGrid.js | 4 +--- .../src/components/MediaLibrary/MediaLibraryModal.js | 2 +- .../src/components/MediaLibrary/MediaLibraryTop.js | 4 +--- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 4 +--- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index 8042101fd961..c0e4fc43c402 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -14,7 +14,11 @@ import { loadMediaDisplayURL as loadMediaDisplayURLAction, closeMediaLibrary as closeMediaLibraryAction, } from 'Actions/mediaLibrary'; -import { selectMediaFiles, getStartingMediaFolder, getMediaFolderNavDisabled } from 'Reducers/mediaLibrary'; +import { + selectMediaFiles, + getStartingMediaFolder, + getMediaFolderNavDisabled, +} from 'Reducers/mediaLibrary'; import { dirname } from 'path'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; @@ -373,8 +377,7 @@ class MediaLibrary extends React.Component { t, } = this.props; - const currentMediaFolder = - this.state.currentMediaFolder || startingMediaFolder; + const currentMediaFolder = this.state.currentMediaFolder || startingMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFolders = (currentDirFiles || []).filter(file => file.isDirectory); const currentDirFilesOrderedByTreeType = currentDirFolders.concat( diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index fb636ff502a0..6095c56dddd7 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -38,7 +38,12 @@ const BreadCrumbsItemDivider = styled.div` class MediaLibraryBreadcrumbs extends React.Component { render() { - const { handleBreadcrumbClick, currentMediaFolder, defaultMediaFolder, mediaFolderNavDisabled } = this.props; + const { + handleBreadcrumbClick, + currentMediaFolder, + defaultMediaFolder, + mediaFolderNavDisabled, + } = this.props; var hiddenPath = trim(defaultMediaFolder, '/') .split('/') .slice(0, -1) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js index a7e5e76b2286..ddad8d56d637 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryCardGrid.js @@ -33,7 +33,7 @@ function CardWrapper(props) { return null; } const file = mediaItems[index]; - + return (
onAssetClick(file)} - isDraft={file.draft} draftText={cardDraftText} width={cardWidth} @@ -140,7 +139,6 @@ function PaginatedGrid({ type={file.type} isViewableImage={file.isViewableImage} mediaFolderNavDisabled={mediaFolderNavDisabled} - /> ))} {!canLoadMore ? null : } diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js index 3f04684a8c79..3b6463fa6a0b 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -113,7 +113,7 @@ function MediaLibraryModal({ (!hasFilteredFiles && t('mediaLibrary.mediaLibraryModal.noImagesFound')) || (!hasSearchResults && t('mediaLibrary.mediaLibraryModal.noResults')); - const hasSelection = hasMedia && !isEmpty(selectedFile); + const hasSelection = hasMedia && !isEmpty(selectedFile); return ( diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index ba3879ca146d..39fd6071c4f4 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -111,9 +111,7 @@ function MediaLibraryTop({ placeholder={t('mediaLibrary.mediaLibraryModal.createFolder')} folders={folders} /> - ) : ( - null - )} + ) : null} {deleteButtonLabel} diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index aacfdf18bb9b..b5aa1aea8394 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -1,4 +1,4 @@ -import { Map, List } from 'immutable'; +import { Map } from 'immutable'; import uuid from 'uuid/v4'; import { MEDIA_LIBRARY_OPEN, @@ -26,11 +26,9 @@ import { State, MediaLibraryInstance, MediaFile, - MediaFileMap, DisplayURLState, EntryField, } from '../types/redux'; -import { dirname } from 'path'; const defaultState: { isVisible: boolean; From 73de67d6f8887358278bb79aac81cbf72490b068 Mon Sep 17 00:00:00 2001 From: erezrokah Date: Wed, 7 Apr 2021 17:12:03 +0400 Subject: [PATCH 33/45] fix: typetcript error --- packages/netlify-cms-core/src/actions/mediaLibrary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 8cd97a00a68b..0e6dd7f0124b 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -214,7 +214,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, null, 'assetStore'); - const files: MediaFile[] = selectMediaFiles(state, field); + const files: MediaFile[] = selectMediaFiles(state); const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.slug); const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); From 84a30636af61f563387c0f477828bc0e1fec6ad4 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Thu, 8 Apr 2021 11:14:02 +0200 Subject: [PATCH 34/45] feat(media dir nav): bugfix - current media folder returned correctly --- .../components/MediaLibrary/MediaLibraryBreadCrumbs.js | 2 +- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 6095c56dddd7..52e4e9ef749a 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -48,7 +48,7 @@ class MediaLibraryBreadcrumbs extends React.Component { .split('/') .slice(0, -1) .join('/'); - var currentMediaFolderParts = trim(currentMediaFolder.replace(hiddenPath, ''), '/').split('/'); + var currentMediaFolderParts = trim((currentMediaFolder || '').replace(hiddenPath, ''), '/').split('/'); var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { return { isDefaultMediaDirectory: index === 0, diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index b5aa1aea8394..ce8c39a092ad 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -228,16 +228,15 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) { } } export function getStartingMediaFolder(state: State, field?: EntryField) { - const editingDraft = selectEditingDraft(state.entryDraft); const { entryDraft } = state; const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const integration = selectIntegration(state, null, 'assetStore'); - if (editingDraft && !integration) { - const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - return mediaFolder; + if (integration) { + return null; } - return null; + const mediaFolder = selectMediaFolder(state.config, collection, entry, field); + return mediaFolder; } export function getMediaFolderNavDisabled(state: State, field?: EntryField) { From 23bc33bba1d888343d7787a8554cf79b04c9ac71 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 12 Apr 2021 09:37:01 +0200 Subject: [PATCH 35/45] feat(media dir nav): media files filtered in new way --- .../components/MediaLibrary/MediaLibrary.js | 12 ++--- .../netlify-cms-core/src/reducers/entries.ts | 6 +-- .../src/reducers/mediaLibrary.ts | 47 ++++++++++++++----- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index c0e4fc43c402..e1c9dd5a0e7e 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -16,7 +16,7 @@ import { } from 'Actions/mediaLibrary'; import { selectMediaFiles, - getStartingMediaFolder, + getInitialMediaFolder, getMediaFolderNavDisabled, } from 'Reducers/mediaLibrary'; import { dirname } from 'path'; @@ -201,8 +201,8 @@ class MediaLibrary extends React.Component { event.persist(); event.stopPropagation(); event.preventDefault(); - const { defaultMediaFolder } = this.props; - const currentMediaFolder = this.state.currentMediaFolder || defaultMediaFolder; + const { initialMediaFolder } = this.props; + const currentMediaFolder = this.state.currentMediaFolder || initialMediaFolder; const { persistMedia, privateUpload, config, t, field } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; @@ -372,12 +372,12 @@ class MediaLibrary extends React.Component { privateUpload, displayURLs, defaultMediaFolder, - startingMediaFolder, + initialMediaFolder, mediaFolderNavDisabled, t, } = this.props; - const currentMediaFolder = this.state.currentMediaFolder || startingMediaFolder; + const currentMediaFolder = this.state.currentMediaFolder || initialMediaFolder; const currentDirFiles = files.filter(file => dirname(file.path) === currentMediaFolder); const currentDirFolders = (currentDirFiles || []).filter(file => file.isDirectory); const currentDirFilesOrderedByTreeType = currentDirFolders.concat( @@ -433,7 +433,7 @@ function mapStateToProps(state) { isVisible: mediaLibrary.get('isVisible'), canInsert: mediaLibrary.get('canInsert'), files: selectMediaFiles(state, field), - startingMediaFolder: getStartingMediaFolder(state, field), + initialMediaFolder: getInitialMediaFolder(state, field), mediaFolderNavDisabled: getMediaFolderNavDisabled(state, field), displayURLs: mediaLibrary.get('displayURLs'), dynamicSearch: mediaLibrary.get('dynamicSearch'), diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index ba31e7169dba..c354e696bffe 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -783,8 +783,7 @@ export function selectMediaFilePublicPath( if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = config['media_folder']!; - const relativePath = `/${mediaPath}`.replace(mediaFolder, ''); + const name = 'public_folder'; let publicFolder = config[name]!; @@ -793,8 +792,7 @@ export function selectMediaFilePublicPath( if (customFolder) { publicFolder = evaluateFolder(name, config, collection!, entryMap, field); } - - return join(publicFolder, relativePath); + return join(publicFolder, basename(mediaPath)); } export function selectEditingDraft(state: EntryDraft) { diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index ce8c39a092ad..3646892c485c 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -1,4 +1,4 @@ -import { Map } from 'immutable'; +import { Map, List } from 'immutable'; import uuid from 'uuid/v4'; import { MEDIA_LIBRARY_OPEN, @@ -26,9 +26,11 @@ import { State, MediaLibraryInstance, MediaFile, + MediaFileMap, DisplayURLState, EntryField, } from '../types/redux'; +import { dirname } from 'path'; const defaultState: { isVisible: boolean; @@ -227,7 +229,7 @@ function mediaLibrary(state = Map(defaultState), action: MediaLibraryAction) { return state; } } -export function getStartingMediaFolder(state: State, field?: EntryField) { +export function getInitialMediaFolder(state: State, field?: EntryField) { const { entryDraft } = state; const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); @@ -235,8 +237,7 @@ export function getStartingMediaFolder(state: State, field?: EntryField) { if (integration) { return null; } - const mediaFolder = selectMediaFolder(state.config, collection, entry, field); - return mediaFolder; + return selectMediaFolder(state.config, collection, entry, field); } export function getMediaFolderNavDisabled(state: State, field?: EntryField) { @@ -244,17 +245,39 @@ export function getMediaFolderNavDisabled(state: State, field?: EntryField) { const { entryDraft } = state; const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - if (field && field.has('disable_media_folder_navigation')) { - disableMediaFolderNav = field.get('disable_media_folder_navigation', true); - } else if (collection && collection.has('disable_media_folder_navigation')) { - disableMediaFolderNav = collection.get('disable_media_folder_navigation', true); + if (field && field.has('media_folder')) { + disableMediaFolderNav = field.get('media_folder') ? true : false; + } else if (collection && collection.has('media_folder')) { + disableMediaFolderNav = collection.get('media_folder') ? true : false; } return disableMediaFolderNav; } - -export function selectMediaFiles(state: State) { - const { mediaLibrary } = state; - return mediaLibrary.get('files') || []; +export function selectMediaFiles(state: State, field?: EntryField) { + const { mediaLibrary, entryDraft } = state; + const editingDraft = selectEditingDraft(state.entryDraft); + const integration = selectIntegration(state, null, 'assetStore'); + let files; + if (editingDraft && !integration) { + const entryFiles = entryDraft + .getIn(['entry', 'mediaFiles'], List()) + .toJS() as MediaFile[]; + console.log(entryFiles); + const entry = entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); + const mediaFolder = selectMediaFolder(state.config, collection, entry, field); + const uniqMediaFiles: MediaFile[] = []; + entryFiles.concat(mediaLibrary.get('files') || []).forEach(mediaFile => { + if (!(uniqMediaFiles.find(uniqueMediaFile => uniqueMediaFile.id === mediaFile.id))) { + uniqMediaFiles.push(mediaFile); + } + }); + files = uniqMediaFiles + .filter(f => f.path.startsWith(mediaFolder)) + .map(file => ({ key: file.id, ...file })) + } else { + files = mediaLibrary.get('files') || []; + } + return files; } export function selectMediaFileByPath(state: State, path: string) { From d04ef684165b2536508f0686dfc0a25cd222c512 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 12 Apr 2021 09:40:26 +0200 Subject: [PATCH 36/45] feat(media dir nav): remove console statement --- packages/netlify-cms-core/src/reducers/mediaLibrary.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index 3646892c485c..d9e537a8107b 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -261,7 +261,6 @@ export function selectMediaFiles(state: State, field?: EntryField) { const entryFiles = entryDraft .getIn(['entry', 'mediaFiles'], List()) .toJS() as MediaFile[]; - console.log(entryFiles); const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); From f31cdcd1c1dc68d154922722f07f7e900f773d7c Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 18 Apr 2021 18:13:13 +0400 Subject: [PATCH 37/45] fix: format and lint errors --- .../MediaLibrary/MediaLibraryBreadCrumbs.js | 5 +- .../MediaLibraryCard.spec.js.snap | 338 +++--------------- .../reducers/__tests__/mediaLibrary.spec.js | 2 + .../src/reducers/mediaLibrary.ts | 6 +- 4 files changed, 48 insertions(+), 303 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 52e4e9ef749a..adc6d2208f8d 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -48,7 +48,10 @@ class MediaLibraryBreadcrumbs extends React.Component { .split('/') .slice(0, -1) .join('/'); - var currentMediaFolderParts = trim((currentMediaFolder || '').replace(hiddenPath, ''), '/').split('/'); + var currentMediaFolderParts = trim( + (currentMediaFolder || '').replace(hiddenPath, ''), + '/', + ).split('/'); var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { return { isDefaultMediaDirectory: index === 0, diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap index 0a641e16891c..dce0819a16cb 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap +++ b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/__snapshots__/MediaLibraryCard.spec.js.snap @@ -2,7 +2,7 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` - .emotion-16 { + .emotion-8 { width: 100px; height: 240px; margin: 10px; @@ -10,63 +10,13 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; - position: relative; } -.emotion-16:focus { +.emotion-8:focus { outline: none; } -.emotion-2 { - position: absolute; - top: 0; - left: 0; - z-index: 1; -} - -.emotion-0 { - display: inline-block; - -webkit-appearance: none; - -moz-appearance: none; - position: relative; - cursor: pointer; - left: 0; - top: 0; - width: 24px; - height: 24px; - padding: 0 !important; - background: transparent !important; - border-width: 0 !important; -} - -.emotion-0:after { - border: 1px solid #fff; - content: ''; - background-color: rgba(0,0,0,0.2); - width: 24px; - height: 24px; - border-radius: 50%; - position: absolute; - top: 0; - left: 0px; -} - -.emotion-0:hover:after { - border-color: #33BCB0; -} - -.emotion-0:checked:after, -.emotion-0.checked:after { - background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; - background-size: 13px; -} - -.emotion-0:focus:after, -.emotion-0:focus:checked:after { - border-color: #fff; -} - -.emotion-8 { +.emotion-4 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -77,43 +27,22 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` position: relative; } -.emotion-6 { +.emotion-2 { width: 100%; height: 160px; - object-fit: cover; + object-fit: contain; border-radius: 2px 2px 0 0; } -.emotion-14 { +.emotion-6 { color: #798291; padding: 8px; - font-size: 0.8em; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; -} - -.emotion-10 { - color: #798291; - font-weight: 600; + margin-top: 20px; overflow-wrap: break-word; line-height: 1.3 !important; - text-align: left; -} - -.emotion-12 { - text-align: left; } -.emotion-4 { +.emotion-0 { color: #70399f; background-color: #f6d8ff; position: absolute; @@ -122,55 +51,37 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = ` }
- -
-

Draft

-
-
- image.png -
-
- - 0 bytes -
-
+ image.png +

`; exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` - .emotion-14 { + .emotion-6 { width: 100px; height: 240px; margin: 10px; @@ -178,63 +89,13 @@ exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; - position: relative; } -.emotion-14:focus { +.emotion-6:focus { outline: none; } .emotion-2 { - position: absolute; - top: 0; - left: 0; - z-index: 1; -} - -.emotion-0 { - display: inline-block; - -webkit-appearance: none; - -moz-appearance: none; - position: relative; - cursor: pointer; - left: 0; - top: 0; - width: 24px; - height: 24px; - padding: 0 !important; - background: transparent !important; - border-width: 0 !important; -} - -.emotion-0:after { - border: 1px solid #fff; - content: ''; - background-color: rgba(0,0,0,0.2); - width: 24px; - height: 24px; - border-radius: 50%; - position: absolute; - top: 0; - left: 0px; -} - -.emotion-0:hover:after { - border-color: #33BCB0; -} - -.emotion-0:checked:after, -.emotion-0.checked:after { - background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; - background-size: 13px; -} - -.emotion-0:focus:after, -.emotion-0:focus:checked:after { - border-color: #fff; -} - -.emotion-6 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -245,86 +106,47 @@ exports[`MediaLibraryCard should match snapshot for non draft image 1`] = ` position: relative; } -.emotion-4 { +.emotion-0 { width: 100%; height: 160px; - object-fit: cover; + object-fit: contain; border-radius: 2px 2px 0 0; } -.emotion-12 { +.emotion-4 { color: #798291; padding: 8px; - font-size: 0.8em; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; -} - -.emotion-8 { - color: #798291; - font-weight: 600; + margin-top: 20px; overflow-wrap: break-word; line-height: 1.3 !important; - text-align: left; -} - -.emotion-10 { - text-align: left; }
- -
-
-
-
- image.png -
-
- - 0 bytes -
-
+ image.png +

`; exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` - .emotion-12 { + .emotion-6 { width: 100px; height: 240px; margin: 10px; @@ -332,63 +154,13 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` border-radius: 5px; cursor: pointer; overflow: hidden; - position: relative; } -.emotion-12:focus { +.emotion-6:focus { outline: none; } .emotion-2 { - position: absolute; - top: 0; - left: 0; - z-index: 1; -} - -.emotion-0 { - display: inline-block; - -webkit-appearance: none; - -moz-appearance: none; - position: relative; - cursor: pointer; - left: 0; - top: 0; - width: 24px; - height: 24px; - padding: 0 !important; - background: transparent !important; - border-width: 0 !important; -} - -.emotion-0:after { - border: 1px solid #fff; - content: ''; - background-color: rgba(0,0,0,0.2); - width: 24px; - height: 24px; - border-radius: 50%; - position: absolute; - top: 0; - left: 0px; -} - -.emotion-0:hover:after { - border-color: #33BCB0; -} - -.emotion-0:checked:after, -.emotion-0.checked:after { - background: #33BCB0 url('data:image/svg+xml;utf8,') no-repeat center 6px; - background-size: 13px; -} - -.emotion-0:focus:after, -.emotion-0:focus:checked:after { - border-color: #fff; -} - -.emotion-6 { height: 162px; background-color: #f2f2f2; background-size: 16px 16px; @@ -399,74 +171,44 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = ` position: relative; } -.emotion-10 { +.emotion-4 { color: #798291; padding: 8px; - font-size: 0.8em; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-pack: center; - -webkit-justify-content: center; - -ms-flex-pack: center; - justify-content: center; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; -} - -.emotion-8 { - color: #798291; - font-weight: 600; + margin-top: 20px; overflow-wrap: break-word; line-height: 1.3 !important; - text-align: left; } -.emotion-4 { +.emotion-0 { width: 100%; height: 160px; - object-fit: cover; + object-fit: contain; border-radius: 2px 2px 0 0; padding: 1em; font-size: 3em; }
- -
-
Not Viewable
-
-
- image.png -
-
+ image.png +

`; diff --git a/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js index fbb2700db757..669de9fb6430 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js @@ -59,6 +59,7 @@ describe('mediaLibrary', () => { { id: 3, path: '/static/images/posts/index.png' }, ], data: {}, + mediaLibrary: {}, }); const state = { config: {}, @@ -98,6 +99,7 @@ describe('mediaLibrary', () => { entryDraft: fromJS({ entry, }), + mediaLibrary: {}, }; expect(selectMediaFiles(state, imageField)).toEqual([ diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index 3646892c485c..470ebba0bed1 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -30,7 +30,6 @@ import { DisplayURLState, EntryField, } from '../types/redux'; -import { dirname } from 'path'; const defaultState: { isVisible: boolean; @@ -261,19 +260,18 @@ export function selectMediaFiles(state: State, field?: EntryField) { const entryFiles = entryDraft .getIn(['entry', 'mediaFiles'], List()) .toJS() as MediaFile[]; - console.log(entryFiles); const entry = entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); const mediaFolder = selectMediaFolder(state.config, collection, entry, field); const uniqMediaFiles: MediaFile[] = []; entryFiles.concat(mediaLibrary.get('files') || []).forEach(mediaFile => { - if (!(uniqMediaFiles.find(uniqueMediaFile => uniqueMediaFile.id === mediaFile.id))) { + if (!uniqMediaFiles.find(uniqueMediaFile => uniqueMediaFile.id === mediaFile.id)) { uniqMediaFiles.push(mediaFile); } }); files = uniqMediaFiles .filter(f => f.path.startsWith(mediaFolder)) - .map(file => ({ key: file.id, ...file })) + .map(file => ({ key: file.id, ...file })); } else { files = mediaLibrary.get('files') || []; } From ab1dab8d60408718fd915999836083a1f0a65d78 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 19 Apr 2021 16:36:05 +0200 Subject: [PATCH 38/45] feat(media dir nav): derive current media folder in entries module --- .../netlify-cms-core/src/actions/media.ts | 1 - .../src/actions/mediaLibrary.ts | 2 - .../netlify-cms-core/src/reducers/entries.ts | 49 ++++++++++++++++--- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/netlify-cms-core/src/actions/media.ts b/packages/netlify-cms-core/src/actions/media.ts index 152a355c75d7..81fcd8c78b91 100644 --- a/packages/netlify-cms-core/src/actions/media.ts +++ b/packages/netlify-cms-core/src/actions/media.ts @@ -96,7 +96,6 @@ export function getAsset({ collection, entry, path, field }: GetAssetArgs) { const state = getState(); const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, field); - let { asset, isLoading, error } = state.medias[resolvedPath] || {}; if (isLoading) { return emptyAsset; diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 0e6dd7f0124b..50e5a5119c59 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -224,7 +224,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { entry, fileName, field, - currentMediaFolder, ); const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path); const editingDraft = selectEditingDraft(state.entryDraft); @@ -278,7 +277,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { entry, fileName, field, - currentMediaFolder, ); assetProxy = createAssetProxy({ file, diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index c354e696bffe..f79550722462 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -727,16 +727,31 @@ function evaluateFolder( return currentFolder; } +export function getMediaFolderConfig( + config: CmsConfig, + collection: Collection | null, + entryMap: EntryMap | undefined, + field: EntryField | undefined, +) { + return evaluateFolder('media_folder', config, collection!, entryMap, field); +} + +export function getPublicFolderConfig( + config: CmsConfig, + collection: Collection | null, + entryMap: EntryMap | undefined, + field: EntryField | undefined, +) { + return evaluateFolder('public_folder', config, collection!, entryMap, field); +} + export function selectMediaFolder( config: CmsConfig, collection: Collection | null, entryMap: EntryMap | undefined, field: EntryField | undefined, - currentMediaFolder?: string, ) { - if (currentMediaFolder) { - return trim(currentMediaFolder, '/'); - } + const name = 'media_folder'; let mediaFolder = config[name]; @@ -756,20 +771,31 @@ export function selectMediaFolder( return trim(mediaFolder, '/'); } +export function selectCurrentMediaFolder( + config: CmsConfig, + collection: Collection | null, + entryMap: EntryMap | undefined, + mediaPath: string, + field: EntryField | undefined, + ) { + const mediaFolder = trim(getMediaFolderConfig(config, collection, entryMap, field), '/'); + const publicFolder = trim(getPublicFolderConfig(config, collection, entryMap, field), '/'); + mediaPath = trim(mediaPath, '/'); + return publicFolder ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); +} + export function selectMediaFilePath( config: CmsConfig, collection: Collection | null, entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, - currentMediaFolder?: string, ) { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectMediaFolder(config, collection, entryMap, field, currentMediaFolder); - + const mediaFolder = selectCurrentMediaFolder(config, collection, entryMap, mediaPath, field); return join(mediaFolder, basename(mediaPath)); } @@ -792,7 +818,14 @@ export function selectMediaFilePublicPath( if (customFolder) { publicFolder = evaluateFolder(name, config, collection!, entryMap, field); } - return join(publicFolder, basename(mediaPath)); + + let mediaFolder = config['media_folder']!; + const customMediaFolder = hasCustomFolder('media_folder', collection, entryMap?.get('slug'), field); + + if (customMediaFolder) { + mediaFolder = evaluateFolder('media_folder', config, collection!, entryMap, field); + } + return join(publicFolder, mediaPath.replace(trim(mediaFolder, '/'), '')); } export function selectEditingDraft(state: EntryDraft) { From 11ee88176182e751ef0310571078ad30f70f5d0a Mon Sep 17 00:00:00 2001 From: Andrew Paterson Date: Tue, 20 Apr 2021 14:35:00 +0200 Subject: [PATCH 39/45] feat(media dir nav): bugfix --- .../netlify-cms-core/src/reducers/entries.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index f79550722462..2aede1f927ff 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -727,22 +727,21 @@ function evaluateFolder( return currentFolder; } -export function getMediaFolderConfig( +export function selectPublicFolder( config: CmsConfig, collection: Collection | null, entryMap: EntryMap | undefined, field: EntryField | undefined, ) { - return evaluateFolder('media_folder', config, collection!, entryMap, field); -} + const name = 'public_folder'; + let publicFolder = config[name]; -export function getPublicFolderConfig( - config: CmsConfig, - collection: Collection | null, - entryMap: EntryMap | undefined, - field: EntryField | undefined, -) { - return evaluateFolder('public_folder', config, collection!, entryMap, field); + const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); + if (customFolder) { + publicFolder = evaluateFolder(name, config, collection!, entryMap, field); + + } + return trim(publicFolder, '/'); } export function selectMediaFolder( @@ -778,8 +777,8 @@ export function selectCurrentMediaFolder( mediaPath: string, field: EntryField | undefined, ) { - const mediaFolder = trim(getMediaFolderConfig(config, collection, entryMap, field), '/'); - const publicFolder = trim(getPublicFolderConfig(config, collection, entryMap, field), '/'); + const mediaFolder = trim(selectMediaFolder(config, collection, entryMap, field), '/'); + const publicFolder = trim(selectPublicFolder(config, collection, entryMap, field), '/'); mediaPath = trim(mediaPath, '/'); return publicFolder ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); } From 7a27a652a4b0a22b6a6b5bbec3921033946838e7 Mon Sep 17 00:00:00 2001 From: Andrew Paterson Date: Tue, 20 Apr 2021 16:58:24 +0200 Subject: [PATCH 40/45] feat(media dir nav): bugfix --- packages/netlify-cms-core/src/actions/mediaLibrary.ts | 3 ++- packages/netlify-cms-core/src/reducers/entries.ts | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 50e5a5119c59..fdcd60d5642a 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -224,6 +224,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { entry, fileName, field, + currentMediaFolder, ); const existingFile = files.find(existingFile => existingFile.path.toLowerCase() === path); const editingDraft = selectEditingDraft(state.entryDraft); @@ -277,6 +278,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { entry, fileName, field, + currentMediaFolder, ); assetProxy = createAssetProxy({ file, @@ -284,7 +286,6 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { field, }); } - dispatch(addAsset(assetProxy)); let mediaFile: ImplementationMediaFile; diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 2aede1f927ff..405340ed50e0 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -739,7 +739,6 @@ export function selectPublicFolder( const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); if (customFolder) { publicFolder = evaluateFolder(name, config, collection!, entryMap, field); - } return trim(publicFolder, '/'); } @@ -777,10 +776,10 @@ export function selectCurrentMediaFolder( mediaPath: string, field: EntryField | undefined, ) { - const mediaFolder = trim(selectMediaFolder(config, collection, entryMap, field), '/'); - const publicFolder = trim(selectPublicFolder(config, collection, entryMap, field), '/'); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field); + const publicFolder = selectPublicFolder(config, collection, entryMap, field); mediaPath = trim(mediaPath, '/'); - return publicFolder ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); + return mediaPath.startsWith(publicFolder) ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); } export function selectMediaFilePath( @@ -789,12 +788,13 @@ export function selectMediaFilePath( entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, + currentMediaFolder?: string, ) { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectCurrentMediaFolder(config, collection, entryMap, mediaPath, field); + const mediaFolder = currentMediaFolder ? currentMediaFolder : selectCurrentMediaFolder(config, collection, entryMap, mediaPath, field); return join(mediaFolder, basename(mediaPath)); } From f9ce92e75dc664aff8b3228ee070a3fb48af0684 Mon Sep 17 00:00:00 2001 From: Andrew Paterson Date: Wed, 21 Apr 2021 16:05:20 +0200 Subject: [PATCH 41/45] feat(media dir nav): small refactor to fix failing tests. --- .../src/actions/mediaLibrary.ts | 1 + .../src/reducers/__tests__/entries.spec.js | 12 ++++ .../netlify-cms-core/src/reducers/entries.ts | 67 +++++++++++++------ 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index fdcd60d5642a..0e6dd7f0124b 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -286,6 +286,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { field, }); } + dispatch(addAsset(assetProxy)); let mediaFile: ImplementationMediaFile; diff --git a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js index f5d314486ebb..3f9549f98662 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js @@ -559,6 +559,18 @@ describe('entries', () => { ), ).toBe('/images/image.png'); }); + + it('should use full file path', () => { + expect( + selectMediaFilePublicPath( + { media_folder: 'static/media', public_folder: '/media' }, + null, + 'static/media/images/foo/image.png', + undefined, + undefined, + ), + ).toBe('/media/images/foo/image.png'); + }); }); describe('selectEntries', () => { diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 405340ed50e0..6544af3a838b 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -740,7 +740,7 @@ export function selectPublicFolder( if (customFolder) { publicFolder = evaluateFolder(name, config, collection!, entryMap, field); } - return trim(publicFolder, '/'); + return publicFolder; } export function selectMediaFolder( @@ -749,7 +749,6 @@ export function selectMediaFolder( entryMap: EntryMap | undefined, field: EntryField | undefined, ) { - const name = 'media_folder'; let mediaFolder = config[name]; @@ -775,11 +774,13 @@ export function selectCurrentMediaFolder( entryMap: EntryMap | undefined, mediaPath: string, field: EntryField | undefined, - ) { +) { const mediaFolder = selectMediaFolder(config, collection, entryMap, field); - const publicFolder = selectPublicFolder(config, collection, entryMap, field); + const publicFolder = trim(selectPublicFolder(config, collection, entryMap, field), '/'); mediaPath = trim(mediaPath, '/'); - return mediaPath.startsWith(publicFolder) ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); + return mediaPath.startsWith(publicFolder) + ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) + : dirname(mediaPath); } export function selectMediaFilePath( @@ -794,10 +795,29 @@ export function selectMediaFilePath( return mediaPath; } - const mediaFolder = currentMediaFolder ? currentMediaFolder : selectCurrentMediaFolder(config, collection, entryMap, mediaPath, field); + const mediaFolder = currentMediaFolder + ? currentMediaFolder + : selectCurrentMediaFolder(config, collection, entryMap, mediaPath, field); return join(mediaFolder, basename(mediaPath)); } +export function removeMediaFolderFromPath( + config: CmsConfig, + collection: Collection | null, + mediaPath: string, + entryMap: EntryMap | undefined, + field: EntryField | undefined, +) { + const trimmedMediaPath = trim(mediaPath, '/') + let mediaFolder = selectMediaFolder(config, collection, entryMap, field); + if (!mediaFolder || mediaFolder === '') { + mediaFolder = selectPublicFolder(config, collection, entryMap, field) || ''; + } + return trimmedMediaPath.startsWith(mediaFolder) + ? trimmedMediaPath.replace(mediaFolder, '') + : mediaPath; +} + export function selectMediaFilePublicPath( config: CmsConfig, collection: Collection | null, @@ -809,23 +829,32 @@ export function selectMediaFilePublicPath( return mediaPath; } - const name = 'public_folder'; - let publicFolder = config[name]!; + const publicFolder = selectPublicFolder(config, collection, entryMap, field) || ''; + return join(publicFolder, removeMediaFolderFromPath(config, collection, mediaPath, entryMap, field)); +} - const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); +// export function selectMediaFilePublicPath( +// config: CmsConfig, +// collection: Collection | null, +// mediaPath: string, +// entryMap: EntryMap | undefined, +// field: EntryField | undefined, +// ) { +// if (isAbsolutePath(mediaPath)) { +// return mediaPath; +// } - if (customFolder) { - publicFolder = evaluateFolder(name, config, collection!, entryMap, field); - } +// const name = 'public_folder'; +// let publicFolder = config[name]!; - let mediaFolder = config['media_folder']!; - const customMediaFolder = hasCustomFolder('media_folder', collection, entryMap?.get('slug'), field); +// const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); - if (customMediaFolder) { - mediaFolder = evaluateFolder('media_folder', config, collection!, entryMap, field); - } - return join(publicFolder, mediaPath.replace(trim(mediaFolder, '/'), '')); -} +// if (customFolder) { +// publicFolder = evaluateFolder(name, config, collection!, entryMap, field); +// } + +// return join(publicFolder, basename(mediaPath)); +// } export function selectEditingDraft(state: EntryDraft) { const entry = state.get('entry'); From da3ff3ec37e15a974e09fad226fda441bb2d83c3 Mon Sep 17 00:00:00 2001 From: andrew-paterson Date: Mon, 26 Apr 2021 15:53:27 +0200 Subject: [PATCH 42/45] feat(media dir nav): handle relative media_folder set on field --- packages/netlify-cms-core/src/reducers/entries.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 6544af3a838b..7fa6d540d42e 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -778,9 +778,19 @@ export function selectCurrentMediaFolder( const mediaFolder = selectMediaFolder(config, collection, entryMap, field); const publicFolder = trim(selectPublicFolder(config, collection, entryMap, field), '/'); mediaPath = trim(mediaPath, '/'); - return mediaPath.startsWith(publicFolder) + + let currentMediaFolder = mediaPath.startsWith(publicFolder) ? dirname(join(mediaFolder, mediaPath.replace(publicFolder, ''))) : dirname(mediaPath); + + const customFolder = hasCustomFolder('media_folder', collection, entryMap?.get('slug'), field); + if (customFolder) { + const folder = evaluateFolder('media_folder', config, collection!, entryMap, field); + if (!folder.startsWith('/')) { + currentMediaFolder = mediaFolder; + } + } + return currentMediaFolder; } export function selectMediaFilePath( From 10fe4f21ebc05ba27056712df5a390dd7e3ffb6f Mon Sep 17 00:00:00 2001 From: erezrokah Date: Sun, 30 May 2021 22:07:14 +0300 Subject: [PATCH 43/45] style: run prettier --- .../components/MediaLibrary/MediaLibraryBreadCrumbs.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index adc6d2208f8d..4fc884a0b779 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -44,15 +44,12 @@ class MediaLibraryBreadcrumbs extends React.Component { defaultMediaFolder, mediaFolderNavDisabled, } = this.props; - var hiddenPath = trim(defaultMediaFolder, '/') - .split('/') - .slice(0, -1) - .join('/'); - var currentMediaFolderParts = trim( + const hiddenPath = trim(defaultMediaFolder, '/').split('/').slice(0, -1).join('/'); + const currentMediaFolderParts = trim( (currentMediaFolder || '').replace(hiddenPath, ''), '/', ).split('/'); - var breadcrumbsArray = currentMediaFolderParts.map((part, index) => { + const breadcrumbsArray = currentMediaFolderParts.map((part, index) => { return { isDefaultMediaDirectory: index === 0, path: `${hiddenPath}/${currentMediaFolderParts.slice(0, index + 1).join('/')}`, From d3736d55554d04a7174e811e7399ff378645cbfa Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Thu, 29 Aug 2024 14:25:03 +0200 Subject: [PATCH 44/45] fix: css lint --- .../decap-cms-core/src/components/MediaLibrary/MediaLibrary.js | 2 +- .../src/components/MediaLibrary/MediaLibraryBreadCrumbs.js | 2 +- .../src/components/MediaLibrary/MediaLibraryCreateFolder.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibrary.js index d7e4506ba4b2..93396eef8dd0 100644 --- a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -6,6 +6,7 @@ import { orderBy, map } from 'lodash'; import { translate } from 'react-polyglot'; import fuzzy from 'fuzzy'; import { fileExtension } from 'decap-cms-lib-util'; +import { dirname } from 'path'; import { loadMedia as loadMediaAction, @@ -20,7 +21,6 @@ import { getInitialMediaFolder, getMediaFolderNavDisabled, } from '../../reducers/mediaLibrary'; -import { dirname } from 'path'; import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; /** diff --git a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js index 4fc884a0b779..7a9ce473ca9a 100644 --- a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js +++ b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryBreadCrumbs.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { trim } from 'lodash'; import styled from '@emotion/styled'; -import { Icon } from 'netlify-cms-ui-default'; +import { Icon } from 'decap-cms-ui-default'; const BreadCrumbsContainer = styled.div` display: flex; diff --git a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js index 7e3faaa5446e..30c8e2f8e312 100644 --- a/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js +++ b/packages/decap-cms-core/src/components/MediaLibrary/MediaLibraryCreateFolder.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; -import { Icon, lengths, colors, zIndex } from 'netlify-cms-ui-default'; +import { Icon, lengths, colors, zIndex } from 'decap-cms-ui-default'; const CreateFolderContainer = styled.div` height: 37px; From fbee62322d2677c2b1259b304deaa028cac7a9a6 Mon Sep 17 00:00:00 2001 From: Martin Jagodic Date: Thu, 29 Aug 2024 14:30:32 +0200 Subject: [PATCH 45/45] fix: js lint --- packages/decap-cms-core/src/reducers/mediaLibrary.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/decap-cms-core/src/reducers/mediaLibrary.ts b/packages/decap-cms-core/src/reducers/mediaLibrary.ts index 6fd0ffa34aac..f4ba4d66e8bc 100644 --- a/packages/decap-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/decap-cms-core/src/reducers/mediaLibrary.ts @@ -1,6 +1,5 @@ import { Map, List } from 'immutable'; import { v4 as uuid } from 'uuid'; -import { dirname } from 'path'; import { MEDIA_LIBRARY_OPEN,