Skip to content

Commit

Permalink
Merge pull request #3118 from nextcloud/feature/trashbin
Browse files Browse the repository at this point in the history
Add a UI for the Nextcloud CalDAV trashbin
  • Loading branch information
ChristophWurst authored May 31, 2021
2 parents d358987 + ba0d245 commit 0d6baaf
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 23 deletions.
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default {
locale: (state) => state.settings.momentLocale,
}),
selectedDate() {
return getDateFromFirstdayParam(this.$route.params.firstDay)
return getDateFromFirstdayParam(this.$route.params?.firstDay ?? 'now')
},
previousShortKeyConf() {
return {
Expand Down
29 changes: 29 additions & 0 deletions src/components/AppNavigation/CalendarList/Moment.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<span class="live-relative-timestamp" :data-timestamp="timestamp * 1000" :title="title">{{ formatted }}</span>
</template>

<script>
import moment from '@nextcloud/moment'
export default {
name: 'Moment',
props: {
timestamp: {
type: Number,
required: true,
},
format: {
type: String,
default: 'LLL',
},
},
computed: {
title() {
return moment.unix(this.timestamp).format(this.format)
},
formatted() {
return moment.unix(this.timestamp).fromNow()
},
},
}
</script>
180 changes: 180 additions & 0 deletions src/components/AppNavigation/CalendarList/Trashbin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<!--
- @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
-
- @author 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
-
- @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/>.
-->

<template>
<AppNavigationItem :title="t('calendar', 'Trashbin')"
:pinned="true"
icon="icon-delete"
@click.prevent="onShow">
<template #extra>
<Modal v-if="showModal"
@close="showModal = false">
<div class="modal__content">
<h2>{{ t('calendar', 'Trashbin') }}</h2>
<span v-if="!items.length">{{ t('calendar', 'You do not have any deleted calendars or events') }}</span>
<table v-else>
<tr>
<th>{{ t('calendar', 'Name') }}</th>
<th class="deletedAt">
{{ t('calendar', 'Deleted at') }}
</th>
<th>&nbsp;</th>
</tr>
<tr v-for="item in items" :key="item.url">
<td>{{ item.name }}</td>
<td class="deletedAt">
<Moment class="timestamp" :timestamp="item.deletedAt" />
</td>
<td>
<button @click="restore(item)">
restore
</button>
</td>
</tr>
</table>
</div>
</Modal>
</template>
</AppNavigationItem>
</template>

<script>
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import Modal from '@nextcloud/vue/dist/Components/Modal'
import logger from '../../../utils/logger'
import { showError } from '@nextcloud/dialogs'
import { mapGetters } from 'vuex'
import Moment from './Moment'

export default {
name: 'Trashbin',
components: {
AppNavigationItem,
Modal,
Moment,
},
data() {
return {
showModal: false,
}
},
computed: {
...mapGetters([
'trashBin',
]),
calendars() {
return this.$store.getters.sortedDeletedCalendars
},
objects() {
return this.$store.getters.deletedCalendarObjects
},
items() {
const formattedCalendars = this.calendars.map(calendar => ({
calendar,
type: 'calendar',
key: calendar.url,
name: calendar.displayname,
url: calendar._url,
deletedAt: calendar._props['{http://nextcloud.com/ns}deleted-at'],
}))
const formattedCalendarObjects = this.objects.map(vobject => {
let eventSummary = t('calendar', 'Untitled event')
try {
// TODO: there _has to be_ a less error prone way …
eventSummary = vobject.calendarComponent?._components?.get('VEVENT')[0]?._properties?.get('SUMMARY')[0]?.value
} catch (e) {
// ignore
}
return {
vobject,
type: 'object',
key: vobject.id,
name: `${eventSummary} (${vobject.calendar.displayName})`,
url: vobject.uri,
deletedAt: vobject.dav._props['{http://nextcloud.com/ns}deleted-at'],
}
})

return formattedCalendars.concat(formattedCalendarObjects)
},
},
methods: {
async onShow() {
this.showModal = true

try {
await Promise.all([
this.$store.dispatch('loadDeletedCalendars'),
this.$store.dispatch('loadDeletedCalendarObjects'),
])

logger.debug('deleted calendars loaded', {
calendars: this.calendars,
objects: this.objects,
})
} catch (error) {
logger.error('could not load deleted calendars and objects', {
error,
})

showError(t('calendar', 'Could not load deleted calendars and objects'))
}
},
async restore(item) {
logger.debug('restoring ' + item.url, item)
try {
switch (item.type) {
case 'calendar':
await this.$store.dispatch('restoreCalendar', { calendar: item.calendar })
this.$store.dispatch('loadCollections')
break
case 'object':
await this.$store.dispatch('restoreCalendarObject', { vobject: item.vobject })
break
}
} catch (error) {
logger.error('could not restore ' + item.url, { error })

showError(t('calendar', 'Could not restore calendar or event'))
}
},
},
}
</script>

<style lang="scss" scoped>
.modal__content {
width: 40vw;
margin: 2vw;
}
table {
width: 100%;
}
th, td {
padding: 4px;
}
th {
font-weight: bold;
}
.deletedAt {
text-align: right;
}
</style>
39 changes: 30 additions & 9 deletions src/services/caldavService.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,30 @@ const initializeClientForPublicView = async() => {
/**
* Fetch all calendars from the server
*
* @returns {Promise<CalendarHome>}
*/
const getCalendarHome = () => getClient().calendarHomes[0]

/**
* Fetch all collections in the calendar home from the server
*
* @returns {Promise<Collection[]>}
*/
const findAll = () => {
return getCalendarHome().findAllCalDAVCollectionsGrouped()
}

/**
* Fetch all deleted calendars from the server
*
* @returns {Promise<Calendar[]>}
*/
const findAllCalendars = () => {
return getClient().calendarHomes[0].findAllCalendars()
const findAllDeletedCalendars = async() => {
const collections = await getClient()
.calendarHomes[0]
.findAll()
return collections
.filter(coll => coll._props['{DAV:}resourcetype'].includes('{http://nextcloud.com/ns}deleted-calendar'))
}

/**
Expand Down Expand Up @@ -116,7 +136,7 @@ const findPublicCalendarsByTokens = async(tokens) => {
* @returns {Promise<ScheduleInbox[]>}
*/
const findSchedulingInbox = async() => {
const inboxes = await getClient().calendarHomes[0].findAllScheduleInboxes()
const inboxes = await getCalendarHome().findAllScheduleInboxes()
return inboxes[0]
}

Expand All @@ -134,7 +154,7 @@ const findSchedulingInbox = async() => {
* @returns {Promise<ScheduleOutbox>}
*/
const findSchedulingOutbox = async() => {
const outboxes = await getClient().calendarHomes[0].findAllScheduleOutboxes()
const outboxes = await getCalendarHome().findAllScheduleOutboxes()
return outboxes[0]
}

Expand All @@ -149,7 +169,7 @@ const findSchedulingOutbox = async() => {
* @returns {Promise<Calendar>}
*/
const createCalendar = async(displayName, color, components, order, timezoneIcs) => {
return getClient().calendarHomes[0].createCalendarCollection(displayName, color, components, order, timezoneIcs)
return getCalendarHome().createCalendarCollection(displayName, color, components, order, timezoneIcs)
}

/**
Expand All @@ -164,7 +184,7 @@ const createCalendar = async(displayName, color, components, order, timezoneIcs)
* @returns {Promise<Calendar>}
*/
const createSubscription = async(displayName, color, source, order) => {
return getClient().calendarHomes[0].createSubscribedCollection(displayName, color, source, order)
return getCalendarHome().createSubscribedCollection(displayName, color, source, order)
}

/**
Expand All @@ -173,7 +193,7 @@ const createSubscription = async(displayName, color, source, order) => {
* @returns {Promise<Calendar>}
*/
const enableBirthdayCalendar = async() => {
await getClient().calendarHomes[0].enableBirthdayCalendar()
await getCalendarHome().enableBirthdayCalendar()
return getBirthdayCalendar()
}

Expand All @@ -183,7 +203,7 @@ const enableBirthdayCalendar = async() => {
* @returns {Promise<Calendar>}
*/
const getBirthdayCalendar = async() => {
return getClient().calendarHomes[0].find(CALDAV_BIRTHDAY_CALENDAR)
return getCalendarHome().find(CALDAV_BIRTHDAY_CALENDAR)
}

/**
Expand Down Expand Up @@ -218,7 +238,8 @@ const findPrincipalByUrl = async(url) => {
export {
initializeClientForUserView,
initializeClientForPublicView,
findAllCalendars,
findAll,
findAllDeletedCalendars,
findPublicCalendarsByTokens,
findSchedulingInbox,
findSchedulingOutbox,
Expand Down
3 changes: 3 additions & 0 deletions src/services/windowTitleService.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export default function(router, store) {
if (mutation.type !== 'setMomentLocale') {
return
}
if (!router.currentRoute.params?.firstDay) {
return
}

const date = getDateFromFirstdayParam(router.currentRoute.params.firstDay)
const view = router.currentRoute.params.view
Expand Down
Loading

0 comments on commit 0d6baaf

Please sign in to comment.