diff --git a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx new file mode 100644 index 0000000000..9bd91fbac3 --- /dev/null +++ b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.jsx @@ -0,0 +1,92 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Tooltip } from '@patternfly/react-core'; +import { CopyIcon } from '@patternfly/react-icons'; + +import styled from 'styled-components'; + +const CopyButton = styled(Button)` + padding: 2px 4px; + margin-left: 8px; + border: none; + &:hover { + background-color: #0066cc; + color: white; + } +`; + +export const clipboardCopyFunc = (event, text) => { + const clipboard = event.currentTarget.parentElement; + const el = document.createElement('input'); + el.value = text; + clipboard.appendChild(el); + el.select(); + document.execCommand('copy'); + clipboard.removeChild(el); +}; + +class ClipboardCopyButton extends React.Component { + constructor(props) { + super(props); + + this.state = { + copied: false, + }; + + this.handleCopyClick = this.handleCopyClick.bind(this); + } + + handleCopyClick = event => { + const { stringToCopy, switchDelay } = this.props; + if (this.timer) { + window.clearTimeout(this.timer); + this.setState({ copied: false }); + } + clipboardCopyFunc(event, stringToCopy); + this.setState({ copied: true }, () => { + this.timer = window.setTimeout(() => { + this.setState({ copied: false }); + this.timer = null; + }, switchDelay); + }); + }; + + render() { + const { clickTip, entryDelay, exitDelay, hoverTip } = this.props; + const { copied } = this.state; + + return ( + + + + + + ); + } +} + +ClipboardCopyButton.propTypes = { + clickTip: PropTypes.string.isRequired, + entryDelay: PropTypes.number, + exitDelay: PropTypes.number, + hoverTip: PropTypes.string.isRequired, + stringToCopy: PropTypes.string.isRequired, + switchDelay: PropTypes.number, +}; + +ClipboardCopyButton.defaultProps = { + entryDelay: 100, + exitDelay: 1600, + switchDelay: 2000, +}; + +export default ClipboardCopyButton; diff --git a/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx new file mode 100644 index 0000000000..0cdbab8302 --- /dev/null +++ b/awx/ui_next/src/components/ClipboardCopyButton/ClipboardCopyButton.test.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { mountWithContexts } from '@testUtils/enzymeHelpers'; +import ClipboardCopyButton from './ClipboardCopyButton'; + +document.execCommand = jest.fn(); + +jest.useFakeTimers(); + +describe('ClipboardCopyButton', () => { + test('renders the expected content', () => { + const wrapper = mountWithContexts( + + ); + expect(wrapper).toHaveLength(1); + }); + test('clicking button calls execCommand to copy to clipboard', () => { + const wrapper = mountWithContexts( + + ).find('ClipboardCopyButton'); + expect(wrapper.state('copied')).toBe(false); + wrapper.find('Button').simulate('click'); + expect(document.execCommand).toBeCalledWith('copy'); + expect(wrapper.state('copied')).toBe(true); + jest.runAllTimers(); + wrapper.update(); + expect(wrapper.state('copied')).toBe(false); + }); +}); diff --git a/awx/ui_next/src/components/ClipboardCopyButton/index.js b/awx/ui_next/src/components/ClipboardCopyButton/index.js new file mode 100644 index 0000000000..45adfe436e --- /dev/null +++ b/awx/ui_next/src/components/ClipboardCopyButton/index.js @@ -0,0 +1 @@ +export { default } from './ClipboardCopyButton'; diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx index e44970edf0..f9a0f55327 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectList.test.jsx @@ -13,6 +13,7 @@ const mockProjects = [ url: '/api/v2/projects/1', type: 'project', scm_type: 'git', + scm_revision: 'hfadsh89sa9gsaisdf0jogos0fgd9sgdf89adsf98', summary_fields: { last_job: { id: 9000, @@ -30,6 +31,7 @@ const mockProjects = [ url: '/api/v2/projects/2', type: 'project', scm_type: 'svn', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', summary_fields: { last_job: { id: 9002, @@ -47,6 +49,7 @@ const mockProjects = [ url: '/api/v2/projects/3', type: 'project', scm_type: 'insights', + scm_revision: '4893adfi749493afjksjoaiosdgjoaisdjadfisjaso', summary_fields: { last_job: { id: 9003, diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index ed81638f45..5eff31dd21 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -12,6 +12,7 @@ import { Link as _Link } from 'react-router-dom'; import { SyncIcon } from '@patternfly/react-icons'; import styled from 'styled-components'; +import ClipboardCopyButton from '@components/ClipboardCopyButton'; import DataListCell from '@components/DataListCell'; import DataListCheck from '@components/DataListCheck'; import ListActionButton from '@components/ListActionButton'; @@ -102,6 +103,14 @@ class ProjectListItem extends React.Component { {project.scm_type.toUpperCase()} , + + {project.scm_revision.substring(0, 7)} + + , {project.summary_fields.user_capabilities.start && ( diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx index 1573efac80..bb5a7bcc4b 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx @@ -17,6 +17,7 @@ describe('', () => { url: '/api/v2/projects/1', type: 'project', scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', summary_fields: { last_job: { id: 9000, @@ -43,6 +44,7 @@ describe('', () => { url: '/api/v2/projects/1', type: 'project', scm_type: 'git', + scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf', summary_fields: { last_job: { id: 9000,