diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-06-28 13:04:34 +0200 |
---|---|---|
committer | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-07-01 10:56:33 +0200 |
commit | ee443f249edd2a7f8506b98ade7e58eacfed5def (patch) | |
tree | 3c1233c2ae440d785b59bad3863408e97d01efaf | |
parent | 8bbc0c2d3bc40122297b19665cf1df3ff3670e1f (diff) |
Approve request from contacts UI
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
-rw-r--r-- | src/components/AppContent/CircleContent.vue | 2 | ||||
-rw-r--r-- | src/components/CircleDetails.vue | 2 | ||||
-rw-r--r-- | src/components/MemberList.vue | 16 | ||||
-rw-r--r-- | src/components/MembersList/MembersListItem.vue | 69 | ||||
-rw-r--r-- | src/mixins/CircleActionsMixin.js | 5 | ||||
-rw-r--r-- | src/models/circle.d.ts | 8 | ||||
-rw-r--r-- | src/models/circle.ts | 16 | ||||
-rw-r--r-- | src/models/constants.d.ts | 11 | ||||
-rw-r--r-- | src/models/constants.ts | 19 | ||||
-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 |
14 files changed, 172 insertions, 30 deletions
diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue index 1fe478e4..cbe59bcc 100644 --- a/src/components/AppContent/CircleContent.vue +++ b/src/components/AppContent/CircleContent.vue @@ -47,7 +47,7 @@ <!-- not a member --> <template v-if="!circle.isMember"> <!-- Pending request validation --> - <EmptyContent v-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> diff --git a/src/components/CircleDetails.vue b/src/components/CircleDetails.vue index b9346491..20552a86 100644 --- a/src/components/CircleDetails.vue +++ b/src/components/CircleDetails.vue @@ -64,7 +64,7 @@ </a> <!-- Only show the join button if the circle is accepting requests --> - <button v-if="!circle.isMember && circle.canJoin" + <button v-if="!circle.isPendingMember && !circle.isMember && circle.canJoin" :disabled="loadingJoin" class="primary" @click="joinCircle"> 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 873db246..60e83765 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 { @@ -105,9 +106,11 @@ export default { async joinCircle() { this.loadingJoin = true try { - await joinCircle(this.circle.id) + const initiator = await joinCircle(this.circle.id) + this.circle.initiator = new Member(initiator) } catch (error) { showError(t('contacts', 'Unable to join the circle')) + console.error('Unable to join the circle', error) } finally { this.loadingJoin = false } diff --git a/src/models/circle.d.ts b/src/models/circle.d.ts index badc2ced..910c4b30 100644 --- a/src/models/circle.d.ts +++ b/src/models/circle.d.ts @@ -68,6 +68,10 @@ export default class Circle { */ get initiator(): Member; /** + * Set new circle initiator + */ + set initiator(initiator: Member); + /** * Circle ownership */ get owner(): Member; @@ -125,6 +129,10 @@ export default class Circle { */ get isMember(): boolean; /** + * Is the initiator a pending member of this circle? + */ + get isPendingMember(): boolean; + /** * Can the initiator delete this circle? */ get canDelete(): boolean; diff --git a/src/models/circle.ts b/src/models/circle.ts index 38e0ab0d..75346a0b 100644 --- a/src/models/circle.ts +++ b/src/models/circle.ts @@ -122,6 +122,13 @@ export default class Circle { } /** + * Set new circle initiator + */ + set initiator(initiator: Member) { + this._initiator = initiator + } + + /** * Circle ownership */ get owner(): Member { @@ -162,7 +169,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) } @@ -247,6 +254,13 @@ export default class Circle { } /** + * Is the initiator a pending member of this circle? + */ + get isPendingMember() { + return this.initiator?.level === MemberLevels.NONE + } + + /** * Can the initiator delete this circle? */ get canDelete() { diff --git a/src/models/constants.d.ts b/src/models/constants.d.ts index b2aaa26c..dcfe6110 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,7 @@ export declare enum CircleConfigs { CIRCLE_INVITE, FEDERATED } +export declare enum MemberStatus { + INVITED = "invited", + REQUESTING = "Requesting" +} diff --git a/src/models/constants.ts b/src/models/constants.ts index 9b7e83aa..b7a9b7f5 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', 'Pending'), + // [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,8 @@ export enum CircleConfigs { CIRCLE_INVITE = CIRCLE_CONFIG_CIRCLE_INVITE, FEDERATED = CIRCLE_CONFIG_FEDERATED, } + +export enum MemberStatus { + INVITED = 'invited', + 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 } |