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') }}
-
-
-
-
+
+
+
+
+
+ {{ t('contacts', 'Joining circle') }}
+
+
+
+
+ {{ t('contacts', 'Your request to join this circle is pending approval') }}
+
+
+
+ {{ t('contacts', 'You are not a member of this 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">
-
+
+ {{ t('contacts', 'Circle owned by {owner}', { owner: circle.owner.displayName}) }}
+
+
+
+ {{ t('contacts', 'Leave circle') }}
+
+
+
+
+
+ {{ joinButtonTitle }}
+
+
+
+
+
+
+ {{ copyButtonText }}
+
+
+
+
+ {{ t('contacts', 'Delete') }}
+
- {{ t('contacts', 'Description') }}
+
+ {{ t('contacts', 'Description') }}
+
-
+ :contenteditable="circle.isOwner"
+ :placeholder="descriptionPlaceholder"
+ class="circle-details-section__description"
+ @update:value="onDescriptionChangeDebounce" />
-
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
*