mirror of
https://github.com/nextcloud/server.git
synced 2026-04-26 08:38:11 -04:00
feat(settings): Split account management into navigation and content
The should ease the maintenance of it due to reduced complexity. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
1100e908b7
commit
30d5b02811
9 changed files with 355 additions and 332 deletions
|
|
@ -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 {
|
|||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
// non reactive properties
|
||||
return {
|
||||
mdiAccountGroup,
|
||||
rowHeight: 55,
|
||||
|
||||
UserRow,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
UserRow,
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
52
apps/settings/src/composables/useGroupsNavigation.ts
Normal file
52
apps/settings/src/composables/useGroupsNavigation.ts
Normal file
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
14
apps/settings/src/views/user-types.d.ts
vendored
Normal file
14
apps/settings/src/views/user-types.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export interface IGroup {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
/**
|
||||
* Overall user count
|
||||
*/
|
||||
usercount: number
|
||||
|
||||
/**
|
||||
* Number of disabled users
|
||||
*/
|
||||
disabled: number
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue