Merge pull request #38806 from nextcloud/feat/f2v/actions-1

pull/38924/head
John Molakvoæ 12 months ago committed by GitHub
commit 32bbe3db81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,15 +0,0 @@
codecov:
branch: master
ci:
- drone.nextcloud.com
- !scrutinizer-ci.com
coverage:
precision: 2
round: down
range: "70...100"
status:
project: off
patch: off
comment: off

@ -25,8 +25,8 @@ jobs:
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1
id: versions
with:
fallbackNode: '^12'
fallbackNpm: '^6'
fallbackNode: '^16'
fallbackNpm: '^7'
test:
runs-on: ubuntu-latest
@ -47,8 +47,13 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Test
run: npm run test
- name: Test and process coverage
run: npm run test:coverage
- name: Collect coverage
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
with:
files: ./coverage/lcov.info
jsunit:
runs-on: ubuntu-latest

1
.gitignore vendored

@ -154,6 +154,7 @@ Vagrantfile
/config/config-autotest-backup.php
/config/autoconfig.php
clover.xml
/coverage
# Tests - dependencies
tests/acceptance/vendor/

@ -0,0 +1,34 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
export const getCurrentUser = function() {
return {
uid: 'test',
displayName: 'Test',
isAdmin: false,
}
}
export const getRequestToken = function() {
return 'test-token-1234'
}
export const onRequestTokenUpdate = function() {}

@ -0,0 +1,24 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
export default {
delete: async () => ({ status: 200, data: {} }),
}

@ -0,0 +1,22 @@
/**
* @copyright Copyright (c) 2023 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/>.
*
*/
export default 'SvgMock'

