From 4b73e11db0627f4046d5e23e2fa3b53b3f6788de Mon Sep 17 00:00:00 2001 From: Erez Rokah Date: Wed, 3 Feb 2021 04:54:49 -0800 Subject: [PATCH] feat(media-library): add copy to clipboard, allow avif (#4914) --- __mocks__/styleMock.js | 1 + jest.config.js | 1 + .../components/MediaLibrary/MediaLibrary.js | 12 ++- .../MediaLibrary/MediaLibraryButtons.js | 85 ++++++++++++++++--- .../MediaLibrary/MediaLibraryModal.js | 1 + .../MediaLibrary/MediaLibraryTop.js | 34 ++++++-- .../__tests__/MediaLibraryButtons.spec.js | 44 ++++++++++ packages/netlify-cms-locales/src/en/index.js | 5 ++ 8 files changed, 162 insertions(+), 21 deletions(-) create mode 100644 __mocks__/styleMock.js create mode 100644 packages/netlify-cms-core/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/__mocks__/styleMock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/jest.config.js b/jest.config.js index bcbb4caf..15f0381e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,7 @@ module.exports = { 'netlify-cms-backend-github': '/packages/netlify-cms-backend-github/src/index.ts', 'netlify-cms-lib-widgets': '/packages/netlify-cms-lib-widgets/src/index.ts', 'netlify-cms-widget-object': '/packages/netlify-cms-widget-object/src/index.js', + '\\.(css|less)$': '/__mocks__/styleMock.js', }, testURL: 'http://localhost:8080', snapshotSerializers: ['jest-emotion'], diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index dc8f0ced..a3d39477 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -21,7 +21,17 @@ import MediaLibraryModal, { fileShape } from './MediaLibraryModal'; * Extensions used to determine which files to show when the media library is * accessed from an image insertion field. */ -const IMAGE_EXTENSIONS_VIEWABLE = ['jpg', 'jpeg', 'webp', 'gif', 'png', 'bmp', 'tiff', 'svg']; +const IMAGE_EXTENSIONS_VIEWABLE = [ + 'jpg', + 'jpeg', + 'webp', + 'gif', + 'png', + 'bmp', + 'tiff', + 'svg', + 'avif', +]; const IMAGE_EXTENSIONS = [...IMAGE_EXTENSIONS_VIEWABLE]; class MediaLibrary extends React.Component { diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js index cf564059..80b4f48e 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryButtons.js @@ -1,7 +1,11 @@ +import React from 'react'; +import PropTypes from 'prop-types'; import { css } from '@emotion/core'; import styled from '@emotion/styled'; import { FileUploadButton } from 'UI'; -import { buttons, colors, colorsRaw, shadows, zIndex } from 'netlify-cms-ui-default'; +import copyToClipboard from 'copy-text-to-clipboard'; +import { isAbsolutePath } from 'netlify-cms-lib-util'; +import { buttons, shadows, zIndex } from 'netlify-cms-ui-default'; const styles = { button: css` @@ -55,18 +59,77 @@ export const InsertButton = styled.button` ${buttons.green}; `; -export const DownloadButton = styled.button` +const ActionButton = styled.button` ${styles.button}; - background-color: ${colors.button}; - color: ${colors.buttonText}; - ${props => - props.focused === true && + !props.disabled && css` - &:focus, - &:hover { - color: ${colorsRaw.white}; - background-color: #555a65; - } + ${buttons.gray} `} `; + +export const DownloadButton = ActionButton; + +export class CopyToClipBoardButton extends React.Component { + mounted = false; + timeout; + + state = { + copied: false, + }; + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handleCopy = () => { + clearTimeout(this.timeout); + const { path, draft, name } = this.props; + copyToClipboard(isAbsolutePath(path) || !draft ? path : name); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.mounted && this.setState({ copied: false }), 1500); + }; + + getTitle = () => { + const { t, path, draft } = this.props; + if (this.state.copied) { + return t('mediaLibrary.mediaLibraryCard.copied'); + } + + if (!path) { + return t('mediaLibrary.mediaLibraryCard.copy'); + } + + if (isAbsolutePath(path)) { + return t('mediaLibrary.mediaLibraryCard.copyUrl'); + } + + if (draft) { + return t('mediaLibrary.mediaLibraryCard.copyName'); + } + + return t('mediaLibrary.mediaLibraryCard.copyPath'); + }; + + render() { + const { disabled } = this.props; + + return ( + + {this.getTitle()} + + ); + } +} + +CopyToClipBoardButton.propTypes = { + disabled: PropTypes.bool.isRequired, + draft: PropTypes.bool, + path: PropTypes.string, + name: PropTypes.string, + t: 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 30109485..a6ffe363 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryModal.js @@ -128,6 +128,7 @@ const MediaLibraryModal = ({ hasSelection={hasSelection} isPersisting={isPersisting} isDeleting={isDeleting} + selectedFile={selectedFile} /> {!shouldShowEmptyMessage ? null : ( diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js index c2cec3e2..b0350482 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibraryTop.js @@ -3,7 +3,13 @@ import PropTypes from 'prop-types'; import styled from '@emotion/styled'; import MediaLibrarySearch from './MediaLibrarySearch'; import MediaLibraryHeader from './MediaLibraryHeader'; -import { UploadButton, DeleteButton, DownloadButton, InsertButton } from './MediaLibraryButtons'; +import { + UploadButton, + DeleteButton, + DownloadButton, + CopyToClipBoardButton, + InsertButton, +} from './MediaLibraryButtons'; const LibraryTop = styled.div` position: relative; @@ -37,12 +43,11 @@ const MediaLibraryTop = ({ hasSelection, isPersisting, isDeleting, + selectedFile, }) => { const shouldShowButtonLoader = isPersisting || isDeleting; const uploadEnabled = !shouldShowButtonLoader; const deleteEnabled = !shouldShowButtonLoader && hasSelection; - const downloadEnabled = hasSelection; - const insertEnabled = hasSelection; const uploadButtonLabel = isPersisting ? t('mediaLibrary.mediaLibraryModal.uploading') @@ -66,11 +71,14 @@ const MediaLibraryTop = ({ isPrivate={privateUpload} /> - + + {downloadButtonLabel} {!canInsert ? null : ( - + {insertButtonLabel} )} @@ -121,6 +129,14 @@ MediaLibraryTop.propTypes = { hasSelection: PropTypes.bool.isRequired, isPersisting: PropTypes.bool, isDeleting: PropTypes.bool, + selectedFile: PropTypes.oneOfType([ + PropTypes.shape({ + path: PropTypes.string.isRequired, + draft: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, + }), + PropTypes.shape({}), + ]), }; export default MediaLibraryTop; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js new file mode 100644 index 00000000..1c806c11 --- /dev/null +++ b/packages/netlify-cms-core/src/components/MediaLibrary/__tests__/MediaLibraryButtons.spec.js @@ -0,0 +1,44 @@ +import React from 'react'; +import { CopyToClipBoardButton } from '../MediaLibraryButtons'; +import { render } from '@testing-library/react'; + +describe('CopyToClipBoardButton', () => { + const props = { + disabled: false, + t: jest.fn(key => key), + }; + + it('should use copy text when no path is defined', () => { + const { container } = render(); + + expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copy'); + }); + + it('should use copyUrl text when path is absolute and is draft', () => { + const { container } = render( + , + ); + + expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl'); + }); + + it('should use copyUrl text when path is absolute and is not draft', () => { + const { container } = render( + , + ); + + expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl'); + }); + + it('should use copyName when path is not absolute and is draft', () => { + const { container } = render(); + + expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyName'); + }); + + it('should use copyPath when path is not absolute and is not draft', () => { + const { container } = render(); + + expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyPath'); + }); +}); diff --git a/packages/netlify-cms-locales/src/en/index.js b/packages/netlify-cms-locales/src/en/index.js index f48279fb..99db1493 100644 --- a/packages/netlify-cms-locales/src/en/index.js +++ b/packages/netlify-cms-locales/src/en/index.js @@ -193,6 +193,11 @@ const en = { mediaLibrary: { mediaLibraryCard: { draft: 'Draft', + copy: 'Copy', + copyUrl: 'Copy URL', + copyPath: 'Copy Path', + copyName: 'Copy Name', + copied: 'Copied', }, mediaLibrary: { onDelete: 'Are you sure you want to delete selected media?',