mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Merge pull request #3851 from modoboa/refacto/vuetify_based_calendar
Replaced FullCalendar by Vuetify internal calendar
This commit is contained in:
@@ -4,7 +4,7 @@ LABEL org.opencontainers.image.authors="Antoine Nguyen <tonio@ngyn.org>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install Radicale pytz radicale-modoboa-auth-oauth2
|
||||
RUN pip install -U Radicale pytz radicale-modoboa-auth-oauth2
|
||||
|
||||
EXPOSE 5232
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ type = none
|
||||
|
||||
level = debug
|
||||
|
||||
request_content_on_debug = true
|
||||
|
||||
[headers]
|
||||
|
||||
# Additional HTTP headers
|
||||
|
||||
@@ -15,11 +15,6 @@
|
||||
"gettext:compile": "vue-gettext-compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@mdi/font": "^7.3.67",
|
||||
"@tiptap/extension-highlight": "^3.11.0",
|
||||
"@tiptap/extension-text-align": "^3.11.0",
|
||||
@@ -41,7 +36,7 @@
|
||||
"vue-router": "^4.6.3",
|
||||
"vue3-apexcharts": "^1.10.0",
|
||||
"vue3-gettext": "^4.0.0-beta.1",
|
||||
"vuetify": "^3.10.11"
|
||||
"vuetify": "^3.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-basic-ssl": "^1.1.0",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
:error-messages="formErrors.start"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
type="datetime-local"
|
||||
:type="dateType"
|
||||
class="mr-4"
|
||||
/>
|
||||
<v-text-field
|
||||
@@ -36,7 +36,7 @@
|
||||
:error-messages="formErrors.end"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
type="datetime-local"
|
||||
:type="dateType"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex mb-3">
|
||||
@@ -133,14 +133,27 @@ const title = computed(() => {
|
||||
return props.event ? $gettext('Edit event') : $gettext('New event')
|
||||
})
|
||||
|
||||
const dateType = computed(() => {
|
||||
return form.value.allDay ? 'date' : 'datetime-local'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.info,
|
||||
(value) => {
|
||||
if (value) {
|
||||
const start =
|
||||
typeof value.start === 'number'
|
||||
? DateTime.fromMillis(value.start)
|
||||
: DateTime.fromJSDate(value.start)
|
||||
const end =
|
||||
typeof value.end === 'number'
|
||||
? DateTime.fromMillis(value.end)
|
||||
: DateTime.fromJSDate(value.end)
|
||||
const format = value.timed ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'
|
||||
form.value = {
|
||||
start: DateTime.fromISO(value.startStr).toFormat('yyyy-MM-dd HH:mm'),
|
||||
end: DateTime.fromISO(value.endStr).toFormat('yyyy-MM-dd HH:mm'),
|
||||
allDay: value.allDay,
|
||||
start: start.toFormat(format),
|
||||
end: end.toFormat(format),
|
||||
allDay: !value.timed,
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -153,10 +166,9 @@ watch(
|
||||
if (value) {
|
||||
form.value = { ...value }
|
||||
form.value.calendar = form.value.calendar.pk
|
||||
form.value.start = DateTime.fromISO(value.start).toFormat(
|
||||
'yyyy-MM-dd HH:mm'
|
||||
)
|
||||
form.value.end = DateTime.fromISO(value.end).toFormat('yyyy-MM-dd HH:mm')
|
||||
const format = form.value.timed ? 'yyyy-MM-dd HH:mm' : 'yyyy-MM-dd'
|
||||
form.value.start = DateTime.fromMillis(value.start).toFormat(format)
|
||||
form.value.end = DateTime.fromMillis(value.end).toFormat(format)
|
||||
api.getAttendees().then((resp) => {
|
||||
attendees.value = resp.data
|
||||
})
|
||||
@@ -176,11 +188,21 @@ async function saveEvent() {
|
||||
}
|
||||
working.value = true
|
||||
try {
|
||||
var data = JSON.parse(JSON.stringify(form.value))
|
||||
const fromDate = new Date(data.start)
|
||||
data.start = fromDate.toISOString()
|
||||
const toDate = new Date(data.end)
|
||||
data.end = toDate.toISOString()
|
||||
const data = {
|
||||
title: form.value.title,
|
||||
description: form.value.description,
|
||||
allDay: form.value.allDay,
|
||||
attendees: form.value.attendees,
|
||||
calendar: form.value.calendar,
|
||||
}
|
||||
|
||||
if (data.allDay) {
|
||||
data.start_date = form.value.start
|
||||
data.end_date = form.value.end
|
||||
} else {
|
||||
data.start = new Date(form.value.start).toISOString()
|
||||
data.end = new Date(form.value.end).toISOString()
|
||||
}
|
||||
if (!props.event) {
|
||||
await api.createUserEvent(form.value.calendar, data)
|
||||
busStore.displayNotification({ msg: $gettext('Event added') })
|
||||
|
||||
@@ -1,43 +1,130 @@
|
||||
<template>
|
||||
<div class="text-h5 ml-4 mb-6">
|
||||
{{ $gettext('Calendar') }}
|
||||
</div>
|
||||
<FullCalendar ref="calendarRef" :options="calendarOptions" />
|
||||
<div style="display: grid; grid-template-rows: auto auto 1fr; height: 96vh">
|
||||
<div class="text-h5 ml-4 mb-6">
|
||||
{{ $gettext('Calendar') }}
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showInformation" persistent max-width="800px">
|
||||
<CalendarDetail :calendar="selectedCalendar" @close="closeInformation" />
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showCalendarForm" persistent max-width="800px">
|
||||
<CalendarForm
|
||||
:initial-calendar="selectedCalendar"
|
||||
@color-changed="refreshCalendarEvents"
|
||||
@close="closeCalendarForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showEventForm" persistent max-width="800px">
|
||||
<EventForm
|
||||
:info="selectInfo"
|
||||
:event="selectedEvent"
|
||||
@refresh-calendar="refreshCalendarEvents"
|
||||
@close="closeEventForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showAccessRulesForm" persistent max-width="800px">
|
||||
<CalendarAccessRulesForm
|
||||
v-if="selectedCalendar"
|
||||
:calendar-pk="selectedCalendar.pk"
|
||||
@close="closeAccessRulesForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showImportEventsForm" persistent max-width="800px">
|
||||
<ImportEventsForm
|
||||
v-if="selectedCalendar"
|
||||
:calendar="selectedCalendar"
|
||||
@events-imported="refreshCalendarEvents"
|
||||
@close="closeImportEventsForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<ConfirmDialog ref="confirm" />
|
||||
<v-toolbar v-if="calendarRef" color="white" flat>
|
||||
<v-btn
|
||||
class="me-4"
|
||||
color="grey-darken-2"
|
||||
variant="outlined"
|
||||
@click="setToday"
|
||||
>
|
||||
{{ $gettext('Today') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="grey-darken-2"
|
||||
size="small"
|
||||
variant="text"
|
||||
icon
|
||||
@click="goPrev"
|
||||
>
|
||||
<v-icon size="small"> mdi-chevron-left </v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="grey-darken-2"
|
||||
size="small"
|
||||
variant="text"
|
||||
icon
|
||||
@click="goNext"
|
||||
>
|
||||
<v-icon size="small"> mdi-chevron-right </v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>
|
||||
{{ calendarRef.title }}
|
||||
</v-toolbar-title>
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn color="grey-darken-2" variant="outlined" v-bind="props">
|
||||
<span>{{ typeToLabel[ctype] }}</span>
|
||||
<v-icon end> mdi-menu-down </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item @click="ctype = 'day'">
|
||||
<v-list-item-title>{{ $gettext('Day') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="ctype = 'week'">
|
||||
<v-list-item-title>{{ $gettext('Week') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="ctype = 'month'">
|
||||
<v-list-item-title>{{ $gettext('Month') }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar>
|
||||
|
||||
<div style="overflow: auto">
|
||||
<v-calendar
|
||||
ref="calendarRef"
|
||||
v-model="focus"
|
||||
:events="events"
|
||||
first-day-of-week="1"
|
||||
:type="ctype"
|
||||
:locale="current"
|
||||
event-name="title"
|
||||
:event-color="getEventColor"
|
||||
:event-ripple="false"
|
||||
@mousedown:event="startDrag"
|
||||
@mousedown:time="startTime"
|
||||
@mousedown:day="startDay"
|
||||
@click:day="dayClickCallback"
|
||||
@mouseleave="cancelDrag"
|
||||
@mousemove:time="mouseMove"
|
||||
@mousemove:day="mouseMoveDay"
|
||||
@mouseup:time="endDrag"
|
||||
@mouseup:day="endDragDay"
|
||||
@change="fetchUserEvents"
|
||||
>
|
||||
<template #event="{ event, timed, eventSummary }">
|
||||
<div class="v-event-draggable">
|
||||
<component :is="eventSummary"></component>
|
||||
</div>
|
||||
<div
|
||||
v-if="timed"
|
||||
class="v-event-drag-bottom"
|
||||
@mousedown.stop="extendBottom(event)"
|
||||
></div>
|
||||
</template>
|
||||
</v-calendar>
|
||||
</div>
|
||||
|
||||
<v-dialog v-model="showInformation" persistent max-width="800px">
|
||||
<CalendarDetail :calendar="selectedCalendar" @close="closeInformation" />
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showCalendarForm" persistent max-width="800px">
|
||||
<CalendarForm
|
||||
:initial-calendar="selectedCalendar"
|
||||
@color-changed="refreshCalendarEvents"
|
||||
@close="closeCalendarForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showEventForm" persistent max-width="800px">
|
||||
<EventForm
|
||||
:info="selectInfo"
|
||||
:event="selectedEvent"
|
||||
@refresh-calendar="refreshCalendarEvents"
|
||||
@close="closeEventForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showAccessRulesForm" persistent max-width="800px">
|
||||
<CalendarAccessRulesForm
|
||||
v-if="selectedCalendar"
|
||||
:calendar-pk="selectedCalendar.pk"
|
||||
@close="closeAccessRulesForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-dialog v-model="showImportEventsForm" persistent max-width="800px">
|
||||
<ImportEventsForm
|
||||
v-if="selectedCalendar"
|
||||
:calendar="selectedCalendar"
|
||||
@events-imported="refreshCalendarEvents"
|
||||
@close="closeImportEventsForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
<ConfirmDialog ref="confirm" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -53,10 +140,6 @@ import CalendarDetail from '@/components/calendars/CalendarDetail'
|
||||
import ConfirmDialog from '@/components/tools/ConfirmDialog'
|
||||
import EventForm from '@/components/calendars/EventForm'
|
||||
import ImportEventsForm from '@/components/calendars/ImportEventsForm'
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||
import interactionPlugin from '@fullcalendar/interaction'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
|
||||
const { $gettext, current } = useGettext()
|
||||
const layoutStore = useLayoutStore()
|
||||
@@ -64,6 +147,7 @@ const busStore = useBusStore()
|
||||
|
||||
const calendarRef = ref()
|
||||
const confirm = ref()
|
||||
const events = ref([])
|
||||
const selectInfo = ref()
|
||||
const selectedCalendar = ref(null)
|
||||
const selectedEvent = ref(null)
|
||||
@@ -73,6 +157,22 @@ const showCalendarForm = ref(false)
|
||||
const showInformation = ref(false)
|
||||
const showEventForm = ref(false)
|
||||
const showImportEventsForm = ref(false)
|
||||
const ctype = ref('week')
|
||||
const focus = ref('')
|
||||
const dragEvent = ref(null)
|
||||
const dragTime = ref(null)
|
||||
const dragDay = ref(null)
|
||||
const createEvent = ref(null)
|
||||
const createStart = ref(null)
|
||||
const extendOriginal = ref(null)
|
||||
const updating = ref(false)
|
||||
const moving = ref(false)
|
||||
|
||||
const typeToLabel = {
|
||||
day: $gettext('Day'),
|
||||
week: $gettext('Week'),
|
||||
month: $gettext('Month'),
|
||||
}
|
||||
|
||||
const leftMenuItems = computed(() => {
|
||||
const result = [
|
||||
@@ -121,21 +221,36 @@ const leftMenuItems = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
async function fetchUserCalendarEvents(
|
||||
calendarPk,
|
||||
info,
|
||||
successCallback,
|
||||
failureCallback
|
||||
) {
|
||||
try {
|
||||
const resp = await api.getUserCalendarEvents(calendarPk, {
|
||||
start: info.startStr,
|
||||
end: info.endStr,
|
||||
async function fetchUserEvents({ start, end }) {
|
||||
let newEvents = []
|
||||
for (const calendar of userCalendars.value) {
|
||||
const resp = await api.getUserCalendarEvents(calendar.pk, {
|
||||
start: start.date,
|
||||
end: end.date,
|
||||
})
|
||||
successCallback(resp.data)
|
||||
} catch (err) {
|
||||
failureCallback(err)
|
||||
newEvents = newEvents.concat(
|
||||
resp.data.map((event) => {
|
||||
const newEvent = { ...event }
|
||||
newEvent.start = Date.parse(event.start)
|
||||
newEvent.end = Date.parse(event.end)
|
||||
newEvent.timed = !event.allDay
|
||||
return newEvent
|
||||
})
|
||||
)
|
||||
}
|
||||
events.value = newEvents
|
||||
}
|
||||
|
||||
const setToday = () => {
|
||||
focus.value = ''
|
||||
}
|
||||
|
||||
const goPrev = () => {
|
||||
calendarRef.value.prev()
|
||||
}
|
||||
|
||||
const goNext = () => {
|
||||
calendarRef.value.next()
|
||||
}
|
||||
|
||||
function openCalendarForm() {
|
||||
@@ -178,58 +293,231 @@ function closeImportEventsForm() {
|
||||
showImportEventsForm.value = false
|
||||
}
|
||||
|
||||
function fetchUserCalendars() {
|
||||
api.getUserCalendars().then((resp) => {
|
||||
userCalendars.value = resp.data
|
||||
async function fetchUserCalendars() {
|
||||
const resp = await api.getUserCalendars()
|
||||
userCalendars.value = resp.data
|
||||
}
|
||||
|
||||
function refreshCalendarEvents() {
|
||||
fetchUserEvents({
|
||||
start: calendarRef.value.renderProps.start,
|
||||
end: calendarRef.value.renderProps.end,
|
||||
})
|
||||
}
|
||||
|
||||
function refreshCalendarEvents(calendarPk) {
|
||||
const fullc = calendarRef.value.getApi()
|
||||
const evtSource = fullc.getEventSourceById(`user-${calendarPk}`)
|
||||
evtSource.refetch()
|
||||
}
|
||||
|
||||
function selectCallback(info) {
|
||||
selectInfo.value = info
|
||||
showEventForm.value = true
|
||||
}
|
||||
|
||||
async function eventClickCallback(info) {
|
||||
const resp = await api.getUserEvent(
|
||||
info.event.extendedProps.calendar.pk,
|
||||
info.event.id
|
||||
)
|
||||
selectedEvent.value = resp.data
|
||||
showEventForm.value = true
|
||||
}
|
||||
|
||||
function updateEventDates(calEvent) {
|
||||
var data = {
|
||||
start: calEvent.start,
|
||||
end: calEvent.end,
|
||||
const dayClickCallback = (nativeEvent, { date }) => {
|
||||
if (!selectedEvent.value) {
|
||||
const startDate = new Date(date)
|
||||
const endDate = new Date(date)
|
||||
selectInfo.value = {
|
||||
color: '#2196F3',
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
timed: false,
|
||||
}
|
||||
showEventForm.value = true
|
||||
}
|
||||
const evtCalendar = calEvent.extendedProps.calendar
|
||||
if (calEvent.allDay) {
|
||||
const end = DateTime.fromJSDate(data.start)
|
||||
end.plus({ days: 1 })
|
||||
data.start.setHours(0, 0, 0)
|
||||
data.end = end
|
||||
}
|
||||
|
||||
async function updateEventDates(calEvent) {
|
||||
updating.value = true
|
||||
var data = {
|
||||
start: DateTime.fromMillis(calEvent.start).toJSDate(),
|
||||
end: calEvent.end ? DateTime.fromMillis(calEvent.end).toJSDate() : null,
|
||||
}
|
||||
const evtCalendar = calEvent.calendar
|
||||
if (!calEvent.timed) {
|
||||
data.start_date = data.start.toISOString().split('T')[0]
|
||||
data.end_date = data.end.toISOString().split('T')[0]
|
||||
data.allDay = calEvent.allDay
|
||||
} else if (!calEvent.end) {
|
||||
const end = DateTime.fromJSDate(data.start)
|
||||
end.plus({ hours: 1 })
|
||||
data.end = end
|
||||
}
|
||||
api.patchUserEvent(evtCalendar.pk, calEvent.id, data).then((response) => {
|
||||
try {
|
||||
await api.patchUserEvent(evtCalendar.pk, calEvent.id, data)
|
||||
busStore.displayNotification({ msg: $gettext('Event updated') })
|
||||
})
|
||||
} finally {
|
||||
updating.value = false
|
||||
}
|
||||
}
|
||||
function eventDropCallback(info) {
|
||||
updateEventDates(info.event)
|
||||
|
||||
const roundTime = (time, down = true) => {
|
||||
const roundTo = 15 // minutes
|
||||
const roundDownTime = roundTo * 60 * 1000
|
||||
|
||||
return down
|
||||
? time - (time % roundDownTime)
|
||||
: time + (roundDownTime - (time % roundDownTime))
|
||||
}
|
||||
function eventResizeCallback(info) {
|
||||
updateEventDates(info.event)
|
||||
const toTime = (tms) => {
|
||||
return new Date(
|
||||
tms.year,
|
||||
tms.month - 1,
|
||||
tms.day,
|
||||
tms.hour,
|
||||
tms.minute
|
||||
).getTime()
|
||||
}
|
||||
|
||||
const getEventColor = (event) => {
|
||||
const rgb = parseInt(event.color.substring(1), 16)
|
||||
const r = (rgb >> 16) & 0xff
|
||||
const g = (rgb >> 8) & 0xff
|
||||
const b = (rgb >> 0) & 0xff
|
||||
|
||||
return event === dragEvent.value
|
||||
? `rgba(${r}, ${g}, ${b}, 0.7)`
|
||||
: event === createEvent.value
|
||||
? `rgba(${r}, ${g}, ${b}, 0.7)`
|
||||
: event.color
|
||||
}
|
||||
|
||||
const startDrag = (nativeEvent, { event, timed }) => {
|
||||
if (event) {
|
||||
dragEvent.value = event
|
||||
if (timed) {
|
||||
dragTime.value = null
|
||||
extendOriginal.value = null
|
||||
} else {
|
||||
dragDay.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = (nativeEvent, tms) => {
|
||||
const mouse = toTime(tms)
|
||||
|
||||
if (dragEvent.value && dragTime.value === null) {
|
||||
const start = dragEvent.value.start
|
||||
|
||||
dragTime.value = mouse - start
|
||||
} else {
|
||||
createStart.value = roundTime(mouse)
|
||||
createEvent.value = {
|
||||
title: $gettext('New event'),
|
||||
color: '#2196F3',
|
||||
start: createStart.value,
|
||||
end: createStart.value,
|
||||
timed: true,
|
||||
}
|
||||
|
||||
events.value.push(createEvent.value)
|
||||
}
|
||||
}
|
||||
|
||||
const startDay = (nativeEvent, tms) => {
|
||||
const mouse = toTime(tms)
|
||||
|
||||
if (dragEvent.value && dragDay.value === null) {
|
||||
dragDay.value = mouse - dragEvent.value.start
|
||||
}
|
||||
}
|
||||
|
||||
const mouseMove = (nativeEvent, tms) => {
|
||||
const mouse = toTime(tms)
|
||||
|
||||
if (dragEvent.value && dragTime.value !== null) {
|
||||
moving.value = true
|
||||
const start = dragEvent.value.start
|
||||
const end = dragEvent.value.end
|
||||
const duration = end - start
|
||||
const newStartTime = mouse - dragTime.value
|
||||
const newStart = roundTime(newStartTime)
|
||||
const newEnd = newStart + duration
|
||||
|
||||
dragEvent.value.start = newStart
|
||||
dragEvent.value.end = newEnd
|
||||
} else if (createEvent.value && createStart.value !== null) {
|
||||
moving.value = true
|
||||
const mouseRounded = roundTime(mouse, false)
|
||||
const min = Math.min(mouseRounded, createStart.value)
|
||||
const max = Math.max(mouseRounded, createStart.value)
|
||||
|
||||
createEvent.value.start = min
|
||||
createEvent.value.end = max
|
||||
}
|
||||
}
|
||||
|
||||
const mouseMoveDay = (nativeEvent, tms) => {
|
||||
const mouse = toTime(tms)
|
||||
if (dragEvent.value && dragDay.value !== null) {
|
||||
moving.value = true
|
||||
const start = dragEvent.value.start
|
||||
const end = dragEvent.value.end
|
||||
const duration = end - start
|
||||
const newStartTime = mouse - dragDay.value
|
||||
const newStart = roundTime(newStartTime)
|
||||
const newEnd = newStart + duration
|
||||
|
||||
dragEvent.value.start = newStart
|
||||
dragEvent.value.end = newEnd
|
||||
}
|
||||
}
|
||||
|
||||
const extendBottom = (event) => {
|
||||
createEvent.value = event
|
||||
createStart.value = event.start
|
||||
extendOriginal.value = event.end
|
||||
}
|
||||
|
||||
const endDrag = () => {
|
||||
if (createEvent.value && !extendOriginal.value) {
|
||||
selectInfo.value = createEvent.value
|
||||
showEventForm.value = true
|
||||
} else {
|
||||
if (moving.value) {
|
||||
if (dragEvent.value) {
|
||||
updateEventDates(dragEvent.value)
|
||||
} else if (createEvent.value) {
|
||||
updateEventDates(createEvent.value)
|
||||
}
|
||||
} else {
|
||||
selectedEvent.value = dragEvent.value
|
||||
showEventForm.value = true
|
||||
}
|
||||
|
||||
dragTime.value = null
|
||||
dragEvent.value = null
|
||||
createEvent.value = null
|
||||
createStart.value = null
|
||||
extendOriginal.value = null
|
||||
moving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const endDragDay = () => {
|
||||
if (dragEvent.value) {
|
||||
if (moving.value) {
|
||||
updateEventDates(dragEvent.value)
|
||||
} else {
|
||||
selectedEvent.value = dragEvent.value
|
||||
showEventForm.value = true
|
||||
}
|
||||
}
|
||||
dragEvent.value = null
|
||||
dragDay.value = null
|
||||
moving.value = false
|
||||
}
|
||||
|
||||
const cancelDrag = () => {
|
||||
if (createEvent.value) {
|
||||
if (extendOriginal.value) {
|
||||
createEvent.value.end = extendOriginal.value
|
||||
} else {
|
||||
const i = events.value.indexOf(createEvent.value)
|
||||
if (i !== -1) {
|
||||
events.value.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createEvent.value = null
|
||||
createStart.value = null
|
||||
dragTime.value = null
|
||||
dragEvent.value = null
|
||||
moving.value = false
|
||||
}
|
||||
|
||||
function closeEventForm() {
|
||||
@@ -252,40 +540,12 @@ async function deleteCalendar(calendarPk) {
|
||||
return
|
||||
}
|
||||
await api.deleteUserCalendar(calendarPk)
|
||||
const fullc = calendarRef.value.getApi()
|
||||
const evtSource = fullc.getEventSourceById(`user-${calendarPk}`)
|
||||
evtSource.remove()
|
||||
fetchUserCalendars()
|
||||
await fetchUserCalendars()
|
||||
events.value = []
|
||||
calendarRef.value.checkChange()
|
||||
busStore.displayNotification({ msg: $gettext('Calendar deleted') })
|
||||
}
|
||||
|
||||
const calendarOptions = {
|
||||
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay',
|
||||
},
|
||||
scrollTime: '09:00:00',
|
||||
height: '100%',
|
||||
firstDay: 1,
|
||||
initialView: 'timeGridWeek',
|
||||
locale: current,
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
editable: true,
|
||||
dayMaxEventRows: true,
|
||||
businessHours: {
|
||||
daysOfWeek: [1, 2, 3, 4, 5],
|
||||
startTime: '09:00',
|
||||
endTime: '18:00',
|
||||
},
|
||||
select: selectCallback,
|
||||
eventClick: eventClickCallback,
|
||||
eventDrop: eventDropCallback,
|
||||
eventResize: eventResizeCallback,
|
||||
}
|
||||
|
||||
watch(
|
||||
leftMenuItems,
|
||||
(value) => {
|
||||
@@ -294,23 +554,47 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(userCalendars, (value) => {
|
||||
const fullc = calendarRef.value.getApi()
|
||||
if (!fullc) {
|
||||
return
|
||||
}
|
||||
for (const calendar of value) {
|
||||
const evtSource = fullc.getEventSourceById(`user-${calendar.pk}`)
|
||||
if (!evtSource) {
|
||||
fullc.addEventSource({
|
||||
id: `user-${calendar.pk}`,
|
||||
events: fetchUserCalendarEvents.bind(this, calendar.pk),
|
||||
})
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
calendarRef.value.scrollToTime('07:00')
|
||||
calendarRef.value.checkChange()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchUserCalendars()
|
||||
})
|
||||
await fetchUserCalendars()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-event-draggable {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.v-event-timed {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.v-event-drag-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 4px;
|
||||
height: 4px;
|
||||
cursor: ns-resize;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
height: 4px;
|
||||
border-top: 1px solid white;
|
||||
border-bottom: 1px solid white;
|
||||
width: 16px;
|
||||
margin-left: -8px;
|
||||
opacity: 0.8;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+4
-38
@@ -245,35 +245,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@fullcalendar/core@^6.1.15":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/core/-/core-6.1.19.tgz#60379111ba0cfb9fd9bc6594fee47cf855fc77b2"
|
||||
integrity sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==
|
||||
dependencies:
|
||||
preact "~10.12.1"
|
||||
|
||||
"@fullcalendar/daygrid@^6.1.15", "@fullcalendar/daygrid@~6.1.19":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/daygrid/-/daygrid-6.1.19.tgz#1d70a7f3f4cc13d3f27f55ed6880aae09a1a367b"
|
||||
integrity sha512-IAAfnMICnVWPjpT4zi87i3FEw0xxSza0avqY/HedKEz+l5MTBYvCDPOWDATpzXoLut3aACsjktIyw9thvIcRYQ==
|
||||
|
||||
"@fullcalendar/interaction@^6.1.15":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/interaction/-/interaction-6.1.19.tgz#48d85601a4203ede7cb400323fe5a9c163e28804"
|
||||
integrity sha512-GOciy79xe8JMVp+1evAU3ytdwN/7tv35t5i1vFkifiuWcQMLC/JnLg/RA2s4sYmQwoYhTw/p4GLcP0gO5B3X5w==
|
||||
|
||||
"@fullcalendar/timegrid@^6.1.15":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/timegrid/-/timegrid-6.1.19.tgz#bb4b2e3039c995b61284ee5b76ad9dcc696b71ce"
|
||||
integrity sha512-OuzpUueyO9wB5OZ8rs7TWIoqvu4v3yEqdDxZ2VcsMldCpYJRiOe7yHWKr4ap5Tb0fs7Rjbserc/b6Nt7ol6BRg==
|
||||
dependencies:
|
||||
"@fullcalendar/daygrid" "~6.1.19"
|
||||
|
||||
"@fullcalendar/vue3@^6.1.15":
|
||||
version "6.1.19"
|
||||
resolved "https://registry.yarnpkg.com/@fullcalendar/vue3/-/vue3-6.1.19.tgz#43cecccfaca27679823b98c73ba2e042c60e98cb"
|
||||
integrity sha512-j5eUSxx0xIy3ADljo0f5B9PhjqXnCQ+7nUMPfsslc2eGVjp4F74YvY3dyd6OBbg13IvpsjowkjncGipYMQWmTA==
|
||||
|
||||
"@humanfs/core@^0.19.1":
|
||||
version "0.19.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
|
||||
@@ -1936,11 +1907,6 @@ postcss@^8.5.3, postcss@^8.5.6:
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
preact@~10.12.1:
|
||||
version "10.12.1"
|
||||
resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.1.tgz#8f9cb5442f560e532729b7d23d42fd1161354a21"
|
||||
integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==
|
||||
|
||||
prelude-ls@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
@@ -2449,10 +2415,10 @@ vue@^3.4.31:
|
||||
"@vue/server-renderer" "3.5.24"
|
||||
"@vue/shared" "3.5.24"
|
||||
|
||||
vuetify@^3.10.11:
|
||||
version "3.10.11"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.10.11.tgz#d2da1ffe9ffcd15e9192c31e08cc0469e57e705b"
|
||||
integrity sha512-hfllXT0/C3O5nZyIRalaDU7ClMIrKrKAbjH0T8xbSUb7FcJrHOqPZfEkSXwrKxajv6EA1rwEOvCZoLDhunnjrQ==
|
||||
vuetify@^3.11.0:
|
||||
version "3.11.4"
|
||||
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.11.4.tgz#14e41b7a7b821501039251fdf952cec520e505d9"
|
||||
integrity sha512-614MKpopMzNjkQVD5CS/xr77p4CqtF2wNqQByAi7oTEcKzA+gOsy5O3l97ZIqomNidOSGXyVg5s6t5bNO9p4+Q==
|
||||
|
||||
w3c-keyname@^2.2.0:
|
||||
version "2.2.8"
|
||||
|
||||
@@ -6,6 +6,7 @@ import uuid
|
||||
import caldav
|
||||
from caldav.elements import dav, ical
|
||||
from caldav import Calendar
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import vobject
|
||||
|
||||
from django.utils import timezone
|
||||
@@ -53,8 +54,10 @@ class Caldav_Backend(CalendarBackend):
|
||||
start = datetime.datetime.combine(
|
||||
vevent.dtstart.value, datetime.time.min
|
||||
).replace(tzinfo=tz)
|
||||
# Small back to make vuetify calendar happy. 'All day' events are generally
|
||||
# created from one day to the day after (even for a 1 day duration...)
|
||||
end = datetime.datetime.combine(
|
||||
vevent.dtend.value, datetime.time.min
|
||||
vevent.dtend.value - relativedelta(days=1), datetime.time.min
|
||||
).replace(tzinfo=tz)
|
||||
result.update({"allDay": all_day, "start": start, "end": end})
|
||||
if "attendee" in vevent.contents:
|
||||
@@ -80,17 +83,16 @@ class Caldav_Backend(CalendarBackend):
|
||||
def create_event(self, data):
|
||||
"""Create a new event."""
|
||||
uid = uuid.uuid4()
|
||||
cal = vobject.iCalendar()
|
||||
cal.add("vevent")
|
||||
cal.vevent.add("uid").value = str(uid)
|
||||
cal.vevent.add("summary").value = data["title"]
|
||||
if not data["allDay"]:
|
||||
cal.vevent.add("dtstart").value = data["start"]
|
||||
cal.vevent.add("dtend").value = data["end"]
|
||||
dtstart = data["start"]
|
||||
dtend = data["end"]
|
||||
else:
|
||||
cal.vevent.add("dtstart").value = data["start"].date()
|
||||
cal.vevent.add("dtend").value = data["end"].date()
|
||||
self.remote_cal.add_event(cal)
|
||||
dtstart = data["start_date"]
|
||||
# Ensure consistent behavior with other calendar clients (like TB.)
|
||||
dtend = data["end_date"] + relativedelta(days=1)
|
||||
self.remote_cal.save_event(
|
||||
uid=uid, dtstart=dtstart, dtend=dtend, summary=data["title"]
|
||||
)
|
||||
return uid
|
||||
|
||||
def update_event(self, uid, original_data):
|
||||
@@ -102,8 +104,8 @@ class Caldav_Backend(CalendarBackend):
|
||||
if "title" in data:
|
||||
orig_evt.summary.value = data["title"]
|
||||
if data.get("allDay"):
|
||||
data["start"] = data["start"].date()
|
||||
data["end"] = data["end"].date()
|
||||
data["start"] = data["start_date"]
|
||||
data["end"] = data["end_date"]
|
||||
if "start" in data:
|
||||
del orig_evt.contents["dtstart"]
|
||||
orig_evt.add("dtstart").value = data["start"]
|
||||
@@ -141,7 +143,9 @@ class Caldav_Backend(CalendarBackend):
|
||||
|
||||
def get_events(self, start, end):
|
||||
"""Retrieve a list of events."""
|
||||
orig_events = self.remote_cal.search(server_expand=True, start=start, end=end)
|
||||
orig_events = self.remote_cal.search(
|
||||
server_expand=False, start=start, end=end, event=True
|
||||
)
|
||||
events = []
|
||||
for event in orig_events:
|
||||
events.append(self._serialize_event(event))
|
||||
|
||||
@@ -49,6 +49,9 @@ class Calendar:
|
||||
def add_event(self, data):
|
||||
return True
|
||||
|
||||
def save_event(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
def event_by_url(self, url):
|
||||
res = objects.Event(url=url, data=EV1, parent=self)
|
||||
return res
|
||||
|
||||
@@ -116,11 +116,11 @@ class EventSerializer(serializers.Serializer):
|
||||
|
||||
id = serializers.CharField(read_only=True)
|
||||
title = serializers.CharField()
|
||||
start = serializers.DateTimeField()
|
||||
end = serializers.DateTimeField()
|
||||
start = serializers.DateTimeField(required=False)
|
||||
end = serializers.DateTimeField(required=False)
|
||||
allDay = serializers.BooleanField(default=False)
|
||||
color = serializers.CharField(read_only=True)
|
||||
description = serializers.CharField(required=False)
|
||||
description = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
attendees = AttendeeSerializer(many=True, required=False)
|
||||
|
||||
@@ -147,10 +147,13 @@ class WritableEventSerializer(EventSerializer):
|
||||
)
|
||||
new_calendar_type = serializers.CharField(required=False)
|
||||
|
||||
start_date = serializers.DateField(required=False)
|
||||
end_date = serializers.DateField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Set calendar list."""
|
||||
calendar_type = kwargs.pop("calendar_type")
|
||||
super(EventSerializer, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.update_calendar_field(calendar_type)
|
||||
|
||||
def update_calendar_field(self, calendar_type):
|
||||
@@ -170,11 +173,13 @@ class WritableEventSerializer(EventSerializer):
|
||||
def validate(self, data):
|
||||
"""Make sure dates are present with allDay flag."""
|
||||
errors = {}
|
||||
if "allDay" in data:
|
||||
if "start" not in data:
|
||||
errors["start"] = _("This field is required.")
|
||||
if "end" not in data:
|
||||
errors["end"] = _("This field is required.")
|
||||
if data.get("allDay", False):
|
||||
mandatory_fields = ["start_date", "end_date"]
|
||||
else:
|
||||
mandatory_fields = ["start", "end"]
|
||||
for field in mandatory_fields:
|
||||
if not data.get(field):
|
||||
errors[field] = _("This field is required.")
|
||||
if errors:
|
||||
raise serializers.ValidationError(errors)
|
||||
return data
|
||||
|
||||
@@ -428,8 +428,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
|
||||
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/"
|
||||
data = {
|
||||
"title": "Test event",
|
||||
"start": "2018-03-27T00:00:00Z",
|
||||
"end": "2018-03-28T00:00:00Z",
|
||||
"start_date": "2018-03-27",
|
||||
"end_date": "2018-03-28",
|
||||
"allDay": True,
|
||||
"color": "#ffdddd",
|
||||
"description": "Description",
|
||||
@@ -461,8 +461,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
|
||||
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/"
|
||||
data = {
|
||||
"title": "Test event",
|
||||
"start": "2018-03-27T00:00:00Z",
|
||||
"end": "2018-03-28T00:00:00Z",
|
||||
"start_date": "2018-03-27",
|
||||
"end_date": "2018-03-28",
|
||||
"allDay": True,
|
||||
"color": "#ffdddd",
|
||||
"description": "Description",
|
||||
@@ -483,8 +483,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
|
||||
|
||||
url = f"/api/v2/user-calendars/{self.calendar.pk}/events/1234/"
|
||||
data = {
|
||||
"start": "2018-03-27T00:00:00Z",
|
||||
"end": "2018-03-28T00:00:00Z",
|
||||
"start_date": "2018-03-27",
|
||||
"end_date": "2018-03-28",
|
||||
"allDay": True,
|
||||
}
|
||||
response = self.client.patch(url, data=data, format="json")
|
||||
@@ -496,8 +496,8 @@ class EventViewSetTestCase(TestDataMixin, ModoAPITestCase):
|
||||
self.client.post(reverse("core:login"), data)
|
||||
data = {
|
||||
"title": "Test event",
|
||||
"start": "2018-03-27T00:00:00Z",
|
||||
"end": "2018-03-28T00:00:00Z",
|
||||
"start_date": "2018-03-27",
|
||||
"end_date": "2018-03-28",
|
||||
"allDay": True,
|
||||
"color": "#ffdddd",
|
||||
"description": "Description",
|
||||
|
||||
@@ -21,7 +21,9 @@ from . import serializers
|
||||
|
||||
def parse_date_from_iso(value):
|
||||
"""Return a tz aware datetime parsed from an ISO date."""
|
||||
return dateutil.parser.parse(value)
|
||||
result = dateutil.parser.parse(value)
|
||||
print(result)
|
||||
return result
|
||||
|
||||
|
||||
class CheckTokenMixin:
|
||||
|
||||
Reference in New Issue
Block a user