From 8a5bb96edfe4e6abb0151a8563ddfba4690c7789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raimund=20Schl=C3=BC=C3=9Fler?= Date: Wed, 15 May 2019 22:34:14 +0200 Subject: [PATCH] Implement sharing menu in calendar list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raimund Schlüßler --- css/src/Calendars/Calendar.scss | 64 ++++++++++++ css/src/Calendars/CalendarSharee.scss | 85 ++++++++++++++++ css/src/Calendars/CalendarShares.scss | 62 ++++++++++++ css/src/style.scss | 7 +- css/tasks.scss | 4 + src/components/CalendarShare.vue | 140 ++++++++++++++++++++++++++ src/components/CalendarSharee.vue | 129 ++++++++++++++++++++++++ src/components/TheList.vue | 41 +++++++- src/store/calendars.js | 119 ++++++++++++++-------- 9 files changed, 608 insertions(+), 43 deletions(-) create mode 100644 css/src/Calendars/Calendar.scss create mode 100644 css/src/Calendars/CalendarSharee.scss create mode 100644 css/src/Calendars/CalendarShares.scss create mode 100644 src/components/CalendarShare.vue create mode 100644 src/components/CalendarSharee.vue diff --git a/css/src/Calendars/Calendar.scss b/css/src/Calendars/Calendar.scss new file mode 100644 index 00000000..4b6270ee --- /dev/null +++ b/css/src/Calendars/Calendar.scss @@ -0,0 +1,64 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * @author Team Popcorn + * @author Raimund Schlüßler + * + * @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 . + * + */ + +.calendar { + display: flex; + flex-wrap: wrap; + align-items: center; + white-space: nowrap; + text-overflow: ellipsis; + + > a:first-of-type { + // put actions at the end + margin-left: auto; + } + + &__name { + display: block; + flex: 0 1 auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + &__share, + &__menu .icon-more { + width: 44px; + height: 44px; + opacity: .5; + cursor: pointer; + &:hover, + &:focus, + &:active { + opacity: .7; + } + } + &__share { + &--shared { + opacity: .7; + } + } + &--disabled &__name { + opacity: .5; + } +} diff --git a/css/src/Calendars/CalendarSharee.scss b/css/src/Calendars/CalendarSharee.scss new file mode 100644 index 00000000..68fd914e --- /dev/null +++ b/css/src/Calendars/CalendarSharee.scss @@ -0,0 +1,85 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * @author Team Popcorn + * @author Raimund Schlüßler + * + * @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 . + * + */ + +.calendar-sharee { + padding: 0 5px; + display: inline-flex; + align-items: center; + width: 100%; + + .icon { + margin-right: 5px; + opacity: 0.2; + width: 16px; + height: 16px; + display: inline-block; + margin-bottom: 2px; + &.icon-loading-small { + opacity: 1; + } + } + + &__identifier { + width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + display: inline-block; + vertical-align: top; + opacity: 0.7; + } + + &__utils { + padding: 0 !important; + float: right; + position: relative !important; + display: inline-flex; + align-items: center; + flex-shrink: 0; + height: 20px; + + .icon-delete { + display: inline-block; + width: 20px; + height: 20px; + opacity: 0.4; + margin-bottom: 2px; + margin-left: 4px; + &:hover { + box-shadow: unset !important; + } + } + + // loading state + &--disabled { + opacity: .2 !important; + } + .checkbox + label { + padding: 0 !important; + } + label { + opacity: 0.7; + } + } +} diff --git a/css/src/Calendars/CalendarShares.scss b/css/src/Calendars/CalendarShares.scss new file mode 100644 index 00000000..425c0fed --- /dev/null +++ b/css/src/Calendars/CalendarShares.scss @@ -0,0 +1,62 @@ +/** + * @copyright Copyright (c) 2019 John Molakvoæ + * + * @author John Molakvoæ + * @author Team Popcorn + * @author Raimund Schlüßler + * + * @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 . + * + */ + +.calendar-shares { + width: calc(100% - 6px); + margin: 6px; + &__list { + margin-top: 8px; + margin-bottom: 12px; + display: flex; + flex-direction: column; + } + + &__shareematch--bold { + font-weight: bold; + } + + .icon-loading::after { + top: 70%; + left: 95%; + height: 14px; + width: 14px; + } + + .multiselect { + width: inherit; + margin: 0; + .multiselect__tags:focus-within, + .multiselect__tags:hover { + border-color: var(--color-primary-element); + } + + &:not(.showContent) .multiselect__content-wrapper { + display: none; + } + + .multiselect__content-wrapper { + z-index: 101 !important; + } + } +} diff --git a/css/src/style.scss b/css/src/style.scss index 2c90a4b2..3d637c39 100644 --- a/css/src/style.scss +++ b/css/src/style.scss @@ -47,8 +47,11 @@ display: none; } - &.list:not(.active) .app-navigation-entry-utils-menu-button { - display: none; + &.list:not(.active) { + .app-navigation-entry-utils-menu-button, + .calendar__share { + display: none; + } } .app-navigation-entry-edit { diff --git a/css/tasks.scss b/css/tasks.scss index 251aa849..1814a862 100644 --- a/css/tasks.scss +++ b/css/tasks.scss @@ -24,3 +24,7 @@ @import './src/sprites-bw'; @import './src/style'; @import './src/markdown'; + +@import './src/Calendars/Calendar.scss'; +@import './src/Calendars/CalendarShares.scss'; +@import './src/Calendars/CalendarSharee.scss'; diff --git a/src/components/CalendarShare.vue b/src/components/CalendarShare.vue new file mode 100644 index 00000000..d23d212c --- /dev/null +++ b/src/components/CalendarShare.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/components/CalendarSharee.vue b/src/components/CalendarSharee.vue new file mode 100644 index 00000000..ca9a388c --- /dev/null +++ b/src/components/CalendarSharee.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/components/TheList.vue b/src/components/TheList.vue index f39c58a1..a8992ad3 100644 --- a/src/components/TheList.vue +++ b/src/components/TheList.vue @@ -65,11 +65,19 @@ License along with this library. If not, see . {{ calendar.displayName }} +
  • {{ calendarCount(calendar.id) | counterFormatter }}
  • + + +
+ + + +
document.querySelector('#list_' + calendar.id + ' input.edit').focus() ) }, + toggleShare: function(calendar) { + if (this.shareOpen === calendar.id) { + this.shareOpen = '' + } else { + this.shareOpen = calendar.id + } + }, + hasShares: function(calendar) { + return calendar.shares.length > 0 + }, + // info tooltip about number of shares + sharedWithTooltip: function(calendar) { + return this.hasShares(calendar) + ? n('tasks', + 'Shared with {num} entity', + 'Shared with {num} entities', + calendar.shares.length, { + num: calendar.shares.length + }) + : '' // disable the tooltip + }, resetView: function(calendar) { if (this.editing === calendar.id) { this.editing = '' } + if (this.shareOpen === calendar.id) { + this.shareOpen = '' + } this.tooltipTarget = '' }, copyCalDAVUrl(event, calendar) { diff --git a/src/store/calendars.js b/src/store/calendars.js index ea569970..aa918390 100644 --- a/src/store/calendars.js +++ b/src/store/calendars.js @@ -75,11 +75,32 @@ export function mapDavCollectionToCalendar(calendar) { tasks: {}, url: calendar.url, dav: calendar, + shares: calendar.shares.map(sharee => Object.assign({}, mapDavShareeToSharee(sharee))), supportsTasks: calendar.components.includes('VTODO'), loadedCompleted: false, } } +/** + * Maps a dav collection to the sharee array + * + * @param {Object} sharee The sharee object from the cdav library shares + * @returns {Object} + */ +export function mapDavShareeToSharee(sharee) { + const id = sharee.href.split('/').slice(-1)[0] + const name = sharee['common-name'] + ? sharee['common-name'] + : id + return { + displayName: name, + id: id, + writeable: sharee.access[0].endsWith('read-write'), + isGroup: sharee.href.startsWith('principal:principals/groups/'), + uri: sharee.href + } +} + const getters = { /** @@ -309,16 +330,19 @@ const mutations = { * @param {Object} state The store data * @param {Object} data Destructuring object * @param {Calendar} data.calendar The calendar - * @param {String} data.sharee The sharee - * @param {String} data.id The id - * @param {Boolean} data.group The group + * @param {String} data.user The userId + * @param {String} data.displayName The displayName + * @param {String} data.uri The sharing principalScheme uri + * @param {Boolean} data.isGroup Is this a group ? */ - shareCalendar(state, { calendar, sharee, id, group }) { - let newSharee = { - displayname: sharee, - id, + shareCalendar(state, { calendar, user, displayName, uri, isGroup }) { + calendar = state.calendars.find(search => search.id === calendar.id) + const newSharee = { + displayName, + id: user, writeable: false, - group + isGroup, + uri } calendar.shares.push(newSharee) }, @@ -327,34 +351,27 @@ const mutations = { * Removes a sharee from calendar shares list * * @param {Object} state The store data - * @param {Object} sharee The sharee + * @param {Object} data Destructuring object + * @param {Calendar} data.calendar The calendar + * @param {String} data.uri The sharee uri */ - removeSharee(state, sharee) { - let calendar = state.calendars.find(search => { - for (let i in search.shares) { - if (search.shares[i] === sharee) { - return true - } - } - }) - calendar.shares.splice(calendar.shares.indexOf(sharee), 1) + removeSharee(state, { calendar, uri }) { + calendar = state.calendars.find(search => search.id === calendar.id) + let shareIndex = calendar.shares.findIndex(sharee => sharee.uri === uri) + calendar.shares.splice(shareIndex, 1) }, /** * Toggles sharee's writable permission * * @param {Object} state The store data - * @param {Object} sharee The sharee + * @param {Object} data Destructuring object + * @param {Object} data.calendar The calendar + * @param {String} data.uri The sharee uri */ - updateShareeWritable(state, sharee) { - let calendar = state.calendars.find(search => { - for (let i in search.shares) { - if (search.shares[i] === sharee) { - return true - } - } - }) - sharee = calendar.shares.find(search => search === sharee) + updateShareeWritable(state, { calendar, uri }) { + calendar = state.calendars.find(search => search.id === calendar.id) + let sharee = calendar.shares.find(sharee => sharee.uri === uri) sharee.writeable = !sharee.writeable } } @@ -567,20 +584,36 @@ const actions = { * Removes a sharee from a calendar * * @param {Object} context The store mutations Current context - * @param {Object} sharee Calendar sharee object + * @param {Object} data Destructuring object + * @param {Object} data.calendar The calendar + * @param {String} data.uri The sharee uri */ - removeSharee(context, sharee) { - context.commit('removeSharee', sharee) + async removeSharee(context, { calendar, uri }) { + try { + await calendar.dav.unshare(uri) + context.commit('removeSharee', { calendar, uri }) + } catch (error) { + throw error + } }, /** * Toggles permissions of calendar sharees writeable rights * * @param {Object} context The store mutations Current context - * @param {Object} sharee Calendar sharee object + * @param {Object} data Destructuring object + * @param {Object} data.calendar The calendar + * @param {String} data.uri The sharee uri + * @param {Boolean} data.writeable The sharee permission */ - toggleShareeWritable(context, sharee) { - context.commit('updateShareeWritable', sharee) + async toggleShareeWritable(context, { calendar, uri, writeable }) { + try { + await calendar.dav.share(uri, writeable) + context.commit('updateShareeWritable', { calendar, uri, writeable }) + } catch (error) { + throw error + } + }, /** @@ -588,13 +621,19 @@ const actions = { * * @param {Object} context The store mutations Current context * @param {Calendar} data.calendar The calendar - * @param {String} data.sharee The sharee - * @param {Boolean} data.id The id - * @param {Boolean} data.group The group + * @param {String} data.user The userId + * @param {String} data.displayName The displayName + * @param {String} data.uri The sharing principalScheme uri + * @param {Boolean} data.isGroup Is this a group ? */ - shareCalendar(context, { calendar, sharee, id, group }) { - // Share a calendar with the entered group or user - context.commit('shareCalendar', { calendar, sharee, id, group }) + async shareCalendar(context, { calendar, user, displayName, uri, isGroup }) { + // Share calendar with entered group or user + try { + await calendar.dav.share(uri) + context.commit('shareCalendar', { calendar, user, displayName, uri, isGroup }) + } catch (error) { + throw error + } }, }