Skip to content

Commit 48a58d6

Browse files
abhinavohriSebastianKrupinski
authored andcommitted
feat(invitees): add copy button for attendee emails
Signed-off-by: Abhinav Ohri <[email protected]>
1 parent 2c37b80 commit 48a58d6

File tree

3 files changed

+139
-24
lines changed

3 files changed

+139
-24
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<div class="attendee-display" :class="{ 'attendee-display--compact': hasMembers }">
8+
<div class="attendee-display__name">
9+
<slot name="displayname">
10+
{{ displayName }}
11+
</slot>
12+
</div>
13+
<span v-if="email && !hasMembers" :title="email" class="attendee-display__button-wrapper">
14+
<NcButton
15+
class="attendee-display__button"
16+
variant="tertiary-no-background"
17+
:aria-label="$t('calendar', 'Copy name and email address')"
18+
@click="handleCopy">
19+
<template #icon>
20+
<ContentCopy :size="16" />
21+
</template>
22+
</NcButton>
23+
</span>
24+
</div>
25+
</template>
26+
27+
<script>
28+
import { showError, showSuccess } from '@nextcloud/dialogs'
29+
import { NcButton } from '@nextcloud/vue'
30+
import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
31+
32+
export default {
33+
name: 'AttendeeDisplay',
34+
components: {
35+
NcButton,
36+
ContentCopy,
37+
},
38+
39+
props: {
40+
displayName: {
41+
type: String,
42+
required: true,
43+
},
44+
45+
email: {
46+
type: String,
47+
required: true,
48+
},
49+
50+
hasMembers: {
51+
type: Boolean,
52+
default: false,
53+
},
54+
},
55+
56+
methods: {
57+
async handleCopy() {
58+
try {
59+
const text = `${this.displayName} <${this.email}>`
60+
await navigator.clipboard.writeText(text)
61+
showSuccess(this.$t('calendar', 'Copied to clipboard'))
62+
} catch (e) {
63+
showError(this.$t('calendar', 'Failed to copy'))
64+
}
65+
},
66+
},
67+
}
68+
</script>
69+
70+
<style lang="scss" scoped>
71+
.attendee-display {
72+
display: flex;
73+
align-items: center;
74+
gap: var(--default-grid-baseline);
75+
overflow: hidden;
76+
margin-bottom: calc(var(--default-grid-baseline) * 4);
77+
78+
&--compact {
79+
margin-bottom: 0;
80+
}
81+
82+
&__name {
83+
text-overflow: ellipsis;
84+
overflow: hidden;
85+
white-space: nowrap;
86+
}
87+
88+
&__button-wrapper {
89+
flex-shrink: 0;
90+
display: flex;
91+
}
92+
93+
&__button {
94+
flex-shrink: 0;
95+
}
96+
}
97+
</style>

src/components/Editor/Invitees/InviteesListItem.vue

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,21 @@
1616
:common-name="commonName"
1717
:timezone="timezone"
1818
:is-group="isGroup" />
19-
<div
20-
class="invitees-list-item__displayname"
21-
:class="{ 'invitees-list-item__groupname': members.length }">
22-
{{ commonName }}
23-
<span
24-
v-if="members.length"
25-
class="invitees-list-item__member-count">
26-
({{ $n('calendar', '%n member', '%n members', members.length) }})
27-
</span>
28-
</div>
19+
20+
<AttendeeDisplay
21+
:display-name="commonName"
22+
:email="attendeeEmail"
23+
:has-members="!!members.length">
24+
<template #displayname>
25+
{{ commonName }}
26+
<span
27+
v-if="members.length"
28+
class="invitees-list-item__member-count">
29+
({{ $n('calendar', '%n member', '%n members', members.length) }})
30+
</span>
31+
</template>
32+
</AttendeeDisplay>
33+
2934
<div class="invitees-list-item__actions">
3035
<NcButton
3136
v-if="members.length"
@@ -116,6 +121,7 @@ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
116121
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
117122
import Delete from 'vue-material-design-icons/TrashCanOutline.vue'
118123
import AvatarParticipationStatus from '../AvatarParticipationStatus.vue'
124+
import AttendeeDisplay from './AttendeeDisplay.vue'
119125
import { getAttendeeDetails } from '../../../services/attendeeDetails.js'
120126
import useCalendarObjectInstanceStore from '../../../store/calendarObjectInstance.js'
121127
import { removeMailtoPrefix } from '../../../utils/attendee.js'
@@ -132,6 +138,7 @@ export default {
132138
NcButton,
133139
ChevronDown,
134140
ChevronUp,
141+
AttendeeDisplay,
135142
},
136143
137144
props: {
@@ -207,6 +214,15 @@ export default {
207214
return ''
208215
},
209216
217+
/**
218+
* Email address without the 'mailto:' prefix
219+
*
220+
* @return {string}
221+
*/
222+
attendeeEmail() {
223+
return this.attendee.uri ? removeMailtoPrefix(this.attendee.uri) : ''
224+
},
225+
210226
radioName() {
211227
return this._uid + '-role-radio-input-group'
212228
},
@@ -302,17 +318,6 @@ export default {
302318
display: flex;
303319
}
304320
305-
.invitees-list-item__displayname {
306-
margin-bottom: 17px;
307-
text-overflow: ellipsis;
308-
overflow: hidden;
309-
white-space: nowrap;
310-
}
311-
312-
.invitees-list-item__groupname {
313-
margin-bottom: 0px;
314-
}
315-
316321
.avatar-participation-status {
317322
margin-top: 5px;
318323
}

src/components/Editor/Invitees/OrganizerListItem.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
:organizer-display-name="commonName"
1515
:schedule-status="organizer.attendeeProperty.getParameterFirstValue('SCHEDULE-STATUS')"
1616
participation-status="ACCEPTED" />
17-
<div class="invitees-list-item__displayname">
18-
{{ commonName }}
19-
</div>
17+
18+
<AttendeeDisplay
19+
:display-name="commonName"
20+
:email="organizerEmail" />
21+
2022
<div class="invitees-list-item__organizer-hint">
2123
{{ $t('calendar', '(organizer)') }}
2224
</div>
@@ -54,11 +56,13 @@ import NcActionButton from '@nextcloud/vue/components/NcActionButton'
5456
import NcActions from '@nextcloud/vue/components/NcActions'
5557
import Crown from 'vue-material-design-icons/CrownOutline.vue'
5658
import AvatarParticipationStatus from '../AvatarParticipationStatus.vue'
59+
import AttendeeDisplay from './AttendeeDisplay.vue'
5760
import { removeMailtoPrefix } from '../../../utils/attendee.js'
5861
5962
export default {
6063
name: 'OrganizerListItem',
6164
components: {
65+
AttendeeDisplay,
6266
AvatarParticipationStatus,
6367
Crown,
6468
NcActions,
@@ -117,6 +121,15 @@ export default {
117121
return ''
118122
},
119123
124+
/**
125+
* Email address without the 'mailto:' prefix
126+
*
127+
* @return {string}
128+
*/
129+
organizerEmail() {
130+
return this.organizer.uri ? removeMailtoPrefix(this.organizer.uri) : ''
131+
},
132+
120133
isResource() {
121134
// The organizer does not have a tooltip
122135
return false

0 commit comments

Comments
 (0)