diff --git a/package-lock.json b/package-lock.json index d2e243efbc..0d24989e53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5930,11 +5930,18 @@ "dev": true }, "cdav-library": { - "version": "git+https://github.com/nextcloud/cdav-library.git#c4f7d8225c2c8cfa5a3d269b3d986698d257dd5d", + "version": "git+https://github.com/nextcloud/cdav-library.git#7fdc09ccc4917635c5189a878be9a899ee81f799", "from": "git+https://github.com/nextcloud/cdav-library.git", "requires": { - "core-js": "^3.12.1", + "core-js": "^3.13.0", "regenerator-runtime": "^0.13.7" + }, + "dependencies": { + "core-js": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.13.1.tgz", + "integrity": "sha512-JqveUc4igkqwStL2RTRn/EPFGBOfEZHxJl/8ej1mXJR75V3go2mFF4bmUYkEIT1rveHKnkUlcJX/c+f1TyIovQ==" + } } }, "chalk": { diff --git a/src/components/AppNavigation/AppNavigationHeader/AppNavigationHeaderDatePicker.vue b/src/components/AppNavigation/AppNavigationHeader/AppNavigationHeaderDatePicker.vue index eeaff526e6..44f0f02fb5 100644 --- a/src/components/AppNavigation/AppNavigationHeader/AppNavigationHeaderDatePicker.vue +++ b/src/components/AppNavigation/AppNavigationHeader/AppNavigationHeaderDatePicker.vue @@ -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 { diff --git a/src/components/AppNavigation/CalendarList/Moment.vue b/src/components/AppNavigation/CalendarList/Moment.vue new file mode 100644 index 0000000000..960edf8ebb --- /dev/null +++ b/src/components/AppNavigation/CalendarList/Moment.vue @@ -0,0 +1,29 @@ + + + diff --git a/src/components/AppNavigation/CalendarList/Trashbin.vue b/src/components/AppNavigation/CalendarList/Trashbin.vue new file mode 100644 index 0000000000..7e052867cd --- /dev/null +++ b/src/components/AppNavigation/CalendarList/Trashbin.vue @@ -0,0 +1,180 @@ + + + + + + + diff --git a/src/services/caldavService.js b/src/services/caldavService.js index df271ff5c1..f1f445a53c 100644 --- a/src/services/caldavService.js +++ b/src/services/caldavService.js @@ -75,10 +75,30 @@ const initializeClientForPublicView = async() => { /** * Fetch all calendars from the server * + * @returns {Promise} + */ +const getCalendarHome = () => getClient().calendarHomes[0] + +/** + * Fetch all collections in the calendar home from the server + * + * @returns {Promise} + */ +const findAll = () => { + return getCalendarHome().findAllCalDAVCollectionsGrouped() +} + +/** + * Fetch all deleted calendars from the server + * * @returns {Promise} */ -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')) } /** @@ -116,7 +136,7 @@ const findPublicCalendarsByTokens = async(tokens) => { * @returns {Promise} */ const findSchedulingInbox = async() => { - const inboxes = await getClient().calendarHomes[0].findAllScheduleInboxes() + const inboxes = await getCalendarHome().findAllScheduleInboxes() return inboxes[0] } @@ -134,7 +154,7 @@ const findSchedulingInbox = async() => { * @returns {Promise} */ const findSchedulingOutbox = async() => { - const outboxes = await getClient().calendarHomes[0].findAllScheduleOutboxes() + const outboxes = await getCalendarHome().findAllScheduleOutboxes() return outboxes[0] } @@ -149,7 +169,7 @@ const findSchedulingOutbox = async() => { * @returns {Promise} */ 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) } /** @@ -164,7 +184,7 @@ const createCalendar = async(displayName, color, components, order, timezoneIcs) * @returns {Promise} */ const createSubscription = async(displayName, color, source, order) => { - return getClient().calendarHomes[0].createSubscribedCollection(displayName, color, source, order) + return getCalendarHome().createSubscribedCollection(displayName, color, source, order) } /** @@ -173,7 +193,7 @@ const createSubscription = async(displayName, color, source, order) => { * @returns {Promise} */ const enableBirthdayCalendar = async() => { - await getClient().calendarHomes[0].enableBirthdayCalendar() + await getCalendarHome().enableBirthdayCalendar() return getBirthdayCalendar() } @@ -183,7 +203,7 @@ const enableBirthdayCalendar = async() => { * @returns {Promise} */ const getBirthdayCalendar = async() => { - return getClient().calendarHomes[0].find(CALDAV_BIRTHDAY_CALENDAR) + return getCalendarHome().find(CALDAV_BIRTHDAY_CALENDAR) } /** @@ -218,7 +238,8 @@ const findPrincipalByUrl = async(url) => { export { initializeClientForUserView, initializeClientForPublicView, - findAllCalendars, + findAll, + findAllDeletedCalendars, findPublicCalendarsByTokens, findSchedulingInbox, findSchedulingOutbox, diff --git a/src/services/windowTitleService.js b/src/services/windowTitleService.js index 435d598b79..8b61935573 100644 --- a/src/services/windowTitleService.js +++ b/src/services/windowTitleService.js @@ -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 diff --git a/src/store/calendars.js b/src/store/calendars.js index ebbf7bd405..cc5b449145 100644 --- a/src/store/calendars.js +++ b/src/store/calendars.js @@ -27,7 +27,8 @@ import Vue from 'vue' import { createCalendar, createSubscription, - findAllCalendars, + findAll, + findAllDeletedCalendars, findPublicCalendarsByTokens, } from '../services/caldavService.js' import { mapCDavObjectToCalendarObject } from '../models/calendarObject' @@ -47,6 +48,9 @@ import { const state = { calendars: [], + trashBin: undefined, + deletedCalendars: [], + deletedCalendarObjects: [], calendarsById: {}, initialCalendarsLoaded: false, } @@ -63,10 +67,68 @@ const mutations = { addCalendar(state, { calendar }) { const object = getDefaultCalendarObject(calendar) - state.calendars.push(object) + if (!state.calendars.some(existing => existing.id === object.id)) { + state.calendars.push(object) + } Vue.set(state.calendarsById, object.id, object) }, + addTrashBin(state, { trashBin }) { + state.trashBin = trashBin + }, + + /** + * Adds deleted calendar into state + * + * @param {Object} state the store data + * @param {Object} data destructuring object + * @param {Object} data.calendar calendar the calendar to add + */ + addDeletedCalendar(state, { calendar }) { + if (state.deletedCalendars.some(c => c.url === calendar.url)) { + // This calendar is already known + return + } + state.deletedCalendars.push(calendar) + }, + + /** + * Removes a deleted calendar + * + * @param {Object} state the store data + * @param {Object} data destructuring object + * @param {Object} data.calendar the deleted calendar to remove + */ + removeDeletedCalendar(state, { calendar }) { + state.deletedCalendars = state.deletedCalendars.filter(c => c !== calendar) + }, + + /** + * Removes a deleted calendar object + * + * @param {Object} state the store data + * @param {Object} data destructuring object + * @param {Object} data.vobject the deleted calendar object to remove + */ + removeDeletedCalendarObject(state, { vobject }) { + state.deletedCalendarObjects = state.deletedCalendarObjects.filter(vo => vo.id !== vobject.id) + }, + + /** + * Adds a deleted vobject into state + * + * @param {Object} state the store data + * @param {Object} data destructuring object + * @param {Object} data.vobject the calendar vobject to add + */ + addDeletedCalendarObject(state, { vobject }) { + if (state.deletedCalendarObjects.some(c => c.uri === vobject.uri)) { + // This vobject is already known + return + } + state.deletedCalendarObjects.push(vobject) + }, + /** * Deletes a calendar * @@ -338,6 +400,45 @@ const getters = { .sort((a, b) => a.order - b.order) }, + hasTrashBin(state) { + return state.trashBin !== undefined + }, + + trashBin(state) { + return state.trashBin + }, + + /** + * List of deleted sorted calendars + * + * @param {Object} state the store data + * @returns {Array} + */ + sortedDeletedCalendars(state) { + return state.deletedCalendars + .sort((a, b) => a.deletedAt - b.deletedAt) + }, + + /** + * List of deleted calendars objects + * + * @param {Object} state the store data + * @returns {Array} + */ + deletedCalendarObjects(state) { + const calendarUriMap = {} + state.calendars.forEach(calendar => { + const withoutTrail = calendar.url.replace(/\/$/, '') + const uri = withoutTrail.substr(withoutTrail.lastIndexOf('/') + 1) + calendarUriMap[uri] = calendar + }) + + return state.deletedCalendarObjects.map(obj => ({ + calendar: calendarUriMap[obj.dav._props['{http://nextcloud.com/ns}calendar-uri']], + ...obj, + })) + }, + /** * List of sorted subscriptions * @@ -432,19 +533,55 @@ const getters = { const actions = { /** - * Retrieve and commit calendars + * Retrieve and commit calendars and other collections * * @param {Object} context the store mutations - * @returns {Promise} the calendars + * @returns {Promise} the results */ - async getCalendars({ commit, state, getters }) { - const calendars = await findAllCalendars() + async loadCollections({ commit, state, getters }) { + const { calendars, trashBins } = await findAll() + console.info('calendar home scanned', calendars, trashBins) calendars.map((calendar) => mapDavCollectionToCalendar(calendar, getters.getCurrentUserPrincipal)).forEach(calendar => { commit('addCalendar', { calendar }) }) + if (trashBins.length) { + commit('addTrashBin', { trashBin: trashBins[0] }) + } commit('initialCalendarsLoaded') - return state.calendars + return { + calendars: state.calendars, + trashBin: state.trashBin, + } + }, + + /** + * Retrieve and commit deleted calendars + * + * @param {Object} context the store mutations + * @returns {Promise} the calendars + */ + async loadDeletedCalendars({ commit }) { + const calendars = await findAllDeletedCalendars() + + calendars.forEach(calendar => commit('addDeletedCalendar', { calendar })) + }, + + /** + * Retrieve and commit deleted calendar objects + */ + async loadDeletedCalendarObjects({ commit, state }) { + const vobjects = await state.trashBin.findDeletedObjects() + console.info('vobjects loaded', { vobjects }) + + vobjects.forEach(vobject => { + try { + const calendarObject = mapCDavObjectToCalendarObject(vobject, undefined) + commit('addDeletedCalendarObject', { vobject: calendarObject }) + } catch (error) { + console.error('could not convert calendar object', vobject, error) + } + }) }, /** @@ -529,6 +666,22 @@ const actions = { context.commit('deleteCalendar', { calendar }) }, + async restoreCalendar({ commit, state }, { calendar }) { + await state.trashBin.restore(calendar.url) + + commit('removeDeletedCalendar', { calendar }) + }, + + async restoreCalendarObject({ commit, state, dispatch }, { vobject }) { + await state.trashBin.restore(vobject.uri) + + // Clean up the data locally + commit('removeDeletedCalendarObject', { vobject }) + + // Make sure the affected calendar is refreshed + commit('incrementModificationCount') + }, + /** * Toggle whether a calendar is enabled * diff --git a/src/views/Calendar.vue b/src/views/Calendar.vue index 28ebdcd79e..af0a639578 100644 --- a/src/views/Calendar.vue +++ b/src/views/Calendar.vue @@ -33,6 +33,7 @@ +