You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ro...@apache.org on 2016/12/23 16:39:47 UTC

fauxton commit: updated refs/heads/master to 2e770d6

Repository: couchdb-fauxton
Updated Branches:
  refs/heads/master 2a3dccaf4 -> 2e770d66b


rip and replace zeroclipboard with clipboardjs

PR: #825
PR-URL: https://github.com/apache/couchdb-fauxton/pull/825
Reviewed-By: Robert Kowalski <ro...@kowalski.gd>


Project: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/commit/2e770d66
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/tree/2e770d66
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/diff/2e770d66

Branch: refs/heads/master
Commit: 2e770d66b2b849a60468ad390dfed57da54561a9
Parents: 2a3dcca
Author: Ryan Millay <ry...@gmail.com>
Authored: Mon Dec 19 15:38:11 2016 -0500
Committer: Robert Kowalski <ro...@apache.org>
Committed: Fri Dec 23 17:39:30 2016 +0100

----------------------------------------------------------------------
 .../activetasks/assets/less/activetasks.less    |  45 ------
 app/addons/activetasks/components.react.jsx     |  62 +--------
 app/addons/components/__tests__/copy.test.js    |  39 ++++++
 app/addons/components/components/apibar.js      |  15 +-
 app/addons/components/components/copy.js        | 100 ++++++++++++++
 .../components/react-components.react.jsx       |   4 +-
 .../tests/nightwatch/copyToClipboard.js         |  92 +++++++++++++
 .../tests/nightwatch/deletesDatabase.js         |   2 +
 .../nightwatch/deletesDatabaseSpecialChars.js   |   3 +-
 .../documents/changes/components.react.jsx      |  15 +-
 .../designdocinfo/components.react.jsx          |  11 +-
 .../index-results.components.react.jsx          |  12 +-
 app/addons/fauxton/assets/less/components.less  |   9 +-
 app/addons/fauxton/components.react.jsx         | 136 +------------------
 .../notifications/notifications.react.jsx       |   8 +-
 .../fauxton/tests/componentsSpec.react.jsx      |  19 ---
 assets/less/notification-center.less            |   5 +-
 jest-setup.js                                   |   1 -
 package.json                                    |   5 +-
 19 files changed, 282 insertions(+), 301 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/activetasks/assets/less/activetasks.less
----------------------------------------------------------------------
diff --git a/app/addons/activetasks/assets/less/activetasks.less b/app/addons/activetasks/assets/less/activetasks.less
index cad281c..94444c9 100644
--- a/app/addons/activetasks/assets/less/activetasks.less
+++ b/app/addons/activetasks/assets/less/activetasks.less
@@ -141,51 +141,6 @@
       color: @brandPrimaryDark;
     }
   }
-
-  .view-source-sequence-btn { // "View" Button
-    background-color: #999;
-    display: inline;
-    border-radius: 3px;
-    padding: 2px;
-    margin: 3px;
-    color: #fff !important;
-    white-space: nowrap;
-  }
-
-  .view-source-sequence-tray {
-    padding: 16px 20px 28px;
-
-    position: relative;
-    min-width: 365px;
-    top: 15px;
-    float: right;
-
-    &:before {
-      right: 110px;
-    }
-    input.input-xxlarge {
-      margin-bottom: 0px;
-      width: 250px;
-      .border-radius(5px 0 0 5px);
-    }
-
-    a.btn {
-      color: white;
-      background-color: @linkColor;
-      margin-left: 0;
-      line-height: 1.5em;
-      border: 0px;
-      padding: 10px 10px 9px;
-      font-size: 14px;
-      .border-radius(0 5px 5px 0);
-
-
-      &:hover, &.copy-button.zeroclipboard-is-hover {
-        background-color: #cbcbcb;
-        color: white;
-      }
-    }
-  }
 }
 
 .active-tasks__polling-wrapper {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/activetasks/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/activetasks/components.react.jsx b/app/addons/activetasks/components.react.jsx
index f739823..33c6c4c 100644
--- a/app/addons/activetasks/components.react.jsx
+++ b/app/addons/activetasks/components.react.jsx
@@ -365,71 +365,12 @@ var ActiveTaskTableBodyContents = React.createClass({
         <td>{startedOnMsg}</td>
         <td>{updatedOnMsg}</td>
         <td>{rowData.pid}</td>
-        <td>{progressMsg}<ActiveTasksViewSourceSequence item={this.props.item}/></td>
+        <td>{progressMsg}</td>
       </tr>
     );
   }
 });
 
