mirror of https://github.com/nextcloud/calendar
323 lines
9.4 KiB
Vue
323 lines
9.4 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
|
|
- @copyright Copyright (c) 2023 Jonas Heinrich <heinrich@synyx.net>
|
|
-
|
|
- @author Georg Ehrke <oc.list@georgehrke.com>
|
|
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
|
- @author Jonas Heinrich <heinrich@synyx.net>
|
|
-
|
|
- @license AGPL-3.0-or-later
|
|
-
|
|
- This program is free software: you can redistribute it and/or modify
|
|
- it under the terms of the GNU Affero General Public License as
|
|
- published by the Free Software Foundation, either version 3 of the
|
|
- License, or (at your option) any later version.
|
|
-
|
|
- This program is distributed in the hope that it will be useful,
|
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
- GNU Affero General Public License for more details.
|
|
-
|
|
- You should have received a copy of the GNU Affero General Public License
|
|
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
-
|
|
-->
|
|
|
|
<template>
|
|
<div>
|
|
<InviteesListSearch v-if="!isReadOnly && !isSharedWithMe && hasUserEmailAddress"
|
|
:already-invited-emails="alreadyInvitedEmails"
|
|
:organizer="calendarObjectInstance.organizer"
|
|
@add-attendee="addAttendee" />
|
|
<OrganizerListItem v-if="hasOrganizer"
|
|
:is-read-only="isReadOnly || isSharedWithMe"
|
|
:organizer="calendarObjectInstance.organizer" />
|
|
<InviteesListItem v-for="invitee in inviteesWithoutOrganizer"
|
|
:key="invitee.email"
|
|
:attendee="invitee"
|
|
:is-read-only="isReadOnly || isSharedWithMe"
|
|
:organizer-display-name="organizerDisplayName"
|
|
:members="invitee.members"
|
|
@remove-attendee="removeAttendee" />
|
|
<NoAttendeesView v-if="isReadOnly && isListEmpty"
|
|
:message="noInviteesMessage" />
|
|
<NoAttendeesView v-if="!isReadOnly && isListEmpty && hasUserEmailAddress"
|
|
:message="noInviteesMessage" />
|
|
<NoAttendeesView v-if="isSharedWithMe"
|
|
:message="noOwnerMessage" />
|
|
<OrganizerNoEmailError v-if="!isReadOnly && isListEmpty && !hasUserEmailAddress" />
|
|
|
|
<div class="invitees-list-button-group">
|
|
<NcButton v-if="isCreateTalkRoomButtonVisible"
|
|
class="invitees-list-button-group__button"
|
|
:disabled="isCreateTalkRoomButtonDisabled"
|
|
@click="createTalkRoom">
|
|
{{ $t('calendar', 'Create Talk room for this event') }}
|
|
</NcButton>
|
|
|
|
<NcButton v-if="!isReadOnly"
|
|
class="invitees-list-button-group__button"
|
|
:disabled="isListEmpty || !isOrganizer"
|
|
@click="openFreeBusy">
|
|
{{ $t('calendar', 'Show busy times') }}
|
|
</NcButton>
|
|
<FreeBusy v-if="showFreeBusyModel"
|
|
:attendees="calendarObjectInstance.attendees"
|
|
:organizer="calendarObjectInstance.organizer"
|
|
:start-date="calendarObjectInstance.startDate"
|
|
:end-date="calendarObjectInstance.endDate"
|
|
@close="closeFreeBusy" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { NcButton } from '@nextcloud/vue'
|
|
import { mapState } from 'vuex'
|
|
import InviteesListSearch from './InviteesListSearch.vue'
|
|
import InviteesListItem from './InviteesListItem.vue'
|
|
import OrganizerListItem from './OrganizerListItem.vue'
|
|
import NoAttendeesView from '../NoAttendeesView.vue'
|
|
import OrganizerNoEmailError from '../OrganizerNoEmailError.vue'
|
|
import { createTalkRoom, doesContainTalkLink } from '../../../services/talkService.js'
|
|
import FreeBusy from '../FreeBusy/FreeBusy.vue'
|
|
import {
|
|
showSuccess,
|
|
showError,
|
|
} from '@nextcloud/dialogs'
|
|
import { organizerDisplayName, removeMailtoPrefix } from '../../../utils/attendee.js'
|
|
|
|
export default {
|
|
name: 'InviteesList',
|
|
components: {
|
|
NcButton,
|
|
FreeBusy,
|
|
OrganizerNoEmailError,
|
|
NoAttendeesView,
|
|
InviteesListItem,
|
|
InviteesListSearch,
|
|
OrganizerListItem,
|
|
},
|
|
props: {
|
|
isReadOnly: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
calendarObjectInstance: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
isSharedWithMe: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
creatingTalkRoom: false,
|
|
showFreeBusyModel: false,
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState({
|
|
talkEnabled: state => state.settings.talkEnabled,
|
|
}),
|
|
noInviteesMessage() {
|
|
return this.$t('calendar', 'No attendees yet')
|
|
},
|
|
noOwnerMessage() {
|
|
return this.$t('calendar', 'You do not own this calendar, so you cannot add attendees to this event')
|
|
},
|
|
invitees() {
|
|
return this.calendarObjectInstance.attendees.filter(attendee => {
|
|
return !['RESOURCE', 'ROOM'].includes(attendee.attendeeProperty.userType)
|
|
})
|
|
},
|
|
groups() {
|
|
return this.invitees.filter(attendee => {
|
|
if (attendee.attendeeProperty.userType === 'GROUP') {
|
|
attendee.members = this.invitees.filter(invitee => {
|
|
return invitee.attendeeProperty.member
|
|
&& invitee.attendeeProperty.member.includes(attendee.uri)
|
|
&& attendee.attendeeProperty.userType === 'GROUP'
|
|
})
|
|
return attendee.members.length > 0
|
|
}
|
|
return false
|
|
})
|
|
},
|
|
inviteesWithoutOrganizer() {
|
|
|
|
if (!this.calendarObjectInstance.organizer) {
|
|
return this.invitees
|
|
}
|
|
|
|
return this.invitees
|
|
.filter(attendee => {
|
|
// Filter attendees which are part of an invited group
|
|
if (this.groups.some(function(group) {
|
|
return attendee.attendeeProperty.member
|
|
&& attendee.attendeeProperty.member.includes(group.uri)
|
|
&& attendee.attendeeProperty.userType === 'INDIVIDUAL'
|
|
})) {
|
|
return false
|
|
}
|
|
|
|
// Filter empty groups
|
|
if (attendee.attendeeProperty.userType === 'GROUP') {
|
|
return attendee.members.length > 0
|
|
}
|
|
|
|
return attendee.uri !== this.calendarObjectInstance.organizer.uri
|
|
})
|
|
},
|
|
isOrganizer() {
|
|
return this.calendarObjectInstance.organizer !== null
|
|
&& this.$store.getters.getCurrentUserPrincipal !== null
|
|
&& removeMailtoPrefix(this.calendarObjectInstance.organizer.uri) === this.$store.getters.getCurrentUserPrincipal.emailAddress
|
|
},
|
|
hasOrganizer() {
|
|
return this.calendarObjectInstance.organizer !== null
|
|
},
|
|
|
|
organizerDisplayName() {
|
|
return organizerDisplayName(this.calendarObjectInstance.organizer)
|
|
},
|
|
isListEmpty() {
|
|
return !this.calendarObjectInstance.organizer && this.invitees.length === 0
|
|
},
|
|
alreadyInvitedEmails() {
|
|
const emails = this.invitees.map(attendee => removeMailtoPrefix(attendee.uri))
|
|
|
|
const principal = this.$store.getters.getCurrentUserPrincipal
|
|
if (principal) {
|
|
emails.push(principal.emailAddress)
|
|
}
|
|
|
|
return emails
|
|
},
|
|
hasUserEmailAddress() {
|
|
const principal = this.$store.getters.getCurrentUserPrincipal
|
|
if (!principal) {
|
|
return false
|
|
}
|
|
|
|
return !!principal.emailAddress
|
|
},
|
|
isCreateTalkRoomButtonVisible() {
|
|
return this.talkEnabled
|
|
},
|
|
isCreateTalkRoomButtonDisabled() {
|
|
if (this.creatingTalkRoom) {
|
|
return true
|
|
}
|
|
|
|
if (doesContainTalkLink(this.calendarObjectInstance.location)) {
|
|
return true
|
|
}
|
|
if (doesContainTalkLink(this.calendarObjectInstance.description)) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
},
|
|
},
|
|
methods: {
|
|
addAttendee({ commonName, email, calendarUserType, language, timezoneId, member }) {
|
|
this.$store.commit('addAttendee', {
|
|
calendarObjectInstance: this.calendarObjectInstance,
|
|
commonName,
|
|
uri: email,
|
|
calendarUserType,
|
|
participationStatus: 'NEEDS-ACTION',
|
|
role: 'REQ-PARTICIPANT',
|
|
rsvp: true,
|
|
language,
|
|
timezoneId,
|
|
organizer: this.$store.getters.getCurrentUserPrincipal,
|
|
member,
|
|
})
|
|
},
|
|
removeAttendee(attendee) {
|
|
// Remove attendee from participating group
|
|
if (attendee.member) {
|
|
this.groups.forEach(group => {
|
|
if (attendee.member.includes(group.uri)) {
|
|
group.members = group.members.filter(member => {
|
|
if (!attendee.member.includes(group.uri)) {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
})
|
|
}
|
|
this.$store.commit('removeAttendee', {
|
|
calendarObjectInstance: this.calendarObjectInstance,
|
|
attendee,
|
|
})
|
|
},
|
|
openFreeBusy() {
|
|
this.showFreeBusyModel = true
|
|
},
|
|
closeFreeBusy() {
|
|
this.showFreeBusyModel = false
|
|
},
|
|
async createTalkRoom() {
|
|
const NEW_LINE = '\r\n'
|
|
try {
|
|
this.creatingTalkRoom = true
|
|
const url = await createTalkRoom(
|
|
this.calendarObjectInstance.title,
|
|
this.calendarObjectInstance.description,
|
|
)
|
|
|
|
// Store in LOCATION property if it's missing/empty. Append to description otherwise.
|
|
if ((this.calendarObjectInstance.location ?? '').trim() === '') {
|
|
this.$store.commit('changeLocation', {
|
|
calendarObjectInstance: this.calendarObjectInstance,
|
|
location: url,
|
|
})
|
|
showSuccess(this.$t('calendar', 'Successfully appended link to talk room to location.'))
|
|
} else {
|
|
if (!this.calendarObjectInstance.description) {
|
|
this.$store.commit('changeDescription', {
|
|
calendarObjectInstance: this.calendarObjectInstance,
|
|
description: url,
|
|
})
|
|
} else {
|
|
this.$store.commit('changeDescription', {
|
|
calendarObjectInstance: this.calendarObjectInstance,
|
|
description: this.calendarObjectInstance.description + NEW_LINE + NEW_LINE + url + NEW_LINE,
|
|
})
|
|
}
|
|
showSuccess(this.$t('calendar', 'Successfully appended link to talk room to description.'))
|
|
}
|
|
} catch (error) {
|
|
showError(this.$t('calendar', 'Error creating Talk room'))
|
|
} finally {
|
|
this.creatingTalkRoom = false
|
|
}
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.invitees-list-button-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 5px;
|
|
}
|
|
|
|
.invitees-list-button-group__button {
|
|
flex: 1 0 200px;
|
|
|
|
::v-deep .button-vue__text {
|
|
white-space: unset !important;
|
|
overflow: unset !important;
|
|
text-overflow: unset !important;
|
|
}
|
|
}
|
|
</style>
|