diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue index 164aaf5b7..eda997dfe 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 eadea9a72..d6787a6e6 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 675f57506..38398fab7 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 44540647a..0ffef57c9 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 6723bf10a..b2b4d1a23 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 ae50e85c5..9eb5a1209 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 000000000..3913a3e91 --- /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.leaveCircle, + 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/services/circles.d.ts b/src/services/circles.d.ts index 12ecb11cb..87dc4017d 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 b72d6ead7..d5c20db6a 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 bd9efabac..72d8cb60e 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 *