-var ActiveTasksViewSourceSequence = React.createClass({
-  getInitialState () {
-    return {
-      contentVisible: false
-    };
-  },
-
-  toggleTray (e) {
-    e.preventDefault();
-    this.setState({contentVisible: !this.state.contentVisible});
-  },
-
-  closeTray () {
-    this.setState({contentVisible: false});
-  },
-
-  sequences (item) {
-    if (_.isNumber(item) || _.isString(item)) {
-      return <ComponentsReact.ClipboardWithTextField onClipBoardClick={() => {}} textToCopy={item.toString()} uniqueKey={item.toString()}/>;
-    }
-
-    if (_.isArray(item)) {
-      return _.map(item, function (seq, i) {
-          return <ComponentsReact.ClipboardWithTextField onClipBoardClick={() => {}} textToCopy={seq.toString()} uniqueKey={`${i + Math.random(100)}`} key={i}/>;
-        });
-    }
-
-    return  <ComponentsReact.ClipboardWithTextField textToCopy="???" onClipBoardClick={() => {}} uniqueKey='unknownRevision'/>;
-  },
-
-  render () {
-    if (!_.has(this.props.item, 'source_seq')) {
-      return null;
-    }
-
-    const sequences = this.sequences(this.props.item.source_seq);
-    return (
-      <div>
-        Current source sequence:
-        <a href="#"
-          className="view-source-sequence-btn"
-          onClick={this.toggleTray}
-          data-bypass="true">
-          View
-        </a>
-        <TrayContents
-          ref="view_source_sequence_btn"
-          contentVisible={this.state.contentVisible}
-          closeTray={this.closeTray}
-          container={this}
-          className="view-source-sequence-tray">
-          <span className="add-on">Source Sequence</span>
-          {sequences}
-        </TrayContents>
-      </div>
-    );
-  }
-});
-
 export const ActiveTasksPollingWidgetController = React.createClass({
 
   getStoreState () {
@@ -542,7 +483,6 @@ export default {
   TableHeader: TableHeader,
   ActiveTasksTableBody: ActiveTasksTableBody,
   ActiveTaskTableBodyContents: ActiveTaskTableBodyContents,
-  ActiveTasksViewSourceSequence: ActiveTasksViewSourceSequence,
 
   ActiveTasksPollingWidgetController: ActiveTasksPollingWidgetController
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/components/__tests__/copy.test.js
----------------------------------------------------------------------
diff --git a/app/addons/components/__tests__/copy.test.js b/app/addons/components/__tests__/copy.test.js
new file mode 100644
index 0000000..64216c7
--- /dev/null
+++ b/app/addons/components/__tests__/copy.test.js
@@ -0,0 +1,39 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+import { Copy } from "../components/copy";
+import { mount } from "enzyme";
+import React from "react";
+import ReactDOM from "react-dom";
+import uuid from 'uuid';
+
+describe('Copy', () => {
+
+  it('shows a copy icon by default', () => {
+    const wrapper = mount(<Copy uniqueKey={uuid.v4()} text="copy me"/>);
+    expect(wrapper.find('.icon-paste').length).toBe(1);
+  });
+
+  it('shows text if specified', () => {
+    const wrapper = mount(<Copy uniqueKey={uuid.v4()} text="copy me" displayType="text" />);
+    expect(wrapper.find('.icon-paste').length).toBe(0);
+  });
+
+  it('shows custom text if specified', () => {
+    const wrapper = mount(<Copy uniqueKey={uuid.v4()} displayType="text" textDisplay="booyah!" text="copy me" />);
+    expect(wrapper.html()).toMatch(/booyah!/);
+  });
+
+  it('shows an input field and button if specified', () => {
+    const wrapper = mount(<Copy uniqueKey={uuid.v4()} displayType='input' text="http://localhost:8000/_all_dbs" />);
+    expect(wrapper.find('input').length).toBe(1);
+  });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/components/components/apibar.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/apibar.js b/app/addons/components/components/apibar.js
index 6718604..e51e418 100644
--- a/app/addons/components/components/apibar.js
+++ b/app/addons/components/components/apibar.js
@@ -14,11 +14,12 @@ import React from "react";
 import ReactDOM from "react-dom";
 import FauxtonAPI from "../../../core/api";
 import {TrayContents, TrayWrapper, connectToStores} from './tray';
-import FauxtonComponents from "../../fauxton/components.react";
+import { Copy } from "./copy";
 import Actions from "../actions";
 import Stores from "../stores";
 import {ToggleHeaderButton} from './toggleheaderbutton';
 const { componentStore } = Stores;
+import uuid from 'uuid';
 
 export const APIBar = React.createClass({
   propTypes: {
@@ -62,12 +63,12 @@ export const APIBar = React.createClass({
             {this.getDocIcon()}
           </span>
 
-          <FauxtonComponents.ClipboardWithTextField
-            onClipBoardClick={this.showCopiedMessage}
-            text="Copy URL"
-            textToCopy={endpoint}
-            showCopyIcon={false}
-            uniqueKey="clipboard-apiurl" />
+          <Copy
+            textDisplay="Copy URL"
+            text={endpoint}
+            displayType="input"
+            uniqueKey={uuid.v4()}
+            onClipboardClick={this.showCopiedMessage} />
 
           <div className="add-on">
             <a

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/components/components/copy.js
----------------------------------------------------------------------
diff --git a/app/addons/components/components/copy.js b/app/addons/components/components/copy.js
new file mode 100644
index 0000000..2c00793
--- /dev/null
+++ b/app/addons/components/components/copy.js
@@ -0,0 +1,100 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Clipboard from 'clipboard';
+
+let clipboard;
+
+// Locates the specific element on the DOM, configures the clipboard, and
+// sets the callback on 'success' (usually a Fauxton notification).
+export const initializeClipboard = (uniqueKey, cb) => {
+  clipboard = new Clipboard('#copy-' + uniqueKey);
+  clipboard.on('success', function(e) {
+    cb();
+  });
+};
+
+// Cleans up the fake elements left around by clipboard.js
+export const destroyClipboard = () => {
+  clipboard.destroy();
+};
+
+export class Copy extends React.Component {
+  componentDidMount () {
+    initializeClipboard(this.props.uniqueKey, this.props.onClipboardClick);
+  }
+
+  componentWillUnmount () {
+    destroyClipboard();
+  }
+
+  // Necessary for copy elements that are not unmounted even though they are
+  // no longer visible (ex. Notification  rows in the notification center).
+  componentDidUpdate () {
+    initializeClipboard(this.props.uniqueKey, this.props.onClipboardClick);
+  }
+
+  getClipboardElement () {
+    if (this.props.displayType === 'icon') {
+      return (<i className="fontawesome icon-paste"></i>);
+    }
+    return this.props.textDisplay;
+  }
+
+  getClipboardButton () {
+    const btnClasses = this.props.displayType === 'input' ? "btn copy-button" : "copy" + " clipboard-copy-element";
+    return (
+      <button
+        className={btnClasses}
+        data-clipboard-text={this.props.text}
+        title={this.props.title}
+        id={"copy-" + this.props.uniqueKey}
+      >
+        {this.getClipboardElement()}
+      </button>
+    );
+  }
+
+  render () {
+    if (this.props.displayType === 'input') {
+      return (
+        <p>
+          <input
+            type="text"
+            className="input-xxlarge text-field-to-copy"
+            readOnly
+            value={this.props.text} />
+          {this.getClipboardButton()}
+        </p>
+      );
+    }
+    return (
+      this.getClipboardButton()
+    );
+  }
+};
+
+Copy.defaultProps = {
+  displayType: 'icon',
+  textDisplay: 'Copy',
+  title: 'Copy to clipboard',
+  onClipboardClick: function () { }
+};
+
+Copy.propTypes = {
+  text: React.PropTypes.string.isRequired,
+  displayType: React.PropTypes.oneOf(['icon', 'text', 'input']),
+  uniqueKey: React.PropTypes.string.isRequired,
+  onClipboardClick: React.PropTypes.func.isRequired
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/components/react-components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/components/react-components.react.jsx b/app/addons/components/react-components.react.jsx
index 6c8fdcd..c404871 100644
--- a/app/addons/components/react-components.react.jsx
+++ b/app/addons/components/react-components.react.jsx
@@ -29,6 +29,7 @@ import {ApiBarController} from './components/apibar';
 import {DeleteDatabaseModal} from './components/deletedatabasemodal';
 import {TabElement, TabElementWrapper} from './components/tabelement';
 import {Polling, RefreshBtn} from './components/polling';
+import {Copy} from './components/copy';
 
 export default {
   BadgeList,
@@ -54,5 +55,6 @@ export default {
   DeleteDatabaseModal,
   TabElement,
   TabElementWrapper,
-  RefreshBtn
+  RefreshBtn,
+  Copy
 };

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/components/tests/nightwatch/copyToClipboard.js
----------------------------------------------------------------------
diff --git a/app/addons/components/tests/nightwatch/copyToClipboard.js b/app/addons/components/tests/nightwatch/copyToClipboard.js
new file mode 100644
index 0000000..1b9942a
--- /dev/null
+++ b/app/addons/components/tests/nightwatch/copyToClipboard.js
@@ -0,0 +1,92 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+
+module.exports = {
+
+  // Since we can't directly access the clipboard to verify, we'll confirm that
+  // the text to copy is correct and the successful callback is displayed.
+
+  'Copy API URL to Clipboard Test' : (client) => {
+
+    const waitTime = client.globals.maxWaitTime;
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#/_all_dbs')
+
+      .clickWhenVisible('.control-toggle-api-url', waitTime, false)
+      .waitForElementVisible('.text-field-to-copy', waitTime, false)
+      .assert.attributeEquals('.text-field-to-copy', 'value', baseUrl + '/_all_dbs')
+      .waitForElementVisible('.copy-button', waitTime, false)
+      .assert.attributeEquals('.copy-button', 'data-clipboard-text', baseUrl + '/_all_dbs')
+      .click('.copy-button')
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .assert.containsText('.global-notification > span', 'The API URL has been copied to the clipboard.')
+    .end();
+  },
+
+  'Copy MD5 Checksum to Clipboard Test' : (client) => {
+
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .populateDatabase(newDatabaseName)
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      .clickWhenVisible('.design-doc-name > span', waitTime, false)
+      .clickWhenVisible('a[href*="_info"]', waitTime, false)
+      .clickWhenVisible('li > button.clipboard-copy-element', waitTime, false)
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .assert.containsText('.global-notification > span', 'The MD5 sha has been copied to your clipboard.')
+    .end();
+  },
+
+  'Copy Changes Feed Data to Clipboard Test' : (client) => {
+
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_changes')
+
+      .clickWhenVisible('.change-wrapper:first-child .row-fluid:first-child .clipboard-copy-element', waitTime, false)
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .assert.containsText('.global-notification > span', 'The document seq number has been copied to your clipboard.')
+      .clickWhenVisible('.change-wrapper:first-child .row-fluid:nth-child(2) .clipboard-copy-element', waitTime, false)
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .assert.containsText('.global-notification > span', 'The document ID has been copied to your clipboard.')
+    .end();
+  },
+
+  'Cppy Document from Table View to Clipboard Test' : (client) => {
+    const waitTime = client.globals.maxWaitTime;
+    const newDatabaseName = client.globals.testDatabaseName;
+    const baseUrl = client.globals.test_settings.launch_url;
+
+    client
+      .loginToGUI()
+      .url(baseUrl + '/#/database/' + newDatabaseName + '/_all_docs')
+
+      .clickWhenVisible('.fonticon-table', waitTime, false)
+      .clickWhenVisible('.table-view-docs tr:first-child .clipboard-copy-element', waitTime, false)
+      .waitForElementVisible('.global-notification', waitTime, false)
+      .assert.containsText('.global-notification > span', 'The document content has been copied to the clipboard.')
+    .end();
+  }
+};

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/databases/tests/nightwatch/deletesDatabase.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/nightwatch/deletesDatabase.js b/app/addons/databases/tests/nightwatch/deletesDatabase.js
index c015a6f..9e2212c 100644
--- a/app/addons/databases/tests/nightwatch/deletesDatabase.js
+++ b/app/addons/databases/tests/nightwatch/deletesDatabase.js
@@ -45,6 +45,8 @@ module.exports = {
       .waitForElementPresent('a[href="database/' + newDatabaseName + '/_all_docs"]', waitTime, false)
       .assert.elementPresent('a[href="database/' + newDatabaseName + '/_all_docs"]')
       .clickWhenVisible('[title="Delete ' + newDatabaseName + '"]', waitTime, false)
+      .waitForElementVisible('.delete-db-modal', waitTime, false)
+      .clickWhenVisible('.delete-db-modal input[type="text"]', waitTime, false)
       .setValue('.delete-db-modal input[type="text"]', [newDatabaseName, client.Keys.ENTER])
       .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
       .waitForElementPresent('.fauxton-table-list', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
----------------------------------------------------------------------
diff --git a/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js b/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
index a0769d6..71b253a 100644
--- a/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
+++ b/app/addons/databases/tests/nightwatch/deletesDatabaseSpecialChars.js
@@ -44,7 +44,8 @@ module.exports = {
       .waitForElementPresent('a[href="database/' + encodeURIComponent(newDatabaseName) + '/_all_docs"]', waitTime, false)
       .assert.elementPresent('a[href="database/' + encodeURIComponent(newDatabaseName) + '/_all_docs"]')
       .clickWhenVisible('[title="Delete ' + newDatabaseName + '"]', waitTime, false)
-      .waitForElementPresent('.delete-db-modal input[type="text"]', waitTime, false)
+      .waitForElementPresent('.delete-db-modal', waitTime, false)
+      .clickWhenVisible('.delete-db-modal input[type="text"]', waitTime, false)
       .setValue('.delete-db-modal input[type="text"]', [newDatabaseName, client.Keys.ENTER])
       .waitForElementNotPresent('.global-notification .fonticon-cancel', waitTime, false)
       .waitForElementPresent('.fauxton-table-list', waitTime, false)

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/documents/changes/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/changes/components.react.jsx b/app/addons/documents/changes/components.react.jsx
index fb01a36..1b9aefd 100644
--- a/app/addons/documents/changes/components.react.jsx
+++ b/app/addons/documents/changes/components.react.jsx
@@ -20,10 +20,11 @@ import Components from "../../fauxton/components.react";
 import ReactComponents from "../../components/react-components.react";
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 import "../../../../assets/js/plugins/prettify";
+import uuid from 'uuid';
 
 const store = Stores.changesStore;
 const BadgeList = ReactComponents.BadgeList;
-
+const {Copy} = ReactComponents;
 
 class ChangesController extends React.Component {
   constructor (props) {
@@ -255,7 +256,7 @@ class ChangeRow extends React.Component {
     };
   }
 
-  onClipboardClick (target) {
+  showCopiedMessage (target) {
     let msg = 'The document ID has been copied to your clipboard.';
     if (target === 'seq') {
       msg = 'The document seq number has been copied to your clipboard.';
@@ -280,7 +281,10 @@ class ChangeRow extends React.Component {
             <div className="span2">seq</div>
             <div className="span8 change-sequence">{change.seq}</div>
             <div className="span2 text-right">
-              <Components.Clipboard text={change.seq} onClipboardClick={() => this.onClipboardClick('seq')} />
+              <Copy
+                uniqueKey={uuid.v4()}
+                text={change.seq.toString()}
+                onClipboardClick={() => this.showCopiedMessage('seq')} />
             </div>
           </div>
 
@@ -290,7 +294,10 @@ class ChangeRow extends React.Component {
               <ChangeID id={change.id} deleted={change.deleted} databaseName={databaseName} />
             </div>
             <div className="span2 text-right">
-              <Components.Clipboard text={change.id} onClipboardClick={() => this.onClipboardClick('id')} />
+              <Copy
+                uniqueKey={uuid.v4()}
+                text={change.id}
+                onClipboardClick={() => this.showCopiedMessage('id')} />
             </div>
           </div>
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/documents/designdocinfo/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/designdocinfo/components.react.jsx b/app/addons/documents/designdocinfo/components.react.jsx
index a6909e8..d5a7070 100644
--- a/app/addons/documents/designdocinfo/components.react.jsx
+++ b/app/addons/documents/designdocinfo/components.react.jsx
@@ -16,10 +16,10 @@ import React from "react";
 import Stores from "./stores";
 import Actions from "./actions";
 import ReactComponents from "../../components/react-components.react";
-import GeneralComponents from "../../fauxton/components.react";
 var designDocInfoStore = Stores.designDocInfoStore;
 var LoadLines = ReactComponents.LoadLines;
-var Clipboard = GeneralComponents.Clipboard;
+var Copy = ReactComponents.Copy;
+import uuid from 'uuid';
 
 
 var DesignDocInfo = React.createClass({
@@ -130,9 +130,10 @@ var DesignDocInfo = React.createClass({
           <ul>
             <li>
               <span className="item-title">MD5 Signature:</span>
-              <Clipboard
-                onClipboardClick={this.showCopiedMessage}
-                text={viewIndex.signature} />
+              <Copy
+                uniqueKey={uuid.v4()}
+                text={viewIndex.signature}
+                onClipboardClick={this.showCopiedMessage} />
             </li>
           </ul>
 

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/documents/index-results/index-results.components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/documents/index-results/index-results.components.react.jsx b/app/addons/documents/index-results/index-results.components.react.jsx
index b78ca1d..0e84ff3 100644
--- a/app/addons/documents/index-results/index-results.components.react.jsx
+++ b/app/addons/documents/index-results/index-results.components.react.jsx
@@ -17,13 +17,12 @@ import Stores from "./stores";
 import Actions from "./actions";
 import Components from "../../components/react-components.react";
 import Documents from "../resources";
-import FauxtonComponents from "../..//fauxton/components.react";
 import { SplitButton, MenuItem } from "react-bootstrap";
 import ReactSelect from "react-select";
 import "../../../../assets/js/plugins/prettify";
+import uuid from 'uuid';
 
-const {LoadLines, BulkActionComponent} = Components;
-const { Clipboard } = FauxtonComponents;
+const {LoadLines, BulkActionComponent, Copy} = Components;
 const store  = Stores.indexResultsStore;
 
 var NoResultsScreen = React.createClass({
@@ -159,10 +158,11 @@ var TableRow = React.createClass({
     var text = JSON.stringify(el, null, '  ');
     return (
       <td title={text} className="tableview-el-copy">
-        <Clipboard
-          onClipboardClick={this.showCopiedMessage}
+        <Copy
           title={text}
-          text={text} />
+          text={text}
+          uniqueKey={uuid.v4()}
+          onClipboardClick={this.showCopiedMessage} />
       </td>
     );
   },

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/fauxton/assets/less/components.less
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/assets/less/components.less b/app/addons/fauxton/assets/less/components.less
index 8e737d1..c124c9b 100644
--- a/app/addons/fauxton/assets/less/components.less
+++ b/app/addons/fauxton/assets/less/components.less
@@ -23,10 +23,6 @@
       }
     }
 
-    .zeroclipboard-is-hover {
-      background-color: #cbcbcb;
-    }
-
     .icon-question-sign {
       margin-left: 3px;
     }
@@ -44,6 +40,7 @@
 
 }
 
-a.clipboard-copy-element:hover {
-  text-decoration: none;
+button.clipboard-copy-element {
+  background: transparent;
+  border: 0;
 }

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/fauxton/components.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/components.react.jsx b/app/addons/fauxton/components.react.jsx
index 3ca7bfd..dc85481 100644
--- a/app/addons/fauxton/components.react.jsx
+++ b/app/addons/fauxton/components.react.jsx
@@ -14,142 +14,10 @@ import app from "../../app";
 import FauxtonAPI from "../../core/api";
 import React from "react";
 import ReactDOM from "react-dom";
-import ZeroClipboard from "zeroclipboard";
 import { Modal } from "react-bootstrap";
 import "velocity-animate/velocity";
 import "velocity-animate/velocity.ui";
-import "zeroclipboard/dist/ZeroClipboard.swf";
 
-function getZeroClipboardSwfPath () {
-  return './dashboard.assets/ZeroClipboard.swf';
-}
-
-// super basic right now, but can be expanded later to handle all the varieties of copy-to-clipboards
-// (target content element, custom label, classes, notifications, etc.)
-var Clipboard = React.createClass({
-  propTypes: function () {
-    return {
-      text: React.PropTypes.string.isRequired,
-      displayType: React.PropTypes.string.oneOf(['icon', 'text'])
-    };
-  },
-
-  getDefaultProps: function () {
-    return {
-      displayType: 'icon',
-      textDisplay: 'Copy',
-      onClipboardClick: function () { },
-      title: 'Copy to clipboard'
-    };
-  },
-
-  componentWillMount: function () {
-    ZeroClipboard.config({ swfPath: getZeroClipboardSwfPath() });
-  },
-
-  getClipboardElement: function () {
-    if (this.props.displayType === 'icon') {
-      return (<i className="fontawesome icon-paste"></i>);
-    }
-    return this.props.textDisplay;
-  },
-
-  componentDidMount: function () {
-    var el = ReactDOM.findDOMNode(this);
-      this.clipboard = new ZeroClipboard(el);
-      this.clipboard.on('ready', function () {
-        this.clipboard.on('copy', function () {
-          this.props.onClipboardClick();
-        }.bind(this));
-      }.bind(this));
-
-      this.clipboard.on('error', function (event) {
-        console.log('ZeroClipboard error of type "' + event.name + '": ' + event.message);
-      });
-  },
-
-  onClick: function (event) {
-    event.preventDefault();
-  },
-
-  render: function () {
-    return (
-      <a href="#"
-        onClick={this.onClick}
-        ref="copy"
-        className="copy clipboard-copy-element"
-        data-clipboard-text={this.props.text}
-        data-bypass="true"
-        title={this.props.title}
-      >
-        {this.getClipboardElement()}
-      </a>
-    );
-  }
-});
-
-// use like this:
-//  <ComponentsReact.ClipboardWithTextField textToCopy={yourText} uniqueKey={someUniqueValue}>
-//  </ComponentsReact.ClipboardWithTextField>
-// pass in the text and a unique key, the key has to be unique or you'll get a warning
-var ClipboardWithTextField = React.createClass({
-  propTypes: {
-    onClipBoardClick: React.PropTypes.func.isRequired,
-    textToCopy: React.PropTypes.string.isRequired,
-    uniqueKey: React.PropTypes.string.isRequired,
-    showCopyIcon: React.PropTypes.bool
-  },
-
-  getDefaultProps: function () {
-    return {
-      showCopyIcon: true,
-      text: 'Copy'
-    };
-  },
-
-  componentWillMount: function () {
-    ZeroClipboard.config({ swfPath: getZeroClipboardSwfPath() });
-  },
-
-  componentDidMount: function () {
-    var el = ReactDOM.findDOMNode(this.refs["copy-text-" + this.props.uniqueKey]);
-    this.clipboard = new ZeroClipboard(el);
-    this.clipboard.on('ready', function () {
-      this.clipboard.on('copy', function () {
-        this.props.onClipBoardClick();
-      }.bind(this));
-    }.bind(this));
-  },
-
-  getCopyIcon: function () {
-    if (!this.props.showCopyIcon) {
-      return null;
-    }
-    return (<i className="fontawesome icon-paste"></i>);
-  },
-
-  render: function () {
-    return (
-      <p key={this.props.uniqueKey}>
-        <input
-          type="text"
-          className="input-xxlarge text-field-to-copy"
-          readOnly
-          value={this.props.textToCopy} />
-        <a
-          id={"copy-text-" + this.props.uniqueKey}
-          className="btn copy-button clipboard-copy-element"
-          data-clipboard-text={this.props.textToCopy}
-          data-bypass="true"
-          ref={"copy-text-" + this.props.uniqueKey}
-          title="Copy to clipboard"
-        >
-          {this.getCopyIcon()} {this.props.text}
-        </a>
-      </p>
-    );
-  }
-});
 
 // formats a block of code and pretty-prints it in the page. Currently uses the prettyPrint plugin
 var CodeFormat = React.createClass({
@@ -181,7 +49,7 @@ var CodeFormat = React.createClass({
   },
 
   render: function () {
-    var code = JSON.stringify(this.props.code, null, " ");
+    const code = JSON.stringify(this.props.code, null, " ");
     return (
       <div><pre className={this.getClasses()}>{code}</pre></div>
     );
@@ -332,8 +200,6 @@ var ConfirmationModal = React.createClass({
 
 
 export default {
-  Clipboard: Clipboard,
-  ClipboardWithTextField: ClipboardWithTextField,
   CodeFormat: CodeFormat,
   Pagination: Pagination,
   ConfirmationModal: ConfirmationModal

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/fauxton/notifications/notifications.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/notifications/notifications.react.jsx b/app/addons/fauxton/notifications/notifications.react.jsx
index a7ab3ae..37f5848 100644
--- a/app/addons/fauxton/notifications/notifications.react.jsx
+++ b/app/addons/fauxton/notifications/notifications.react.jsx
@@ -16,15 +16,15 @@ import React from "react";
 import ReactDOM from "react-dom";
 import Actions from "./actions";
 import Stores from "./stores";
-import Components from "../components.react";
+import Components from "../../components/react-components.react";
 import VelocityReact from "velocity-react";
 import "velocity-animate/velocity";
 import "velocity-animate/velocity.ui";
+import uuid from 'uuid';
 
 var store = Stores.notificationStore;
-var Clipboard = Components.Clipboard;
 var VelocityComponent = VelocityReact.VelocityComponent;
-
+const {Copy} = Components;
 
 // The one-stop-shop for Fauxton notifications. This controller handler the header notifications and the rightmost
 // notification center panel
@@ -427,7 +427,7 @@ var NotificationPanelRow = React.createClass({
             <div className="notification-actions">
               <span className="time-elapsed">{timeElapsed}</span>
               <span className="divider">|</span>
-              <Clipboard text={this.props.item.cleanMsg} displayType="text" />
+              <Copy uniqueKey={uuid.v4()} text={this.props.item.cleanMsg} displayType="text" />
             </div>
           </div>
           <button type="button" onClick={this.clearNotification}>�</button>

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/app/addons/fauxton/tests/componentsSpec.react.jsx
----------------------------------------------------------------------
diff --git a/app/addons/fauxton/tests/componentsSpec.react.jsx b/app/addons/fauxton/tests/componentsSpec.react.jsx
index 3c1e16b..c33555d 100644
--- a/app/addons/fauxton/tests/componentsSpec.react.jsx
+++ b/app/addons/fauxton/tests/componentsSpec.react.jsx
@@ -149,22 +149,3 @@ describe('Pagination', function () {
   });
 
 });
-
-
-describe('Clipboard', function () {
-
-  it('shows a clipboard icon by default', function () {
-    const clipboard = mount(<Views.Clipboard text="copy me" />);
-    assert.equal(clipboard.find('.icon-paste').length, 1);
-  });
-
-  it('shows text if specified', function () {
-    const clipboard = mount(<Views.Clipboard text="copy me" displayType="text" />);
-    assert.equal(clipboard.find('.icon-paste').length, 0);
-  });
-
-  it('shows custom text if specified ', function () {
-    var clipboard = mount(<Views.Clipboard displayType="text" textDisplay='booyah!' text="copy me" />);
-    assert.ok(/booyah!/.test(clipboard.html()));
-  });
-});

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/assets/less/notification-center.less
----------------------------------------------------------------------
diff --git a/assets/less/notification-center.less b/assets/less/notification-center.less
index af9939a..657c2bd 100644
--- a/assets/less/notification-center.less
+++ b/assets/less/notification-center.less
@@ -138,6 +138,7 @@ body #dashboard #notification-center-btn {
           height: 20px;
           margin-top: -4px;
           margin-left: 6px;
+          font-size: 100%;
           &:hover {
             color: @hoverRed;
           }
@@ -171,10 +172,6 @@ body #dashboard #notification-center-btn {
         color: @blue;
         text-decoration: none;
       }
-      .copy.zeroclipboard-is-hover {
-        color: @hoverRed;
-        text-decoration: underline;
-      }
     }
 
     footer {

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/jest-setup.js
----------------------------------------------------------------------
diff --git a/jest-setup.js b/jest-setup.js
index 35502e6..0eca991 100644
--- a/jest-setup.js
+++ b/jest-setup.js
@@ -13,6 +13,5 @@
 const jest = require('jest');
 
 window.$ = window.jQuery = require('jquery');
-jest.mock('zeroclipboard', () => {});
 
 global.fetch = require('jest-fetch-mock');

http://git-wip-us.apache.org/repos/asf/couchdb-fauxton/blob/2e770d66/package.json
----------------------------------------------------------------------
diff --git a/package.json b/package.json
index 8656fc7..4be5550 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
     "brace": "^0.7.0",
     "chai": "^3.5.0",
     "clean-css": "^3.4.9",
+    "clipboard": "^1.5.16",
     "couchapp": "~0.11.0",
     "css-loader": "^0.26.0",
     "d3": "^3.4.11",
@@ -98,13 +99,13 @@
     "url": "~0.7.9",
     "url-loader": "^0.5.7",
     "urls": "~0.0.3",
+    "uuid": "^3.0.1",
     "velocity-animate": "^1.2.3",
     "velocity-react": "1.1.11",
     "visualizeRevTree": "git+https://github.com/neojski/visualizeRevTree.git#gh-pages",
     "webpack": "^1.12.12",
     "webpack-dev-server": "^1.14.1",
-    "whatwg-fetch": "^2.0.1",
-    "zeroclipboard": "^2.2.0"
+    "whatwg-fetch": "^2.0.1"
   },
   "scripts": {
     "stylecheck": "eslint --ext=js,jsx .",