From d890eeaa49251b210307c9a7016b725df405b853 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 13 May 2026 16:19:04 +0200 Subject: [PATCH] feat(appstore): show new column with groups the app is limited to - resolves: https://github.com/nextcloud/server/issues/30503 If there is enough space we can directly show the groups this app is limited to in the table. This is especially helpful if you want to quickly check your enabled apps. Signed-off-by: Ferdinand Thiessen --- .../src/components/AppTable/AppTable.vue | 27 +++++++++++++-- .../src/components/AppTable/AppTableRow.vue | 24 ++++++++++++++ .../AppstoreSidebar/AppDetailsTab.vue | 16 +++------ .../src/composables/useLimitedGroups.ts | 33 +++++++++++++++++++ apps/appstore/src/store/groups.ts | 25 ++++++++++++++ 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 apps/appstore/src/composables/useLimitedGroups.ts diff --git a/apps/appstore/src/components/AppTable/AppTable.vue b/apps/appstore/src/components/AppTable/AppTable.vue index dc29e1d4496fc..1419ff3da1c82 100644 --- a/apps/appstore/src/components/AppTable/AppTable.vue +++ b/apps/appstore/src/components/AppTable/AppTable.vue @@ -19,14 +19,21 @@ const tableElement = useTemplateRef('table') const { width: tableWidth } = useElementSize(tableElement) const isNarrow = computed(() => tableWidth.value < 768) +const isWide = computed(() => tableWidth.value >= 1280) @@ -63,14 +74,26 @@ const isNarrow = computed(() => tableWidth.value < 768) width: 60%; } +.appTable_wide .appTable__colName { + width: 37%; +} + .appTable__colSupport { width: 15%; } +.appTable_wide .appTable__colSupport { + width: 12%; +} + .appTable__colActions { width: 25%; } +.appTable_wide .appTable__colActions { + width: 20%; +} + .appTable_narrow .appTable__colActions { width: calc(3 * var(--default-grid-baseline) + 2 * var(--default-clickable-area)); } diff --git a/apps/appstore/src/components/AppTable/AppTableRow.vue b/apps/appstore/src/components/AppTable/AppTableRow.vue index 1291d3f0a580f..d6fc9a6c1dafd 100644 --- a/apps/appstore/src/components/AppTable/AppTableRow.vue +++ b/apps/appstore/src/components/AppTable/AppTableRow.vue @@ -12,16 +12,19 @@ import { t } from '@nextcloud/l10n' import { computed } from 'vue' import { useRoute } from 'vue-router' import NcButton from '@nextcloud/vue/components/NcButton' +import NcChip from '@nextcloud/vue/components/NcChip' import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import AppActions from '../AppActions.vue' import AppIcon from '../AppIcon.vue' import BadgeAppDaemon from '../BadgeAppDaemon.vue' import BadgeAppLevel from '../BadgeAppLevel.vue' import { useActions } from '../../composables/useActions.ts' +import { useLimitedGroups } from '../../composables/useLimitedGroups.ts' const { app, isNarrow } = defineProps<{ app: IAppstoreApp | IAppstoreExApp isNarrow?: boolean + isWide?: boolean }>() const route = useRoute() @@ -46,6 +49,7 @@ const detailsAction = computed(() => ({ inline: false, })) +const groupsAppIsLimitedTo = useLimitedGroups(() => app) const rawActions = useActions(() => app) const actions = computed(() => [ ...rawActions.value, @@ -80,6 +84,21 @@ const actions = computed(() => [ + +
    + +
