The should ease the maintenance of it due to reduced complexity. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>tags/v29.0.0beta2
@@ -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() |
@@ -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> |
@@ -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> |
@@ -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 |