diff options
author | John Molakvoæ <skjnldsv@users.noreply.github.com> | 2021-07-01 12:25:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-01 12:25:32 +0200 |
commit | e07690525c94a82c203e328a66020c3ae8b68cfd (patch) | |
tree | 76aeb4a23f92d3e47070f141c9e1f764e85b3ee6 | |
parent | 017026a8e42717cc8e1935af5ce61aa46b3e9f79 (diff) | |
parent | 904273d4a1ade3fa07e26cac1fab44f0c2cb0333 (diff) |
Merge pull request #2316 from nextcloud/fix/pending-member
-rw-r--r-- | src/components/AppContent/CircleContent.vue | 40 | ||||
-rw-r--r-- | src/components/AppNavigation/CircleNavigationItem.vue | 1 | ||||
-rw-r--r-- | src/components/CircleDetails.vue | 22 | ||||
-rw-r--r-- | src/components/MemberList.vue | 16 | ||||
-rw-r--r-- | src/components/MembersList/MembersListItem.vue | 69 | ||||
-rw-r--r-- | src/mixins/CircleActionsMixin.js | 19 | ||||
-rw-r--r-- | src/models/circle.d.ts | 18 | ||||
-rw-r--r-- | src/models/circle.ts | 33 | ||||
-rw-r--r-- | src/models/constants.d.ts | 12 | ||||
-rw-r--r-- | src/models/constants.ts | 20 | ||||
-rw-r--r-- | src/models/member.d.ts | 5 | ||||
-rw-r--r-- | src/models/member.ts | 8 | ||||
-rw-r--r-- | src/services/circles.d.ts | 8 | ||||
-rw-r--r-- | src/services/circles.ts | 12 | ||||
-rw-r--r-- | src/store/circles.js | 21 |
15 files changed, 226 insertions, 78 deletions
diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue index a0f1de97..cbe59bcc 100644 --- a/src/components/AppContent/CircleContent.vue +++ b/src/components/AppContent/CircleContent.vue @@ -46,45 +46,27 @@ <CircleDetails :circle="circle"> <!-- not a member --> <template v-if="!circle.isMember"> - <!-- Join request in progress --> - <EmptyContent v-if="loadingJoin" icon="icon-loading"> - {{ t('contacts', 'Joining circle') }} - </EmptyContent> - <!-- Pending request validation --> - <EmptyContent v-else-if="circle.isPendingJoin" icon="icon-loading"> + <EmptyContent v-if="circle.isPendingMember" icon="icon-loading"> {{ t('contacts', 'Your request to join this circle is pending approval') }} </EmptyContent> <EmptyContent v-else icon="icon-circles"> {{ t('contacts', 'You are not a member of {circle}', { circle: circle.displayName}) }} - - <!-- Only show the join button if the circle is accepting requests --> - <template v-if="circle.canJoin" #desc> - <button :disabled="loadingJoin" class="primary" @click="requestJoin"> - <Login slot="icon" - :size="16" - decorative /> - {{ t('contacts', 'Request to join') }} - </button> - </template> </EmptyContent> </template> </CircleDetails> </AppContent> </template> <script> +import { showError } from '@nextcloud/dialogs' import AppContent from '@nextcloud/vue/dist/Components/AppContent' import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent' import isMobile from '@nextcloud/vue/dist/Mixins/isMobile' -import Login from 'vue-material-design-icons/Login' - import CircleDetails from '../CircleDetails' import MemberList from '../MemberList' import RouterMixin from '../../mixins/RouterMixin' -import { joinCircle } from '../../services/circles.ts' -import { showError } from '@nextcloud/dialogs' export default { name: 'CircleContent', @@ -93,7 +75,6 @@ export default { AppContent, CircleDetails, EmptyContent, - Login, MemberList, }, @@ -108,7 +89,6 @@ export default { data() { return { - loadingJoin: false, loadingList: false, showDetails: false, } @@ -164,22 +144,6 @@ export default { } }, - /** - * Request to join this circle - */ - async requestJoin() { - this.loadingJoin = true - - try { - await joinCircle(this.circle.id) - } catch (error) { - showError(t('contacts', 'Unable to join the circle')) - } finally { - this.loadingJoin = false - } - - }, - // Hide the circle details hideDetails() { this.showDetails = false diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue index 9b4ec1d6..e2ee53eb 100644 --- a/src/components/AppNavigation/CircleNavigationItem.vue +++ b/src/components/AppNavigation/CircleNavigationItem.vue @@ -61,6 +61,7 @@ <!-- join circle --> <ActionButton v-else-if="!circle.isMember && circle.canJoin" + :disabled="loadingJoin" @click="joinCircle"> {{ joinButtonTitle }} <LocationEnter slot="icon" diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue index 774d7dce..20552a86 100644 --- a/src/components/CircleDetails.vue +++ b/src/components/CircleDetails.vue @@ -54,7 +54,7 @@ </template> </DetailsHeader> - <section class="circle-details-section"> + <section class="circle-details-section circle-details-section__actions"> <!-- copy circle link --> <a class="circle-details__action-copy-link button" :href="circleUrl" @@ -62,6 +62,17 @@ @click.stop.prevent="copyToClipboard(circleUrl)"> {{ copyButtonText }} </a> + + <!-- Only show the join button if the circle is accepting requests --> + <button v-if="!circle.isPendingMember && !circle.isMember && circle.canJoin" + :disabled="loadingJoin" + class="primary" + @click="joinCircle"> + <Login slot="icon" + :size="16" + decorative /> + {{ t('contacts', 'Request to join') }} + </button> </section> <section v-if="showDescription" class="circle-details-section"> @@ -118,6 +129,7 @@ import AppContentDetails from '@nextcloud/vue/dist/Components/AppContentDetails' import Avatar from '@nextcloud/vue/dist/Components/Avatar' import RichContenteditable from '@nextcloud/vue/dist/Components/RichContenteditable' +import Login from 'vue-material-design-icons/Login' import Logout from 'vue-material-design-icons/Logout' import { CircleEdit, editCircle } from '../services/circles.ts' @@ -135,6 +147,7 @@ export default { CircleConfigs, ContentHeading, DetailsHeader, + Login, Logout, RichContenteditable, }, @@ -223,6 +236,13 @@ export default { margin-top: 24px; } + &__actions { + display: flex; + a, button { + margin-right: 8px; + } + } + &__description { max-width: 800px; } diff --git a/src/components/MemberList.vue b/src/components/MemberList.vue index e8a1a6d7..5c8e31ff 100644 --- a/src/components/MemberList.vue +++ b/src/components/MemberList.vue @@ -21,14 +21,16 @@ --> <template> - <AppContentList v-if="!hasMembers && loading"> - <EmptyContent icon="icon-loading"> + <AppContentList v-if="!hasMembers" class="members-list"> + <EmptyContent v-if="loading" icon="icon-loading"> {{ t('contacts', 'Loading members list …') }} </EmptyContent> - </AppContentList> - <AppContentList v-else-if="!hasMembers"> - <EmptyContent icon="icon-contacts"> + <EmptyContent v-else-if="!circle.isMember" icon="icon-contacts"> + {{ t('contacts', 'The list of members is only visible to members of this circle') }} + </EmptyContent> + + <EmptyContent v-else icon="icon-contacts"> {{ t('contacts', 'There is no member in this circle') }} </EmptyContent> </AppContentList> @@ -302,5 +304,9 @@ export default { width: 100%; } } + + &::v-deep .empty-content { + margin: auto; + } } </style> diff --git a/src/components/MembersList/MembersListItem.vue b/src/components/MembersList/MembersListItem.vue index 47795d00..f4dd220e 100644 --- a/src/components/MembersList/MembersListItem.vue +++ b/src/components/MembersList/MembersListItem.vue @@ -34,12 +34,28 @@ :title="source.displayName" :user="source.userId" class="members-list__item"> - <Actions @close="onMenuClose"> - <template v-if="loading"> - <ActionText icon="icon-loading-small"> - {{ t('contacts', 'Loading …') }} - </ActionText> - </template> + <!-- Accept invite --> + <template v-if="!loading && isPendingApproval && circle.canManageMembers"> + <Actions> + <ActionButton + icon="icon-checkmark" + @click="acceptMember"> + {{ t('contacts', 'Accept membership request') }} + </ActionButton> + </Actions> + <Actions> + <ActionButton + icon="icon-close" + @click="deleteMember"> + {{ t('contacts', 'Reject membership request') }} + </ActionButton> + </Actions> + </template> + + <Actions v-else @close="onMenuClose"> + <ActionText v-if="loading" icon="icon-loading-small"> + {{ t('contacts', 'Loading …') }} + </ActionText> <!-- Normal menu --> <template v-else> @@ -78,7 +94,7 @@ </template> <script> -import { CIRCLES_MEMBER_LEVELS, MemberLevels } from '../../models/constants.ts' +import { CIRCLES_MEMBER_LEVELS, MemberLevels, MemberStatus } from '../../models/constants.ts' import Actions from '@nextcloud/vue/dist/Components/Actions' import ListItemIcon from '@nextcloud/vue/dist/Components/ListItemIcon' @@ -134,6 +150,10 @@ export default { * @returns {string} */ levelName() { + if (this.source.level === MemberLevels.NONE) { + return t('contacts', 'Pending') + } + return CIRCLES_MEMBER_LEVELS[this.source.level] || CIRCLES_MEMBER_LEVELS[MemberLevels.MEMBER] }, @@ -188,6 +208,15 @@ export default { }, /** + * Is the current member pending moderator approval? + * @returns {boolean} + */ + isPendingApproval() { + return this.source?.level === MemberLevels.NONE + && this.source?.status === MemberStatus.REQUESTING + }, + + /** * Can the current user change the level of others? * @returns {boolean} */ @@ -195,7 +224,8 @@ export default { // we can change if the member is at the same // or lower level as the current user // BUT not an owner as there can/must always be one - return this.availableLevelsChange.length > 0 + return this.source.level > MemberLevels.NONE + && this.availableLevelsChange.length > 0 && this.currentUserLevel >= this.source.level && this.circle.canManageMembers && !(this.circle.isOwner && this.isCurrentUser) @@ -206,7 +236,7 @@ export default { * @returns {boolean} */ canDelete() { - return this.currentUserLevel > MemberLevels.MEMBER + return this.circle.canManageMembers && this.source.level <= this.currentUserLevel && !this.isCurrentUser }, @@ -278,6 +308,22 @@ export default { } }, + async acceptMember() { + this.loading = true + + try { + await await this.$store.dispatch('acceptCircleMember', { + circleId: this.circle.id, + memberId: this.source.id, + }) + } catch (error) { + console.error('Could not accept member join request', this.source, error) + showError(t('contacts', 'Could not accept member join request')) + } finally { + this.loading = false + } + }, + /** * Reset menu on close */ @@ -295,12 +341,12 @@ export default { order: 1; padding-top: 22px; padding-left: 8px; + user-select: none; white-space: nowrap; text-overflow: ellipsis; + pointer-events: none; color: var(--color-primary-element); line-height: 22px; - user-select: none; - pointer-events: none; } .members-list__item { @@ -312,4 +358,5 @@ export default { background-color: var(--color-background-hover); } } + </style> diff --git a/src/mixins/CircleActionsMixin.js b/src/mixins/CircleActionsMixin.js index 00f0c299..d79b6511 100644 --- a/src/mixins/CircleActionsMixin.js +++ b/src/mixins/CircleActionsMixin.js @@ -25,6 +25,7 @@ import { showError } from '@nextcloud/dialogs' import { joinCircle } from '../services/circles.ts' import Circle from '../models/circle.ts' import CopyToClipboardMixin from './CopyToClipboardMixin' +import Member from '../models/member.ts' export default { @@ -40,6 +41,7 @@ export default { data() { return { loadingAction: false, + loadingJoin: false, } }, @@ -92,6 +94,9 @@ export default { member, leave: true, }) + + // Reset initiator + this.circle.initiator = null } catch (error) { console.error('Could not leave the circle', member, error) showError(t('contacts', 'Could not leave the circle {displayName}', this.circle)) @@ -102,13 +107,21 @@ export default { }, async joinCircle() { - this.loadingAction = true + this.loadingJoin = true try { - await joinCircle(this.circle.id) + const initiator = await joinCircle(this.circle.id) + const member = new Member(initiator, this.circle) + + // Update initiator with newest membership values + this.circle.initiator = member + + // Append new member + member.circle.addMember(member) } catch (error) { showError(t('contacts', 'Unable to join the circle')) + console.error('Unable to join the circle', error) } finally { - this.loadingAction = false + this.loadingJoin = false } }, diff --git a/src/models/circle.d.ts b/src/models/circle.d.ts index badc2ced..25dc0536 100644 --- a/src/models/circle.d.ts +++ b/src/models/circle.d.ts @@ -65,8 +65,14 @@ export default class Circle { /** * Circle ini_initiator the current * user info for this circle + * null if not a member */ - get initiator(): Member; + get initiator(): Member | null; + /** + * Set new circle initiator + * null if not a member + */ + set initiator(initiator: Member | null); /** * Circle ownership */ @@ -123,7 +129,11 @@ export default class Circle { /** * Is the initiator a member of this circle? */ - get isMember(): boolean; + get isMember(): boolean | 0 | undefined; + /** + * Is the initiator a pending member of this circle? + */ + get isPendingMember(): boolean; /** * Can the initiator delete this circle? */ @@ -131,11 +141,11 @@ export default class Circle { /** * Can the initiator leave this circle? */ - get canLeave(): boolean; + get canLeave(): boolean | 0 | undefined; /** * Can the initiator add/remove members to this circle? */ - get canManageMembers(): boolean; + get canManageMembers(): boolean | 0 | undefined; /** * Vue router param */ diff --git a/src/models/circle.ts b/src/models/circle.ts index 38e0ab0d..de08d191 100644 --- a/src/models/circle.ts +++ b/src/models/circle.ts @@ -23,7 +23,7 @@ import Vue from 'vue' import Member from './member' -import { CircleConfig, CircleConfigs, MemberLevels } from './constants' +import { CircleConfigs, MemberLevels } from './constants' type MemberList = Record<string, Member> @@ -116,12 +116,24 @@ export default class Circle { /** * Circle ini_initiator the current * user info for this circle + * null if not a member */ - get initiator(): Member { + get initiator(): Member|null { return this._initiator } /** + * Set new circle initiator + * null if not a member + */ + set initiator(initiator: Member|null) { + if (initiator && initiator.constructor.name !== Member.name) { + throw new Error('Initiator must be a Member type') + } + Vue.set(this, '_initiator', initiator) + } + + /** * Circle ownership */ get owner(): Member { @@ -135,7 +147,7 @@ export default class Circle { if (owner.constructor.name !== Member.name) { throw new Error('Owner must be a Member type') } - this._owner = owner + Vue.set(this, '_owner', owner) } /** @@ -162,7 +174,7 @@ export default class Circle { const singleId = member.singleId if (this._members[singleId]) { - console.warn('Ignoring duplicate member', member) + console.warn('Replacing existing member data', member) } Vue.set(this._members, singleId, member) } @@ -243,7 +255,15 @@ export default class Circle { * Is the initiator a member of this circle? */ get isMember() { - return this.initiator?.level > MemberLevels.NONE + return this.initiator?.level + && this.initiator?.level > MemberLevels.NONE + } + + /** + * Is the initiator a pending member of this circle? + */ + get isPendingMember() { + return this.initiator?.level === MemberLevels.NONE } /** @@ -264,7 +284,8 @@ export default class Circle { * Can the initiator add/remove members to this circle? */ get canManageMembers() { - return this.initiator?.level >= MemberLevels.MODERATOR + return this.initiator?.level + && this.initiator?.level >= MemberLevels.MODERATOR } // PARAMS --------------------------------------------- diff --git a/src/models/constants.d.ts b/src/models/constants.d.ts index b2aaa26c..172623bb 100644 --- a/src/models/constants.d.ts +++ b/src/models/constants.d.ts @@ -19,13 +19,14 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ +export declare type DefaultGroup = string; export declare type CircleConfig = number; export declare type MemberLevel = number; export declare type MemberType = number; export declare const LIST_SIZE = 60; -export declare const GROUP_ALL_CONTACTS: string; -export declare const GROUP_NO_GROUP_CONTACTS: string; -export declare const GROUP_RECENTLY_CONTACTED: string; +export declare const GROUP_ALL_CONTACTS: DefaultGroup; +export declare const GROUP_NO_GROUP_CONTACTS: DefaultGroup; +export declare const GROUP_RECENTLY_CONTACTED: DefaultGroup; export declare const ROUTE_CIRCLE = "circle"; export declare const ELLIPSIS_COUNT = 5; export declare const CIRCLE_DESC: string; @@ -76,3 +77,8 @@ export declare enum CircleConfigs { CIRCLE_INVITE, FEDERATED } +export declare enum MemberStatus { + INVITED = "Invited", + MEMBER = "Member", + REQUESTING = "Requesting" +} diff --git a/src/models/constants.ts b/src/models/constants.ts index d8356e17..67b0e7ee 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -28,6 +28,7 @@ interface OC extends Nextcloud.Common.OC { } declare const OC: OC +export type DefaultGroup = string export type CircleConfig = number export type MemberLevel = number export type MemberType = number @@ -35,10 +36,10 @@ export type MemberType = number // Global sizes export const LIST_SIZE = 60 -// Dynamic groups -export const GROUP_ALL_CONTACTS = t('contacts', 'All contacts') -export const GROUP_NO_GROUP_CONTACTS = t('contacts', 'Not grouped') -export const GROUP_RECENTLY_CONTACTED = t('contactsinteraction', 'Recently contacted') +// Dynamic default groups +export const GROUP_ALL_CONTACTS: DefaultGroup = t('contacts', 'All contacts') +export const GROUP_NO_GROUP_CONTACTS: DefaultGroup = t('contacts', 'Not grouped') +export const GROUP_RECENTLY_CONTACTED: DefaultGroup = t('contactsinteraction', 'Recently contacted') // Circle route, see vue-router conf export const ROUTE_CIRCLE = 'circle' @@ -78,6 +79,7 @@ const CIRCLE_CONFIG_ROOT: CircleConfig = 4096 // Circle cannot be inside anot const CIRCLE_CONFIG_CIRCLE_INVITE: CircleConfig = 8192 // Circle must confirm when invited in another circle const CIRCLE_CONFIG_FEDERATED: CircleConfig = 16384 // Federated +// Existing members types export const CIRCLES_MEMBER_TYPES = { [MEMBER_TYPE_CIRCLE]: t('circles', 'Circle'), [MEMBER_TYPE_USER]: t('circles', 'User'), @@ -86,14 +88,16 @@ export const CIRCLES_MEMBER_TYPES = { [MEMBER_TYPE_CONTACT]: t('circles', 'Contact'), } +// Available circles promote/demote levels export const CIRCLES_MEMBER_LEVELS = { - // [MEMBER_LEVEL_NONE]: t('circles', 'None'), + // [MEMBER_LEVEL_NONE]: t('circles', 'Pending'), [MEMBER_LEVEL_MEMBER]: t('circles', 'Member'), [MEMBER_LEVEL_MODERATOR]: t('circles', 'Moderator'), [MEMBER_LEVEL_ADMIN]: t('circles', 'Admin'), [MEMBER_LEVEL_OWNER]: t('circles', 'Owner'), } +// Available circle configs in the circle details view export const PUBLIC_CIRCLE_CONFIG = { [t('contacts', 'Invites')]: { [CIRCLE_CONFIG_OPEN]: t('contacts', 'Anyone can request membership'), @@ -205,3 +209,9 @@ export enum CircleConfigs { CIRCLE_INVITE = CIRCLE_CONFIG_CIRCLE_INVITE, FEDERATED = CIRCLE_CONFIG_FEDERATED, } + +export enum MemberStatus { + INVITED = 'Invited', + MEMBER = 'Member', + REQUESTING = 'Requesting', +} diff --git a/src/models/member.d.ts b/src/models/member.d.ts index cc929ca0..988bb0d9 100644 --- a/src/models/member.d.ts +++ b/src/models/member.d.ts @@ -66,6 +66,11 @@ export default class Member { */ get level(): MemberLevel; /** + * Member request status + * + */ + get status(): string; + /** * Set member level */ set level(level: MemberLevel); diff --git a/src/models/member.ts b/src/models/member.ts index ac28975f..0e2adc5c 100644 --- a/src/models/member.ts +++ b/src/models/member.ts @@ -117,6 +117,14 @@ export default class Member { } /** + * Member request status + * + */ + get status(): string { + return this._data.status + } + + /** * Set member level */ set level(level: MemberLevel) { diff --git a/src/services/circles.d.ts b/src/services/circles.d.ts index a19351db..e130d160 100644 --- a/src/services/circles.d.ts +++ b/src/services/circles.d.ts @@ -120,4 +120,12 @@ export declare const deleteMember: (circleId: string, memberId: string) => Promi * @returns {Array} */ export declare const changeMemberLevel: (circleId: string, memberId: string, level: MemberLevel) => Promise<unknown[]>; +/** + * Accept a circle member request + * + * @param {string} circleId the circle id + * @param {string} memberId the member id + * @returns {Array} + */ +export declare const acceptMember: (circleId: string, memberId: string) => Promise<any>; export {}; diff --git a/src/services/circles.ts b/src/services/circles.ts index d43c88ce..5d93bc7a 100644 --- a/src/services/circles.ts +++ b/src/services/circles.ts @@ -182,3 +182,15 @@ export const changeMemberLevel = async function(circleId: string, memberId: stri }) return Object.values(response.data.ocs.data) } + +/** + * Accept a circle member request + * + * @param {string} circleId the circle id + * @param {string} memberId the member id + * @returns {Array} + */ +export const acceptMember = async function(circleId: string, memberId: string) { + const response = await axios.put(generateOcsUrl('apps/circles/circles/{circleId}/members/{memberId}', { circleId, memberId })) + return response.data.ocs.data +} diff --git a/src/store/circles.js b/src/store/circles.js index d424c15d..e542816e 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, getCircle, getCircles, leaveCircle, addMembers } from '../services/circles.ts' +import { acceptMember, createCircle, deleteCircle, deleteMember, getCircleMembers, getCircle, getCircles, leaveCircle, addMembers } from '../services/circles.ts' import Member from '../models/member.ts' import Circle from '../models/circle.ts' import logger from '../services/logger' @@ -82,7 +82,7 @@ const mutations = { */ addMemberToCircle(state, { circleId, member }) { const circle = state.circles[circleId] - circle.addmember(member) + circle.addMember(member) }, /** @@ -256,6 +256,23 @@ const actions = { logger.debug('Deleted member', { circleId, memberId }) }, + /** + * Accept a circle member request + * + * @param {Object} context the store mutations Current context + * @param {Object} data destructuring object + * @param {string} data.circleId the circle id + * @param {string} data.memberId the member id + */ + async acceptCircleMember(context, { circleId, memberId }) { + const circle = context.getters.getCircle(circleId) + + const result = await acceptMember(circleId, memberId) + const member = new Member(result, circle) + + await context.commit('addMemberToCircle', { circleId, member }) + }, + } export default { state, mutations, getters, actions } |