Skip to content

Commit 9afcec7

Browse files
authored
Merge pull request #44465 from nextcloud/feat/allow-to-sort-groups-abc
feat(settings): Allow to sort groups in the account management alphabetically
2 parents 6d3b4b0 + 82be5e0 commit 9afcec7

14 files changed

+238
-34
lines changed

apps/settings/lib/Controller/UsersController.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ public function usersList(): TemplateResponse {
125125
/* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
126126
$sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT;
127127
$isLDAPUsed = false;
128-
if ($this->config->getSystemValue('sort_groups_by_name', false)) {
128+
if ($this->config->getSystemValueBool('sort_groups_by_name', false)) {
129129
$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
130130
} else {
131131
if ($this->appManager->isEnabledForUser('user_ldap')) {
@@ -212,13 +212,19 @@ public function usersList(): TemplateResponse {
212212
/* LANGUAGES */
213213
$languages = $this->l10nFactory->getLanguages();
214214

215+
/** Using LDAP or admins (system config) can enfore sorting by group name, in this case the frontend setting is overwritten */
216+
$forceSortGroupByName = $sortGroupsBy === \OC\Group\MetaData::SORT_GROUPNAME;
217+
215218
/* FINAL DATA */
216219
$serverData = [];
217220
// groups
218221
$serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups);
219222
// Various data
220223
$serverData['isAdmin'] = $isAdmin;
221-
$serverData['sortGroups'] = $sortGroupsBy;
224+
$serverData['sortGroups'] = $forceSortGroupByName
225+
? \OC\Group\MetaData::SORT_GROUPNAME
226+
: (int)$this->config->getAppValue('core', 'group.sortBy', (string)\OC\Group\MetaData::SORT_USERCOUNT);
227+
$serverData['forceSortGroupByName'] = $forceSortGroupByName;
222228
$serverData['quotaPreset'] = $quotaPreset;
223229
$serverData['allowUnlimitedQuota'] = $allowUnlimitedQuota;
224230
$serverData['userCount'] = $userCount;
@@ -247,7 +253,7 @@ public function usersList(): TemplateResponse {
247253
* @return JSONResponse
248254
*/
249255
public function setPreference(string $key, string $value): JSONResponse {
250-
$allowed = ['newUser.sendEmail'];
256+
$allowed = ['newUser.sendEmail', 'group.sortBy'];
251257
if (!in_array($key, $allowed, true)) {
252258
return new JSONResponse([], Http::STATUS_FORBIDDEN);
253259
}

apps/settings/src/components/Users/UserSettingsDialog.vue

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,32 @@
4848
</NcCheckboxRadioSwitch>
4949
</NcAppSettingsSection>
5050

51+
<NcAppSettingsSection id="groups-sorting"
52+
:name="t('settings', 'Sorting')">
53+
<NcNoteCard v-if="isGroupSortingEnforced" type="warning">
54+
{{ t('settings', 'The system config enforces sorting the groups by name. This also disables showing the member count.') }}
55+
</NcNoteCard>
56+
<fieldset>
57+
<legend>{{ t('settings', 'Group list sorting') }}</legend>
58+
<NcCheckboxRadioSwitch type="radio"
59+
:checked.sync="groupSorting"
60+
data-test="sortGroupsByMemberCount"
61+
:disabled="isGroupSortingEnforced"
62+
name="group-sorting-mode"
63+
value="member-count">
64+
{{ t('settings', 'By member count') }}
65+
</NcCheckboxRadioSwitch>
66+
<NcCheckboxRadioSwitch type="radio"
67+
:checked.sync="groupSorting"
68+
data-test="sortGroupsByName"
69+
:disabled="isGroupSortingEnforced"
70+
name="group-sorting-mode"
71+
value="name">
72+
{{ t('settings', 'By name') }}
73+
</NcCheckboxRadioSwitch>
74+
</fieldset>
75+
</NcAppSettingsSection>
76+
5177
<NcAppSettingsSection id="email-settings"
5278
:name="t('settings', 'Send email')">
5379
<NcCheckboxRadioSwitch type="switch"
@@ -81,8 +107,10 @@ import axios from '@nextcloud/axios'
81107
import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDialog.js'
82108
import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js'
83109
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
110+
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
84111
import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
85112
113+
import { GroupSorting } from '../../constants/GroupManagement.ts'
86114
import { unlimitedQuota } from '../../utils/userUtils.ts'
87115
88116
export default {
@@ -92,6 +120,7 @@ export default {
92120
NcAppSettingsDialog,
93121
NcAppSettingsSection,
94122
NcCheckboxRadioSwitch,
123+
NcNoteCard,
95124
NcSelect,
96125
},
97126
@@ -110,6 +139,22 @@ export default {
110139
},
111140
112141
computed: {
142+
groupSorting: {
143+
get() {
144+
return this.$store.getters.getGroupSorting === GroupSorting.GroupName ? 'name' : 'member-count'
145+
},
146+
set(sorting) {
147+
this.$store.commit('setGroupSorting', sorting === 'name' ? GroupSorting.GroupName : GroupSorting.UserCount)
148+
},
149+
},
150+
151+
/**
152+
* Admin has configured `sort_groups_by_name` in the system config
153+
*/
154+
isGroupSortingEnforced() {
155+
return this.$store.getters.getServerData.forceSortGroupByName
156+
},
157+
113158
isModalOpen: {
114159
get() {
115160
return this.open
@@ -261,3 +306,9 @@ export default {
261306
},
262307
}
263308
</script>
309+
310+
<style scoped lang="scss">
311+
fieldset {
312+
font-weight: bold;
313+
}
314+
</style>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @copyright Copyright (c) 2024 Ferdinand Thiessen <[email protected]>
3+
*
4+
* @author Ferdinand Thiessen <[email protected]>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
/**
24+
* https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34
25+
*/
26+
export enum GroupSorting {
27+
UserCount = 1,
28+
GroupName = 2
29+
}

apps/settings/src/store/users.js

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,16 @@
3030
import { getBuilder } from '@nextcloud/browser-storage'
3131
import { getCapabilities } from '@nextcloud/capabilities'
3232
import { parseFileSize } from '@nextcloud/files'
33-
import { generateOcsUrl } from '@nextcloud/router'
33+
import { showError } from '@nextcloud/dialogs'
34+
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
3435
import axios from '@nextcloud/axios'
3536

37+
import { GroupSorting } from '../constants/GroupManagement.ts'
3638
import api from './api.js'
3739
import logger from '../logger.ts'
3840

3941
const localStorage = getBuilder('settings').persist(true).build()
4042

41-
const orderGroups = function(groups, orderBy) {
42-
/* const SORT_USERCOUNT = 1;
43-
* const SORT_GROUPNAME = 2;
44-
* https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34
45-
*/
46-
if (orderBy === 1) {
47-
return groups.sort((a, b) => a.usercount - a.disabled < b.usercount - b.disabled)
48-
} else {
49-
return groups.sort((a, b) => a.name.localeCompare(b.name))
50-
}
51-
}
52-
5343
const defaults = {
5444
group: {
5545
id: '',
@@ -64,7 +54,7 @@ const defaults = {
6454
const state = {
6555
users: [],
6656
groups: [],
67-
orderBy: 1,
57+
orderBy: GroupSorting.UserCount,
6858
minPasswordLength: 0,
6959
usersOffset: 0,
7060
usersLimit: 25,
@@ -100,8 +90,6 @@ const mutations = {
10090
state.groups = groups.map(group => Object.assign({}, defaults.group, group))
10191
state.orderBy = orderBy
10292
state.userCount = userCount
103-
state.groups = orderGroups(state.groups, state.orderBy)
104-
10593
},
10694
addGroup(state, { gid, displayName }) {
10795
try {
@@ -114,7 +102,6 @@ const mutations = {
114102
name: displayName,
115103
})
116104
state.groups.unshift(group)
117-
state.groups = orderGroups(state.groups, state.orderBy)
118105
} catch (e) {
119106
console.error('Can\'t create group', e)
120107
}
@@ -125,7 +112,6 @@ const mutations = {
125112
const updatedGroup = state.groups[groupIndex]
126113
updatedGroup.name = displayName
127114
state.groups.splice(groupIndex, 1, updatedGroup)
128-
state.groups = orderGroups(state.groups, state.orderBy)
129115
}
130116
},
131117
removeGroup(state, gid) {
@@ -143,7 +129,6 @@ const mutations = {
143129
}
144130
const groups = user.groups
145131
groups.push(gid)
146-
state.groups = orderGroups(state.groups, state.orderBy)
147132
},
148133
removeUserGroup(state, { userid, gid }) {
149134
const group = state.groups.find(groupSearch => groupSearch.id === gid)
@@ -154,7 +139,6 @@ const mutations = {
154139
}
155140
const groups = user.groups
156141
groups.splice(groups.indexOf(gid), 1)
157-
state.groups = orderGroups(state.groups, state.orderBy)
158142
},
159143
addUserSubAdmin(state, { userid, gid }) {
160144
const groups = state.users.find(user => user.id === userid).subadmin
@@ -254,6 +238,23 @@ const mutations = {
254238
localStorage.setItem(`account_settings__${key}`, JSON.stringify(value))
255239
state.showConfig[key] = value
256240
},
241+
242+
setGroupSorting(state, sorting) {
243+
const oldValue = state.orderBy
244+
state.orderBy = sorting
245+
246+
// Persist the value on the server
247+
axios.post(
248+
generateUrl('/settings/users/preferences/group.sortBy'),
249+
{
250+
value: String(sorting),
251+
},
252+
).catch((error) => {
253+
state.orderBy = oldValue
254+
showError(t('settings', 'Could not set group sorting'))
255+
logger.error(error)
256+
})
257+
},
257258
}
258259

259260
const getters = {
@@ -267,6 +268,21 @@ const getters = {
267268
// Can't be subadmin of admin or disabled
268269
return state.groups.filter(group => group.id !== 'admin' && group.id !== 'disabled')
269270
},
271+
getSortedGroups(state) {
272+
const groups = [...state.groups]
273+
if (state.orderBy === GroupSorting.UserCount) {
274+
return groups.sort((a, b) => {
275+
const numA = a.usercount - a.disabled
276+
const numB = b.usercount - b.disabled
277+
return (numA < numB) ? 1 : (numB < numA ? -1 : a.name.localeCompare(b.name))
278+
})
279+
} else {
280+
return groups.sort((a, b) => a.name.localeCompare(b.name))
281+
}
282+
},
283+
getGroupSorting(state) {
284+
return state.orderBy
285+
},
270286
getPasswordPolicyMinLength(state) {
271287
return state.minPasswordLength
272288
},

apps/settings/src/views/UserManagementNavigation.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const selectedGroupDecoded = computed(() => selectedGroup.value ? decodeURICompo
149149
/** Overall user count */
150150
const userCount = computed(() => store.getters.getUserCount)
151151
/** All available groups */
152-
const groups = computed(() => store.getters.getGroups)
152+
const groups = computed(() => store.getters.getSortedGroups)
153153
const { adminGroup, disabledGroup, userGroups } = useFormatGroups(groups)
154154
155155
/** True if the current user is an administrator */

config/config.sample.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,7 @@
13531353
* Sort groups in the user settings by name instead of the user count
13541354
*
13551355
* By enabling this the user count beside the group name is disabled as well.
1356+
* @deprecated since Nextcloud 29 - Use the frontend instead or set the app config value `group.sortBy` for `core` to `2`
13561357
*/
13571358
'sort_groups_by_name' => false,
13581359

cypress/e2e/settings/users_groups.cy.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
*/
2222

2323
import { User } from '@nextcloud/cypress'
24-
import { getUserListRow, handlePasswordConfirmation, toggleEditButton } from './usersUtils'
24+
import { assertNotExistOrNotVisible, getUserListRow, handlePasswordConfirmation, toggleEditButton } from './usersUtils'
2525

2626
// eslint-disable-next-line n/no-extraneous-import
2727
import randomString from 'crypto-random-string'
@@ -223,3 +223,82 @@ describe('Settings: Delete a non empty group', () => {
223223
})
224224
})
225225
})
226+
227+
describe.only('Settings: Sort groups in the UI', () => {
228+
before(() => {
229+
// Clear state
230+
cy.runOccCommand('group:list --output json').then((output) => {
231+
const groups = Object.keys(JSON.parse(output.stdout)).filter((group) => group !== 'admin')
232+
groups.forEach((group) => {
233+
cy.runOccCommand(`group:delete "${group}"`)
234+
})
235+
})
236+
237+
// Add two groups and add one user to group B
238+
cy.runOccCommand('group:add A')
239+
cy.runOccCommand('group:add B')
240+
cy.createRandomUser().then((user) => {
241+
cy.runOccCommand(`group:adduser B "${user.userId}"`)
242+
})
243+
244+
// Visit the settings as admin
245+
cy.login(admin)
246+
cy.visit('/settings/users')
247+
})
248+
249+
it('Can set sort by member count', () => {
250+
// open the settings dialog
251+
cy.contains('button', 'Account management settings').click()
252+
253+
cy.contains('.modal-container', 'Account management settings').within(() => {
254+
cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').scrollIntoView()
255+
cy.get('[data-test="sortGroupsByMemberCount"] input[type="radio"]').check({ force: true })
256+
// close the settings dialog
257+
cy.get('button.modal-container__close').click()
258+
})
259+
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
260+
})
261+
262+
it('See that the groups are sorted by the member count', () => {
263+
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
264+
cy.get('li').eq(0).should('contain', 'B') // 1 member
265+
cy.get('li').eq(1).should('contain', 'A') // 0 members
266+
})
267+
})
268+
269+
it('See that the order is preserved after a reload', () => {
270+
cy.reload()
271+
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
272+
cy.get('li').eq(0).should('contain', 'B') // 1 member
273+
cy.get('li').eq(1).should('contain', 'A') // 0 members
274+
})
275+
})
276+
277+
it('Can set sort by group name', () => {
278+
// open the settings dialog
279+
cy.contains('button', 'Account management settings').click()
280+
281+
cy.contains('.modal-container', 'Account management settings').within(() => {
282+
cy.get('[data-test="sortGroupsByName"] input[type="radio"]').scrollIntoView()
283+
cy.get('[data-test="sortGroupsByName"] input[type="radio"]').check({ force: true })
284+
// close the settings dialog
285+
cy.get('button.modal-container__close').click()
286+
})
287+
cy.waitUntil(() => cy.get('.modal-container').should(el => assertNotExistOrNotVisible(el)))
288+
})
289+
290+
it('See that the groups are sorted by the user count', () => {
291+
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
292+
cy.get('li').eq(0).should('contain', 'A')
293+
cy.get('li').eq(1).should('contain', 'B')
294+
})
295+
})
296+
297+
it('See that the order is preserved after a reload', () => {
298+
cy.reload()
299+
cy.get('ul[data-cy-users-settings-navigation-groups="custom"]').within(() => {
300+
cy.get('li').eq(0).should('contain', 'A')
301+
cy.get('li').eq(1).should('contain', 'B')
302+
})
303+
})
304+
})

dist/core-common.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core-common.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-users-3239.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-users-3239.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/settings-vue-settings-apps-users-management.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)