Skip to content

Commit

Permalink
Provide "copy value to clipboard" feature for MVEAs (CFM-413) (#216)
Browse files Browse the repository at this point in the history
* Provide "copy value to clipboard" feature for MVEAs (CFM-413)

* Put "Copy" button in a consistent place and avoid line wrapping when it appears. (CFM-413)

* Trivial refactoring to simplify component @click action. (CFM-413)
  • Loading branch information
arlen authored Sep 24, 2024
1 parent e285d14 commit 50b5a0e
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 24 deletions.
3 changes: 3 additions & 0 deletions app/resources/locales/en_US/error.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
5 changes: 4 additions & 1 deletion app/resources/locales/en_US/information.po
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,7 @@ msgid "report.for"
msgstr "Report for "

msgid "table.list"
msgstr "{0} List"
msgstr "{0} List"

msgid "value.copied"
msgstr "Value copied to clipboard."
6 changes: 6 additions & 0 deletions app/resources/locales/en_US/operation.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
10 changes: 8 additions & 2 deletions app/src/View/Helper/VueHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ class VueHelper extends Helper {
'enumeration' => [
'SuspendableStatusEnum.S'
],
'error' => [
'copy.error'
],
'field' => [
'email',
'login',
Expand All @@ -58,7 +61,8 @@ class VueHelper extends Helper {
'global.value.none',
'datepicker.hour',
'record',
'report.for'
'report.for',
'value.copied'
],
'operation' => [
'add',
Expand All @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions app/webroot/css/co-base.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
19 changes: 19 additions & 0 deletions app/webroot/css/co-responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
71 changes: 71 additions & 0 deletions app/webroot/js/comanage/components/common/copy-value-button.js
Original file line number Diff line number Diff line change
@@ -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: `
<button
type="button"
class="cm-copy-value-button btn btn-sm btn-default"
:aria-label="this.ariaLabel"
@click.stop.prevent="copyValue(this.valueToCopy)">
<span class="material-icons-outlined">{{ this.copyIcon }}</span>
<span class="cm-copy-value-text">{{ this.txt.copy }}</span>
</button>
`
}
96 changes: 75 additions & 21 deletions app/webroot/js/comanage/components/mvea/mvea-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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: {
Expand All @@ -57,7 +64,7 @@ export default {
template: `
<!-- Names -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'names'" @click="followRowLink">
<div class="field-data force-wrap">
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>
<!-- If there is a display name use it. Otherwise, check language and produce right-to-left
order or left-to-right order. This approach is similar to Model/Entity/Name.php::_getFullName().
Expand All @@ -73,7 +80,23 @@ export default {
<span v-else class="mvea-name-ltr">
{{ this.mvea.honorific }} {{ this.mvea.given }} {{ this.mvea.middle }} {{ this.mvea.family }} {{ this.mvea.suffix }}
</span>
</a>
</a>
<!-- XXX As noted above, replace the following logic when full_name is available. -->
<copy-value-button
v-if="this.mvea.display_name"
:txt="this.txt"
:valueToCopy="this.mvea.display_name">
</copy-value-button>
<copy-value-button
v-else-if="['hu', 'ja', 'ko', 'za-Hans', 'za-Hant'].indexOf(this.mvea.language) != -1"
:txt="this.txt"
:valueToCopy="this.mvea.family + ' ' + this.mvea.given">
</copy-value-button>
<copy-value-button
v-else
:txt="this.txt"
:valueToCopy="this.mvea.honorific + ' ' + this.mvea.given + ' ' + this.mvea.middle + ' ' + this.mvea.family + ' ' + this.mvea.suffix">
</copy-value-button>
</div>
<div class="field-data data-label">
<span v-if="this.mvea.primary_name" class="mr-1 badge bg-outline-secondary primary">{{ this.txt.primary }}</span>
Expand All @@ -83,8 +106,12 @@ export default {
</li>
<!-- Email Addresses -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'email_addresses'" @click="followRowLink">
<div class="field-data force-wrap">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.mail }}</a>
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.mail }}</a>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mvea.mail">
</copy-value-button>
</div>
<div class="field-data data-label">
<span v-if="!(this.mvea.verified)" class="mr-1 badge bg-warning unverified">{{ this.txt.unverified }}</span>
Expand All @@ -93,8 +120,12 @@ export default {
</li>
<!-- Identifiers -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'identifiers'" @click="followRowLink">
<div class="field-data force-wrap">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.identifier }}</a>
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.identifier }}</a>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mvea.identifier">
</copy-value-button>
</div>
<div class="field-data data-label">
<span v-if="this.mvea.status == 'S'" class="mr-1 badge bg-danger">{{ this.txt["SuspendableStatusEnum.S"] }}</span>
Expand All @@ -104,53 +135,76 @@ export default {
</li>
<!-- Ad Hoc Attributes -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'ad_hoc_attributes'" @click="followRowLink">
<div class="field-data force-wrap">
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.value != '' ? this.mvea.value : this.txt["global.value.none"] }}</a>
<copy-value-button
v-if="this.mvea.value != ''"
:txt="this.txt"
:valueToCopy="this.mvea.value">
</copy-value-button>
</div>
<div v-if="this.mvea.tag != ''" class="field-data data-label">
<span class="mr-1 badge bg-light ad-hoc">{{ this.mvea.tag }}</span>
</div>
</li>
<!-- Addresses -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'addresses'" @click="followRowLink">
<div class="field-data force-wrap">
<div class="field-data force-wrap with-copy-icon">
<address>
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.room }} {{ this.mvea.street }}</a>
<span v-if="this.mvea.locality != '' || this.mvea.state != ''" class="addr-locality-state">
<br>{{ this.mvea.locality }}{{ this.mvea.locality != '' && this.mvea.state != '' ? ', ' : ''}}{{ this.mvea.state }}
</span>
<span v-if="this.mvea.postal_code != '' || this.mvea.country != ''" class="addr-postalcode-country">
<br>{{ this.mvea.postal_code }} {{ this.mvea.country }}
</span>
</address>
<a :href="mveaLink" class="row-link" @click.prevent>
{{ this.mvea.room }} {{ this.mvea.street }}
<span v-if="this.mvea.locality != '' || this.mvea.state != ''" class="addr-locality-state">
<br>{{ this.mvea.locality }}{{ this.mvea.locality != '' && this.mvea.state != '' ? ', ' : ''}}{{ this.mvea.state }}
</span>
<span v-if="this.mvea.postal_code != '' || this.mvea.country != ''" class="addr-postalcode-country">
<br>{{ this.mvea.postal_code }} {{ this.mvea.country }}
</span>
</a>
</address>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mveaAddress">
</copy-value-button>
</div>
<div class="field-data data-label">
<span class="mr-1 badge bg-light">{{ this.mvea.type.display_name }}</span>
</div>
</li>
<!-- Telephone Numbers -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'telephone_numbers'" @click="followRowLink">
<div class="field-data force-wrap">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.country_code }} {{ this.mvea.area_code }} {{ this.mvea.number }}</a>
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.country_code }} {{ this.mvea.area_code }} {{ this.mvea.number }}</a>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mvea.country_code+this.mvea.area_code+this.mvea.number">
</copy-value-button>
</div>
<div class="field-data data-label">
<span class="mr-1 badge bg-light">{{ this.mvea.type.display_name }}</span>
</div>
</li>
<!-- Urls -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'urls'" @click="followRowLink">
<div class="field-data force-wrap">
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.description != '' && this.mvea.description != null ? this.mvea.description : this.mvea.url }}</a>
<a :href="this.mvea.url" class="canvas-url-link" :title="this.txt['global.visit.link']"><span class="material-icons">north_east</span></a>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mvea.url">
</copy-value-button>
</div>
<div class="field-data data-label">
<span class="mr-1 badge bg-light">{{ this.mvea.type.display_name }}</span>
</div>
</li>
<!-- Pronouns -->
<li class="field-data-container linked-row" v-if="this.core.mveaType == 'pronouns'" @click="followRowLink">
<div class="field-data force-wrap">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.type_id }}</a>
<div class="field-data force-wrap with-copy-icon">
<a :href="mveaLink" class="row-link" @click.prevent>{{ this.mvea.pronouns }}</a>
<copy-value-button
:txt="this.txt"
:valueToCopy="this.mvea.pronouns">
</copy-value-button>
</div>
<div class="field-data data-label">
<span class="mr-1 badge bg-light">{{ this.mvea.type.display_name }}</span>
Expand Down

0 comments on commit 50b5a0e

Please sign in to comment.