+
[ color: var(--color-text-maxcontrast); } +.appTableRow__groupsCell { + display: flex; + gap: var(--default-grid-baseline); +} + .appTableRow__actionsCell { display: flex; gap: var(--default-grid-baseline); diff --git a/apps/appstore/src/components/AppstoreSidebar/AppDetailsTab.vue b/apps/appstore/src/components/AppstoreSidebar/AppDetailsTab.vue index 6d26d20e15be7..3d3ccc29aac77 100644 --- a/apps/appstore/src/components/AppstoreSidebar/AppDetailsTab.vue +++ b/apps/appstore/src/components/AppstoreSidebar/AppDetailsTab.vue @@ -16,6 +16,7 @@ import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' import BadgeAppDaemon from '../BadgeAppDaemon.vue' import BadgeAppLevel from '../BadgeAppLevel.vue' import BadgeAppScore from '../BadgeAppScore.vue' +import { useLimitedGroups } from '../../composables/useLimitedGroups.ts' import { useAppsStore } from '../../store/apps.ts' const { app } = defineProps<{ app: IAppstoreApp | IAppstoreExApp }>() @@ -43,15 +44,8 @@ const appAuthors = computed(() => { .join(', ') }) -const groupsAppIsLimitedto = computed(() => { - if (!app.groups) { - return [] - } - - return app.groups.map((group) => ({ id: group, name: group })) -}) - const appstoreUrl = computed(() => `https://apps.nextcloud.com/apps/${app.id}`) +const groupsAppIsLimitedTo = useLimitedGroups(() => app) /** * Further external resources (e.g. website) @@ -144,16 +138,16 @@ function authorName(xmlNode): string { -
+

{{ t('appstore', 'Limited to groups') }}

  • - {{ group.name }} + {{ group.displayName }}
diff --git a/apps/appstore/src/composables/useLimitedGroups.ts b/apps/appstore/src/composables/useLimitedGroups.ts new file mode 100644 index 0000000000000..d2c8360be2359 --- /dev/null +++ b/apps/appstore/src/composables/useLimitedGroups.ts @@ -0,0 +1,33 @@ +/*! + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { MaybeRefOrGetter } from 'vue' +import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts' + +import { readonly, ref, toValue, watch } from 'vue' +import { useGroupsStore } from '../store/groups.ts' + +/** + * Get the groups an app is limited to and keep it up to date + * + * @param app - The app to get the groups + */ +export function useLimitedGroups(app: MaybeRefOrGetter) { + const groupsStore = useGroupsStore() + const groupsAppIsLimitedTo = ref<{ id: string, displayName: string }[]>([]) + watch(() => toValue(app).groups, async () => { + const groups = toValue(app).groups + if (groups === undefined) { + groupsAppIsLimitedTo.value = [] + return + } + + const promises = groups.map((group) => groupsStore.fetchGroupById(group)) + const results = await Promise.all(promises) + groupsAppIsLimitedTo.value = results.filter(Boolean) as { id: string, displayName: string }[] + }, { immediate: true }) + + return readonly(groupsAppIsLimitedTo) +} diff --git a/apps/appstore/src/store/groups.ts b/apps/appstore/src/store/groups.ts index 703b9587dcc1d..3e29bdf48d56f 100644 --- a/apps/appstore/src/store/groups.ts +++ b/apps/appstore/src/store/groups.ts @@ -8,13 +8,25 @@ import type { NcSelectUsersModel } from '@nextcloud/vue/components/NcSelectUsers import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' +import PQueue from 'p-queue' import { defineStore } from 'pinia' import { computed, ref } from 'vue' import logger from '../utils/logger.ts' +const queue = new PQueue({ concurrency: 3 }) + export const useGroupsStore = defineStore('groups', () => { const groups = ref(new Map()) + /** + * Get group details by id + * + * @param groupId - The id of the group to fetch + */ + async function fetchGroupById(groupId: string) { + return await queue.add(() => internalFetchGroupById(groupId)) + } + /** * Search the API for groups matching the query * @@ -59,5 +71,18 @@ export const useGroupsStore = defineStore('groups', () => { groups: computed(() => Array.from(groups.value.values())), searchGroups, getGroupById, + fetchGroupById, + } + + /** + * Handle fetching group details by id + * + * @param groupId - The id of the group to fetch + */ + async function internalFetchGroupById(groupId: string) { + if (!groups.value.has(groupId)) { + await searchGroups(groupId) + } + return groups.value.get(groupId) } })