Skip to content

Commit

Permalink
feat(FileAction): add file action support
Browse files Browse the repository at this point in the history
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Mar 18, 2023
1 parent 7d30829 commit 5a79f3c
Show file tree
Hide file tree
Showing 3 changed files with 324 additions and 0 deletions.
193 changes: 193 additions & 0 deletions __tests__/fileAction.spec.ts
@@ -0,0 +1,193 @@
import { getFileActions, registerFileAction, FileAction } from '../lib/fileAction'
import logger from '../lib/utils/logger';

declare global {
interface Window {
OC: any;
_nc_fileactions: FileAction[];
}
}

describe('FileActions init', () => {
test('Getting empty uninitialized FileActions', () => {
logger.debug = jest.fn()
const fileActions = getFileActions()
expect(window._nc_fileactions).toBeUndefined()
expect(fileActions).toHaveLength(0)
expect(logger.debug).toHaveBeenCalledTimes(0)
})

test('Initializing FileActions', () => {
logger.debug = jest.fn()
const action = new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
})

expect(action.id).toBe('test')
expect(action.displayName([])).toBe('Test')
expect(action.iconSvgInline([])).toBe('<svg></svg>')

registerFileAction(action)

expect(window._nc_fileactions).toHaveLength(1)
expect(getFileActions()).toHaveLength(1)
expect(getFileActions()[0]).toStrictEqual(action)
expect(logger.debug).toHaveBeenCalled()
})

test('Duplicate FileAction gets rejected', () => {
logger.error = jest.fn()
const action = new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
})

registerFileAction(action)
expect(getFileActions()).toHaveLength(1)
expect(getFileActions()[0]).toStrictEqual(action)

const action2 = new FileAction({
id: 'test',
displayName: () => 'Test 2',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
})

registerFileAction(action2)
expect(getFileActions()).toHaveLength(1)
expect(getFileActions()[0]).toStrictEqual(action)
expect(logger.error).toHaveBeenCalledWith('FileAction test already registered', { action: action2 })
})
})

describe('Invalid FileAction creation', () => {
test('Invalid id', () => {
expect(() => {
new FileAction({
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
} as any as FileAction)
}).toThrowError('Invalid id')
})
test('Invalid displayName', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
} as any as FileAction)
}).toThrowError('Invalid displayName function')
})
test('Invalid iconSvgInline', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: '<svg></svg>',
exec: () => true,
} as any as FileAction)
}).toThrowError('Invalid iconSvgInline function')
})
test('Invalid exec', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: false,
} as any as FileAction)
}).toThrowError('Invalid exec function')
})
test('Invalid enabled', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
enabled: false,
} as any as FileAction)
}).toThrowError('Invalid enabled function')
})
test('Invalid execBatch', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
execBatch: false,
} as any as FileAction)
}).toThrowError('Invalid execBatch function')
})
test('Invalid order', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
order: 'invalid',
} as any as FileAction)
}).toThrowError('Invalid order')
})
test('Invalid default', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
default: 'invalid',
} as any as FileAction)
}).toThrowError('Invalid default')
})
test('Invalid inline', () => {
expect(() => {
new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
inline: true,
} as any as FileAction)
}).toThrowError('Invalid inline function')
})
})

describe('FileActions creation', () => {
test('create valid FileAction', () => {
logger.debug = jest.fn()
const action = new FileAction({
id: 'test',
displayName: () => 'Test',
iconSvgInline: () => '<svg></svg>',
exec: () => true,
execBatch: () => true,
enabled: () => true,
order: 100,
default: true,
inline: (mount) => mount.append('test'),
})

const mount = document.createElement('div')

expect(action.id).toBe('test')
expect(action.displayName([])).toBe('Test')
expect(action.iconSvgInline([])).toBe('<svg></svg>')
expect(action.exec({} as any)).toBe(true)
expect(action.execBatch?.([])).toBe(true)
expect(action.enabled?.({} as any, {})).toBe(true)
expect(action.order).toBe(100)
expect(action.default).toBe(true)
action.inline?.(mount)
expect(mount.innerHTML).toBe('test')
})
})
129 changes: 129 additions & 0 deletions lib/fileAction.ts
@@ -0,0 +1,129 @@
/**
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import { Node } from "./files/node"
import logger from "./utils/logger"

interface FileActionData {
/** Unique ID */
id: string
/** Translatable string displayed in the menu */
displayName: (files: Node[]) => string
/** Svg as inline string. <svg><path fill="..." /></svg> */
iconSvgInline: (files: Node[]) => string
// Condition wether this action is shown or not
enabled?: (files: Node[], view) => boolean
/** Function executed on single file action
* @returns true if the action was executed, false otherwise
* @throws Error if the action failed
*/
exec: (file: Node) => boolean,
/** Function executed on multiple files action
* @returns true if the action was executed, false otherwise
* @throws Error if the action failed
*/
execBatch?: (files: Node[]) => boolean
/** This action order in the list */
order?: number,
/** Make this action the default */
default?: boolean,
/** If defined, will provide a mount point
* to render the action inline.
*/
inline?: (mount: HTMLElement) => void,
}

// Allow class/interface merging and proxying
export interface FileAction extends FileActionData {}
export class FileAction {
private _action: FileActionData

constructor(action: FileActionData) {
this.validateAction(action)
this._action = action

// Forward any getter to action data
return new Proxy(this, {
get(target, property: string) {
return Reflect.get(target._action, property)
}
})
}

private validateAction(action: FileActionData) {
if (!action.id || typeof action.id !== 'string') {
throw new Error('Invalid id')
}

if (!action.displayName || typeof action.displayName !== 'function') {
throw new Error('Invalid displayName function')
}

if (!action.iconSvgInline || typeof action.iconSvgInline !== 'function') {
throw new Error('Invalid iconSvgInline function')
}

if (!action.exec || typeof action.exec !== 'function') {
throw new Error('Invalid exec function')
}

// Optional properties --------------------------------------------
if ('enabled' in action && typeof action.execBatch !== 'function') {
throw new Error('Invalid enabled function')
}

if ('execBatch' in action && typeof action.execBatch !== 'function') {
throw new Error('Invalid execBatch function')
}

if ('order' in action && typeof action.order !== 'number') {
throw new Error('Invalid order')
}

if ('default' in action && typeof action.default !== 'boolean') {
throw new Error('Invalid default')
}

if ('inline' in action && typeof action.inline !== 'function') {
throw new Error('Invalid inline function')
}
}
}

export const registerFileAction = function(action: FileAction): void {
if (typeof window._nc_fileactions === 'undefined') {
window._nc_fileactions = []
logger.debug('FileActions initialized')
}

// Check duplicates
if (window._nc_fileactions.find(search => search.id === action.id)) {
logger.error(`FileAction ${action.id} already registered`, { action })
return
}

window._nc_fileactions.push(action)
}

export const getFileActions = function(): FileAction[] {
return window._nc_fileactions || []
}
2 changes: 2 additions & 0 deletions lib/index.ts
Expand Up @@ -23,6 +23,7 @@

export { formatFileSize } from './humanfilesize'
export { type Entry } from './newFileMenu'
import { FileAction } from './fileAction'
import { type Entry, getNewFileMenu, NewFileMenu } from './newFileMenu'

export { FileType } from './files/fileType'
Expand All @@ -35,6 +36,7 @@ declare global {
interface Window {
OC: any;
_nc_newfilemenu: NewFileMenu;
_nc_fileactions: FileAction[];
}
}

Expand Down

0 comments on commit 5a79f3c

Please sign in to comment.