Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable28] fix(files): add mount root property and adjust delete wording #43404

Merged
merged 1 commit into from
Feb 7, 2024
Merged
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: 11 additions & 0 deletions apps/dav/lib/Connector/Sabre/FilesPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class FilesPlugin extends ServerPlugin {
public const DATA_FINGERPRINT_PROPERTYNAME = '{http://owncloud.org/ns}data-fingerprint';
public const HAS_PREVIEW_PROPERTYNAME = '{http://nextcloud.org/ns}has-preview';
public const MOUNT_TYPE_PROPERTYNAME = '{http://nextcloud.org/ns}mount-type';
public const MOUNT_ROOT_PROPERTYNAME = '{http://nextcloud.org/ns}is-mount-root';
public const IS_ENCRYPTED_PROPERTYNAME = '{http://nextcloud.org/ns}is-encrypted';
public const METADATA_ETAG_PROPERTYNAME = '{http://nextcloud.org/ns}metadata_etag';
public const UPLOAD_TIME_PROPERTYNAME = '{http://nextcloud.org/ns}upload_time';
Expand Down Expand Up @@ -361,6 +362,16 @@ public function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node)
return $node->getFileInfo()->getMountPoint()->getMountType();
});

/**
* This is a special property which is used to determine if a node
* is a mount root or not, e.g. a shared folder.
* If so, then the node can only be unshared and not deleted.
* @see https://github.com/nextcloud/server/blob/cc75294eb6b16b916a342e69998935f89222619d/lib/private/Files/View.php#L696-L698
*/
$propFind->handle(self::MOUNT_ROOT_PROPERTYNAME, function () use ($node) {
return $node->getNode()->getInternalPath() === '' ? 'true' : 'false';
});

$propFind->handle(self::SHARE_NOTE, function () use ($node, $httpRequest): ?string {
$user = $this->userSession->getUser();
if ($user === null) {
Expand Down
81 changes: 63 additions & 18 deletions apps/files/src/actions/deleteAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
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'

const view = {
Expand All @@ -50,36 +50,81 @@ describe('Delete action conditions tests', () => {
permissions: Permission.ALL,
})

// const file2 = new File({
// id: 1,
// source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
// owner: 'admin',
// mime: 'text/plain',
// permissions: Permission.ALL,
// })
const file2 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'is-mount-root': true,
'mount-type': 'shared',
},
})

const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})

const folder2 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'is-mount-root': true,
'mount-type': 'shared',
},
})

const folder3 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
attributes: {
'is-mount-root': true,
'mount-type': 'external',
},
})

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

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

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

// TODO: Fix this test
// test('Shared node values', () => {
// jest.spyOn(auth, 'getCurrentUser').mockReturnValue(null)
// expect(action.displayName([file2], view)).toBe('Unshare')
// })
test('Shared root node displayName', () => {
expect(action.displayName([file2], view)).toBe('Leave this share')
expect(action.displayName([folder2], view)).toBe('Leave this share')
expect(action.displayName([file2, folder2], view)).toBe('Leave these shares')
})

test('External storage root node displayName', () => {
expect(action.displayName([folder3], view)).toBe('Disconnect storage')
expect(action.displayName([folder3, folder3], view)).toBe('Disconnect storages')
})

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

describe('Delete action enabled tests', () => {
Expand Down
93 changes: 87 additions & 6 deletions apps/files/src/actions/deleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,102 @@
*
*/
import { emit } from '@nextcloud/event-bus'
import { Permission, Node, View, FileAction } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import { Permission, Node, View, FileAction, FileType } from '@nextcloud/files'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'

import CloseSvg from '@mdi/svg/svg/close.svg?raw'
import NetworkOffSvg from '@mdi/svg/svg/network-off.svg?raw'
import TrashCanSvg from '@mdi/svg/svg/trash-can.svg?raw'

import logger from '../logger.js'

const canUnshareOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'shared')
}

const canDisconnectOnly = (nodes: Node[]) => {
return nodes.every(node => node.attributes['is-mount-root'] === true
&& node.attributes['mount-type'] === 'external')
}

const isMixedUnshareAndDelete = (nodes: Node[]) => {
if (nodes.length === 1) {
return false
}

const hasSharedItems = nodes.some(node => canUnshareOnly([node]))
const hasDeleteItems = nodes.some(node => !canUnshareOnly([node]))
return hasSharedItems && hasDeleteItems
}

const isAllFiles = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.File)
}

const isAllFolders = (nodes: Node[]) => {
return !nodes.some(node => node.type !== FileType.Folder)
}

export const action = new FileAction({
id: 'delete',
displayName(nodes: Node[], view: View) {
return view.id === 'trashbin'
? t('files', 'Delete permanently')
: t('files', 'Delete')
/**
* If we're in the trashbin, we can only delete permanently
*/
if (view.id === 'trashbin') {
return t('files', 'Delete permanently')
}

/**
* If we're in the sharing view, we can only unshare
*/
if (isMixedUnshareAndDelete(nodes)) {
return t('files', 'Delete and unshare')
}

/**
* If those nodes are all the root node of a
* share, we can only unshare them.
*/
if (canUnshareOnly(nodes)) {
return n('files', 'Leave this share', 'Leave these shares', nodes.length)
}

/**
* If those nodes are all the root node of an
* external storage, we can only disconnect it.
*/
if (canDisconnectOnly(nodes)) {
return n('files', 'Disconnect storage', 'Disconnect storages', nodes.length)
}

/**
* If we're only selecting files, use proper wording
*/
if (isAllFiles(nodes)) {
return n('files', 'Delete file', 'Delete files', nodes.length)
}

/**
* If we're only selecting folders, use proper wording
*/
if (isAllFolders(nodes)) {
return n('files', 'Delete folder', 'Delete folders', nodes.length)
}

return t('files', 'Delete')

Check warning on line 108 in apps/files/src/actions/deleteAction.ts

View check run for this annotation

Codecov / codecov/patch

apps/files/src/actions/deleteAction.ts#L108

Added line #L108 was not covered by tests
},
iconSvgInline: () => {
iconSvgInline: (nodes: Node[]) => {
if (canUnshareOnly(nodes)) {
return CloseSvg
}

if (canDisconnectOnly(nodes)) {
return NetworkOffSvg

Check warning on line 116 in apps/files/src/actions/deleteAction.ts

View check run for this annotation

Codecov / codecov/patch

apps/files/src/actions/deleteAction.ts#L116

Added line #L116 was not covered by tests
}

return TrashCanSvg
},

Expand Down
1 change: 1 addition & 0 deletions apps/files/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ registerRecentView()
registerPreviewServiceWorker()

registerDavProperty('nc:hidden', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('nc:is-mount-root', { nc: 'http://nextcloud.org/ns' })

initLivePhotos()
5 changes: 5 additions & 0 deletions apps/files_sharing/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { registerDavProperty } from '@nextcloud/files'
import registerSharingViews from './views/shares'

import './actions/acceptShareAction'
Expand All @@ -29,3 +30,7 @@ import './actions/restoreShareAction'
import './actions/sharingStatusAction'

registerSharingViews()

registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' })
registerDavProperty('oc:share-types', { oc: 'http://owncloud.org/ns' })
registerDavProperty('ocs:share-permissions', { ocs: 'http://open-collaboration-services.org/ns' })
4 changes: 2 additions & 2 deletions dist/files-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files-init.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/files_sharing-init.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/files_sharing-init.js.map

Large diffs are not rendered by default.

Loading