diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-03-11 11:01:01 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2025-03-13 21:30:43 +0100 |
commit | ede015a4248e6a4304a18e34e475a04d1a5bf717 (patch) | |
tree | e43da88526466e89e6664a296c23650e92d46e23 | |
parent | 04ae68a8f72da18c1731f7c83e059304232c4e38 (diff) | |
download | nextcloud-server-ede015a4248e6a4304a18e34e475a04d1a5bf717.tar.gz nextcloud-server-ede015a4248e6a4304a18e34e475a04d1a5bf717.zip |
fix(files_trashbin): correctly sort custom columns in trashbin view
1. Refactor to make code better testable (move columns and view source to `files_views` folder)
2. Fix deletion time fallback (JS Date vs unix timestamp for
"delted"-column)
3. Correctly sort `deletedBy` and `originalLocation` columns to use
natural sort like any other column
4. Add unit tests for columns and views
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | __tests__/setup-global.js | 7 | ||||
-rw-r--r-- | apps/files_trashbin/src/files-init.ts | 20 | ||||
-rw-r--r-- | apps/files_trashbin/src/files_views/columns.spec.ts | 211 | ||||
-rw-r--r-- | apps/files_trashbin/src/files_views/columns.ts (renamed from apps/files_trashbin/src/columns.ts) | 138 | ||||
-rw-r--r-- | apps/files_trashbin/src/files_views/trashbinView.spec.ts | 52 | ||||
-rw-r--r-- | apps/files_trashbin/src/files_views/trashbinView.ts | 33 | ||||
-rw-r--r-- | vitest.config.ts | 6 |
7 files changed, 387 insertions, 80 deletions
diff --git a/__tests__/setup-global.js b/__tests__/setup-global.js new file mode 100644 index 00000000000..93230b0deab --- /dev/null +++ b/__tests__/setup-global.js @@ -0,0 +1,7 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: CC0-1.0 + */ +export function setup() { + process.env.TZ = 'UTC' +} diff --git a/apps/files_trashbin/src/files-init.ts b/apps/files_trashbin/src/files-init.ts index f516d6f5be5..b4526c97143 100644 --- a/apps/files_trashbin/src/files-init.ts +++ b/apps/files_trashbin/src/files-init.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { trashbinView } from './files_views/trashbinView.ts' import './trashbin.scss' import { translate as t } from '@nextcloud/l10n' @@ -18,23 +19,6 @@ import './actions/restoreAction' import { emptyTrashAction } from './fileListActions/emptyTrashAction.ts' const Navigation = getNavigation() -Navigation.register(new View({ - id: 'trashbin', - name: t('files_trashbin', 'Deleted files'), - caption: t('files_trashbin', 'List of files that have been deleted.'), - - emptyTitle: t('files_trashbin', 'No deleted files'), - emptyCaption: t('files_trashbin', 'Files and folders you have deleted will show up here'), - - icon: DeleteSvg, - order: 50, - sticky: true, - - defaultSortKey: 'deleted', - - columns, - - getContents, -})) +Navigation.register(trashbinView) registerFileListAction(emptyTrashAction) diff --git a/apps/files_trashbin/src/files_views/columns.spec.ts b/apps/files_trashbin/src/files_views/columns.spec.ts new file mode 100644 index 00000000000..12fb1628bb3 --- /dev/null +++ b/apps/files_trashbin/src/files_views/columns.spec.ts @@ -0,0 +1,211 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { File } from '@nextcloud/files' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' +import { deleted, deletedBy, originalLocation } from './columns.ts' +import { trashbinView } from './trashbinView.ts' +import * as ncAuth from '@nextcloud/auth' + +describe('files_trashbin: file list columns', () => { + + describe('column: original location', () => { + it('has id set', () => { + expect(originalLocation.id).toBe('files_trashbin--original-location') + }) + + it('has title set', () => { + expect(originalLocation.title).toBe('Original location') + }) + + it('correctly sorts nodes by original location', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'z-folder/a.txt' } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'folder/b.txt' } }) + + expect(originalLocation.sort).toBeTypeOf('function') + expect(originalLocation.sort!(nodeA, nodeB)).toBeGreaterThan(0) + expect(originalLocation.sort!(nodeB, nodeA)).toBeLessThan(0) + }) + + it('renders a node with original location', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'folder/a.txt' } }) + const el: HTMLElement = originalLocation.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('folder') + expect(el.title).toBe('folder') + }) + + it('renders a node when original location is missing', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' }) + const el: HTMLElement = originalLocation.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('Unknown') + expect(el.title).toBe('Unknown') + }) + + it('renders a node when original location is the root', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-original-location': 'a.txt' } }) + const el: HTMLElement = originalLocation.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('All files') + expect(el.title).toBe('All files') + }) + }) + + describe('column: deleted time', () => { + it('has id set', () => { + expect(deleted.id).toBe('files_trashbin--deleted') + }) + + it('has title set', () => { + expect(deleted.title).toBe('Deleted') + }) + + it('correctly sorts nodes by deleted time', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684522 } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684422 } }) + + expect(deleted.sort).toBeTypeOf('function') + expect(deleted.sort!(nodeA, nodeB)).toBeLessThan(0) + expect(deleted.sort!(nodeB, nodeA)).toBeGreaterThan(0) + }) + + it('correctly sorts nodes by deleted time and falls back to mtime', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684522 } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', mtime: new Date(1741684422000) }) + + expect(deleted.sort).toBeTypeOf('function') + expect(deleted.sort!(nodeA, nodeB)).toBeLessThan(0) + expect(deleted.sort!(nodeB, nodeA)).toBeGreaterThan(0) + }) + + it('correctly sorts nodes even if no deletion date is provided', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', mtime: new Date(1741684422000) }) + + expect(deleted.sort).toBeTypeOf('function') + expect(deleted.sort!(nodeA, nodeB)).toBeGreaterThan(0) + expect(deleted.sort!(nodeB, nodeA)).toBeLessThan(0) + }) + + describe('rendering', () => { + afterAll(() => { + vi.useRealTimers() + }) + + beforeEach(() => { + vi.useFakeTimers({ now: 1741684582000 }) + }) + + it('renders a node with deletion date', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deletion-time': 1741684522 } }) + const el: HTMLElement = deleted.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('a minute ago') + expect(el.title).toBe('March 11, 2025 9:15 AM') + }) + + it('renders a node when deletion date is missing and falls back to mtime', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', mtime: new Date(1741684522000) }) + const el: HTMLElement = deleted.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('a minute ago') + expect(el.title).toBe('March 11, 2025 9:15 AM') + }) + + it('renders a node when deletion date is missing', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' }) + const el: HTMLElement = deleted.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('A long time ago') + }) + }) + + describe('column: deleted by', () => { + it('has id set', () => { + expect(deletedBy.id).toBe('files_trashbin--deleted-by') + }) + + it('has title set', () => { + expect(deletedBy.title).toBe('Deleted by') + }) + + it('correctly sorts nodes by user-id of deleting user', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'zzz' } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'aaa' } }) + + expect(deletedBy.sort).toBeTypeOf('function') + expect(deletedBy.sort!(nodeA, nodeB)).toBeGreaterThan(0) + expect(deletedBy.sort!(nodeB, nodeA)).toBeLessThan(0) + }) + + it('correctly sorts nodes by display name of deleting user', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'zzz' } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'aaa' } }) + + expect(deletedBy.sort).toBeTypeOf('function') + expect(deletedBy.sort!(nodeA, nodeB)).toBeGreaterThan(0) + expect(deletedBy.sort!(nodeB, nodeA)).toBeLessThan(0) + }) + + it('correctly sorts nodes by display name of deleting user before user id', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': '000', 'trashbin-deleted-by-id': 'zzz' } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'aaa', 'trashbin-deleted-by-id': '999' } }) + + expect(deletedBy.sort).toBeTypeOf('function') + expect(deletedBy.sort!(nodeA, nodeB)).toBeLessThan(0) + expect(deletedBy.sort!(nodeB, nodeA)).toBeGreaterThan(0) + }) + + it('correctly sorts nodes even when one is missing', () => { + const nodeA = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'aaa' } }) + const nodeB = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'zzz' } }) + const nodeC = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/b.txt', mime: 'text/plain' }) + + expect(deletedBy.sort).toBeTypeOf('function') + // aaa is less then "Unknown" + expect(deletedBy.sort!(nodeA, nodeC)).toBeLessThan(0) + // zzz is greater than "Unknown" + expect(deletedBy.sort!(nodeB, nodeC)).toBeGreaterThan(0) + }) + + it('renders a node with deleting user', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'user-id' } }) + const el: HTMLElement = deletedBy.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toMatch(/\suser-id\s/) + }) + + it('renders a node with deleting user display name', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-display-name': 'user-name', 'trashbin-deleted-by-id': 'user-id' } }) + const el: HTMLElement = deletedBy.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toMatch(/\suser-name\s/) + }) + + it('renders a node even when information is missing', () => { + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain' }) + const el: HTMLElement = deletedBy.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('Unknown') + }) + + it('renders a node when current user is the deleting user', () => { + vi.spyOn(ncAuth, 'getCurrentUser').mockImplementationOnce(() => ({ + uid: 'user-id', + displayName: 'user-display-name', + isAdmin: false, + })) + + const node = new File({ owner: 'test', source: 'https://example.com/remote.php/dav/files/test/a.txt', mime: 'text/plain', attributes: { 'trashbin-deleted-by-id': 'user-id' } }) + const el: HTMLElement = deletedBy.render(node, trashbinView) + expect(el).toBeInstanceOf(HTMLElement) + expect(el.textContent).toBe('You') + }) + }) + + }) + +}) diff --git a/apps/files_trashbin/src/columns.ts b/apps/files_trashbin/src/files_views/columns.ts index d7c453f63e2..72d3fc61f16 100644 --- a/apps/files_trashbin/src/columns.ts +++ b/apps/files_trashbin/src/files_views/columns.ts @@ -4,56 +4,16 @@ */ import moment from '@nextcloud/moment' -import { Column, Node } from '@nextcloud/files' import { getCurrentUser } from '@nextcloud/auth' +import { Column, Node } from '@nextcloud/files' +import { getCanonicalLocale, getLanguage, translate as t } from '@nextcloud/l10n' import { dirname } from '@nextcloud/paths' -import { translate as t } from '@nextcloud/l10n' import Vue from 'vue' import NcUserBubble from '@nextcloud/vue/dist/Components/NcUserBubble.js' -const parseOriginalLocation = (node: Node): string => { - const path = node.attributes?.['trashbin-original-location'] !== undefined ? String(node.attributes?.['trashbin-original-location']) : null - if (!path) { - return t('files_trashbin', 'Unknown') - } - const dir = dirname(path) - if (dir === path) { // Node is in root folder - return t('files_trashbin', 'All files') - } - return dir.replace(/^\//, '') -} - -interface DeletedBy { - userId: null | string - displayName: null | string - label: null | string -} - -const generateLabel = (userId: null | string, displayName: null | string) => { - const currentUserId = getCurrentUser()?.uid - if (userId === currentUserId) { - return t('files_trashbin', 'You') - } - if (!userId && !displayName) { - return t('files_trashbin', 'Unknown') - } - return null -} - -const parseDeletedBy = (node: Node): DeletedBy => { - const userId = node.attributes?.['trashbin-deleted-by-id'] !== undefined ? String(node.attributes?.['trashbin-deleted-by-id']) : null - const displayName = node.attributes?.['trashbin-deleted-by-display-name'] !== undefined ? String(node.attributes?.['trashbin-deleted-by-display-name']) : null - const label = generateLabel(userId, displayName) - return { - userId, - displayName, - label, - } -} - -const originalLocation = new Column({ - id: 'original-location', +export const originalLocation = new Column({ + id: 'files_trashbin--original-location', title: t('files_trashbin', 'Original location'), render(node) { const originalLocation = parseOriginalLocation(node) @@ -65,12 +25,12 @@ const originalLocation = new Column({ sort(nodeA, nodeB) { const locationA = parseOriginalLocation(nodeA) const locationB = parseOriginalLocation(nodeB) - return locationA.localeCompare(locationB) + return locationA.localeCompare(locationB, [getLanguage(), getCanonicalLocale()], { numeric: true, usage: 'sort' }) }, }) -const deletedBy = new Column({ - id: 'deleted-by', +export const deletedBy = new Column({ + id: 'files_trashbin--deleted-by', title: t('files_trashbin', 'Deleted by'), render(node) { const { userId, displayName, label } = parseDeletedBy(node) @@ -84,23 +44,27 @@ const deletedBy = new Column({ const propsData = { size: 32, user: userId ?? undefined, - displayName: displayName ?? t('files_trashbin', 'Unknown'), + displayName: displayName ?? userId, } const userBubble = new UserBubble({ propsData }).$mount().$el return userBubble as HTMLElement }, sort(nodeA, nodeB) { - const deletedByA = parseDeletedBy(nodeA).label ?? parseDeletedBy(nodeA).displayName ?? t('files_trashbin', 'Unknown') - const deletedByB = parseDeletedBy(nodeB).label ?? parseDeletedBy(nodeB).displayName ?? t('files_trashbin', 'Unknown') - return deletedByA.localeCompare(deletedByB) + const deletedByA = parseDeletedBy(nodeA) + const deletedbyALabel = deletedByA.label ?? deletedByA.displayName ?? deletedByA.userId + const deletedByB = parseDeletedBy(nodeB) + const deletedByBLabel = deletedByB.label ?? deletedByB.displayName ?? deletedByB.userId + // label is set if uid and display name are unset - if label is unset at least uid or display name is set. + return deletedbyALabel!.localeCompare(deletedByBLabel!, [getLanguage(), getCanonicalLocale()], { numeric: true, usage: 'sort' }) }, }) -const deleted = new Column({ - id: 'deleted', +export const deleted = new Column({ + id: 'files_trashbin--deleted', title: t('files_trashbin', 'Deleted'), + render(node) { - const deletionTime = node.attributes?.['trashbin-deletion-time'] + const deletionTime = node.attributes?.['trashbin-deletion-time'] || ((node?.mtime?.getTime() ?? 0) / 1000) const span = document.createElement('span') if (deletionTime) { span.title = moment.unix(deletionTime).format('LLL') @@ -112,15 +76,67 @@ const deleted = new Column({ span.textContent = t('files_trashbin', 'A long time ago') return span }, + sort(nodeA, nodeB) { - const deletionTimeA = nodeA.attributes?.['trashbin-deletion-time'] || nodeA?.mtime || 0 - const deletionTimeB = nodeB.attributes?.['trashbin-deletion-time'] || nodeB?.mtime || 0 + // deletion time is a unix timestamp while mtime is a JS Date -> we need to align the numbers (seconds vs milliseconds) + const deletionTimeA = nodeA.attributes?.['trashbin-deletion-time'] || ((nodeA?.mtime?.getTime() ?? 0) / 1000) + const deletionTimeB = nodeB.attributes?.['trashbin-deletion-time'] || ((nodeB?.mtime?.getTime() ?? 0) / 1000) return deletionTimeB - deletionTimeA }, }) -export const columns = [ - originalLocation, - deletedBy, - deleted, -] +/** + * Get the original file location of a trashbin file. + * + * @param node The node to parse + */ +function parseOriginalLocation(node: Node): string { + const path = stringOrNull(node.attributes?.['trashbin-original-location']) + if (!path) { + return t('files_trashbin', 'Unknown') + } + + const dir = dirname(path) + if (dir === path) { // Node is in root folder + return t('files_trashbin', 'All files') + } + + return dir.replace(/^\//, '') +} + +/** + * Parse a trashbin file to get information about the user that deleted the file. + * + * @param node The node to parse + */ +function parseDeletedBy(node: Node) { + const userId = stringOrNull(node.attributes?.['trashbin-deleted-by-id']) + const displayName = stringOrNull(node.attributes?.['trashbin-deleted-by-display-name']) + + let label: string|undefined + const currentUserId = getCurrentUser()?.uid + if (userId === currentUserId) { + label = t('files_trashbin', 'You') + } + if (!userId && !displayName) { + label = t('files_trashbin', 'Unknown') + } + + return { + userId, + displayName, + label, + } +} + +/** + * If the attribute is given it will be stringified and returned - otherwise null is returned. + * + * @param attribute The attribute to check + */ +function stringOrNull(attribute: unknown): string | null { + if (attribute) { + return String(attribute) + } + return null +} diff --git a/apps/files_trashbin/src/files_views/trashbinView.spec.ts b/apps/files_trashbin/src/files_views/trashbinView.spec.ts new file mode 100644 index 00000000000..7f5a45ee9cd --- /dev/null +++ b/apps/files_trashbin/src/files_views/trashbinView.spec.ts @@ -0,0 +1,52 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { describe, expect, it } from 'vitest' +import isSvg from 'is-svg' + +import { deleted, deletedBy, originalLocation } from './columns' +import { TRASHBIN_VIEW_ID, trashbinView } from './trashbinView.ts' +import { getContents } from '../services/trashbin.ts' + +describe('files_trasbin: trashbin files view', () => { + it('has correct strings', () => { + expect(trashbinView.id).toBe(TRASHBIN_VIEW_ID) + expect(trashbinView.name).toBe('Deleted files') + expect(trashbinView.caption).toBe('List of files that have been deleted.') + expect(trashbinView.emptyTitle).toBe('No deleted files') + expect(trashbinView.emptyCaption).toBe('Files and folders you have deleted will show up here') + }) + + it('sorts by deleted time', () => { + expect(trashbinView.defaultSortKey).toBe('deleted') + }) + + it('is sticky to the bottom in the view list', () => { + expect(trashbinView.sticky).toBe(true) + }) + + it('has order defined', () => { + expect(trashbinView.order).toBeTypeOf('number') + expect(trashbinView.order).toBe(50) + }) + + it('has valid icon', () => { + expect(trashbinView.icon).toBeTypeOf('string') + expect(isSvg(trashbinView.icon)).toBe(true) + }) + + it('has custom columns', () => { + expect(trashbinView.columns).toHaveLength(3) + expect(trashbinView.columns).toEqual([ + originalLocation, + deletedBy, + deleted, + ]) + }) + + it('has get content method', () => { + expect(trashbinView.getContents).toBeTypeOf('function') + expect(trashbinView.getContents).toBe(getContents) + }) +}) diff --git a/apps/files_trashbin/src/files_views/trashbinView.ts b/apps/files_trashbin/src/files_views/trashbinView.ts new file mode 100644 index 00000000000..cb2a83368ea --- /dev/null +++ b/apps/files_trashbin/src/files_views/trashbinView.ts @@ -0,0 +1,33 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { View } from '@nextcloud/files' +import { t } from '@nextcloud/l10n' +import { deleted, deletedBy, originalLocation } from './columns.ts' +import { getContents } from '../services/trashbin.ts' + +import svgDelete from '@mdi/svg/svg/delete.svg?raw' + +export const trashbinView = new View({ + id: 'trashbin', + name: t('files_trashbin', 'Deleted files'), + caption: t('files_trashbin', 'List of files that have been deleted.'), + + emptyTitle: t('files_trashbin', 'No deleted files'), + emptyCaption: t('files_trashbin', 'Files and folders you have deleted will show up here'), + + icon: svgDelete, + order: 50, + sticky: true, + + defaultSortKey: 'deleted', + + columns: [ + originalLocation, + deletedBy, + deleted, + ], + + getContents, +}) diff --git a/vitest.config.ts b/vitest.config.ts index cdf322223bd..690598d9053 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -21,7 +21,11 @@ export default defineConfig({ provider: 'v8', reporter: ['lcov', 'text'], }, - setupFiles: ['__tests__/mock-window.js', '__tests__/setup-testing-library.js'], + setupFiles: [ + '__tests__/mock-window.js', + '__tests__/setup-testing-library.js', + ], + globalSetup: '__tests__/setup-global.js', server: { deps: { inline: [/@nextcloud\//], |