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 }) { ...@@ -97,41 +97,49 @@ function HelpPanel({ auth, session }) {
return ( return (
<SidebarPanel <SidebarPanel
title="Need some help?" title="Help"
panelName={uiConstants.PANEL_HELP} panelName={uiConstants.PANEL_HELP}
onActiveChanged={onActiveChanged} onActiveChanged={onActiveChanged}
> >
<h3 className="help-panel__sub-panel-title"> <div className="u-layout-row">
{subPanelTitles[activeSubPanel]} <h3 className="help-panel__sub-panel-title">
</h3> {subPanelTitles[activeSubPanel]}
<div className="help-panel__content"> </h3>
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} />
)}
<div className="help-panel__footer"> <div className="help-panel__footer">
{activeSubPanel === 'versionInfo' && ( {activeSubPanel === 'versionInfo' && (
<a <a
href="#" href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--left" className="help-panel__sub-panel-link"
onClick={e => openSubPanel(e, 'tutorial')} onClick={e => openSubPanel(e, 'tutorial')}
> >
<SvgIcon name="arrow-left" className="help-panel__icon" />
<div>Getting started</div> <div>Getting started</div>
<SvgIcon
name="arrow-right"
className="help-panel__sub-panel-link-icon"
/>
</a> </a>
)} )}
{activeSubPanel === 'tutorial' && ( {activeSubPanel === 'tutorial' && (
<a <a
href="#" href="#"
className="help-panel__sub-panel-link help-panel__sub-panel-link--right" className="help-panel__sub-panel-link"
onClick={e => openSubPanel(e, 'versionInfo')} onClick={e => openSubPanel(e, 'versionInfo')}
> >
<div>About this version</div> <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> </a>
)} )}
</div> </div>
</div> </div>
<div className="help-panel__content">
{activeSubPanel === 'tutorial' && <Tutorial />}
{activeSubPanel === 'versionInfo' && (
<VersionInfo versionData={versionData} />
)}
</div>
<div className="help-panel-tabs"> <div className="help-panel-tabs">
<HelpPanelTab <HelpPanelTab
linkText="Help topics" linkText="Help topics"
......
...@@ -7,12 +7,30 @@ const VersionInfo = require('../version-info'); ...@@ -7,12 +7,30 @@ const VersionInfo = require('../version-info');
describe('VersionInfo', function() { describe('VersionInfo', function() {
let fakeVersionData; let fakeVersionData;
// Services
let fakeFlash;
// Mocked dependencies
let fakeCopyToClipboard;
function createComponent(props) { 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(() => { beforeEach(() => {
fakeCopyToClipboard = {
copyText: sinon.stub(),
};
VersionInfo.$imports.$mock({
'../util/copy-to-clipboard': fakeCopyToClipboard,
});
fakeVersionData = { fakeVersionData = {
version: 'fakeVersion', version: 'fakeVersion',
userAgent: 'fakeUserAgent', userAgent: 'fakeUserAgent',
...@@ -21,6 +39,7 @@ describe('VersionInfo', function() { ...@@ -21,6 +39,7 @@ describe('VersionInfo', function() {
account: 'fakeAccount', account: 'fakeAccount',
timestamp: 'fakeTimestamp', timestamp: 'fakeTimestamp',
}; };
fakeVersionData.asFormattedString = sinon.stub().returns('fakeString');
}); });
it('renders `versionData` information', () => { it('renders `versionData` information', () => {
...@@ -33,4 +52,30 @@ describe('VersionInfo', function() { ...@@ -33,4 +52,30 @@ describe('VersionInfo', function() {
assert.include(componentText, 'fakeAccount'); assert.include(componentText, 'fakeAccount');
assert.include(componentText, 'fakeTimestamp'); 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,25 +3,47 @@ ...@@ -3,25 +3,47 @@
const propTypes = require('prop-types'); const propTypes = require('prop-types');
const { createElement } = require('preact'); 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 * 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 ( return (
<dl className="version-info"> <div>
<dt className="version-info__key">Version</dt> <dl className="version-info">
<dd className="version-info__value">{versionData.version}</dd> <dt className="version-info__key">Version</dt>
<dt className="version-info__key">User Agent</dt> <dd className="version-info__value">{versionData.version}</dd>
<dd className="version-info__value">{versionData.userAgent}</dd> <dt className="version-info__key">User Agent</dt>
<dt className="version-info__key">URL</dt> <dd className="version-info__value">{versionData.userAgent}</dd>
<dd className="version-info__value">{versionData.url}</dd> <dt className="version-info__key">URL</dt>
<dt className="version-info__key">Fingerprint</dt> <dd className="version-info__value">{versionData.url}</dd>
<dd className="version-info__value">{versionData.fingerprint}</dd> <dt className="version-info__key">Fingerprint</dt>
<dt className="version-info__key">Account</dt> <dd className="version-info__value">{versionData.fingerprint}</dd>
<dd className="version-info__value">{versionData.account}</dd> <dt className="version-info__key">Account</dt>
<dt className="version-info__key">Date</dt> <dd className="version-info__value">{versionData.account}</dd>
<dd className="version-info__value">{versionData.timestamp}</dd> <dt className="version-info__key">Date</dt>
</dl> <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 = { ...@@ -32,6 +54,11 @@ VersionInfo.propTypes = {
* @type {import('../util/version-info').VersionData} * @type {import('../util/version-info').VersionData}
*/ */
versionData: propTypes.object.isRequired, versionData: propTypes.object.isRequired,
/** injected properties */
flash: propTypes.object.isRequired,
}; };
module.exports = VersionInfo; VersionInfo.injectedProps = ['flash'];
module.exports = withServices(VersionInfo);
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
* to copy text. * to copy text.
*/ */
function copyText(text) { function copyText(text) {
const temp = document.createElement('span'); const temp = document.createElement('pre');
temp.className = 'copy-text'; temp.className = 'copy-text';
temp.textContent = text; temp.textContent = text;
document.body.appendChild(temp); document.body.appendChild(temp);
......
...@@ -80,6 +80,17 @@ describe('VersionData', () => { ...@@ -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', () => { describe('#asEncodedURLString', () => {
['timestamp', 'account'].forEach(prop => { ['timestamp', 'account'].forEach(prop => {
it(`includes encoded value for ${prop} in URL string`, () => { it(`includes encoded value for ${prop} in URL string`, () => {
......
...@@ -55,19 +55,29 @@ class VersionData { ...@@ -55,19 +55,29 @@ class VersionData {
} }
/** /**
* Return a single, encoded URL string of version data suitable for use in * Return a single formatted string representing version data, suitable for
* a querystring (as the value of a single parameter) * copying to the clipboard.
* *
* @return {string} - URI-encoded string * @return {string} - Single, multiline string representing current version data
*/ */
asEncodedURLString() { asFormattedString() {
let versionString = ''; let versionString = '';
for (let prop in this) { for (let prop in this) {
if (Object.prototype.hasOwnProperty.call(this, prop)) { if (Object.prototype.hasOwnProperty.call(this, prop)) {
versionString += `${prop}: ${this[prop]}\r\n`; 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 { .help-panel {
&__sub-panel-title { &__sub-panel-title {
margin: 0; margin: 0;
padding: 0.5em; padding: 0.5em 0;
text-align: center;
font-size: 1.25em; font-size: 1.25em;
font-weight: 600; font-weight: 500;
} }
&__content { &__content {
...@@ -22,6 +21,7 @@ ...@@ -22,6 +21,7 @@
&__footer { &__footer {
padding: 0.5em 0; padding: 0.5em 0;
margin-left: auto;
display: flex; display: flex;
align-items: center; align-items: center;
} }
...@@ -29,14 +29,11 @@ ...@@ -29,14 +29,11 @@
&__sub-panel-link { &__sub-panel-link {
display: flex; display: flex;
align-items: center; align-items: center;
margin-left: auto;
color: $brand; color: $brand;
&--right {
margin-left: auto;
}
&-icon { &-icon {
margin: 5px; margin-left: 2px;
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
......
.tutorial { .tutorial {
&__list { &__list {
margin-top: 1em; margin-top: 0em;
padding-left: 2em; padding-left: 2em;
} }
......
.version-info { .version-info {
margin-bottom: 0; margin-top: 0.5em;
&__key { &__key {
float: left; float: left;
...@@ -18,4 +18,33 @@ ...@@ -18,4 +18,33 @@
overflow-wrap: break-word; // Keep really long userids from overflowing overflow-wrap: break-word; // Keep really long userids from overflowing
color: $grey-6; 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