Unverified Commit 246ac93c authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1480 from hypothesis/copy-version-details

Add Copy-version-details to help panel; tighten design/layout
parents e7fbacf5 95587ba9
......@@ -97,41 +97,49 @@ function HelpPanel({ auth, session }) {
return (
<SidebarPanel
title="Need some help?"
title="Help"
panelName={uiConstants.PANEL_HELP}
onActiveChanged={onActiveChanged}
>
<div className="u-layout-row">
<h3 className="help-panel__sub-panel-title">
{subPanelTitles[activeSubPanel]}
</h3>
<div className="help-panel__content">
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} />
)}
<div className="help-panel__footer">
{activeSubPanel === 'versionInfo' && (
<a
href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--left"
className="help-panel__sub-panel-link"
onClick={e => openSubPanel(e, 'tutorial')}
>
<SvgIcon name="arrow-left" className="help-panel__icon" />
<div>Getting started</div>
<SvgIcon
name="arrow-right"
className="help-panel__sub-panel-link-icon"
/>
</a>
)}
{activeSubPanel === 'tutorial' && (
<a
href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--right"
className="help-panel__sub-panel-link"
onClick={e => openSubPanel(e, 'versionInfo')}
>
<div>About this version</div>
<SvgIcon name="arrow-right" className="help-panel__icon" />
<SvgIcon
name="arrow-right"
className="help-panel__sub-panel-link-icon"
/>
</a>
)}
</div>
</div>
<div className="help-panel__content">
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} />
)}
</div>
<div className="help-panel-tabs">
<HelpPanelTab
linkText="Help topics"
......
......@@ -7,12 +7,30 @@ const VersionInfo = require('../version-info');
describe('VersionInfo', function() {
let fakeVersionData;
// Services
let fakeFlash;
// Mocked dependencies
let fakeCopyToClipboard;
function createComponent(props) {
return mount(<VersionInfo versionData={fakeVersionData} {...props} />);
// Services
fakeFlash = {
info: sinon.stub(),
error: sinon.stub(),
};
return mount(
<VersionInfo flash={fakeFlash} versionData={fakeVersionData} {...props} />
);
}
beforeEach(() => {
fakeCopyToClipboard = {
copyText: sinon.stub(),
};
VersionInfo.$imports.$mock({
'../util/copy-to-clipboard': fakeCopyToClipboard,
});
fakeVersionData = {
version: 'fakeVersion',
userAgent: 'fakeUserAgent',
......@@ -21,6 +39,7 @@ describe('VersionInfo', function() {
account: 'fakeAccount',
timestamp: 'fakeTimestamp',
};
fakeVersionData.asFormattedString = sinon.stub().returns('fakeString');
});
it('renders `versionData` information', () => {
......@@ -33,4 +52,30 @@ describe('VersionInfo', function() {
assert.include(componentText, 'fakeAccount');
assert.include(componentText, 'fakeTimestamp');
});
describe('copy version info to clipboard', () => {
it('copies version info to clipboard when copy button clicked', () => {
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledWith(fakeCopyToClipboard.copyText, 'fakeString');
});
it('confirms info copy when successful', () => {
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledWith(fakeFlash.info, 'Copied version info to clipboard');
});
it('flashes an error if info copying unsuccessful', () => {
fakeCopyToClipboard.copyText.throws();
const wrapper = createComponent();
wrapper.find('button').simulate('click');
assert.calledWith(fakeFlash.error, 'Unable to copy version info');
});
});
});
......@@ -3,11 +3,26 @@
const propTypes = require('prop-types');
const { createElement } = require('preact');
const { copyText } = require('../util/copy-to-clipboard');
const { withServices } = require('../util/service-context');
const SvgIcon = require('./svg-icon');
/**
* Display current client version info
*/
function VersionInfo({ versionData }) {
function VersionInfo({ flash, versionData }) {
const copyVersionData = () => {
try {
copyText(versionData.asFormattedString());
flash.info('Copied version info to clipboard');
} catch (err) {
flash.error('Unable to copy version info');
}
};
return (
<div>
<dl className="version-info">
<dt className="version-info__key">Version</dt>
<dd className="version-info__value">{versionData.version}</dd>
......@@ -22,6 +37,13 @@ function VersionInfo({ versionData }) {
<dt className="version-info__key">Date</dt>
<dd className="version-info__value">{versionData.timestamp}</dd>
</dl>
<div className="version-info__actions">
<button className="version-info__copy-btn" onClick={copyVersionData}>
<SvgIcon name="copy" className="version-info__copy-btn-icon" />
Copy version details
</button>
</div>
</div>
);
}
......@@ -32,6 +54,11 @@ VersionInfo.propTypes = {
* @type {import('../util/version-info').VersionData}
*/
versionData: propTypes.object.isRequired,
/** injected properties */
flash: propTypes.object.isRequired,
};
module.exports = VersionInfo;
VersionInfo.injectedProps = ['flash'];
module.exports = withServices(VersionInfo);
......@@ -11,7 +11,7 @@
* to copy text.
*/
function copyText(text) {
const temp = document.createElement('span');
const temp = document.createElement('pre');
temp.className = 'copy-text';
temp.textContent = text;
document.body.appendChild(temp);
......
......@@ -80,6 +80,17 @@ describe('VersionData', () => {
});
});
describe('#asFormattedString', () => {
['timestamp', 'account', 'url'].forEach(prop => {
it(`includes a line for the value of ${prop} in the string`, () => {
const versionData = new VersionData({}, {});
const formatted = versionData.asFormattedString();
const subStr = `${prop}: ${versionData[prop]}\r\n`;
assert.include(formatted, subStr);
});
});
});
describe('#asEncodedURLString', () => {
['timestamp', 'account'].forEach(prop => {
it(`includes encoded value for ${prop} in URL string`, () => {
......
......@@ -55,19 +55,29 @@ class VersionData {
}
/**
* Return a single, encoded URL string of version data suitable for use in
* a querystring (as the value of a single parameter)
* Return a single formatted string representing version data, suitable for
* copying to the clipboard.
*
* @return {string} - URI-encoded string
* @return {string} - Single, multiline string representing current version data
*/
asEncodedURLString() {
asFormattedString() {
let versionString = '';
for (let prop in this) {
if (Object.prototype.hasOwnProperty.call(this, prop)) {
versionString += `${prop}: ${this[prop]}\r\n`;
}
}
return encodeURIComponent(versionString);
return versionString;
}
/**
* Return a single, encoded URL string of version data suitable for use in
* a querystring (as the value of a single parameter)
*
* @return {string} - URI-encoded string
*/
asEncodedURLString() {
return encodeURIComponent(this.asFormattedString());
}
}
......
.help-panel {
&__sub-panel-title {
margin: 0;
padding: 0.5em;
text-align: center;
padding: 0.5em 0;
font-size: 1.25em;
font-weight: 600;
font-weight: 500;
}
&__content {
......@@ -22,6 +21,7 @@
&__footer {
padding: 0.5em 0;
margin-left: auto;
display: flex;
align-items: center;
}
......@@ -29,14 +29,11 @@
&__sub-panel-link {
display: flex;
align-items: center;
color: $brand;
&--right {
margin-left: auto;
}
color: $brand;
&-icon {
margin: 5px;
margin-left: 2px;
width: 12px;
height: 12px;
}
......
.tutorial {
&__list {
margin-top: 1em;
margin-top: 0em;
padding-left: 2em;
}
......
.version-info {
margin-bottom: 0;
margin-top: 0.5em;
&__key {
float: left;
......@@ -18,4 +18,33 @@
overflow-wrap: break-word; // Keep really long userids from overflowing
color: $grey-6;
}
&__actions {
display: flex;
justify-content: center;
padding-bottom: 0.5em;
}
&__copy-btn {
@include outline-on-keyboard-focus;
display: flex;
align-items: center;
border-radius: 2px;
border: none;
padding: 0.5em;
background-color: $grey-1;
color: $grey-5;
font-weight: 700;
&-icon {
color: $grey-5;
margin: 0 5px;
}
&:hover {
transition: 0.2s ease-out;
background-color: $grey-2;
color: $grey-6;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment