diff --git a/__tests__/navigation.spec.ts b/__tests__/navigation.spec.ts
index 61a0c9ac4..d9e56d833 100644
--- a/__tests__/navigation.spec.ts
+++ b/__tests__/navigation.spec.ts
@@ -4,9 +4,7 @@
*/
import { describe, it, expect, vi } from 'vitest'
import { Navigation, getNavigation } from '../lib/navigation/navigation'
-import { View } from '../lib/navigation/view'
-
-const mockView = (id = 'view', order = 1) => new View({ id, order, name: 'View', icon: '', getContents: () => Promise.reject(new Error()) })
+import { mockView } from './view.spec'
describe('getNavigation', () => {
it('creates a new navigation if needed', () => {
@@ -40,15 +38,23 @@ describe('getNavigation', () => {
describe('Navigation', () => {
it('Can register a view', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
navigation.register(view)
expect(navigation.views).toEqual([view])
})
+ it('Throws when trying to register invalid view', async () => {
+ const navigation = new Navigation()
+ expect(() => {
+ // @ts-expect-error mocking to test invalid input
+ navigation.register({ id: 'someid' })
+ }).toThrowError()
+ })
+
it('Throws when registering the same view twice', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
navigation.register(view)
expect(() => navigation.register(view)).toThrow(/already registered/)
expect(navigation.views).toEqual([view])
@@ -56,7 +62,7 @@ describe('Navigation', () => {
it('Emits update event after registering a view', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
const listener = vi.fn()
navigation.addEventListener('update', listener)
@@ -68,7 +74,7 @@ describe('Navigation', () => {
it('Can remove a view', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
navigation.register(view)
expect(navigation.views).toEqual([view])
navigation.remove(view.id)
@@ -77,7 +83,7 @@ describe('Navigation', () => {
it('Emits update event after removing a view', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
const listener = vi.fn()
navigation.register(view)
navigation.addEventListener('update', listener)
@@ -98,7 +104,7 @@ describe('Navigation', () => {
it('Can set a view as active', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
navigation.register(view)
expect(navigation.active).toBe(null)
@@ -109,7 +115,7 @@ describe('Navigation', () => {
it('Emits event when setting a view as active', async () => {
const navigation = new Navigation()
- const view = mockView()
+ const { view } = mockView()
navigation.register(view)
// add listener
diff --git a/__tests__/view.spec.ts b/__tests__/view.spec.ts
index c990efbf9..04291c128 100644
--- a/__tests__/view.spec.ts
+++ b/__tests__/view.spec.ts
@@ -161,31 +161,7 @@ describe('Invalid View creation', () => {
describe('View creation', () => {
test('Create a View', async () => {
- const folder = new Folder({
- source: 'https://example.org/dav/files/admin/',
- root: '/files/admin',
- owner: 'admin',
- })
- const view = new View({
- id: 'test',
- name: 'Test',
- caption: 'Test caption',
- emptyTitle: 'Test empty title',
- emptyCaption: 'Test empty caption',
- getContents: () => Promise.resolve({ folder, contents: [] }),
- hidden: true,
- icon: '',
- order: 1,
- params: {},
- columns: [],
- emptyView: () => {},
- parent: 'parent',
- sticky: false,
- expanded: false,
- defaultSortKey: 'key',
- loadChildViews: async () => {},
- })
-
+ const { view, folder } = mockView()
expect(view.id).toBe('test')
expect(view.name).toBe('Test')
expect(view.caption).toBe('Test caption')
@@ -205,3 +181,33 @@ describe('View creation', () => {
await expect(view.loadChildViews?.({} as unknown as View)).resolves.toBe(undefined)
})
})
+
+export function mockView() {
+ const folder = new Folder({
+ source: 'https://example.org/dav/files/admin/',
+ root: '/files/admin',
+ owner: 'admin',
+ })
+
+ const view = new View({
+ id: 'test',
+ name: 'Test',
+ caption: 'Test caption',
+ emptyTitle: 'Test empty title',
+ emptyCaption: 'Test empty caption',
+ getContents: () => Promise.resolve({ folder, contents: [] }),
+ hidden: true,
+ icon: '',
+ order: 1,
+ params: {},
+ columns: [],
+ emptyView: () => {},
+ parent: 'parent',
+ sticky: false,
+ expanded: false,
+ defaultSortKey: 'key',
+ loadChildViews: async () => {},
+ })
+
+ return { folder, view }
+}
diff --git a/lib/fileListHeaders.ts b/lib/fileListHeaders.ts
index f707e1e05..7cd787958 100644
--- a/lib/fileListHeaders.ts
+++ b/lib/fileListHeaders.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { Folder } from './node/folder.ts'
-import type { View } from './navigation/view.ts'
+import type { IFolder } from './node/folder.ts'
+import type { IView } from './navigation/view.ts'
import logger from './utils/logger.ts'
@@ -14,11 +14,11 @@ export interface HeaderData {
/** Order */
order: number
/** Condition wether this header is shown or not */
- enabled?: (folder: Folder, view: View) => boolean
+ enabled?: (folder: IFolder, view: IView) => boolean
/** Executed when file list is initialized */
- render: (el: HTMLElement, folder: Folder, view: View) => void
+ render: (el: HTMLElement, folder: IFolder, view: IView) => void
/** Executed when root folder changed */
- updated(folder: Folder, view: View)
+ updated(folder: IFolder, view: IView)
}
export class Header {
diff --git a/lib/navigation/column.ts b/lib/navigation/column.ts
index 67a0484a8..0a3cbda0e 100644
--- a/lib/navigation/column.ts
+++ b/lib/navigation/column.ts
@@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { Node } from '../node/node.ts'
-import type { View } from './view.ts'
+import type { INode } from '../node/node.ts'
+import type { IView } from './view.ts'
interface ColumnData {
/** Unique column ID */
@@ -12,14 +12,14 @@ interface ColumnData {
/** Translated column title */
title: string
/** The content of the cell. The element will be appended within */
- render: (node: Node, view: View) => HTMLElement
- /** Function used to sort Nodes between them */
- sort?: (nodeA: Node, nodeB: Node) => number
+ render: (node: INode, view: IView) => HTMLElement
+ /** Function used to sort INodes between them */
+ sort?: (nodeA: INode, nodeB: INode) => number
/**
* Custom summary of the column to display at the end of the list.
* Will not be displayed if nothing is provided
*/
- summary?: (node: Node[], view: View) => string
+ summary?: (node: INode[], view: IView) => string
}
export class Column implements ColumnData {
diff --git a/lib/navigation/navigation.ts b/lib/navigation/navigation.ts
index 1a710cb6c..a0408f5bc 100644
--- a/lib/navigation/navigation.ts
+++ b/lib/navigation/navigation.ts
@@ -2,7 +2,10 @@
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import type { View } from './view'
+
+import type { IView } from './view'
+
+import { validateView } from './view'
import { TypedEventTarget } from 'typescript-event-target'
import logger from '../utils/logger'
@@ -10,7 +13,7 @@ import logger from '../utils/logger'
* The event is emitted when the navigation view was updated.
* It contains the new active view in the `detail` attribute.
*/
-interface UpdateActiveViewEvent extends CustomEvent {
+interface UpdateActiveViewEvent extends CustomEvent {
type: 'updateActive'
}
@@ -44,19 +47,22 @@ interface UpdateViewsEvent extends CustomEvent {
*/
export class Navigation extends TypedEventTarget<{ updateActive: UpdateActiveViewEvent, update: UpdateViewsEvent }> {
- private _views: View[] = []
- private _currentView: View | null = null
+ private _views: IView[] = []
+ private _currentView: IView | null = null
/**
* Register a new view on the navigation
* @param view The view to register
- * @throws `Error` is thrown if a view with the same id is already registered
+ * @throws {Error} if a view with the same id is already registered
+ * @throws {Error} if the registered view is invalid
*/
- register(view: View): void {
+ register(view: IView): void {
if (this._views.find(search => search.id === view.id)) {
- throw new Error(`View id ${view.id} is already registered`)
+ throw new Error(`IView id ${view.id} is already registered`)
}
+ validateView(view)
+
this._views.push(view)
this.dispatchTypedEvent('update', new CustomEvent('update') as UpdateViewsEvent)
}
@@ -78,23 +84,23 @@ export class Navigation extends TypedEventTarget<{ updateActive: UpdateActiveVie
* @fires UpdateActiveViewEvent
* @param view New active view
*/
- setActive(view: View | null): void {
+ setActive(view: IView | null): void {
this._currentView = view
- const event = new CustomEvent('updateActive', { detail: view })
+ const event = new CustomEvent('updateActive', { detail: view })
this.dispatchTypedEvent('updateActive', event as UpdateActiveViewEvent)
}
/**
* The currently active files view
*/
- get active(): View | null {
+ get active(): IView | null {
return this._currentView
}
/**
* All registered views
*/
- get views(): View[] {
+ get views(): IView[] {
return this._views
}
diff --git a/lib/navigation/view.ts b/lib/navigation/view.ts
index 8d7d85e09..c924750bc 100644
--- a/lib/navigation/view.ts
+++ b/lib/navigation/view.ts
@@ -14,7 +14,7 @@ export type ContentsWithRoot = {
contents: Node[]
}
-interface ViewData {
+export interface IView {
/** Unique view ID */
id: string
/** Translated view name */
@@ -97,12 +97,12 @@ interface ViewData {
loadChildViews?: (view: View) => Promise
}
-export class View implements ViewData {
+export class View implements IView {
- private _view: ViewData
+ private _view: IView
- constructor(view: ViewData) {
- isValidView(view)
+ constructor(view: IView) {
+ validateView(view)
this._view = view
}
@@ -193,14 +193,12 @@ export class View implements ViewData {
}
/**
- * Typescript cannot validate an interface.
- * Please keep in sync with the View interface requirements.
+ * Validate a view interface to check all required properties are satisfied.
*
- * @param {ViewData} view the view to check
- * @return {boolean} true if the column is valid
+ * @param view the view to check
* @throws {Error} if the view is not valid
*/
-const isValidView = function(view: ViewData): boolean {
+export function validateView(view: IView) {
if (!view.id || typeof view.id !== 'string') {
throw new Error('View id is required and must be a string')
}
@@ -261,6 +259,4 @@ const isValidView = function(view: ViewData): boolean {
if (view.loadChildViews && typeof view.loadChildViews !== 'function') {
throw new Error('View loadChildViews must be a function')
}
-
- return true
}
diff --git a/lib/types.ts b/lib/types.ts
index 6f75aff34..861c9876e 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -3,25 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
-import { Folder, Node } from './node/index.ts'
-import { View } from './navigation/index.ts'
+import type { IFolder, INode } from './node/index.ts'
+import type { IView } from './navigation/index.ts'
export type ActionContextSingle = {
- nodes: [Node],
- view: View,
- folder: Folder,
- contents: Node[],
+ nodes: [INode],
+ view: IView,
+ folder: IFolder,
+ contents: INode[],
}
export type ActionContext = {
- nodes: Node[],
- view: View,
- folder: Folder,
- contents: Node[],
+ nodes: INode[],
+ view: IView,
+ folder: IFolder,
+ contents: INode[],
}
export type ViewActionContext = {
- view: View,
- folder: Folder,
- contents: Node[],
+ view: IView,
+ folder: IFolder,
+ contents: INode[],
}