-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(FileAction): add file action support
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
- Loading branch information
Showing
3 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 || [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters