diff --git a/src/components/AppContent/CircleContent.vue b/src/components/AppContent/CircleContent.vue index c7fcc62e5..a3961f607 100644 --- a/src/components/AppContent/CircleContent.vue +++ b/src/components/AppContent/CircleContent.vue @@ -5,7 +5,7 @@ + @@ -31,6 +32,9 @@ import AccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue' import CircleDetails from '../CircleDetails.vue' import RouterMixin from '../../mixins/RouterMixin.js' import IsMobileMixin from '../../mixins/IsMobileMixin.ts' +import UserGroupDetails from '../UserGroupDetails.vue' +import useUserGroupStore from '../../store/userGroup.ts' +import { mapStores } from 'pinia' export default { name: 'CircleContent', @@ -41,6 +45,7 @@ export default { EmptyContent, AccountGroup, IconLoading, + UserGroupDetails, }, mixins: [IsMobileMixin, RouterMixin], @@ -66,6 +71,9 @@ export default { circle() { return this.$store.getters.getCircle(this.selectedCircle) }, + userGroup() { + return this.userGroupStore.getUserGroup(this.selectedUserGroup) + }, members() { return Object.values(this.circle?.members || []) }, @@ -78,6 +86,7 @@ export default { isEmptyCircle() { return this.members.length === 0 }, + ...mapStores(useUserGroupStore), }, watch: { @@ -86,12 +95,21 @@ export default { this.fetchCircleMembers(newCircle.id) } }, + userGroup(newUserGroup) { + if (newUserGroup?.id) { + this.fetchUserGroupMembers(newUserGroup.id) + } + }, }, beforeMount() { if (this.circle?.id) { this.fetchCircleMembers(this.circle.id) } + + if (this.userGroup?.id) { + this.fetchUserGroupMembers(this.userGroup.id) + } }, methods: { @@ -108,6 +126,18 @@ export default { this.loadingList = false } }, + async fetchUserGroupMembers(userGroupId) { + this.loadingList = true + + try { + await this.userGroupStore.getUserGroupMembers(userGroupId) + } catch (error) { + console.error(error) + showError(t('contacts', 'There was an error fetching the member list')) + } finally { + this.loadingList = false + } + }, }, } diff --git a/src/components/AppNavigation/CircleNavigationItem.vue b/src/components/AppNavigation/CircleNavigationItem.vue index 92e8620e8..c6db198ce 100644 --- a/src/components/AppNavigation/CircleNavigationItem.vue +++ b/src/components/AppNavigation/CircleNavigationItem.vue @@ -92,6 +92,7 @@ import AccountGroupOutline from 'vue-material-design-icons/AccountGroupOutline.v import Circle from '../../models/circle.ts' import CircleActionsMixin from '../../mixins/CircleActionsMixin.js' +import UserGroup from '../../models/userGroup.ts' export default { name: 'CircleNavigationItem', @@ -116,7 +117,7 @@ export default { props: { circle: { - type: Circle, + type: [Circle, UserGroup], required: true, }, }, diff --git a/src/components/AppNavigation/RootNavigation.vue b/src/components/AppNavigation/RootNavigation.vue index 503b27537..18bb56925 100644 --- a/src/components/AppNavigation/RootNavigation.vue +++ b/src/components/AppNavigation/RootNavigation.vue @@ -169,6 +169,7 @@ + + diff --git a/src/components/UserGroupDetails/UserGroupMember.vue b/src/components/UserGroupDetails/UserGroupMember.vue new file mode 100644 index 000000000..3ea0b18ce --- /dev/null +++ b/src/components/UserGroupDetails/UserGroupMember.vue @@ -0,0 +1,93 @@ + + + + + + + diff --git a/src/mixins/RouterMixin.js b/src/mixins/RouterMixin.js index c875783d0..e88635ff3 100644 --- a/src/mixins/RouterMixin.js +++ b/src/mixins/RouterMixin.js @@ -14,6 +14,9 @@ export default { selectedCircle() { return this.$route.params.selectedCircle }, + selectedUserGroup() { + return this.$route.params.selectedUserGroup + }, selectedChart() { return this.$route.params.selectedChart }, diff --git a/src/models/constants.ts b/src/models/constants.ts index 3e0f5d84a..4cb3c937b 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -27,6 +27,7 @@ export const CHART_ALL_CONTACTS: DefaultChart = t('contacts', 'Organization char // Circle route, see vue-router conf export const ROUTE_CIRCLE = 'circle' export const ROUTE_CHART = 'chart' +export const ROUTE_USER_GROUP = 'user_group' // Contact settings export const CONTACTS_SETTINGS: DefaultGroup = t('contacts', 'Contacts settings') diff --git a/src/models/userGroup.ts b/src/models/userGroup.ts new file mode 100644 index 000000000..29d825f63 --- /dev/null +++ b/src/models/userGroup.ts @@ -0,0 +1,73 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { ROUTE_USER_GROUP } from './constants' + +export default class UserGroup { + + private _data: object + private _members: string[] + + constructor(group: object) { + this._data = group + this._members = [] + } + + addMember(member: object) { + if (!this._members.includes(member)) { + this._members.push(member) + } + } + + get id(): string { + return this._data.id + } + + get displayName(): string { + return this._data.displayname + } + + get population(): number { + return this._data.usercount + } + + get canLeave(): boolean { + // users can't leave groups + return false + } + + get isOwner(): boolean { + return false + } + + get isMember(): boolean { + // Only the groups that a user has been added to will be visible to them + return true + } + + get canManageMembers(): boolean { + return false + } + + get canJoin(): boolean { + return false + } + + get members(): string[] { + return this._members + } + + get router(): object { + return { + name: 'user_group', + params: { selectedUserGroup: this.id, selectedGroup: ROUTE_USER_GROUP }, + } + } + + toString(): string { + return this.displayName + } + +} diff --git a/src/router/index.js b/src/router/index.js index d84537a23..0014b0a1d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -6,7 +6,7 @@ import { createRouter, createWebHistory } from 'vue-router' import { generateUrl } from '@nextcloud/router' -import { ROUTE_CIRCLE, ROUTE_CHART } from '../models/constants.ts' +import { ROUTE_CIRCLE, ROUTE_CHART, ROUTE_USER_GROUP } from '../models/constants.ts' import Contacts from '../views/Contacts.vue' // if index.php is in the url AND we got this far, then it's working: @@ -53,6 +53,11 @@ export default createRouter({ name: 'contact', component: Contacts, }, + { + path: `${ROUTE_USER_GROUP}/:selectedUserGroup`, + name: 'user_group', + component: Contacts, + }, ], }, ], diff --git a/src/services/userGroup.ts b/src/services/userGroup.ts new file mode 100644 index 000000000..4d945954d --- /dev/null +++ b/src/services/userGroup.ts @@ -0,0 +1,17 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +export const getUserGroups = async function(userId: string): string[] { + const response = await axios.get(generateOcsUrl('/cloud/users/{userId}/groups/details', { userId })) + return response.data.ocs.data.groups +} + +export const getUserGroupMembers = async function(groupId: string): string[] { + const response = await axios.get(generateOcsUrl('/cloud/groups/{groupId}/users', { groupId })) + return response.data.ocs.data.users +} diff --git a/src/store/userGroup.ts b/src/store/userGroup.ts new file mode 100644 index 000000000..967072c81 --- /dev/null +++ b/src/store/userGroup.ts @@ -0,0 +1,47 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getUserGroups, getUserGroupMembers } from '../services/userGroup.ts' +import logger from '../services/logger.js' +import UserGroup from '../models/userGroup.ts' + +import { defineStore } from 'pinia' + +export default defineStore('userGroup', { + state: () => ({ + userGroups: {}, + }), + + getters: { + userGroupList: (state) => Object.values(state.userGroups), + getUserGroup: (state) => (groupId: string) => state.userGroups[groupId], + }, + + actions: { + async getUserGroups(userId: string): object[] { + const userGroups = await getUserGroups(userId) + + userGroups.forEach(group => { + try { + const newUserGroup = new UserGroup(group) + this.userGroups[newUserGroup.id] = newUserGroup + } catch (error) { + logger.error('Failed to add group', { group, error }) + } + }) + + return userGroups + }, + async getUserGroupMembers(groupId: string): string[] { + const members = await getUserGroupMembers(groupId) + + members.forEach(member => { + this.getUserGroup(groupId).addMember(member) + }) + + return members + }, + }, +}) diff --git a/src/views/Contacts.vue b/src/views/Contacts.vue index ee76ac609..a4bb7090a 100644 --- a/src/views/Contacts.vue +++ b/src/views/Contacts.vue @@ -27,7 +27,9 @@ - + @@ -50,7 +52,7 @@