Improve free/busy UI

Signed-off-by: hamza mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
hamza mahjoubi 2024-01-08 09:40:23 +01:00 committed by Hamza Mahjoubi
parent 21e0c08712
commit ccaca16184
7 changed files with 272 additions and 52 deletions

View File

@ -28,7 +28,7 @@
}
.blocking-event-free-busy {
border-color: red;
border-color: var(--color-primary-element);
border-style: solid;
border-left-width: 2px;
border-right-width: 2px;
@ -38,10 +38,12 @@
}
.blocking-event-free-busy.blocking-event-free-busy--first-row {
border-radius: var(--border-radius) var(--border-radius) 0 0;
border-top-width: 2px;
}
.blocking-event-free-busy.blocking-event-free-busy--last-row {
border-radius: 0 0 var(--border-radius) var(--border-radius) ;
border-bottom-width: 2px;
}
@ -66,7 +68,8 @@
&__colors {
width: 100%;
display:flex;
flex-wrap: wrap;
flex-direction: column;
padding: 5px;
.freebusy-caption-item {
display: flex;
align-items: center;

View File

@ -15,7 +15,7 @@
{{ config.name }}
</h2>
<div class="booking__time">
{{date}} {{ startTime }} - {{ endTime }}
{{ date }} {{ startTime }} - {{ endTime }}
</div>
<!-- Description needs to stay inline due to its whitespace -->
<span class="booking__description">{{ config.description }}</span>
@ -146,7 +146,7 @@ export default {
},
date() {
return timeStampToLocaleDate(this.timeSlot.start, this.timeZoneId)
}
},
},
methods: {
save() {

View File

@ -28,24 +28,94 @@
<div v-if="loadingIndicator" class="loading-indicator">
<div class="icon-loading" />
</div>
<FullCalendar ref="freeBusyFullCalendar"
:options="options" />
<div class="freebusy-caption">
<div class="freebusy-caption__calendar-user-types" />
<div class="freebusy-caption__colors">
<div v-for="color in colorCaption" :key="color.color" class="freebusy-caption-item">
<div class="freebusy-caption-item__color" :style="{ 'background-color': color.color }" />
<div class="freebusy-caption-item__label">
{{ color.label }}
</div>
</div>
<div class="modal__content__header">
<h2>{{ t('calendar', 'Find a time') }}</h2>
<h3>{{ eventTitle }}</h3>
<div class="modal__content__header__attendees">
{{ t('calendar', 'with') }}
<NcUserBubble :display-name="organizer.commonName" />
<NcUserBubble v-for="attendee in attendees"
:key="attendee.id"
class="modal__content__header__attendees__user-bubble"
:display-name="attendee.commonName">
<template #name>
<a href="#"
title="Remove user"
class="icon-close"
@click="removeAttendee(attendee)" />
</template>
</NcUserBubble>
</div>
</div>
<div class="modal__content__actions">
<InviteesListSearch class="modal__content__actions__select"
:already-invited-emails="alreadyInvitedEmails"
:organizer="organizer"
@add-attendee="addAttendee" />
<div class="modal__content__actions__date">
<NcButton type="secondary"
@click="handleActions('today')">
{{ $t('calendar', 'Today') }}
</NcButton>
<NcButton type="secondary"
@click="handleActions('left')">
<template #icon>
<ChevronLeftIcon :size="20" />
</template>
</NcButton>
<NcButton type="secondary"
@click="handleActions('right')">
<template #icon>
<ChevronRightIcon :size="20" />
</template>
</NcButton>
<NcDateTimePicker :value="currentDate"
confirm
@confirm="(date)=>handleActions('picker', date)" />
<NcPopover :focus-trap="false">
<template #trigger>
<NcButton type="tertiary-no-background">
<template #icon>
<HelpCircleIcon :size="20" />
</template>
</NcButton>
</template>
<template>
<div class="freebusy-caption">
<div class="freebusy-caption__calendar-user-types" />
<div class="freebusy-caption__colors">
<div v-for="color in colorCaption" :key="color.color" class="freebusy-caption-item">
<div class="freebusy-caption-item__color" :style="{ 'background-color': color.color }" />
<div class="freebusy-caption-item__label">
{{ color.label }}
</div>
</div>
</div>
</div>
</template>
</NcPopover>
</div>
</div>
<FullCalendar ref="freeBusyFullCalendar"
:options="options" />
<div class="modal__content__footer">
<div class="modal__content__footer__title">
<h3>
{{ formattedcurrentStart }}
</h3>
<p>{{ formattedCurrentTime }}<span class="modal__content__footer__title__timezone">{{ formattedTimeZoen }}</span></p>
</div>
<NcButton type="primary"
@click="save">
{{ $t('calendar', 'Done') }}
<template #icon>
<CheckIcon :size="20" />
</template>
</NcButton>
</div>
</div>
<DatePicker ref="datePicker"
:date="currentDate"
:is-all-day="true"
@change="setCurrentDate" />
</Modal>
</template>
@ -53,7 +123,9 @@
// Import FullCalendar itself
import FullCalendar from '@fullcalendar/vue'
import resourceTimelinePlugin from '@fullcalendar/resource-timeline'
import interactionPlugin from '@fullcalendar/interaction'
import { NcDateTimePicker, NcButton, NcPopover, NcUserBubble, NcModal as Modal } from '@nextcloud/vue'
// Import event sources
import freeBusyBlockedForAllEventSource from '../../../fullcalendar/eventSources/freeBusyBlockedForAllEventSource.js'
import freeBusyFakeBlockingEventSource from '../../../fullcalendar/eventSources/freeBusyFakeBlockingEventSource.js'
@ -71,16 +143,29 @@ import {
mapGetters,
mapState,
} from 'vuex'
import { NcModal as Modal } from '@nextcloud/vue'
import DatePicker from '../../Shared/DatePicker.vue'
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
import ChevronLeftIcon from 'vue-material-design-icons/ChevronLeft.vue'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import HelpCircleIcon from 'vue-material-design-icons/HelpCircle.vue'
import InviteesListSearch from '../Invitees/InviteesListSearch.vue'
import { getColorForFBType } from '../../../utils/freebusy.js'
export default {
name: 'FreeBusy',
components: {
FullCalendar,
DatePicker,
InviteesListSearch,
NcDateTimePicker,
Modal,
NcButton,
NcPopover,
NcUserBubble,
ChevronRightIcon,
ChevronLeftIcon,
CheckIcon,
HelpCircleIcon,
},
props: {
/**
@ -113,13 +198,30 @@ export default {
type: Date,
required: true,
},
eventTitle: {
type: String,
required: true,
},
alreadyInvitedEmails: {
type: Array,
required: true,
},
},
data() {
return {
loadingIndicator: true,
currentDate: this.startDate,
currentStart: this.startDate,
currentEnd: this.endDate,
lang: getFullCalendarLocale().locale,
formattingOptions: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
}
},
mounted() {
const calendar = this.$refs.freeBusyFullCalendar.getApi()
calendar.scrollToTime(this.scrollTime)
},
computed: {
...mapGetters({
timezoneId: 'getResolvedTimezone',
@ -139,8 +241,28 @@ export default {
resourceTimelinePlugin,
momentPluginFactory(this.$store),
VTimezoneNamedTimezone,
interactionPlugin,
]
},
formattedcurrentStart() {
return this.currentStart.toLocaleDateString(this.lang, this.formattingOptions)
},
formattedCurrentTime() {
const options = { hour: '2-digit', minute: '2-digit', hour12: true }
const startTime = this.currentStart.toLocaleTimeString(this.lang, options)
const endTime = this.currentEnd.toLocaleTimeString(this.lang, options)
return `${startTime} - ${endTime} `
},
scrollTime() {
const options = { hour: '2-digit', minute: '2-digit', seconds: '2-digit', hour12: false }
return this.currentDate.getHours() > 0 ? new Date(this.currentDate.getTime() - 60 * 60 * 1000).toLocaleTimeString(this.lang, options) : '10:00:00'
},
formattedTimeZoen() {
return this.timezoneId.replace('/', '-')
},
eventSources() {
return [
freeBusyResourceEventSource(
@ -151,8 +273,8 @@ export default {
freeBusyFakeBlockingEventSource(
this._uid,
this.resources,
this.startDate,
this.endDate
this.currentStart,
this.currentEnd
),
freeBusyBlockedForAllEventSource(
this.organizer.attendeeProperty,
@ -163,13 +285,23 @@ export default {
},
resources() {
const resources = []
const roles = {
CHAIR: this.$t('calendar', 'chairperson'),
'REQ-PARTICIPANT': this.$t('calendar', 'required participant'),
'NON-PARTICIPANT': this.$t('calendar', 'non-participant'),
'OPT-PARTICIPANT': this.$t('calendar', 'optional participant'),
}
for (const attendee of [this.organizer, ...this.attendees]) {
let title = attendee.commonName || attendee.uri.slice(7)
if (attendee === this.organizer) {
title = this.$t('calendar', '{organizer} (organizer)', {
organizer: title,
})
} else {
title = this.$t('calendar', '{attendee} ({role})', {
attendee: title,
role: roles[attendee.role],
})
}
resources.push({
@ -224,22 +356,25 @@ export default {
// Data
eventSources: this.eventSources,
resources: this.resources,
// Events
datesSet: function({ start }) {
// Keep the current date in sync
this.setCurrentDate(start, true)
}.bind(this),
// Plugins
plugins: this.plugins,
// Interaction:
editable: false,
selectable: false,
selectable: true,
select: this.handleSelect,
// Localization:
...getDateFormattingConfig(),
...getFullCalendarLocale(),
// Rendering
height: 'auto',
loading: this.loading,
headerToolbar: false,
resourceAreaColumns: [
{
field: 'title',
headerContent: 'Attendees',
},
],
// Timezones:
timeZone: this.timezoneId,
// Formatting of the title
@ -254,39 +389,101 @@ export default {
}
},
},
mounted() {
// Move file picker into the right header menu
// TODO: make this a slot once fullcalendar support it
// ref https://github.com/fullcalendar/fullcalendar-vue/issues/14
// ref https://github.com/fullcalendar/fullcalendar-vue/issues/126
const picker = this.$refs.datePicker
// Remove from original position
picker.$el.parentNode.removeChild(picker.$el)
// Insert into calendar
this.$el.querySelector('.fc-toolbar-chunk:last-child').appendChild(picker.$el)
},
methods: {
handleSelect(arg) {
this.currentStart = arg.start
this.currentEnd = arg.end
},
save() {
this.$emit('update-dates', { start: this.currentStart, end: this.currentEnd })
},
addAttendee(attendee) {
this.$emit('add-attendee', attendee)
},
removeAttendee(attendee) {
this.$emit('remove-attendee', attendee)
},
loading(isLoading) {
this.loadingIndicator = isLoading
},
setCurrentDate(date, updatedViaCalendar) {
this.currentDate = date
if (!updatedViaCalendar) {
const calendar = this.$refs.freeBusyFullCalendar.getApi()
handleActions(action, date = null) {
const calendar = this.$refs.freeBusyFullCalendar.getApi()
switch (action) {
case 'today':
calendar.today()
break
case 'left':
calendar.prev()
break
case 'right':
calendar.next()
break
case 'picker':
calendar.gotoDate(date)
break
}
this.currentDate = calendar.getDate()
calendar.scrollToTime(this.scrollTime)
},
},
}
</script>
<style lang='scss' scoped>
.icon-close {
display: block;
height: 100%;
}
.modal__content {
padding: 50px;
//when the calendar is open, it's cut at the bottom, adding a margin fixes it
margin-bottom: 95px;
&__actions{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
&__select{
width: 260px;
}
&__date{
display: flex;
justify-content: space-between;
align-items: center;
& > *{
margin-left: 5px;
}
}
}
&__header{
h3{
font-weight: 500;
}
margin-bottom: 20px;
&__attendees{
&__user-bubble{
margin-right: 5px;
}
}
}
&__footer{
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
&__title{
h3{
font-weight: 500;
}
&__timezone{
color: var(--color-text-lighter);
}
}
}
}
:deep(.vs__search ) {
text-overflow: ellipsis;
}
::v-deep .mx-input{
height: 38px !important;
}

View File

@ -74,6 +74,11 @@
:organizer="calendarObjectInstance.organizer"
:start-date="calendarObjectInstance.startDate"
:end-date="calendarObjectInstance.endDate"
:event-title="calendarObjectInstance.title"
:already-invited-emails="alreadyInvitedEmails"
@remove-attendee="removeAttendee"
@add-attendee="addAttendee"
@update-dates="saveNewDate"
@close="closeFreeBusy" />
</div>
</div>
@ -331,6 +336,10 @@ export default {
closeFreeBusy() {
this.showFreeBusyModel = false
},
saveNewDate(dates) {
this.$emit('update-dates', dates)
this.showFreeBusyModel = false
},
async createTalkRoom() {
const NEW_LINE = '\r\n'
try {

View File

@ -430,6 +430,7 @@ const mutations = {
* @param {string=} data.language Preferred language of the attendee
* @param {string=} data.timezoneId Preferred timezone of the attendee
* @param {object=} data.organizer Principal of the organizer to be set if not present
* @param data.member
*/
addAttendee(state, { calendarObjectInstance, commonName, uri, calendarUserType = null, participationStatus = null, role = null, rsvp = null, language = null, timezoneId = null, organizer = null, member = null }) {
const attendee = AttendeeProperty.fromNameAndEMail(commonName, uri)

View File

@ -37,16 +37,16 @@ export function getColorForFBType(type = 'BUSY') {
return 'rgba(255,255,255,0)'
case 'BUSY-TENTATIVE':
return 'rgb(221,203,85)'
return 'rgba(184,129,0,0.3)'
case 'BUSY':
return 'rgb(201,136,121)'
return 'rgba(217,24,18,0.3)'
case 'BUSY-UNAVAILABLE':
return 'rgb(182,70,157)'
return 'rgba(219,219,219)'
default:
return 'rgb(0,130,201)'
return 'rgba(0,113,173,0.3)'
}
}

View File

@ -243,7 +243,8 @@
<InviteesList v-if="!isLoading"
:calendar-object-instance="calendarObjectInstance"
:is-read-only="isReadOnly"
:is-shared-with-me="isSharedWithMe" />
:is-shared-with-me="isSharedWithMe"
@update-dates="updateDates" />
</div>
<SaveButtons v-if="showSaveButtons"
class="app-sidebar-tab__buttons"
@ -427,6 +428,15 @@ export default {
window.removeEventListener('keydown', this.keyboardDuplicateEvent)
},
methods: {
/**
* Update the start and end date of this event
*
* @param {object} dates The new start and end date
*/
updateDates(dates) {
this.updateStartDate(dates.start)
this.updateEndDate(dates.end)
},
/**
* Updates the access-class of this event
*