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
48 changes: 35 additions & 13 deletions apps/files/src/actions/viewInFolderAction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ const viewFiles = {
name: 'Files',
} as IView

const viewSearch = {
id: 'search',
name: 'Search',
} as IView

const rootFolder = new Folder({
id: 10,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/',
owner: 'admin',
permissions: Permission.ALL,
root: '/files/admin',
})

const nestedFolder = new Folder({
id: 11,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar/',
owner: 'admin',
permissions: Permission.ALL,
root: '/files/admin',
})

describe('View in folder action conditions tests', () => {
test('Default values', () => {
expect(action.id).toBe('view-in-folder')
Expand All @@ -41,10 +62,10 @@ describe('View in folder action conditions tests', () => {
})

describe('View in folder action enabled tests', () => {
test('Enabled for trashbin', () => {
test('Enabled when search result is shown outside its parent folder', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
Expand All @@ -54,16 +75,16 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
view: viewSearch,
folder: rootFolder,
contents: [],
})).toBe(true)
})

test('Disabled for files', () => {
test('Disabled when file is already shown in its parent folder', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
Expand All @@ -74,7 +95,7 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view: viewFiles,
folder: {} as Folder,
folder: nestedFolder,
contents: [],
})).toBe(false)
})
Expand All @@ -93,7 +114,7 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
folder: nestedFolder,
contents: [],
})).toBe(false)
})
Expand All @@ -111,7 +132,7 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
folder: nestedFolder,
contents: [],
})).toBe(false)
})
Expand All @@ -136,7 +157,7 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [file1, file2],
view,
folder: {} as Folder,
folder: nestedFolder,
contents: [],
})).toBe(false)
})
Expand All @@ -154,16 +175,17 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [folder],
view,
folder: {} as Folder,
folder: rootFolder,
contents: [],
})).toBe(false)
})

test('Disabled for files outside the user root folder', () => {
const file = new Folder({
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/trashbin/admin/trash/image.jpg.d1731053878',
owner: 'admin',
mime: 'image/jpeg',
permissions: Permission.READ,
root: '/trashbin/admin',
})
Expand All @@ -172,7 +194,7 @@ describe('View in folder action enabled tests', () => {
expect(action.enabled!({
nodes: [file],
view,
folder: {} as Folder,
folder: rootFolder,
contents: [],
})).toBe(false)
})
Expand Down
12 changes: 6 additions & 6 deletions apps/files/src/actions/viewInFolderAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,12 @@ export const action: IFileAction = {
},
iconSvgInline: () => FolderEyeSvg,

enabled({ nodes, view }) {
enabled({ nodes, folder }) {
// Not enabled for public shares
if (isPublicShare()) {
return false
}

// Only works outside of the main files view
if (view.id === 'files') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when is this needed? Meaning when is this action ever useful in the files view?
As the files view is already the action target view this would end up in?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When doing search this action never appears, even though the file is within some xyz folder, and instead of just the file. I want to go to the actual folder. So instead of just disabling by view.id files, we can check if we are in the same folder as actual file, then no need to show this.

return false
}

// Only works on single node
if (nodes.length !== 1 || !nodes[0]) {
return false
Expand All @@ -43,6 +38,11 @@ export const action: IFileAction = {
return false
}

// Only show if not in same folder
if (folder.path === node.dirname) {
return false
}

if (node.permissions === Permission.NONE) {
return false
}
Expand Down
124 changes: 124 additions & 0 deletions apps/files/src/components/FilesSidebar/FilesSidebarSubname.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { IFolder } from '@nextcloud/files'

import { File, Permission } from '@nextcloud/files'
import { enableAutoDestroy, shallowMount } from '@vue/test-utils'
import { afterEach, describe, expect, test } from 'vitest'
import FilesSidebarSubname from './FilesSidebarSubname.vue'

enableAutoDestroy(afterEach)

describe('FilesSidebarSubname', () => {
const folder = { path: '/Current' } as IFolder

test('renders path in a separate section from metadata', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/report.pdf',
owner: 'admin',
mime: 'application/pdf',
permissions: Permission.READ,
root: '/files/admin',
size: 1024,
})

const wrapper = shallowMount(FilesSidebarSubname, {
propsData: { folder, node: file },
})
const sections = wrapper.element.children

expect(sections).toHaveLength(2)
expect(sections[0].textContent).toContain('Location')
expect(sections[0].textContent).toContain('/report.pdf')
expect(sections[1].textContent).toContain('1 KB')
})

test('renders full path for nested file', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Documents/Projects/report.pdf',
owner: 'admin',
mime: 'application/pdf',
permissions: Permission.READ,
root: '/files/admin',
size: 1024,
})

const wrapper = shallowMount(FilesSidebarSubname, {
propsData: { folder, node: file },
})

expect(wrapper.text()).toContain('/Documents/Projects/report.pdf')
})

