diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index fe8b193b9..b86ddf222 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -54,6 +54,9 @@ msgstr "Username \"{0}\" not found in api_users table" msgid "auto.viewvar.type.unknown" msgstr "Unknown Auto View Var Type {0}" +msgid "copy.error" +msgstr "Could not copy." + msgid "coid" msgstr "CO ID not found" diff --git a/app/resources/locales/en_US/information.po b/app/resources/locales/en_US/information.po index b661506a9..3d35cff34 100644 --- a/app/resources/locales/en_US/information.po +++ b/app/resources/locales/en_US/information.po @@ -118,4 +118,7 @@ msgid "report.for" msgstr "Report for " msgid "table.list" -msgstr "{0} List" \ No newline at end of file +msgstr "{0} List" + +msgid "value.copied" +msgstr "Value copied to clipboard." \ No newline at end of file diff --git a/app/resources/locales/en_US/operation.po b/app/resources/locales/en_US/operation.po index ac1743b10..3828656ba 100644 --- a/app/resources/locales/en_US/operation.po +++ b/app/resources/locales/en_US/operation.po @@ -102,6 +102,12 @@ msgstr "Configure {0}" msgid "configure.plugin" msgstr "Configure Plugin" +msgid "copy" +msgstr "Copy" + +msgid "copy.value" +msgstr "Copy value" + msgid "dashboard.configuration" msgstr "{0} Configuration" diff --git a/app/src/View/Helper/VueHelper.php b/app/src/View/Helper/VueHelper.php index 3175e6732..552218571 100644 --- a/app/src/View/Helper/VueHelper.php +++ b/app/src/View/Helper/VueHelper.php @@ -46,6 +46,9 @@ class VueHelper extends Helper { 'enumeration' => [ 'SuspendableStatusEnum.S' ], + 'error' => [ + 'copy.error' + ], 'field' => [ 'email', 'login', @@ -58,7 +61,8 @@ class VueHelper extends Helper { 'global.value.none', 'datepicker.hour', 'record', - 'report.for' + 'report.for', + 'value.copied' ], 'operation' => [ 'add', @@ -67,7 +71,9 @@ class VueHelper extends Helper { 'autocomplete.pager.show.more', 'autocomplete.people.label', 'autocomplete.people.placeholder', - 'close' + 'close', + 'copy', + 'copy.value' ], 'result' => [ 'failed', diff --git a/app/webroot/css/co-base.css b/app/webroot/css/co-base.css index 4c46e81da..18ebcd491 100644 --- a/app/webroot/css/co-base.css +++ b/app/webroot/css/co-base.css @@ -1917,6 +1917,34 @@ code, code.source-record { overflow-wrap: anywhere; } +.with-copy-icon { + display: flex; + justify-content: space-between; + gap: 0.5em; + align-items: center; +} +button.cm-copy-value-button { + display: flex; + align-items: center; + gap: 0.1em; + padding: 0 0.5em 0.2em; + text-transform: unset; + box-shadow: none; + background-color: var(--cmg-color-body-bg); + border: 1px solid var(--cmg-color-bg-006); + color: var(--cmg-color-link); + white-space: nowrap; +} +button.cm-copy-value-button:hover, +button.cm-copy-value-button:active, +button.cm-copy-value-button:focus { + background-color: var(--cmg-color-btn-bg-001); + border: 1px solid var(--cmg-color-btn-bg-001); + color: var(--cmg-color-txt-inverse) !important; +} +.cm-copy-value-button .material-icons-outlined { + font-size: 1em !important; +} /* INDEX VIEWS and TABLES */ table { width: 100%; diff --git a/app/webroot/css/co-responsive.css b/app/webroot/css/co-responsive.css index 0f68053c7..cb6607a04 100644 --- a/app/webroot/css/co-responsive.css +++ b/app/webroot/css/co-responsive.css @@ -100,6 +100,7 @@ display: grid; grid-template-columns: 1fr 1fr; align-items: center; + min-height: 2.5em; } #mvea-canvas-roles .field-data-container { grid-template-columns: 1fr 1fr 2fr auto; @@ -203,6 +204,24 @@ padding: 0 0.75em; border-right: 1px dashed var(--cmg-color-bg-006); } + .with-copy-icon { + justify-content: flex-start; + position: relative; + } + .with-copy-icon .cm-copy-value-button { + display: none; + /* The following two rules are a trade-off; remove them to make the copy button appear just to the right of the + value being copied; the trade-off is that long lines will wrap when the button appears causing the row to + jump in size which can also be distracting. */ + position: absolute; + right: 1em; + } + .field-data-container:hover .with-copy-icon .cm-copy-value-button, + .field-data-container:active .with-copy-icon .cm-copy-value-button, + .field-data-container:focus .with-copy-icon .cm-copy-value-button, + .field-data-container:focus-within .with-copy-icon .cm-copy-value-button { + display: flex; + } /* SEARCH RESULTS */ body.search .page-title-container { justify-content: start; diff --git a/app/webroot/js/comanage/components/common/copy-value-button.js b/app/webroot/js/comanage/components/common/copy-value-button.js new file mode 100644 index 000000000..3ffb360ee --- /dev/null +++ b/app/webroot/js/comanage/components/common/copy-value-button.js @@ -0,0 +1,71 @@ +/** + * COmanage Registry Copy Value Component JavaScript + * + * Portions licensed to the University Corporation for Advanced Internet + * Development, Inc. ("UCAID") under one or more contributor license agreements. + * See the NOTICE file distributed with this work for additional information + * regarding copyright ownership. + * + * UCAID licenses this file to you 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. + * + * @link https://www.internet2.edu/comanage COmanage Project + * @package registry + * @since COmanage Registry v5.0.0 + * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + */ + +import { + constructLanguageString +} from '../utils/helpers.js'; + +export default { + props: { + valueToCopy: String, + txt: Object + }, + data() { + return { + copyIcon: "content_copy", + ariaLabel: this.txt['copy.value'] + } + }, + methods: { + async copyValue(val) { + try { + // remove extra white spaces and trim the value + let valWithNormalizedSpaces = val.replace(/\s+/g, ' ').trim(); + // copy to clipboard + await navigator.clipboard.writeText(valWithNormalizedSpaces); + // provide feedback + this.copyIcon = 'thumb_up'; + this.ariaLabel = this.txt['value.copied']; + // reset feedback + setTimeout(() => this.copyIcon = 'content_copy', 800); + setTimeout(() => this.ariaLabel = this.txt['copy.value'], 2200); + } catch($e) { + // this will be rendered if browser is not on HTTPS + alert(this.txt["copy.error"] + "\n" + $e); + } + } + }, + template: ` + + ` +} diff --git a/app/webroot/js/comanage/components/mvea/mvea-item.js b/app/webroot/js/comanage/components/mvea/mvea-item.js index 9960e2f08..ad6fba924 100644 --- a/app/webroot/js/comanage/components/mvea/mvea-item.js +++ b/app/webroot/js/comanage/components/mvea/mvea-item.js @@ -24,6 +24,7 @@ * @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) */ +import CopyValueButton from '../common/copy-value-button.js'; import { constructLanguageString } from '../utils/helpers.js'; @@ -34,9 +35,15 @@ export default { core: Object, txt: Object }, + components: { + CopyValueButton + }, computed: { mveaLink: function() { return this.core.webroot + this.core.mveaController + (this.core.action == 'edit' ? '/edit/' : '/view/') + this.mvea.id; + }, + mveaAddress: function() { + return this.mvea.room + ' ' + this.mvea.street + ' ' + this.mvea.locality + ' ' + this.mvea.state + ' ' + this.mvea.postal_code + ' ' + this.mvea.country; } }, methods: { @@ -57,7 +64,7 @@ export default { template: `
  • -
    +
    {{ this.txt.primary }} @@ -83,8 +106,12 @@ export default {
  • -
    - {{ this.mvea.mail }} +
    {{ this.txt.unverified }} @@ -93,8 +120,12 @@ export default {
  • -
    - {{ this.mvea.identifier }} +
    {{ this.txt["SuspendableStatusEnum.S"] }} @@ -104,8 +135,13 @@ export default {
  • -
    +
    {{ this.mvea.tag }} @@ -113,16 +149,22 @@ export default {
  • -
    +
    - {{ this.mvea.room }} {{ this.mvea.street }} - -
    {{ this.mvea.locality }}{{ this.mvea.locality != '' && this.mvea.state != '' ? ', ' : ''}}{{ this.mvea.state }} -
    - -
    {{ this.mvea.postal_code }} {{ this.mvea.country }} -
    -
    + + {{ this.mvea.room }} {{ this.mvea.street }} + +
    {{ this.mvea.locality }}{{ this.mvea.locality != '' && this.mvea.state != '' ? ', ' : ''}}{{ this.mvea.state }} +
    + +
    {{ this.mvea.postal_code }} {{ this.mvea.country }} +
    +
    + + +
    {{ this.mvea.type.display_name }} @@ -130,8 +172,12 @@ export default {
  • -
    - {{ this.mvea.country_code }} {{ this.mvea.area_code }} {{ this.mvea.number }} +
    {{ this.mvea.type.display_name }} @@ -139,9 +185,13 @@ export default {
  • -
    +
    {{ this.mvea.type.display_name }} @@ -149,8 +199,12 @@ export default {
  • -
    - {{ this.mvea.type_id }} +
    {{ this.mvea.type.display_name }}