Skip to content

Commit

Permalink
Merge pull request #6520 from nextcloud/feat/editor/attendee-status
Browse files Browse the repository at this point in the history
feat(editor): improve attendee and resource status display
  • Loading branch information
st3iny authored Jan 16, 2025
2 parents e2f3ad0 + 9c23ed8 commit 7dd04d3
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 107 deletions.
246 changes: 150 additions & 96 deletions src/components/Editor/AvatarParticipationStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,101 +16,13 @@
:display-name="commonName"
:is-no-user="true" />
<template v-if="!isGroup">
<template v-if="participationStatus === 'ACCEPTED' && isViewedByOrganizer">
<IconCheck class="avatar-participation-status__indicator"
fill-color="#32CD32"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Invitation accepted') }}
</div>
</template>
<template v-else-if="isResource && participationStatus === 'ACCEPTED'">
<IconCheck class="avatar-participation-status__indicator"
fill-color="#32CD32"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Available') }}
</div>
</template>
<template v-else-if="isSuggestion">
<IconCheck class="avatar-participation-status__indicator"
fill-color="#32CD32"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Suggested') }}
</div>
</template>
<template v-else-if="participationStatus === 'TENTATIVE'">
<IconCheck class="avatar-participation-status__indicator"
fill-color="#32CD32"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Participation marked as tentative') }}
</div>
</template>
<template v-else-if="participationStatus === 'ACCEPTED' && !isViewedByOrganizer">
<IconCheck class="avatar-participation-status__indicator"
fill-color="#32CD32"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Accepted {organizerName}\'s invitation', {
organizerName: organizerDisplayName,
}) }}
</div>
</template>
<template v-else-if="isResource && participationStatus === 'DECLINED'">
<IconClose class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Not available') }}
</div>
</template>
<template v-else-if="participationStatus === 'DECLINED' && isViewedByOrganizer">
<IconClose class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Invitation declined') }}
</div>
</template>
<template v-else-if="participationStatus === 'DECLINED' && !isViewedByOrganizer">
<IconClose class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Declined {organizerName}\'s invitation', {
organizerName: organizerDisplayName,
}) }}
</div>
</template>
<template v-else-if="participationStatus === 'DELEGATED'">
<IconDelegated class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Invitation is delegated') }}
</div>
</template>
<template v-else-if="isResource">
<IconNoResponse class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Checking availability') }}
</div>
</template>
<template v-else-if="isViewedByOrganizer">
<IconNoResponse class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Awaiting response') }}
</div>
</template>
<template v-else>
<IconNoResponse class="avatar-participation-status__indicator"
:size="20" />
<div class="avatar-participation-status__text">
{{ t('calendar', 'Has not responded to {organizerName}\'s invitation yet', {
organizerName: organizerDisplayName,
}) }}
</div>
</template>
<component :is="status.icon"
class="avatar-participation-status__indicator"
:fill-color="status.fillColor"
:size="20" />
<div class="avatar-participation-status__text">
{{ status.text }}
</div>
</template>
</div>
</template>
Expand All @@ -132,7 +44,6 @@ export default {
IconNoResponse,
IconClose,
IconDelegated,

},
props: {
avatarLink: {
Expand All @@ -143,6 +54,10 @@ export default {
type: String,
required: true,
},
scheduleStatus: {
type: String,
required: false,
},
commonName: {
type: String,
required: true,
Expand Down Expand Up @@ -172,6 +87,145 @@ export default {
required: true,
},
},
computed: {
/**
* @return {icon: object, fillColor: string|undefined, text: string}
*/
status() {
const acceptedIcon = {
icon: IconCheck,
fillColor: '#32CD32',
}
const declinedIcon = {
icon: IconClose,
fillColor: '#ff4402',
}

if (this.isSuggestion) {
return {
...acceptedIcon,
text: t('calendar', 'Suggested'),
}
}

// Try to use the participation status first
switch (this.participationStatus) {
case 'ACCEPTED':
if (this.isResource) {
return {
...acceptedIcon,
text: t('calendar', 'Available'),
}
}

if (this.attendeeIsOrganizer && !this.isViewedByOrganizer) {
return {
...acceptedIcon,
text: t('calendar', 'Invited you'),
}
}

if (this.isViewedByOrganizer) {
return {
...acceptedIcon,
text: t('calendar', 'Invitation accepted'),
}
}

return {
...acceptedIcon,
text: t('calendar', 'Accepted {organizerName}\'s invitation', {
organizerName: this.organizerDisplayName,
}),
}
case 'TENTATIVE':
return {
...acceptedIcon,
text: t('calendar', 'Participation marked as tentative'),
}
case 'DELEGATED':
return {
icon: IconDelegated,
text: t('calendar', 'Invitation is delegated'),
}
case 'DECLINED':
if (this.isResource) {
return {
...declinedIcon,
text: t('calendar', 'Not available'),
}
}

if (this.isViewedByOrganizer) {
return {
...declinedIcon,
text: t('calendar', 'Invitation declined'),
}
}

return {
...declinedIcon,
text: t('calendar', 'Declined {organizerName}\'s invitation', {
organizerName: this.organizerDisplayName,
}),
}
}

// Schedule status is only present on the original event of the organizer
// TODO: Is this a bug or compliant with RFCs?
if (this.isViewedByOrganizer) {
// No status or status 1.0 indicate that the invitation is pending
if (!this.scheduleStatus || this.scheduleStatus === '1.0') {
if (this.isResource) {
return {
icon: IconNoResponse,
text: t('calendar', 'Availability will be checked'),
}
}

return {
icon: IconNoResponse,
text: t('calendar', 'Invitation will be sent'),
}
}

// Status 3.7, 3.8, 5.1, 5.2 and 5.3 indicate delivery failures.
// Could be due to insufficient permissions or some temporary failure.
if (this.scheduleStatus[0] === '3' || this.scheduleStatus[0] === '5') {
if (this.isResource) {
return {
icon: IconNoResponse,
text: t('calendar', 'Failed to check availability'),
}
}

return {
icon: IconNoResponse,
text: t('calendar', 'Failed to deliver invitation'),
}
}

return {
icon: IconNoResponse,
text: t('calendar', 'Awaiting response'),
}
}

if (this.isResource) {
return {
icon: IconNoResponse,
text: t('calendar', 'Checking availability'),
}
}

return {
icon: IconNoResponse,
text: t('calendar', 'Has not responded to {organizerName}\'s invitation yet', {
organizerName: this.organizerDisplayName,
}),
}
},
},
}
</script>
<style lang="scss" scoped>
Expand Down
6 changes: 6 additions & 0 deletions src/components/Editor/Invitees/InviteesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
:is-shared-with-me="isSharedWithMe"
:organizer="calendarObjectInstance.organizer"
:organizer-selection="organizerSelection"
:is-viewed-by-organizer="isViewedByOrganizer"
@change-organizer="changeOrganizer" />
<InviteesListItem v-for="invitee in limitedInviteesWithoutOrganizer"
:key="invitee.email"
:attendee="invitee"
:is-read-only="isReadOnly"
:organizer-display-name="organizerDisplayName"
:members="invitee.members"
:is-viewed-by-organizer="isViewedByOrganizer"
@remove-attendee="removeAttendee" />
<div v-if="limit > 0 && inviteesWithoutOrganizer.length > limit"
class="invitees-list__more">
Expand Down Expand Up @@ -287,6 +289,10 @@ export default {

return false
},
isViewedByOrganizer() {
const organizerEmail = removeMailtoPrefix(this.calendarObjectInstance.organizer.uri)
return organizerEmail === this.principalsStore.getCurrentUserPrincipalEmail
},
statusHeader() {
if (!this.isReadOnly) {
return ''
Expand Down
9 changes: 5 additions & 4 deletions src/components/Editor/Invitees/InviteesListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
:is-resource="false"
:avatar-link="avatarLink"
:participation-status="attendee.participationStatus"
:schedule-status="attendee.attendeeProperty.getParameterFirstValue('SCHEDULE-STATUS')"
:organizer-display-name="organizerDisplayName"
:common-name="commonName"
:is-group="isGroup" />
Expand Down Expand Up @@ -136,6 +137,10 @@ export default {
default: () => [],
required: false,
},
isViewedByOrganizer: {
type: Boolean,
default: false,
},
},
data() {
return {
Expand Down Expand Up @@ -191,10 +196,6 @@ export default {
isNonParticipant() {
return this.attendee.role === 'NON-PARTICIPANT'
},
isViewedByOrganizer() {
// TODO: check if also viewed by organizer
return !this.isReadOnly
},
isGroup() {
return this.attendee.attendeeProperty.userType === 'GROUP'
},
Expand Down
8 changes: 5 additions & 3 deletions src/components/Editor/Invitees/OrganizerListItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
:is-resource="isResource"
:common-name="commonName"
:organizer-display-name="commonName"
:schedule-status="organizer.attendeeProperty.getParameterFirstValue('SCHEDULE-STATUS')"
participation-status="ACCEPTED" />
<div class="invitees-list-item__displayname">
{{ commonName }}
Expand Down Expand Up @@ -77,6 +78,10 @@ export default {
type: Boolean,
required: true,
},
isViewedByOrganizer: {
type: Boolean,
default: false,
},
},
computed: {
/**
Expand All @@ -101,9 +106,6 @@ export default {

return ''
},
isViewedByOrganizer() {
return true
},
isResource() {
// The organizer does not have a tooltip
return false
Expand Down
6 changes: 6 additions & 0 deletions src/components/Editor/Resources/ResourceList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
:resource="resource"
:is-read-only="isReadOnly"
:organizer-display-name="organizerDisplayName"
:is-viewed-by-organizer="isViewedByOrganizer"
@remove-resource="removeResource" />

<NoAttendeesView v-if="isListEmpty && hasUserEmailAddress"
Expand All @@ -34,6 +35,7 @@
:is-read-only="false"
:organizer-display-name="organizerDisplayName"
:is-suggestion="true"
:is-viewed-by-organizer="isViewedByOrganizer"
@add-suggestion="addResource" />
</div>
</template>
Expand Down Expand Up @@ -105,6 +107,10 @@ export default {
const emailAddress = this.principalsStore.getCurrentUserPrincipal?.emailAddress
return !!emailAddress
},
isViewedByOrganizer() {
const organizerEmail = removeMailtoPrefix(this.calendarObjectInstance.organizer.uri)
return organizerEmail === this.principalsStore.getCurrentUserPrincipalEmail
},
},
watch: {
resources() {
Expand Down
Loading

0 comments on commit 7dd04d3

Please sign in to comment.