Merge pull request #60355 from nextcloud/feat/groups-column-appstore

feat(appstore): show new column with groups the app is limited to
This commit is contained in:
Andy Scherzinger 2026-05-18 12:05:12 +02:00 committed by GitHub
commit f76278f8e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 139 additions and 40 deletions

View file

@ -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)
</script>
<template>
<table ref="table" :class="[$style.appTable, { [$style.appTable_narrow]: isNarrow }]">
<table
ref="table"
:class="[$style.appTable, {
[$style.appTable_narrow]: isNarrow,
[$style.appTable_wide]: isWide,
}]">
<colgroup>
<col :class="$style.appTable__colName">
<col :class="$style.appTable__colVersion">
<col v-if="!isNarrow" :class="$style.appTable__colSupport">
<col v-if="isWide" :class="$style.appTable__colGroups">
<col :class="$style.appTable__colActions">
</colgroup>
<thead hidden>
@ -36,6 +43,9 @@ const isNarrow = computed(() => tableWidth.value < 768)
<th v-if="!isNarrow">
{{ t('appstore', 'Support level') }}
</th>
<th v-if="isWide">
{{ t('appstore', 'Groups') }}
</th>
<th>{{ t('appstore', 'Actions') }}</th>
</tr>
</thead>
@ -44,7 +54,8 @@ const isNarrow = computed(() => tableWidth.value < 768)
v-for="app in apps"
:key="app.id"
:app
:isNarrow />
:isNarrow
:isWide />
</tbody>
</table>
</template>
@ -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));
}

View file

@ -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<AppAction>(() => ({
inline: false,
}))
const groupsAppIsLimitedTo = useLimitedGroups(() => app)
const rawActions = useActions(() => app)
const actions = computed(() => [
...rawActions.value,
@ -80,6 +84,21 @@ const actions = computed(() => [
<BadgeAppDaemon v-if="'daemon' in app && app.daemon" :daemon="app.daemon" />
</div>
</td>
<td v-if="isWide">
<ul
v-if="groupsAppIsLimitedTo.length > 0"
:class="$style.appTableRow__groupsCell"
:title="groupsAppIsLimitedTo.map((group) => group.displayName).join(', ')">
<template v-for="group, index in groupsAppIsLimitedTo" :key="group.id">
<li v-if="index === 3" aria-hidden="true">
</li>
<li :class="{ 'hidden-visually': index > 2 }">
<NcChip :text="group.displayName" noClose />
</li>
</template>
</ul>
</td>
<td>
<div :class="$style.appTableRow__actionsCell">
<AppActions
@ -117,6 +136,11 @@ 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);

View file

@ -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 {
</ul>
</NcNoteCard>
<div v-if="groupsAppIsLimitedto.length" :class="$style.appstoreDetailsTab__section">
<div v-if="groupsAppIsLimitedTo.length" :class="$style.appstoreDetailsTab__section">
<h4 :id="idLimitedToGroups">
{{ t('appstore', 'Limited to groups') }}
</h4>
<ul :aria-labelledby="idLimitedToGroups" :class="$style.appstoreDetailsTab__sectionDetails">
<li
v-for="group of groupsAppIsLimitedto"
v-for="group of groupsAppIsLimitedTo"
:key="group.id"
:title="group.id">
{{ group.name }}
{{ group.displayName }}
</li>
</ul>
</div>

View file

@ -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<IAppstoreApp | IAppstoreExApp>) {
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)
}

View file

@ -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<string, NcSelectUsersModel>())
/**
* 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)
}
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/* extracted by css-entry-points-plugin */
@import './appstore-appstore-main-eATUKVIF.chunk.css';
@import './appstore-appstore-main-fIugqNvM.chunk.css';
@import './common-Web-C_oBIsvc.chunk.css';
@import './common-ArrowRight-D7L4ZBkR.chunk.css';
@import './common-NcModal-kyWZ3UFC-CBh34man.chunk.css';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long