614 lines
18 KiB
Vue
614 lines
18 KiB
Vue
<!--
|
|
- @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
|
|
-
|
|
- @author Georg Ehrke <oc.list@georgehrke.com>
|
|
- @author Richard Steinmetz <richard@steinmetz.cloud>
|
|
-
|
|
- @license AGPL-3.0-or-later
|
|
-
|
|
- 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/>.
|
|
-
|
|
-->
|
|
|
|
<template>
|
|
<Popover
|
|
ref="popover"
|
|
:shown="showPopover"
|
|
:auto-hide="false"
|
|
:placement="placement"
|
|
:boundary="boundaryElement"
|
|
popover-base-class="event-popover"
|
|
:triggers="[]"
|
|
>
|
|
<div class="event-popover__inner">
|
|
<template v-if="isLoading && !isSaving">
|
|
<PopoverLoadingIndicator />
|
|
</template>
|
|
|
|
<template v-else-if="isError">
|
|
<div class="event-popover__top-right-actions">
|
|
<Actions>
|
|
<ActionButton @click="cancel">
|
|
<template #icon>
|
|
<Close :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Close") }}
|
|
</ActionButton>
|
|
</Actions>
|
|
</div>
|
|
|
|
<EmptyContent
|
|
:name="$t('calendar', 'Event does not exist')"
|
|
:description="error"
|
|
>
|
|
<template #icon>
|
|
<CalendarBlank :size="20" decorative />
|
|
</template>
|
|
</EmptyContent>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="event-popover__top-right-actions">
|
|
<Actions v-if="!isLoading && !isError && !isNew" :force-menu="true">
|
|
<ActionLink
|
|
v-if="!hideEventExport && hasDownloadURL"
|
|
:href="downloadURL"
|
|
>
|
|
<template #icon>
|
|
<Download :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Export") }}
|
|
</ActionLink>
|
|
<ActionButton
|
|
v-if="!canCreateRecurrenceException && !isReadOnly"
|
|
@click="duplicateEvent()"
|
|
>
|
|
<template #icon>
|
|
<ContentDuplicate :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Duplicate") }}
|
|
</ActionButton>
|
|
<ActionButton
|
|
v-if="canDelete && !canCreateRecurrenceException"
|
|
@click="deleteAndLeave(false)"
|
|
>
|
|
<template #icon>
|
|
<Delete :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Delete") }}
|
|
</ActionButton>
|
|
<ActionButton
|
|
v-if="canDelete && canCreateRecurrenceException"
|
|
@click="deleteAndLeave(false)"
|
|
>
|
|
<template #icon>
|
|
<Delete :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Delete this occurrence") }}
|
|
</ActionButton>
|
|
<ActionButton
|
|
v-if="canDelete && canCreateRecurrenceException"
|
|
@click="deleteAndLeave(true)"
|
|
>
|
|
<template #icon>
|
|
<Delete :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Delete this and all future") }}
|
|
</ActionButton>
|
|
</Actions>
|
|
<Actions>
|
|
<ActionButton @click="cancel">
|
|
<template #icon>
|
|
<Close :size="20" decorative />
|
|
</template>
|
|
{{ $t("calendar", "Close") }}
|
|
</ActionButton>
|
|
</Actions>
|
|
</div>
|
|
|
|
<CalendarPickerHeader
|
|
:value="selectedCalendar"
|
|
:calendars="calendars"
|
|
:is-read-only="isReadOnlyOrViewing || !canModifyCalendar"
|
|
@update:value="changeCalendar"
|
|
/>
|
|
|
|
<PropertyTitle
|
|
:value="titleOrPlaceholder"
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
@update:value="updateTitle"
|
|
/>
|
|
|
|
<PropertyTitleTimePicker
|
|
:start-date="startDate"
|
|
:start-timezone="startTimezone"
|
|
:end-date="endDate"
|
|
:end-timezone="endTimezone"
|
|
:is-all-day="isAllDay"
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
:can-modify-all-day="canModifyAllDay"
|
|
:user-timezone="currentUserTimezone"
|
|
@update-start-date="updateStartDate"
|
|
@update-start-timezone="updateStartTimezone"
|
|
@update-end-date="updateEndDate"
|
|
@update-end-timezone="updateEndTimezone"
|
|
@toggle-all-day="toggleAllDay"
|
|
/>
|
|
|
|
<div style='display:flex ;margin-left: 7%;'>
|
|
<div style='width:70%'>
|
|
<PropertySelectAbsenceType
|
|
:value="absenceType"
|
|
:is-read-only="isReadOnly"
|
|
:prop-model="rfcProps.absenceType"
|
|
:noWrap='true'
|
|
sle
|
|
@update:value="updateAbsenceType" />
|
|
</div>
|
|
<!-- <div style='width:30% ;margin-top: -11px;'>
|
|
<PropertyIsPrivate
|
|
:is-read-only="isReadOnly"
|
|
:is-private="isPrivate"
|
|
@toggle-is-private="toggleIsPrivate"/>
|
|
</div> -->
|
|
|
|
</div>
|
|
|
|
<!-- <PropertyIsLeave
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
:is-leave="isLeave"
|
|
@toggle-is-leave="toggleIsLeave"
|
|
/> -->
|
|
|
|
<!-- <PropertyIsCalendarPending
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
:is-calendar-pending="isCalendarPending"
|
|
@toggle-is-calendar-pending="toggleIsCalendarPending" /> -->
|
|
|
|
<PropertySelectClient
|
|
class="property-location"
|
|
url="/apps/gestion/ajaxGetClientsName"
|
|
:is-read-only="isReadOnly"
|
|
:prop-model="rfcProps.clients"
|
|
:value="client"
|
|
:linkify-links="true"
|
|
@update:value="updateClient"
|
|
/>
|
|
|
|
<PropertySelectLieu
|
|
class="property-location"
|
|
url="/apps/gestion/ajaxGetLieux"
|
|
:is-read-only="isReadOnly"
|
|
:prop-model="rfcProps.locations"
|
|
:value="location"
|
|
:linkify-links="true"
|
|
@update:value="updateLocation"
|
|
/>
|
|
|
|
<PropertySelectArticle
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
url="/apps/gestion/ajaxGetProduits?orderDirection=ASC"
|
|
:prop-model="rfcProps.articles"
|
|
:value="description"
|
|
:linkify-links="true"
|
|
@add-single-value="addArticle"
|
|
@remove-single-value="removeArticle"
|
|
/>
|
|
|
|
<PropertyText
|
|
:is-read-only="isReadOnly"
|
|
:prop-model="rfcProps.comment"
|
|
:value="comment"
|
|
:linkify-links="false"
|
|
@update:value="updateComment"
|
|
/>
|
|
|
|
<InviteesList
|
|
class="event-popover__invitees"
|
|
:hide-if-empty="true"
|
|
:hide-buttons="true"
|
|
:hide-errors="true"
|
|
:show-header="true"
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
:is-shared-with-me="isSharedWithMe"
|
|
:calendar-object-instance="calendarObjectInstance"
|
|
:limit="3"
|
|
/>
|
|
|
|
<InvitationResponseButtons
|
|
v-if="isViewedByAttendee && isViewing"
|
|
class="event-popover__response-buttons"
|
|
:attendee="userAsAttendee"
|
|
:calendar-id="calendarId"
|
|
@close="closeEditorAndSkipAction"
|
|
/>
|
|
|
|
|
|
<SaveButtons
|
|
v-if="!isWidget"
|
|
class="event-popover__buttons"
|
|
:can-create-recurrence-exception="canCreateRecurrenceException"
|
|
:is-new="isNew"
|
|
:is-read-only="isReadOnlyOrViewing"
|
|
:force-this-and-all-future="forceThisAndAllFuture"
|
|
:show-more-button="true"
|
|
:more-button-type="isViewing ? 'tertiary' : undefined"
|
|
:grow-horizontally="!isViewing && canCreateRecurrenceException"
|
|
:disabled="isSaving"
|
|
:is-calendar-pending="isCalendarPending"
|
|
@save-this-only="saveAndView(false)"
|
|
@save-this-and-all-future="saveAndView(true)"
|
|
@show-more="showMore"
|
|
@save-pending-calendar-event="saveAsPendingCalendarEvent"
|
|
>
|
|
<NcButton
|
|
@click="viewDefunt"
|
|
v-if="defuntUrl"
|
|
:type="undefined"
|
|
:class="'d-flex w-max-content'"
|
|
>
|
|
Voir le defunt
|
|
</NcButton>
|
|
<NcButton
|
|
v-if="!isReadOnly && isViewing"
|
|
:type="isViewedByAttendee ? 'tertiary' : undefined"
|
|
@click="isViewing = false"
|
|
>
|
|
<template #icon>
|
|
<EditIcon :size="20" />
|
|
</template>
|
|
{{ $t("calendar", "Edit") }}
|
|
</NcButton>
|
|
</SaveButtons>
|
|
</template>
|
|
</div>
|
|
</Popover>
|
|
</template>
|
|
<script>
|
|
import {
|
|
NcActions as Actions,
|
|
NcActionButton as ActionButton,
|
|
NcActionLink as ActionLink,
|
|
NcEmptyContent as EmptyContent,
|
|
NcPopover as Popover,
|
|
NcButton,
|
|
} from "@nextcloud/vue";
|
|
import axios from "axios";
|
|
import { generateUrl } from "@nextcloud/router";
|
|
import EditorMixin from "../mixins/EditorMixin.js";
|
|
import PropertyTitle from "../components/Editor/Properties/PropertyTitle.vue";
|
|
import PropertyTitleTimePicker from "../components/Editor/Properties/PropertyTitleTimePicker.vue";
|
|
import PropertyText from "../components/Editor/Properties/PropertyText.vue";
|
|
import SaveButtons from "../components/Editor/SaveButtons.vue";
|
|
import PopoverLoadingIndicator from "../components/Popover/PopoverLoadingIndicator.vue";
|
|
import { getPrefixedRoute } from "../utils/router.js";
|
|
import InvitationResponseButtons from "../components/Editor/InvitationResponseButtons.vue";
|
|
import CalendarPickerHeader from "../components/Editor/CalendarPickerHeader.vue";
|
|
import InviteesList from "../components/Editor/Invitees/InviteesList.vue";
|
|
|
|
import CalendarBlank from "vue-material-design-icons/CalendarBlank.vue";
|
|
import Close from "vue-material-design-icons/Close.vue";
|
|
import Delete from "vue-material-design-icons/Delete.vue";
|
|
import Download from "vue-material-design-icons/Download.vue";
|
|
import ContentDuplicate from "vue-material-design-icons/ContentDuplicate.vue";
|
|
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
|
import { mapState } from "vuex";
|
|
import PropertySelect from "../components/Editor/Properties/PropertySelect.vue";
|
|
import PropertySelectAjax from "../components/Editor/Properties/PropertySelectAjax.vue";
|
|
import PropertySelectAjaxMultiple from "../components/Editor/Properties/PropertySelectAjaxMultiple.vue";
|
|
import PropertySelectLieu from "../components/Editor/Properties/PropertySelectLieu.vue";
|
|
import PropertySelectClient from "../components/Editor/Properties/PropertySelectClient.vue";
|
|
import PropertySelectArticle from "../components/Editor/Properties/PropertySelectArticle.vue";
|
|
import PropertyIsLeave from "../components/Editor/Properties/PropertyIsLeave.vue";
|
|
import PropertyIsCalendarPending from "../components/Editor/Properties/PropertyIsCalendarPending";
|
|
import PropertySelectAbsenceType from "../components/Editor/Properties/PropertySelectAbsenceType.vue";
|
|
import PropertyIsPrivate from "../components/Editor/Properties/PropertyIsPrivate.vue";
|
|
|
|
|
|
export default {
|
|
name: "EditSimple",
|
|
components: {
|
|
PropertyIsLeave,
|
|
PropertyIsCalendarPending,
|
|
PropertySelectAjaxMultiple,
|
|
PropertySelectAjax,
|
|
PropertySelectLieu,
|
|
PropertySelectClient,
|
|
PropertySelectArticle,
|
|
PropertySelect,
|
|
PopoverLoadingIndicator,
|
|
SaveButtons,
|
|
PropertyText,
|
|
PropertyTitleTimePicker,
|
|
PropertyTitle,
|
|
PropertySelectAbsenceType,
|
|
Popover,
|
|
Actions,
|
|
ActionButton,
|
|
ActionLink,
|
|
EmptyContent,
|
|
CalendarBlank,
|
|
Close,
|
|
Download,
|
|
ContentDuplicate,
|
|
Delete,
|
|
InvitationResponseButtons,
|
|
CalendarPickerHeader,
|
|
InviteesList,
|
|
NcButton,
|
|
EditIcon,
|
|
PropertyIsPrivate,
|
|
|
|
},
|
|
mixins: [EditorMixin],
|
|
data() {
|
|
return {
|
|
placement: "auto",
|
|
hasLocation: false,
|
|
hasDescription: false,
|
|
isCalendarPending: false,
|
|
boundaryElement: null,
|
|
isVisible: true,
|
|
isViewing: true,
|
|
defuntUrl: undefined,
|
|
};
|
|
},
|
|
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 || this.isWidget;
|
|
},
|
|
|
|
/**
|
|
* Return the event's title or a placeholder if it is empty
|
|
*
|
|
* @return {string}
|
|
*/
|
|
titleOrPlaceholder() {
|
|
if (this.title === "" && this.isReadOnlyOrViewing && !this.isLoading) {
|
|
return t("calendar", "Untitled event");
|
|
}
|
|
|
|
return this.title;
|
|
},
|
|
},
|
|
watch: {
|
|
$route(to, from) {
|
|
this.repositionPopover();
|
|
|
|
// Hide popover when changing the view until the user selects a slot again
|
|
this.isVisible = to?.params.view === from?.params.view;
|
|
},
|
|
calendarObjectInstance() {
|
|
this.hasLocation = false;
|
|
this.hasDescription = false;
|
|
|
|
if (
|
|
typeof this.calendarObjectInstance.location === "string" &&
|
|
this.calendarObjectInstance.location.trim() !== ""
|
|
) {
|
|
this.hasLocation = true;
|
|
}
|
|
if (
|
|
typeof this.calendarObjectInstance.description === "string" &&
|
|
this.calendarObjectInstance.description.trim() !== ""
|
|
) {
|
|
this.hasDescription = true;
|
|
}
|
|
if (this.calendarObjectInstance) {
|
|
this.getDefuntUrl();
|
|
this.isCalendarPending = this.calendarObjectInstance.isCalendarPending ?? false;
|
|
}
|
|
|
|
},
|
|
isNew: {
|
|
immediate: true,
|
|
handler(isNew) {
|
|
// New events should be editable from the start
|
|
this.isViewing = !isNew;
|
|
},
|
|
},
|
|
},
|
|
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);
|
|
window.addEventListener("keydown", this.keyboardDuplicateEvent);
|
|
|
|
this.$nextTick(() => {
|
|
this.repositionPopover();
|
|
});
|
|
},
|
|
beforeDestroy() {
|
|
window.removeEventListener("keydown", this.keyboardCloseEditor);
|
|
window.removeEventListener("keydown", this.keyboardSaveEvent);
|
|
window.removeEventListener("keydown", this.keyboardDeleteEvent);
|
|
window.removeEventListener("keydown", this.keyboardDuplicateEvent);
|
|
},
|
|
methods: {
|
|
showMore() {
|
|
// Do not save yet
|
|
this.requiresActionOnRouteLeave = false;
|
|
|
|
const params = Object.assign({}, this.$route.params);
|
|
if (this.isNew) {
|
|
this.$router.push({ name: "NewSidebarView", params });
|
|
} else {
|
|
this.$router.push({
|
|
name: getPrefixedRoute(this.$route.name, "EditSidebarView"),
|
|
params,
|
|
});
|
|
}
|
|
},
|
|
getDomElementForPopover(isNew, route) {
|
|
let matchingDomObject;
|
|
if (this.isWidget) {
|
|
const objectId = this.widgetEventDetails.object;
|
|
const recurrenceId = this.widgetEventDetails.recurrenceId;
|
|
|
|
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";
|
|
|
|
if (!matchingDomObject) {
|
|
matchingDomObject = document.querySelector(
|
|
'.fc-event[data-is-new="yes"]'
|
|
);
|
|
}
|
|
} else {
|
|
const objectId = route.params.object;
|
|
const recurrenceId = route.params.recurrenceId;
|
|
|
|
matchingDomObject = document.querySelector(
|
|
`.fc-event[data-object-id="${objectId}"][data-recurrence-id="${recurrenceId}"]`
|
|
);
|
|
this.placement = "auto";
|
|
}
|
|
|
|
if (!matchingDomObject) {
|
|
matchingDomObject = document.querySelector("#app-navigation-vue");
|
|
this.placement = "right";
|
|
}
|
|
|
|
if (!matchingDomObject) {
|
|
matchingDomObject = document.querySelector("body");
|
|
this.placement = "auto";
|
|
}
|
|
|
|
console.info(
|
|
"getDomElementForPopover",
|
|
matchingDomObject,
|
|
this.placement
|
|
);
|
|
return matchingDomObject;
|
|
},
|
|
repositionPopover() {
|
|
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();
|
|
},
|
|
/**
|
|
* Save changes and leave when creating a new event or return to viewing mode when editing
|
|
* an existing event. Stay in editing mode if an error occurrs.
|
|
*
|
|
* @param {boolean} thisAndAllFuture Modify this and all future events
|
|
* @return {Promise<void>}
|
|
*/
|
|
async saveAndView(thisAndAllFuture) {
|
|
// Transitioning from new to edit routes is not implemented for now
|
|
if (this.isNew) {
|
|
await this.saveAndLeave(thisAndAllFuture);
|
|
return;
|
|
}
|
|
|
|
this.isViewing = true;
|
|
try {
|
|
await this.save(thisAndAllFuture);
|
|
this.requiresActionOnRouteLeave = false;
|
|
} catch (error) {
|
|
this.isViewing = false;
|
|
}
|
|
},
|
|
async saveAsPendingCalendarEvent() {
|
|
if (this.isNew) {
|
|
await this.savePendingCalendar(false);
|
|
return;
|
|
}
|
|
|
|
this.isViewing = true;
|
|
try {
|
|
await this.savePendingCalendar(false);
|
|
this.requiresActionOnRouteLeave = false;
|
|
} catch (error) {
|
|
this.isViewing = false;
|
|
}
|
|
},
|
|
|
|
addArticle(article) {
|
|
if (this.description && this.description !== "") {
|
|
this.updateDescription(this.description + ";" + article);
|
|
} else {
|
|
this.updateDescription(article);
|
|
}
|
|
},
|
|
removeArticle(article) {
|
|
if (this.description && this.description !== "") {
|
|
let values = [];
|
|
let items = this.description.split(";");
|
|
items.forEach((item) => {
|
|
if (item !== article) {
|
|
values.push(item);
|
|
}
|
|
});
|
|
this.updateDescription(values.join(";"));
|
|
} else {
|
|
this.updateDescription(null);
|
|
}
|
|
},
|
|
getDefuntUrl() {
|
|
const url = generateUrl(
|
|
`/apps/gestion/api/getDefundIdByCalendarUuid/${this.calendarObjectInstance.eventComponent.uid}`
|
|
);
|
|
axios.get(url).then((response) => {
|
|
console.log(this.calendarObjectInstance);
|
|
if (response.data.id) {
|
|
this.defuntUrl = generateUrl(
|
|
`/apps/gestion/defunt/${response.data.id}/show`
|
|
);
|
|
}
|
|
});
|
|
},
|
|
viewDefunt() {
|
|
window.open(this.defuntUrl, "_blank");
|
|
},
|
|
},
|
|
};
|
|
</script>
|