aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/views')
-rw-r--r--apps/settings/src/views/AdminSettingsSharing.vue44
-rw-r--r--apps/settings/src/views/AppStore.vue88
-rw-r--r--apps/settings/src/views/AppStoreNavigation.vue146
-rw-r--r--apps/settings/src/views/AppStoreSidebar.vue159
-rw-r--r--apps/settings/src/views/Apps.vue388
-rw-r--r--apps/settings/src/views/SettingsApp.vue16
-rw-r--r--apps/settings/src/views/UserManagement.vue104
-rw-r--r--apps/settings/src/views/UserManagementNavigation.vue172
-rw-r--r--apps/settings/src/views/Users.vue485
-rw-r--r--apps/settings/src/views/user-types.d.ts35
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
+}