From 98f4081ee9ed0346a574a34bdf7b5b8b90533afc Mon Sep 17 00:00:00 2001 From: Jonathan Weth <git@jonathanweth.de> Date: Fri, 4 Aug 2023 18:32:13 +0200 Subject: [PATCH] Refactor calendar overview in more different components --- .../frontend/components/calendar/Calendar.vue | 291 +++++++++++++ .../calendar/CalendarDownloadAllButton.vue | 17 + .../components/calendar/CalendarOverview.vue | 402 +++++------------- .../calendar/CalendarWithControls.vue | 48 +++ .../components/calendar/calendar.graphql | 24 ++ .../components/calendar/calendarFeeds.graphql | 13 + .../components/calendar/calendarMixin.js | 30 ++ .../calendar/calendarOverview.graphql | 27 -- .../calendar/calendarSelectedFeedsMixin.js | 47 ++ aleksis/core/mixins.py | 31 +- aleksis/core/models.py | 4 +- aleksis/core/schema/calendar.py | 37 +- 12 files changed, 606 insertions(+), 365 deletions(-) create mode 100644 aleksis/core/frontend/components/calendar/Calendar.vue create mode 100644 aleksis/core/frontend/components/calendar/CalendarDownloadAllButton.vue create mode 100644 aleksis/core/frontend/components/calendar/CalendarWithControls.vue create mode 100644 aleksis/core/frontend/components/calendar/calendar.graphql create mode 100644 aleksis/core/frontend/components/calendar/calendarFeeds.graphql create mode 100644 aleksis/core/frontend/components/calendar/calendarMixin.js delete mode 100644 aleksis/core/frontend/components/calendar/calendarOverview.graphql create mode 100644 aleksis/core/frontend/components/calendar/calendarSelectedFeedsMixin.js diff --git a/aleksis/core/frontend/components/calendar/Calendar.vue b/aleksis/core/frontend/components/calendar/Calendar.vue new file mode 100644 index 000000000..d2f5875c4 --- /dev/null +++ b/aleksis/core/frontend/components/calendar/Calendar.vue @@ -0,0 +1,291 @@ +<template> + <div> + <!-- Calendar title with current calendar time range --> + <v-sheet height="600"> + <v-expand-transition> + <v-progress-linear + v-if="$apollo.queries.calendar.loading" + indeterminate + /> + </v-expand-transition> + <v-calendar + ref="calendar" + v-model="internalCalendarFocus" + show-week + :events="events" + :type="internalCalendarType" + :event_color="getColorForEvent" + @click:date="viewDay" + @click:day="viewDay" + @click:more="viewDay" + @click:event="viewEvent" + @change="setCalendarRange" + > + <template #event="{ event, eventParsed, timed }"> + <component + :is="eventBarComponentForFeed(event.calendarFeedName)" + :event="event" + :event-parsed="eventParsed" + :calendar-type="internalCalendarType" + /> + </template> + </v-calendar> + <component + v-if="calendar && calendar.calendarFeeds && selectedEvent" + :is="detailComponentForFeed(selectedEvent.calendarFeedName)" + v-model="selectedOpen" + :selected-element="selectedElement" + :selected-event="selectedEvent" + :calendar-type="internalCalendarType" + /> + </v-sheet> + </div> +</template> + +<script> +import ButtonMenu from "../generic/ButtonMenu.vue"; +import CalendarSelect from "./CalendarSelect.vue"; +import GenericCalendarFeedDetails from "./GenericCalendarFeedDetails.vue"; +import GenericCalendarFeedEventBar from "./GenericCalendarFeedEventBar.vue"; + +import { + calendarFeedDetailComponents, + calendarFeedEventBarComponents, +} from "aleksisAppImporter"; + +import gqlCalendar from "./calendar.graphql"; +import CalendarControlBar from "./CalendarControlBar.vue"; +import CalendarTypeSelect from "./CalendarTypeSelect.vue"; + +export default { + name: "Calendar", + components: { + CalendarTypeSelect, + CalendarControlBar, + ButtonMenu, + CalendarSelect, + }, + props: { + calendarFeeds: { + type: Array, + required: false, + default: () => [], + }, + params: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + internalCalendarFocus: "", + internalCalendarType: "week", + + calendar: { + calendarFeeds: [], + }, + + selectedEvent: {}, + selectedElement: null, + selectedOpen: false, + + fetchedDateRange: { start: null, end: null }, + + title: "", + + range: { + start: null, + end: null, + }, + }; + }, + emits: ["changeCalendarType", "changeCalendarFocus", "selectEvent"], + apollo: { + calendar: { + query: gqlCalendar, + skip: true, + }, + }, + computed: { + events() { + return this.calendar.calendarFeeds.flatMap((cf) => + cf.events.map((event) => ({ + ...event, + category: cf.verboseName, + calendarFeedName: cf.name, + start: new Date(event.start), + end: new Date(event.end), + color: event.color ? event.color : cf.color, + timed: !event.allDay, + meta: JSON.parse(event.meta), + })) + ); + }, + paramsForSend() { + if (this.params !== null) { + return JSON.stringify(this.params); + } + return null; + }, + queryVariables() { + let extendedStart = this.$refs.calendar.getStartOfWeek( + this.range.start + ).date; + let extendedEnd = this.$refs.calendar.getEndOfWeek(this.range.end).date; + return { + start: extendedStart, + end: extendedEnd, + names: this.calendarFeeds.map((f) => f.name), + params: this.paramsForSend, + }; + }, + }, + watch: { + params(newParams) { + if (this.range.start && this.range.end) { + this.$apollo.queries.calendar.refetch(this.queryVariables); + } + }, + range: { + handler() { + this.fetchMoreCalendarEvents(); + }, + deep: true, + }, + internalCalendarType(val) { + this.$emit("changeCalendarType", val); + }, + internalCalendarFocus(val) { + this.$emit("changeCalendarFocus", val); + }, + selectedEvent(val) { + this.$emit("selectEvent", val); + }, + }, + methods: { + prev() { + this.$refs.calendar.prev(); + }, + next() { + this.$refs.calendar.next(); + }, + setCalendarFocus(val) { + this.internalCalendarFocus = val; + }, + setCalendarType(val) { + this.internalCalendarType = val; + }, + viewDay({ date }) { + this.internalCalendarFocus = date; + this.internalCalendarType = "day"; + }, + viewEvent({ nativeEvent, event }) { + const open = () => { + this.selectedEvent = event; + this.selectedElement = nativeEvent.target; + requestAnimationFrame(() => + requestAnimationFrame(() => (this.selectedOpen = true)) + ); + }; + + if (this.selectedOpen) { + this.selectedOpen = false; + requestAnimationFrame(() => requestAnimationFrame(() => open())); + } else { + open(); + } + + nativeEvent.stopPropagation(); + }, + detailComponentForFeed(feedName) { + if ( + this.calendar.calendarFeeds && + feedName && + Object.keys(calendarFeedDetailComponents).includes(feedName + "details") + ) { + return calendarFeedDetailComponents[feedName + "details"]; + } + return GenericCalendarFeedDetails; + }, + eventBarComponentForFeed(feedName) { + if ( + this.calendar.calendarFeeds && + feedName && + Object.keys(calendarFeedEventBarComponents).includes( + feedName + "eventbar" + ) + ) { + return calendarFeedEventBarComponents[feedName + "eventbar"]; + } + return GenericCalendarFeedEventBar; + }, + getColorForEvent(event) { + return event.color; + }, + setCalendarRange({ start, end }) { + this.range.start = start; + this.range.end = end; + }, + fetchMoreCalendarEvents() { + this.title = this.$refs.calendar.title; + + // Get the start and end dates of the current date range shown in the calendar + let extendedStart, + extendedEnd = (this.queryVariables.start, this.queryVariables.end); + + let olderStart = extendedStart < this.fetchedDateRange.start; + let youngerEnd = extendedEnd > this.fetchedDateRange.end; + + if (this.calendar.calendarFeeds.length === 0) { + // No calendar feeds have been fetched yet, + // so fetch all events in the current date range + + this.$apollo.queries.calendar.setVariables(this.queryVariables); + this.$apollo.queries.calendar.skip = false; + this.fetchedDateRange = { start: extendedStart, end: extendedEnd }; + } else if (olderStart || youngerEnd) { + // Define newly fetched date range + let newStart = olderStart ? extendedStart : this.fetchedDateRange.start; + let newEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.end; + + // Define date range to fetch + let fetchStart = olderStart ? extendedStart : this.fetchedDateRange.end; + let fetchEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.start; + + this.$apollo.queries.calendar.fetchMore({ + variables: this.queryVariables, + updateQuery: (previousResult, { fetchMoreResult }) => { + let previousCalendarFeeds = previousResult.calendar.calendarFeeds; + let newCalendarFeeds = fetchMoreResult.calendar.calendarFeeds; + + previousCalendarFeeds.forEach((calendarFeed, i, calendarFeeds) => { + // Get all events except those that are updated + let keepEvents = calendarFeed.events.filter( + (event) => event.end < fetchStart || event.start > fetchEnd + ); + + /// Update the events of the calendar feed + calendarFeeds[i].events = [ + ...keepEvents, + ...newCalendarFeeds[i].events, + ]; + }); + return { + calendar: { + ...previousResult.calendar, + calendarFeeds: previousCalendarFeeds, + }, + }; + }, + }); + + this.fetchedDateRange = { start: newStart, end: newEnd }; + } + }, + }, + mounted() { + this.$refs.calendar.move(0); + }, +}; +</script> diff --git a/aleksis/core/frontend/components/calendar/CalendarDownloadAllButton.vue b/aleksis/core/frontend/components/calendar/CalendarDownloadAllButton.vue new file mode 100644 index 000000000..08b47421a --- /dev/null +++ b/aleksis/core/frontend/components/calendar/CalendarDownloadAllButton.vue @@ -0,0 +1,17 @@ +<script> +export default { + name: "CalendarDownloadAllButton", + props: { + url: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <v-btn depressed block :href="url"> + <v-icon left>mdi-download-outline</v-icon> + {{ $t("calendar.download_all") }} + </v-btn> +</template> diff --git a/aleksis/core/frontend/components/calendar/CalendarOverview.vue b/aleksis/core/frontend/components/calendar/CalendarOverview.vue index 7153b8e1e..7d602c01b 100644 --- a/aleksis/core/frontend/components/calendar/CalendarOverview.vue +++ b/aleksis/core/frontend/components/calendar/CalendarOverview.vue @@ -1,329 +1,121 @@ <template> <div class="mt-4 mb-4"> - <v-skeleton-loader - v-if=" - $apollo.queries.calendar.loading && calendar.calendarFeeds.length === 0 - " - type="date-picker-options, actions" - /> - <div v-else> - <h1 - class="mb-4 mx-4" - v-if="$vuetify.breakpoint.mdAndDown && $refs.calendar" + <h1 + class="mb-4 mx-4" + v-if="$vuetify.breakpoint.mdAndDown && $refs.calendar" + > + {{ $refs.calendar.title }} + </h1> + <v-row align="stretch"> + <!-- Control bar with prev, next and today buttons --> + <v-col cols="12" sm="4" lg="3" xl="2" align-self="center"> + <calendar-control-bar + @prev="$refs.calendar.prev()" + @next="$refs.calendar.next()" + @today="calendarFocus = ''" + /> + </v-col> + + <!-- Calendar title with current calendar time range --> + <v-col v-if="$vuetify.breakpoint.lgAndUp" align-self="center"> + <h1 class="mx-2" v-if="$refs.calendar">{{ $refs.calendar.title }}</h1> + </v-col> + + <!-- Button menu for selecting currently active calendars (only tablets/mobile) --> + <v-col + v-if="$vuetify.breakpoint.mdAndDown" + cols="12" + sm="4" + align-self="center" + class="d-flex justify-center" > - {{ $refs.calendar.title }} - </h1> - <v-row align="stretch"> - <!-- Control bar with prev, next and today buttons --> - <v-col cols="12" sm="4" lg="3" xl="2" align-self="center"> - <calendar-control-bar - @prev="$refs.calendar.prev()" - @next="$refs.calendar.next()" - @today="calendarFocus = new Date()" - /> - </v-col> - - <!-- Calendar title with current calendar time range --> - <v-col v-if="$vuetify.breakpoint.lgAndUp" align-self="center"> - <h1 class="mx-2" v-if="$refs.calendar">{{ $refs.calendar.title }}</h1> - </v-col> - - <!-- Button menu for selecting currently active calendars (only tablets/mobile) --> - <v-col - v-if="$vuetify.breakpoint.mdAndDown" - cols="12" - sm="4" - align-self="center" - class="d-flex justify-center" - > - <button-menu - icon="mdi-calendar-check-outline" - text-translation-key="calendar.select" - > - <calendar-select - v-model="selectedCalendarFeedNames" - :calendar-feeds="calendar.calendarFeeds" - @input="storeActivatedCalendars" - /> - </button-menu> - </v-col> - - <v-spacer v-if="$vuetify.breakpoint.lgAndUp" /> - - <!-- Calendar type select (month, week, day) --> - <v-col - cols="12" - sm="4" - lg="3" - align-self="center" - :align="$vuetify.breakpoint.smAndUp ? 'right' : 'center'" + <button-menu + icon="mdi-calendar-check-outline" + text-translation-key="calendar.select" > - <calendar-type-select v-model="currentCalendarType" /> - </v-col> - </v-row> - <v-row> - <v-col v-if="$vuetify.breakpoint.lgAndUp" lg="3" xl="2"> - <!-- Mini date picker --> - <v-date-picker - no-title - v-model="calendarFocus" - :style="{ margin: '0px -8px' }" - :first-day-of-week="1" - ></v-date-picker> - - <!-- Calendar select (only desktop) --> - <v-list flat subheader> - <v-subheader> - {{ $t("calendar.my_calendars") }} - </v-subheader> - <calendar-select - class="mb-4" - v-model="selectedCalendarFeedNames" - :calendar-feeds="calendar.calendarFeeds" - @input="storeActivatedCalendars" - /> - <v-btn depressed block v-if="calendar" :href="calendar.allFeedsUrl"> - <v-icon left>mdi-download-outline</v-icon> - {{ $t("calendar.download_all") }} - </v-btn> - </v-list> - </v-col> - - <!-- Actual calendar --> - <v-col lg="9" xl="10"> - <v-sheet height="600"> - <v-expand-transition> - <v-progress-linear - v-if="$apollo.queries.calendar.loading" - indeterminate - /> - </v-expand-transition> - <v-calendar - ref="calendar" - v-model="calendarFocus" - show-week - :events="events" - :type="currentCalendarType" - :event_color="getColorForEvent" - @click:date="viewDay" - @click:day="viewDay" - @click:more="viewDay" - @click:event="viewEvent" - @change="fetchMoreCalendarEvents" - > - <template #event="{ event, eventParsed, timed }"> - <component - :is="eventBarComponentForFeed(event.calendarFeedName)" - :event="event" - :event-parsed="eventParsed" - :calendar-type="currentCalendarType" - /> - </template> - </v-calendar> - <component - v-if="calendar && calendar.calendarFeeds && selectedEvent" - :is="detailComponentForFeed(selectedEvent.calendarFeedName)" - v-model="selectedOpen" - :selected-element="selectedElement" - :selected-event="selectedEvent" - :calendar-type="currentCalendarType" - /> - </v-sheet> - </v-col> - </v-row> - </div> + <calendar-select + v-model="selectedCalendarFeedNames" + :calendar-feeds="calendar.calendarFeeds" + @input="storeActivatedCalendars" + /> + </button-menu> + </v-col> + + <v-spacer v-if="$vuetify.breakpoint.lgAndUp" /> + + <!-- Calendar type select (month, week, day) --> + <v-col + cols="12" + sm="4" + lg="3" + align-self="center" + :align="$vuetify.breakpoint.smAndUp ? 'right' : 'center'" + > + <calendar-type-select v-model="calendarType" /> + </v-col> + </v-row> + <v-row> + <v-col v-if="$vuetify.breakpoint.lgAndUp" lg="3" xl="2"> + <!-- Mini date picker --> + <v-date-picker + no-title + v-model="calendarFocus" + :style="{ margin: '0px -8px' }" + :first-day-of-week="1" + ></v-date-picker> + + <!-- Calendar select (only desktop) --> + <v-list flat subheader> + <v-subheader> + {{ $t("calendar.my_calendars") }} + </v-subheader> + <calendar-select + class="mb-4" + v-model="selectedCalendarFeedNames" + :calendar-feeds="calendar.calendarFeeds" + @input="storeActivatedCalendars" + /> + <calendar-download-all-button + v-if="calendar?.allFeedsUrl" + :url="calendar.allFeedsUrl" + /> + </v-list> + </v-col> + + <!-- Actual calendar --> + <v-col lg="9" xl="10"> + <calendar + :calendar-feeds="selectedFeedsForCalendar" + @changeCalendarFocus="setCalendarFocus" + @changeCalendarType="setCalendarType" + ref="calendar" + /> + </v-col> + </v-row> </div> </template> <script> import ButtonMenu from "../generic/ButtonMenu.vue"; import CalendarSelect from "./CalendarSelect.vue"; -import GenericCalendarFeedDetails from "./GenericCalendarFeedDetails.vue"; -import GenericCalendarFeedEventBar from "./GenericCalendarFeedEventBar.vue"; -import { - calendarFeedDetailComponents, - calendarFeedEventBarComponents, -} from "aleksisAppImporter"; - -import gqlCalendarOverview from "./calendarOverview.graphql"; -import gqlSetCalendarStatus from "./setCalendarStatus.graphql"; import CalendarControlBar from "./CalendarControlBar.vue"; import CalendarTypeSelect from "./CalendarTypeSelect.vue"; +import Calendar from "./Calendar.vue"; +import CalendarDownloadAllButton from "./CalendarDownloadAllButton.vue"; +import calendarMixin from "./calendarMixin"; +import calendarSelectedFeedsMixin from "./calendarSelectedFeedsMixin"; export default { name: "CalendarOverview", + mixins: [calendarMixin, calendarSelectedFeedsMixin], components: { + Calendar, CalendarTypeSelect, CalendarControlBar, ButtonMenu, CalendarSelect, - }, - data() { - return { - calendarFocus: "", - calendar: { - calendarFeeds: [], - }, - selectedCalendarFeedNames: [], - currentCalendarType: "week", - selectedEvent: {}, - selectedElement: null, - selectedOpen: false, - fetchedDateRange: { start: null, end: null }, - }; - }, - apollo: { - calendar: { - query: gqlCalendarOverview, - skip: true, - result({ data }) { - this.selectedCalendarFeedNames = data.calendar.calendarFeeds - .filter((c) => c.activated) - .map((c) => c.name); - }, - }, - }, - computed: { - events() { - return this.calendar.calendarFeeds - .filter((c) => this.selectedCalendarFeedNames.includes(c.name)) - .flatMap((cf) => - cf.feed.events.map((event) => ({ - ...event, - category: cf.verboseName, - calendarFeedName: cf.name, - start: new Date(event.start), - end: new Date(event.end), - color: event.color ? event.color : cf.color, - timed: !event.allDay, - meta: JSON.parse(event.meta), - })) - ); - }, - }, - methods: { - viewDay({ date }) { - this.calendarFocus = date; - this.currentCalendarType = "day"; - }, - viewEvent({ nativeEvent, event }) { - const open = () => { - this.selectedEvent = event; - this.selectedElement = nativeEvent.target; - requestAnimationFrame(() => - requestAnimationFrame(() => (this.selectedOpen = true)) - ); - }; - - if (this.selectedOpen) { - this.selectedOpen = false; - requestAnimationFrame(() => requestAnimationFrame(() => open())); - } else { - open(); - } - - nativeEvent.stopPropagation(); - }, - detailComponentForFeed(feedName) { - if ( - this.calendar.calendarFeeds && - feedName && - Object.keys(calendarFeedDetailComponents).includes(feedName + "details") - ) { - return calendarFeedDetailComponents[feedName + "details"]; - } - return GenericCalendarFeedDetails; - }, - eventBarComponentForFeed(feedName) { - if ( - this.calendar.calendarFeeds && - feedName && - Object.keys(calendarFeedEventBarComponents).includes( - feedName + "eventbar" - ) - ) { - return calendarFeedEventBarComponents[feedName + "eventbar"]; - } - return GenericCalendarFeedEventBar; - }, - getColorForEvent(event) { - return event.color; - }, - storeActivatedCalendars() { - // Store currently activated calendars in the backend - this.$apollo.mutate({ - mutation: gqlSetCalendarStatus, - variables: { - calendars: this.selectedCalendarFeedNames, - }, - }); - }, - fetchMoreCalendarEvents({ start, end }) { - // Get the start and end dates of the current date range shown in the calendar - let extendedStart = this.$refs.calendar.getStartOfWeek(start).date; - let extendedEnd = this.$refs.calendar.getEndOfWeek(end).date; - - let olderStart = extendedStart < this.fetchedDateRange.start; - let youngerEnd = extendedEnd > this.fetchedDateRange.end; - - if (this.calendar.calendarFeeds.length === 0) { - // No calendar feeds have been fetched yet, - // so fetch all events in the current date range - - this.$apollo.queries.calendar.setVariables({ - start: extendedStart, - end: extendedEnd, - }); - this.$apollo.queries.calendar.skip = false; - this.fetchedDateRange = { start: extendedStart, end: extendedEnd }; - } else if (olderStart || youngerEnd) { - // Define newly fetched date range - let newStart = olderStart ? extendedStart : this.fetchedDateRange.start; - let newEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.end; - - // Define date range to fetch - let fetchStart = olderStart ? extendedStart : this.fetchedDateRange.end; - let fetchEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.start; - - this.$apollo.queries.calendar.fetchMore({ - variables: { - start: fetchStart, - end: fetchEnd, - }, - updateQuery: (previousResult, { fetchMoreResult }) => { - let previousCalendarFeeds = previousResult.calendar.calendarFeeds; - let newCalendarFeeds = fetchMoreResult.calendar.calendarFeeds; - - previousCalendarFeeds.forEach((calendarFeed, i, calendarFeeds) => { - // Get all events except those that are updated - let keepEvents = calendarFeed.feed.events.filter( - (event) => event.end < fetchStart || event.start > fetchEnd - ); - - /// Update the events of the calendar feed - calendarFeeds[i].feed.events = [ - ...keepEvents, - ...newCalendarFeeds[i].feed.events, - ]; - }); - return { - calendar: { - ...previousResult.calendar, - calendarFeeds: previousCalendarFeeds, - }, - }; - }, - }); - - this.fetchedDateRange = { start: newStart, end: newEnd }; - } - }, - }, - mounted() { - this.$refs.calendar.move(0); + CalendarDownloadAllButton, }, }; </script> - -<style scoped></style> diff --git a/aleksis/core/frontend/components/calendar/CalendarWithControls.vue b/aleksis/core/frontend/components/calendar/CalendarWithControls.vue new file mode 100644 index 000000000..304904f3d --- /dev/null +++ b/aleksis/core/frontend/components/calendar/CalendarWithControls.vue @@ -0,0 +1,48 @@ +<template> + <div> + <div class="d-flex mb-3 justify-space-between"> + <!-- Control bar with prev, next and today buttons --> + <calendar-control-bar + @prev="$refs.calendar.prev()" + @next="$refs.calendar.next()" + @today="calendarFocus = ''" + /> + + <!-- Calendar title with current calendar time range --> + <div class="mx-2" v-if="$refs.calendar">{{ $refs.calendar.title }}</div> + + <calendar-type-select v-model="calendarType" /> + </div> + + <!-- Actual calendar --> + <calendar + :calendar-feeds="calendarFeeds" + @changeCalendarFocus="setCalendarFocus" + @changeCalendarType="setCalendarType" + v-bind="$attrs" + ref="calendar" + /> + </div> +</template> + +<script> +import CalendarControlBar from "./CalendarControlBar.vue"; +import CalendarTypeSelect from "./CalendarTypeSelect.vue"; +import Calendar from "./Calendar.vue"; +import calendarMixin from "./calendarMixin"; +export default { + name: "CalendarWithControls", + components: { + Calendar, + CalendarTypeSelect, + CalendarControlBar, + }, + mixins: [calendarMixin], + props: { + calendarFeeds: { + type: Array, + required: true, + }, + }, +}; +</script> diff --git a/aleksis/core/frontend/components/calendar/calendar.graphql b/aleksis/core/frontend/components/calendar/calendar.graphql new file mode 100644 index 000000000..5f2efd518 --- /dev/null +++ b/aleksis/core/frontend/components/calendar/calendar.graphql @@ -0,0 +1,24 @@ +query ($names: [String], $start: Date, $end: Date, $params: String) { + calendar { + calendarFeeds(names: $names) { + name + verboseName + description + url + color + activated + events(start: $start, end: $end, params: $params) { + name + start + end + color + description + location + uid + allDay + status + meta + } + } + } +} diff --git a/aleksis/core/frontend/components/calendar/calendarFeeds.graphql b/aleksis/core/frontend/components/calendar/calendarFeeds.graphql new file mode 100644 index 000000000..917cf3869 --- /dev/null +++ b/aleksis/core/frontend/components/calendar/calendarFeeds.graphql @@ -0,0 +1,13 @@ +query { + calendar { + allFeedsUrl + calendarFeeds { + name + verboseName + description + url + color + activated + } + } +} diff --git a/aleksis/core/frontend/components/calendar/calendarMixin.js b/aleksis/core/frontend/components/calendar/calendarMixin.js new file mode 100644 index 000000000..902525615 --- /dev/null +++ b/aleksis/core/frontend/components/calendar/calendarMixin.js @@ -0,0 +1,30 @@ +/** + * Mixin for use with adaptable components showing details for calendar feeds. + */ + +const calendarMixin = { + data() { + return { + calendarFocus: "", + calendarType: "week", + }; + }, + methods: { + setCalendarFocus(val) { + this.calendarFocus = val; + }, + setCalendarType(val) { + this.calendarType = val; + }, + }, + watch: { + calendarFocus(val) { + this.$refs.calendar.setCalendarFocus(val); + }, + calendarType(val) { + this.$refs.calendar.setCalendarType(val); + }, + }, +}; + +export default calendarMixin; diff --git a/aleksis/core/frontend/components/calendar/calendarOverview.graphql b/aleksis/core/frontend/components/calendar/calendarOverview.graphql deleted file mode 100644 index f277ef4bb..000000000 --- a/aleksis/core/frontend/components/calendar/calendarOverview.graphql +++ /dev/null @@ -1,27 +0,0 @@ -query ($start: Date, $end: Date) { - calendar { - allFeedsUrl - calendarFeeds { - name - verboseName - description - url - color - activated - feed { - events(start: $start, end: $end) { - name - start - end - color - description - location - uid - allDay - status - meta - } - } - } - } -} diff --git a/aleksis/core/frontend/components/calendar/calendarSelectedFeedsMixin.js b/aleksis/core/frontend/components/calendar/calendarSelectedFeedsMixin.js new file mode 100644 index 000000000..b8d93e624 --- /dev/null +++ b/aleksis/core/frontend/components/calendar/calendarSelectedFeedsMixin.js @@ -0,0 +1,47 @@ +/** + * Mixin for use with adaptable components showing details for calendar feeds. + */ +import gqlCalendarFeeds from "./calendarFeeds.graphql"; +import gqlSetCalendarStatus from "./setCalendarStatus.graphql"; + +const calendarSelectedFeedsMixin = { + data() { + return { + calendar: { + calendarFeeds: [], + }, + selectedCalendarFeedNames: [], + }; + }, + apollo: { + calendar: { + query: gqlCalendarFeeds, + result({ data }) { + console.log(data); + this.selectedCalendarFeedNames = data.calendar.calendarFeeds + .filter((c) => c.activated) + .map((c) => c.name); + }, + }, + }, + computed: { + selectedFeedsForCalendar() { + return this.selectedCalendarFeedNames.map((name) => { + return { name }; + }); + }, + }, + methods: { + storeActivatedCalendars() { + // Store currently activated calendars in the backend + this.$apollo.mutate({ + mutation: gqlSetCalendarStatus, + variables: { + calendars: this.selectedCalendarFeedNames, + }, + }); + }, + }, +}; + +export default calendarSelectedFeedsMixin; diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py index c6c19af0a..a2e133cb4 100644 --- a/aleksis/core/mixins.py +++ b/aleksis/core/mixins.py @@ -676,18 +676,20 @@ class CalendarEventMixin(RegistryObject): return cls.color @classmethod - def create_event(cls, reference_object, feed: ExtendedICal20Feed, request) -> dict[str, Any]: + def create_event( + cls, reference_object, feed: ExtendedICal20Feed, request, params=None + ) -> dict[str, Any]: """Create an event for the given reference object and add it to the feed.""" values = {} for field in cls.get_event_field_names(): - field_value = cls.get_event_field_value(reference_object, field, request) + field_value = cls.get_event_field_value(reference_object, field, request, params) if field_value is not None: values[field] = field_value feed.add_item(**values) return values @classmethod - def start_feed(cls, request) -> ExtendedICal20Feed: + def start_feed(cls, request, params=None) -> ExtendedICal20Feed: """Start the feed and return it.""" feed = ExtendedICal20Feed( title=cls.get_verbose_name(request), @@ -704,25 +706,25 @@ class CalendarEventMixin(RegistryObject): raise NotImplementedError @classmethod - def create_feed(cls, request: HttpRequest) -> ExtendedICal20Feed: + def create_feed(cls, request: HttpRequest, params: Optional[dict] = None) -> ExtendedICal20Feed: """Create the calendar feed with all events.""" - feed = cls.start_feed(request) + feed = cls.start_feed(request, params) - for reference_object in cls.get_objects(request): - cls.create_event(reference_object, feed, request) + for reference_object in cls.get_objects(request, params): + cls.create_event(reference_object, feed, request, params) return feed @classmethod - def get_calendar_object(cls, request: HttpRequest) -> Calendar: + def get_calendar_object(cls, request: HttpRequest, params=None) -> Calendar: """Return the calendar object.""" - feed = cls.create_feed(request) + feed = cls.create_feed(request, params) return feed.get_calendar_object() @classmethod - def get_single_events(cls, request: HttpRequest, start=None, end=None): + def get_single_events(cls, request: HttpRequest, start=None, end=None, params=None): """Get single events for this calendar feed.""" - feed = cls.create_feed(request) + feed = cls.create_feed(request, params) return feed.get_single_events(start, end) @classmethod @@ -731,7 +733,7 @@ class CalendarEventMixin(RegistryObject): return [field_map[0] for field_map in ITEM_ELEMENT_FIELD_MAP] @classmethod - def get_event_field_value(cls, reference_object, field_name: str, request): + def get_event_field_value(cls, reference_object, field_name: str, request, params=None): """Return the value for the given field name.""" method_name = f"value_{field_name}" if hasattr(cls, method_name) and callable(getattr(cls, method_name)): @@ -761,5 +763,10 @@ class CalendarEventMixin(RegistryObject): """Return a list of valid feed names.""" return [feed.name for feed in cls.valid_feeds] + @classmethod def get_object_by_name(cls, name): return cls.registered_objects_dict.get(name) + + @classmethod + def get_activated(cls, person): + return cls.name in person.preferences["calendar__activated_calendars"] diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 4151d51b3..0d404c1b1 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1624,7 +1624,7 @@ class CalendarEvent(CalendarEventMixin, ExtensiblePolymorphicModel): return cls.get_color(request) @classmethod - def get_objects(cls, request) -> Iterable: + def get_objects(cls, request, params=None) -> Iterable: """Return all objects that should be included in the calendar.""" return cls.objects.instance_of(cls) @@ -1697,7 +1697,7 @@ class BirthdayEvent(CalendarEventMixin): return get_site_preferences()["calendar__birthday_color"] @classmethod - def get_objects(cls, request) -> QuerySet: + def get_objects(cls, request, params=None) -> QuerySet: qs = Person.objects.filter(date_of_birth__isnull=False) qs = qs.filter( Q(pk=request.user.person.pk) diff --git a/aleksis/core/schema/calendar.py b/aleksis/core/schema/calendar.py index 110e2cf82..f3fcdf98d 100644 --- a/aleksis/core/schema/calendar.py +++ b/aleksis/core/schema/calendar.py @@ -1,3 +1,4 @@ +import json from datetime import datetime from django.core.exceptions import PermissionDenied @@ -53,22 +54,24 @@ class CalendarEventType(ObjectType): return root.get("X-META", "{}") -class CalendarFeedType(ObjectType): +class CalendarType(ObjectType): + name = graphene.String(required=True) + verbose_name = graphene.String(required=True) + description = graphene.String() events = graphene.List( CalendarEventType, start=graphene.Date(required=False), end=graphene.Date(required=False), + params=graphene.String(required=False), ) - def resolve_events(root, info, start=None, end=None, **kwargs): - return root.get_single_events(start, end) + def resolve_events(root, info, start=None, end=None, params=None, **kwargs): + if params: + params = json.loads(params) + feed = root.create_feed(info.context, params) + return feed.get_single_events(start, end) -class CalendarType(ObjectType): - name = graphene.String(required=True) - verbose_name = graphene.String(required=True) - description = graphene.String() - feed = graphene.Field(CalendarFeedType) color = graphene.String() url = graphene.String() @@ -81,9 +84,6 @@ class CalendarType(ObjectType): def resolve_description(root, info, **kwargs): return root.get_description(info.context) - def resolve_feed(root, info, **kwargs): - return root.create_feed(info.context) - def resolve_url(root, info, **kwargs): return info.context.build_absolute_uri(reverse("calendar_feed", args=[root.name])) @@ -91,7 +91,7 @@ class CalendarType(ObjectType): return root.get_color(info.context) def resolve_activated(root, info, **kwargs): - return root.name in info.context.user.person.preferences["calendar__activated_calendars"] + return root.get_activated(info.context.user.person) class SetCalendarStatusMutation(graphene.Mutation): @@ -111,17 +111,16 @@ class SetCalendarStatusMutation(graphene.Mutation): class CalendarBaseType(ObjectType): - calendar_feeds = graphene.List(CalendarType) - - calendar_feeds_by_names = graphene.List(CalendarType, names=graphene.List(graphene.String)) + calendar_feeds = graphene.List( + CalendarType, names=graphene.List(graphene.String, required=False) + ) all_feeds_url = graphene.String() - def resolve_calendar_feeds(root, info, **kwargs): + def resolve_calendar_feeds(root, info, names=None, **kwargs): + if names is not None: + return [CalendarEventMixin.get_object_by_name(name) for name in names] return CalendarEventMixin.valid_feeds - def resolve_calendar_feeds_by_names(root, info, names, **kwargs): - return [CalendarEventMixin.get_object_by_name(name) for name in names] - def resolve_all_feeds_url(root, info, **kwargs): return info.context.build_absolute_uri(reverse("all_calendar_feeds")) -- GitLab