Skip to content

Commit 81d6276

Browse files
GVodyanovhamza221
authored andcommitted
fix: bring back the team management UI and gate it
This reverts commit d91b271. The team management UI was removed in preparation for the Teams app, but main serves Nextcloud 33 to 35. Once the app was updated on a 34 instance, teams could no longer be managed there at all. Restore the UI unchanged here; the following commit puts it behind a server version gate so it stays hidden on 35 and newer. Signed-off-by: Grigory Vodyanov <scratchx@gmx.com> Assisted-by: ClaudeCode:claude-opus-5
1 parent d8fa573 commit 81d6276

34 files changed

Lines changed: 5648 additions & 32 deletions

lib/AppInfo/Application.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
class Application extends App implements IBootstrap {
2323
public const APP_ID = 'contacts';
2424

25+
/**
26+
* Last server major version that ships team management inside Contacts.
27+
*
28+
* Starting with Nextcloud 35 teams are managed by the dedicated Teams app,
29+
* so the team management UI of this app is only exposed up to this version.
30+
*/
31+
public const MAX_SERVER_VERSION_WITH_TEAM_MANAGEMENT = 34;
32+
2533
public function __construct() {
2634
parent::__construct(self::APP_ID);
2735
}

lib/Controller/PageController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use OCP\IRequest;
2020
use OCP\IUserSession;
2121
use OCP\L10N\IFactory;
22+
use OCP\ServerVersion;
2223
use OCP\Util;
2324

2425
class PageController extends Controller {
@@ -33,6 +34,7 @@ public function __construct(
3334
private IAppManager $appManager,
3435
private CompareVersion $compareVersion,
3536
private GroupSharingService $groupSharingService,
37+
private ServerVersion $serverVersion,
3638
) {
3739
parent::__construct(Application::APP_ID, $request);
3840
}
@@ -59,6 +61,9 @@ public function index(): TemplateResponse {
5961
$isCirclesEnabled = $this->appManager->isEnabledForUser('circles') === true;
6062
// if circles is not installed, we use 0.0.0
6163
$isCircleVersionCompatible = $this->compareVersion->isCompatible($circleVersion ? $circleVersion : '0.0.0', 22);
64+
// Feature gate: teams are managed by the Teams app from Nextcloud 35 on,
65+
// so we only render our own team management UI on older servers.
66+
$isServerVersionWithTeamManagement = $this->serverVersion->getMajorVersion() <= Application::MAX_SERVER_VERSION_WITH_TEAM_MANAGEMENT;
6267
// Check whether group sharing is enabled or not
6368
$isGroupSharingEnabled = $this->groupSharingService->isGroupSharingAllowed($user);
6469
$talkVersion = $this->appManager->getAppVersion('spreed');
@@ -73,7 +78,7 @@ public function index(): TemplateResponse {
7378
$this->initialState->provideInitialState('allowSocialSync', $syncAllowedByAdmin);
7479
$this->initialState->provideInitialState('enableSocialSync', $bgSyncEnabledByUser);
7580
$this->initialState->provideInitialState('isContactsInteractionEnabled', $isContactsInteractionEnabled);
76-
$this->initialState->provideInitialState('isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible);
81+
$this->initialState->provideInitialState('isTeamManagementEnabled', $isCirclesEnabled && $isCircleVersionCompatible && $isServerVersionWithTeamManagement);
7782
$this->initialState->provideInitialState('isTalkEnabled', $isTalkEnabled && $isTalkVersionCompatible);
7883

7984
Util::addStyle(Application::APP_ID, 'contacts-main');
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<template>
7+
<AppContent :aria-label="t('contacts', 'Teams')">
8+
<EmptyContent v-if="!circle && !userGroup" :name="t('contacts', 'Please select a team')">
9+
<template #icon>
10+
<AccountGroup :size="20" />
11+
</template>
12+
</EmptyContent>
13+
14+
<EmptyContent v-else-if="loading" class="empty-content" :name="t('contacts', 'Loading team…')">
15+
<template #icon>
16+
<IconLoading :size="20" />
17+
</template>
18+
</EmptyContent>
19+
20+
<UserGroupDetails v-else-if="userGroup" :user-group="userGroup" />
21+
<CircleDetails v-else :circle="circle" />
22+
</AppContent>
23+
</template>
24+
25+
<script>
26+
import { showError } from '@nextcloud/dialogs'
27+
import {
28+
NcAppContent as AppContent,
29+
NcEmptyContent as EmptyContent,
30+
NcLoadingIcon as IconLoading,
31+
} from '@nextcloud/vue'
32+
import { mapStores } from 'pinia'
33+
import AccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
34+
import CircleDetails from '../CircleDetails.vue'
35+
import UserGroupDetails from '../UserGroupDetails.vue'
36+
import IsMobileMixin from '../../mixins/IsMobileMixin.ts'
37+
import RouterMixin from '../../mixins/RouterMixin.js'
38+
import logger from '../../services/logger.js'
39+
import useUserGroupStore from '../../store/userGroup.ts'
40+
41+
export default {
42+
name: 'CircleContent',
43+
44+
components: {
45+
AppContent,
46+
CircleDetails,
47+
EmptyContent,
48+
AccountGroup,
49+
IconLoading,
50+
UserGroupDetails,
51+
},
52+
53+
mixins: [IsMobileMixin, RouterMixin],
54+
55+
props: {
56+
loading: {
57+
type: Boolean,
58+
default: true,
59+
},
60+
},
61+
62+
data() {
63+
return {
64+
loadingList: false,
65+
}
66+
},
67+
68+
computed: {
69+
// store variables
70+
circles() {
71+
return this.$store.getters.getCircles
72+
},
73+
74+
circle() {
75+
return this.$store.getters.getCircle(this.selectedCircle)
76+
},
77+
78+
userGroup() {
79+
return this.userGroupStore.getUserGroup(this.selectedUserGroup)
80+
},
81+
82+
members() {
83+
return Object.values(this.circle?.members || [])
84+
},
85+
86+
/**
87+
* Is the current circle empty
88+
*
89+
* @return {boolean}
90+
*/
91+
isEmptyCircle() {
92+
return this.members.length === 0
93+
},
94+
95+
...mapStores(useUserGroupStore),
96+
},
97+
98+
watch: {
99+
circle(newCircle) {
100+
if (newCircle?.id) {
101+
this.fetchCircleMembers(newCircle.id)
102+
}
103+
},
104+
105+
userGroup(newUserGroup) {
106+
if (newUserGroup?.id) {
107+
this.fetchUserGroupMembers(newUserGroup.id)
108+
}
109+
},
110+
},
111+
112+
beforeMount() {
113+
if (this.circle?.id) {
114+
this.fetchCircleMembers(this.circle.id)
115+
}
116+
117+
if (this.userGroup?.id) {
118+
this.fetchUserGroupMembers(this.userGroup.id)
119+
}
120+
},
121+
122+
methods: {
123+
async fetchCircleMembers(circleId) {
124+
this.loadingList = true
125+
this.logger.debug('Fetching members for', { circleId })
126+
127+
try {
128+
await this.$store.dispatch('getCircleMembers', circleId)
129+
} catch (error) {
130+
logger.error(error)
131+
showError(t('contacts', 'There was an error fetching the member list'))
132+
} finally {
133+
this.loadingList = false
134+
}
135+
},
136+
137+
async fetchUserGroupMembers(userGroupId) {
138+
this.loadingList = true
139+
140+
try {
141+
await this.userGroupStore.getUserGroupMembers(userGroupId)
142+
} catch (error) {
143+
logger.error(error)
144+
showError(t('contacts', 'There was an error fetching the member list'))
145+
} finally {
146+
this.loadingList = false
147+
}
148+
},
149+
},
150+
}
151+
</script>
152+
153+
<style lang="scss" scoped>
154+
// TODO: replace my button component when available
155+
button {
156+
height: 44px;
157+
display: flex;
158+
justify-content: center;
159+
align-items: center;
160+
span {
161+
margin-inline-end: 10px;
162+
}
163+
}
164+
165+
.empty-content {
166+
height: 100%;
167+
}
168+
</style>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<AppNavigationItem
7+
:key="circle.key"
8+
:name="circle.displayName"
9+
:to="circle.router"
10+
:force-menu="true">
11+
<!-- force-menu avoids a Tab-trap in NcAppNavigationItem/NcActions when only
12+
one action (copy link) is available: their Tab handler focuses a menu
13+
trigger button that isn't rendered in single-inline-action mode. -->
14+
<template #icon>
15+
<AccountStar v-if="circle.isOwner" :size="20" />
16+
<AccountGroupOutline v-else :size="20" />
17+
</template>
18+
<template #actions>
19+
<ActionText v-if="loadingAction">
20+
<template #icon>
21+
<IconLoading :size="20" />
22+
</template>
23+
{{ t('contacts', 'Loading …') }}
24+
</ActionText>
25+
<template v-else>
26+
<ActionButton
27+
v-if="circle.canManageMembers"
28+
:close-after-click="true"
29+
@click="addMemberToCircle">
30+
<template #icon>
31+
<IconCog :size="20" />
32+
</template>
33+
{{ t('contacts', 'Manage team') }}
34+
</ActionButton>
35+
36+
<!-- copy circle link -->
37+
<ActionLink
38+
:href="circleUrl"
39+
:icon="copyLinkIcon"
40+
@click.stop.prevent="copyToClipboard(circleUrl)">
41+
{{ copyButtonText }}
42+
</ActionLink>
43+
44+
<!-- leave circle -->
45+
<ActionButton
46+
v-if="circle.canLeave"
47+
@click="confirmLeaveCircle">
48+
{{ t('contacts', 'Leave team') }}
49+
<template #icon>
50+
<ExitToApp :size="16" />
51+
</template>
52+
</ActionButton>
53+
54+
<!-- join circle -->
55+
<ActionButton
56+
v-else-if="!circle.isMember && circle.canJoin"
57+
:disabled="loadingJoin"
58+
@click="joinCircle">
59+
{{ joinButtonTitle }}
60+
<template #icon>
61+
<LocationEnter :size="16" />
62+
</template>
63+
</ActionButton>
64+
65+
<!-- delete circle -->
66+
<ActionButton
67+
v-if="circle.canDelete"
68+
@click="confirmDeleteCircle">
69+
<template #icon>
70+
<IconDelete :size="20" />
71+
</template>
72+
{{ t('contacts', 'Delete team') }}
73+
</ActionButton>
74+
</template>
75+
</template>
76+
77+
<template #counter>
78+
<NcCounterBubble
79+
v-if="memberCount > 0"
80+
:count="memberCount" />
81+
</template>
82+
</AppNavigationItem>
83+
</template>
84+
85+
<script>
86+
import {
87+
NcActionButton as ActionButton,
88+
NcActionLink as ActionLink,
89+
NcActionText as ActionText,
90+
NcAppNavigationItem as AppNavigationItem,
91+
NcLoadingIcon as IconLoading,
92+
NcCounterBubble,
93+
} from '@nextcloud/vue'
94+
import AccountGroupOutline from 'vue-material-design-icons/AccountGroupOutline.vue'
95+
import AccountStar from 'vue-material-design-icons/AccountStarOutline.vue'
96+
import IconCog from 'vue-material-design-icons/CogOutline.vue'
97+
import ExitToApp from 'vue-material-design-icons/ExitToApp.vue'
98+
import LocationEnter from 'vue-material-design-icons/LocationEnter.vue'
99+
import IconDelete from 'vue-material-design-icons/TrashCanOutline.vue'
100+
import CircleActionsMixin from '../../mixins/CircleActionsMixin.js'
101+
import Circle from '../../models/circle.ts'
102+
import UserGroup from '../../models/userGroup.ts'
103+
104+
export default {
105+
name: 'CircleNavigationItem',
106+
107+
components: {
108+
ActionButton,
109+
ActionLink,
110+
ActionText,
111+
NcCounterBubble,
112+
AppNavigationItem,
113+
ExitToApp,
114+
IconCog,
115+
IconDelete,
116+
LocationEnter,
117+
AccountStar,
118+
AccountGroupOutline,
119+
IconLoading,
120+
},
121+
122+
mixins: [CircleActionsMixin],
123+
124+
props: {
125+
circle: {
126+
type: [Circle, UserGroup],
127+
required: true,
128+
},
129+
},
130+
131+
computed: {
132+
memberCount() {
133+
return this.circle.populationInherited || 0
134+
},
135+
},
136+
}
137+
</script>

0 commit comments

Comments
 (0)