Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions apps/appstore/src/components/AppTable/AppTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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));
}
Expand Down
24 changes: 24 additions & 0 deletions apps/appstore/src/components/AppTable/AppTableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -46,6 +49,7 @@ const detailsAction = computed<AppAction>(() => ({
inline: false,
}))

const groupsAppIsLimitedTo = useLimitedGroups(() => app)
const rawActions = useActions(() => app)
const actions = computed(() => [
...rawActions.value,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
16 changes: 5 additions & 11 deletions apps/appstore/src/components/AppstoreSidebar/AppDetailsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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>
Expand Down
33 changes: 33 additions & 0 deletions apps/appstore/src/composables/useLimitedGroups.ts
Original file line number Diff line number Diff line change
@@ -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)
}
25 changes: 25 additions & 0 deletions apps/appstore/src/store/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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)
}
})
Loading