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
11 changes: 6 additions & 5 deletions apps/files_external/src/components/ExternalStorageTableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { computed, ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import AddExternalStorageDialog from './AddExternalStorageDialog/AddExternalStorageDialog.vue'
import { useUsers } from '../composables/useEntities.ts'
import { useGroups, useUsers } from '../composables/useEntities.ts'
import { useStorages } from '../store/storages.ts'
import { StorageStatus, StorageStatusIcons, StorageStatusMessage } from '../types.ts'

Expand Down Expand Up @@ -51,6 +51,7 @@ const status = computed(() => {
})

const users = useUsers(() => props.storage.applicableUsers || [])
const groups = useGroups(() => props.storage.applicableGroups || [])

/**
* Handle deletion of the external storage mount point
Expand Down Expand Up @@ -113,11 +114,11 @@ async function reloadStatus() {
<td v-if="isAdmin">
<div :class="$style.storageTableRow__cellApplicable">
<NcChip
v-for="group of storage.applicableGroups"
:key="group"
v-for="group of groups"
:key="group.id"
:iconPath="mdiAccountGroupOutline"
noClose
:text="group" />
:text="group.displayName" />
<NcUserBubble
v-for="user of users"
:key="user.user"
Expand Down Expand Up @@ -169,7 +170,7 @@ async function reloadStatus() {
align-items: center;

max-height: calc(48px + 2 * var(--default-grid-baseline));
overflow: scroll;
overflow: auto;
}

.storageTableRow__status_warning {
Expand Down
99 changes: 99 additions & 0 deletions apps/files_external/src/composables/useEntities.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { flushPromises } from '@vue/test-utils'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { effectScope, nextTick, ref } from 'vue'

const axios = vi.hoisted(() => ({ get: vi.fn(), post: vi.fn() }))
vi.mock('@nextcloud/axios', () => ({ default: axios }))

const { useGroups, useUsers } = await import('./useEntities.ts')

describe('useUsers', () => {
beforeEach(() => {
vi.resetAllMocks()
})

test('resolves display names for given uids', async () => {
axios.post.mockResolvedValue({ data: { users: { alice: 'Alice Doe' } } })

let users
effectScope().run(() => {
users = useUsers(() => ['alice'])
})
await flushPromises()

expect(axios.post).toHaveBeenCalledWith(expect.stringContaining('/displaynames'), { users: ['alice'] })
expect(users!.value).toEqual([{ id: 'user:alice', user: 'alice', displayName: 'Alice Doe' }])
})
})

describe('useGroups', () => {
beforeEach(() => {
vi.resetAllMocks()
})

test('resolves display names for given gids via the applicable endpoint', async () => {
axios.get.mockResolvedValue({ data: { groups: { grp1: 'Marketing Team' } } })

let groups
effectScope().run(() => {
groups = useGroups(() => ['grp1'])
})
await flushPromises()

expect(axios.get).toHaveBeenCalledWith(
expect.stringContaining('applicable'),
{ params: { pattern: 'grp1', limit: 50 } },
)
expect(groups!.value[0]).toMatchObject({ id: 'grp1', displayName: 'Marketing Team' })
})

test('falls back to the raw gid when the group is unknown', async () => {
axios.get.mockResolvedValue({ data: { groups: {} } })

let groups
effectScope().run(() => {
groups = useGroups(() => ['deleted-group'])
})
await flushPromises()

expect(groups!.value[0]).toMatchObject({ id: 'deleted-group', displayName: 'deleted-group' })
})

test('does not repeat a request for a gid still waiting its turn when the list shrinks back', async () => {
const resolvers: Record<string, (value: { data: { groups: Record<string, string> } }) => void> = {}
axios.get.mockImplementation((_url: string, config: { params: { pattern: string } }) => new Promise((resolve) => {
resolvers[config.params.pattern] = resolve
}))

// missingGroups is computed as [a, b]; 'a' starts its request immediately,
// 'b' is still queued behind it in the loop (not requested yet)
const gids = ref(['a', 'b'])
let groups
effectScope().run(() => {
groups = useGroups(gids)
})
await nextTick()
expect(axios.get).toHaveBeenCalledTimes(1)

// list shrinks back to just 'a' while both 'a' and 'b' are already marked pending
gids.value = ['a']
await nextTick()
expect(axios.get).toHaveBeenCalledTimes(1)

resolvers.a({ data: { groups: { a: 'A Team' } } })
await flushPromises()

// 'b' still gets its request once its turn in the loop comes up, just not a duplicate one
expect(axios.get).toHaveBeenCalledTimes(2)

resolvers.b({ data: { groups: { b: 'B Team' } } })
await flushPromises()

expect(axios.get).toHaveBeenCalledTimes(2)
})
})
28 changes: 24 additions & 4 deletions apps/files_external/src/composables/useEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { generateUrl } from '@nextcloud/router'
import { computed, reactive, toValue, watchEffect } from 'vue'

const displayNames = reactive(new Map<string, string>())
const groupDisplayNames = reactive(new Map<string, string>())
const pendingGroups = new Set<string>()

/**
* Fetch and provide user display names for given UIDs
Expand Down Expand Up @@ -40,24 +42,42 @@ export function useUsers(uids: MaybeRefOrGetter<string[]>) {
}

/**
* Map group ids to IUserData objects
* Fetch and provide group display names for given GIDs, mapped to IUserData objects
*
* @param gids - The group ids to create entities for
*/
export function useGroups(gids: MaybeRefOrGetter<string[]>) {
return computed(() => toValue(gids).map(mapGroupToUserData))
const groups = computed(() => toValue(gids).map((gid) => mapGroupToUserData(gid, groupDisplayNames.get(gid))))

watchEffect(async () => {
const missingGroups = toValue(gids).filter((gid) => !groupDisplayNames.has(gid) && !pendingGroups.has(gid))
missingGroups.forEach((gid) => pendingGroups.add(gid))
for (const gid of missingGroups) {
try {
const { data } = await axios.get(generateUrl('apps/files_external/ajax/applicable'), {
params: { pattern: gid, limit: 50 },
})
groupDisplayNames.set(gid, data.groups[gid] ?? gid)
} finally {
pendingGroups.delete(gid)
}
}
})

return groups
}

/**
* Map a group id to an IUserData object
*
* @param gid - The group id to map
* @param displayName - The resolved display name for the group, falls back to the group id
*/
export function mapGroupToUserData(gid: string) {
export function mapGroupToUserData(gid: string, displayName?: string) {
return {
id: gid,
isNoUser: true,
displayName: gid,
displayName: displayName || gid,
iconSvg: svgAccountGroupOutline,
}
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import{a as t}from"./index-xFyBfRrS.chunk.mjs";import{t as e}from"./translation-DoG5ZELJ-BmllVdG7.chunk.mjs";import{C as m,a}from"./CommentView-Dhc6U55M.chunk.mjs";import{l as p}from"./activity-BZ6xFlZg.chunk.mjs";import{b as i,r as s,o as n,c,m as u}from"./Web-DfK43Mjv.chunk.mjs";import{_ as l}from"./public-C1mLBHT3.chunk.mjs";import"./index-DqXN05Ck.chunk.mjs";import"./NcModal-DiDvgcU8-D5SheMiq.chunk.mjs";import"./logger-D3RVzcfQ-BO8h8n4L.chunk.mjs";import"./createElementId-DhjFt1I9-Bnrf3uOu.chunk.mjs";import"./index-Cp0kcsps.chunk.mjs";import"./TrashCanOutline-DQrHTd8q.chunk.mjs";import"./mdi-CW3n8UeZ.chunk.mjs";import"./pinia-BFWhBQ3b.chunk.mjs";import"./PencilOutline-DHoz5paq.chunk.mjs";/* empty css */import"./NcAvatar-1KxMUN7V-B2ZhCMT3.chunk.mjs";import"./index-BUgWwKkm.chunk.mjs";import"./util-DBjHxD9z.chunk.mjs";import"./ArrowRight-BaRJ4rMK.chunk.mjs";import"./colors-Cv9F-jWS-CXrLxPaW.chunk.mjs";import"./NcUserStatusIcon-BF5OEQFU-B7JEw9np.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-BJuPH7S7-3EVedHy6.chunk.mjs";import"./NcUserBubble-BXR7vT0V-vC2oGZXL.chunk.mjs";import"./GetComments-CFK5MR0g.chunk.mjs";import"./index-NAGd6Ttu.chunk.mjs";const d=i({components:{Comment:a},mixins:[m],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(o){t(e("comments","Could not reload comments")),p.error("Could not reload comments",{error:o})}}}});function C(o,f,y,w,D,N){const r=s("Comment");return n(),c(r,u(o.editorData,{autoComplete:o.autoComplete,resourceType:o.resourceType,editor:!0,userData:o.userData,resourceId:o.resourceId,class:"comments-action",onNew:o.onNewComment}),null,16,["autoComplete","resourceType","userData","resourceId","onNew"])}const S=l(d,[["render",C],["__scopeId","data-v-29a1e244"]]);export{S as default};
//# sourceMappingURL=ActivityCommentAction-UQ5y5YTr.chunk.mjs.map
import{a as t}from"./index-B8Ex2jDj.chunk.mjs";import{t as e}from"./translation-DoG5ZELJ-BmllVdG7.chunk.mjs";import{C as m,a}from"./CommentView-C1qNY2B6.chunk.mjs";import{l as p}from"./activity-BZ6xFlZg.chunk.mjs";import{b as i,r as s,o as n,c,m as u}from"./Web-DfK43Mjv.chunk.mjs";import{_ as l}from"./public-C1mLBHT3.chunk.mjs";import"./index-B4Pb_6Dn.chunk.mjs";import"./NcModal-DiDvgcU8-dbt7Dqpx.chunk.mjs";import"./logger-D3RVzcfQ-BO8h8n4L.chunk.mjs";import"./createElementId-DhjFt1I9-Bnrf3uOu.chunk.mjs";import"./index-Cp0kcsps.chunk.mjs";import"./TrashCanOutline-DQrHTd8q.chunk.mjs";import"./mdi-CW3n8UeZ.chunk.mjs";import"./pinia-BFWhBQ3b.chunk.mjs";import"./PencilOutline-D8vovvJI.chunk.mjs";/* empty css */import"./NcAvatar-1KxMUN7V-CO2gVIA6.chunk.mjs";import"./index-BUgWwKkm.chunk.mjs";import"./util-DBjHxD9z.chunk.mjs";import"./ArrowRight-BaRJ4rMK.chunk.mjs";import"./colors-Cv9F-jWS-CXrLxPaW.chunk.mjs";import"./NcUserStatusIcon-BF5OEQFU-B7JEw9np.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-BJuPH7S7-Dclz8r5l.chunk.mjs";import"./NcUserBubble-BXR7vT0V-DLtND5ia.chunk.mjs";import"./GetComments-CFK5MR0g.chunk.mjs";import"./index-NAGd6Ttu.chunk.mjs";const d=i({components:{Comment:a},mixins:[m],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(o){t(e("comments","Could not reload comments")),p.error("Could not reload comments",{error:o})}}}});function C(o,f,y,w,D,N){const r=s("Comment");return n(),c(r,u(o.editorData,{autoComplete:o.autoComplete,resourceType:o.resourceType,editor:!0,userData:o.userData,resourceId:o.resourceId,class:"comments-action",onNew:o.onNewComment}),null,16,["autoComplete","resourceType","userData","resourceId","onNew"])}const S=l(d,[["render",C],["__scopeId","data-v-29a1e244"]]);export{S as default};
//# sourceMappingURL=ActivityCommentAction-BxrXEwhI.chunk.mjs.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
import{t as s}from"./translation-DoG5ZELJ-BmllVdG7.chunk.mjs";import{C as p,a}from"./CommentView-Dhc6U55M.chunk.mjs";import{_ as i}from"./public-C1mLBHT3.chunk.mjs";import{r as n,o as c,c as u,m as l}from"./Web-DfK43Mjv.chunk.mjs";import"./index-Cp0kcsps.chunk.mjs";import"./pinia-BFWhBQ3b.chunk.mjs";import"./PencilOutline-DHoz5paq.chunk.mjs";import"./logger-D3RVzcfQ-BO8h8n4L.chunk.mjs";import"./createElementId-DhjFt1I9-Bnrf3uOu.chunk.mjs";import"./NcModal-DiDvgcU8-D5SheMiq.chunk.mjs";/* empty css */import"./NcAvatar-1KxMUN7V-B2ZhCMT3.chunk.mjs";import"./index-BUgWwKkm.chunk.mjs";import"./util-DBjHxD9z.chunk.mjs";import"./ArrowRight-BaRJ4rMK.chunk.mjs";import"./colors-Cv9F-jWS-CXrLxPaW.chunk.mjs";import"./NcUserStatusIcon-BF5OEQFU-B7JEw9np.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-BJuPH7S7-3EVedHy6.chunk.mjs";import"./TrashCanOutline-DQrHTd8q.chunk.mjs";import"./NcUserBubble-BXR7vT0V-vC2oGZXL.chunk.mjs";import"./index-xFyBfRrS.chunk.mjs";import"./index-DqXN05Ck.chunk.mjs";import"./mdi-CW3n8UeZ.chunk.mjs";import"./activity-BZ6xFlZg.chunk.mjs";import"./GetComments-CFK5MR0g.chunk.mjs";import"./index-NAGd6Ttu.chunk.mjs";const d={name:"ActivityCommentEntry",components:{Comment:a},mixins:[p],props:{comment:{type:Object,required:!0},reloadCallback:{type:Function,required:!0}},data(){return{commentMessage:""}},watch:{comment(){this.commentMessage=this.comment.props.message}},mounted(){this.commentMessage=this.comment.props.message},methods:{t:s}};function g(t,e,o,f,m,C){const r=n("Comment");return c(),u(r,l({ref:"comment",tag:"li"},o.comment.props,{autoComplete:t.autoComplete,resourceType:t.resourceType,message:m.commentMessage,resourceId:t.resourceId,userData:t.genMentionsData(o.comment.props.mentions),class:"comments-activity",onDelete:e[0]||(e[0]=y=>o.reloadCallback())}),null,16,["autoComplete","resourceType","message","resourceId","userData"])}const Q=i(d,[["render",g],["__scopeId","data-v-afc310f1"]]);export{Q as default};
//# sourceMappingURL=ActivityCommentEntry-Bk6bzGdP.chunk.mjs.map
import{t as s}from"./translation-DoG5ZELJ-BmllVdG7.chunk.mjs";import{C as p,a}from"./CommentView-C1qNY2B6.chunk.mjs";import{_ as i}from"./public-C1mLBHT3.chunk.mjs";import{r as n,o as c,c as u,m as l}from"./Web-DfK43Mjv.chunk.mjs";import"./index-Cp0kcsps.chunk.mjs";import"./pinia-BFWhBQ3b.chunk.mjs";import"./PencilOutline-D8vovvJI.chunk.mjs";import"./logger-D3RVzcfQ-BO8h8n4L.chunk.mjs";import"./createElementId-DhjFt1I9-Bnrf3uOu.chunk.mjs";import"./NcModal-DiDvgcU8-dbt7Dqpx.chunk.mjs";/* empty css */import"./NcAvatar-1KxMUN7V-CO2gVIA6.chunk.mjs";import"./index-BUgWwKkm.chunk.mjs";import"./util-DBjHxD9z.chunk.mjs";import"./ArrowRight-BaRJ4rMK.chunk.mjs";import"./colors-Cv9F-jWS-CXrLxPaW.chunk.mjs";import"./NcUserStatusIcon-BF5OEQFU-B7JEw9np.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-BJuPH7S7-Dclz8r5l.chunk.mjs";import"./TrashCanOutline-DQrHTd8q.chunk.mjs";import"./NcUserBubble-BXR7vT0V-DLtND5ia.chunk.mjs";import"./index-B8Ex2jDj.chunk.mjs";import"./index-B4Pb_6Dn.chunk.mjs";import"./mdi-CW3n8UeZ.chunk.mjs";import"./activity-BZ6xFlZg.chunk.mjs";import"./GetComments-CFK5MR0g.chunk.mjs";import"./index-NAGd6Ttu.chunk.mjs";const d={name:"ActivityCommentEntry",components:{Comment:a},mixins:[p],props:{comment:{type:Object,required:!0},reloadCallback:{type:Function,required:!0}},data(){return{commentMessage:""}},watch:{comment(){this.commentMessage=this.comment.props.message}},mounted(){this.commentMessage=this.comment.props.message},methods:{t:s}};function g(t,e,o,f,m,C){const r=n("Comment");return c(),u(r,l({ref:"comment",tag:"li"},o.comment.props,{autoComplete:t.autoComplete,resourceType:t.resourceType,message:m.commentMessage,resourceId:t.resourceId,userData:t.genMentionsData(o.comment.props.mentions),class:"comments-activity",onDelete:e[0]||(e[0]=y=>o.reloadCallback())}),null,16,["autoComplete","resourceType","message","resourceId","userData"])}const Q=i(d,[["render",g],["__scopeId","data-v-afc310f1"]]);export{Q as default};
//# sourceMappingURL=ActivityCommentEntry-C_k66IhQ.chunk.mjs.map
Loading
Loading