@ -0,0 +1,184 @@
/**
* @copyright Copyright (c) 2023 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 { action } from './deleteAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../services/Navigation'
import logger from '../logger'
const view = {
id: 'files',
name: 'Files',
} as Navigation
const trashbinView = {
id: 'trashbin',
name: 'Trashbin',
} as Navigation
describe('Delete action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('delete')
expect(action.displayName([], view)).toBe('Delete')
expect(action.iconSvgInline([], view)).toBe('SvgMock')
expect(action.order).toBe(100)
})
test('Default trashbin view values', () => {
expect(action.displayName([], trashbinView)).toBe('Delete permanently')
})
})
describe('Delete action enabled tests', () => {
test('Enabled with DELETE permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true)
})
test('Disabled without DELETE permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled without nodes', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
test('Disabled if not all nodes can be deleted', () => {
const folder1 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
permissions: Permission.DELETE,
})
const folder2 = new Folder({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
owner: 'admin',
permissions: Permission.READ,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder1], view)).toBe(true)
expect(action.enabled!([folder2], view)).toBe(false)
expect(action.enabled!([folder1, folder2], view)).toBe(false)
})
})
describe('Delete action execute tests', () => {
test('Delete action', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.exec(file, view, '/')
expect(exec).toBe(true)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(eventBus.emit).toBeCalledTimes(1)
expect(eventBus.emit).toBeCalledWith('files:node:deleted', file)
})
test('Delete action batch', async () => {
jest.spyOn(axios, 'delete')
jest.spyOn(eventBus, 'emit')
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const file2 = new File({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.execBatch!([file1, file2], view, '/')
expect(exec).toStrictEqual([true, true])
expect(axios.delete).toBeCalledTimes(2)
expect(axios.delete).toHaveBeenNthCalledWith(1, 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt')
expect(axios.delete).toHaveBeenNthCalledWith(2, 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt')
expect(eventBus.emit).toBeCalledTimes(2)
expect(eventBus.emit).toHaveBeenNthCalledWith(1, 'files:node:deleted', file1)
expect(eventBus.emit).toHaveBeenNthCalledWith(2, 'files:node:deleted', file2)
})
test('Delete fails', async () => {
jest.spyOn(axios, 'delete').mockImplementation(() => { throw new Error('Mock error') })
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ | Permission.UPDATE | Permission.DELETE,
})
const exec = await action.exec(file, view, '/')
expect(exec).toBe(false)
expect(axios.delete).toBeCalledTimes(1)
expect(axios.delete).toBeCalledWith('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(eventBus.emit).toBeCalledTimes(0)
expect(logger.error).toBeCalledTimes(1)
})
})

@ -25,11 +25,11 @@ import { translate as t } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
import TrashCan from '@mdi/svg/svg/trash-can.svg?raw'
import { registerFileAction, FileAction } from '../services/FileAction.ts'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
import type { Navigation } from '../services/Navigation.ts'
import type { Navigation } from '../services/Navigation'
registerFileAction(new FileAction({
export const action = new FileAction({
id: 'delete',
displayName(nodes: Node[], view: Navigation) {
return view.id === 'trashbin'
@ -63,4 +63,6 @@ registerFileAction(new FileAction({
},
order: 100,
}))
})
registerFileAction(action)

@ -0,0 +1,185 @@
/**
* @copyright Copyright (c) 2023 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 { action } from './downloadAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import * as eventBus from '@nextcloud/event-bus'
import axios from '@nextcloud/axios'
import type { Navigation } from '../services/Navigation'
import logger from '../logger'
const view = {
id: 'files',
name: 'Files',
} as Navigation
describe('Download action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('download')
expect(action.displayName([], view)).toBe('Download')
expect(action.iconSvgInline([], view)).toBe('SvgMock')
expect(action.order).toBe(30)
})
})
describe('Download action enabled tests', () => {
test('Enabled with READ permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.ALL,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true)
})
test('Disabled without READ permissions', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled if not all nodes have READ permissions', () => {
const folder1 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
permissions: Permission.READ,
})
const folder2 = new Folder({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
owner: 'admin',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder1], view)).toBe(true)
expect(action.enabled!([folder2], view)).toBe(false)
expect(action.enabled!([folder1, folder2], view)).toBe(false)
})
test('Disabled without nodes', () => {
expect(action.enabled).toBeDefined()
expect(action.enabled!([], view)).toBe(false)
})
})
describe('Download action execute tests', () => {
const link = {
click: jest.fn(),
} as unknown as HTMLAnchorElement
beforeEach(() => {
jest.spyOn(document, 'createElement').mockImplementation(() => link)
})
test('Download single file', async () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const exec = await action.exec(file, view, '/')
// Silent action
expect(exec).toBe(null)
expect(link.download).toEqual('')
expect(link.href).toEqual('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(link.click).toHaveBeenCalledTimes(1)
})
test('Download single file with batch', async () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const exec = await action.execBatch!([file], view, '/')
// Silent action
expect(exec).toStrictEqual([null])
expect(link.download).toEqual('')
expect(link.href).toEqual('https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt')
expect(link.click).toHaveBeenCalledTimes(1)
})
test('Download single folder', async () => {
const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
owner: 'admin',
permissions: Permission.READ,
})
const exec = await action.exec(folder, view, '/')
// Silent action
expect(exec).toBe(null)
expect(link.download).toEqual('')
expect(link.href.startsWith('/index.php/apps/files/ajax/download.php?dir=%2F&files=%5B%22FooBar%22%5D&downloadStartSecret=')).toBe(true)
expect(link.click).toHaveBeenCalledTimes(1)
})
test('Download multiple nodes', async () => {
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Dir/foo.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const file2 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Dir/bar.txt',
owner: 'admin',
mime: 'text/plain',
permissions: Permission.READ,
})
const exec = await action.execBatch!([file1, file2], view, '/Dir')
// Silent action
expect(exec).toStrictEqual([null, null])
expect(link.download).toEqual('')
expect(link.href.startsWith('/index.php/apps/files/ajax/download.php?dir=%2FDir&files=%5B%22foo.txt%22%2C%22bar.txt%22%5D&downloadStartSecret=')).toBe(true)
expect(link.click).toHaveBeenCalledTimes(1)
})
})

@ -0,0 +1,82 @@
/**
* @copyright Copyright (c) 2023 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 { emit } from '@nextcloud/event-bus'
import { Permission, Node, FileType } from '@nextcloud/files'
import { translate as t } from '@nextcloud/l10n'
import ArrowDown from '@mdi/svg/svg/arrow-down.svg?raw'
import { registerFileAction, FileAction } from '../services/FileAction'
import { generateUrl } from '@nextcloud/router'
import type { Navigation } from '../services/Navigation'
const triggerDownload = function(url: string) {
const hiddenElement = document.createElement('a')
hiddenElement.download = ''
hiddenElement.href = url
hiddenElement.click()
}
const downloadNodes = function(dir: string, nodes: Node[]) {
const secret = Math.random().toString(36).substring(2)
const url = generateUrl('/apps/files/ajax/download.php?dir={dir}&files={files}&downloadStartSecret={secret}', {
dir,
secret,
files: JSON.stringify(nodes.map(node => node.basename)),
})
triggerDownload(url)
}
export const action = new FileAction({
id: 'download',
displayName: () => t('files', 'Download'),
iconSvgInline: () => ArrowDown,
enabled(nodes: Node[]) {
return nodes.length > 0 && nodes
.map(node => node.permissions)
.every(permission => (permission & Permission.READ) !== 0)
},
async exec(node: Node, view: Navigation, dir: string) {
if (node.type === FileType.Folder) {
downloadNodes(dir, [node])
return null
}
triggerDownload(node.source)
return null
},
async execBatch(nodes: Node[], view: Navigation, dir: string) {
if (nodes.length === 1) {
this.exec(nodes[0], view, dir)
return [null]
}
downloadNodes(dir, nodes)
return new Array(nodes.length).fill(null)
},
order: 30,
})
registerFileAction(action)

@ -0,0 +1,163 @@
/**
* @copyright Copyright (c) 2023 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 { action } from './openFolderAction'
import { expect } from '@jest/globals'
import { File, Folder, Permission } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import type { Navigation } from '../services/Navigation'
const view = {
id: 'files',
name: 'Files',
} as Navigation
describe('Open folder action conditions tests', () => {
test('Default values', () => {
const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
owner: 'admin',
permissions: Permission.READ,
})
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('open-folder')
expect(action.displayName([folder], view)).toBe('Open folder FooBar')
expect(action.iconSvgInline([], view)).toBe('SvgMock')
expect(action.default).toBe(true)
expect(action.order).toBe(-100)
})
})
describe('Open folder action enabled tests', () => {
test('Enabled for folders', () => {
const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
owner: 'admin',
permissions: Permission.READ,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder], view)).toBe(true)
})
test('Disabled for non-dav ressources', () => {
const folder = new Folder({
id: 1,
source: 'https://domain.com/data/FooBar/',
owner: 'admin',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder], view)).toBe(false)
})
test('Disabled if more than one node', () => {
const folder1 = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
permissions: Permission.READ,
})
const folder2 = new Folder({
id: 2,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Bar/',
owner: 'admin',
permissions: Permission.READ,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder1, folder2], view)).toBe(false)
})
test('Disabled for files', () => {
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
mime: 'text/plain',
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled without READ permissions', () => {
const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
permissions: Permission.NONE,
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([folder], view)).toBe(false)
})
})
describe('Open folder action execute tests', () => {
test('Open folder', async () => {
const goToRouteMock = jest.fn()
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
const folder = new Folder({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/FooBar/',
owner: 'admin',
permissions: Permission.READ,
})
const exec = await action.exec(folder, view, '/')
// Silent action
expect(exec).toBe(null)
expect(goToRouteMock).toBeCalledTimes(1)
expect(goToRouteMock).toBeCalledWith(null, null, { dir: '/FooBar' })
})
test('Open folder fails without node', async () => {
const goToRouteMock = jest.fn()
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
// @ts-ignore null as Node
const exec = await action.exec(null, view, '/')
expect(exec).toBe(false)
expect(goToRouteMock).toBeCalledTimes(0)
})
test('Open folder fails without Folder', async () => {
const goToRouteMock = jest.fn()
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
owner: 'admin',
mime: 'text/plain',
})
const exec = await action.exec(file, view, '/')
expect(exec).toBe(false)
expect(goToRouteMock).toBeCalledTimes(0)
})
})

@ -27,7 +27,7 @@ import type { Navigation } from '../services/Navigation'
import { join } from 'path'
import { registerFileAction, FileAction } from '../services/FileAction'
registerFileAction(new FileAction({
export const action = new FileAction({
id: 'open-folder',
displayName(files: Node[]) {
// Only works on single node
@ -43,6 +43,11 @@ registerFileAction(new FileAction({
}
const node = nodes[0]
if (!node.isDavRessource) {
return false
}
return node.type === FileType.Folder
&& (node.permissions & Permission.READ) !== 0
},
@ -59,11 +64,10 @@ registerFileAction(new FileAction({
)
return null
},
async execBatch(nodes: Node[], view: Navigation, dir: string) {
return Promise.all(nodes.map(node => this.exec(node, view, dir)))
},
// Main action if enabled, meaning folders only
order: -100,
default: true,
}))
order: -100,
})
registerFileAction(action)

@ -0,0 +1,144 @@
/**
* @copyright Copyright (c) 2023 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 { action } from './sidebarAction'
import { expect } from '@jest/globals'
import { File } from '@nextcloud/files'
import { FileAction } from '../services/FileAction'
import type { Navigation } from '../services/Navigation'
import logger from '../logger'
const view = {
id: 'files',
name: 'Files',
} as Navigation
describe('Open sidebar action conditions tests', () => {
test('Default values', () => {
expect(action).toBeInstanceOf(FileAction)
expect(action.id).toBe('details')
expect(action.displayName([], view)).toBe('Details')
expect(action.iconSvgInline([], view)).toBe('SvgMock')
expect(action.default).toBe(true)
expect(action.order).toBe(-50)
})
})
describe('Open folder action enabled tests', () => {
test('Enabled for ressources within user root folder', () => {
window.OCA = { Files: { Sidebar: {} } }
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(true)
})
test('Disabled if more than one node', () => {
window.OCA = { Files: { Sidebar: {} } }
const file1 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foo.txt',
owner: 'admin',
mime: 'text/plain',
})
const file2 = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/bar.txt',
owner: 'admin',
mime: 'text/plain',
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file1, file2], view)).toBe(false)
})
test('Disabled if no Sidebar', () => {
window.OCA = {}
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
test('Disabled for non-dav ressources', () => {
window.OCA = { Files: { Sidebar: {} } }
const file = new File({
id: 1,
source: 'https://domain.com/documents/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
})
expect(action.enabled).toBeDefined()
expect(action.enabled!([file], view)).toBe(false)
})
})
describe('Open sidebar action exec tests', () => {
test('Open sidebar', async () => {
const openMock = jest.fn()
window.OCA = { Files: { Sidebar: { open: openMock } } }
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
})
const exec = await action.exec(file, view, '/')
// Silent action
expect(exec).toBe(null)
expect(openMock).toBeCalledWith('/foobar.txt')
})
test('Open sidebar fails', async () => {
const openMock = jest.fn(() => { throw new Error('Mock error') })
window.OCA = { Files: { Sidebar: { open: openMock } } }
jest.spyOn(logger, 'error').mockImplementation(() => jest.fn())
const file = new File({
id: 1,
source: 'https://cloud.domain.com/remote.php/dav/files/admin/foobar.txt',
owner: 'admin',
mime: 'text/plain',
})
const exec = await action.exec(file, view, '/')
expect(exec).toBe(false)
expect(openMock).toBeCalledTimes(1)
expect(logger.error).toBeCalledTimes(1)
})
})

@ -23,19 +23,30 @@ import { translate as t } from '@nextcloud/l10n'
import InformationSvg from '@mdi/svg/svg/information-variant.svg?raw'
import type { Node } from '@nextcloud/files'
import { registerFileAction, FileAction } from '../services/FileAction.ts'
import { registerFileAction, FileAction } from '../services/FileAction'
import logger from '../logger.js'
export const ACTION_DETAILS = 'details'
registerFileAction(new FileAction({
export const action = new FileAction({
id: ACTION_DETAILS,
displayName: () => t('files', 'Details'),
iconSvgInline: () => InformationSvg,
// Sidebar currently supports user folder only, /files/USER
enabled: (files: Node[]) => !!window?.OCA?.Files?.Sidebar
&& files.some(node => node.root?.startsWith('/files/')),
enabled: (nodes: Node[]) => {
// Only works on single node
if (nodes.length !== 1) {
return false
}
// Only work if the sidebar is available
if (!window?.OCA?.Files?.Sidebar) {
return false
}
return nodes[0].root?.startsWith('/files/') ?? false
},
async exec(node: Node) {
try {
@ -51,4 +62,7 @@ registerFileAction(new FileAction({
default: true,
order: -50,
}))
})
registerFileAction(action)

@ -39,11 +39,11 @@ interface FileActionData {
/** Unique ID */
id: string
/** Translatable string displayed in the menu */
displayName: (files: Node[], view) => string
displayName: (files: Node[], view: Navigation) => string
/** Svg as inline string. <svg><path fill="..." /></svg> */
iconSvgInline: (files: Node[], view) => string
iconSvgInline: (files: Node[], view: Navigation) => string
/** Condition wether this action is shown or not */
enabled?: (files: Node[], view) => boolean
enabled?: (files: Node[], view: Navigation) => boolean
/**
* Function executed on single file action
* @returns true if the action was executed, false otherwise
@ -64,12 +64,12 @@ interface FileActionData {
/**
* If true, the renderInline function will be called
*/
inline?: (file: Node, view) => boolean,
inline?: (file: Node, view: Navigation) => boolean,
/**
* If defined, the returned html element will be
* appended before the actions menu.
*/
renderInline?: (file: Node, view) => HTMLElement,
renderInline?: (file: Node, view: Navigation) => HTMLElement,
}
export class FileAction {

@ -22,7 +22,6 @@
$expectedFiles = [
'.',
'..',
'.codecov.yml',
'.devcontainer',
'.drone.yml',
'.editorconfig',
@ -44,6 +43,8 @@ $expectedFiles = [
'.tag',
'.tx',
'.user.ini',
'__mocks__',
'__tests__',
'3rdparty',
'AUTHORS',
'CHANGELOG.md',
@ -60,6 +61,7 @@ $expectedFiles = [
'autotest.sh',
'babel.config.js',
'build',
'codecov.yml',
'composer.json',
'composer.lock',
'config',
@ -74,7 +76,7 @@ $expectedFiles = [
'dist',
'index.html',
'index.php',
'jest.config.js',
'jest.config.ts',
'lib',
'occ',
'ocm-provider',

@ -0,0 +1,7 @@
codecov:
branch: master
ci:
- drone.nextcloud.com
- '!scrutinizer-ci.com'
comment: false

5
custom.d.ts vendored

@ -24,6 +24,11 @@ declare module '*.svg?raw' {
export default content
}
declare module '*.svg' {
const content: any
export default content
}
declare module '*.vue' {
import Vue from 'vue'
export default Vue

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,4 +1,4 @@
/*
/**
* @copyright Copyright (c) 2020 Marco Ambrosini <marcoambrosini@pm.me>
*
* @author Marco Ambrosini <marcoambrosini@pm.me>
@ -19,42 +19,55 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import type { Config } from 'jest'
// TODO: find a way to consolidate this in one place, with webpack.common.js
const ignorePatterns = [
'vue-material-design-icons',
'@juliushaertl',
'tributejs',
'@mdi/svg',
'@nextcloud/vue',
'ansi-regex',
'char-regex',
'splitpanes',
'string-length',
'strip-ansi',
'ansi-regex',
'char-regex',
'tributejs',
'vue-material-design-icons',
]
module.exports = {
testMatch: ['<rootDir>/apps/*/src/**/*.(spec|test).(ts|js)'],
modulePathIgnorePatterns: ["<rootDir>/apps-extra/"],
transformIgnorePatterns: [
'node_modules/(?!(' + ignorePatterns.join('|') + ')/)',
],
setupFilesAfterEnv: ['<rootDir>/tests/jestSetup.js'],
resetMocks: false,
collectCoverageFrom: [
'<rootDir>/apps/*/src/**/*.{js,vue}',
],
const config: Config = {
testMatch: ['<rootDir>/**/*.(spec|test).(ts|js)'],
clearMocks: true,
setupFilesAfterEnv: ['<rootDir>/__tests__/jest-setup.ts'],
testEnvironment: 'jest-environment-jsdom',
preset: 'ts-jest/presets/js-with-ts',
moduleFileExtensions: [
'js',
'vue',
roots: [
'<rootDir>/__mocks__',
'<rootDir>/__tests__',
'<rootDir>/apps',
'<rootDir>/core',
],
transform: {
// process `*.js` files with `babel-jest`
'.*\\.(js)$': 'babel-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.vue$': '@vue/vue2-jest',
},
transformIgnorePatterns: [
'node_modules/(?!(' + ignorePatterns.join('|') + ')/)',
],
// Allow mocking svg files
moduleNameMapper: {
'^.+\\.svg(\\?raw)?$': '<rootDir>/__mocks__/svg.js',
},
modulePathIgnorePatterns: [
'<rootDir>/apps2/',
'<rootDir>/apps-extra/',
],
}
export default config

399
package-lock.json generated

@ -105,6 +105,7 @@
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",
"@types/dockerode": "^3.3.17",
"@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.5",
"@vue/test-utils": "^1.3.5",
@ -128,7 +129,6 @@
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.5.0",
"jsdoc": "^4.0.2",
"jsdom": "^22.0.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.1.1",
"karma-coverage": "2.2.0",
@ -144,6 +144,7 @@
"sinon": "<= 5.0.7",
"style-loader": "^3.3.1",
"tar": "^6.1.15",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.3",
@ -4638,9 +4639,10 @@
}
},
"node_modules/@types/jest": {
"version": "29.5.1",
"version": "29.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
"integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
@ -7039,6 +7041,18 @@
"version": "1.0.0",
"license": "ISC"
},
"node_modules/bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"dependencies": {
"fast-json-stable-stringify": "2.x"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bser": {
"version": "2.1.1",
"dev": true,
@ -8201,17 +8215,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/cssstyle": {
"version": "3.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"rrweb-cssom": "^0.6.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/csstype": {
"version": "3.1.2",
"license": "MIT"
@ -8452,19 +8455,6 @@
"node": ">= 12"
}
},
"node_modules/data-urls": {
"version": "4.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/date-format": {
"version": "4.0.14",
"dev": true,
@ -15301,77 +15291,6 @@
"node": ">= 12"
}
},
"node_modules/jsdom": {
"version": "22.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"abab": "^2.0.6",
"cssstyle": "^3.0.0",
"data-urls": "^4.0.0",
"decimal.js": "^10.4.3",
"domexception": "^4.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.4",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.1",
"ws": "^8.13.0",
"xml-name-validator": "^4.0.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"canvas": "^2.5.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/punycode": {
"version": "2.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/jsdom/node_modules/tough-cookie": {
"version": "4.1.2",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/jsdom/node_modules/universalify": {
"version": "0.2.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/jsesc": {
"version": "2.5.2",
"dev": true,
@ -19863,11 +19782,6 @@
"node": ">=8"
}
},
"node_modules/rrweb-cssom": {
"version": "0.6.0",
"dev": true,
"license": "MIT"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"dev": true,
@ -21993,25 +21907,6 @@
"node": ">=6"
}
},
"node_modules/tr46": {
"version": "4.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"punycode": "^2.3.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/tr46/node_modules/punycode": {
"version": "2.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/tributejs": {
"version": "5.1.3",
"license": "MIT"
@ -22041,6 +21936,97 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/ts-jest": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz",
"integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==",
"dev": true,
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^29.0.0",
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "^21.0.1"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@jest/types": "^29.0.0",
"babel-jest": "^29.0.0",
"jest": "^29.0.0",
"typescript": ">=4.3 <6"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@jest/types": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"node_modules/ts-jest/node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"node_modules/ts-jest/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-jest/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/ts-jest/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/ts-loader": {
"version": "9.4.2",
"dev": true,
@ -23914,18 +23900,6 @@
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "12.0.1",
"dev": true,
"license": "MIT",
"dependencies": {
"tr46": "^4.1.1",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/which": {
"version": "2.0.2",
"dev": true,
@ -27456,7 +27430,9 @@
}
},
"@types/jest": {
"version": "29.5.1",
"version": "29.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
"integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
"dev": true,
"requires": {
"expect": "^29.0.0",
@ -29016,6 +28992,15 @@
}
}
},
"bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"requires": {
"fast-json-stable-stringify": "2.x"
}
},
"bser": {
"version": "2.1.1",
"dev": true,
@ -29766,13 +29751,6 @@
"version": "0.5.0",
"dev": true
},
"cssstyle": {
"version": "3.0.0",
"dev": true,
"requires": {
"rrweb-cssom": "^0.6.0"
}
},
"csstype": {
"version": "3.1.2"
},
@ -29933,15 +29911,6 @@
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"optional": true
},
"data-urls": {
"version": "4.0.0",
"dev": true,
"requires": {
"abab": "^2.0.6",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.0"
}
},
"date-format": {
"version": "4.0.14",
"dev": true
@ -34346,55 +34315,6 @@
"dev": true,
"peer": true
},
"jsdom": {
"version": "22.0.0",
"dev": true,
"requires": {
"abab": "^2.0.6",
"cssstyle": "^3.0.0",
"data-urls": "^4.0.0",
"decimal.js": "^10.4.3",
"domexception": "^4.0.0",
"form-data": "^4.0.0",
"html-encoding-sniffer": "^3.0.0",
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.1",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.4",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.6.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^4.1.2",
"w3c-xmlserializer": "^4.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^2.0.0",
"whatwg-mimetype": "^3.0.0",
"whatwg-url": "^12.0.1",
"ws": "^8.13.0",
"xml-name-validator": "^4.0.0"
},
"dependencies": {
"punycode": {
"version": "2.3.0",
"dev": true
},
"tough-cookie": {
"version": "4.1.2",
"dev": true,
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.2.0",
"url-parse": "^1.5.3"
}
},
"universalify": {
"version": "0.2.0",
"dev": true
}
}
},
"jsesc": {
"version": "2.5.2",
"dev": true
@ -37253,10 +37173,6 @@
}
}
},
"rrweb-cssom": {
"version": "0.6.0",
"dev": true
},
"run-parallel": {
"version": "1.2.0",
"dev": true,
@ -38647,19 +38563,6 @@
}
}
},
"tr46": {
"version": "4.1.1",
"dev": true,
"requires": {
"punycode": "^2.3.0"
},
"dependencies": {
"punycode": {
"version": "2.3.0",
"dev": true
}
}
},
"tributejs": {
"version": "5.1.3"
},
@ -38674,6 +38577,60 @@
"trough": {
"version": "2.1.0"
},
"ts-jest": {
"version": "29.1.0",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz",
"integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==",
"dev": true,
"requires": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^29.0.0",
"json5": "^2.2.3",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "^21.0.1"
},
"dependencies": {
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
"integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true
}
}
},
"ts-loader": {
"version": "9.4.2",
"dev": true,
@ -39873,14 +39830,6 @@
"version": "3.0.0",
"dev": true
},
"whatwg-url": {
"version": "12.0.1",
"dev": true,
"requires": {
"tr46": "^4.1.1",
"webidl-conversions": "^7.0.0"
}
},
"which": {
"version": "2.0.2",
"dev": true,

@ -16,6 +16,7 @@
"lint:fix": "eslint 'apps/*/src/**/*.{vue,js}' 'core/src/**/*.{vue,js}' --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:jsunit": "karma start tests/karma.config.js --single-run",
"sass": "sass --style compressed --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then echo -n \"$cssdir \"; fi; done)",
"sass:watch": "sass --watch --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then echo -n \"$cssdir \"; fi; done)",
@ -130,6 +131,7 @@
"@testing-library/user-event": "^14.4.3",
"@testing-library/vue": "^5.8.3",
"@types/dockerode": "^3.3.17",
"@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.5",
"@vue/test-utils": "^1.3.5",
@ -153,7 +155,6 @@
"jest": "^29.0.3",
"jest-environment-jsdom": "^29.5.0",
"jsdoc": "^4.0.2",
"jsdom": "^22.0.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.1.1",
"karma-coverage": "2.2.0",
@ -169,6 +170,7 @@
"sinon": "<= 5.0.7",
"style-loader": "^3.3.1",
"tar": "^6.1.15",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tslib": "^2.5.3",

@ -2,7 +2,7 @@
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./apps/**/*.ts", "./core/**/*.ts", "./*.d.ts"],
"compilerOptions": {
"types": ["cypress", "node", "vue"],
"types": ["cypress", "jest", "node", "vue"],
"outDir": "./dist/",
"target": "ESNext",
"module": "esnext",

Loading…
Cancel
Save