diff options
-rw-r--r-- | apps/settings/src/components/UserList.vue | 34 | ||||
-rw-r--r-- | apps/settings/src/components/Users/UserSettingsDialog.vue | 31 | ||||
-rw-r--r-- | apps/settings/src/composables/useGroupsNavigation.ts | 52 | ||||
-rw-r--r-- | apps/settings/src/main-apps-users-management.ts | 7 | ||||
-rw-r--r-- | apps/settings/src/store/index.js | 26 | ||||
-rw-r--r-- | apps/settings/src/views/UserManagement.vue | 296 | ||||
-rw-r--r-- | apps/settings/src/views/UserManagementNavigation.vue | 221 | ||||
-rw-r--r-- | apps/settings/src/views/user-types.d.ts | 14 | ||||
-rw-r--r-- | cypress/e2e/settings/users_columns.cy.ts | 6 |
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 |