Skip to content

Commit dc44271

Browse files
Merge pull request #4658 from nextcloud/teams_groups_readonly
feat: show a user's groups as teams
2 parents f93e4ec + 16b6be5 commit dc44271

12 files changed

Lines changed: 461 additions & 12 deletions

File tree

src/components/AppContent/CircleContent.vue

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
<template>
77
<AppContent>
8-
<EmptyContent v-if="!circle" :name="t('contacts', 'Please select a team')">
8+
<EmptyContent v-if="!circle && !userGroup" :name="t('contacts', 'Please select a team')">
99
<template #icon>
1010
<AccountGroup :size="20" />
1111
</template>
@@ -17,6 +17,7 @@
1717
</template>
1818
</EmptyContent>
1919

20+
<UserGroupDetails v-else-if="userGroup" :user-group="userGroup" />
2021
<CircleDetails v-else :circle="circle" />
2122
</AppContent>
2223
</template>
@@ -31,6 +32,9 @@ import AccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
3132
import CircleDetails from '../CircleDetails.vue'
3233
import RouterMixin from '../../mixins/RouterMixin.js'
3334
import IsMobileMixin from '../../mixins/IsMobileMixin.ts'
35+
import UserGroupDetails from '../UserGroupDetails.vue'
36+
import useUserGroupStore from '../../store/userGroup.ts'
37+
import { mapStores } from 'pinia'
3438
3539
export default {
3640
name: 'CircleContent',
@@ -41,6 +45,7 @@ export default {
4145
EmptyContent,
4246
AccountGroup,
4347
IconLoading,
48+
UserGroupDetails,
4449
},
4550
4651
mixins: [IsMobileMixin, RouterMixin],
@@ -66,6 +71,9 @@ export default {
6671
circle() {
6772
return this.$store.getters.getCircle(this.selectedCircle)
6873
},
74+
userGroup() {
75+
return this.userGroupStore.getUserGroup(this.selectedUserGroup)
76+
},
6977
members() {
7078
return Object.values(this.circle?.members || [])
7179
},
@@ -78,6 +86,7 @@ export default {
7886
isEmptyCircle() {
7987
return this.members.length === 0
8088
},
89+
...mapStores(useUserGroupStore),
8190
},
8291
8392
watch: {
@@ -86,12 +95,21 @@ export default {
8695
this.fetchCircleMembers(newCircle.id)
8796
}
8897
},
98+
userGroup(newUserGroup) {
99+
if (newUserGroup?.id) {
100+
this.fetchUserGroupMembers(newUserGroup.id)
101+
}
102+
},
89103
},
90104
91105
beforeMount() {
92106
if (this.circle?.id) {
93107
this.fetchCircleMembers(this.circle.id)
94108
}
109+
110+
if (this.userGroup?.id) {
111+
this.fetchUserGroupMembers(this.userGroup.id)
112+
}
95113
},
96114
97115
methods: {
@@ -108,6 +126,18 @@ export default {
108126
this.loadingList = false
109127
}
110128
},
129+
async fetchUserGroupMembers(userGroupId) {
130+
this.loadingList = true
131+
132+
try {
133+
await this.userGroupStore.getUserGroupMembers(userGroupId)
134+
} catch (error) {
135+
console.error(error)
136+
showError(t('contacts', 'There was an error fetching the member list'))
137+
} finally {
138+
this.loadingList = false
139+
}
140+
},
111141
},
112142
}
113143
</script>

