feat(media-library): add copy to clipboard, allow avif (#4914)
This commit is contained in:
parent
51e301c594
commit
4b73e11db0
1
__mocks__/styleMock.js
Normal file
1
__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = {};
|
@ -7,6 +7,7 @@ module.exports = {
|
|||||||
'netlify-cms-backend-github': '<rootDir>/packages/netlify-cms-backend-github/src/index.ts',
|
'netlify-cms-backend-github': '<rootDir>/packages/netlify-cms-backend-github/src/index.ts',
|
||||||
'netlify-cms-lib-widgets': '<rootDir>/packages/netlify-cms-lib-widgets/src/index.ts',
|
'netlify-cms-lib-widgets': '<rootDir>/packages/netlify-cms-lib-widgets/src/index.ts',
|
||||||
'netlify-cms-widget-object': '<rootDir>/packages/netlify-cms-widget-object/src/index.js',
|
'netlify-cms-widget-object': '<rootDir>/packages/netlify-cms-widget-object/src/index.js',
|
||||||
|
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||||
},
|
},
|
||||||
testURL: 'http://localhost:8080',
|
testURL: 'http://localhost:8080',
|
||||||
snapshotSerializers: ['jest-emotion'],
|
snapshotSerializers: ['jest-emotion'],
|
||||||
|
@ -21,7 +21,17 @@ import MediaLibraryModal, { fileShape } from './MediaLibraryModal';
|
|||||||
* Extensions used to determine which files to show when the media library is
|
* Extensions used to determine which files to show when the media library is
|
||||||
* accessed from an image insertion field.
|
* 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];
|
const IMAGE_EXTENSIONS = [...IMAGE_EXTENSIONS_VIEWABLE];
|
||||||
|
|
||||||
class MediaLibrary extends React.Component {
|
class MediaLibrary extends React.Component {
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { css } from '@emotion/core';
|
import { css } from '@emotion/core';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { FileUploadButton } from 'UI';
|
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 = {
|
const styles = {
|
||||||
button: css`
|
button: css`
|
||||||
@ -55,18 +59,77 @@ export const InsertButton = styled.button`
|
|||||||
${buttons.green};
|
${buttons.green};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DownloadButton = styled.button`
|
const ActionButton = styled.button`
|
||||||
${styles.button};
|
${styles.button};
|
||||||
background-color: ${colors.button};
|
|
||||||
color: ${colors.buttonText};
|
|
||||||
|
|
||||||
${props =>
|
${props =>
|
||||||
props.focused === true &&
|
!props.disabled &&
|
||||||
css`
|
css`
|
||||||
&:focus,
|
${buttons.gray}
|
||||||
&:hover {
|
|
||||||
color: ${colorsRaw.white};
|
|
||||||
background-color: #555a65;
|
|
||||||
}
|
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ActionButton disabled={disabled} onClick={this.handleCopy}>
|
||||||
|
{this.getTitle()}
|
||||||
|
</ActionButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CopyToClipBoardButton.propTypes = {
|
||||||
|
disabled: PropTypes.bool.isRequired,
|
||||||
|
draft: PropTypes.bool,
|
||||||
|
path: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
t: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
@ -128,6 +128,7 @@ const MediaLibraryModal = ({
|
|||||||
hasSelection={hasSelection}
|
hasSelection={hasSelection}
|
||||||
isPersisting={isPersisting}
|
isPersisting={isPersisting}
|
||||||
isDeleting={isDeleting}
|
isDeleting={isDeleting}
|
||||||
|
selectedFile={selectedFile}
|
||||||
/>
|
/>
|
||||||
{!shouldShowEmptyMessage ? null : (
|
{!shouldShowEmptyMessage ? null : (
|
||||||
<EmptyMessage content={emptyMessage} isPrivate={privateUpload} />
|
<EmptyMessage content={emptyMessage} isPrivate={privateUpload} />
|
||||||
|
@ -3,7 +3,13 @@ import PropTypes from 'prop-types';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import MediaLibrarySearch from './MediaLibrarySearch';
|
import MediaLibrarySearch from './MediaLibrarySearch';
|
||||||
import MediaLibraryHeader from './MediaLibraryHeader';
|
import MediaLibraryHeader from './MediaLibraryHeader';
|
||||||
import { UploadButton, DeleteButton, DownloadButton, InsertButton } from './MediaLibraryButtons';
|
import {
|
||||||
|
UploadButton,
|
||||||
|
DeleteButton,
|
||||||
|
DownloadButton,
|
||||||
|
CopyToClipBoardButton,
|
||||||
|
InsertButton,
|
||||||
|
} from './MediaLibraryButtons';
|
||||||
|
|
||||||
const LibraryTop = styled.div`
|
const LibraryTop = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -37,12 +43,11 @@ const MediaLibraryTop = ({
|
|||||||
hasSelection,
|
hasSelection,
|
||||||
isPersisting,
|
isPersisting,
|
||||||
isDeleting,
|
isDeleting,
|
||||||
|
selectedFile,
|
||||||
}) => {
|
}) => {
|
||||||
const shouldShowButtonLoader = isPersisting || isDeleting;
|
const shouldShowButtonLoader = isPersisting || isDeleting;
|
||||||
const uploadEnabled = !shouldShowButtonLoader;
|
const uploadEnabled = !shouldShowButtonLoader;
|
||||||
const deleteEnabled = !shouldShowButtonLoader && hasSelection;
|
const deleteEnabled = !shouldShowButtonLoader && hasSelection;
|
||||||
const downloadEnabled = hasSelection;
|
|
||||||
const insertEnabled = hasSelection;
|
|
||||||
|
|
||||||
const uploadButtonLabel = isPersisting
|
const uploadButtonLabel = isPersisting
|
||||||
? t('mediaLibrary.mediaLibraryModal.uploading')
|
? t('mediaLibrary.mediaLibraryModal.uploading')
|
||||||
@ -66,11 +71,14 @@ const MediaLibraryTop = ({
|
|||||||
isPrivate={privateUpload}
|
isPrivate={privateUpload}
|
||||||
/>
|
/>
|
||||||
<ButtonsContainer>
|
<ButtonsContainer>
|
||||||
<DownloadButton
|
<CopyToClipBoardButton
|
||||||
onClick={onDownload}
|
disabled={!hasSelection}
|
||||||
disabled={!downloadEnabled}
|
path={selectedFile.path}
|
||||||
focused={downloadEnabled}
|
name={selectedFile.name}
|
||||||
>
|
draft={selectedFile.draft}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
<DownloadButton onClick={onDownload} disabled={!hasSelection}>
|
||||||
{downloadButtonLabel}
|
{downloadButtonLabel}
|
||||||
</DownloadButton>
|
</DownloadButton>
|
||||||
<UploadButton
|
<UploadButton
|
||||||
@ -94,7 +102,7 @@ const MediaLibraryTop = ({
|
|||||||
{deleteButtonLabel}
|
{deleteButtonLabel}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
{!canInsert ? null : (
|
{!canInsert ? null : (
|
||||||
<InsertButton onClick={onInsert} disabled={!insertEnabled}>
|
<InsertButton onClick={onInsert} disabled={!hasSelection}>
|
||||||
{insertButtonLabel}
|
{insertButtonLabel}
|
||||||
</InsertButton>
|
</InsertButton>
|
||||||
)}
|
)}
|
||||||
@ -121,6 +129,14 @@ MediaLibraryTop.propTypes = {
|
|||||||
hasSelection: PropTypes.bool.isRequired,
|
hasSelection: PropTypes.bool.isRequired,
|
||||||
isPersisting: PropTypes.bool,
|
isPersisting: PropTypes.bool,
|
||||||
isDeleting: 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;
|
export default MediaLibraryTop;
|
||||||
|
@ -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(<CopyToClipBoardButton {...props} />);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use copyUrl text when path is absolute and is draft', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<CopyToClipBoardButton {...props} path="https://www.images.com/image.png" draft />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use copyUrl text when path is absolute and is not draft', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<CopyToClipBoardButton {...props} path="https://www.images.com/image.png" />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyUrl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use copyName when path is not absolute and is draft', () => {
|
||||||
|
const { container } = render(<CopyToClipBoardButton {...props} path="image.png" draft />);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyName');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use copyPath when path is not absolute and is not draft', () => {
|
||||||
|
const { container } = render(<CopyToClipBoardButton {...props} path="image.png" />);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent('mediaLibrary.mediaLibraryCard.copyPath');
|
||||||
|
});
|
||||||
|
});
|
@ -193,6 +193,11 @@ const en = {
|
|||||||
mediaLibrary: {
|
mediaLibrary: {
|
||||||
mediaLibraryCard: {
|
mediaLibraryCard: {
|
||||||
draft: 'Draft',
|
draft: 'Draft',
|
||||||
|
copy: 'Copy',
|
||||||
|
copyUrl: 'Copy URL',
|
||||||
|
copyPath: 'Copy Path',
|
||||||
|
copyName: 'Copy Name',
|
||||||
|
copied: 'Copied',
|
||||||
},
|
},
|
||||||
mediaLibrary: {
|
mediaLibrary: {
|
||||||
onDelete: 'Are you sure you want to delete selected media?',
|
onDelete: 'Are you sure you want to delete selected media?',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user