mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
Merge pull request #51336 from nextcloud/perf/paginate-filter-groups
fix(settings): Fix infinitely loading account management page with pagination of groups
This commit is contained in:
commit
4a9bd9bb6d
28 changed files with 1196 additions and 271 deletions
|
|
@ -42,9 +42,11 @@ return [
|
|||
['root' => '/cloud', 'name' => 'Users#enableUser', 'url' => '/users/{userId}/enable', 'verb' => 'PUT'],
|
||||
['root' => '/cloud', 'name' => 'Users#disableUser', 'url' => '/users/{userId}/disable', 'verb' => 'PUT'],
|
||||
['root' => '/cloud', 'name' => 'Users#getUsersGroups', 'url' => '/users/{userId}/groups', 'verb' => 'GET'],
|
||||
['root' => '/cloud', 'name' => 'Users#getUsersGroupsDetails', 'url' => '/users/{userId}/groups/details', 'verb' => 'GET'],
|
||||
['root' => '/cloud', 'name' => 'Users#addToGroup', 'url' => '/users/{userId}/groups', 'verb' => 'POST'],
|
||||
['root' => '/cloud', 'name' => 'Users#removeFromGroup', 'url' => '/users/{userId}/groups', 'verb' => 'DELETE'],
|
||||
['root' => '/cloud', 'name' => 'Users#getUserSubAdminGroups', 'url' => '/users/{userId}/subadmins', 'verb' => 'GET'],
|
||||
['root' => '/cloud', 'name' => 'Users#getUserSubAdminGroupsDetails', 'url' => '/users/{userId}/subadmins/details', 'verb' => 'GET'],
|
||||
['root' => '/cloud', 'name' => 'Users#addSubAdmin', 'url' => '/users/{userId}/subadmins', 'verb' => 'POST'],
|
||||
['root' => '/cloud', 'name' => 'Users#removeSubAdmin', 'url' => '/users/{userId}/subadmins', 'verb' => 'DELETE'],
|
||||
['root' => '/cloud', 'name' => 'Users#resendWelcomeMessage', 'url' => '/users/{userId}/welcome', 'verb' => 'POST'],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace OCA\Provisioning_API\Controller;
|
|||
|
||||
use InvalidArgumentException;
|
||||
use OC\Authentication\Token\RemoteWipe;
|
||||
use OC\Group\Group;
|
||||
use OC\KnownUser\KnownUserService;
|
||||
use OC\User\Backend;
|
||||
use OCA\Provisioning_API\ResponseDefinitions;
|
||||
|
|
@ -52,6 +53,7 @@ use OCP\Util;
|
|||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* @psalm-import-type Provisioning_APIGroupDetails from ResponseDefinitions
|
||||
* @psalm-import-type Provisioning_APIUserDetails from ResponseDefinitions
|
||||
*/
|
||||
class UsersController extends AUserDataOCSController {
|
||||
|
|
@ -1402,6 +1404,127 @@ class UsersController extends AUserDataOCSController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoSubAdminRequired
|
||||
*
|
||||
* Get a list of groups with details
|
||||
*
|
||||
* @param string $userId ID of the user
|
||||
* @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
|
||||
* @throws OCSException
|
||||
*
|
||||
* 200: Users groups returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUsersGroupsDetails(string $userId): DataResponse {
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
if ($targetUser === null) {
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
|
||||
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
|
||||
if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
|
||||
// Self lookup or admin lookup
|
||||
$groups = array_map(
|
||||
function (Group $group) {
|
||||
return [
|
||||
'id' => $group->getGID(),
|
||||
'displayname' => $group->getDisplayName(),
|
||||
'usercount' => $group->count(),
|
||||
'disabled' => $group->countDisabled(),
|
||||
'canAdd' => $group->canAddUser(),
|
||||
'canRemove' => $group->canRemoveUser(),
|
||||
];
|
||||
},
|
||||
array_values($this->groupManager->getUserGroups($targetUser)),
|
||||
);
|
||||
return new DataResponse([
|
||||
'groups' => $groups,
|
||||
]);
|
||||
} else {
|
||||
$subAdminManager = $this->groupManager->getSubAdmin();
|
||||
|
||||
// Looking up someone else
|
||||
if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
|
||||
// Return the group that the method caller is subadmin of for the user in question
|
||||
$gids = array_values(array_intersect(
|
||||
array_map(
|
||||
static fn (IGroup $group) => $group->getGID(),
|
||||
$subAdminManager->getSubAdminsGroups($loggedInUser),
|
||||
),
|
||||
$this->groupManager->getUserGroupIds($targetUser)
|
||||
));
|
||||
$groups = array_map(
|
||||
function (string $gid) {
|
||||
$group = $this->groupManager->get($gid);
|
||||
return [
|
||||
'id' => $group->getGID(),
|
||||
'displayname' => $group->getDisplayName(),
|
||||
'usercount' => $group->count(),
|
||||
'disabled' => $group->countDisabled(),
|
||||
'canAdd' => $group->canAddUser(),
|
||||
'canRemove' => $group->canRemoveUser(),
|
||||
];
|
||||
},
|
||||
$gids,
|
||||
);
|
||||
return new DataResponse([
|
||||
'groups' => $groups,
|
||||
]);
|
||||
} else {
|
||||
// Not permitted
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoSubAdminRequired
|
||||
*
|
||||
* Get a list of the groups the user is a subadmin of, with details
|
||||
*
|
||||
* @param string $userId ID of the user
|
||||
* @return DataResponse<Http::STATUS_OK, array{groups: list<Provisioning_APIGroupDetails>}, array{}>
|
||||
* @throws OCSException
|
||||
*
|
||||
* 200: Users subadmin groups returned
|
||||
*/
|
||||
#[NoAdminRequired]
|
||||
public function getUserSubAdminGroupsDetails(string $userId): DataResponse {
|
||||
$loggedInUser = $this->userSession->getUser();
|
||||
|
||||
$targetUser = $this->userManager->get($userId);
|
||||
if ($targetUser === null) {
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
}
|
||||
|
||||
$isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
|
||||
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
|
||||
if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
|
||||
$subAdminManager = $this->groupManager->getSubAdmin();
|
||||
$groups = array_map(
|
||||
function (IGroup $group) {
|
||||
return [
|
||||
'id' => $group->getGID(),
|
||||
'displayname' => $group->getDisplayName(),
|
||||
'usercount' => $group->count(),
|
||||
'disabled' => $group->countDisabled(),
|
||||
'canAdd' => $group->canAddUser(),
|
||||
'canRemove' => $group->canRemoveUser(),
|
||||
];
|
||||
},
|
||||
array_values($subAdminManager->getSubAdminsGroups($targetUser)),
|
||||
);
|
||||
return new DataResponse([
|
||||
'groups' => $groups,
|
||||
]);
|
||||
}
|
||||
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a group
|
||||
*
|
||||
|
|
|
|||
|
|
@ -4115,6 +4115,168 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/groups/details": {
|
||||
"get": {
|
||||
"operationId": "users-get-users-groups-details",
|
||||
"summary": "Get a list of groups with details",
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/subadmins/details": {
|
||||
"get": {
|
||||
"operationId": "users-get-user-sub-admin-groups-details",
|
||||
"summary": "Get a list of the groups the user is a subadmin of, with details",
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users subadmin groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/welcome": {
|
||||
"post": {
|
||||
"operationId": "users-resend-welcome-message",
|
||||
|
|
|
|||
|
|
@ -2547,6 +2547,168 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/groups/details": {
|
||||
"get": {
|
||||
"operationId": "users-get-users-groups-details",
|
||||
"summary": "Get a list of groups with details",
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/subadmins/details": {
|
||||
"get": {
|
||||
"operationId": "users-get-user-sub-admin-groups-details",
|
||||
"summary": "Get a list of the groups the user is a subadmin of, with details",
|
||||
"tags": [
|
||||
"users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users subadmin groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/welcome": {
|
||||
"post": {
|
||||
"operationId": "users-resend-welcome-message",
|
||||
|
|
|
|||
|
|
@ -134,8 +134,15 @@ class UsersController extends Controller {
|
|||
$this->userSession
|
||||
);
|
||||
|
||||
$groupsInfo->setSorting($sortGroupsBy);
|
||||
[$adminGroup, $groups] = $groupsInfo->get();
|
||||
$adminGroup = $this->groupManager->get('admin');
|
||||
$adminGroupData = [
|
||||
'id' => $adminGroup->getGID(),
|
||||
'name' => $adminGroup->getDisplayName(),
|
||||
'usercount' => $sortGroupsBy === MetaData::SORT_USERCOUNT ? $adminGroup->count() : 0,
|
||||
'disabled' => $adminGroup->countDisabled(),
|
||||
'canAdd' => $adminGroup->canAddUser(),
|
||||
'canRemove' => $adminGroup->canRemoveUser(),
|
||||
];
|
||||
|
||||
if (!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
|
||||
$isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
|
||||
|
|
@ -196,7 +203,7 @@ class UsersController extends Controller {
|
|||
/* FINAL DATA */
|
||||
$serverData = [];
|
||||
// groups
|
||||
$serverData['groups'] = array_merge_recursive($adminGroup, [$recentUsersGroup, $disabledUsersGroup], $groups);
|
||||
$serverData['systemGroups'] = [$adminGroupData, $recentUsersGroup, $disabledUsersGroup];
|
||||
// Various data
|
||||
$serverData['isAdmin'] = $isAdmin;
|
||||
$serverData['isDelegatedAdmin'] = $isDelegatedAdmin;
|
||||
|
|
|
|||
200
apps/settings/src/components/AppNavigationGroupList.vue
Normal file
200
apps/settings/src/components/AppNavigationGroupList.vue
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Fragment>
|
||||
<NcAppNavigationCaption :name="t('settings', 'Groups')"
|
||||
:disabled="loadingAddGroup"
|
||||
:aria-label="loadingAddGroup ? t('settings', 'Creating group…') : t('settings', 'Create group')"
|
||||
force-menu
|
||||
is-heading
|
||||
:open.sync="isAddGroupOpen">
|
||||
<template v-if="isAdminOrDelegatedAdmin" #actionsTriggerIcon>
|
||||
<NcLoadingIcon v-if="loadingAddGroup" />
|
||||
<NcIconSvgWrapper v-else :path="mdiPlus" />
|
||||
</template>
|
||||
<template v-if="isAdminOrDelegatedAdmin" #actions>
|
||||
<NcActionText>
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiAccountGroup" />
|
||||
</template>
|
||||
{{ t('settings', 'Create group') }}
|
||||
</NcActionText>
|
||||
<NcActionInput :label="t('settings', 'Group name')"
|
||||
data-cy-users-settings-new-group-name
|
||||
:label-outside="false"
|
||||
:disabled="loadingAddGroup"
|
||||
:value.sync="newGroupName"
|
||||
:error="hasAddGroupError"
|
||||
:helper-text="hasAddGroupError ? t('settings', 'Please enter a valid group name') : ''"
|
||||
@submit="createGroup" />
|
||||
</template>
|
||||
</NcAppNavigationCaption>
|
||||
|
||||
<NcAppNavigationSearch v-model="groupsSearchQuery"
|
||||
:label="t('settings', 'Search groups…')" />
|
||||
|
||||
<p id="group-list-desc" class="hidden-visually">
|
||||
{{ t('settings', 'List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list.') }}
|
||||
</p>
|
||||
<NcAppNavigationList class="account-management__group-list"
|
||||
aria-describedby="group-list-desc"
|
||||
data-cy-users-settings-navigation-groups="custom">
|
||||
<GroupListItem v-for="group in userGroups"
|
||||
:id="group.id"
|
||||
ref="groupListItems"
|
||||
:key="group.id"
|
||||
:active="selectedGroupDecoded === group.id"
|
||||
:name="group.title"
|
||||
:count="group.count" />
|
||||
<div v-if="loadingGroups" role="note">
|
||||
<NcLoadingIcon :name="t('settings', 'Loading groups…')" />
|
||||
</div>
|
||||
</NcAppNavigationList>
|
||||
</Fragment>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, onBeforeMount } from 'vue'
|
||||
import { Fragment } from 'vue-frag'
|
||||
import { useRoute, useRouter } from 'vue-router/composables'
|
||||
import { useElementVisibility } from '@vueuse/core'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { mdiAccountGroup, mdiPlus } from '@mdi/js'
|
||||
|
||||
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
|
||||
import NcActionText from '@nextcloud/vue/components/NcActionText'
|
||||
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
|
||||
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
|
||||
import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
|
||||
import GroupListItem from './GroupListItem.vue'
|
||||
|
||||
import { useFormatGroups } from '../composables/useGroupsNavigation.ts'
|
||||
import { useStore } from '../store'
|
||||
import { searchGroups } from '../service/groups.ts'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
const store = useStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadGroups()
|
||||
})
|
||||
|
||||
/** Current active group in the view - this is URL encoded */
|
||||
const selectedGroup = computed(() => route.params?.selectedGroup)
|
||||
/** Current active group - URL decoded */
|
||||
const selectedGroupDecoded = computed(() => selectedGroup.value ? decodeURIComponent(selectedGroup.value) : null)
|
||||
/** All available groups */
|
||||
const groups = computed(() => store.getters.getSortedGroups)
|
||||
/** User groups */
|
||||
const { userGroups } = useFormatGroups(groups)
|
||||
/** Server settings for current user */
|
||||
const settings = computed(() => store.getters.getServerData)
|
||||
/** True if the current user is a (delegated) admin */
|
||||
const isAdminOrDelegatedAdmin = computed(() => settings.value.isAdmin || settings.value.isDelegatedAdmin)
|
||||
|
||||
/** True if the 'add-group' dialog is open - needed to be able to close it when the group is created */
|
||||
const isAddGroupOpen = ref(false)
|
||||
/** True if the group creation is in progress to show loading spinner and disable adding another one */
|
||||
const loadingAddGroup = ref(false)
|
||||
/** Error state for creating a new group */
|
||||
const hasAddGroupError = ref(false)
|
||||
/** Name of the group to create (used in the group creation dialog) */
|
||||
const newGroupName = ref('')
|
||||
|
||||
/** True if groups are loading */
|
||||
const loadingGroups = ref(false)
|
||||
/** Search offset */
|
||||
const offset = ref(0)
|
||||
/** Search query for groups */
|
||||
const groupsSearchQuery = ref('')
|
||||
|
||||
const groupListItems = ref([])
|
||||
const lastGroupListItem = computed(() => {
|
||||
return groupListItems.value
|
||||
.findLast(component => component?.$vnode?.key === userGroups.value?.at(-1)?.id) // Order of refs is not guaranteed to match source array order
|
||||
?.$refs?.listItem?.$el
|
||||
})
|
||||
const isLastGroupVisible = useElementVisibility(lastGroupListItem)
|
||||
watch(isLastGroupVisible, async () => {
|
||||
if (!isLastGroupVisible.value) {
|
||||
return
|
||||
}
|
||||
await loadGroups()
|
||||
})
|
||||
|
||||
watch(groupsSearchQuery, async () => {
|
||||
store.commit('resetGroups')
|
||||
offset.value = 0
|
||||
await loadGroups()
|
||||
})
|
||||
|
||||
/** Cancelable promise for search groups request */
|
||||
const promise = ref(null)
|
||||
|
||||
/**
|
||||
* Load groups
|
||||
*/
|
||||
async function loadGroups() {
|
||||
if (promise.value) {
|
||||
promise.value.cancel()
|
||||
}
|
||||
loadingGroups.value = true
|
||||
try {
|
||||
promise.value = searchGroups({
|
||||
search: groupsSearchQuery.value,
|
||||
offset: offset.value,
|
||||
limit: 25,
|
||||
})
|
||||
const groups = await promise.value
|
||||
if (groups.length > 0) {
|
||||
offset.value += 25
|
||||
}
|
||||
for (const group of groups) {
|
||||
store.commit('addGroup', group)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(t('settings', 'Failed to load groups'), { error })
|
||||
}
|
||||
promise.value = null
|
||||
loadingGroups.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*/
|
||||
async function createGroup() {
|
||||
hasAddGroupError.value = false
|
||||
const groupId = newGroupName.value.trim()
|
||||
if (groupId === '') {
|
||||
hasAddGroupError.value = true
|
||||
return
|
||||
}
|
||||
|
||||
isAddGroupOpen.value = false
|
||||
loadingAddGroup.value = true
|
||||
|
||||
try {
|
||||
await store.dispatch('addGroup', groupId)
|
||||
await router.push({
|
||||
name: 'group',
|
||||
params: {
|
||||
selectedGroup: encodeURIComponent(groupId),
|
||||
},
|
||||
})
|
||||
const newGroupListItem = groupListItems.value.findLast(component => component?.$vnode?.key === groupId)
|
||||
newGroupListItem?.$refs?.listItem?.$el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
newGroupName.value = ''
|
||||
} catch {
|
||||
showError(t('settings', 'Failed to create group'))
|
||||
}
|
||||
loadingAddGroup.value = false
|
||||
}
|
||||
</script>
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
</NcModal>
|
||||
|
||||
<NcAppNavigationItem :key="id"
|
||||
ref="listItem"
|
||||
:exact="true"
|
||||
:name="name"
|
||||
:to="{ name: 'group', params: { selectedGroup: encodeURIComponent(id) } }"
|
||||
|
|
|
|||
|
|
@ -34,8 +34,6 @@
|
|||
users,
|
||||
settings,
|
||||
hasObfuscated,
|
||||
groups,
|
||||
subAdminsGroups,
|
||||
quotaOptions,
|
||||
languages,
|
||||
externalActions,
|
||||
|
|
@ -173,15 +171,8 @@ export default {
|
|||
},
|
||||
|
||||
groups() {
|
||||
// data provided php side + remove the recent and disabled groups
|
||||
return this.$store.getters.getGroups
|
||||
return this.$store.getters.getSortedGroups
|
||||
.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
},
|
||||
|
||||
subAdminsGroups() {
|
||||
// data provided php side
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
|
||||
quotaOptions() {
|
||||
|
|
|
|||
|
|
@ -64,29 +64,32 @@
|
|||
:input-label="!settings.isAdmin && !settings.isDelegatedAdmin ? t('settings', 'Member of the following groups (required)') : t('settings', 'Member of the following groups')"
|
||||
:placeholder="t('settings', 'Set account groups')"
|
||||
:disabled="loading.groups || loading.all"
|
||||
:options="canAddGroups"
|
||||
:options="availableGroups"
|
||||
:value="newUser.groups"
|
||||
label="name"
|
||||
:close-on-select="false"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:required="!settings.isAdmin && !settings.isDelegatedAdmin"
|
||||
@input="handleGroupInput"
|
||||
@option:created="createGroup" />
|
||||
:create-option="(value) => ({ id: value, name: value, isCreating: true })"
|
||||
@search="searchGroups"
|
||||
@option:created="createGroup"
|
||||
@option:selected="options => addGroup(options.at(-1))" />
|
||||
<!-- If user is not admin, they are a subadmin.
|
||||
Subadmins can't create users outside their groups
|
||||
Therefore, empty select is forbidden -->
|
||||
</div>
|
||||
<div v-if="subAdminsGroups.length > 0"
|
||||
class="dialog__item">
|
||||
<div class="dialog__item">
|
||||
<NcSelect v-model="newUser.subAdminsGroups"
|
||||
class="dialog__select"
|
||||
:input-label="t('settings', 'Admin of the following groups')"
|
||||
:placeholder="t('settings', 'Set account as admin for …')"
|
||||
:disabled="loading.groups || loading.all"
|
||||
:options="subAdminsGroups"
|
||||
:close-on-select="false"
|
||||
:multiple="true"
|
||||
label="name" />
|
||||
label="name"
|
||||
@search="searchGroups" />
|
||||
</div>
|
||||
<div class="dialog__item">
|
||||
<NcSelect v-model="newUser.quota"
|
||||
|
|
@ -142,6 +145,9 @@ import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
|||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
|
||||
import { searchGroups } from '../../service/groups.ts'
|
||||
import logger from '../../logger.ts'
|
||||
|
||||
export default {
|
||||
name: 'NewUserDialog',
|
||||
|
||||
|
|
@ -172,11 +178,14 @@ export default {
|
|||
|
||||
data() {
|
||||
return {
|
||||
availableGroups: this.$store.getters.getSortedGroups.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled'),
|
||||
possibleManagers: [],
|
||||
// TRANSLATORS This string describes a manager in the context of an organization
|
||||
managerInputLabel: t('settings', 'Manager'),
|
||||
// TRANSLATORS This string describes a manager in the context of an organization
|
||||
managerLabel: t('settings', 'Set line manager'),
|
||||
// Cancelable promise for search groups request
|
||||
promise: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -200,27 +209,9 @@ export default {
|
|||
return this.$store.getters.getPasswordPolicyMinLength
|
||||
},
|
||||
|
||||
groups() {
|
||||
// data provided php side + remove the recent and disabled groups
|
||||
return this.$store.getters.getGroups
|
||||
.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
},
|
||||
|
||||
subAdminsGroups() {
|
||||
// data provided php side
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
|
||||
canAddGroups() {
|
||||
// disabled if no permission to add new users to group
|
||||
return this.groups.map(group => {
|
||||
// clone object because we don't want
|
||||
// to edit the original groups
|
||||
group = Object.assign({}, group)
|
||||
group.$isDisabled = group.canAdd === false
|
||||
return group
|
||||
})
|
||||
return this.availableGroups.filter(group => group.id !== 'admin' && group.id !== '__nc_internal_recent' && group.id !== 'disabled')
|
||||
},
|
||||
|
||||
languages() {
|
||||
|
|
@ -281,13 +272,24 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
handleGroupInput(groups) {
|
||||
/**
|
||||
* Filter out groups with no id to prevent duplicate selected options
|
||||
*
|
||||
* Created groups are added programmatically by `createGroup()`
|
||||
*/
|
||||
this.newUser.groups = groups.filter(group => Boolean(group.id))
|
||||
async searchGroups(query, toggleLoading) {
|
||||
if (this.promise) {
|
||||
this.promise.cancel()
|
||||
}
|
||||
toggleLoading(true)
|
||||
try {
|
||||
this.promise = searchGroups({
|
||||
search: query,
|
||||
offset: 0,
|
||||
limit: 25,
|
||||
})
|
||||
const groups = await this.promise
|
||||
this.availableGroups = groups
|
||||
} catch (error) {
|
||||
logger.error(t('settings', 'Failed to search groups'), { error })
|
||||
}
|
||||
this.promise = null
|
||||
toggleLoading(false)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -300,11 +302,27 @@ export default {
|
|||
this.loading.groups = true
|
||||
try {
|
||||
await this.$store.dispatch('addGroup', gid)
|
||||
this.newUser.groups.push(this.groups.find(group => group.id === gid))
|
||||
this.loading.groups = false
|
||||
this.availableGroups.push({ id: gid, name: gid })
|
||||
this.newUser.groups.push({ id: gid, name: gid })
|
||||
} catch (error) {
|
||||
this.loading.groups = false
|
||||
logger.error(t('settings', 'Failed to create group'), { error })
|
||||
}
|
||||
this.loading.groups = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
*/
|
||||
async addGroup(group) {
|
||||
if (group.isCreating) {
|
||||
return
|
||||
}
|
||||
if (group.canAdd === false) {
|
||||
return
|
||||
}
|
||||
this.newUser.groups.push(group)
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
scope="col">
|
||||
<span>{{ t('settings', 'Groups') }}</span>
|
||||
</th>
|
||||
<th v-if="subAdminsGroups.length > 0 && (settings.isAdmin || settings.isDelegatedAdmin)"
|
||||
<th v-if="settings.isAdmin || settings.isDelegatedAdmin"
|
||||
class="header__cell header__cell--large"
|
||||
data-cy-user-list-header-subadmins
|
||||
scope="col">
|
||||
|
|
@ -125,11 +125,6 @@ export default Vue.extend({
|
|||
return this.$store.getters.getServerData
|
||||
},
|
||||
|
||||
subAdminsGroups() {
|
||||
// @ts-expect-error: allow untyped $store
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
|
||||
passwordLabel(): string {
|
||||
if (this.hasObfuscated) {
|
||||
// TRANSLATORS This string is for a column header labelling either a password or a message that the current user has insufficient permissions
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
:data-loading="loading.groups || undefined"
|
||||
:input-id="'groups' + uniqueId"
|
||||
:close-on-select="false"
|
||||
:disabled="isLoadingField"
|
||||
:disabled="isLoadingField || loading.groupsDetails"
|
||||
:loading="loading.groups"
|
||||
:multiple="true"
|
||||
:append-to-body="false"
|
||||
|
|
@ -116,7 +116,8 @@
|
|||
:value="userGroups"
|
||||
label="name"
|
||||
:no-wrap="true"
|
||||
:create-option="(value) => ({ name: value, isCreating: true })"
|
||||
:create-option="(value) => ({ id: value, name: value, isCreating: true })"
|
||||
@search="searchGroups"
|
||||
@option:created="createGroup"
|
||||
@option:selected="options => addUserGroup(options.at(-1))"
|
||||
@option:deselected="removeUserGroup" />
|
||||
|
|
@ -127,10 +128,10 @@
|
|||
</span>
|
||||
</td>
|
||||
|
||||
<td v-if="subAdminsGroups.length > 0 && (settings.isAdmin || settings.isDelegatedAdmin)"
|
||||
<td v-if="settings.isAdmin || settings.isDelegatedAdmin"
|
||||
data-cy-user-list-cell-subadmins
|
||||
class="row__cell row__cell--large row__cell--multiline">
|
||||
<template v-if="editing && (settings.isAdmin || settings.isDelegatedAdmin) && subAdminsGroups.length > 0">
|
||||
<template v-if="editing && (settings.isAdmin || settings.isDelegatedAdmin)">
|
||||
<label class="hidden-visually"
|
||||
:for="'subadmins' + uniqueId">
|
||||
{{ t('settings', 'Set account as admin for') }}
|
||||
|
|
@ -139,21 +140,22 @@
|
|||
:data-loading="loading.subadmins || undefined"
|
||||
:input-id="'subadmins' + uniqueId"
|
||||
:close-on-select="false"
|
||||
:disabled="isLoadingField"
|
||||
:disabled="isLoadingField || loading.subAdminGroupsDetails"
|
||||
:loading="loading.subadmins"
|
||||
label="name"
|
||||
:append-to-body="false"
|
||||
:multiple="true"
|
||||
:no-wrap="true"
|
||||
:options="subAdminsGroups"
|
||||
:options="availableSubAdminGroups"
|
||||
:placeholder="t('settings', 'Set account as admin for')"
|
||||
:value="userSubAdminsGroups"
|
||||
:value="userSubAdminGroups"
|
||||
@search="searchGroups"
|
||||
@option:deselected="removeUserSubAdmin"
|
||||
@option:selected="options => addUserSubAdmin(options.at(-1))" />
|
||||
</template>
|
||||
<span v-else-if="!isObfuscated"
|
||||
:title="userSubAdminsGroupsLabels?.length > 40 ? userSubAdminsGroupsLabels : null">
|
||||
{{ userSubAdminsGroupsLabels }}
|
||||
:title="userSubAdminGroupsLabels?.length > 40 ? userSubAdminGroupsLabels : null">
|
||||
{{ userSubAdminGroupsLabels }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
|
@ -296,6 +298,8 @@ import UserRowActions from './UserRowActions.vue'
|
|||
|
||||
import UserRowMixin from '../../mixins/UserRowMixin.js'
|
||||
import { isObfuscated, unlimitedQuota } from '../../utils/userUtils.ts'
|
||||
import { searchGroups, loadUserGroups, loadUserSubAdminGroups } from '../../service/groups.ts'
|
||||
import logger from '../../logger.ts'
|
||||
|
||||
export default {
|
||||
name: 'UserRow',
|
||||
|
|
@ -330,14 +334,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
subAdminsGroups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
quotaOptions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -370,6 +366,8 @@ export default {
|
|||
password: false,
|
||||
mailAddress: false,
|
||||
groups: false,
|
||||
groupsDetails: false,
|
||||
subAdminGroupsDetails: false,
|
||||
subadmins: false,
|
||||
quota: false,
|
||||
delete: false,
|
||||
|
|
@ -381,6 +379,8 @@ export default {
|
|||
editedDisplayName: this.user.displayname,
|
||||
editedPassword: '',
|
||||
editedMail: this.user.email ?? '',
|
||||
// Cancelable promise for search groups request
|
||||
promise: null,
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -412,13 +412,13 @@ export default {
|
|||
|
||||
userGroupsLabels() {
|
||||
return this.userGroups
|
||||
.map(group => group.name)
|
||||
.map(group => group.name ?? group.id)
|
||||
.join(', ')
|
||||
},
|
||||
|
||||
userSubAdminsGroupsLabels() {
|
||||
return this.userSubAdminsGroups
|
||||
.map(group => group.name)
|
||||
userSubAdminGroupsLabels() {
|
||||
return this.userSubAdminGroups
|
||||
.map(group => group.name ?? group.id)
|
||||
.join(', ')
|
||||
},
|
||||
|
||||
|
|
@ -554,6 +554,56 @@ export default {
|
|||
this.loadingPossibleManagers = false
|
||||
},
|
||||
|
||||
async loadGroupsDetails() {
|
||||
this.loading.groups = true
|
||||
this.loading.groupsDetails = true
|
||||
try {
|
||||
const groups = await loadUserGroups({ userId: this.user.id })
|
||||
this.availableGroups = this.availableGroups.map(availableGroup => groups.find(group => group.id === availableGroup.id) ?? availableGroup)
|
||||
} catch (error) {
|
||||
logger.error(t('settings', 'Failed to load groups with details'), { error })
|
||||
}
|
||||
this.loading.groups = false
|
||||
this.loading.groupsDetails = false
|
||||
},
|
||||
|
||||
async loadSubAdminGroupsDetails() {
|
||||
this.loading.subadmins = true
|
||||
this.loading.subAdminGroupsDetails = true
|
||||
try {
|
||||
const groups = await loadUserSubAdminGroups({ userId: this.user.id })
|
||||
this.availableSubAdminGroups = this.availableSubAdminGroups.map(availableGroup => groups.find(group => group.id === availableGroup.id) ?? availableGroup)
|
||||
} catch (error) {
|
||||
logger.error(t('settings', 'Failed to load subadmin groups with details'), { error })
|
||||
}
|
||||
this.loading.subadmins = false
|
||||
this.loading.subAdminGroupsDetails = false
|
||||
},
|
||||
|
||||
async searchGroups(query, toggleLoading) {
|
||||
if (query === '') {
|
||||
return // Prevent unexpected search behaviour e.g. on option:created
|
||||
}
|
||||
if (this.promise) {
|
||||
this.promise.cancel()
|
||||
}
|
||||
toggleLoading(true)
|
||||
try {
|
||||
this.promise = await searchGroups({
|
||||
search: query,
|
||||
offset: 0,
|
||||
limit: 25,
|
||||
})
|
||||
const groups = await this.promise
|
||||
this.availableGroups = groups
|
||||
this.availableSubAdminGroups = groups.filter(group => group.id !== 'admin')
|
||||
} catch (error) {
|
||||
logger.error(t('settings', 'Failed to search groups'), { error })
|
||||
}
|
||||
this.promise = null
|
||||
toggleLoading(false)
|
||||
},
|
||||
|
||||
async searchUserManager(query) {
|
||||
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
|
||||
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
|
||||
|
|
@ -700,17 +750,18 @@ export default {
|
|||
* @param {string} gid Group id
|
||||
*/
|
||||
async createGroup({ name: gid }) {
|
||||
this.loading = { groups: true, subadmins: true }
|
||||
this.loading.groups = true
|
||||
try {
|
||||
await this.$store.dispatch('addGroup', gid)
|
||||
this.availableGroups.push({ id: gid, name: gid })
|
||||
this.availableSubAdminGroups.push({ id: gid, name: gid })
|
||||
const userid = this.user.id
|
||||
await this.$store.dispatch('addUserGroup', { userid, gid })
|
||||
this.userGroups.push({ id: gid, name: gid })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading = { groups: false, subadmins: false }
|
||||
logger.error(t('settings', 'Failed to create group'), { error })
|
||||
}
|
||||
return this.$store.getters.getGroups[this.groups.length]
|
||||
this.loading.groups = false
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -724,19 +775,19 @@ export default {
|
|||
// Ignore
|
||||
return
|
||||
}
|
||||
this.loading.groups = true
|
||||
const userid = this.user.id
|
||||
const gid = group.id
|
||||
if (group.canAdd === false) {
|
||||
return false
|
||||
return
|
||||
}
|
||||
this.loading.groups = true
|
||||
try {
|
||||
await this.$store.dispatch('addUserGroup', { userid, gid })
|
||||
this.userGroups.push(group)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading.groups = false
|
||||
}
|
||||
this.loading.groups = false
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -756,6 +807,7 @@ export default {
|
|||
userid,
|
||||
gid,
|
||||
})
|
||||
this.userGroups = this.userGroups.filter(group => group.id !== gid)
|
||||
this.loading.groups = false
|
||||
// remove user from current list if current list is the removed group
|
||||
if (this.$route.params.selectedGroup === gid) {
|
||||
|
|
@ -780,10 +832,11 @@ export default {
|
|||
userid,
|
||||
gid,
|
||||
})
|
||||
this.loading.subadmins = false
|
||||
this.userSubAdminGroups.push(group)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
this.loading.subadmins = false
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -801,6 +854,7 @@ export default {
|
|||
userid,
|
||||
gid,
|
||||
})
|
||||
this.userSubAdminGroups = this.userSubAdminGroups.filter(group => group.id !== gid)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
|
|
@ -901,6 +955,8 @@ export default {
|
|||
if (this.editing) {
|
||||
await this.$nextTick()
|
||||
this.$refs.displayNameField?.$refs?.inputField?.$refs?.input?.focus()
|
||||
this.loadGroupsDetails()
|
||||
this.loadSubAdminGroupsDetails()
|
||||
}
|
||||
if (this.editedDisplayName !== this.user.displayname) {
|
||||
this.editedDisplayName = this.user.displayname
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@
|
|||
</NcNoteCard>
|
||||
<fieldset>
|
||||
<legend>{{ t('settings', 'Group list sorting') }}</legend>
|
||||
<NcNoteCard class="dialog__note"
|
||||
type="info"
|
||||
:text="t('settings', 'Sorting only applies to the currently loaded groups for performance reasons. Groups will be loaded as you navigate or search through the list.')" />
|
||||
<NcCheckboxRadioSwitch type="radio"
|
||||
:checked.sync="groupSorting"
|
||||
data-test="sortGroupsByMemberCount"
|
||||
|
|
@ -322,6 +325,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog {
|
||||
&__note {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,6 @@ export default {
|
|||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
subAdminsGroups: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
quotaOptions: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
|
|
@ -49,38 +41,19 @@ export default {
|
|||
formattedFullTime,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
availableGroups: this.user.groups.map(id => ({ id, name: id })),
|
||||
availableSubAdminGroups: this.user.subadmin.map(id => ({ id, name: id })),
|
||||
userGroups: this.user.groups.map(id => ({ id, name: id })),
|
||||
userSubAdminGroups: this.user.subadmin.map(id => ({ id, name: id })),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showConfig() {
|
||||
return this.$store.getters.getShowConfig
|
||||
},
|
||||
|
||||
/* GROUPS MANAGEMENT */
|
||||
userGroups() {
|
||||
const userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
|
||||
return userGroups
|
||||
},
|
||||
userSubAdminsGroups() {
|
||||
const userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id))
|
||||
return userSubAdminsGroups
|
||||
},
|
||||
availableGroups() {
|
||||
return this.groups.map((group) => {
|
||||
// clone object because we don't want
|
||||
// to edit the original groups
|
||||
const groupClone = Object.assign({}, group)
|
||||
|
||||
// two settings here:
|
||||
// 1. user NOT in group but no permission to add
|
||||
// 2. user is in group but no permission to remove
|
||||
groupClone.$isDisabled
|
||||
= (group.canAdd === false
|
||||
&& !this.user.groups.includes(group.id))
|
||||
|| (group.canRemove === false
|
||||
&& this.user.groups.includes(group.id))
|
||||
return groupClone
|
||||
})
|
||||
},
|
||||
|
||||
/* QUOTA MANAGEMENT */
|
||||
usedSpace() {
|
||||
const quotaUsed = this.user.quota.used > 0 ? this.user.quota.used : 0
|
||||
|
|
|
|||
83
apps/settings/src/service/groups.ts
Normal file
83
apps/settings/src/service/groups.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IGroup } from '../views/user-types.d.ts'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { CancelablePromise } from 'cancelable-promise'
|
||||
|
||||
interface Group {
|
||||
id: string
|
||||
displayname: string
|
||||
usercount: number
|
||||
disabled: number
|
||||
canAdd: boolean
|
||||
canRemove: boolean
|
||||
}
|
||||
|
||||
const formatGroup = (group: Group): Required<IGroup> => ({
|
||||
id: group.id,
|
||||
name: group.displayname,
|
||||
usercount: group.usercount,
|
||||
disabled: group.disabled,
|
||||
canAdd: group.canAdd,
|
||||
canRemove: group.canRemove,
|
||||
})
|
||||
|
||||
/**
|
||||
* Search groups
|
||||
*
|
||||
* @param {object} options Options
|
||||
* @param {string} options.search Search query
|
||||
* @param {number} options.offset Offset
|
||||
* @param {number} options.limit Limit
|
||||
*/
|
||||
export const searchGroups = ({ search, offset, limit }): CancelablePromise<Required<IGroup>[]> => {
|
||||
const controller = new AbortController()
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
onCancel(() => controller.abort())
|
||||
try {
|
||||
const { data } = await axios.get(
|
||||
generateOcsUrl('/cloud/groups/details?search={search}&offset={offset}&limit={limit}', { search, offset, limit }), {
|
||||
signal: controller.signal,
|
||||
},
|
||||
)
|
||||
const groups: Group[] = data.ocs?.data?.groups ?? []
|
||||
const formattedGroups = groups.map(formatGroup)
|
||||
resolve(formattedGroups)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user groups
|
||||
*
|
||||
* @param {object} options Options
|
||||
* @param {string} options.userId User id
|
||||
*/
|
||||
export const loadUserGroups = async ({ userId }): Promise<Required<IGroup>[]> => {
|
||||
const url = generateOcsUrl('/cloud/users/{userId}/groups/details', { userId })
|
||||
const { data } = await axios.get(url)
|
||||
const groups: Group[] = data.ocs?.data?.groups ?? []
|
||||
const formattedGroups = groups.map(formatGroup)
|
||||
return formattedGroups
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user subadmin groups
|
||||
*
|
||||
* @param {object} options Options
|
||||
* @param {string} options.userId User id
|
||||
*/
|
||||
export const loadUserSubAdminGroups = async ({ userId }): Promise<Required<IGroup>[]> => {
|
||||
const url = generateOcsUrl('/cloud/users/{userId}/subadmins/details', { userId })
|
||||
const { data } = await axios.get(url)
|
||||
const groups: Group[] = data.ocs?.data?.groups ?? []
|
||||
const formattedGroups = groups.map(formatGroup)
|
||||
return formattedGroups
|
||||
}
|
||||
|
|
@ -8,15 +8,22 @@ import { getCapabilities } from '@nextcloud/capabilities'
|
|||
import { parseFileSize } from '@nextcloud/files'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import { GroupSorting } from '../constants/GroupManagement.ts'
|
||||
import { naturalCollator } from '../utils/sorting.ts'
|
||||
import api from './api.js'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
const usersSettings = loadState('settings', 'usersSettings', {})
|
||||
|
||||
const localStorage = getBuilder('settings').persist(true).build()
|
||||
|
||||
const defaults = {
|
||||
/**
|
||||
* @type {import('../views/user-types').IGroup}
|
||||
*/
|
||||
group: {
|
||||
id: '',
|
||||
name: '',
|
||||
|
|
@ -29,14 +36,14 @@ const defaults = {
|
|||
|
||||
const state = {
|
||||
users: [],
|
||||
groups: [],
|
||||
orderBy: GroupSorting.UserCount,
|
||||
groups: [...(usersSettings.systemGroups ?? [])],
|
||||
orderBy: usersSettings.sortGroups ?? GroupSorting.UserCount,
|
||||
minPasswordLength: 0,
|
||||
usersOffset: 0,
|
||||
usersLimit: 25,
|
||||
disabledUsersOffset: 0,
|
||||
disabledUsersLimit: 25,
|
||||
userCount: 0,
|
||||
userCount: usersSettings.userCount ?? 0,
|
||||
showConfig: {
|
||||
showStoragePath: localStorage.getItem('account_settings__showStoragePath') === 'true',
|
||||
showUserBackend: localStorage.getItem('account_settings__showUserBackend') === 'true',
|
||||
|
|
@ -63,21 +70,17 @@ const mutations = {
|
|||
setPasswordPolicyMinLength(state, length) {
|
||||
state.minPasswordLength = length !== '' ? length : 0
|
||||
},
|
||||
initGroups(state, { groups, orderBy, userCount }) {
|
||||
state.groups = groups.map(group => Object.assign({}, defaults.group, group))
|
||||
state.orderBy = orderBy
|
||||
state.userCount = userCount
|
||||
},
|
||||
addGroup(state, { gid, displayName }) {
|
||||
/**
|
||||
* @param {object} state store state
|
||||
* @param {import('../views/user-types.js').IGroup} newGroup new group
|
||||
*/
|
||||
addGroup(state, newGroup) {
|
||||
try {
|
||||
if (typeof state.groups.find((group) => group.id === gid) !== 'undefined') {
|
||||
if (typeof state.groups.find((group) => group.id === newGroup.id) !== 'undefined') {
|
||||
return
|
||||
}
|
||||
// extend group to default values
|
||||
const group = Object.assign({}, defaults.group, {
|
||||
id: gid,
|
||||
name: displayName,
|
||||
})
|
||||
const group = Object.assign({}, defaults.group, newGroup)
|
||||
state.groups.unshift(group)
|
||||
} catch (e) {
|
||||
console.error('Can\'t create group', e)
|
||||
|
|
@ -157,6 +160,9 @@ const mutations = {
|
|||
state.userCount += user.enabled ? 1 : -1 // update Active Users count
|
||||
user.groups.forEach(userGroup => {
|
||||
const group = state.groups.find(groupSearch => groupSearch.id === userGroup)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
group.disabled += user.enabled ? -1 : 1 // update group disabled count
|
||||
})
|
||||
break
|
||||
|
|
@ -165,9 +171,11 @@ const mutations = {
|
|||
state.userCount++ // increment Active Users count
|
||||
|
||||
user.groups.forEach(userGroup => {
|
||||
state.groups
|
||||
.find(groupSearch => groupSearch.id === userGroup)
|
||||
.usercount++ // increment group total count
|
||||
const group = state.groups.find(groupSearch => groupSearch.id === userGroup)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
group.usercount++ // increment group total count
|
||||
})
|
||||
break
|
||||
case 'remove':
|
||||
|
|
@ -186,6 +194,9 @@ const mutations = {
|
|||
disabledGroup.usercount-- // decrement Disabled Users count
|
||||
user.groups.forEach(userGroup => {
|
||||
const group = state.groups.find(groupSearch => groupSearch.id === userGroup)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
group.disabled-- // decrement group disabled count
|
||||
})
|
||||
}
|
||||
|
|
@ -215,6 +226,20 @@ const mutations = {
|
|||
state.disabledUsersOffset = 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset group list
|
||||
*
|
||||
* @param {object} state the store state
|
||||
*/
|
||||
resetGroups(state) {
|
||||
const systemGroups = state.groups.filter(group => [
|
||||
'admin',
|
||||
'__nc_internal_recent',
|
||||
'disabled',
|
||||
].includes(group.id))
|
||||
state.groups = [...systemGroups]
|
||||
},
|
||||
|
||||
setShowConfig(state, { key, value }) {
|
||||
localStorage.setItem(`account_settings__${key}`, JSON.stringify(value))
|
||||
state.showConfig[key] = value
|
||||
|
|
@ -245,20 +270,16 @@ const getters = {
|
|||
getGroups(state) {
|
||||
return state.groups
|
||||
},
|
||||
getSubadminGroups(state) {
|
||||
// Can't be subadmin of admin, recent, or disabled
|
||||
return state.groups.filter(group => group.id !== 'admin' && group.id !== '__nc_internal_recent' && group.id !== 'disabled')
|
||||
},
|
||||
getSortedGroups(state) {
|
||||
const groups = [...state.groups]
|
||||
if (state.orderBy === GroupSorting.UserCount) {
|
||||
return groups.sort((a, b) => {
|
||||
const numA = a.usercount - a.disabled
|
||||
const numB = b.usercount - b.disabled
|
||||
return (numA < numB) ? 1 : (numB < numA ? -1 : a.name.localeCompare(b.name))
|
||||
return (numA < numB) ? 1 : (numB < numA ? -1 : naturalCollator.compare(a.name, b.name))
|
||||
})
|
||||
} else {
|
||||
return groups.sort((a, b) => a.name.localeCompare(b.name))
|
||||
return groups.sort((a, b) => naturalCollator.compare(a.name, b.name))
|
||||
}
|
||||
},
|
||||
getGroupSorting(state) {
|
||||
|
|
@ -444,7 +465,7 @@ const actions = {
|
|||
.then((response) => {
|
||||
if (Object.keys(response.data.ocs.data.groups).length > 0) {
|
||||
response.data.ocs.data.groups.forEach(function(group) {
|
||||
context.commit('addGroup', { gid: group, displayName: group })
|
||||
context.commit('addGroup', { id: group, name: group })
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
|
@ -511,7 +532,7 @@ const actions = {
|
|||
return api.requireAdmin().then((response) => {
|
||||
return api.post(generateOcsUrl('cloud/groups'), { groupid: gid })
|
||||
.then((response) => {
|
||||
context.commit('addGroup', { gid, displayName: gid })
|
||||
context.commit('addGroup', { id: gid, name: gid })
|
||||
return { gid, displayName: gid }
|
||||
})
|
||||
.catch((error) => { throw error })
|
||||
|
|
|
|||
14
apps/settings/src/utils/sorting.ts
Normal file
14
apps/settings/src/utils/sorting.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getCanonicalLocale, getLanguage } from '@nextcloud/l10n'
|
||||
|
||||
export const naturalCollator = Intl.Collator(
|
||||
[getLanguage(), getCanonicalLocale()],
|
||||
{
|
||||
numeric: true,
|
||||
usage: 'sort',
|
||||
},
|
||||
)
|
||||
|
|
@ -55,11 +55,6 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
beforeMount() {
|
||||
this.$store.commit('initGroups', {
|
||||
groups: this.$store.getters.getServerData.groups,
|
||||
orderBy: this.$store.getters.getServerData.sortGroups,
|
||||
userCount: this.$store.getters.getServerData.userCount,
|
||||
})
|
||||
this.$store.dispatch('getPasswordPolicyMinLength')
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<template>
|
||||
<NcAppNavigation :aria-label="t('settings', 'Account management')">
|
||||
<NcAppNavigation class="account-management__navigation"
|
||||
:aria-label="t('settings', 'Account management')">
|
||||
<NcAppNavigationNew button-id="new-user-button"
|
||||
:text="t('settings','New account')"
|
||||
@click="showNewUserMenu"
|
||||
|
|
@ -79,42 +80,7 @@
|
|||
</NcAppNavigationItem>
|
||||
</NcAppNavigationList>
|
||||
|
||||
<NcAppNavigationCaption :name="t('settings', 'Groups')"
|
||||
:disabled="loadingAddGroup"
|
||||
:aria-label="loadingAddGroup ? t('settings', 'Creating group…') : t('settings', 'Create group')"
|
||||
force-menu
|
||||
is-heading
|
||||
:open.sync="isAddGroupOpen">
|
||||
<template v-if="isAdminOrDelegatedAdmin" #actionsTriggerIcon>
|
||||
<NcLoadingIcon v-if="loadingAddGroup" />
|
||||
<NcIconSvgWrapper v-else :path="mdiPlus" />
|
||||
</template>
|
||||
<template v-if="isAdminOrDelegatedAdmin" #actions>
|
||||
<NcActionText>
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiAccountGroup" />
|
||||
</template>
|
||||
{{ t('settings', 'Create group') }}
|
||||
</NcActionText>
|
||||
<NcActionInput :label="t('settings', 'Group name')"
|
||||
data-cy-users-settings-new-group-name
|
||||
:label-outside="false"
|
||||
:disabled="loadingAddGroup"
|
||||
:value.sync="newGroupName"
|
||||
:error="hasAddGroupError"
|
||||
:helper-text="hasAddGroupError ? t('settings', 'Please enter a valid group name') : ''"
|
||||
@submit="createGroup" />
|
||||
</template>
|
||||
</NcAppNavigationCaption>
|
||||
|
||||
<NcAppNavigationList class="account-management__group-list" data-cy-users-settings-navigation-groups="custom">
|
||||
<GroupListItem v-for="group in userGroups"
|
||||
:id="group.id"
|
||||
:key="group.id"
|
||||
:active="selectedGroupDecoded === group.id"
|
||||
:name="group.title"
|
||||
:count="group.count" />
|
||||
</NcAppNavigationList>
|
||||
<AppNavigationGroupList />
|
||||
|
||||
<template #footer>
|
||||
<NcButton class="account-management__settings-toggle"
|
||||
|
|
@ -131,31 +97,26 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mdiAccount, mdiAccountGroup, mdiAccountOff, mdiCog, mdiPlus, mdiShieldAccount, mdiHistory } from '@mdi/js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { mdiAccount, mdiAccountOff, mdiCog, mdiPlus, mdiShieldAccount, mdiHistory } from '@mdi/js'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import NcActionInput from '@nextcloud/vue/components/NcActionInput'
|
||||
import NcActionText from '@nextcloud/vue/components/NcActionText'
|
||||
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
|
||||
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
|
||||
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
|
||||
import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
|
||||
import NcAppNavigationNew from '@nextcloud/vue/components/NcAppNavigationNew'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
|
||||
import GroupListItem from '../components/GroupListItem.vue'
|
||||
import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue'
|
||||
import AppNavigationGroupList from '../components/AppNavigationGroupList.vue'
|
||||
|
||||
import { useStore } from '../store'
|
||||
import { useRoute, useRouter } from 'vue-router/composables'
|
||||
import { useRoute } from 'vue-router/composables'
|
||||
import { useFormatGroups } from '../composables/useGroupsNavigation'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useStore()
|
||||
|
||||
/** State of the 'new-account' dialog */
|
||||
|
|
@ -170,51 +131,13 @@ const selectedGroupDecoded = computed(() => selectedGroup.value ? decodeURICompo
|
|||
const userCount = computed(() => store.getters.getUserCount)
|
||||
/** All available groups */
|
||||
const groups = computed(() => store.getters.getSortedGroups)
|
||||
const { adminGroup, recentGroup, disabledGroup, userGroups } = useFormatGroups(groups)
|
||||
const { adminGroup, recentGroup, disabledGroup } = useFormatGroups(groups)
|
||||
|
||||
/** Server settings for current user */
|
||||
const settings = computed(() => store.getters.getServerData)
|
||||
/** True if the current user is a (delegated) admin */
|
||||
const isAdminOrDelegatedAdmin = computed(() => settings.value.isAdmin || settings.value.isDelegatedAdmin)
|
||||
|
||||
/** True if the 'add-group' dialog is open - needed to be able to close it when the group is created */
|
||||
const isAddGroupOpen = ref(false)
|
||||
/** True if the group creation is in progress to show loading spinner and disable adding another one */
|
||||
const loadingAddGroup = ref(false)
|
||||
/** Error state for creating a new group */
|
||||
const hasAddGroupError = ref(false)
|
||||
/** Name of the group to create (used in the group creation dialog) */
|
||||
const newGroupName = ref('')
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*/
|
||||
async function createGroup() {
|
||||
hasAddGroupError.value = false
|
||||
const groupId = newGroupName.value.trim()
|
||||
if (groupId === '') {
|
||||
hasAddGroupError.value = true
|
||||
return
|
||||
}
|
||||
|
||||
isAddGroupOpen.value = false
|
||||
loadingAddGroup.value = true
|
||||
|
||||
try {
|
||||
await store.dispatch('addGroup', groupId)
|
||||
await router.push({
|
||||
name: 'group',
|
||||
params: {
|
||||
selectedGroup: encodeURIComponent(groupId),
|
||||
},
|
||||
})
|
||||
newGroupName.value = ''
|
||||
} catch {
|
||||
showError(t('settings', 'Failed to create group'))
|
||||
}
|
||||
loadingAddGroup.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the new-user form dialog
|
||||
*/
|
||||
|
|
@ -227,7 +150,12 @@ function showNewUserMenu() {
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.account-management{
|
||||
.account-management {
|
||||
&__navigation {
|
||||
:deep(.app-navigation__body) {
|
||||
will-change: scroll-position;
|
||||
}
|
||||
}
|
||||
&__system-list {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
|
|
|
|||
17
apps/settings/src/views/user-types.d.ts
vendored
17
apps/settings/src/views/user-types.d.ts
vendored
|
|
@ -3,7 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
export interface IGroup {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Display name
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
|
|
@ -15,4 +22,14 @@ export interface IGroup {
|
|||
* Number of disabled users
|
||||
*/
|
||||
disabled: number
|
||||
|
||||
/**
|
||||
* True if users can be added to this group
|
||||
*/
|
||||
canAdd?: boolean
|
||||
|
||||
/**
|
||||
* True if users can be removed from this group
|
||||
*/
|
||||
canRemove?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,15 +55,17 @@ describe('Settings: Assign user to a group', { testIsolation: false }, () => {
|
|||
})
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').contains('li', groupName).should('exist')
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').contains('li', groupName).within(() => {
|
||||
cy.get('.counter-bubble__counter')
|
||||
.should('not.exist') // is hidden when 0
|
||||
})
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('.counter-bubble__counter')
|
||||
.should('not.exist') // is hidden when 0
|
||||
})
|
||||
|
||||
it('see that the user is in the list', () => {
|
||||
|
|
@ -101,8 +103,7 @@ describe('Settings: Assign user to a group', { testIsolation: false }, () => {
|
|||
|
||||
it('see the group was successfully assigned', () => {
|
||||
// see a new memeber
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.contains('li', groupName)
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('.counter-bubble__counter')
|
||||
.should('contain', '1')
|
||||
})
|
||||
|
|
@ -121,18 +122,20 @@ describe('Settings: Delete an empty group', { testIsolation: false }, () => {
|
|||
before(() => {
|
||||
cy.runOccCommand(`group:add '${groupName}'`)
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
// see that the list of groups contains the group foo
|
||||
cy.contains(groupName).should('exist').scrollIntoView()
|
||||
// open the actions menu for the group
|
||||
cy.contains('li', groupName).within(() => {
|
||||
cy.get('button.action-item__menutoggle').click({ force: true })
|
||||
})
|
||||
})
|
||||
// see that the list of groups contains the group foo
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
// open the actions menu for the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('button.action-item__menutoggle')
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
it('can delete the group', () => {
|
||||
|
|
@ -146,10 +149,9 @@ describe('Settings: Delete an empty group', { testIsolation: false }, () => {
|
|||
})
|
||||
|
||||
it('deleted group is not shown anymore', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
// see that the list of groups does not contain the group
|
||||
cy.contains(groupName).should('not.exist')
|
||||
})
|
||||
// see that the list of groups does not contain the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('not.exist')
|
||||
// and also not in database
|
||||
cy.runOccCommand('group:list --output=json').then(($response) => {
|
||||
const groups: string[] = Object.keys(JSON.parse($response.stdout))
|
||||
|
|
@ -169,19 +171,22 @@ describe('Settings: Delete a non empty group', () => {
|
|||
cy.runOccCommand(`group:addUser '${groupName}' '${$user.userId}'`)
|
||||
})
|
||||
cy.login(admin)
|
||||
cy.intercept('GET', '**/ocs/v2.php/cloud/groups/details?search=&offset=*&limit=*').as('loadGroups')
|
||||
cy.visit('/settings/users')
|
||||
cy.wait('@loadGroups')
|
||||
})
|
||||
after(() => cy.deleteUser(testUser))
|
||||
|
||||
it('see that the group is in the list', () => {
|
||||
// see that the list of groups contains the group
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').contains('li', groupName).should('exist').scrollIntoView()
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('exist')
|
||||
.scrollIntoView()
|
||||
})
|
||||
|
||||
it('can delete the group', () => {
|
||||
// open the menu
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]')
|
||||
.contains('li', groupName)
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.find('button.action-item__menutoggle')
|
||||
.click({ force: true })
|
||||
|
||||
|
|
@ -195,10 +200,9 @@ describe('Settings: Delete a non empty group', () => {
|
|||
})
|
||||
|
||||
it('deleted group is not shown anymore', () => {
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
|
||||
// see that the list of groups does not contain the group foo
|
||||
cy.contains(groupName).should('not.exist')
|
||||
})
|
||||
// see that the list of groups does not contain the group foo
|
||||
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').find('li').contains(groupName)
|
||||
.should('not.exist')
|
||||
// and also not in database
|
||||
cy.runOccCommand('group:list --output=json').then(($response) => {
|
||||
const groups: string[] = Object.keys(JSON.parse($response.stdout))
|
||||
|
|
|
|||
|
|
@ -239,6 +239,8 @@ describe('Settings: Change user properties', function() {
|
|||
cy.get('.vs__selected').should('not.exist')
|
||||
// Open the dropdown menu
|
||||
cy.get('[role="combobox"]').click({ force: true })
|
||||
// Search for the group
|
||||
cy.get('[role="combobox"]').type('userstestgroup')
|
||||
// select the group
|
||||
cy.contains('li', groupName).click({ force: true })
|
||||
|
||||
|
|
|
|||
4
dist/settings-apps-view-4529.js
vendored
4
dist/settings-apps-view-4529.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-apps-view-4529.js.map
vendored
2
dist/settings-apps-view-4529.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/settings-users-3239.js
vendored
4
dist/settings-users-3239.js
vendored
File diff suppressed because one or more lines are too long
2
dist/settings-users-3239.js.map
vendored
2
dist/settings-users-3239.js.map
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
162
openapi.json
162
openapi.json
|
|
@ -26005,6 +26005,168 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/groups/details": {
|
||||
"get": {
|
||||
"operationId": "provisioning_api-full-users-get-users-groups-details",
|
||||
"summary": "Get a list of groups with details",
|
||||
"tags": [
|
||||
"provisioning_api-full/users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProvisioningApi-fullGroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/subadmins/details": {
|
||||
"get": {
|
||||
"operationId": "provisioning_api-full-users-get-user-sub-admin-groups-details",
|
||||
"summary": "Get a list of the groups the user is a subadmin of, with details",
|
||||
"tags": [
|
||||
"provisioning_api-full/users"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer_auth": []
|
||||
},
|
||||
{
|
||||
"basic_auth": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"in": "path",
|
||||
"description": "ID of the user",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCS-APIRequest",
|
||||
"in": "header",
|
||||
"description": "Required to be true for the API request to pass",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Users subadmin groups returned",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ocs"
|
||||
],
|
||||
"properties": {
|
||||
"ocs": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"meta",
|
||||
"data"
|
||||
],
|
||||
"properties": {
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/OCSMeta"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/ProvisioningApi-fullGroupDetails"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/ocs/v2.php/cloud/users/{userId}/welcome": {
|
||||
"post": {
|
||||
"operationId": "provisioning_api-full-users-resend-welcome-message",
|
||||
|
|
|
|||
Loading…
Reference in a new issue