diff --git a/package.json b/package.json index 03331c95..c3fedc5c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "color-hash": "^1.0.3", "commander": "^2.19.0", "connected-react-router": "6.0.0", + "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^4.6.0", "cross-env": "^5.2.0", "date-fns": "^1.30.1", diff --git a/src/actions/LogActions.js b/src/actions/LogActions.js index 7feeaf85..eaac6597 100644 --- a/src/actions/LogActions.js +++ b/src/actions/LogActions.js @@ -1,6 +1,7 @@ /* @flow */ import * as LOG from 'actions/constants/log'; +import copy from 'copy-to-clipboard'; import type { Action, ThunkAction, GetState, Dispatch } from 'flowtype'; import type { LogEntry } from 'reducers/LogReducer'; @@ -12,6 +13,12 @@ export type LogAction = | { type: typeof LOG.CLOSE, } + | { + type: typeof LOG.COPY_RESET, + } + | { + type: typeof LOG.COPY_SUCCESS, + } | { type: typeof LOG.ADD, payload: LogEntry, @@ -39,3 +46,24 @@ export const add = (type: string, message: any): Action => ({ message, }, }); + +export const copyToClipboard = (): ThunkAction => ( + dispatch: Dispatch, + getState: GetState +): void => { + const { entries } = getState().log; + try { + const res = copy(JSON.stringify(entries)); + if (res) { + dispatch({ + type: LOG.COPY_SUCCESS, + }); + } + } catch (err) { + console.error(err); + } +}; + +export const resetCopyState = (): Action => ({ + type: LOG.COPY_RESET, +}); diff --git a/src/actions/constants/log.js b/src/actions/constants/log.js index 35117873..33b89731 100644 --- a/src/actions/constants/log.js +++ b/src/actions/constants/log.js @@ -3,3 +3,5 @@ export const OPEN: 'log__open' = 'log__open'; export const CLOSE: 'log__close' = 'log__close'; export const ADD: 'log__add' = 'log__add'; +export const COPY_SUCCESS: 'log__copy_success' = 'log__copy_success'; +export const COPY_RESET: 'log__copy_reset' = 'log__copy_reset'; diff --git a/src/components/Log/index.js b/src/components/Log/index.js index 5fd33408..fadff58d 100644 --- a/src/components/Log/index.js +++ b/src/components/Log/index.js @@ -5,6 +5,8 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import colors from 'config/colors'; import { H2 } from 'components/Heading'; +import Button from 'components/Button'; +import Tooltip from 'components/Tooltip'; import ReactJson from 'react-json-view'; import Icon from 'components/Icon'; import P from 'components/Paragraph'; @@ -18,6 +20,8 @@ import l10nMessages from './index.messages'; type Props = { log: $ElementType, toggle: typeof LogActions.toggle, + copyToClipboard: typeof LogActions.copyToClipboard, + resetCopyState: typeof LogActions.resetCopyState, }; const Wrapper = styled.div` @@ -59,8 +63,20 @@ const LogWrapper = styled.div` overflow: scroll; `; +const CopyWrapper = styled.div``; + +const ButtonCopy = styled(Button)` + margin-top: 10px; +`; + const Log = (props: Props): ?React$Element => { if (!props.log.opened) return null; + + const copyBtn = ( + props.copyToClipboard()}> + + + ); return ( @@ -75,6 +91,19 @@ const Log = (props: Props): ?React$Element => { + {props.log.copied ? ( + } + afterVisibleChange={props.resetCopyState} + > + {copyBtn} + + ) : ( + {copyBtn} + )} ); }; @@ -85,5 +114,7 @@ export default connect( }), (dispatch: Dispatch) => ({ toggle: bindActionCreators(LogActions.toggle, dispatch), + copyToClipboard: bindActionCreators(LogActions.copyToClipboard, dispatch), + resetCopyState: bindActionCreators(LogActions.resetCopyState, dispatch), }) )(Log); diff --git a/src/components/Log/index.messages.js b/src/components/Log/index.messages.js index 859bca74..702d677c 100644 --- a/src/components/Log/index.messages.js +++ b/src/components/Log/index.messages.js @@ -13,6 +13,14 @@ const definedMessages: Messages = defineMessages({ defaultMessage: 'Log', description: 'application event and error', }, + TR_COPY_TO_CLIPBOARD: { + id: 'TR_COPY_TO_CLIPBOARD', + defaultMessage: 'Copy to clipboard', + }, + TR_COPIED: { + id: 'TR_COPIED', + defaultMessage: 'Copied!', + }, }); export default definedMessages; diff --git a/src/components/Tooltip/index.js b/src/components/Tooltip/index.js index 7edc9f5e..2fca2817 100644 --- a/src/components/Tooltip/index.js +++ b/src/components/Tooltip/index.js @@ -34,12 +34,15 @@ const Tooltip = ({ readMoreLink, children, enterDelayMs, + defaultVisible = false, + ...rest }) => ( } placement={placement} mouseEnterDelay={enterDelayMs || 0} + defaultVisible={defaultVisible} overlay={() => ( {content} @@ -52,6 +55,7 @@ const Tooltip = ({ )} )} + {...rest} > {children} @@ -66,6 +70,7 @@ Tooltip.propTypes = { content: PropTypes.oneOfType([PropTypes.element, PropTypes.string]), readMoreLink: PropTypes.string, enterDelayMs: PropTypes.number, + defaultVisible: PropTypes.bool, }; export default Tooltip; diff --git a/src/reducers/LogReducer.js b/src/reducers/LogReducer.js index 4d85f24e..20b2c51c 100644 --- a/src/reducers/LogReducer.js +++ b/src/reducers/LogReducer.js @@ -12,11 +12,13 @@ export type LogEntry = { export type State = { opened: boolean, entries: Array, + copied: boolean, }; export const initialState: State = { opened: false, entries: [], + copied: false, }; export default (state: State = initialState, action: Action): State => { @@ -31,6 +33,7 @@ export default (state: State = initialState, action: Action): State => { return { ...state, opened: false, + copied: false, }; case LOG.ADD: @@ -39,6 +42,18 @@ export default (state: State = initialState, action: Action): State => { entries: state.entries.concat([action.payload]), }; + case LOG.COPY_SUCCESS: + return { + ...state, + copied: true, + }; + + case LOG.COPY_RESET: + return { + ...state, + copied: false, + }; + default: return state; } diff --git a/yarn.lock b/yarn.lock index 5e1cc386..da6c31c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3114,6 +3114,13 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-to-clipboard@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9" + integrity sha512-c3GdeY8qxCHGezVb1EFQfHYK/8NZRemgcTIzPq7PuxjHAf/raKibn2QdhHPb/y6q74PMgH6yizaDZlRmw6QyKw== + dependencies: + toggle-selection "^1.0.3" + copy-webpack-plugin@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.6.0.tgz#e7f40dd8a68477d405dd1b7a854aae324b158bae" @@ -11076,6 +11083,11 @@ to-regex@^3.0.1, to-regex@^3.0.2: regex-not "^1.0.2" safe-regex "^1.1.0" +toggle-selection@^1.0.3: + version "1.0.6" + resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= + toposort@^1.0.0: version "1.0.7" resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"