aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/settings/src/components/UserList.vue34
-rw-r--r--apps/settings/src/components/Users/UserSettingsDialog.vue31
-rw-r--r--apps/settings/src/composables/useGroupsNavigation.ts52
-rw-r--r--apps/settings/src/main-apps-users-management.ts7
-rw-r--r--apps/settings/src/store/index.js26
-rw-r--r--apps/settings/src/views/UserManagement.vue296
-rw-r--r--apps/settings/src/views/UserManagementNavigation.vue221
-rw-r--r--apps/settings/src/views/user-types.d.ts14
-rw-r--r--cypress/e2e/settings/users_columns.cy.ts6
9 files changed, 355 insertions, 332 deletions
diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue
index 5672788bafe..3ddb617dda5 100644
--- a/apps/settings/src/components/UserList.vue
+++ b/apps/settings/src/components/UserList.vue
@@ -36,8 +36,7 @@
<NcLoadingIcon v-if="isInitialLoad && loading.users"
:name="t('settings', 'Loading accounts …')"
:size="64" />
- <NcIconSvgWrapper v-else
- :svg="usersSvg" />
+ <NcIconSvgWrapper v-else :path="mdiAccountGroup" :size="64" />
</template>
</NcEmptyContent>
@@ -78,16 +77,16 @@
</template>
<script>
-import Vue from 'vue'
+import { mdiAccountGroup } from '@mdi/js'
+import { showError } from '@nextcloud/dialogs'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Fragment } from 'vue-frag'
+import Vue from 'vue'
import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import { showError } from '@nextcloud/dialogs'
-
import VirtualList from './Users/VirtualList.vue'
import NewUserModal from './Users/NewUserModal.vue'
import UserListFooter from './Users/UserListFooter.vue'
@@ -97,9 +96,7 @@ import UserRow from './Users/UserRow.vue'
import { defaultQuota, isObfuscated, unlimitedQuota } from '../utils/userUtils.ts'
import logger from '../logger.ts'
-import usersSvg from '../../img/users.svg?raw'
-
-const newUser = {
+const newUser = Object.freeze({
id: '',
displayName: '',
password: '',
@@ -112,7 +109,7 @@ const newUser = {
code: 'en',
name: t('settings', 'Default language'),
},
-}
+})
export default {
name: 'UserList',
@@ -139,19 +136,26 @@ export default {
},
},
- data() {
+ setup() {
+ // non reactive properties
return {
+ mdiAccountGroup,
+ rowHeight: 55,
+
UserRow,
+ }
+ },
+
+ data() {
+ return {
loading: {
all: false,
groups: false,
users: false,
},
+ newUser: { ...newUser },
isInitialLoad: true,
- rowHeight: 55,
- usersSvg,
searchQuery: '',
- newUser: Object.assign({}, newUser),
}
},
@@ -252,7 +256,7 @@ export default {
watch: {
// watch url change and group select
- async selectedGroup(val, old) {
+ async selectedGroup(val) {
this.isInitialLoad = true
// if selected is the disabled group but it's empty
await this.redirectIfDisabled()
diff --git a/apps/settings/src/components/Users/UserSettingsDialog.vue b/apps/settings/src/components/Users/UserSettingsDialog.vue
index 26659e85b5e..e87f53cdf38 100644
--- a/apps/settings/src/components/Users/UserSettingsDialog.vue
+++ b/apps/settings/src/components/Users/UserSettingsDialog.vue
@@ -60,9 +60,8 @@
<NcAppSettingsSection id="default-settings"
:name="t('settings', 'Defaults')">
- <label for="default-quota-select">{{ t('settings', 'Default quota') }}</label>
<NcSelect v-model="defaultQuota"
- input-id="default-quota-select"
+ :input-label="t('settings', 'Default quota')"
placement="top"
:taggable="true"
:options="quotaOptions"
@@ -75,9 +74,11 @@
</template>
<script>
-import axios from '@nextcloud/axios'
+import { getBuilder } from '@nextcloud/browser-storage'
+import { formatFileSize, parseFileSize } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
+import axios from '@nextcloud/axios'
import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDialog.js'
import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
@@ -102,6 +103,15 @@ export default {
},
},
+ setup() {
+ const localStorage = getBuilder('settings')
+ .persist(true)
+ .clearOnLogout(true)
+ .build()
+
+ return { localStorage }
+ },
+
data() {
return {
selectedQuota: false,
@@ -213,7 +223,7 @@ export default {
methods: {
getLocalstorage(key) {
// force initialization
- const localConfig = this.$localStorage.get(key)
+ const localConfig = JSON.parse(this.localStorage.getItem(key) ?? 'null')
// if localstorage is null, fallback to original values
this.$store.commit('setShowConfig', { key, value: localConfig !== null ? localConfig === 'true' : this.showConfig[key] })
return this.showConfig[key]
@@ -221,7 +231,7 @@ export default {
setLocalStorage(key, status) {
this.$store.commit('setShowConfig', { key, value: status })
- this.$localStorage.set(key, status)
+ this.localStorage.setItem(key, JSON.stringify(status))
return status
},
@@ -236,12 +246,12 @@ export default {
quota = quota?.id || quota.label
}
// only used for new presets sent through @Tag
- const validQuota = OC.Util.computerFileSize(quota)
+ const validQuota = parseFileSize(quota)
if (validQuota === null) {
return unlimitedQuota
} else {
// unify format output
- quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
+ quota = formatFileSize(parseFileSize(quota))
return { id: quota, label: quota }
}
},
@@ -271,10 +281,3 @@ export default {
},
}
</script>
-
-<style lang="scss" scoped>
-label[for="default-quota-select"] {
- display: block;
- padding: 4px 0;
-}
-</style>
diff --git a/apps/settings/src/composables/useGroupsNavigation.ts b/apps/settings/src/composables/useGroupsNavigation.ts
new file mode 100644
index 00000000000..835664fbdc1
--- /dev/null
+++ b/apps/settings/src/composables/useGroupsNavigation.ts
@@ -0,0 +1,52 @@
+import type { ComputedRef, Ref } from 'vue'
+import type { IGroup } from '../views/user-types'
+
+import { computed } from 'vue'
+
+/**
+ * Format a group to a menu entry
+ *
+ * @param group the group
+ */
+function formatGroupMenu(group?: IGroup) {
+ if (typeof group === 'undefined') {
+ return null
+ }
+
+ const item = {
+ id: group.id,
+ title: group.name,
+ usercount: group.usercount,
+ count: Math.max(0, group.usercount - group.disabled),
+ }
+
+ return item
+}
+
+export const useFormatGroups = (groups: Ref<IGroup[]>|ComputedRef<IGroup[]>) => {
+ /**
+ * All non-disabled non-admin groups
+ */
+ const userGroups = computed(() => {
+ const formatted = groups.value
+ // filter out disabled and admin
+ .filter(group => group.id !== 'disabled' && group.id !== 'admin')
+ // format group
+ .map(group => formatGroupMenu(group))
+ // remove invalid
+ .filter(group => group !== null)
+ return formatted as NonNullable<ReturnType<typeof formatGroupMenu>>[]
+ })
+
+ /**
+ * The admin group if found otherwise null
+ */
+ const adminGroup = computed(() => formatGroupMenu(groups.value.find(group => group.id === 'admin')))
+
+ /**
+ * The group of disabled users
+ */
+ const disabledGroup = computed(() => formatGroupMenu(groups.value.find(group => group.id === 'disabled')))
+
+ return { adminGroup, disabledGroup, userGroups }
+}
diff --git a/apps/settings/src/main-apps-users-management.ts b/apps/settings/src/main-apps-users-management.ts
index 37d7e9ba821..08f94695355 100644
--- a/apps/settings/src/main-apps-users-management.ts
+++ b/apps/settings/src/main-apps-users-management.ts
@@ -29,12 +29,13 @@ import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import SettingsApp from './views/SettingsApp.vue'
import router from './router/index.ts'
-import store from './store/index.js'
+import { useStore } from './store/index.js'
import { getRequestToken } from '@nextcloud/auth'
import { PiniaVuePlugin, createPinia } from 'pinia'
Vue.use(VTooltip, { defaultHtml: false })
+const store = useStore()
sync(store, router)
// CSP config for webpack dynamic chunk loading
@@ -44,10 +45,6 @@ __webpack_nonce__ = btoa(getRequestToken() ?? '')
// bind to window
Vue.prototype.t = t
Vue.prototype.n = n
-Vue.prototype.OC = window.OC
-Vue.prototype.OCA = window.OCA
-// @ts-expect-error This is a private property we use
-Vue.prototype.oc_userconfig = window.oc_userconfig
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
diff --git a/apps/settings/src/store/index.js b/apps/settings/src/store/index.js
index 7f477d3882d..809673ec575 100644
--- a/apps/settings/src/store/index.js
+++ b/apps/settings/src/store/index.js
@@ -45,14 +45,20 @@ const mutations = {
},
}
-export default new Store({
- modules: {
- users,
- apps,
- settings,
- oc,
- },
- strict: debug,
+let store = null
- mutations,
-})
+export const useStore = () => {
+ if (store === null) {
+ store = new Store({
+ modules: {
+ users,
+ apps,
+ settings,
+ oc,
+ },
+ strict: debug,
+ mutations,
+ })
+ }
+ return store
+}
diff --git a/apps/settings/src/views/UserManagement.vue b/apps/settings/src/views/UserManagement.vue
index d25071e63ed..64c5c826560 100644
--- a/apps/settings/src/views/UserManagement.vue
+++ b/apps/settings/src/views/UserManagement.vue
@@ -21,192 +21,31 @@
-->
<template>
- <Fragment>
- <NcAppNavigation :aria-label="t('settings', 'Account management')">
- <NcAppNavigationNew button-id="new-user-button"
- :text="t('settings','New account')"
- @click="showNewUserMenu"
- @keyup.enter="showNewUserMenu"
- @keyup.space="showNewUserMenu">
- <template #icon>
- <Plus :size="20" />
- </template>
- </NcAppNavigationNew>
-
- <NcAppNavigationList data-cy-users-settings-navigation-groups="system">
- <NcAppNavigationItem id="everyone"
- :exact="true"
- :name="t('settings', 'Active accounts')"
- :to="{ name: 'users' }">
- <template #icon>
- <AccountGroup :size="20" />
- </template>
- <template #counter>
- <NcCounterBubble v-if="userCount" :type="!selectedGroupDecoded ? 'highlighted' : undefined">
- {{ userCount }}
- </NcCounterBubble>
- </template>
- </NcAppNavigationItem>
-
- <NcAppNavigationItem v-if="settings.isAdmin"
- id="admin"
- :exact="true"
- :name="t('settings', 'Admins')"
- :to="{ name: 'group', params: { selectedGroup: 'admin' } }">
- <template #icon>
- <ShieldAccount :size="20" />
- </template>
- <template v-if="adminGroupMenu.count > 0" #counter>
- <NcCounterBubble :type="selectedGroupDecoded === 'admin' ? 'highlighted' : undefined">
- {{ adminGroupMenu.count }}
- </NcCounterBubble>
- </template>
- </NcAppNavigationItem>
-
- <!-- Hide the disabled if none, if we don't have the data (-1) show it -->
- <NcAppNavigationItem v-if="disabledGroupMenu.usercount > 0 || disabledGroupMenu.usercount === -1"
- id="disabled"
- :exact="true"
- :name="t('settings', 'Disabled users')"
- :to="{ name: 'group', params: { selectedGroup: 'disabled' } }">
- <template #icon>
- <AccountOff :size="20" />
- </template>
- <template v-if="disabledGroupMenu.usercount > 0" #counter>
- <NcCounterBubble :type="selectedGroupDecoded === 'disabled' ? 'highlighted' : undefined">
- {{ disabledGroupMenu.usercount }}
- </NcCounterBubble>
- </template>
- </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 #actionsTriggerIcon>
- <NcLoadingIcon v-if="loadingAddGroup" />
- <Plus v-else :size="20" />
- </template>
- <template #actions>
- <NcActionText>
- <template #icon>
- <AccountGroup :size="20" />
- </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 data-cy-users-settings-navigation-groups="custom">
- <GroupListItem v-for="group in groupList"
- :id="group.id"
- :key="group.id"
- :active="selectedGroupDecoded === group.id"
- :name="group.title"
- :count="group.count" />
- </NcAppNavigationList>
-
- <template #footer>
- <ul class="app-navigation-entry__settings">
- <NcAppNavigationItem :name="t('settings', 'Account management settings')"
- @click="isDialogOpen = true">
- <template #icon>
- <Cog :size="20" />
- </template>
- </NcAppNavigationItem>
- </ul>
- </template>
- </NcAppNavigation>
-
- <NcAppContent :page-heading="pageHeading">
- <UserList :selected-group="selectedGroupDecoded"
- :external-actions="externalActions" />
- </NcAppContent>
-
- <UserSettingsDialog :open.sync="isDialogOpen" />
- </Fragment>
+ <NcAppContent :page-heading="pageHeading">
+ <UserList :selected-group="selectedGroupDecoded"
+ :external-actions="externalActions" />
+ </NcAppContent>
</template>
<script>
-import Vue from 'vue'
-import VueLocalStorage from 'vue-localstorage'
-import { Fragment } from 'vue-frag'
import { translate as t } from '@nextcloud/l10n'
-import { showError } from '@nextcloud/dialogs'
+import { defineComponent } from 'vue'
-import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
-import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
-import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
-import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
-import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
-import NcAppNavigationList from '@nextcloud/vue/dist/Components/NcAppNavigationList.js'
-import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
-import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-
-import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
-import AccountOff from 'vue-material-design-icons/AccountOff.vue'
-import Cog from 'vue-material-design-icons/Cog.vue'
-import Plus from 'vue-material-design-icons/Plus.vue'
-
-import GroupListItem from '../components/GroupListItem.vue'
import UserList from '../components/UserList.vue'
-import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue'
-Vue.use(VueLocalStorage)
-
-export default {
+export default defineComponent({
name: 'UserManagement',
components: {
- AccountGroup,
- AccountOff,
- Cog,
- Fragment,
- GroupListItem,
- NcActionInput,
- NcActionText,
NcAppContent,
- NcAppNavigation,
- NcAppNavigationCaption,
- NcAppNavigationItem,
- NcAppNavigationList,
- NcAppNavigationNew,
- NcCounterBubble,
- NcLoadingIcon,
- Plus,
UserList,
- UserSettingsDialog,
- },
-
- props: {
- selectedGroup: {
- type: String,
- default: null,
- },
},
data() {
return {
// temporary value used for multiselect change
externalActions: [],
- newGroupName: '',
- isAddGroupOpen: false,
- loadingAddGroup: false,
- hasAddGroupError: false,
- isDialogOpen: false,
}
},
@@ -222,54 +61,13 @@ export default {
return matchHeading[this.selectedGroupDecoded] ?? t('settings', 'Account group: {group}', { group: this.selectedGroupDecoded })
},
- showConfig() {
- return this.$store.getters.getShowConfig
+ selectedGroup() {
+ return this.$route.params.selectedGroup
},
selectedGroupDecoded() {
return this.selectedGroup ? decodeURIComponent(this.selectedGroup) : null
},
-
- users() {
- return this.$store.getters.getUsers
- },
-
- groups() {
- return this.$store.getters.getGroups
- },
-
- usersOffset() {
- return this.$store.getters.getUsersOffset
- },
-
- usersLimit() {
- return this.$store.getters.getUsersLimit
- },
-
- userCount() {
- return this.$store.getters.getUserCount
- },
-
- settings() {
- return this.$store.getters.getServerData
- },
-
- groupList() {
- const groups = Array.isArray(this.groups) ? this.groups : []
-
- return groups
- // filter out disabled and admin
- .filter(group => group.id !== 'disabled' && group.id !== 'admin')
- .map(group => this.formatGroupMenu(group))
- },
-
- adminGroupMenu() {
- return this.formatGroupMenu(this.groups.find(group => group.id === 'admin'))
- },
-
- disabledGroupMenu() {
- return this.formatGroupMenu(this.groups.find(group => group.id === 'disabled'))
- },
},
beforeMount() {
@@ -283,26 +81,16 @@ export default {
created() {
// init the OCA.Settings.UserList object
+ window.OCA = window.OCA ?? {}
+ window.OCA.Settings = window.OCA.Settings ?? {}
+ window.OCA.Settings.UserList = window.OCA.Settings.UserList ?? {}
// and add the registerAction method
- Object.assign(OCA, {
- Settings: {
- UserList: {
- registerAction: this.registerAction,
- },
- },
- })
+ window.OCA.Settings.UserList.registerAction = this.registerAction
},
methods: {
t,
- showNewUserMenu() {
- this.$store.commit('setShowConfig', {
- key: 'showNewUserForm',
- value: true,
- })
- },
-
/**
* Register a new action for the user menu
*
@@ -319,60 +107,8 @@ export default {
})
return this.externalActions
},
-
- /**
- * Create a new group
- */
- async createGroup() {
- this.hasAddGroupError = false
- const groupId = this.newGroupName.trim()
- if (groupId === '') {
- this.hasAddGroupError = true
- return
- }
-
- this.isAddGroupOpen = false
- this.loadingAddGroup = true
- try {
- await this.$store.dispatch('addGroup', groupId)
- await this.$router.push({
- name: 'group',
- params: {
- selectedGroup: encodeURIComponent(groupId),
- },
- })
- this.newGroupName = ''
- } catch {
- showError(t('settings', 'Failed to create group'))
- }
- this.loadingAddGroup = false
- },
-
- /**
- * Format a group to a menu entry
- *
- * @param {object} group the group
- * @return {object}
- */
- formatGroupMenu(group) {
- const item = {}
- if (typeof group === 'undefined') {
- return {}
- }
-
- item.id = group.id
- item.title = group.name
- item.usercount = group.usercount
-
- // users count for all groups
- if (group.usercount - group.disabled > 0) {
- item.count = group.usercount - group.disabled
- }
-
- return item
- },
},
-}
+})
</script>
<style lang="scss" scoped>
@@ -383,10 +119,4 @@ export default {
flex-direction: column;
max-height: 100%;
}
-
-.app-navigation-entry__settings {
- height: auto !important;
- // Prevent shrinking or growing
- flex: 0 0 auto;
-}
</style>
diff --git a/apps/settings/src/views/UserManagementNavigation.vue b/apps/settings/src/views/UserManagementNavigation.vue
index a32313f8edf..4959040a1bf 100644
--- a/apps/settings/src/views/UserManagementNavigation.vue
+++ b/apps/settings/src/views/UserManagementNavigation.vue
@@ -1,5 +1,222 @@
<template>
- <div>...</div>
+ <NcAppNavigation :aria-label="t('settings', 'Account management')">
+ <NcAppNavigationNew button-id="new-user-button"
+ :text="t('settings','New account')"
+ @click="showNewUserMenu"
+ @keyup.enter="showNewUserMenu"
+ @keyup.space="showNewUserMenu">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiPlus" />
+ </template>
+ </NcAppNavigationNew>
+
+ <NcAppNavigationList class="account-management__system-list"
+ data-cy-users-settings-navigation-groups="system">
+ <NcAppNavigationItem id="everyone"
+ :exact="true"
+ :name="t('settings', 'Active accounts')"
+ :to="{ name: 'users' }">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiAccount" />
+ </template>
+ <template #counter>
+ <NcCounterBubble v-if="userCount" :type="!selectedGroupDecoded ? 'highlighted' : undefined">
+ {{ userCount }}
+ </NcCounterBubble>
+ </template>
+ </NcAppNavigationItem>
+
+ <NcAppNavigationItem v-if="isAdmin"
+ id="admin"
+ :exact="true"
+ :name="t('settings', 'Admins')"
+ :to="{ name: 'group', params: { selectedGroup: 'admin' } }">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiShieldAccount" />
+ </template>
+ <template #counter>
+ <NcCounterBubble v-if="adminGroup && adminGroup.count > 0"
+ :type="selectedGroupDecoded === 'admin' ? 'highlighted' : undefined">
+ {{ adminGroup.count }}
+ </NcCounterBubble>
+ </template>
+ </NcAppNavigationItem>
+
+ <!-- Hide the disabled if none, if we don't have the data (-1) show it -->
+ <NcAppNavigationItem v-if="disabledGroup && (disabledGroup.usercount > 0 || disabledGroup.usercount === -1)"
+ id="disabled"
+ :exact="true"
+ :name="t('settings', 'Disabled accounts')"
+ :to="{ name: 'group', params: { selectedGroup: 'disabled' } }">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiAccountOff" />
+ </template>
+ <template v-if="disabledGroup.usercount > 0" #counter>
+ <NcCounterBubble :type="selectedGroupDecoded === 'disabled' ? 'highlighted' : undefined">
+ {{ disabledGroup.usercount }}
+ </NcCounterBubble>
+ </template>
+ </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 #actionsTriggerIcon>
+ <NcLoadingIcon v-if="loadingAddGroup" />
+ <NcIconSvgWrapper v-else :path="mdiPlus" />
+ </template>
+ <template #actions>
+ <NcActionText>
+ <template #icon>
+ <AccountGroup :size="20" />
+ </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>
+
+ <template #footer>
+ <NcButton class="account-management__settings-toggle"
+ type="tertiary"
+ @click="isDialogOpen = true">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiCog" />
+ </template>
+ {{ t('settings', 'Account management settings') }}
+ </NcButton>
+ <UserSettingsDialog :open.sync="isDialogOpen" />
+ </template>
+ </NcAppNavigation>
</template>
-<script setup>
+
+<script setup lang="ts">
+import { mdiAccount, mdiAccountOff, mdiCog, mdiPlus, mdiShieldAccount } from '@mdi/js'
+import { showError } from '@nextcloud/dialogs'
+import { translate as t } from '@nextcloud/l10n'
+import { computed, ref } from 'vue'
+
+import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
+import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js'
+import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
+import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
+import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
+import NcAppNavigationList from '@nextcloud/vue/dist/Components/NcAppNavigationList.js'
+import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
+import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+
+import GroupListItem from '../components/GroupListItem.vue'
+import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue'
+import { useStore } from '../store'
+import { useRoute, useRouter } from 'vue-router/composables'
+import { useFormatGroups } from '../composables/useGroupsNavigation'
+
+const route = useRoute()
+const router = useRouter()
+const store = useStore()
+
+/** State of the 'new-account' dialog */
+const isDialogOpen = ref(false)
+
+/** 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)
+
+/** Overall user count */
+const userCount = computed(() => store.getters.getUserCount)
+/** All available groups */
+const groups = computed(() => store.getters.getGroups)
+const { adminGroup, disabledGroup, userGroups } = useFormatGroups(groups)
+
+/** True if the current user is an administrator */
+const isAdmin = computed(() => store.getters.getServerData.isAdmin)
+
+/** 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
+ */
+function showNewUserMenu() {
+ store.commit('setShowConfig', {
+ key: 'showNewUserForm',
+ value: true,
+ })
+}
</script>
+
+<style scoped lang="scss">
+.account-management{
+ &__system-list {
+ height: auto !important;
+ overflow: visible !important;
+ }
+
+ &__group-list {
+ height: 100% !important;
+ }
+
+ &__settings-toggle {
+ margin-bottom: 12px;
+ }
+}
+</style>
diff --git a/apps/settings/src/views/user-types.d.ts b/apps/settings/src/views/user-types.d.ts
new file mode 100644
index 00000000000..790a9c5b1ae
--- /dev/null
+++ b/apps/settings/src/views/user-types.d.ts
@@ -0,0 +1,14 @@
+export interface IGroup {
+ id: string
+ name: string
+
+ /**
+ * Overall user count
+ */
+ usercount: number
+
+ /**
+ * Number of disabled users
+ */
+ disabled: number
+}
diff --git a/cypress/e2e/settings/users_columns.cy.ts b/cypress/e2e/settings/users_columns.cy.ts
index 363e0628508..5f2a293b824 100644
--- a/cypress/e2e/settings/users_columns.cy.ts
+++ b/cypress/e2e/settings/users_columns.cy.ts
@@ -34,7 +34,7 @@ describe('Settings: Show and hide columns', function() {
beforeEach(function() {
// open the settings dialog
- cy.get('.app-navigation-entry__settings').contains('Account management settings').click()
+ cy.contains('button', 'Account management settings').click()
// reset all visibility toggles
cy.get('.modal-container #settings-section_visibility-settings input[type="checkbox"]').uncheck({ force: true })
@@ -57,7 +57,7 @@ describe('Settings: Show and hide columns', function() {
})
// open the settings dialog
- cy.get('.app-navigation-entry__settings').contains('Account management settings').click()
+ cy.contains('button', 'Account management settings').click()
cy.contains('.modal-container', 'Account management settings').within(() => {
// enable the language toggle
@@ -88,7 +88,7 @@ describe('Settings: Show and hide columns', function() {
})
// open the settings dialog
- cy.get('.app-navigation-entry__settings').contains('Account management settings').click()
+ cy.contains('button', 'Account management settings').click()
cy.contains('.modal-container', 'Account management settings').within(() => {
// disable the last login toggle