Skip to content

Commit 286f2e0

Browse files
authored
Merge pull request #42703 from nextcloud/backport/42584/stable28
2 parents 7832559 + 99d3f13 commit 286f2e0

86 files changed

Lines changed: 247 additions & 149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/files/src/actions/deleteAction.spec.ts

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import { action } from './deleteAction'
2323
import { expect } from '@jest/globals'
2424
import { File, Folder, Permission, View, FileAction } from '@nextcloud/files'
25+
import * as auth from '@nextcloud/auth'
2526
import * as eventBus from '@nextcloud/event-bus'
2627
import axios from '@nextcloud/axios'
2728
import logger from '../logger'
@@ -37,26 +38,55 @@ const trashbinView = {
3738
} as View
3839

3940
describe('Delete action conditions tests', () => {
41+
afterEach(() => {
42+
jest.restoreAllMocks()
43+
})
44+
45+
const file = new File({
46+
id: 1,
47+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
48+
owner: 'test',
49+
mime: 'text/plain',
50+
permissions: Permission.ALL,
51+
})
52+
53+
const file2 = new File({
54+
id: 1,
55+
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
56+
owner: 'admin',
57+
mime: 'text/plain',
58+
permissions: Permission.ALL,
59+
})
60+
4061
test('Default values', () => {
4162
expect(action).toBeInstanceOf(FileAction)
4263
expect(action.id).toBe('delete')
43-
expect(action.displayName([], view)).toBe('Delete')
64+
expect(action.displayName([file], view)).toBe('Delete')
4465
expect(action.iconSvgInline([], view)).toBe('<svg>SvgMock</svg>')
4566
expect(action.default).toBeUndefined()
4667
expect(action.order).toBe(100)
4768
})
4869

4970
test('Default trashbin view values', () => {
50-
expect(action.displayName([], trashbinView)).toBe('Delete permanently')
71+
expect(action.displayName([file], trashbinView)).toBe('Delete permanently')
72+
})
73+
74+
test('Shared node values', () => {
75+
jest.spyOn(auth, 'getCurrentUser').mockReturnValue(null)
76+
expect(action.displayName([file2], view)).toBe('Unshare')
77+
})
78+
79+
test('Shared and owned nodes values', () => {
80+
expect(action.displayName([file, file2], view)).toBe('Delete and unshare')
5181
})
5282
})
5383

