Skip to content

Commit

Permalink
fix(files): group duplicate shares
Browse files Browse the repository at this point in the history
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Jan 11, 2024
1 parent b77afb4 commit e4fdd24
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 25 deletions.
70 changes: 48 additions & 22 deletions apps/files/src/actions/deleteAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import { action } from './deleteAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
import * as auth from '@nextcloud/auth'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import logger from '../logger'
Expand All @@ -37,26 +38,51 @@ const trashbinView = {
} as View

describe('Delete action conditions tests', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.ALL,
})

const file2 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'user2',
mime: 'text/plain',
permissions: Permission.ALL,
})

test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('delete')
expect(action.displayName([], view)).toBe('Delete')
expect(action.displayName([file], view)).toBe('Delete')
expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>')
expect(action.default).toBeUndefined()
expect(action.order).toBe(100)
})

test('Default trashbin view values', () => {
expect(action.displayName([], trashbinView)).toBe('Delete permanently')
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
})

test('Shared node view values', () => {
jest.spyOn(auth, 'getCurrentUser').mockReturnValue(null)
expect(action.displayName([file2], view)).toBe('Unshare')
})

test('Shared and owned nodes view values', () => {
expect(action.displayName([file, file2], view)).toBe('Delete and unshare')
})
})

describe('Delete action enabled tests', () => {
test('Enabled with DELETE permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.ALL,
})
Expand All @@ -68,8 +94,8 @@ describe('Delete action enabled tests', () => {
test('Disabled without DELETE permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ,
})
Expand All @@ -86,14 +112,14 @@ describe('Delete action enabled tests', () => {
test('Disabled if not all nodes can be deleted', () => {
const folder1 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/Foo/',
owner: 'test',
permissions: Permission.DELETE,
})
const folder2 = new Folder({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/Bar/',
owner: 'test',
permissions: Permission.READ,
})

Expand All @@ -111,8 +137,8 @@ describe('Delete action execute tests', () => {

const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
Expand All @@ -121,7 +147,7 @@ describe('Delete action execute tests', () => {

expect(exec).toBe(true)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/test/foobar.txt')

expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
Expand All @@ -133,16 +159,16 @@ describe('Delete action execute tests', () => {

const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})

const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
Expand All @@ -151,8 +177,8 @@ describe('Delete action execute tests', () => {

expect(exec).toStrictEqual([true, true])
expect(axios.delete).toBeCalledTimes(2)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt')
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')

expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
Expand All @@ -165,8 +191,8 @@ describe('Delete action execute tests', () => {

const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
owner: 'test',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
Expand All @@ -175,7 +201,7 @@ describe('Delete action execute tests', () => {

expect(exec).toBe(false)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/test/foobar.txt')

expect(eventBus.emit).toBeCalledTimes(0)
expect(logger.error).toBeCalledTimes(1)
Expand Down
10 changes: 8 additions & 2 deletions apps/files/src/actions/deleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ import CloseSvg from '@mdi/svg/svg/close.svg?raw'
import logger from '../logger.js'
import { getCurrentUser } from '@nextcloud/auth'

// A file which the owner is NOT the current user
// is considered shared. Other methods like group folders
// will always use the current user as the owner.
// It will therefore not be considered shared.
const isAllUnshare = (nodes: Node[]) => {
return !nodes.some(node => node.owner === getCurrentUser()?.uid)
return !nodes.some(node => node.owner !== getCurrentUser()?.uid)
}

// Check whether the selection contains a mix of files
// the current user owns and files owned by other users.
const isMixedUnshareAndDelete = (nodes: Node[]) => {
const hasUnshareItems = nodes.some(node => node.owner !== getCurrentUser()?.uid)
const hasDeleteItems = nodes.some(node => node.owner === getCurrentUser()?.uid)
Expand Down Expand Up @@ -73,7 +79,7 @@ export const action = new FileAction({

// Let's delete even if it's moved to the trashbin
// since it has been removed from the current view
// and changing the view will trigger a reload anyway.
// and changing the view will trigger a reload anyway.
emit('files:node:deleted', node)
return true
} catch (error) {
Expand Down
19 changes: 19 additions & 0 deletions apps/files_sharing/src/actions/sharingStatusAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export const action = new FileAction({
const ownerId = node?.attributes?.['owner-id']
const ownerDisplayName = node?.attributes?.['owner-display-name']

// Mixed share types
if (Array.isArray(node.attributes?.['share-types'])) {
return t('files_sharing', 'Shared multiple times with different people')
}

if (ownerId && ownerId !== getCurrentUser()?.uid) {
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName })
}
Expand All @@ -73,6 +78,11 @@ export const action = new FileAction({
const node = nodes[0]
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]

// Mixed share types
if (Array.isArray(node.attributes?.['share-types'])) {
return AccountPlusSvg
}

// Link shares
if (shareTypes.includes(Type.SHARE_TYPE_LINK)
|| shareTypes.includes(Type.SHARE_TYPE_EMAIL)) {
Expand Down Expand Up @@ -105,6 +115,15 @@ export const action = new FileAction({

const node = nodes[0]
const ownerId = node?.attributes?.['owner-id']
const isMixed = Array.isArray(node.attributes?.['share-types'])

// If the node is shared multiple times with
// different share types to the current user
if (isMixed) {
return true
}

// If the node is shared by someone else
if (ownerId && ownerId !== getCurrentUser()?.uid) {
return true
}
Expand Down
26 changes: 25 additions & 1 deletion apps/files_sharing/src/services/SharingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
attributes: {
...ocsEntry,
'has-preview': hasPreview,
// Also check the sharingStatusAction.ts code
'owner-id': ocsEntry?.uid_owner,
'owner-display-name': ocsEntry?.displayname_owner,
'share-types': ocsEntry?.share_type,
favorite: ocsEntry?.tags?.includes(window.OC.TAG_FAVORITE) ? 1 : 0,
},
})
Expand Down Expand Up @@ -144,6 +148,17 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> {
})
}

/**
* Group an array of objects (here Nodes) by a key
* and return an array of arrays of them.
*/
const groupBy = function(nodes: (Folder | File)[], key: string) {
return Object.values(nodes.reduce(function(acc, curr) {
(acc[curr[key]] = acc[curr[key]] || []).push(curr)
return acc
}, {})) as (Folder | File)[][]
}

export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
const promises = [] as AxiosPromise<OCSResponse<any>>[]

Expand All @@ -162,12 +177,21 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true,

const responses = await Promise.all(promises)
const data = responses.map((response) => response.data.ocs.data).flat()
let contents = data.map(ocsEntryToNode).filter((node) => node !== null) as (Folder | File)[]
let contents = data.map(ocsEntryToNode)
.filter((node) => node !== null) as (Folder | File)[]

if (filterTypes.length > 0) {
contents = contents.filter((node) => filterTypes.includes(node.attributes?.share_type))
}

// Merge duplicate shares and group their attributes
// Also check the sharingStatusAction.ts code
contents = groupBy(contents, 'source').map((nodes) => {
const node = nodes[0]
node.attributes['share-types'] = nodes.map(node => node.attributes['share-types'])
return node
})

return {
folder: new Folder({
id: 0,
Expand Down

0 comments on commit e4fdd24

Please sign in to comment.