src/components/AppNavigation/CircleNavigationItem.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import AccountGroupOutline from 'vue-material-design-icons/AccountGroupOutline.v
9292
9393
import Circle from '../../models/circle.ts'
9494
import CircleActionsMixin from '../../mixins/CircleActionsMixin.js'
95+
import UserGroup from '../../models/userGroup.ts'
9596
9697
export default {
9798
name: 'CircleNavigationItem',
@@ -116,7 +117,7 @@ export default {
116117
117118
props: {
118119
circle: {
119-
type: Circle,
120+
type: [Circle, UserGroup],
120121
required: true,
121122
},
122123
},

src/components/AppNavigation/RootNavigation.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169

170170
<script>
171171
import { GROUP_ALL_CONTACTS, CHART_ALL_CONTACTS, GROUP_NO_GROUP_CONTACTS, GROUP_RECENTLY_CONTACTED, ELLIPSIS_COUNT, CIRCLE_DESC, CONTACTS_SETTINGS } from '../../models/constants.ts'
172+
import useUserGroupStore from '../../store/userGroup.ts'
172173
173174
import {
174175
NcActionInput as ActionInput,
@@ -182,6 +183,7 @@ import {
182183
} from '@nextcloud/vue'
183184
184185
import naturalCompare from 'string-natural-compare'
186+
import { mapStores } from 'pinia'
185187
186188
import CircleNavigationItem from './CircleNavigationItem.vue'
187189
import Cog from 'vue-material-design-icons/CogOutline.vue'
@@ -288,6 +290,9 @@ export default {
288290
sortedContacts() {
289291
return this.$store.getters.getSortedContacts
290292
},
293+
userGroups() {
294+
return this.userGroupStore.userGroupList
295+
},
291296
292297
// list all the contacts that doesn't have a group
293298
ungroupedContacts() {
@@ -332,7 +337,8 @@ export default {
332337
333338
// generate circles menu from the circles store
334339
circlesMenu() {
335-
const menu = this.circles || []
340+
const menu = [...(this.circles || []), ...(this.userGroups || [])]
341+
336342
menu.sort((a, b) => {
337343
// If user is member of a and b, sort by level
338344
if (a?.initiator?.level !== b?.initiator?.level && a?.initiator?.level && b?.initiator?.level) {
@@ -378,6 +384,7 @@ export default {
378384
? t('contacts', 'Show all teams')
379385
: t('contacts', 'Collapse teams')
380386
},
387+
...mapStores(useUserGroupStore),
381388
},
382389
383390
methods: {
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<div class="usergroup-details-container">
8+
<div class="usergroup-details-grid">
9+
<div class="usergroup-details__header-wrapper">
10+
<div class="usergroup-details-grid__avatar">
11+
<NcAvatar :disable-tooltip="true"
12+
:display-name="userGroup.displayName"
13+
:is-no-user="true"
14+
:size="75" />
15+
</div>
16+
<div class="usergroup-details__header">
17+
<div class="usergroup-name-wrapper">
18+
<h2 class="usergroup-name">
19+
<span :title="userGroup.displayName">{{ userGroup.displayName }}</span>
20+
</h2>
21+
<div class="usergroup-details__subtitle">
22+
{{ t('contacts', 'This is a read-only group managed by administrators. Group members can only view this group.') }}
23+
</div>
24+
</div>
25+
<div class="actions">
26+
<NcButton variant="secondary" @click.stop.prevent="copyToClipboard(userGroupUrl)">
27+
<template #icon>
28+
<CopyIcon :size="20" />
29+
</template>
30+
{{ t('contacts', 'Copy link') }}
31+
</NcButton>
32+
</div>
33+
</div>
34+
</div>
35+
<div class="usergroup-details__main-content">
36+
<h3 class="usergroup-details__content-heading">
37+
{{ t('contacts', 'Members') }}
38+
</h3>
39+
<div class="usergroup-details__member-grid">
40+
<UserGroupMember v-for="member in userGroup.members"
41+
:key="`user-group-member-${member}`"
42+
:member="member" />
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</template>
48+
49+
<script>
50+
import { NcAvatar, NcButton } from '@nextcloud/vue'
51+
import CopyIcon from 'vue-material-design-icons/ContentCopy.vue'
52+
53+
import CopyToClipboardMixin from '../mixins/CopyToClipboardMixin.js'
54+
import UserGroup from '../models/userGroup.ts'
55+
import UserGroupMember from './UserGroupDetails/UserGroupMember.vue'
56+
57+
export default {
58+
name: 'UserGroupDetails',
59+
60+
components: {
61+
CopyIcon,
62+
NcAvatar,
63+
NcButton,
64+
UserGroupMember,
65+
},
66+
67+
mixins: [CopyToClipboardMixin],
68+
69+
props: {
70+
userGroup: {
71+
type: UserGroup,
72+
required: true,
73+
},
74+
},
75+
76+
computed: {
77+
userGroupUrl() {
78+
return window.location.href
79+
},
80+
},
81+
}
82+
</script>
83+
84+
<style lang="scss" scoped>
85+
.usergroup-details {
86+
&-container {
87+
padding-inline: 20px;
88+
margin-top: 1rem;
89+
}
90+
91+
&-grid {
92+
display: grid;
93+
grid-template-columns: 1fr;
94+
gap: 36px;
95+
max-width: 800px;
96+
margin-inline: auto;
97+
98+
.usergroup-name-wrapper {
99+
width: 100%;
100+
}
101+
}
102+
103+
&__header-wrapper {
104+
display: grid;
105+
grid-template-columns: auto 1fr;
106+
align-items: center;
107+
gap: 24px;
108+
}
109+
110+
&__header {
111+
background-color: transparent;
112+
display: flex;
113+
flex-direction: column;
114+
align-items: flex-start;
115+
gap: 2px;
116+
width: 100%;
117+
118+
.usergroup-name {
119+
font-size: 1.5rem;
120+
font-weight: bold;
121+
margin: 0px;
122+
margin-bottom: 2px;
123+
}
124+
}
125+
126+
&__subtitle {
127+
color: var(--color-text-maxcontrast);
128+
margin-bottom: 8px;
129+
}
130+
131+
&__main-content {
132+
margin-inline-start: 99px;
133+
134+
@media (max-width: 768px) {
135+
margin-inline-start: 0px;
136+
}
137+
}
138+
139+
&__content-heading {
140+
font-weight: bold;
141+
font-size: 1.2rem;
142+
line-height: 0px;
143+
margin-top: 4px;
144+
margin-bottom: 8px;
145+
}
146+
147+
&__member-grid {
148+
display: grid;
149+
grid-template-columns: repeat(2, 1fr);
150+
gap: 8px;
151+
max-width: 500px;
152+
153+
@media (max-width: 768px) {
154+
grid-template-columns: 1fr;
155+
}
156+
157+
@media (min-width: 1200px) {
158+
grid-template-columns: repeat(3, 1fr);
159+
}
160+
}
161+
162+
}
163+
</style>

0 commit comments

Comments
 (0)