From 9facd8fbf3621df573f0e56e54da0612482c3c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6=20=28skjnldsv=29?= Date: Tue, 27 Apr 2021 19:38:41 +0200 Subject: Refactor circle actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ (skjnldsv) --- src/components/AppContent/CircleContent.vue | 48 +++---- .../AppNavigation/CircleNavigationItem.vue | 93 +------------ src/components/AppNavigation/RootNavigation.vue | 10 +- src/components/CircleDetails.vue | 134 +++++++++++++++--- src/components/CircleDetails/CircleConfigs.vue | 35 +++-- src/components/CircleDetails/ContentHeading.vue | 13 ++ src/components/ContactDetails.vue | 1 + src/components/DetailsHeader.vue | 10 +- src/components/MembersList/MembersListItem.vue | 13 +- src/mixins/CircleActionsMixin.js | 152 +++++++++++++++++++++ src/models/constants.ts | 6 +- src/services/circles.d.ts | 14 +- src/services/circles.ts | 18 ++- src/store/circles.js | 24 +++- 14 files changed, 405 insertions(+), 166 deletions(-) create mode 100644 src/mixins/CircleActionsMixin.js diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue index 164aaf5b..eda997df 100644 --- a/src/components/AppContent/CircleContent.vue +++ b/src/components/AppContent/CircleContent.vue @@ -36,34 +36,36 @@ - - - - {{ t('contacts', 'You are not a member of this circle') }} - - - - - - - {{ t('contacts', 'Your request to join this circle is pending approval') }} - - - - {{ t('contacts', 'Joining circle') }} - - - diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue index eadea9a7..d6787a6e 100644 --- a/src/components/AppNavigation/CircleNavigationItem.vue +++ b/src/components/AppNavigation/CircleNavigationItem.vue @@ -1,5 +1,5 @@ + @click="confirmLeaveCircle"> {{ t('contacts', 'Leave circle') }} + @click="confirmDeleteCircle"> {{ t('contacts', 'Delete') }} @@ -83,8 +83,6 @@ diff --git a/src/components/AppNavigation/RootNavigation.vue b/src/components/AppNavigation/RootNavigation.vue index 675f5750..38398fab 100644 --- a/src/components/AppNavigation/RootNavigation.vue +++ b/src/components/AppNavigation/RootNavigation.vue @@ -69,7 +69,7 @@ - - + - - + + @input="onDisplayNameChangeDebounce"> - diff --git a/src/components/ContactDetails.vue b/src/components/ContactDetails.vue index f4e53ab4..5c316633 100644 --- a/src/components/ContactDetails.vue +++ b/src/components/ContactDetails.vue @@ -771,6 +771,7 @@ export default { .app-content-details { flex: 1 1 100%; min-width: 0; + padding: 0 80px; } // List of all properties diff --git a/src/components/DetailsHeader.vue b/src/components/DetailsHeader.vue index 6723bf10..b2b4d1a2 100644 --- a/src/components/DetailsHeader.vue +++ b/src/components/DetailsHeader.vue @@ -32,7 +32,7 @@

-
+
@@ -84,14 +84,12 @@ export default { display: flex; align-items: center; padding: 50px 0 20px; - font-weight: bold; &__avatar { position: relative; - flex: 1 1 var(--avatar-size); - min-width: var(--avatar-size); - max-width: 120px; + flex: 0 0 var(--avatar-size); margin: 10px; + margin-left: 0; display: flex; justify-content: flex-end; } @@ -114,7 +112,7 @@ export default { min-width: 100px; max-width: 100%; margin: 0; - padding: 4px 5px; + padding: 0; white-space: nowrap; text-overflow: ellipsis; border: none; diff --git a/src/components/MembersList/MembersListItem.vue b/src/components/MembersList/MembersListItem.vue index ae50e85c..9eb5a120 100644 --- a/src/components/MembersList/MembersListItem.vue +++ b/src/components/MembersList/MembersListItem.vue @@ -165,7 +165,7 @@ export default { // Object.keys returns those as string .map(level => parseInt(level, 10)) // we cannot set to a level higher than the current user's level - .filter(level => level < this.currentUserLevel) + .filter(level => level <= this.currentUserLevel) // we cannot set to the level this member is already .filter(level => level !== this.source.level) }, @@ -209,6 +209,10 @@ export default { * @returns {string} */ levelChangeLabel(level) { + if (level === MemberLevels.OWNER) { + return t('contacts', 'Promote as sole owner') + } + if (this.source.level < level) { return t('contacts', 'Promote to {level}', { level: CIRCLES_MEMBER_LEVELS[level] }) } @@ -245,6 +249,13 @@ export default { await changeMemberLevel(this.circle.id, this.source.id, level) this.showLevelMenu = false + // If we changed an owner, let's refresh the whole dataset to update all ownership & memberships + if (level === MemberLevels.OWNER) { + await this.$store.dispatch('getCircle', this.circle.id) + await this.$store.dispatch('getCircleMembers', this.circle.id) + return + } + // this.source is a class. We're modifying the class setter, not the prop itself // eslint-disable-next-line vue/no-mutating-props this.source.level = level diff --git a/src/mixins/CircleActionsMixin.js b/src/mixins/CircleActionsMixin.js new file mode 100644 index 00000000..a11cc1c0 --- /dev/null +++ b/src/mixins/CircleActionsMixin.js @@ -0,0 +1,152 @@ +/** + * @copyright Copyright (c) 2021 John Molakvoæ + * + * @author John Molakvoæ + * + * @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 . + * + */ +import { emit } from '@nextcloud/event-bus' +import { showError } from '@nextcloud/dialogs' + +import { joinCircle } from '../services/circles.ts' +import Circle from '../models/circle.ts' +import CopyToClipboardMixin from './CopyToClipboardMixin' + +export default { + + props: { + circle: { + type: Circle, + required: true, + }, + }, + + mixins: [CopyToClipboardMixin], + + data() { + return { + loadingAction: false, + } + }, + + computed: { + copyButtonText() { + if (this.copied) { + return this.copySuccess + ? t('contacts', 'Copied') + : t('contacts', 'Could not copy') + } + return t('contacts', 'Copy link') + }, + + circleUrl() { + const route = this.$router.resolve(this.circle.router) + return window.location.origin + route.href + }, + + joinButtonTitle() { + if (this.circle.requireJoinAccept) { + return t('contacts', 'Request to join') + } + return t('contacts', 'Join circle') + }, + }, + + methods: { + confirmLeaveCircle() { + OC.dialogs.confirmDestructive( + t('contacts', 'You are about to leave {circle}.\n Are you sure ?', { + circle: this.circle.displayName, + }), + t('contacts', 'Please confirm circle leave'), + OC.dialogs.YES_NO_BUTTONS, + this.leaveCircle, + true + ) + }, + async leaveCircle(confirm) { + if (!confirm) { + console.debug('Circle leave cancelled') + return + } + + this.loadingAction = true + const member = this.circle.initiator + + try { + await this.$store.dispatch('deleteMemberFromCircle', { + member, + leave: true, + }) + } catch (error) { + console.error('Could not leave the circle', member, error) + showError(t('contacts', 'Could not leave the circle {displayName}', this.circle)) + } finally { + this.loadingAction = false + } + + }, + + async joinCircle() { + this.loadingAction = true + try { + await joinCircle(this.circle.id) + } catch (error) { + showError(t('contacts', 'Unable to join the circle')) + } finally { + this.loadingAction = false + } + + }, + + confirmDeleteCircle() { + OC.dialogs.confirmDestructive( + t('contacts', 'You are about to delete {circle}.\n Are you sure ?', { + circle: this.circle.displayName, + }), + t('contacts', 'Please confirm circle deletion'), + OC.dialogs.YES_NO_BUTTONS, + this.deleteCircle, + true + ) + }, + async deleteCircle(confirm) { + if (!confirm) { + console.debug('Circle deletion cancelled') + return + } + + this.loadingAction = true + + try { + this.$store.dispatch('deleteCircle', this.circle.id) + } catch (error) { + showError(t('contacts', 'Unable to delete the circle')) + } finally { + this.loadingAction = false + } + }, + + /** + * Trigger the entity picker view + */ + async addMemberToCircle() { + await this.$router.push(this.circle.router) + emit('contacts:circles:append', this.circle.id) + }, + }, +} diff --git a/src/models/constants.ts b/src/models/constants.ts index 3f5aed68..520e4b63 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -96,7 +96,7 @@ export const PUBLIC_CIRCLE_CONFIG = { [t('contacts', 'Invites')]: { [CIRCLE_CONFIG_OPEN]: t('contacts', 'Open, anyone can join'), [CIRCLE_CONFIG_INVITE]: t('contacts', 'Members need to accept invitation'), - [CIRCLE_CONFIG_REQUEST]: t('contacts', 'Members need to be accepted by a moderator'), + [CIRCLE_CONFIG_REQUEST]: t('contacts', 'Members need to be accepted by a moderator (requires Open)'), [CIRCLE_CONFIG_FRIEND]: t('contacts', 'Members can also invite'), // Let's manage password protection independently as we also need a password // [CIRCLE_CONFIG_PROTECTED]: t('contacts', 'Password protect'), @@ -104,11 +104,11 @@ export const PUBLIC_CIRCLE_CONFIG = { [t('contacts', 'Visibility')]: { [CIRCLE_CONFIG_VISIBLE]: t('contacts', 'Visible to everyone'), - [CIRCLE_CONFIG_HIDDEN]: t('contacts', 'Hide this circle from listings'), }, [t('contacts', 'Circle membership')]: { - [CIRCLE_CONFIG_CIRCLE_INVITE]: t('contacts', 'Circle must confirm when invited in another circle'), + // TODO: implement backend + // [CIRCLE_CONFIG_CIRCLE_INVITE]: t('contacts', 'Circle must confirm when invited in another circle'), [CIRCLE_CONFIG_ROOT]: t('contacts', 'Prevent circle from being a member of another circle'), }, } diff --git a/src/services/circles.d.ts b/src/services/circles.d.ts index 12ecb11c..87dc4017 100644 --- a/src/services/circles.d.ts +++ b/src/services/circles.d.ts @@ -37,6 +37,12 @@ export declare enum CircleEdit { * @returns {Array} */ export declare const getCircles: () => Promise; +/** + * Get a specific circle + * @param {string} circleId + * @returns {Object} + */ +export declare const getCircle: (circleId: string) => Promise; /** * Create a new circle * @@ -47,14 +53,14 @@ export declare const createCircle: (name: string) => Promise; /** * Delete an existing circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Object} */ export declare const deleteCircle: (circleId: string) => Promise; /** * Edit an existing circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @param {CircleEditType} type the edit type * @param {any} data the data * @returns {Object} @@ -63,14 +69,14 @@ export declare const editCircle: (circleId: string, type: CircleEditType, value: /** * Join a circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Array} */ export declare const joinCircle: (circleId: string) => Promise; /** * Leave a circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Array} */ export declare const leaveCircle: (circleId: string) => Promise; diff --git a/src/services/circles.ts b/src/services/circles.ts index b72d6ead..d5c20db6 100644 --- a/src/services/circles.ts +++ b/src/services/circles.ts @@ -46,6 +46,16 @@ export const getCircles = async function() { return response.data.ocs.data } +/** + * Get a specific circle + * @param {string} circleId + * @returns {Object} + */ +export const getCircle = async function(circleId: string) { + const response = await axios.get(generateOcsUrl('apps/circles/circles/{circleId}', { circleId })) + return response.data.ocs.data +} + /** * Create a new circle * @@ -62,7 +72,7 @@ export const createCircle = async function(name: string) { /** * Delete an existing circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Object} */ export const deleteCircle = async function(circleId: string) { @@ -73,7 +83,7 @@ export const deleteCircle = async function(circleId: string) { /** * Edit an existing circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @param {CircleEditType} type the edit type * @param {any} data the data * @returns {Object} @@ -86,7 +96,7 @@ export const editCircle = async function(circleId: string, type: CircleEditType, /** * Join a circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Array} */ export const joinCircle = async function(circleId: string) { @@ -97,7 +107,7 @@ export const joinCircle = async function(circleId: string) { /** * Leave a circle * - * @param {string} circleId the circle name + * @param {string} circleId the circle id * @returns {Array} */ export const leaveCircle = async function(circleId: string) { diff --git a/src/store/circles.js b/src/store/circles.js index bd9efaba..72d8cb60 100644 --- a/src/store/circles.js +++ b/src/store/circles.js @@ -23,7 +23,7 @@ import { showError } from '@nextcloud/dialogs' import Vue from 'vue' -import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircles, leaveCircle, addMembers } from '../services/circles.ts' +import { createCircle, deleteCircle, deleteMember, getCircleMembers, getCircle, getCircles, leaveCircle, addMembers } from '../services/circles.ts' import Member from '../models/member.ts' import Circle from '../models/circle.ts' @@ -102,7 +102,6 @@ const getters = { } const actions = { - /** * Retrieve and commit circles * @@ -131,6 +130,27 @@ const actions = { return circles }, + /** + * Retrieve and commit circles + * + * @param {Object} context the store mutations + * @param {string} circleId the circle id + * @returns {Object[]} the circles + */ + async getCircle(context, circleId) { + const circle = await getCircle(circleId) + console.debug('Retrieved 1 circle', circle) + + try { + const newCircle = new Circle(circle) + context.commit('addCircle', newCircle) + } catch (error) { + console.error('This circle failed to be processed', circle, error) + } + + return circle + }, + /** * Retrieve and commit circle members * -- cgit v1.2.3