diff options
Diffstat (limited to 'apps/settings/src/views')
-rw-r--r-- | apps/settings/src/views/AdminSettingsSharing.vue | 44 | ||||
-rw-r--r-- | apps/settings/src/views/AppStore.vue | 88 | ||||
-rw-r--r-- | apps/settings/src/views/AppStoreNavigation.vue | 146 | ||||
-rw-r--r-- | apps/settings/src/views/AppStoreSidebar.vue | 159 | ||||
-rw-r--r-- | apps/settings/src/views/Apps.vue | 388 | ||||
-rw-r--r-- | apps/settings/src/views/SettingsApp.vue | 16 | ||||
-rw-r--r-- | apps/settings/src/views/UserManagement.vue | 104 | ||||
-rw-r--r-- | apps/settings/src/views/UserManagementNavigation.vue | 172 | ||||
-rw-r--r-- | apps/settings/src/views/Users.vue | 485 | ||||
-rw-r--r-- | apps/settings/src/views/user-types.d.ts | 35 |
10 files changed, 764 insertions, 873 deletions
diff --git a/apps/settings/src/views/AdminSettingsSharing.vue b/apps/settings/src/views/AdminSettingsSharing.vue new file mode 100644 index 00000000000..d26fba6c8fa --- /dev/null +++ b/apps/settings/src/views/AdminSettingsSharing.vue @@ -0,0 +1,44 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcSettingsSection data-cy-settings-sharing-section + :limit-width="true" + :doc-url="documentationLink" + :name="t('settings', 'Sharing')" + :description="t('settings', 'As admin you can fine-tune the sharing behavior. Please see the documentation for more information.')"> + <NcNoteCard v-if="!sharingAppEnabled" type="warning"> + {{ t('settings', 'You need to enable the File sharing App.') }} + </NcNoteCard> + <AdminSettingsSharingForm v-else /> + </NcSettingsSection> +</template> + +<script lang="ts"> +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { defineComponent } from 'vue' + +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' +import AdminSettingsSharingForm from '../components/AdminSettingsSharingForm.vue' + +export default defineComponent({ + name: 'AdminSettingsSharing', + components: { + AdminSettingsSharingForm, + NcNoteCard, + NcSettingsSection, + }, + data() { + return { + documentationLink: loadState<string>('settings', 'sharingDocumentation', ''), + sharingAppEnabled: loadState<boolean>('settings', 'sharingAppEnabled', false), + } + }, + methods: { + t, + }, +}) +</script> diff --git a/apps/settings/src/views/AppStore.vue b/apps/settings/src/views/AppStore.vue new file mode 100644 index 00000000000..82c8c31e75d --- /dev/null +++ b/apps/settings/src/views/AppStore.vue @@ -0,0 +1,88 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <!-- Apps list --> + <NcAppContent class="app-settings-content" + :page-heading="appStoreLabel"> + <h2 class="app-settings-content__label" v-text="viewLabel" /> + + <AppStoreDiscoverSection v-if="currentCategory === 'discover'" /> + <NcEmptyContent v-else-if="isLoading" + class="empty-content__loading" + :name="t('settings', 'Loading app list')"> + <template #icon> + <NcLoadingIcon :size="64" /> + </template> + </NcEmptyContent> + <AppList v-else :category="currentCategory" /> + </NcAppContent> +</template> + +<script setup lang="ts"> +import { translate as t } from '@nextcloud/l10n' +import { computed, getCurrentInstance, onBeforeMount, onBeforeUnmount, watchEffect } from 'vue' +import { useRoute } from 'vue-router/composables' + +import { useAppsStore } from '../store/apps-store' +import { APPS_SECTION_ENUM } from '../constants/AppsConstants' + +import NcAppContent from '@nextcloud/vue/components/NcAppContent' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import AppList from '../components/AppList.vue' +import AppStoreDiscoverSection from '../components/AppStoreDiscover/AppStoreDiscoverSection.vue' +import { useAppApiStore } from '../store/app-api-store.ts' + +const route = useRoute() +const store = useAppsStore() +const appApiStore = useAppApiStore() + +/** + * ID of the current active category, default is `discover` + */ +const currentCategory = computed(() => route.params?.category ?? 'discover') + +const appStoreLabel = t('settings', 'App Store') +const viewLabel = computed(() => APPS_SECTION_ENUM[currentCategory.value] ?? store.getCategoryById(currentCategory.value)?.displayName ?? appStoreLabel) + +watchEffect(() => { + window.document.title = `${viewLabel.value} - ${appStoreLabel} - Nextcloud` +}) + +// TODO this part should be migrated to pinia +const instance = getCurrentInstance() +/** Is the app list loading */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isLoading = computed(() => (instance?.proxy as any).$store.getters.loading('list')) +onBeforeMount(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (instance?.proxy as any).$store.dispatch('getCategories', { shouldRefetchCategories: true }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (instance?.proxy as any).$store.dispatch('getAllApps') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((instance?.proxy as any).$store.getters.isAppApiEnabled) { + appApiStore.fetchAllApps() + appApiStore.updateAppsStatus() + } +}) +onBeforeUnmount(() => { + clearInterval(appApiStore.getStatusUpdater) +}) +</script> + +<style scoped> +.empty-content__loading { + height: 100%; +} + +.app-settings-content__label { + margin-block-start: var(--app-navigation-padding); + margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2); + min-height: var(--default-clickable-area); + line-height: var(--default-clickable-area); + vertical-align: center; +} +</style> diff --git a/apps/settings/src/views/AppStoreNavigation.vue b/apps/settings/src/views/AppStoreNavigation.vue new file mode 100644 index 00000000000..83191baac40 --- /dev/null +++ b/apps/settings/src/views/AppStoreNavigation.vue @@ -0,0 +1,146 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <!-- Categories & filters --> + <NcAppNavigation :aria-label="t('settings', 'Apps')"> + <template #list> + <NcAppNavigationItem v-if="appstoreEnabled" + id="app-category-discover" + :to="{ name: 'apps-category', params: { category: 'discover'} }" + :name="APPS_SECTION_ENUM.discover"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.discover" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem id="app-category-installed" + :to="{ name: 'apps-category', params: { category: 'installed'} }" + :name="APPS_SECTION_ENUM.installed"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.installed" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem id="app-category-enabled" + :to="{ name: 'apps-category', params: { category: 'enabled' } }" + :name="APPS_SECTION_ENUM.enabled"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.enabled" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem id="app-category-disabled" + :to="{ name: 'apps-category', params: { category: 'disabled' } }" + :name="APPS_SECTION_ENUM.disabled"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.disabled" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem v-if="store.updateCount > 0" + id="app-category-updates" + :to="{ name: 'apps-category', params: { category: 'updates' } }" + :name="APPS_SECTION_ENUM.updates"> + <template #counter> + <NcCounterBubble>{{ store.updateCount }}</NcCounterBubble> + </template> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.updates" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem id="app-category-your-bundles" + :to="{ name: 'apps-category', params: { category: 'app-bundles' } }" + :name="APPS_SECTION_ENUM['app-bundles']"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.bundles" /> + </template> + </NcAppNavigationItem> + + <NcAppNavigationSpacer /> + + <!-- App store categories --> + <li v-if="appstoreEnabled && categoriesLoading" class="categories--loading"> + <NcLoadingIcon :size="20" :aria-label="t('settings', 'Loading categories')" /> + </li> + <template v-else-if="appstoreEnabled && !categoriesLoading"> + <NcAppNavigationItem v-if="isSubscribed" + id="app-category-supported" + :to="{ name: 'apps-category', params: { category: 'supported' } }" + :name="APPS_SECTION_ENUM.supported"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.supported" /> + </template> + </NcAppNavigationItem> + <NcAppNavigationItem id="app-category-featured" + :to="{ name: 'apps-category', params: { category: 'featured' } }" + :name="APPS_SECTION_ENUM.featured"> + <template #icon> + <NcIconSvgWrapper :path="APPSTORE_CATEGORY_ICONS.featured" /> + </template> + </NcAppNavigationItem> + + <NcAppNavigationItem v-for="category in categories" + :id="`app-category-${category.id}`" + :key="category.id" + :name="category.displayName" + :to="{ + name: 'apps-category', + params: { category: category.id }, + }"> + <template #icon> + <NcIconSvgWrapper :path="category.icon" /> + </template> + </NcAppNavigationItem> + </template> + + <NcAppNavigationItem id="app-developer-docs" + :name="t('settings', 'Developer documentation ↗')" + :href="developerDocsUrl" /> + </template> + </NcAppNavigation> +</template> + +<script setup lang="ts"> +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { computed, onBeforeMount } from 'vue' +import { APPS_SECTION_ENUM } from '../constants/AppsConstants' +import { useAppsStore } from '../store/apps-store' + +import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' +import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' +import NcAppNavigationSpacer from '@nextcloud/vue/components/NcAppNavigationSpacer' +import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' + +import APPSTORE_CATEGORY_ICONS from '../constants/AppstoreCategoryIcons.ts' + +const appstoreEnabled = loadState<boolean>('settings', 'appstoreEnabled', true) +const developerDocsUrl = loadState<string>('settings', 'appstoreDeveloperDocs', '') + +const store = useAppsStore() +const categories = computed(() => store.categories) +const categoriesLoading = computed(() => store.loading.categories) + +/** + * Check if the current instance has a support subscription from the Nextcloud GmbH + * + * For customers of the Nextcloud GmbH the app level will be set to `300` for apps that are supported in their subscription + */ +const isSubscribed = computed(() => store.apps.find(({ level }) => level === 300) !== undefined) + +// load categories when component is mounted +onBeforeMount(() => { + store.loadCategories() + store.loadApps() +}) +</script> + +<style scoped> +/* The categories-loading indicator */ +.categories--loading { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} +</style> diff --git a/apps/settings/src/views/AppStoreSidebar.vue b/apps/settings/src/views/AppStoreSidebar.vue new file mode 100644 index 00000000000..b4041066c67 --- /dev/null +++ b/apps/settings/src/views/AppStoreSidebar.vue @@ -0,0 +1,159 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <!-- Selected app details --> + <NcAppSidebar v-if="showSidebar" + class="app-sidebar" + :class="{ 'app-sidebar--with-screenshot': hasScreenshot }" + :active.sync="activeTab" + :background="hasScreenshot ? app.screenshot : undefined" + :compact="!hasScreenshot" + :name="app.name" + :title="app.name" + :subname="licenseText" + :subtitle="licenseText" + @close="hideAppDetails"> + <!-- Fallback icon incase no app icon is available --> + <template v-if="!hasScreenshot" #header> + <NcIconSvgWrapper class="app-sidebar__fallback-icon" + :svg="appIcon ?? ''" + :size="64" /> + </template> + + <template #description> + <!-- Featured/Supported badges --> + <div class="app-sidebar__badges"> + <AppLevelBadge :level="app.level" /> + <AppDaemonBadge v-if="app.app_api && app.daemon" :daemon="app.daemon" /> + <AppScore v-if="hasRating" :score="rating" /> + </div> + </template> + + <!-- Tab content --> + <AppDescriptionTab :app="app" /> + <AppDetailsTab :app="app" /> + <AppReleasesTab :app="app" /> + <AppDeployDaemonTab :app="app" /> + </NcAppSidebar> +</template> + +<script setup lang="ts"> +import { translate as t } from '@nextcloud/l10n' +import { computed, onMounted, ref, watch } from 'vue' +import { useRoute, useRouter } from 'vue-router/composables' +import { useAppsStore } from '../store/apps-store' + +import NcAppSidebar from '@nextcloud/vue/components/NcAppSidebar' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import AppScore from '../components/AppList/AppScore.vue' +import AppDescriptionTab from '../components/AppStoreSidebar/AppDescriptionTab.vue' +import AppDetailsTab from '../components/AppStoreSidebar/AppDetailsTab.vue' +import AppReleasesTab from '../components/AppStoreSidebar/AppReleasesTab.vue' +import AppDeployDaemonTab from '../components/AppStoreSidebar/AppDeployDaemonTab.vue' +import AppLevelBadge from '../components/AppList/AppLevelBadge.vue' +import AppDaemonBadge from '../components/AppList/AppDaemonBadge.vue' +import { useAppIcon } from '../composables/useAppIcon.ts' +import { useStore } from '../store' +import { useAppApiStore } from '../store/app-api-store.ts' + +const route = useRoute() +const router = useRouter() +const store = useAppsStore() +const appApiStore = useAppApiStore() +const legacyStore = useStore() + +const appId = computed(() => route.params.id ?? '') +const app = computed(() => { + if (legacyStore.getters.isAppApiEnabled) { + const exApp = appApiStore.getAllApps + .find((app) => app.id === appId.value) ?? null + if (exApp) { + return exApp + } + } + return store.getAppById(appId.value)! +}) +const hasRating = computed(() => app.value.appstoreData?.ratingNumOverall > 5) +const rating = computed(() => app.value.appstoreData?.ratingNumRecent > 5 + ? app.value.appstoreData.ratingRecent + : (app.value.appstoreData?.ratingOverall ?? 0.5)) +const showSidebar = computed(() => app.value !== null) + +const { appIcon } = useAppIcon(app) + +/** + * The second text line shown on the sidebar + */ +const licenseText = computed(() => { + if (!app.value) { + return '' + } + if (app.value.license !== '') { + return t('settings', 'Version {version}, {license}-licensed', { version: app.value.version, license: app.value.licence.toString().toUpperCase() }) + } + return t('settings', 'Version {version}', { version: app.value.version }) +}) + +const activeTab = ref('details') +watch([app], () => { activeTab.value = 'details' }) + +/** + * Hide the details sidebar by pushing a new route + */ +const hideAppDetails = () => { + router.push({ + name: 'apps-category', + params: { category: route.params.category }, + }) +} + +/** + * Whether the app screenshot is loaded + */ +const screenshotLoaded = ref(false) +const hasScreenshot = computed(() => app.value?.screenshot && screenshotLoaded.value) +/** + * Preload the app screenshot + */ +const loadScreenshot = () => { + if (app.value?.releases && app.value?.screenshot) { + const image = new Image() + image.onload = () => { + screenshotLoaded.value = true + } + image.src = app.value.screenshot + } +} +// Watch app and set screenshot loaded when +watch([app], loadScreenshot) +onMounted(loadScreenshot) +</script> + +<style scoped lang="scss"> +.app-sidebar { + // If a screenshot is available it should cover the whole figure + &--with-screenshot { + :deep(.app-sidebar-header__figure) { + background-size: cover; + } + } + + &__fallback-icon { + // both 100% to center the icon + width: 100%; + height: 100%; + } + + &__badges { + display: flex; + flex-direction: row; + gap: 12px; + } + + &__version { + color: var(--color-text-maxcontrast); + } +} +</style> diff --git a/apps/settings/src/views/Apps.vue b/apps/settings/src/views/Apps.vue deleted file mode 100644 index ee7f74c7877..00000000000 --- a/apps/settings/src/views/Apps.vue +++ /dev/null @@ -1,388 +0,0 @@ -<!-- - - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> - - - - @author Julius Härtl <jus@bitgrid.net> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> - -<template> - <Content app-name="settings" - :class="{ 'with-app-sidebar': app}" - :content-class="{ 'icon-loading': loadingList }" - :navigation-class="{ 'icon-loading': loading }"> - <!-- Categories & filters --> - <AppNavigation> - <template #list> - <AppNavigationItem id="app-category-your-apps" - :to="{ name: 'apps' }" - :exact="true" - icon="icon-category-installed" - :title="t('settings', 'Your apps')" /> - <AppNavigationItem id="app-category-enabled" - :to="{ name: 'apps-category', params: { category: 'enabled' } }" - icon="icon-category-enabled" - :title="t('settings', 'Active apps')" /> - <AppNavigationItem id="app-category-disabled" - :to="{ name: 'apps-category', params: { category: 'disabled' } }" - icon="icon-category-disabled" - :title="t('settings', 'Disabled apps')" /> - <AppNavigationItem v-if="updateCount > 0" - id="app-category-updates" - :to="{ name: 'apps-category', params: { category: 'updates' } }" - icon="icon-download" - :title="t('settings', 'Updates')"> - <AppNavigationCounter slot="counter"> - {{ updateCount }} - </AppNavigationCounter> - </AppNavigationItem> - <AppNavigationItem id="app-category-your-bundles" - :to="{ name: 'apps-category', params: { category: 'app-bundles' } }" - icon="icon-category-app-bundles" - :title="t('settings', 'App bundles')" /> - - <AppNavigationSpacer /> - - <!-- App store categories --> - <template v-if="settings.appstoreEnabled"> - <AppNavigationItem id="app-category-featured" - :to="{ name: 'apps-category', params: { category: 'featured' } }" - icon="icon-favorite" - :title="t('settings', 'Featured apps')" /> - - <AppNavigationItem v-for="cat in categories" - :key="'icon-category-' + cat.ident" - :icon="'icon-category-' + cat.ident" - :to="{ - name: 'apps-category', - params: { category: cat.ident }, - }" - :title="cat.displayName" /> - </template> - - <AppNavigationItem id="app-developer-docs" - :title="t('settings', 'Developer documentation') + ' ↗'" - @click="openDeveloperDocumentation" /> - </template> - </AppNavigation> - - <!-- Apps list --> - <AppContent class="app-settings-content" :class="{ 'icon-loading': loadingList }"> - <AppList :category="category" :app="app" :search="searchQuery" /> - </AppContent> - - <!-- Selected app details --> - <AppSidebar v-if="id && app" - v-bind="appSidebar" - :class="{'app-sidebar--without-background': !appSidebar.background}" - @close="hideAppDetails"> - <template v-if="!appSidebar.background" #header> - <div class="app-sidebar-header__figure--default-app-icon icon-settings-dark" /> - </template> - - <template #description> - <!-- Featured/Supported badges --> - <div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level"> - <span v-if="app.level === 300" - v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')" - class="supported icon-checkmark-color"> - {{ t('settings', 'Supported') }}</span> - <span v-if="app.level === 200" - v-tooltip.auto="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')" - class="official icon-checkmark"> - {{ t('settings', 'Featured') }}</span> - <AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" /> - </div> - <div class="app-version"> - <p>{{ app.version }}</p> - </div> - </template> - - <!-- Tab content --> - - <AppSidebarTab id="desc" - icon="icon-category-office" - :name="t('settings', 'Details')" - :order="0"> - <AppDetails :app="app" /> - </AppSidebarTab> - <AppSidebarTab v-if="app.appstoreData && app.releases[0].translations.en.changelog" - id="desca" - icon="icon-category-organization" - :name="t('settings', 'Changelog')" - :order="1"> - <div v-for="release in app.releases" :key="release.version" class="app-sidebar-tabs__release"> - <h2>{{ release.version }}</h2> - <Markdown v-if="changelog(release)" :text="changelog(release)" /> - </div> - </AppSidebarTab> - </AppSidebar> - </Content> -</template> - -<script> -import { subscribe, unsubscribe } from '@nextcloud/event-bus' -import Vue from 'vue' -import VueLocalStorage from 'vue-localstorage' - -import AppContent from '@nextcloud/vue/dist/Components/AppContent' -import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' -import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter' -import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem' -import AppNavigationSpacer from '@nextcloud/vue/dist/Components/AppNavigationSpacer' -import AppSidebar from '@nextcloud/vue/dist/Components/AppSidebar' -import AppSidebarTab from '@nextcloud/vue/dist/Components/AppSidebarTab' -import Content from '@nextcloud/vue/dist/Components/Content' - -import AppList from '../components/AppList' -import AppDetails from '../components/AppDetails' -import AppManagement from '../mixins/AppManagement' -import AppScore from '../components/AppList/AppScore' -import Markdown from '../components/Markdown' - -Vue.use(VueLocalStorage) - -export default { - name: 'Apps', - - components: { - AppContent, - AppDetails, - AppList, - AppNavigation, - AppNavigationCounter, - AppNavigationItem, - AppNavigationSpacer, - AppScore, - AppSidebar, - AppSidebarTab, - Content, - Markdown, - }, - - mixins: [AppManagement], - - props: { - category: { - type: String, - default: 'installed', - }, - id: { - type: String, - default: '', - }, - }, - - data() { - return { - searchQuery: '', - screenshotLoaded: false, - } - }, - - computed: { - loading() { - return this.$store.getters.loading('categories') - }, - loadingList() { - return this.$store.getters.loading('list') - }, - app() { - return this.apps.find(app => app.id === this.id) - }, - categories() { - return this.$store.getters.getCategories - }, - apps() { - return this.$store.getters.getAllApps - }, - updateCount() { - return this.$store.getters.getUpdateCount - }, - settings() { - return this.$store.getters.getServerData - }, - - hasRating() { - return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5 - }, - - // sidebar app binding - appSidebar() { - const authorName = (xmlNode) => { - if (xmlNode['@value']) { - // Complex node (with email or homepage attribute) - return xmlNode['@value'] - } - - // Simple text node - return xmlNode - } - - const author = Array.isArray(this.app.author) - ? this.app.author.map(authorName).join(', ') - : authorName(this.app.author) - const license = t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() }) - - const subtitle = t('settings', 'by {author}\n{license}', { author, license }) - - return { - subtitle, - background: this.app.screenshot && this.screenshotLoaded - ? this.app.screenshot - : this.app.preview, - compact: !(this.app.screenshot && this.screenshotLoaded), - title: this.app.name, - - } - }, - changelog() { - return (release) => release.translations.en.changelog - }, - }, - - watch: { - category() { - this.searchQuery = '' - }, - - app() { - this.screenshotLoaded = false - if (this.app?.releases && this.app?.screenshot) { - const image = new Image() - image.onload = (e) => { - this.screenshotLoaded = true - } - image.src = this.app.screenshot - } - }, - }, - - beforeMount() { - this.$store.dispatch('getCategories') - this.$store.dispatch('getAllApps') - this.$store.dispatch('getGroups', { offset: 0, limit: 5 }) - this.$store.commit('setUpdateCount', this.$store.getters.getServerData.updateCount) - }, - - mounted() { - subscribe('nextcloud:unified-search.search', this.setSearch) - subscribe('nextcloud:unified-search.reset', this.resetSearch) - }, - beforeDestroy() { - unsubscribe('nextcloud:unified-search.search', this.setSearch) - unsubscribe('nextcloud:unified-search.reset', this.resetSearch) - }, - - methods: { - setSearch({ query }) { - this.searchQuery = query - }, - resetSearch() { - this.searchQuery = '' - }, - - hideAppDetails() { - this.$router.push({ - name: 'apps-category', - params: { category: this.category }, - }) - }, - openDeveloperDocumentation() { - window.open(this.settings.developerDocumentation) - }, - }, -} -</script> - -<style lang="scss" scoped> -.app-sidebar::v-deep { - &:not(.app-sidebar--without-background) { - // with full screenshot, let's fill the figure - :not(.app-sidebar-header--compact) .app-sidebar-header__figure { - background-size: cover; - } - // revert sidebar app icon so it is black - .app-sidebar-header--compact .app-sidebar-header__figure { - background-size: 32px; - - filter: invert(1); - } - } - - .app-sidebar-header__description { - .app-version { - padding-left: 10px; - } - } - - // default icon slot styling - &.app-sidebar--without-background { - .app-sidebar-header__figure { - display: flex; - align-items: center; - justify-content: center; - &--default-app-icon { - width: 32px; - height: 32px; - background-size: 32px; - } - } - } - - // TODO: migrate to components - .app-sidebar-header__desc { - // allow multi line subtitle for the license - .app-sidebar-header__subtitle { - overflow: visible !important; - height: auto; - white-space: normal !important; - line-height: 16px; - } - } - - .app-sidebar-header__action { - // align with tab content - margin: 0 20px; - input { - margin: 3px; - } - } -} - -// Align the appNavigation toggle with the apps header toolbar -.app-navigation::v-deep button.app-navigation-toggle { - top: 8px; - right: -8px; -} - -.app-sidebar-tabs__release { - h2 { - border-bottom: 1px solid var(--color-border); - } - - // Overwrite changelog heading styles - ::v-deep { - h3 { - font-size: 20px; - } - h4 { - font-size: 17px; - } - } -} -</style> diff --git a/apps/settings/src/views/SettingsApp.vue b/apps/settings/src/views/SettingsApp.vue new file mode 100644 index 00000000000..7e135175ef6 --- /dev/null +++ b/apps/settings/src/views/SettingsApp.vue @@ -0,0 +1,16 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcContent app-name="settings"> + <router-view name="navigation" /> + <router-view /> + <router-view name="sidebar" /> + </NcContent> +</template> + +<script setup lang="ts"> +import NcContent from '@nextcloud/vue/components/NcContent' +</script> diff --git a/apps/settings/src/views/UserManagement.vue b/apps/settings/src/views/UserManagement.vue new file mode 100644 index 00000000000..9ab76f921a0 --- /dev/null +++ b/apps/settings/src/views/UserManagement.vue @@ -0,0 +1,104 @@ +<!-- + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <NcAppContent :page-heading="pageHeading"> + <UserList :selected-group="selectedGroupDecoded" + :external-actions="externalActions" /> + </NcAppContent> +</template> + +<script> +import { translate as t } from '@nextcloud/l10n' +import { emit } from '@nextcloud/event-bus' +import { defineComponent } from 'vue' + +import NcAppContent from '@nextcloud/vue/components/NcAppContent' +import UserList from '../components/UserList.vue' + +export default defineComponent({ + name: 'UserManagement', + + components: { + NcAppContent, + UserList, + }, + + data() { + return { + // temporary value used for multiselect change + externalActions: [], + } + }, + + computed: { + pageHeading() { + if (this.selectedGroupDecoded === null) { + return t('settings', 'All accounts') + } + const matchHeading = { + admin: t('settings', 'Admins'), + disabled: t('settings', 'Disabled accounts'), + } + return matchHeading[this.selectedGroupDecoded] ?? t('settings', 'Account group: {group}', { group: this.selectedGroupDecoded }) + }, + + selectedGroup() { + return this.$route.params.selectedGroup + }, + + selectedGroupDecoded() { + return this.selectedGroup ? decodeURIComponent(this.selectedGroup) : null + }, + }, + + beforeMount() { + this.$store.dispatch('getPasswordPolicyMinLength') + }, + + 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 + window.OCA.Settings.UserList.registerAction = this.registerAction + emit('settings:user-management:loaded') + }, + + methods: { + t, + + /** + * Register a new action for the user menu + * + * @param {string} icon the icon class + * @param {string} text the text to display + * @param {Function} action the function to run + * @param {(user: Record<string, unknown>) => boolean} enabled return true if the action is enabled for the user + * @return {Array} + */ + registerAction(icon, text, action, enabled) { + this.externalActions.push({ + icon, + text, + action, + enabled, + }) + return this.externalActions + }, + }, +}) +</script> + +<style lang="scss" scoped> +.app-content { + // Virtual list needs to be full height and is scrollable + display: flex; + overflow: hidden; + flex-direction: column; + max-height: 100%; +} +</style> diff --git a/apps/settings/src/views/UserManagementNavigation.vue b/apps/settings/src/views/UserManagementNavigation.vue new file mode 100644 index 00000000000..95a12ac7c51 --- /dev/null +++ b/apps/settings/src/views/UserManagementNavigation.vue @@ -0,0 +1,172 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcAppNavigation class="account-management__navigation" + :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', 'All accounts')" + :to="{ name: 'users' }"> + <template #icon> + <NcIconSvgWrapper :path="mdiAccountOutline" /> + </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> + <NcIconSvgWrapper :path="mdiShieldAccountOutline" /> + </template> + <template #counter> + <NcCounterBubble v-if="adminGroup && adminGroup.count > 0" + :type="selectedGroupDecoded === 'admin' ? 'highlighted' : undefined"> + {{ adminGroup.count }} + </NcCounterBubble> + </template> + </NcAppNavigationItem> + + <NcAppNavigationItem v-if="isAdminOrDelegatedAdmin" + id="recent" + :exact="true" + :name="t('settings', 'Recently active')" + :to="{ name: 'group', params: { selectedGroup: '__nc_internal_recent' } }"> + <template #icon> + <NcIconSvgWrapper :path="mdiHistory" /> + </template> + <template #counter> + <NcCounterBubble v-if="recentGroup?.usercount" + :type="selectedGroupDecoded === '__nc_internal_recent' ? 'highlighted' : undefined"> + {{ recentGroup.usercount }} + </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="mdiAccountOffOutline" /> + </template> + <template v-if="disabledGroup.usercount > 0" #counter> + <NcCounterBubble :type="selectedGroupDecoded === 'disabled' ? 'highlighted' : undefined"> + {{ disabledGroup.usercount }} + </NcCounterBubble> + </template> + </NcAppNavigationItem> + </NcAppNavigationList> + + <AppNavigationGroupList /> + + <template #footer> + <NcButton class="account-management__settings-toggle" + type="tertiary" + @click="isDialogOpen = true"> + <template #icon> + <NcIconSvgWrapper :path="mdiCogOutline" /> + </template> + {{ t('settings', 'Account management settings') }} + </NcButton> + <UserSettingsDialog :open.sync="isDialogOpen" /> + </template> + </NcAppNavigation> +</template> + +<script setup lang="ts"> +import { mdiAccountOutline, mdiAccountOffOutline, mdiCogOutline, mdiPlus, mdiShieldAccountOutline, mdiHistory } from '@mdi/js' +import { translate as t } from '@nextcloud/l10n' +import { computed, ref } from 'vue' + +import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation' +import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem' +import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList' +import NcAppNavigationNew from '@nextcloud/vue/components/NcAppNavigationNew' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' + +import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue' +import AppNavigationGroupList from '../components/AppNavigationGroupList.vue' + +import { useStore } from '../store' +import { useRoute } from 'vue-router/composables' +import { useFormatGroups } from '../composables/useGroupsNavigation' + +const route = useRoute() +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.getSortedGroups) +const { adminGroup, recentGroup, disabledGroup } = useFormatGroups(groups) + +/** Server settings for current user */ +const settings = computed(() => store.getters.getServerData) +/** True if the current user is a (delegated) admin */ +const isAdminOrDelegatedAdmin = computed(() => settings.value.isAdmin || settings.value.isDelegatedAdmin) + +/** + * Open the new-user form dialog + */ +function showNewUserMenu() { + store.commit('setShowConfig', { + key: 'showNewUserForm', + value: true, + }) +} +</script> + +<style scoped lang="scss"> +.account-management { + &__navigation { + :deep(.app-navigation__body) { + will-change: scroll-position; + } + } + &__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/Users.vue b/apps/settings/src/views/Users.vue deleted file mode 100644 index c3d3d9f3c72..00000000000 --- a/apps/settings/src/views/Users.vue +++ /dev/null @@ -1,485 +0,0 @@ -<!-- - - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - - - - @author John Molakvoæ <skjnldsv@protonmail.com> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> - -<template> - <Content app-name="settings" :navigation-class="{ 'icon-loading': loadingAddGroup }"> - <AppNavigation> - <AppNavigationNew button-id="new-user-button" - :text="t('settings','New user')" - button-class="icon-add" - @click="showNewUserMenu" - @keyup.enter="showNewUserMenu" - @keyup.space="showNewUserMenu" /> - <template #list> - <AppNavigationItem id="addgroup" - ref="addGroup" - :edit-placeholder="t('settings', 'Enter group name')" - :editable="true" - :loading="loadingAddGroup" - :title="t('settings', 'Add group')" - icon="icon-add" - @click="showAddGroupForm" - @update:title="createGroup" /> - <AppNavigationItem id="everyone" - :exact="true" - :title="t('settings', 'Active users')" - :to="{ name: 'users' }" - icon="icon-contacts-dark"> - <AppNavigationCounter v-if="userCount > 0" slot="counter"> - {{ userCount }} - </AppNavigationCounter> - </AppNavigationItem> - <AppNavigationItem v-if="settings.isAdmin" - id="admin" - :exact="true" - :title="t('settings', 'Admins')" - :to="{ name: 'group', params: { selectedGroup: 'admin' } }" - icon="icon-user-admin"> - <AppNavigationCounter v-if="adminGroupMenu.count" slot="counter"> - {{ adminGroupMenu.count }} - </AppNavigationCounter> - </AppNavigationItem> - - <!-- Hide the disabled if none, if we don't have the data (-1) show it --> - <AppNavigationItem v-if="disabledGroupMenu.usercount > 0 || disabledGroupMenu.usercount === -1" - id="disabled" - :exact="true" - :title="t('settings', 'Disabled users')" - :to="{ name: 'group', params: { selectedGroup: 'disabled' } }" - icon="icon-disabled-users"> - <AppNavigationCounter v-if="disabledGroupMenu.usercount > 0" slot="counter"> - {{ disabledGroupMenu.usercount }} - </AppNavigationCounter> - </AppNavigationItem> - - <AppNavigationCaption v-if="groupList.length > 0" :title="t('settings', 'Groups')" /> - <GroupListItem v-for="group in groupList" - :id="group.id" - :key="group.id" - :title="group.title" - :count="group.count" /> - </template> - <template #footer> - <AppNavigationSettings> - <div> - <p>{{ t('settings', 'Default quota:') }}</p> - <Multiselect :value="defaultQuota" - :options="quotaOptions" - tag-placeholder="create" - :placeholder="t('settings', 'Select default quota')" - label="label" - track-by="id" - :allow-empty="false" - :taggable="true" - @tag="validateQuota" - @input="setDefaultQuota" /> - </div> - <div> - <input id="showLanguages" - v-model="showLanguages" - type="checkbox" - class="checkbox"> - <label for="showLanguages">{{ t('settings', 'Show Languages') }}</label> - </div> - <div> - <input id="showLastLogin" - v-model="showLastLogin" - type="checkbox" - class="checkbox"> - <label for="showLastLogin">{{ t('settings', 'Show last login') }}</label> - </div> - <div> - <input id="showUserBackend" - v-model="showUserBackend" - type="checkbox" - class="checkbox"> - <label for="showUserBackend">{{ t('settings', 'Show user backend') }}</label> - </div> - <div> - <input id="showStoragePath" - v-model="showStoragePath" - type="checkbox" - class="checkbox"> - <label for="showStoragePath">{{ t('settings', 'Show storage path') }}</label> - </div> - <div> - <input id="sendWelcomeMail" - v-model="sendWelcomeMail" - :disabled="loadingSendMail" - type="checkbox" - class="checkbox"> - <label for="sendWelcomeMail">{{ t('settings', 'Send email to new user') }}</label> - </div> - </AppNavigationSettings> - </template> - </AppNavigation> - <AppContent> - <UserList :users="users" - :show-config="showConfig" - :selected-group="selectedGroupDecoded" - :external-actions="externalActions" /> - </AppContent> - </Content> -</template> - -<script> -import AppContent from '@nextcloud/vue/dist/Components/AppContent' -import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' -import AppNavigationCaption from '@nextcloud/vue/dist/Components/AppNavigationCaption' -import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter' -import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem' -import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew' -import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings' -import axios from '@nextcloud/axios' -import Content from '@nextcloud/vue/dist/Components/Content' -import { generateUrl } from '@nextcloud/router' -import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' -import Vue from 'vue' -import VueLocalStorage from 'vue-localstorage' - -import GroupListItem from '../components/GroupListItem' -import UserList from '../components/UserList' - -Vue.use(VueLocalStorage) - -export default { - name: 'Users', - components: { - AppContent, - AppNavigation, - AppNavigationCaption, - AppNavigationCounter, - AppNavigationItem, - AppNavigationNew, - AppNavigationSettings, - Content, - GroupListItem, - Multiselect, - UserList, - }, - props: { - selectedGroup: { - type: String, - default: null, - }, - }, - data() { - return { - // default quota is set to unlimited - unlimitedQuota: { id: 'none', label: t('settings', 'Unlimited') }, - // temporary value used for multiselect change - selectedQuota: false, - externalActions: [], - loadingAddGroup: false, - loadingSendMail: false, - showConfig: { - showStoragePath: false, - showUserBackend: false, - showLastLogin: false, - showNewUserForm: false, - showLanguages: false, - }, - } - }, - computed: { - 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 - }, - - // Local settings - showLanguages: { - get() { return this.getLocalstorage('showLanguages') }, - set(status) { - this.setLocalStorage('showLanguages', status) - }, - }, - showLastLogin: { - get() { return this.getLocalstorage('showLastLogin') }, - set(status) { - this.setLocalStorage('showLastLogin', status) - }, - }, - showUserBackend: { - get() { return this.getLocalstorage('showUserBackend') }, - set(status) { - this.setLocalStorage('showUserBackend', status) - }, - }, - showStoragePath: { - get() { return this.getLocalstorage('showStoragePath') }, - set(status) { - this.setLocalStorage('showStoragePath', status) - }, - }, - - userCount() { - return this.$store.getters.getUserCount - }, - settings() { - return this.$store.getters.getServerData - }, - - // default quota - quotaOptions() { - // convert the preset array into objects - const quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), []) - // add default presets - if (this.settings.allowUnlimitedQuota) { - quotaPreset.unshift(this.unlimitedQuota) - } - return quotaPreset - }, - // mapping saved values to objects - defaultQuota: { - get() { - if (this.selectedQuota !== false) { - return this.selectedQuota - } - if (this.settings.defaultQuota !== this.unlimitedQuota.id && OC.Util.computerFileSize(this.settings.defaultQuota) >= 0) { - // if value is valid, let's map the quotaOptions or return custom quota - return { id: this.settings.defaultQuota, label: this.settings.defaultQuota } - } - return this.unlimitedQuota // unlimited - }, - set(quota) { - this.selectedQuota = quota - }, - - }, - - sendWelcomeMail: { - get() { - return this.settings.newUserSendEmail - }, - async set(value) { - try { - this.loadingSendMail = true - this.$store.commit('setServerData', { - ...this.settings, - newUserSendEmail: value, - }) - await axios.post(generateUrl('/settings/users/preferences/newUser.sendEmail'), { value: value ? 'yes' : 'no' }) - } catch (e) { - console.error('could not update newUser.sendEmail preference: ' + e.message, e) - } finally { - this.loadingSendMail = false - } - }, - }, - - 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() { - this.$store.commit('initGroups', { - groups: this.$store.getters.getServerData.groups, - orderBy: this.$store.getters.getServerData.sortGroups, - userCount: this.$store.getters.getServerData.userCount, - }) - this.$store.dispatch('getPasswordPolicyMinLength') - }, - created() { - // init the OCA.Settings.UserList object - // and add the registerAction method - Object.assign(OCA, { - Settings: { - UserList: { - registerAction: this.registerAction, - }, - }, - }) - }, - methods: { - showNewUserMenu() { - this.showConfig.showNewUserForm = true - if (this.showConfig.showNewUserForm) { - Vue.nextTick(() => { - window.newusername.focus() - }) - } - }, - getLocalstorage(key) { - // force initialization - const localConfig = this.$localStorage.get(key) - // if localstorage is null, fallback to original values - this.showConfig[key] = localConfig !== null ? localConfig === 'true' : this.showConfig[key] - return this.showConfig[key] - }, - setLocalStorage(key, status) { - this.showConfig[key] = status - this.$localStorage.set(key, status) - return status - }, - - /** - * Dispatch default quota set request - * - * @param {string | object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'} - */ - setDefaultQuota(quota = 'none') { - this.$store.dispatch('setAppConfig', { - app: 'files', - key: 'default_quota', - // ensure we only send the preset id - value: quota.id ? quota.id : quota, - }).then(() => { - if (typeof quota !== 'object') { - quota = { id: quota, label: quota } - } - this.defaultQuota = quota - }) - }, - - /** - * Validate quota string to make sure it's a valid human file size - * - * @param {string} quota Quota in readable format '5 GB' - * @return {Promise|boolean} - */ - validateQuota(quota) { - // only used for new presets sent through @Tag - const validQuota = OC.Util.computerFileSize(quota) - if (validQuota === null) { - return this.setDefaultQuota('none') - } else { - // unify format output - return this.setDefaultQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota))) - } - }, - - /** - * Register a new action for the user menu - * - * @param {string} icon the icon class - * @param {string} text the text to display - * @param {Function} action the function to run - * @return {Array} - */ - registerAction(icon, text, action) { - this.externalActions.push({ - icon, - text, - action, - }) - return this.externalActions - }, - - /** - * Create a new group - * - * @param {string} gid The group id - */ - async createGroup(gid) { - // group is not valid - if (gid.trim() === '') { - return - } - - try { - this.loadingAddGroup = true - await this.$store.dispatch('addGroup', gid.trim()) - - this.hideAddGroupForm() - await this.$router.push({ - name: 'group', - params: { - selectedGroup: encodeURIComponent(gid.trim()), - }, - }) - } catch { - this.showAddGroupForm() - } finally { - this.loadingAddGroup = false - } - }, - - showAddGroupForm() { - this.$refs.addGroup.editingActive = true - this.$refs.addGroup.onMenuToggle(false) - this.$nextTick(() => { - this.$refs.addGroup.$refs.editingInput.focusInput() - }) - }, - - hideAddGroupForm() { - this.$refs.addGroup.editingActive = false - this.$refs.addGroup.editingValue = '' - }, - - /** - * 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> -// force hiding the editing action for the add group entry -.app-navigation__list #addgroup::v-deep .app-navigation-entry__utils { - display: none; -} -</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..21c63a13b03 --- /dev/null +++ b/apps/settings/src/views/user-types.d.ts @@ -0,0 +1,35 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +export interface IGroup { + /** + * Id + */ + id: string + + /** + * Display name + */ + name: string + + /** + * Overall user count + */ + usercount: number + + /** + * Number of disabled users + */ + disabled: number + + /** + * True if users can be added to this group + */ + canAdd?: boolean + + /** + * True if users can be removed from this group + */ + canRemove?: boolean +} |