Skip to content

Commit

Permalink
Merge pull request #2918 from nextcloud/enhancement/free-busy-colors
Browse files Browse the repository at this point in the history
Improve free/busy colors
  • Loading branch information
ChristophWurst committed Mar 11, 2021
2 parents 40d0e05 + e0ceafc commit 8ec8fb1
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 15 deletions.
10 changes: 5 additions & 5 deletions css/freebusy.scss
Expand Up @@ -30,19 +30,19 @@
.blocking-event-free-busy {
border-color: red;
border-style: solid;
border-left-width: 1px;
border-right-width: 1px;
border-left-width: 2px;
border-right-width: 2px;
background-color: transparent !important;
opacity: 1.0 !important;
opacity: 0.7 !important;
z-index: 2;
}

.blocking-event-free-busy.blocking-event-free-busy--first-row {
border-top-width: 1px;
border-top-width: 2px;
}

.blocking-event-free-busy.blocking-event-free-busy--last-row {
border-bottom-width: 1px;
border-bottom-width: 2px;
}

.loading-indicator {
Expand Down
20 changes: 18 additions & 2 deletions src/components/Editor/FreeBusy/FreeBusy.vue
Expand Up @@ -53,8 +53,9 @@ import FullCalendar from '@fullcalendar/vue'
import resourceTimelinePlugin from '@fullcalendar/resource-timeline'
// Import event sources
import freeBusyEventSource from '../../../fullcalendar/eventSources/freeBusyEventSource.js'
import freeBusyBlockedForAllEventSource from '../../../fullcalendar/eventSources/freeBusyBlockedForAllEventSource.js'
import freeBusyFakeBlockingEventSource from '../../../fullcalendar/eventSources/freeBusyFakeBlockingEventSource.js'
import freeBusyResourceEventSource from '../../../fullcalendar/eventSources/freeBusyResourceEventSource.js'
// Import localization plugins
import { getDateFormattingConfig } from '../../../fullcalendar/localization/dateFormattingConfig.js'
Expand Down Expand Up @@ -139,7 +140,7 @@ export default {
},
eventSources() {
return [
freeBusyEventSource(
freeBusyResourceEventSource(
this._uid,
this.organizer.attendeeProperty,
this.attendees.map((a) => a.attendeeProperty)
Expand All @@ -150,6 +151,11 @@ export default {
this.startDate,
this.endDate
),
freeBusyBlockedForAllEventSource(
this.organizer.attendeeProperty,
this.attendees.map((a) => a.attendeeProperty),
this.resources
),
]
},
resources() {
Expand Down Expand Up @@ -244,3 +250,13 @@ export default {
padding: 50px;
}
</style>

<style lang="scss">
.blocking-event-free-busy {
// Show the blocking event above any other blocks, especially the *blocked for all* one
z-index: 3 !important;
}
.free-busy-block {
opacity: 0.7 !important;
}
</style>
182 changes: 182 additions & 0 deletions src/fullcalendar/eventSources/freeBusyBlockedForAllEventSource.js
@@ -0,0 +1,182 @@
/*
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*/
import getTimezoneManager from '../../services/timezoneDataProviderService.js'
import { createFreeBusyRequest, getParserManager } from 'calendar-js'
import DateTimeValue from 'calendar-js/src/values/dateTimeValue.js'
import { findSchedulingOutbox } from '../../services/caldavService.js'
import logger from '../../utils/logger.js'
// import AttendeeProperty from 'calendar-js/src/properties/attendeeProperty.js'

/**
* Returns an event source for free-busy
*
* @param {AttendeeProperty} organizer The organizer of the event
* @param {AttendeeProperty[]} attendees Array of the event's attendees
* @param {String[]} resources List of resources
* @returns {{startEditable: boolean, resourceEditable: boolean, editable: boolean, id: string, durationEditable: boolean, events: events}}
*/
export default function(organizer, attendees, resources) {
const resourceIds = resources.map((resource) => resource.id)

return {
id: 'free-busy-free-for-all',
editable: false,
startEditable: false,
durationEditable: false,
resourceEditable: false,
events: async({
start,
end,
timeZone,
}, successCallback, failureCallback) => {
console.debug('freeBusyBlockedForAllEventSource', start, end, timeZone)

let timezoneObject = getTimezoneManager().getTimezoneForId(timeZone)
if (!timezoneObject) {
timezoneObject = getTimezoneManager().getTimezoneForId('UTC')
logger.error(`FreeBusyEventSource: Timezone ${timeZone} not found, falling back to UTC.`)
}

const startDateTime = DateTimeValue.fromJSDate(start, true)
const endDateTime = DateTimeValue.fromJSDate(end, true)

// const organizerAsAttendee = new AttendeeProperty('ATTENDEE', organizer.email)
const freeBusyComponent = createFreeBusyRequest(startDateTime, endDateTime, organizer, attendees)
const freeBusyICS = freeBusyComponent.toICS()

let outbox
try {
outbox = await findSchedulingOutbox()
} catch (error) {
failureCallback(error)
return
}

let freeBusyData
try {
freeBusyData = await outbox.freeBusyRequest(freeBusyICS)
} catch (error) {
failureCallback(error)
return
}

const slots = []
for (const [, data] of Object.entries(freeBusyData)) {
if (!data.success) {
continue
}

const parserManager = getParserManager()
const parser = parserManager.getParserForFileType('text/calendar')
parser.parse(data.calendarData)

// TODO: fix me upstream, parser only exports VEVENT, VJOURNAL and VTODO at the moment
const calendarComponent = parser._calendarComponent
const freeBusyComponent = calendarComponent.getFirstComponent('VFREEBUSY')
if (!freeBusyComponent) {
continue
}

for (const freeBusyProperty of freeBusyComponent.getPropertyIterator('FREEBUSY')) {
if (freeBusyProperty.type === 'FREE') {
// We care about anything BUT free slots
continue
}

slots.push({
start: freeBusyProperty.getFirstValue().start.getInTimezone(timezoneObject).jsDate,
end: freeBusyProperty.getFirstValue().end.getInTimezone(timezoneObject).jsDate,
})
}
}

// Now that we have all the busy slots we try to combine them to iron
// out any overlaps between them.
// The algorithm below will sort the slots by their start time ane then
// iteratively collapse anything that starts and stops within the same
// time. The complexity of this algorithms is n^2, but assuming the
// number of attendees of an event is relatively low, this should be
// fine to calculate.
slots.sort((a, b) => a.start - b.start)
const slotsWithoutOverlap = []
if (slots.length) {
let currentSlotStart = slots[0].start
slots.forEach(() => {
const combined = findNextCombinedSlot(slots, currentSlotStart)
if (combined) {
slotsWithoutOverlap.push(combined)
currentSlotStart = combined.end
}
})
}
console.debug('deduplicated slots', slots, slotsWithoutOverlap)

const events = slotsWithoutOverlap.map(slot => {
return {
groupId: 'free-busy-blocked-for-all',
start: slot.start.toISOString(),
end: slot.end.toISOString(),
resourceIds: resourceIds,
display: 'background',
allDay: false,
backgroundColor: 'var(--color-text-maxcontrast)',
borderColor: 'var(--color-text-maxcontrast)',
}
})

console.debug('freeBusyBlockedForAllEventSource', slots, events)

successCallback(events)
},
}
}

function findNextCombinedSlot(slots, start) {
const slot = slots
.filter(slot => slot.start > start)
.reduce((combined, slot) => {
if (slot.start < combined.start) {
// This slot starts too early
return combined
}

if (slot.end <= combined.end) {
// This slots starts and ends within the combined one
return combined
}

// The slot is extended
return {
start: combined.start,
end: slot.end,
}
}, {
start,
end: start,
})

if (slot.start === slot.end) {
// Empty -> no slot
return undefined
}

return slot
}
Expand Up @@ -23,7 +23,7 @@ import getTimezoneManager from '../../services/timezoneDataProviderService.js'
import { createFreeBusyRequest } from 'calendar-js'
import DateTimeValue from 'calendar-js/src/values/dateTimeValue.js'
import { findSchedulingOutbox } from '../../services/caldavService.js'
import freeBusyEventSourceFunction from './freeBusyEventSourceFunction.js'
import freeBusyResourceEventSourceFunction from './freeBusyResourceEventSourceFunction.js'
import logger from '../../utils/logger.js'
// import AttendeeProperty from 'calendar-js/src/properties/attendeeProperty.js'

Expand Down Expand Up @@ -75,7 +75,7 @@ export default function(id, organizer, attendees) {
}
const events = []
for (const [uri, data] of Object.entries(freeBusyData)) {
events.push(...freeBusyEventSourceFunction(uri, data.calendarData, data.success, startDateTime, endDateTime, timezoneObject))
events.push(...freeBusyResourceEventSourceFunction(uri, data.calendarData, data.success, startDateTime, endDateTime, timezoneObject))
}

console.debug(events)
Expand Down
Expand Up @@ -68,6 +68,10 @@ export default function(uri, calendarData, success, start, end, timezone) {
end: freeBusyProperty.getFirstValue().end.getInTimezone(timezone).jsDate.toISOString(),
resourceId: uri,
display: 'background',
classNames: [
'free-busy-block',
'free-busy-' + freeBusyProperty.type.toLowerCase(),
],
backgroundColor: getColorForFBType(freeBusyProperty.type),
})
}
Expand Down
10 changes: 5 additions & 5 deletions src/utils/freebusy.js
Expand Up @@ -29,18 +29,18 @@
export function getColorForFBType(type = 'BUSY') {
switch (type) {
case 'FREE':
return '#55B85F'
return 'rgb(110,166,143)'

case 'BUSY-TENTATIVE':
return '#4C81FF'
return 'rgb(221,203,85)'

case 'BUSY':
return '#273A7F'
return 'rgb(201,136,121)'

case 'BUSY-UNAVAILABLE':
return '#50347F'
return 'rgb(182,70,157)'

default:
return '#DA9CBD'
return 'rgb(0,130,201)'
}
}
Expand Up @@ -32,7 +32,7 @@ jest.mock('@nextcloud/l10n')
jest.mock('../../../../../src/utils/color.js')
jest.mock("../../../../../src/utils/calendarObject.js")

describe('fullcalendar/eventSourceFunction test suite', () => {
describe('fullcalendar/freeBusyResourceEventSourceFunction test suite', () => {

beforeEach(() => {
translate.mockClear()
Expand Down

0 comments on commit 8ec8fb1

Please sign in to comment.