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\Dashboard\CalendarWidgetV2;
|
||||||
use OCA\Calendar\Events\BeforeAppointmentBookedEvent;
|
use OCA\Calendar\Events\BeforeAppointmentBookedEvent;
|
||||||
use OCA\Calendar\Listener\AppointmentBookedListener;
|
use OCA\Calendar\Listener\AppointmentBookedListener;
|
||||||
|
use OCA\Calendar\Listener\CalendarReferenceListener;
|
||||||
use OCA\Calendar\Listener\UserDeletedListener;
|
use OCA\Calendar\Listener\UserDeletedListener;
|
||||||
use OCA\Calendar\Notification\Notifier;
|
use OCA\Calendar\Notification\Notifier;
|
||||||
use OCA\Calendar\Profile\AppointmentsAction;
|
use OCA\Calendar\Profile\AppointmentsAction;
|
||||||
|
use OCA\Calendar\Reference\ReferenceProvider;
|
||||||
use OCP\AppFramework\App;
|
use OCP\AppFramework\App;
|
||||||
use OCP\AppFramework\Bootstrap\IBootContext;
|
use OCP\AppFramework\Bootstrap\IBootContext;
|
||||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||||
|
use OCP\Collaboration\Reference\RenderReferenceEvent;
|
||||||
use OCP\Dashboard\IAPIWidgetV2;
|
use OCP\Dashboard\IAPIWidgetV2;
|
||||||
use OCP\User\Events\UserDeletedEvent;
|
use OCP\User\Events\UserDeletedEvent;
|
||||||
use function method_exists;
|
use function method_exists;
|
||||||
|
@ -65,9 +68,11 @@ class Application extends App implements IBootstrap {
|
||||||
if (method_exists($context, 'registerProfileLinkAction')) {
|
if (method_exists($context, 'registerProfileLinkAction')) {
|
||||||
$context->registerProfileLinkAction(AppointmentsAction::class);
|
$context->registerProfileLinkAction(AppointmentsAction::class);
|
||||||
}
|
}
|
||||||
|
$context->registerReferenceProvider(ReferenceProvider::class);
|
||||||
|
|
||||||
$context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class);
|
$context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class);
|
||||||
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||||||
|
$context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class);
|
||||||
|
|
||||||
$context->registerNotifierService(Notifier::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>
|
<template>
|
||||||
<div class="datepicker-button-section">
|
<div class="datepicker-button-section">
|
||||||
<NcButton v-shortkey="previousShortKeyConf"
|
<NcButton v-if="!isWidget"
|
||||||
|
v-shortkey="previousShortKeyConf"
|
||||||
:aria-label="previousLabel"
|
:aria-label="previousLabel"
|
||||||
class="datepicker-button-section__previous button"
|
class="datepicker-button-section__previous button"
|
||||||
:name="previousLabel"
|
:name="previousLabel"
|
||||||
|
@ -32,20 +33,23 @@
|
||||||
<ChevronLeftIcon :size="22" />
|
<ChevronLeftIcon :size="22" />
|
||||||
</template>
|
</template>
|
||||||
</NcButton>
|
</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"
|
@click.stop.prevent="toggleDatepicker"
|
||||||
@mousedown.stop.prevent="doNothing"
|
@mousedown.stop.prevent="doNothing"
|
||||||
@mouseup.stop.prevent="doNothing">
|
@mouseup.stop.prevent="doNothing">
|
||||||
{{ selectedDate | formatDateRange(view, locale) }}
|
{{ selectedDate | formatDateRange(view, locale) }}
|
||||||
</NcButton>
|
</NcButton>
|
||||||
<DatePicker ref="datepicker"
|
<DatePicker ref="datepicker"
|
||||||
class="datepicker-button-section__datepicker"
|
:class="isWidget ? 'datepicker-widget':'datepicker-button-section__datepicker'"
|
||||||
|
:append-to-body="isWidget"
|
||||||
:date="selectedDate"
|
:date="selectedDate"
|
||||||
:is-all-day="true"
|
:is-all-day="true"
|
||||||
:open.sync="isDatepickerOpen"
|
:open.sync="isDatepickerOpen"
|
||||||
:type="view === 'multiMonthYear' ? 'year' : 'date'"
|
:type="view === 'multiMonthYear' ? 'year' : 'date'"
|
||||||
@change="navigateToDate" />
|
@change="navigateToDate" />
|
||||||
<NcButton v-shortkey="nextShortKeyConf"
|
<NcButton v-if="!isWidget"
|
||||||
|
v-shortkey="nextShortKeyConf"
|
||||||
:aria-label="nextLabel"
|
:aria-label="nextLabel"
|
||||||
class="datepicker-button-section__next button"
|
class="datepicker-button-section__next button"
|
||||||
:name="nextLabel"
|
:name="nextLabel"
|
||||||
|
@ -82,6 +86,12 @@ export default {
|
||||||
filters: {
|
filters: {
|
||||||
formatDateRange,
|
formatDateRange,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isDatepickerOpen: false,
|
isDatepickerOpen: false,
|
||||||
|
@ -92,6 +102,9 @@ export default {
|
||||||
locale: (state) => state.settings.momentLocale,
|
locale: (state) => state.settings.momentLocale,
|
||||||
}),
|
}),
|
||||||
selectedDate() {
|
selectedDate() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
return getDateFromFirstdayParam(this.$store.getters.widgetDate)
|
||||||
|
}
|
||||||
return getDateFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
return getDateFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
||||||
},
|
},
|
||||||
previousShortKeyConf() {
|
previousShortKeyConf() {
|
||||||
|
@ -139,6 +152,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view() {
|
view() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
return this.$store.getters.widgetView
|
||||||
|
}
|
||||||
return this.$route.params.view
|
return this.$route.params.view
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -190,17 +206,21 @@ export default {
|
||||||
this.navigateToDate(newDate)
|
this.navigateToDate(newDate)
|
||||||
},
|
},
|
||||||
navigateToDate(date) {
|
navigateToDate(date) {
|
||||||
const name = this.$route.name
|
if (this.isWidget) {
|
||||||
const params = Object.assign({}, this.$route.params, {
|
this.$store.commit('setWidgetDate', { widgetDate: getYYYYMMDDFromDate(date) })
|
||||||
firstDay: getYYYYMMDDFromDate(date),
|
} else {
|
||||||
})
|
const name = this.$route.name
|
||||||
|
const params = Object.assign({}, this.$route.params, {
|
||||||
|
firstDay: getYYYYMMDDFromDate(date),
|
||||||
|
})
|
||||||
|
|
||||||
// Don't push new route when day didn't change
|
// Don't push new route when day didn't change
|
||||||
if (this.$route.params.firstDay === getYYYYMMDDFromDate(date)) {
|
if (this.$route.params.firstDay === getYYYYMMDDFromDate(date)) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.push({ name, params })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push({ name, params })
|
|
||||||
},
|
},
|
||||||
toggleDatepicker() {
|
toggleDatepicker() {
|
||||||
this.isDatepickerOpen = !this.isDatepickerOpen
|
this.isDatepickerOpen = !this.isDatepickerOpen
|
||||||
|
@ -212,3 +232,9 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.datepicker-widget{
|
||||||
|
width: 135px;
|
||||||
|
margin: 2px 5px 5px 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -59,6 +59,12 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
NcButton,
|
NcButton,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isAgendaDayViewSelected() {
|
isAgendaDayViewSelected() {
|
||||||
return this.selectedView === 'timeGridDay'
|
return this.selectedView === 'timeGridDay'
|
||||||
|
@ -76,22 +82,30 @@ export default {
|
||||||
return this.selectedView === 'listMonth'
|
return this.selectedView === 'listMonth'
|
||||||
},
|
},
|
||||||
selectedView() {
|
selectedView() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
return this.$store.getters.widgetView
|
||||||
|
}
|
||||||
return this.$route.params.view
|
return this.$route.params.view
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
view(viewName) {
|
view(viewName) {
|
||||||
const name = this.$route.name
|
if (this.isWidget) {
|
||||||
const params = Object.assign({}, this.$route.params, {
|
this.$store.commit('setWidgetView', { viewName })
|
||||||
view: viewName,
|
} else {
|
||||||
})
|
const name = this.$route.name
|
||||||
|
const params = Object.assign({}, this.$route.params, {
|
||||||
|
view: viewName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Don't push new route when view didn't change
|
||||||
|
if (this.$route.params.view === viewName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.push({ name, params })
|
||||||
|
|
||||||
// Don't push new route when view didn't change
|
|
||||||
if (this.$route.params.view === viewName) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push({ name, params })
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<header id="embed-header" role="banner">
|
<header :id="isWidget? 'widget-header' :'embed-header'" role="banner">
|
||||||
<div class="embed-header__date-section">
|
<div :class="isWidget?'widget-header__date-section' :'embed-header__date-section'">
|
||||||
<AppNavigationHeaderDatePicker />
|
<AppNavigationHeaderDatePicker :is-widget="isWidget" />
|
||||||
<AppNavigationHeaderTodayButton />
|
<AppNavigationHeaderTodayButton v-if="!isWidget" />
|
||||||
</div>
|
</div>
|
||||||
<div class="embed-header__views-section">
|
<div :class="isWidget?'widget-header__views-section' :'embed-header__views-section'">
|
||||||
<AppNavigationHeaderViewButtons />
|
<AppNavigationHeaderViewButtons :is-widget="isWidget" />
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO have one button per calendar -->
|
<!-- TODO have one button per calendar -->
|
||||||
<div class="embed-header__share-section">
|
<div v-if="!isWidget" class="widget-header__share-section">
|
||||||
<Actions>
|
<Actions>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Download :size="20" decorative />
|
<Download :size="20" decorative />
|
||||||
|
@ -74,6 +74,12 @@ export default {
|
||||||
CalendarBlank,
|
CalendarBlank,
|
||||||
Download,
|
Download,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
subscriptions: 'sortedSubscriptions',
|
subscriptions: 'sortedSubscriptions',
|
||||||
|
@ -98,3 +104,34 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<FullCalendar ref="fullCalendar"
|
<FullCalendar ref="fullCalendar"
|
||||||
|
:class="isWidget? 'fullcalendar-widget': ''"
|
||||||
:options="options" />
|
:options="options" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -72,6 +73,10 @@ export default {
|
||||||
FullCalendar,
|
FullCalendar,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Whether or not the user is authenticated
|
* Whether or not the user is authenticated
|
||||||
*/
|
*/
|
||||||
|
@ -104,8 +109,8 @@ export default {
|
||||||
options() {
|
options() {
|
||||||
return {
|
return {
|
||||||
// Initialization:
|
// Initialization:
|
||||||
initialDate: getYYYYMMDDFromFirstdayParam(this.$route.params.firstDay),
|
initialDate: getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now'),
|
||||||
initialView: this.$route.params.view,
|
initialView: this.$route?.params.view ?? 'dayGridMonth',
|
||||||
// Data
|
// Data
|
||||||
eventSources: this.eventSources,
|
eventSources: this.eventSources,
|
||||||
// Plugins
|
// Plugins
|
||||||
|
@ -114,12 +119,12 @@ export default {
|
||||||
editable: this.isEditable,
|
editable: this.isEditable,
|
||||||
selectable: this.isAuthenticatedUser,
|
selectable: this.isAuthenticatedUser,
|
||||||
eventAllow,
|
eventAllow,
|
||||||
eventClick: eventClick(this.$store, this.$router, this.$route, window),
|
eventClick: eventClick(this.$store, this.$router, this.$route, window, this.isWidget, this.$refs.fullCalendar),
|
||||||
eventDrop: (...args) => eventDrop(this.$store, this.$refs.fullCalendar.getApi())(...args),
|
eventDrop: this.isWidget ? false : (...args) => eventDrop(this.$store, this.$refs.fullCalendar.getApi())(...args),
|
||||||
eventResize: eventResize(this.$store),
|
eventResize: this.isWidget ? false : eventResize(this.$store),
|
||||||
navLinkDayClick: navLinkDayClick(this.$router, this.$route),
|
navLinkDayClick: this.isWidget ? false : navLinkDayClick(this.$router, this.$route),
|
||||||
navLinkWeekClick: navLinkWeekClick(this.$router, this.$route),
|
navLinkWeekClick: this.isWidget ? false : navLinkWeekClick(this.$router, this.$route),
|
||||||
select: select(this.$store, this.$router, this.$route, window),
|
select: this.isWidget ? false : select(this.$store, this.$router, this.$route, window),
|
||||||
navLinks: true,
|
navLinks: true,
|
||||||
// Localization
|
// Localization
|
||||||
...getDateFormattingConfig(),
|
...getDateFormattingConfig(),
|
||||||
|
@ -151,6 +156,12 @@ export default {
|
||||||
eventSources() {
|
eventSources() {
|
||||||
return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
|
return this.$store.getters.enabledCalendars.map(eventSource(this.$store))
|
||||||
},
|
},
|
||||||
|
widgetView() {
|
||||||
|
return this.$store.getters.widgetView
|
||||||
|
},
|
||||||
|
widgetDate() {
|
||||||
|
return this.$store.getters.widgetDate
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* FullCalendar Plugins
|
* FullCalendar Plugins
|
||||||
*
|
*
|
||||||
|
@ -170,11 +181,19 @@ export default {
|
||||||
isEditable() {
|
isEditable() {
|
||||||
// We do not allow drag and drop when the editor is open.
|
// We do not allow drag and drop when the editor is open.
|
||||||
return this.isAuthenticatedUser
|
return this.isAuthenticatedUser
|
||||||
&& this.$route.name !== 'EditPopoverView'
|
&& this.$route?.name !== 'EditPopoverView'
|
||||||
&& this.$route.name !== 'EditSidebarView'
|
&& this.$route?.name !== 'EditSidebarView'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
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() {
|
modificationCount: debounce(function() {
|
||||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||||
calendarApi.refetchEvents()
|
calendarApi.refetchEvents()
|
||||||
|
@ -226,40 +245,42 @@ export default {
|
||||||
* This view is not used as a router view,
|
* This view is not used as a router view,
|
||||||
* hence we can't use beforeRouteUpdate directly.
|
* hence we can't use beforeRouteUpdate directly.
|
||||||
*/
|
*/
|
||||||
this.$router.beforeEach((to, from, next) => {
|
if (!this.isWidget) {
|
||||||
if (to.params.firstDay !== from.params.firstDay) {
|
this.$router.beforeEach((to, from, next) => {
|
||||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
if (to.params.firstDay !== from.params.firstDay) {
|
||||||
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(to.params.firstDay))
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||||
}
|
calendarApi.gotoDate(getYYYYMMDDFromFirstdayParam(to.params.firstDay))
|
||||||
if (to.params.view !== from.params.view) {
|
}
|
||||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
if (to.params.view !== from.params.view) {
|
||||||
calendarApi.changeView(to.params.view)
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||||
this.saveNewView(to.params.view)
|
calendarApi.changeView(to.params.view)
|
||||||
}
|
this.saveNewView(to.params.view)
|
||||||
|
}
|
||||||
|
|
||||||
if ((from.name === 'NewPopoverView' || from.name === 'NewSidebarView')
|
if ((from.name === 'NewPopoverView' || from.name === 'NewSidebarView')
|
||||||
&& to.name !== 'NewPopoverView'
|
&& to.name !== 'NewPopoverView'
|
||||||
&& to.name !== 'NewSidebarView') {
|
&& to.name !== 'NewSidebarView') {
|
||||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||||
calendarApi.unselect()
|
calendarApi.unselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Trigger the select event programmatically on initial page load to show the new event
|
// Trigger the select event programmatically on initial page load to show the new event
|
||||||
// in the grid. Wait for the next tick because the ref isn't available right away.
|
// in the grid. Wait for the next tick because the ref isn't available right away.
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
if (['NewPopoverView', 'NewSidebarView'].includes(this.$route.name)) {
|
if (['NewPopoverView', 'NewSidebarView'].includes(this.$route.name)) {
|
||||||
const start = new Date(parseInt(this.$route.params.dtstart) * 1000)
|
const start = new Date(parseInt(this.$route.params.dtstart) * 1000)
|
||||||
const end = new Date(parseInt(this.$route.params.dtend) * 1000)
|
const end = new Date(parseInt(this.$route.params.dtend) * 1000)
|
||||||
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
|
if (!isNaN(start.getTime()) && !isNaN(end.getTime())) {
|
||||||
const calendarApi = this.$refs.fullCalendar.getApi()
|
const calendarApi = this.$refs.fullCalendar.getApi()
|
||||||
calendarApi.select({
|
calendarApi.select({
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
allDay: this.$route.params.allDay === '1',
|
allDay: this.$route.params.allDay === '1',
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -277,7 +298,7 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style scoped lang="scss">
|
||||||
.calendar-grid-checkbox {
|
.calendar-grid-checkbox {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
|
@ -293,4 +314,10 @@ export default {
|
||||||
height: 16px;
|
height: 16px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
.fullcalendar-widget{
|
||||||
|
min-height: 500px;
|
||||||
|
:deep(.fc-col-header-cell-cushion){
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,17 +36,23 @@ import { emit } from '@nextcloud/event-bus'
|
||||||
* @param {object} router The Vue router
|
* @param {object} router The Vue router
|
||||||
* @param {object} route The current Vue route
|
* @param {object} route The current Vue route
|
||||||
* @param {Window} window The window object
|
* @param {Window} window The window object
|
||||||
|
* @param {boolean} isWidget Whether the calendar is embedded in a widget
|
||||||
|
* @param {object} widgetRef
|
||||||
* @return {Function}
|
* @return {Function}
|
||||||
*/
|
*/
|
||||||
export default function(store, router, route, window) {
|
export default function(store, router, route, window, isWidget = false, widgetRef = undefined) {
|
||||||
|
|
||||||
return function({ event }) {
|
return function({ event }) {
|
||||||
|
if (isWidget) {
|
||||||
|
store.commit('setWidgetRef', { widgetRef: widgetRef.$el })
|
||||||
|
}
|
||||||
switch (event.extendedProps.objectType) {
|
switch (event.extendedProps.objectType) {
|
||||||
case 'VEVENT':
|
case 'VEVENT':
|
||||||
handleEventClick(event, store, router, route, window)
|
handleEventClick(event, store, router, route, window, isWidget)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'VTODO':
|
case 'VTODO':
|
||||||
handleToDoClick(event, store, route, window)
|
handleToDoClick(event, store, route, window, isWidget)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,8 +66,13 @@ export default function(store, router, route, window) {
|
||||||
* @param {object} router The Vue router
|
* @param {object} router The Vue router
|
||||||
* @param {object} route The current Vue route
|
* @param {object} route The current Vue route
|
||||||
* @param {Window} window The window object
|
* @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
|
let desiredRoute = store.state.settings.skipPopover
|
||||||
? 'EditSidebarView'
|
? 'EditSidebarView'
|
||||||
: 'EditPopoverView'
|
: 'EditPopoverView'
|
||||||
|
@ -95,10 +106,11 @@ function handleEventClick(event, store, router, route, window) {
|
||||||
* @param {object} store The Vuex store
|
* @param {object} store The Vuex store
|
||||||
* @param {object} route The current Vue route
|
* @param {object} route The current Vue route
|
||||||
* @param {Window} window The window object
|
* @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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,13 @@ import { showError } from '@nextcloud/dialogs'
|
||||||
* See inline for more documentation
|
* See inline for more documentation
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
|
props: {
|
||||||
|
// Whether or not the calendar is embedded in a widget
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// Indicator whether or not the event is currently loading, saving or being deleted
|
// 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
|
* Closes the editor and returns to normal calendar-view
|
||||||
*/
|
*/
|
||||||
closeEditor() {
|
closeEditor() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
this.$store.commit('closeWidgetEventDetails')
|
||||||
|
return
|
||||||
|
}
|
||||||
const params = Object.assign({}, this.$store.state.route.params)
|
const params = Object.assign({}, this.$store.state.route.params)
|
||||||
delete params.object
|
delete params.object
|
||||||
delete params.recurrenceId
|
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: {},
|
calendarsById: {},
|
||||||
initialCalendarsLoaded: false,
|
initialCalendarsLoaded: false,
|
||||||
editCalendarModal: undefined,
|
editCalendarModal: undefined,
|
||||||
|
widgetView: 'dayGridMonth',
|
||||||
|
widgetDate: 'now',
|
||||||
|
widgetEventDetailsOpen: false,
|
||||||
|
widgetEventDetails: {},
|
||||||
|
widgetRef: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
@ -83,6 +88,30 @@ const mutations = {
|
||||||
state.trashBin = trashBin
|
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 }) {
|
addScheduleInbox(state, { scheduleInbox }) {
|
||||||
state.scheduleInbox = scheduleInbox
|
state.scheduleInbox = scheduleInbox
|
||||||
},
|
},
|
||||||
|
@ -444,6 +473,22 @@ const getters = {
|
||||||
.sort((a, b) => a.order - b.order)
|
.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) {
|
hasTrashBin(state) {
|
||||||
return state.trashBin !== undefined && state.trashBin.retentionDuration !== 0
|
return state.trashBin !== undefined && state.trashBin.retentionDuration !== 0
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,8 +21,20 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NcContent app-name="calendar" :class="classNames">
|
<div v-if="isWidget" class="calendar-Widget">
|
||||||
<AppNavigation v-if="!isEmbedded && !showEmptyCalendarScreen">
|
<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 -->
|
<!-- Date Picker, View Buttons, Today Button -->
|
||||||
<AppNavigationHeader :is-public="!isAuthenticatedUser" />
|
<AppNavigationHeader :is-public="!isAuthenticatedUser" />
|
||||||
<template #list>
|
<template #list>
|
||||||
|
@ -77,6 +89,7 @@ import EmbedTopNavigation from '../components/AppNavigation/EmbedTopNavigation.v
|
||||||
import EmptyCalendar from '../components/EmptyCalendar.vue'
|
import EmptyCalendar from '../components/EmptyCalendar.vue'
|
||||||
import CalendarGrid from '../components/CalendarGrid.vue'
|
import CalendarGrid from '../components/CalendarGrid.vue'
|
||||||
import EditCalendarModal from '../components/AppNavigation/EditCalendarModal.vue'
|
import EditCalendarModal from '../components/AppNavigation/EditCalendarModal.vue'
|
||||||
|
import EditSimple from './EditSimple.vue'
|
||||||
|
|
||||||
// Import CalDAV related methods
|
// Import CalDAV related methods
|
||||||
import {
|
import {
|
||||||
|
@ -123,6 +136,17 @@ export default {
|
||||||
CalendarListNew,
|
CalendarListNew,
|
||||||
Trashbin,
|
Trashbin,
|
||||||
EditCalendarModal,
|
EditCalendarModal,
|
||||||
|
EditSimple,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
isWidget: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
referenceToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -152,29 +176,39 @@ export default {
|
||||||
attachmentsFolder: state => state.settings.attachmentsFolder,
|
attachmentsFolder: state => state.settings.attachmentsFolder,
|
||||||
}),
|
}),
|
||||||
defaultDate() {
|
defaultDate() {
|
||||||
return getYYYYMMDDFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
|
return getYYYYMMDDFromFirstdayParam(this.$route?.params?.firstDay ?? 'now')
|
||||||
},
|
},
|
||||||
isEditable() {
|
isEditable() {
|
||||||
// We do not allow drag and drop when the editor is open.
|
// We do not allow drag and drop when the editor is open.
|
||||||
return !this.isPublicShare
|
return !this.isPublicShare
|
||||||
&& !this.isEmbedded
|
&& !this.isEmbedded
|
||||||
&& this.$route.name !== 'EditPopoverView'
|
&& !this.isWidget
|
||||||
&& this.$route.name !== 'EditSidebarView'
|
&& this.$route?.name !== 'EditPopoverView'
|
||||||
|
&& this.$route?.name !== 'EditSidebarView'
|
||||||
},
|
},
|
||||||
isSelectable() {
|
isSelectable() {
|
||||||
return !this.isPublicShare && !this.isEmbedded
|
return !this.isPublicShare && !this.isEmbedded && !this.isWidget
|
||||||
},
|
},
|
||||||
isAuthenticatedUser() {
|
isAuthenticatedUser() {
|
||||||
return !this.isPublicShare && !this.isEmbedded
|
return !this.isPublicShare && !this.isEmbedded && !this.isWidget
|
||||||
},
|
},
|
||||||
isPublicShare() {
|
isPublicShare() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return this.$route.name.startsWith('Public')
|
return this.$route.name.startsWith('Public')
|
||||||
},
|
},
|
||||||
isEmbedded() {
|
isEmbedded() {
|
||||||
|
if (this.isWidget) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return this.$route.name.startsWith('Embed')
|
return this.$route.name.startsWith('Embed')
|
||||||
},
|
},
|
||||||
|
showWidgetEventDetails() {
|
||||||
|
return this.$store.getters.widgetEventDetailsOpen && this.$refs.calendarGridWidget.$el === this.$store.getters.widgetRef
|
||||||
|
},
|
||||||
showHeader() {
|
showHeader() {
|
||||||
return this.isPublicShare && this.isEmbedded
|
return this.isPublicShare && this.isEmbedded && this.isWidget
|
||||||
},
|
},
|
||||||
classNames() {
|
classNames() {
|
||||||
if (this.isEmbedded) {
|
if (this.isEmbedded) {
|
||||||
|
@ -229,9 +263,9 @@ export default {
|
||||||
})
|
})
|
||||||
this.$store.dispatch('initializeCalendarJsConfig')
|
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()
|
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 })
|
const calendars = await this.$store.dispatch('getPublicCalendars', { tokens })
|
||||||
this.loadingCalendars = false
|
this.loadingCalendars = false
|
||||||
|
|
||||||
|
@ -302,3 +336,9 @@ export default {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.calendar-Widget {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Popover ref="popover"
|
<Popover ref="popover"
|
||||||
:shown="isVisible"
|
:shown="showPopover"
|
||||||
:auto-hide="false"
|
:auto-hide="false"
|
||||||
:placement="placement"
|
:placement="placement"
|
||||||
:boundary="boundaryElement"
|
:boundary="boundaryElement"
|
||||||
|
@ -148,7 +148,8 @@
|
||||||
:calendar-id="calendarId"
|
:calendar-id="calendarId"
|
||||||
@close="closeEditorAndSkipAction" />
|
@close="closeEditorAndSkipAction" />
|
||||||
|
|
||||||
<SaveButtons class="event-popover__buttons"
|
<SaveButtons v-if="!isWidget"
|
||||||
|
class="event-popover__buttons"
|
||||||
:can-create-recurrence-exception="canCreateRecurrenceException"
|
:can-create-recurrence-exception="canCreateRecurrenceException"
|
||||||
:is-new="isNew"
|
:is-new="isNew"
|
||||||
:is-read-only="isReadOnlyOrViewing"
|
:is-read-only="isReadOnlyOrViewing"
|
||||||
|
@ -236,7 +237,7 @@ export default {
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
hasLocation: false,
|
hasLocation: false,
|
||||||
hasDescription: false,
|
hasDescription: false,
|
||||||
boundaryElement: document.querySelector('#app-content-vue > .fc'),
|
boundaryElement: null,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isViewing: true,
|
isViewing: true,
|
||||||
}
|
}
|
||||||
|
@ -244,15 +245,22 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
hideEventExport: (state) => state.settings.hideEventExport,
|
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
|
* Returns true if the current event is read only or the user is viewing the event
|
||||||
*
|
*
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
isReadOnlyOrViewing() {
|
isReadOnlyOrViewing() {
|
||||||
return this.isReadOnly || this.isViewing
|
return this.isReadOnly || this.isViewing || this.isWidget
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -260,7 +268,7 @@ export default {
|
||||||
this.repositionPopover()
|
this.repositionPopover()
|
||||||
|
|
||||||
// Hide popover when changing the view until the user selects a slot again
|
// 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() {
|
calendarObjectInstance() {
|
||||||
this.hasLocation = false
|
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.keyboardCloseEditor)
|
||||||
window.addEventListener('keydown', this.keyboardSaveEvent)
|
window.addEventListener('keydown', this.keyboardSaveEvent)
|
||||||
window.addEventListener('keydown', this.keyboardDeleteEvent)
|
window.addEventListener('keydown', this.keyboardDeleteEvent)
|
||||||
|
@ -314,8 +330,13 @@ export default {
|
||||||
},
|
},
|
||||||
getDomElementForPopover(isNew, route) {
|
getDomElementForPopover(isNew, route) {
|
||||||
let matchingDomObject
|
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')
|
matchingDomObject = document.querySelector('.fc-highlight')
|
||||||
this.placement = 'auto'
|
this.placement = 'auto'
|
||||||
|
|
||||||
|
@ -344,7 +365,7 @@ export default {
|
||||||
return matchingDomObject
|
return matchingDomObject
|
||||||
},
|
},
|
||||||
repositionPopover() {
|
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.reference = this.getDomElementForPopover(isNew, this.$route)
|
||||||
this.$refs.popover.$children[0].$refs.popper.dispose()
|
this.$refs.popover.$children[0].$refs.popper.dispose()
|
||||||
this.$refs.popover.$children[0].$refs.popper.init()
|
this.$refs.popover.$children[0].$refs.popper.init()
|
||||||
|
|
|
@ -8,6 +8,9 @@ const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-m
|
||||||
// Add dashboard entry
|
// Add dashboard entry
|
||||||
webpackConfig.entry.dashboard = path.join(__dirname, 'src', 'dashboard.js')
|
webpackConfig.entry.dashboard = path.join(__dirname, 'src', 'dashboard.js')
|
||||||
|
|
||||||
|
//Add reference entry
|
||||||
|
webpackConfig.entry['reference'] = path.join(__dirname, 'src', 'reference.js')
|
||||||
|
|
||||||
// Add appointments entries
|
// Add appointments entries
|
||||||
webpackConfig.entry['appointments-booking'] = path.join(__dirname, 'src', 'appointments/main-booking.js')
|
webpackConfig.entry['appointments-booking'] = path.join(__dirname, 'src', 'appointments/main-booking.js')
|
||||||
webpackConfig.entry['appointments-confirmation'] = path.join(__dirname, 'src', 'appointments/main-confirmation.js')
|
webpackConfig.entry['appointments-confirmation'] = path.join(__dirname, 'src', 'appointments/main-confirmation.js')
|
||||||
|
|
Loading…
Reference in New Issue