test('renders full path for root-level file', () => {
const file = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/report.pdf',
owner: 'admin',
mime: 'application/pdf',
permissions: Permission.READ,
root: '/files/admin',
size: 1024,
})

const wrapper = shallowMount(FilesSidebarSubname, {
propsData: { folder, node: file },
})

expect(wrapper.text()).toContain('/report.pdf')
})

test('updates path when selected node changes', async () => {
const firstFile = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/First.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
root: '/files/admin',
})
const secondFile = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Documents/Second.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
root: '/files/admin',
})

const wrapper = shallowMount(FilesSidebarSubname, {
propsData: { folder, node: firstFile },
})
expect(wrapper.text()).toContain('/First.txt')

await wrapper.setProps({ node: secondFile })

expect(wrapper.text()).not.toContain('/First.txt')
expect(wrapper.text()).toContain('/Documents/Second.txt')
})

test('does not render location for a file in the current folder', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Documents/report.pdf',
owner: 'admin',
mime: 'application/pdf',
permissions: Permission.READ,
root: '/files/admin',
size: 1024,
})

const wrapper = shallowMount(FilesSidebarSubname, {
propsData: { folder: { path: '/Documents' } as IFolder, node: file },
})

expect(wrapper.text()).not.toContain('Location')
expect(wrapper.text()).not.toContain('/Documents/report.pdf')
expect(wrapper.text()).toContain('1 KB')
})
})
68 changes: 48 additions & 20 deletions apps/files/src/components/FilesSidebar/FilesSidebarSubname.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-->

<script setup lang="ts">
import type { INode } from '@nextcloud/files'
import type { IFolder, INode } from '@nextcloud/files'

import { mdiStar } from '@mdi/js'
import { formatFileSize } from '@nextcloud/files'
Expand All @@ -14,40 +14,68 @@ import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import NcUserBubble from '@nextcloud/vue/components/NcUserBubble'

const props = defineProps<{ node: INode }>()
const props = defineProps<{
folder: IFolder
node: INode
}>()

const isFavourited = computed(() => props.node.attributes.favorite === 1)
const showLocation = computed(() => props.folder.path !== props.node.dirname)
const size = computed(() => formatFileSize(props.node.size ?? 0))
</script>

<template>
<div :class="$style.filesSidebarSubname">
<NcIconSvgWrapper
v-if="isFavourited"
inline
:path="mdiStar"
:name="t('files', 'Favorite')" />
<div v-if="showLocation" :class="$style.filesSidebarSubname__path">
<span :class="$style.filesSidebarSubname__label">{{ t('files', 'Location') }}:</span>
{{ node.path }}
</div>

<span>{{ size }}</span>
<div :class="$style.filesSidebarSubname__metadata">
<NcIconSvgWrapper
v-if="isFavourited"
inline
:path="mdiStar"
:name="t('files', 'Favorite')" />

<span v-if="node.mtime">
<span :class="$style.filesSidebarSubname__separator">•</span>
<NcDateTime :timestamp="node.mtime" />
</span>
<span>{{ size }}</span>

<template v-if="node.owner">
<span :class="$style.filesSidebarSubname__separator">•</span>
<NcUserBubble
:class="$style.filesSidebarSubname__userBubble"
:title="t('files', 'Owner')"
:user="node.owner"
:display-name="node.attributes['owner-display-name']" />
</template>
<span v-if="node.mtime">
<span :class="$style.filesSidebarSubname__separator">•</span>
<NcDateTime :timestamp="node.mtime" />
</span>

<template v-if="node.owner">
<span :class="$style.filesSidebarSubname__separator">•</span>
<NcUserBubble
:class="$style.filesSidebarSubname__userBubble"
:title="t('files', 'Owner')"
:user="node.owner"
:display-name="node.attributes['owner-display-name']" />
</template>
</div>
</div>
</template>

<style module>
.filesSidebarSubname {
display: flex;
flex-direction: column;
gap: 4px;
}

.filesSidebarSubname__path {
display: flex;
flex-direction: column;
width: 100%;
}

.filesSidebarSubname__label {
color: var(--color-text-maxcontrast);
font-weight: bold;
}

.filesSidebarSubname__metadata {
display: flex;
align-items: center;
flex-wrap: wrap;
Expand Down
6 changes: 4 additions & 2 deletions apps/files/src/views/FilesSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@ function onToggle(open: boolean) {
@closed="onClosed"
@opened="onOpened"
@update:open="onToggle">
<template v-if="sidebar.currentNode" #subname>
<FilesSidebarSubname :node="sidebar.currentNode" />
<template v-if="sidebar.currentNode && sidebar.currentContext" #subname>
<FilesSidebarSubname
:folder="sidebar.currentContext.folder"
:node="sidebar.currentNode" />
</template>

<!-- Actions menu -->
Expand Down