mirror of https://github.com/nextcloud/calendar
Improve free/busy UI
Signed-off-by: hamza mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
parent
21e0c08712
commit
ccaca16184
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue