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
2 changes: 2 additions & 0 deletions packages/web-client/src/sse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export enum MESSAGE_TYPE {
ITEM_TRASHED = 'item-trashed',
ITEM_RESTORED = 'item-restored',
ITEM_MOVED = 'item-moved',
ITEM_FAVORITE_ADDED = 'item-favorite-added',
ITEM_FAVORITE_REMOVED = 'item-favorite-removed',
FOLDER_CREATED = 'folder-created',
SPACE_CREATED = 'space-created',
SPACE_DISABLED = 'space-disabled',
Expand Down
20 changes: 20 additions & 0 deletions packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import {
onSSEFolderCreatedEvent,
onSSEFileTouchedEvent,
onSSEItemMovedEvent,
onSSEItemFavoriteAddedEvent,
onSSEItemFavoriteRemovedEvent,
onSSESpaceMemberAddedEvent,
onSSESpaceMemberRemovedEvent,
onSSESpaceShareUpdatedEvent,
Expand Down Expand Up @@ -964,6 +966,24 @@ export const registerSSEEventListeners = ({
})
)

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_FAVORITE_ADDED, (msg) =>
sseEventWrapper({
topic: MESSAGE_TYPE.ITEM_FAVORITE_ADDED,
msg,
...sseEventWrapperOptions,
method: onSSEItemFavoriteAddedEvent
})
)

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.ITEM_FAVORITE_REMOVED, (msg) =>
sseEventWrapper({
topic: MESSAGE_TYPE.ITEM_FAVORITE_REMOVED,
msg,
...sseEventWrapperOptions,
method: onSSEItemFavoriteRemovedEvent
})
)

clientService.sseAuthenticated.addEventListener(MESSAGE_TYPE.FOLDER_CREATED, (msg) =>
sseEventWrapper({
topic: MESSAGE_TYPE.FOLDER_CREATED,
Expand Down
65 changes: 64 additions & 1 deletion packages/web-runtime/src/container/sse/files.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
createFileRouteOptions,
ImageDimension,
isItemInCurrentFolder
isItemInCurrentFolder,
isLocationCommonActive
} from '@opencloud-eu/web-pkg'
import { SSEEventOptions } from './types'

Expand Down Expand Up @@ -312,3 +313,65 @@ export const onSSEFolderCreatedEvent = async ({

resourcesStore.upsertResource(resource)
}

/**
* Favorites are user specific, hence the favorite state of the resources currently
* loaded only ever needs to be updated for the user receiving the event. While on the
* favorites view the resource additionally needs to be added to or removed from the list.
*/
async function applyFavoriteState(
{ sseData, resourcesStore, spacesStore, userStore, clientService, router }: SSEEventOptions,
starred: boolean
) {
if (sseData.initiatorid === clientService.initiatorId) {
// If initiated by current client (browser tab), action unnecessary. Web manages its own logic, return early.
return
}

if (sseData.affecteduserids?.length && !sseData.affecteduserids.includes(userStore.user?.id)) {
return
}

if (isLocationCommonActive(router, 'files-common-favorites')) {
if (!starred) {
const resource = resourcesStore.resources.find((r) => r.id === sseData.itemid)

if (!resource) {
return
}

return resourcesStore.removeResources([resource])
}

const space = spacesStore.spaces.find((space) => space.id === sseData.spaceid)
if (!space) {
return
}

const resource = await clientService.webdav.getFileInfo(space, {
path: '',
fileId: sseData.itemid
})

if (!resource) {
return
}

return resourcesStore.upsertResource(resource)
}

const currentFolder = resourcesStore.currentFolder
if (currentFolder?.id === sseData.itemid) {
resourcesStore.setCurrentFolder({ ...currentFolder, starred })
}

resourcesStore.updateResourceField({ id: sseData.itemid, field: 'starred', value: starred })
}

export const onSSEItemFavoriteAddedEvent = (options: SSEEventOptions) => {
return applyFavoriteState(options, true)
}

export const onSSEItemFavoriteRemovedEvent = (options: SSEEventOptions) => {
return applyFavoriteState(options, false)
}
133 changes: 128 additions & 5 deletions packages/web-runtime/tests/unit/container/sse/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ import {
onSSEFileLockingEvent,
onSSEFileTouchedEvent,
onSSEFolderCreatedEvent,
onSSEItemFavoriteAddedEvent,
onSSEItemFavoriteRemovedEvent,
onSSEItemMovedEvent,
onSSEItemRenamedEvent,
onSSEItemRestoredEvent,
onSSEItemTrashedEvent
} from '../../../../src/container/sse'
import { Router } from 'vue-router'
import { RouteLocation } from 'vue-router'
import { mock, mockDeep } from 'vitest-mock-extended'
import { Resource, SpaceResource } from '@opencloud-eu/web-client'
import { createTestingPinia } from '@opencloud-eu/web-test-helpers'
import { User } from '@opencloud-eu/web-client/graph/generated'
import { createTestingPinia, defaultComponentMocks } from '@opencloud-eu/web-test-helpers'
import { Language } from 'vue3-gettext'
import PQueue from 'p-queue'

Expand Down Expand Up @@ -423,6 +426,117 @@ describe('file events', () => {
expect(mocks.resourcesStore.upsertResource).not.toHaveBeenCalled()
})
})

describe('onSSEItemFavoriteAddedEvent', () => {
it('calls "updateResourceField" when resource has been marked as favorite', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ resources: [favoritedResource] })
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId,
affecteduserids: ['1']
})
await onSSEItemFavoriteAddedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.updateResourceField).toHaveBeenCalledWith({
id: favoritedResource.id,
field: 'starred',
value: true
})
})
it('calls "setCurrentFolder" when the current folder has been marked as favorite', async () => {
const mocks = getMocks()
const sseData = mock<EventSchemaType>({
itemid: mocks.resourcesStore.currentFolder.id,
spaceid: 'space1'
})
await onSSEItemFavoriteAddedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.setCurrentFolder).toHaveBeenCalledWith(
expect.objectContaining({ id: mocks.resourcesStore.currentFolder.id, starred: true })
)
})
it('calls "upsertResource" when being on the favorites view', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ currentRouteName: 'files-common-favorites' })
mocks.clientService.webdav.getFileInfo.mockResolvedValue(favoritedResource)
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId
})
await onSSEItemFavoriteAddedEvent({ sseData, ...mocks })
expect(mocks.clientService.webdav.getFileInfo).toHaveBeenCalled()
expect(mocks.resourcesStore.upsertResource).toHaveBeenCalledWith(favoritedResource)
})
it('does not trigger any action when the current user is not affected', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ resources: [favoritedResource] })
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId,
affecteduserids: ['2']
})
await onSSEItemFavoriteAddedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.updateResourceField).not.toHaveBeenCalled()
})
it('does not trigger any action when initiator ids are identical', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ resources: [favoritedResource] })
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId,
initiatorid: 'local1'
})
await onSSEItemFavoriteAddedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.updateResourceField).not.toHaveBeenCalled()
})
})

describe('onSSEItemFavoriteRemovedEvent', () => {
it('calls "updateResourceField" when resource has been unmarked as favorite', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ resources: [favoritedResource] })
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId
})
await onSSEItemFavoriteRemovedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.updateResourceField).toHaveBeenCalledWith({
id: favoritedResource.id,
field: 'starred',
value: false
})
})
it('calls "removeResources" when being on the favorites view', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({
resources: [favoritedResource],
currentRouteName: 'files-common-favorites'
})
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId
})
await onSSEItemFavoriteRemovedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.removeResources).toHaveBeenCalledWith([favoritedResource])
expect(mocks.clientService.webdav.getFileInfo).not.toHaveBeenCalled()
})
it('does not trigger any action when the resource is not loaded on the favorites view', async () => {
const mocks = getMocks({ currentRouteName: 'files-common-favorites' })
const sseData = mock<EventSchemaType>({ itemid: 'file1', spaceid: 'space1' })
await onSSEItemFavoriteRemovedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.removeResources).not.toHaveBeenCalled()
})
it('does not trigger any action when initiator ids are identical', async () => {
const favoritedResource = mock<Resource>({ id: 'file1', storageId: 'space1' })
const mocks = getMocks({ resources: [favoritedResource] })
const sseData = mock<EventSchemaType>({
itemid: favoritedResource.id,
spaceid: favoritedResource.storageId,
initiatorid: 'local1'
})
await onSSEItemFavoriteRemovedEvent({ sseData, ...mocks })
expect(mocks.resourcesStore.updateResourceField).not.toHaveBeenCalled()
})
})
})
const getMocks = ({
currentFolder = mockDeep<Resource>({
Expand All @@ -431,8 +545,14 @@ const getMocks = ({
storageId: 'space1'
}),
resources = [],
spaces = [mockDeep<SpaceResource>({ id: 'space1' })]
}: { currentFolder?: Resource; resources?: Resource[]; spaces?: SpaceResource[] } = {}) => {
spaces = [mockDeep<SpaceResource>({ id: 'space1' })],
currentRouteName = 'files-spaces-generic'
}: {
currentFolder?: Resource
resources?: Resource[]
spaces?: SpaceResource[]
currentRouteName?: string
} = {}) => {
createTestingPinia()
const resourcesStore = useResourcesStore()
resourcesStore.currentFolder = currentFolder
Expand All @@ -441,12 +561,15 @@ const getMocks = ({
spacesStore.spaces = spaces
const messageStore = useMessages()
const userStore = useUserStore()
userStore.user = mockDeep<User>({ id: '1' })
const sharesStore = useSharesStore()
const configStore = useConfigStore()
const authStore = useAuthStore()
const clientService = mockDeep<ClientService>({ initiatorId: 'local1' })
const previewService = mockDeep<PreviewService>()
const router = mockDeep<Router>()
const { $router: router } = defaultComponentMocks({
currentRoute: mock<RouteLocation>({ name: currentRouteName })
})
const language = mockDeep<Language>({
$gettext: vi.fn((m) => m)
})
Expand Down