5484
describe('Delete action enabled tests', () => {
5585
test('Enabled with DELETE permissions', () => {
5686
const file = new File({
5787
id: 1,
58-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
59-
owner: 'admin',
88+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
89+
owner: 'test',
6090
mime: 'text/plain',
6191
permissions: Permission.ALL,
6292
})
@@ -68,8 +98,8 @@ describe('Delete action enabled tests', () => {
6898
test('Disabled without DELETE permissions', () => {
6999
const file = new File({
70100
id: 1,
71-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
72-
owner: 'admin',
101+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
102+
owner: 'test',
73103
mime: 'text/plain',
74104
permissions: Permission.READ,
75105
})
@@ -86,14 +116,14 @@ describe('Delete action enabled tests', () => {
86116
test('Disabled if not all nodes can be deleted', () => {
87117
const folder1 = new Folder({
88118
id: 1,
89-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
90-
owner: 'admin',
119+
source: 'https://cloud.domain.com/remote.php/dav/files/test/Foo/',
120+
owner: 'test',
91121
permissions: Permission.DELETE,
92122
})
93123
const folder2 = new Folder({
94124
id: 2,
95-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
96-
owner: 'admin',
125+
source: 'https://cloud.domain.com/remote.php/dav/files/test/Bar/',
126+
owner: 'test',
97127
permissions: Permission.READ,
98128
})
99129

@@ -111,8 +141,8 @@ describe('Delete action execute tests', () => {
111141

112142
const file = new File({
113143
id: 1,
114-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
115-
owner: 'admin',
144+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
145+
owner: 'test',
116146
mime: 'text/plain',
117147
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
118148
})
@@ -121,7 +151,7 @@ describe('Delete action execute tests', () => {
121151

122152
expect(exec).toBe(true)
123153
expect(axios.delete).toBeCalledTimes(1)
124-
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
154+
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/test/foobar.txt')
125155

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

134164
const file1 = new File({
135165
id: 1,
136-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
137-
owner: 'admin',
166+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt',
167+
owner: 'test',
138168
mime: 'text/plain',
139169
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
140170
})
141171

142172
const file2 = new File({
143173
id: 2,
144-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
145-
owner: 'admin',
174+
source: 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt',
175+
owner: 'test',
146176
mime: 'text/plain',
147177
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
148178
})
@@ -151,8 +181,8 @@ describe('Delete action execute tests', () => {
151181

152182
expect(exec).toStrictEqual([true, true])
153183
expect(axios.delete).toBeCalledTimes(2)
154-
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt')
155-
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt')
184+
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/test/foo.txt')
185+
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/test/bar.txt')
156186

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

166196
const file = new File({
167197
id: 1,
168-
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
169-
owner: 'admin',
198+
source: 'https://cloud.domain.com/remote.php/dav/files/test/foobar.txt',
199+
owner: 'test',
170200
mime: 'text/plain',
171201
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
172202
})
@@ -175,7 +205,7 @@ describe('Delete action execute tests', () => {
175205

176206
expect(exec).toBe(false)
177207
expect(axios.delete).toBeCalledTimes(1)
178-
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
208+
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/test/foobar.txt')
179209

180210
expect(eventBus.emit).toBeCalledTimes(0)
181211
expect(logger.error).toBeCalledTimes(1)

apps/files/src/actions/deleteAction.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,42 @@ import { Permission, Node, View, FileAction } from '@nextcloud/files'
2424
import { translate as t } from '@nextcloud/l10n'
2525
import axios from '@nextcloud/axios'
2626
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'
27+
import CloseSvg from '@mdi/svg/svg/close.svg?raw'
2728

2829
import logger from '../logger.js'
30+
import { getCurrentUser } from '@nextcloud/auth'
31+
32+
const isAllUnshare = (nodes: Node[]) => {
33+
return !nodes.some(node => node.owner === getCurrentUser()?.uid)
34+
}
35+
36+
const isMixedUnshareAndDelete = (nodes: Node[]) => {
37+
const hasUnshareItems = nodes.some(node => node.owner !== getCurrentUser()?.uid)
38+
const hasDeleteItems = nodes.some(node => node.owner === getCurrentUser()?.uid)
39+
return hasUnshareItems && hasDeleteItems
40+
}
2941

3042
export const action = new FileAction({
3143
id: 'delete',
3244
displayName(nodes: Node[], view: View) {
45+
if (isMixedUnshareAndDelete(nodes)) {
46+
return t('files', 'Delete and unshare')
47+
}
48+
49+
if (isAllUnshare(nodes)) {
50+
return t('files', 'Unshare')
51+
}
52+
3353
return view.id === 'trashbin'
3454
? t('files', 'Delete permanently')
3555
: t('files', 'Delete')
3656
},
37-
iconSvgInline: () => TrashCanSvg,
57+
iconSvgInline: (nodes: Node[]) => {
58+
if (isAllUnshare(nodes)) {
59+
return CloseSvg
60+
}
61+
return TrashCanSvg
62+
},
3863

3964
enabled(nodes: Node[]) {
4065
return nodes.length > 0 && nodes

apps/files/src/services/Files.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ interface ResponseProps extends DAVResultResponseProps {
4242
export const resultToNode = function(node: FileStat): File | Folder {
4343
const props = node.props as ResponseProps
4444
const permissions = davParsePermissions(props?.permissions)
45-
const owner = getCurrentUser()?.uid as string
45+
const owner = (props['owner-id'] || getCurrentUser()?.uid) as string
4646

4747
const source = generateRemoteUrl('dav' + rootPath + node.filename)
4848
const id = props?.fileid < 0

apps/files_sharing/src/actions/sharingStatusAction.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ export const action = new FileAction({
6262
const ownerId = node?.attributes?.['owner-id']
6363
const ownerDisplayName = node?.attributes?.['owner-display-name']
6464

65+
// Mixed share types
66+
if (Array.isArray(node.attributes?.['share-types'])) {
67+
return t('files_sharing', 'Shared multiple times with different people')
68+
}
69+
6570
if (ownerId && ownerId !== getCurrentUser()?.uid) {
6671
return t('files_sharing', 'Shared by {ownerDisplayName}', { ownerDisplayName })
6772
}
@@ -73,6 +78,11 @@ export const action = new FileAction({
7378
const node = nodes[0]
7479
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
7580

81+
// Mixed share types
82+
if (Array.isArray(node.attributes?.['share-types'])) {
83+
return AccountPlusSvg
84+
}
85+
7686
// Link shares
7787
if (shareTypes.includes(Type.SHARE_TYPE_LINK)
7888
|| shareTypes.includes(Type.SHARE_TYPE_EMAIL)) {
@@ -105,6 +115,15 @@ export const action = new FileAction({
105115

106116
const node = nodes[0]
107117
const ownerId = node?.attributes?.['owner-id']
118+
const isMixed = Array.isArray(node.attributes?.['share-types'])
119+
120+
// If the node is shared multiple times with
121+
// different share types to the current user
122+
if (isMixed) {
123+
return true
124+
}
125+
126+
// If the node is shared by someone else
108127
if (ownerId && ownerId !== getCurrentUser()?.uid) {
109128
return true
110129
}

apps/files_sharing/src/services/SharingService.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
7676
attributes: {
7777
...ocsEntry,
7878
'has-preview': hasPreview,
79+
// Also check the sharingStatusAction.ts code
80+
'owner-id': ocsEntry?.uid_owner,
81+
'owner-display-name': ocsEntry?.displayname_owner,
82+
'share-types': ocsEntry?.share_type,
7983
favorite: ocsEntry?.tags?.includes(window.OC.TAG_FAVORITE) ? 1 : 0,
8084
},
8185
})
@@ -144,6 +148,17 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> {
144148
})
145149
}
146150

151+
/**
152+
* Group an array of objects (here Nodes) by a key
153+
* and return an array of arrays of them.
154+
*/
155+
const groupBy = function(nodes: (Folder | File)[], key: string) {
156+
return Object.values(nodes.reduce(function(acc, curr) {
157+
(acc[curr[key]] = acc[curr[key]] || []).push(curr)
158+
return acc
159+
}, {})) as (Folder | File)[][]
160+
}
161+
147162
export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
148163
const promises = [] as AxiosPromise<OCSResponse<any>>[]
149164

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

163178
const responses = await Promise.all(promises)
164179
const data = responses.map((response) => response.data.ocs.data).flat()
165-
let contents = data.map(ocsEntryToNode).filter((node) => node !== null) as (Folder | File)[]
180+
let contents = data.map(ocsEntryToNode)
181+
.filter((node) => node !== null) as (Folder | File)[]
166182

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

187+
// Merge duplicate shares and group their attributes
188+
// Also check the sharingStatusAction.ts code
189+
contents = groupBy(contents, 'source').map((nodes) => {
190+
const node = nodes[0]
191+
node.attributes['share-types'] = nodes.map(node => node.attributes['share-types'])
192+
return node
193+
})
194+
171195
return {
172196
folder: new Folder({
173197
id: 0,

dist/5925-5925.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/5925-5925.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/comments-comments-app.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/comments-comments-app.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/comments-comments-tab.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)