mirror of https://github.com/nextcloud/calendar
Feat: Calendar widget
Signed-off-by: Hamza Mahjoubi <hamzamahjoubi221@gmail.com>
This commit is contained in:
parent
6a5816ffdd
commit
439c8b99e6
|
@ -28,13 +28,16 @@ use OCA\Calendar\Dashboard\CalendarWidget;
|
|||
use OCA\Calendar\Dashboard\CalendarWidgetV2;
|
||||
use OCA\Calendar\Events\BeforeAppointmentBookedEvent;
|
||||
use OCA\Calendar\Listener\AppointmentBookedListener;
|
||||
use OCA\Calendar\Listener\CalendarReferenceListener;
|
||||
use OCA\Calendar\Listener\UserDeletedListener;
|
||||
use OCA\Calendar\Notification\Notifier;
|
||||
use OCA\Calendar\Profile\AppointmentsAction;
|
||||
use OCA\Calendar\Reference\ReferenceProvider;
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\Collaboration\Reference\RenderReferenceEvent;
|
||||
use OCP\Dashboard\IAPIWidgetV2;
|
||||
use OCP\User\Events\UserDeletedEvent;
|
||||
use function method_exists;
|
||||
|
@ -65,9 +68,11 @@ class Application extends App implements IBootstrap {
|
|||
if (method_exists($context, 'registerProfileLinkAction')) {
|
||||
$context->registerProfileLinkAction(AppointmentsAction::class);
|
||||
}
|
||||
$context->registerReferenceProvider(ReferenceProvider::class);
|
||||
|
||||
$context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class);
|
||||
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||||
$context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class);
|
||||
|
||||
$context->registerNotifierService(Notifier::class);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
|
||||
*
|
||||
* @author 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
|
||||
*
|
||||
* @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/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Listener;
|
||||
|
||||
use OC\App\CompareVersion;
|
||||
use OCA\Calendar\AppInfo\Application;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Collaboration\Reference\RenderReferenceEvent;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\IAppData;
|
||||
use OCP\IConfig;
|
||||
|
||||
|
||||
|
||||
use OCP\Util;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event|RenderReferenceEvent>
|
||||
*/
|
||||
class CalendarReferenceListener implements IEventListener {
|
||||
|
||||
/** @var IInitialState */
|
||||
private $initialStateService;
|
||||
|
||||
/** @var IAppManager */
|
||||
private $appManager;
|
||||
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
|
||||
/** @var CompareVersion */
|
||||
private $compareVersion;
|
||||
|
||||
private IAppData $appData;
|
||||
|
||||
public function __construct(
|
||||
IInitialState $initialStateService,
|
||||
IAppManager $appManager,
|
||||
IConfig $config,
|
||||
IAppData $appData,
|
||||
CompareVersion $compareVersion,
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->initialStateService = $initialStateService;
|
||||
$this->appManager = $appManager;
|
||||
$this->appData = $appData;
|
||||
$this->compareVersion = $compareVersion;
|
||||
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!$event instanceof RenderReferenceEvent) {
|
||||
return;
|
||||
}
|
||||
$defaultEventLimit = $this->config->getAppValue('calendar', 'eventLimit', 'yes');
|
||||
$defaultInitialView = $this->config->getAppValue('calendar', 'currentView', 'dayGridMonth');
|
||||
$defaultShowWeekends = $this->config->getAppValue('calendar', 'showWeekends', 'yes');
|
||||
$defaultWeekNumbers = $this->config->getAppValue('calendar', 'showWeekNr', 'no');
|
||||
$defaultSkipPopover = $this->config->getAppValue('calendar', 'skipPopover', 'no');
|
||||
$defaultTimezone = $this->config->getAppValue('calendar', 'timezone', 'automatic');
|
||||
$defaultSlotDuration = $this->config->getAppValue('calendar', 'slotDuration', '00:30:00');
|
||||
$defaultDefaultReminder = $this->config->getAppValue('calendar', 'defaultReminder', 'none');
|
||||
|
||||
$appVersion = $this->config->getAppValue('calendar', 'installed_version', '');
|
||||
$forceEventAlarmType = $this->config->getAppValue('calendar', 'forceEventAlarmType', '');
|
||||
if (!in_array($forceEventAlarmType, ['DISPLAY', 'EMAIL'], true)) {
|
||||
$forceEventAlarmType = false;
|
||||
}
|
||||
$showResources = $this->config->getAppValue('calendar', 'showResources', 'yes') === 'yes';
|
||||
$publicCalendars = $this->config->getAppValue('calendar', 'publicCalendars', '');
|
||||
|
||||
$talkApiVersion = version_compare($this->appManager->getAppVersion('spreed'), '12.0.0', '>=') ? 'v4' : 'v1';
|
||||
$tasksEnabled = $this->appManager->isEnabledForUser('tasks');
|
||||
|
||||
$circleVersion = $this->appManager->getAppVersion('circles');
|
||||
$isCirclesEnabled = $this->appManager->isEnabledForUser('circles') === true;
|
||||
// if circles is not installed, we use 0.0.0
|
||||
$isCircleVersionCompatible = $this->compareVersion->isCompatible($circleVersion ? $circleVersion : '0.0.0', '22');
|
||||
|
||||
$this->initialStateService->provideInitialState('app_version', $appVersion);
|
||||
$this->initialStateService->provideInitialState('event_limit', $defaultEventLimit);
|
||||
$this->initialStateService->provideInitialState('first_run', false);
|
||||
$this->initialStateService->provideInitialState('initial_view', $defaultInitialView);
|
||||
$this->initialStateService->provideInitialState('show_weekends', $defaultShowWeekends);
|
||||
$this->initialStateService->provideInitialState('show_week_numbers', $defaultWeekNumbers === 'yes');
|
||||
$this->initialStateService->provideInitialState('skip_popover', true);
|
||||
$this->initialStateService->provideInitialState('talk_enabled', false);
|
||||
$this->initialStateService->provideInitialState('talk_api_version', $talkApiVersion);
|
||||
$this->initialStateService->provideInitialState('show_tasks', false);
|
||||
$this->initialStateService->provideInitialState('timezone', $defaultTimezone);
|
||||
$this->initialStateService->provideInitialState('attachments_folder', '/Calendar');
|
||||
$this->initialStateService->provideInitialState('slot_duration', $defaultSlotDuration);
|
||||
$this->initialStateService->provideInitialState('default_reminder', $defaultDefaultReminder);
|
||||
$this->initialStateService->provideInitialState('tasks_enabled', $tasksEnabled);
|
||||
$this->initialStateService->provideInitialState('hide_event_export', true);
|
||||
$this->initialStateService->provideInitialState('force_event_alarm_type', $forceEventAlarmType);
|
||||
$this->initialStateService->provideInitialState('disable_appointments', true);
|
||||
$this->initialStateService->provideInitialState('can_subscribe_link', false);
|
||||
$this->initialStateService->provideInitialState('show_resources', $showResources);
|
||||
$this->initialStateService->provideInitialState('publicCalendars', $publicCalendars);
|
||||
|
||||
Util::addScript(Application::APP_ID, 'calendar-reference');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* @copyright 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
|
||||
*
|
||||
* @author 2024 Hamza Mahjoubi <hamza.mahjoubi221@proton.me>
|
||||
*
|
||||
* @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/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Calendar\Reference;
|
||||
|
||||
use OCA\Calendar\AppInfo\Application;
|
||||
use OCP\Collaboration\Reference\ADiscoverableReferenceProvider;
|
||||
use OCP\Collaboration\Reference\IReference;
|
||||
|
||||
use OCP\Collaboration\Reference\Reference;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class ReferenceProvider extends ADiscoverableReferenceProvider {
|
||||
|
||||
|
||||
public function __construct(
|
||||
private IL10N $l10n,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public function getId(): string {
|
||||
return 'calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getTitle(): string {
|
||||
return 'Calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getOrder(): int {
|
||||
return 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getIconUrl(): string {
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->imagePath(Application::APP_ID, 'calendar-dark.svg')
|
||||
);
|
||||
}
|
||||
|
||||
public function matchReference(string $referenceText): bool {
|
||||
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID);
|
||||
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function resolveReference(string $referenceText): ?IReference {
|
||||
if ($this->matchReference($referenceText)) {
|
||||
$token = $this->getCalendarTokenFromLink($referenceText);
|
||||
|
||||
$reference = new Reference($referenceText);
|
||||
$reference->setTitle('calendar');
|
||||
$reference->setDescription($token);
|
||||
$reference->setRichObject(
|
||||
'calendar_widget',
|
||||
[
|
||||
'title' => 'calendar',
|
||||
'token' => $token,
|
||||
'url' => $referenceText,]
|
||||
);
|
||||
|
||||
return $reference;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getCalendarTokenFromLink(string $url): ?string {
|
||||
|
||||
|
||||
if (preg_match('/\/p\/([a-zA-Z0-9]+)/', $url, $output_array)) {
|
||||
return $output_array[1];
|
||||
}
|
||||
return $url;
|
||||
|
||||
}
|
||||
|
||||
public function getCachePrefix(string $referenceId): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getCacheKey(string $referenceId): ?string {
|
||||
return $referenceId;
|
||||
}
|
||||
}
|
|
@ -22,7 +22,8 @@
|
|||
|
||||
<template>
|
||||
<div class="datepicker-button-section">
|
||||
<NcButton v-shortkey="previousShortKeyConf"
|
||||
<NcButton v-if="!isWidget"
|
||||
v-shortkey="previousShortKeyConf"
|
||||
:aria-label="previousLabel"
|
||||
class="datepicker-button-section__previous button"
|
||||
:name="previousLabel"
|
||||
|
@ -32,20 +33,23 @@
|
|||
<ChevronLeftIcon :size="22" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton class="datepicker-button-section__datepicker-label button datepicker-label"
|
||||
<NcButton v-if="!isWidget"
|
||||
class="datepicker-button-section__datepicker-label button datepicker-label"
|
||||
@click.stop.prevent="toggleDatepicker"
|
||||
@mousedown.stop.prevent="doNothing"
|
||||
@mouseup.stop.prevent="doNothing">
|
||||
{{ selectedDate | formatDateRange(view, locale) }}
|
||||
</NcButton>
|
||||
<DatePicker ref="datepicker"
|
||||
class="datepicker-button-section__datepicker"
|
||||
:class="isWidget ? 'datepicker-widget':'datepicker-button-section__datepicker'"
|
||||
:append-to-body="isWidget"
|
||||
:date="selectedDate"
|
||||
:is-all-day="true"
|
||||
:open.sync="isDatepickerOpen"
|
||||
:type="view === 'multiMonthYear' ? 'year' : 'date'"
|
||||
@change="navigateToDate" />
|
||||
<NcButton v-shortkey="nextShortKeyConf"
|
||||
<NcButton v-if="!isWidget"
|
||||
v-shortkey="nextShortKeyConf"
|
||||
:aria-label="nextLabel"
|
||||
class="datepicker-button-section__next button"
|
||||
:name="nextLabel"
|
||||
|
@ -82,6 +86,12 @@ export default {
|
|||
filters: {
|
||||
formatDateRange,
|
||||
},
|
||||
props: {
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDatepickerOpen: false,
|
||||
|
@ -92,6 +102,9 @@ export default {
|
|||
locale: (state) => state.settings.momentLocale,
|
||||
}),
|
||||
selectedDate() {
|
||||
if (this.isWidget) {
|
||||
return getDateFromFirstdayParam(this.$store.getters.widgetDate)
|
||||
}
|
||||
return getDateFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
||||
},
|
||||
previousShortKeyConf() {
|
||||
|
@ -139,6 +152,9 @@ export default {
|
|||
}
|
||||
},
|
||||
view() {
|
||||
if (this.isWidget) {
|
||||
return this.$store.getters.widgetView
|
||||
}
|
||||
return this.$route.params.view
|
||||
},
|
||||
},
|
||||
|
@ -190,6 +206,9 @@ export default {
|
|||
this.navigateToDate(newDate)
|
||||
},
|
||||
navigateToDate(date) {
|
||||
if (this.isWidget) {
|
||||
this.$store.commit('setWidgetDate', { widgetDate: getYYYYMMDDFromDate(date) })
|
||||
} else {
|
||||
const name = this.$route.name
|
||||
const params = Object.assign({}, this.$route.params, {
|
||||
firstDay: getYYYYMMDDFromDate(date),
|
||||
|
@ -201,6 +220,7 @@ export default {
|
|||
}
|
||||
|
||||
this.$router.push({ name, params })
|
||||
}
|
||||
},
|
||||
toggleDatepicker() {
|
||||
this.isDatepickerOpen = !this.isDatepickerOpen
|
||||
|
@ -212,3 +232,9 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.datepicker-widget{
|
||||
width: 135px;
|
||||
margin: 2px 5px 5px 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -59,6 +59,12 @@ export default {
|
|||
components: {
|
||||
NcButton,
|
||||
},
|
||||
props: {
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isAgendaDayViewSelected() {
|
||||
return this.selectedView === 'timeGridDay'
|
||||
|
@ -76,11 +82,17 @@ export default {
|
|||
return this.selectedView === 'listMonth'
|
||||
},
|
||||
selectedView() {
|
||||
if (this.isWidget) {
|
||||
return this.$store.getters.widgetView
|
||||
}
|
||||
return this.$route.params.view
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
view(viewName) {
|
||||
if (this.isWidget) {
|
||||
this.$store.commit('setWidgetView', { viewName })
|
||||
} else {
|
||||
const name = this.$route.name
|
||||
const params = Object.assign({}, this.$route.params, {
|
||||
view: viewName,
|
||||
|
@ -92,6 +104,8 @@ export default {
|
|||
}
|
||||
|
||||
this.$router.push({ name, params })
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<header id="embed-header" role="banner">
|
||||
<div class="embed-header__date-section">
|
||||
<AppNavigationHeaderDatePicker />
|
||||
<AppNavigationHeaderTodayButton />
|
||||
<header :id="isWidget? 'widget-header' :'embed-header'" role="banner">
|
||||
<div :class="isWidget?'widget-header__date-section' :'embed-header__date-section'">
|
||||
<AppNavigationHeaderDatePicker :is-widget="isWidget" />
|
||||
<AppNavigationHeaderTodayButton v-if="!isWidget" />
|
||||
</div>
|
||||
<div class="embed-header__views-section">
|
||||
<AppNavigationHeaderViewButtons />
|
||||
<div :class="isWidget?'widget-header__views-section' :'embed-header__views-section'">
|
||||
<AppNavigationHeaderViewButtons :is-widget="isWidget" />
|
||||
</div>
|
||||
<!-- TODO have one button per calendar -->
|
||||
<div class="embed-header__share-section">
|
||||
<div v-if="!isWidget" class="widget-header__share-section">
|
||||
<Actions>
|
||||
<template #icon>
|
||||
<Download :size="20" decorative />
|
||||
|
@ -74,6 +74,12 @@ export default {
|
|||
CalendarBlank,
|
||||
Download,
|
||||
},
|
||||
props: {
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
subscriptions: 'sortedSubscriptions',
|
||||
|
@ -98,3 +104,34 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
#widget-header {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-main-background);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
overflow: visible;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
|
||||
.widget-header__date-section{
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.view-button-section {
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.datepicker-button-section {
|
||||
display: flex;
|
||||
&__datepicker-label {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<template>
|
||||
<FullCalendar ref="fullCalendar"
|
||||
:class="isWidget? 'fullcalendar-widget': ''"
|
||||
:options="options" />
|
||||
</template>
|
||||
|
||||
|
@ -72,6 +73,10 @@ export default {
|
|||
FullCalendar,
|
||||
},
|
||||
props: {
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Whether or not the user is authenticated
|
||||
*/
|
||||
|
@ -104,8 +109,8 @@ export default {
|
|||
options() {
|
||||
return {
|
||||
// Initialization:
|
||||
initialDate: getYYYYMMDDFromFirstdayParam(this.$route.params.firstDay),
|
||||
initialView: this.$route.params.view,
|
||||
initialDate: getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now'),
|
||||
initialView: this.$route?.params.view ?? 'dayGridMonth',
|
||||
// Data
|
||||
eventSources: this.eventSources,
|
||||
// Plugins
|
||||
|
@ -114,12 +119,12 @@ export default {
|
|||
editable: this.isEditable,
|
||||
selectable: this.isAuthenticatedUser,
|
||||
eventAllow,
|
||||
eventClick: eventClick(this.$store, this.$router, this.$route, window),
|
||||
eventDrop: (...args) => eventDrop(this.$store, this.$refs.fullCalendar.getApi())(...args),
|
||||
eventResize: eventResize(this.$store),
|
||||
navLinkDayClick: navLinkDayClick(this.$router, this.$route),
|
||||
navLinkWeekClick: navLinkWeekClick(this.$router, this.$route),
|
||||
select: select(this.$store, this.$router, this.$route, window),
|
||||
eventClick: eventClick(this.$store, this.$router, this.$route, window, this.isWidget, this.$refs.fullCalendar),
|
||||
eventDrop: this.isWidget ? false : (...args) => eventDrop(this.$store, this.$refs.fullCalendar.getApi())(...args),
|
||||
eventResize: this.isWidget ? false : eventResize(this.$store),
|
||||
navLinkDayClick: this.isWidget ? false : navLinkDayClick(this.$router, this.$route),
|
||||
navLinkWeekClick: this.isWidget ? false : navLinkWeekClick(this.$router, this.$route),
|
||||
select: this.isWidget ? false : select(this.$store, this.$router, this.$route, window),
|
||||
navLinks: true,
|
||||
// Localization
|
||||
...getDateFormattingConfig(),
|
||||
|
@ -151,6 +156,12 @@ export default {
|
|||
eventSources() {
|
||||
return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
|
||||
},
|
||||
widgetView() {
|
||||
return this.$store.getters.widgetView
|
||||
},
|
||||
widgetDate() {
|
||||
return this.$store.getters.widgetDate
|
||||
},
|
||||
/**
|
||||
* FullCalendar Plugins
|
||||
*
|
||||
|
@ -170,11 +181,19 @@ export default {
|
|||
isEditable() {
|
||||
// We do not allow drag and drop when the editor is open.
|
||||
return this.isAuthenticatedUser
|
||||
&& this.$route.name !== 'EditPopoverView'
|
||||
&& this.$route.name !== 'EditSidebarView'
|
||||
&& this.$route?.name !== 'EditPopoverView'
|
||||
&& this.$route?.name !== 'EditSidebarView'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
widgetView(newView) {
|
||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||
calendarApi.changeView(newView)
|
||||
},
|
||||
widgetDate(newDate) {
|
||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(newDate))
|
||||
},
|
||||
modificationCount: debounce(function() {
|
||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||
calendarApi.refetchEvents()
|
||||
|
@ -226,6 +245,7 @@ export default {
|
|||
* This view is not used as a router view,
|
||||
* hence we can't use beforeRouteUpdate directly.
|
||||
*/
|
||||
if (!this.isWidget) {
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
if (to.params.firstDay !== from.params.firstDay) {
|
||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||
|
@ -262,6 +282,7 @@ export default {
|
|||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
|
@ -277,7 +298,7 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style scoped lang="scss">
|
||||
.calendar-grid-checkbox {
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
|
@ -293,4 +314,10 @@ export default {
|
|||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.fullcalendar-widget{
|
||||
min-height: 500px;
|
||||
:deep(.fc-col-header-cell-cushion){
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,17 +36,23 @@ import { emit } from '@nextcloud/event-bus'
|
|||
* @param {object} router The Vue router
|
||||
* @param {object} route The current Vue route
|
||||
* @param {Window} window The window object
|
||||
* @param {boolean} isWidget Whether the calendar is embedded in a widget
|
||||
* @param {object} widgetRef
|
||||
* @return {Function}
|
||||
*/
|
||||
export default function(store, router, route, window) {
|
||||
export default function(store, router, route, window, isWidget = false, widgetRef = undefined) {
|
||||
|
||||
return function({ event }) {
|
||||
if (isWidget) {
|
||||
store.commit('setWidgetRef', { widgetRef: widgetRef.$el })
|
||||
}
|
||||
switch (event.extendedProps.objectType) {
|
||||
case 'VEVENT':
|
||||
handleEventClick(event, store, router, route, window)
|
||||
handleEventClick(event, store, router, route, window, isWidget)
|
||||
break
|
||||
|
||||
case 'VTODO':
|
||||
handleToDoClick(event, store, route, window)
|
||||
handleToDoClick(event, store, route, window, isWidget)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -60,8 +66,13 @@ export default function(store, router, route, window) {
|
|||
* @param {object} router The Vue router
|
||||
* @param {object} route The current Vue route
|
||||
* @param {Window} window The window object
|
||||
* @param {boolean} isWidget Whether the calendar is embedded in a widget
|
||||
*/
|
||||
function handleEventClick(event, store, router, route, window) {
|
||||
function handleEventClick(event, store, router, route, window, isWidget = false) {
|
||||
if (isWidget) {
|
||||
store.commit('setSelectedEvent', { object: event.extendedProps.objectId, recurrenceId: event.extendedProps.recurrenceId })
|
||||
return
|
||||
}
|
||||
let desiredRoute = store.state.settings.skipPopover
|
||||
? 'EditSidebarView'
|
||||
: 'EditPopoverView'
|
||||
|
@ -95,10 +106,11 @@ function handleEventClick(event, store, router, route, window) {
|
|||
* @param {object} store The Vuex store
|
||||
* @param {object} route The current Vue route
|
||||
* @param {Window} window The window object
|
||||
* @param isWidget
|
||||
*/
|
||||
function handleToDoClick(event, store, route, window) {
|
||||
function handleToDoClick(event, store, route, window, isWidget = false) {
|
||||
|
||||
if (isPublicOrEmbeddedRoute(route.name)) {
|
||||
if (isWidget || isPublicOrEmbeddedRoute(route.name)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,13 @@ import { showError } from '@nextcloud/dialogs'
|
|||
* See inline for more documentation
|
||||
*/
|
||||
export default {
|
||||
props: {
|
||||
// Whether or not the calendar is embedded in a widget
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Indicator whether or not the event is currently loading, saving or being deleted
|
||||
|
@ -396,6 +403,10 @@ export default {
|
|||
* Closes the editor and returns to normal calendar-view
|
||||
*/
|
||||
closeEditor() {
|
||||
if (this.isWidget) {
|
||||
this.$store.commit('closeWidgetEventDetails')
|
||||
return
|
||||
}
|
||||
const params = Object.assign({}, this.$store.state.route.params)
|
||||
delete params.object
|
||||
delete params.recurrenceId
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { registerWidget, NcCustomPickerRenderResult } from '@nextcloud/vue/dist/Functions/registerReference.js'
|
||||
import { linkTo } from '@nextcloud/router'
|
||||
import { getRequestToken } from '@nextcloud/auth'
|
||||
import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import '../css/calendar.scss'
|
||||
|
||||
__webpack_nonce__ = btoa(getRequestToken()) // eslint-disable-line
|
||||
__webpack_public_path__ = linkTo('calendar', 'js/') // eslint-disable-line
|
||||
|
||||
registerWidget('calendar_widget', async (el, { richObjectType, richObject, accessible, interactive }) => {
|
||||
const { default: Vue } = await import('vue')
|
||||
const { default: Calendar } = await import('./views/Calendar.vue')
|
||||
const { default: store } = await import('./store/index.js')
|
||||
Vue.prototype.$t = translate
|
||||
Vue.prototype.$n = translatePlural
|
||||
Vue.mixin({ methods: { t, n } })
|
||||
|
||||
const Widget = Vue.extend(Calendar)
|
||||
const vueElement = new Widget({
|
||||
store,
|
||||
propsData: {
|
||||
isWidget: true,
|
||||
referenceToken: richObject.token,
|
||||
},
|
||||
}).$mount(el)
|
||||
return new NcCustomPickerRenderResult(vueElement.$el, vueElement)
|
||||
}, (el, renderResult) => {
|
||||
renderResult.object.$destroy()
|
||||
}, true)
|
|
@ -59,6 +59,11 @@ const state = {
|
|||
calendarsById: {},
|
||||
initialCalendarsLoaded: false,
|
||||
editCalendarModal: undefined,
|
||||
widgetView: 'dayGridMonth',
|
||||
widgetDate: 'now',
|
||||
widgetEventDetailsOpen: false,
|
||||
widgetEventDetails: {},
|
||||
widgetRef: undefined,
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
|
@ -83,6 +88,30 @@ const mutations = {
|
|||
state.trashBin = trashBin
|
||||
},
|
||||
|
||||
setWidgetView(state, { viewName }) {
|
||||
state.widgetView = viewName
|
||||
},
|
||||
|
||||
setWidgetDate(state, { widgetDate }) {
|
||||
state.widgetDate = widgetDate
|
||||
},
|
||||
|
||||
setWidgetRef(state, { widgetRef }) {
|
||||
state.widgetRef = widgetRef
|
||||
},
|
||||
|
||||
setSelectedEvent(state, { object, recurrenceId }) {
|
||||
state.widgetEventDetailsOpen = true
|
||||
state.widgetEventDetails = {
|
||||
object,
|
||||
recurrenceId,
|
||||
}
|
||||
},
|
||||
|
||||
closeWidgetEventDetails(state) {
|
||||
state.widgetEventDetailsOpen = false
|
||||
},
|
||||
|
||||
addScheduleInbox(state, { scheduleInbox }) {
|
||||
state.scheduleInbox = scheduleInbox
|
||||
},
|
||||
|
@ -444,6 +473,22 @@ const getters = {
|
|||
.sort((a, b) => a.order - b.order)
|
||||
},
|
||||
|
||||
widgetView(state) {
|
||||
return state.widgetView
|
||||
},
|
||||
|
||||
widgetDate(state) {
|
||||
return state.widgetDate
|
||||
},
|
||||
|
||||
widgetEventDetailsOpen(state) {
|
||||
return state.widgetEventDetailsOpen
|
||||
},
|
||||
|
||||
widgetRef(state) {
|
||||
return state.widgetRef
|
||||
},
|
||||
|
||||
hasTrashBin(state) {
|
||||
return state.trashBin !== undefined && state.trashBin.retentionDuration !== 0
|
||||
},
|
||||
|
|
|
@ -21,8 +21,20 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<NcContent app-name="calendar" :class="classNames">
|
||||
<AppNavigation v-if="!isEmbedded && !showEmptyCalendarScreen">
|
||||
<div v-if="isWidget" class="calendar-Widget">
|
||||
<EmbedTopNavigation :is-widget="true" />
|
||||
|
||||
<CalendarGrid v-if="!showEmptyCalendarScreen"
|
||||
ref="calendarGridWidget"
|
||||
:is-widget="isWidget"
|
||||
:is-authenticated-user="isAuthenticatedUser" />
|
||||
<EmptyCalendar v-else />
|
||||
|
||||
<EditSimple v-if="showWidgetEventDetails" :is-widget="true" />
|
||||
</div>
|
||||
|
||||
<NcContent v-else app-name="calendar" :class="classNames">
|
||||
<AppNavigation v-if="!isWidget &&!isEmbedded && !showEmptyCalendarScreen">
|
||||
<!-- Date Picker, View Buttons, Today Button -->
|
||||
<AppNavigationHeader :is-public="!isAuthenticatedUser" />
|
||||
<template #list>
|
||||
|
@ -77,6 +89,7 @@ import EmbedTopNavigation from '../components/AppNavigation/EmbedTopNavigation.v
|
|||
import EmptyCalendar from '../components/EmptyCalendar.vue'
|
||||
import CalendarGrid from '../components/CalendarGrid.vue'
|
||||
import EditCalendarModal from '../components/AppNavigation/EditCalendarModal.vue'
|
||||
import EditSimple from './EditSimple.vue'
|
||||
|
||||
// Import CalDAV related methods
|
||||
import {
|
||||
|
@ -123,6 +136,17 @@ export default {
|
|||
CalendarListNew,
|
||||
Trashbin,
|
||||
EditCalendarModal,
|
||||
EditSimple,
|
||||
},
|
||||
props: {
|
||||
isWidget: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
referenceToken: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -152,29 +176,39 @@ export default {
|
|||
attachmentsFolder: state => state.settings.attachmentsFolder,
|
||||
}),
|
||||
defaultDate() {
|
||||
return getYYYYMMDDFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
||||
return getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now')
|
||||
},
|
||||
isEditable() {
|
||||
// We do not allow drag and drop when the editor is open.
|
||||
return !this.isPublicShare
|
||||
&& !this.isEmbedded
|
||||
&& this.$route.name !== 'EditPopoverView'
|
||||
&& this.$route.name !== 'EditSidebarView'
|
||||
&& !this.isWidget
|
||||
&& this.$route?.name !== 'EditPopoverView'
|
||||
&& this.$route?.name !== 'EditSidebarView'
|
||||
},
|
||||
isSelectable() {
|
||||
return !this.isPublicShare && !this.isEmbedded
|
||||
return !this.isPublicShare && !this.isEmbedded && !this.isWidget
|
||||
},
|
||||
isAuthenticatedUser() {
|
||||
return !this.isPublicShare && !this.isEmbedded
|
||||
return !this.isPublicShare && !this.isEmbedded && !this.isWidget
|
||||
},
|
||||
isPublicShare() {
|
||||
if (this.isWidget) {
|
||||
return false
|
||||
}
|
||||
return this.$route.name.startsWith('Public')
|
||||
},
|
||||
isEmbedded() {
|
||||
if (this.isWidget) {
|
||||
return false
|
||||
}
|
||||
return this.$route.name.startsWith('Embed')
|
||||
},
|
||||
showWidgetEventDetails() {
|
||||
return this.$store.getters.widgetEventDetailsOpen && this.$refs.calendarGridWidget.$el === this.$store.getters.widgetRef
|
||||
},
|
||||
showHeader() {
|
||||
return this.isPublicShare && this.isEmbedded
|
||||
return this.isPublicShare && this.isEmbedded && this.isWidget
|
||||
},
|
||||
classNames() {
|
||||
if (this.isEmbedded) {
|
||||
|
@ -229,9 +263,9 @@ export default {
|
|||
})
|
||||
this.$store.dispatch('initializeCalendarJsConfig')
|
||||
|
||||
if (this.$route.name.startsWith('Public') || this.$route.name.startsWith('Embed')) {
|
||||
if (this.$route?.name.startsWith('Public') || this.$route?.name.startsWith('Embed') || this.isWidget) {
|
||||
await initializeClientForPublicView()
|
||||
const tokens = this.$route.params.tokens.split('-')
|
||||
const tokens = this.isWidget ? [this.referenceToken] : this.$route.params.tokens.split('-')
|
||||
const calendars = await this.$store.dispatch('getPublicCalendars', { tokens })
|
||||
this.loadingCalendars = false
|
||||
|
||||
|
@ -302,3 +336,9 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.calendar-Widget {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
<template>
|
||||
<Popover ref="popover"
|
||||
:shown="isVisible"
|
||||
:shown="showPopover"
|
||||
:auto-hide="false"
|
||||
:placement="placement"
|
||||
:boundary="boundaryElement"
|
||||
|
@ -148,7 +148,8 @@
|
|||
:calendar-id="calendarId"
|
||||
@close="closeEditorAndSkipAction" />
|
||||
|
||||
<SaveButtons class="event-popover__buttons"
|
||||
<SaveButtons v-if="!isWidget"
|
||||
class="event-popover__buttons"
|
||||
:can-create-recurrence-exception="canCreateRecurrenceException"
|
||||
:is-new="isNew"
|
||||
:is-read-only="isReadOnlyOrViewing"
|
||||
|
@ -236,7 +237,7 @@ export default {
|
|||
placement: 'auto',
|
||||
hasLocation: false,
|
||||
hasDescription: false,
|
||||
boundaryElement: document.querySelector('#app-content-vue > .fc'),
|
||||
boundaryElement: null,
|
||||
isVisible: true,
|
||||
isViewing: true,
|
||||
}
|
||||
|
@ -244,15 +245,22 @@ export default {
|
|||
computed: {
|
||||
...mapState({
|
||||
hideEventExport: (state) => state.settings.hideEventExport,
|
||||
widgetEventDetailsOpen: (state) => state.calendars.widgetEventDetailsOpen,
|
||||
widgetEventDetails: (state) => state.calendars.widgetEventDetails,
|
||||
widgetRef: (state) => state.calendars.widgetRef,
|
||||
}),
|
||||
|
||||
showPopover() {
|
||||
return this.isVisible || this.widgetEventDetailsOpen
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if the current event is read only or the user is viewing the event
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isReadOnlyOrViewing() {
|
||||
return this.isReadOnly || this.isViewing
|
||||
return this.isReadOnly || this.isViewing || this.isWidget
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -260,7 +268,7 @@ export default {
|
|||
this.repositionPopover()
|
||||
|
||||
// Hide popover when changing the view until the user selects a slot again
|
||||
this.isVisible = to.params.view === from.params.view
|
||||
this.isVisible = to?.params.view === from?.params.view
|
||||
},
|
||||
calendarObjectInstance() {
|
||||
this.hasLocation = false
|
||||
|
@ -281,7 +289,15 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
if (this.isWidget) {
|
||||
const objectId = this.widgetEventDetails.object
|
||||
const recurrenceId = this.widgetEventDetails.recurrenceId
|
||||
await this.$store.dispatch('getCalendarObjectInstanceByObjectIdAndRecurrenceId', { objectId, recurrenceId })
|
||||
this.calendarId = this.calendarObject.calendarId
|
||||
this.isLoading = false
|
||||
}
|
||||
this.boundaryElement = this.isWidget ? document.querySelector('.fc') : document.querySelector('#app-content-vue > .fc')
|
||||
window.addEventListener('keydown', this.keyboardCloseEditor)
|
||||
window.addEventListener('keydown', this.keyboardSaveEvent)
|
||||
window.addEventListener('keydown', this.keyboardDeleteEvent)
|
||||
|
@ -314,8 +330,13 @@ export default {
|
|||
},
|
||||
getDomElementForPopover(isNew, route) {
|
||||
let matchingDomObject
|
||||
if (this.isWidget) {
|
||||
const objectId = this.widgetEventDetails.object
|
||||
const recurrenceId = this.widgetEventDetails.recurrenceId
|
||||
|
||||
if (isNew) {
|
||||
matchingDomObject = this.widgetRef.querySelector(`.fc-event[data-object-id="${objectId}"][data-recurrence-id="${recurrenceId}"]`)
|
||||
this.placement = 'auto'
|
||||
} else if (isNew) {
|
||||
matchingDomObject = document.querySelector('.fc-highlight')
|
||||
this.placement = 'auto'
|
||||
|
||||
|
@ -344,7 +365,7 @@ export default {
|
|||
return matchingDomObject
|
||||
},
|
||||
repositionPopover() {
|
||||
const isNew = this.$route.name === 'NewPopoverView'
|
||||
const isNew = this.isWidget ? false : this.$route.name === 'NewPopoverView'
|
||||
this.$refs.popover.$children[0].$refs.reference = this.getDomElementForPopover(isNew, this.$route)
|
||||
this.$refs.popover.$children[0].$refs.popper.dispose()
|
||||
this.$refs.popover.$children[0].$refs.popper.init()
|
||||
|
|
|
@ -8,6 +8,9 @@ const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-m
|
|||
// Add dashboard entry
|
||||
webpackConfig.entry.dashboard = path.join(__dirname, 'src', 'dashboard.js')
|
||||
|
||||
//Add reference entry
|
||||
webpackConfig.entry['reference'] = path.join(__dirname, 'src', 'reference.js')
|
||||
|
||||
// Add appointments entries
|
||||
webpackConfig.entry['appointments-booking'] = path.join(__dirname, 'src', 'appointments/main-booking.js')
|
||||
webpackConfig.entry['appointments-confirmation'] = path.join(__dirname, 'src', 'appointments/main-confirmation.js')
|
||||
|
|
Loading…
Reference in New Issue