Merge pull request #5891 from nextcloud/feat/calendar-widget-private-calendar

Feat: calendar widget for private calendars
This commit is contained in:
Hamza 2024-04-17 11:12:38 +02:00 committed by GitHub
commit 97fce2ac52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 113 additions and 19 deletions

View File

@ -73,24 +73,55 @@ class ReferenceProvider extends ADiscoverableReferenceProvider {
public function matchReference(string $referenceText): bool { public function matchReference(string $referenceText): bool {
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID); $start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID);
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID); $startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID);
if (preg_match('/^' . preg_quote($start, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1 || preg_match('/^' . preg_quote($startIndex, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1) {
return true;
}
$start = $this->urlGenerator->getAbsoluteURL('/remote.php/dav/calendars');
if (preg_match('/^' . preg_quote($start, '/') . '\/[a-zA-Z0-9-]+\/[a-zA-Z0-9-]+\/$/i', $referenceText) === 1) {
return true;
}
return preg_match('/^' . preg_quote($start, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1 || preg_match('/^' . preg_quote($startIndex, '/') . '\/p\/[a-zA-Z0-9]+$/i', $referenceText) === 1; return false;
} }
public function resolveReference(string $referenceText): ?IReference { public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) { if ($this->matchReference($referenceText)) {
$token = $this->getCalendarTokenFromLink($referenceText); $type = $this->getType($referenceText);
$reference = new Reference($referenceText); $reference = new Reference($referenceText);
$reference->setTitle('calendar'); $reference->setTitle('calendar');
$reference->setDescription($token); $reference->setDescription('calendar widget');
$reference->setRichObject(
'calendar_widget', switch ($type) {
[ case 'public':
'title' => 'calendar', $token = $this->getCalendarTokenFromLink($referenceText);
'token' => $token, $url = $this->getUrlFromLink($token, 'public');
'url' => $referenceText,] $reference->setRichObject(
); 'calendar_widget',
[
'title' => 'calendar',
'token' => $token,
'isPublic' => true,
'url' => $url,
]
);
break;
case 'private':
$url = $this->getUrlFromLink($referenceText, 'private');
$reference->setRichObject(
'calendar_widget',
[
'title' => 'calendar',
'isPublic' => false,
'url' => $url,
]
);
break;
default:
return null;
}
return $reference; return $reference;
} }
@ -99,14 +130,31 @@ class ReferenceProvider extends ADiscoverableReferenceProvider {
} }
private function getCalendarTokenFromLink(string $url): ?string { private function getCalendarTokenFromLink(string $url): ?string {
if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url, $output_array)) { if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url, $output_array)) {
return $output_array[1]; return $output_array[1];
} }
return $url; return null;
} }
private function getUrlFromLink(string $data, string $type): ?string {
if ($type === 'public') {
return "{$this->urlGenerator->getWebroot()}/remote.php/dav/public-calendars/{$data}/";
} elseif ($type === 'private' && preg_match('/\/remote.php\/dav\/calendars\/([a-zA-Z0-9-]+)\/([a-zA-Z0-9-]+)\//', $data, $output_array)) {
return $this->urlGenerator->getWebroot().$output_array[0];
}
return null;
}
private function getType(string $url): string {
if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url) === 1) {
return 'public';
}
if (preg_match('/\/dav\/calendars\/([^\/]+)\/([^\/]+)/', $url) === 1) {
return 'private';
}
return 'unknown';
}
public function getCachePrefix(string $referenceId): string { public function getCachePrefix(string $referenceId): string {
return ''; return '';

View File

@ -22,9 +22,10 @@
--> -->
<template> <template>
<FullCalendar ref="fullCalendar" <FullCalendar v-if="calendarOptions"
ref="fullCalendar"
:class="isWidget? 'fullcalendar-widget': ''" :class="isWidget? 'fullcalendar-widget': ''"
:options="options" /> :options="calendarOptions" />
</template> </template>
<script> <script>
@ -77,6 +78,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
url: {
type: String,
required: false,
},
/** /**
* Whether or not the user is authenticated * Whether or not the user is authenticated
*/ */
@ -89,6 +94,7 @@ export default {
return { return {
updateTodayJob: null, updateTodayJob: null,
updateTodayJobPreviousDate: null, updateTodayJobPreviousDate: null,
calendarOptions: null,
} }
}, },
computed: { computed: {
@ -154,6 +160,13 @@ export default {
} }
}, },
eventSources() { eventSources() {
if (this.isWidget) {
const calendar = this.$store.getters.getCalendarByUrl(this.url)
if (!calendar) {
return []
}
return [calendar].map(eventSource(this.$store))
}
return this.$store.getters.enabledCalendars.map(eventSource(this.$store)) return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
}, },
widgetView() { widgetView() {
@ -194,6 +207,22 @@ export default {
const calendarApi = this.$refs.fullCalendar.getApi() const calendarApi = this.$refs.fullCalendar.getApi()
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(newDate)) calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(newDate))
}, },
eventSources(sources, oldSources) {
const newSources = sources.filter(source => !oldSources.map(oldSource => oldSource.id).includes(source.id))
const removedSources = oldSources.filter(oldSource => !sources.map(source => source.id).includes(oldSource.id))
// Hackity hack! Unfortunately, calendarOptions.eventSources is not reactive ...
// Ref https://fullcalendar.io/docs/Calendar-addEventSource
// TODO: Find a better/safer way to prevent duplicated event sources
const calendarApi = this.$refs.fullCalendar.getApi()
for (const source of newSources) {
calendarApi.addEventSource(source)
}
const eventSources = calendarApi.getEventSources()
for (const source of removedSources) {
eventSources.find(x => x.id === source.id)?.remove()
}
},
modificationCount: debounce(function() { modificationCount: debounce(function() {
const calendarApi = this.$refs.fullCalendar.getApi() const calendarApi = this.$refs.fullCalendar.getApi()
calendarApi.refetchEvents() calendarApi.refetchEvents()
@ -213,7 +242,7 @@ export default {
* we have to register a resize-observer here, that will automatically * we have to register a resize-observer here, that will automatically
* update the fullCalendar size, when the available space changes. * update the fullCalendar size, when the available space changes.
*/ */
mounted() { mounted() {
if (window.ResizeObserver) { if (window.ResizeObserver) {
const resizeObserver = new ResizeObserver(debounce(() => { const resizeObserver = new ResizeObserver(debounce(() => {
this.$refs.fullCalendar this.$refs.fullCalendar
@ -225,6 +254,8 @@ export default {
} }
}, },
async created() { async created() {
this.calendarOptions = await this.options
this.updateTodayJob = setInterval(() => { this.updateTodayJob = setInterval(() => {
const newDate = getYYYYMMDDFromFirstdayParam('now') const newDate = getYYYYMMDDFromFirstdayParam('now')

View File

@ -20,7 +20,9 @@ registerWidget('calendar_widget', async (el, { richObjectType, richObject, acces
store, store,
propsData: { propsData: {
isWidget: true, isWidget: true,
referenceToken: richObject.token, isPublic: richObject.isPublic,
referenceToken: richObject?.token,
url: richObject.url,
}, },
}).$mount(el) }).$mount(el)
return new NcCustomPickerRenderResult(vueElement.$el, vueElement) return new NcCustomPickerRenderResult(vueElement.$el, vueElement)

View File

@ -27,6 +27,7 @@
<CalendarGrid v-if="!showEmptyCalendarScreen" <CalendarGrid v-if="!showEmptyCalendarScreen"
ref="calendarGridWidget" ref="calendarGridWidget"
:is-widget="isWidget" :is-widget="isWidget"
:url="url"
:is-authenticated-user="isAuthenticatedUser" /> :is-authenticated-user="isAuthenticatedUser" />
<EmptyCalendar v-else /> <EmptyCalendar v-else />
@ -139,14 +140,26 @@ export default {
EditSimple, EditSimple,
}, },
props: { props: {
// Is the calendar in a widget ?
isWidget: { isWidget: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// The reference token for the widget for public share calendars
referenceToken: { referenceToken: {
type: String, type: String,
required: false, required: false,
}, },
// Is public share ?
isPublic: {
type: Boolean,
required: false,
},
// Url of private calendar
url: {
type: String,
required: false,
},
}, },
data() { data() {
return { return {
@ -263,7 +276,7 @@ export default {
}) })
this.$store.dispatch('initializeCalendarJsConfig') this.$store.dispatch('initializeCalendarJsConfig')
if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isWidget) { if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isPublic) {
await initializeClientForPublicView() await initializeClientForPublicView()
const tokens = this.isWidget ? [this.referenceToken] : this.$route.params.tokens.split('-') const tokens = this.isWidget ? [this.referenceToken] : this.$route.params.tokens.split('-')
const calendars = await this.$store.dispatch('getPublicCalendars', { tokens }) const calendars = await this.$store.dispatch('getPublicCalendars', { tokens })