mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
feat(files): migrate recent view
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
parent
6ec35e3799
commit
87b1719c88
25 changed files with 495 additions and 248 deletions
|
|
@ -20,7 +20,6 @@
|
|||
"newfilemenu.js",
|
||||
"operationprogressbar.js",
|
||||
"recentfilelist.js",
|
||||
"recentplugin.js",
|
||||
"semaphore.js",
|
||||
"sidebarpreviewmanager.js",
|
||||
"sidebarpreviewtext.js",
|
||||
|
|
|
|||
|
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
(function (OCA) {
|
||||
/**
|
||||
* Registers the recent file list from the files app sidebar.
|
||||
*
|
||||
* @namespace OCA.Files.RecentPlugin
|
||||
*/
|
||||
OCA.Files.RecentPlugin = {
|
||||
name: 'Recent',
|
||||
|
||||
/**
|
||||
* @type OCA.Files.RecentFileList
|
||||
*/
|
||||
recentFileList: null,
|
||||
|
||||
attach: function () {
|
||||
var self = this;
|
||||
$('#app-content-recent').on('show.plugin-recent', function (e) {
|
||||
self.showFileList($(e.target));
|
||||
});
|
||||
$('#app-content-recent').on('hide.plugin-recent', function () {
|
||||
self.hideFileList();
|
||||
});
|
||||
},
|
||||
|
||||
detach: function () {
|
||||
if (this.recentFileList) {
|
||||
this.recentFileList.destroy();
|
||||
OCA.Files.fileActions.off('setDefault.plugin-recent', this._onActionsUpdated);
|
||||
OCA.Files.fileActions.off('registerAction.plugin-recent', this._onActionsUpdated);
|
||||
$('#app-content-recent').off('.plugin-recent');
|
||||
this.recentFileList = null;
|
||||
}
|
||||
},
|
||||
|
||||
showFileList: function ($el) {
|
||||
if (!this.recentFileList) {
|
||||
this.recentFileList = this._createRecentFileList($el);
|
||||
}
|
||||
return this.recentFileList;
|
||||
},
|
||||
|
||||
hideFileList: function () {
|
||||
if (this.recentFileList) {
|
||||
this.recentFileList.$fileList.empty();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the recent file list.
|
||||
*
|
||||
* @param $el container for the file list
|
||||
* @return {OCA.Files.RecentFileList} file list
|
||||
*/
|
||||
_createRecentFileList: function ($el) {
|
||||
var fileActions = this._createFileActions();
|
||||
// register recent list for sidebar section
|
||||
return new OCA.Files.RecentFileList(
|
||||
$el, {
|
||||
fileActions: fileActions,
|
||||
// The file list is created when a "show" event is handled,
|
||||
// so it should be marked as "shown" like it would have been
|
||||
// done if handling the event with the file list already
|
||||
// created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_createFileActions: function () {
|
||||
// inherit file actions from the files app
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
// note: not merging the legacy actions because legacy apps are not
|
||||
// compatible with the sharing overview and need to be adapted first
|
||||
fileActions.registerDefaultActions();
|
||||
fileActions.merge(OCA.Files.fileActions);
|
||||
|
||||
if (!this._globalActionsInitialized) {
|
||||
// in case actions are registered later
|
||||
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
|
||||
OCA.Files.fileActions.on('setDefault.plugin-recent', this._onActionsUpdated);
|
||||
OCA.Files.fileActions.on('registerAction.plugin-recent', this._onActionsUpdated);
|
||||
this._globalActionsInitialized = true;
|
||||
}
|
||||
|
||||
// when the user clicks on a folder, redirect to the corresponding
|
||||
// folder in the files app instead of opening it directly
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
|
||||
OCA.Files.App.setActiveView('files', {silent: true});
|
||||
var path = OC.joinPaths(context.$file.attr('data-path'), filename);
|
||||
OCA.Files.App.fileList.changeDirectory(path, true, true);
|
||||
});
|
||||
fileActions.setDefault('dir', 'Open');
|
||||
return fileActions;
|
||||
},
|
||||
|
||||
_onActionsUpdated: function (ev) {
|
||||
if (ev.action) {
|
||||
this.recentFileList.fileActions.registerAction(ev.action);
|
||||
} else if (ev.defaultAction) {
|
||||
this.recentFileList.fileActions.setDefault(
|
||||
ev.defaultAction.mime,
|
||||
ev.defaultAction.name
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(OCA);
|
||||
|
||||
OC.Plugins.register('OCA.Files.App', OCA.Files.RecentPlugin);
|
||||
|
||||
|
|
@ -163,15 +163,6 @@ class Application extends App implements IBootstrap {
|
|||
'name' => $l10n->t('All files')
|
||||
];
|
||||
});
|
||||
\OCA\Files\App::getNavigationManager()->add(function () use ($l10n) {
|
||||
return [
|
||||
'id' => 'recent',
|
||||
'appname' => 'files',
|
||||
'script' => 'recentlist.php',
|
||||
'order' => 2,
|
||||
'name' => $l10n->t('Recent')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
private function registerHooks(): void {
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
$config = \OC::$server->getConfig();
|
||||
$userSession = \OC::$server->getUserSession();
|
||||
|
||||
$showgridview = $config->getUserValue($userSession->getUser()->getUID(), 'files', 'show_grid', false);
|
||||
|
||||
$tmpl = new OCP\Template('files', 'recentlist', '');
|
||||
|
||||
// gridview not available for ie
|
||||
$tmpl->assign('showgridview', $showgridview);
|
||||
|
||||
$tmpl->printPage();
|
||||
103
apps/files/src/actions/openInFilesAction.spec.ts
Normal file
103
apps/files/src/actions/openInFilesAction.spec.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* @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 './openInFilesAction'
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Folder, Permission } from '@nextcloud/files'
|
||||
import { DefaultType, FileAction } from '../../../files/src/services/FileAction'
|
||||
import type { Navigation } from '../../../files/src/services/Navigation'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
} as Navigation
|
||||
|
||||
const recentView = {
|
||||
id: 'recent',
|
||||
name: 'Recent',
|
||||
} as Navigation
|
||||
|
||||
describe('Open in files action conditions tests', () => {
|
||||
test('Default values', () => {
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('open-in-files-recent')
|
||||
expect(action.displayName([], recentView)).toBe('Open in Files')
|
||||
expect(action.iconSvgInline([], recentView)).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action enabled tests', () => {
|
||||
test('Enabled with on valid view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], recentView)).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action execute tests', () => {
|
||||
test('Open in files', 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/foobar.txt',
|
||||
owner: 'admin',
|
||||
mime: 'text/plain',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo', openfile: true })
|
||||
})
|
||||
|
||||
test('Open in files with folder', async () => {
|
||||
const goToRouteMock = jest.fn()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const file = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
const exec = await action.exec(file, view, '/')
|
||||
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { fileid: 1, view: 'files' }, { fileid: 1, dir: '/Foo/Bar', openfile: true })
|
||||
})
|
||||
})
|
||||
57
apps/files/src/actions/openInFilesAction.ts
Normal file
57
apps/files/src/actions/openInFilesAction.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* @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 { translate as t } from '@nextcloud/l10n'
|
||||
import { FileType, type Node } from '@nextcloud/files'
|
||||
|
||||
import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction'
|
||||
|
||||
/**
|
||||
* TODO: Move away from a redirect and handle
|
||||
* navigation straight out of the recent view
|
||||
*/
|
||||
export const action = new FileAction({
|
||||
id: 'open-in-files-recent',
|
||||
displayName: () => t('files', 'Open in Files'),
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes, view) => view.id === 'recent',
|
||||
|
||||
async exec(node: Node) {
|
||||
let dir = node.dirname
|
||||
if (node.type === FileType.Folder) {
|
||||
dir = dir + '/' + node.basename
|
||||
}
|
||||
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files', fileid: node.fileid },
|
||||
{ dir, fileid: node.fileid, openfile: true },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
||||
// Before openFolderAction
|
||||
order: -1000,
|
||||
default: DefaultType.HIDDEN,
|
||||
})
|
||||
|
||||
registerFileAction(action)
|
||||
|
|
@ -159,6 +159,7 @@ import { formatFileSize, Permission } from '@nextcloud/files'
|
|||
import { Fragment } from 'vue-frag'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { vOnClickOutside } from '@vueuse/components'
|
||||
import axios from '@nextcloud/axios'
|
||||
import CancelablePromise from 'cancelable-promise'
|
||||
|
|
@ -367,10 +368,16 @@ export default Vue.extend({
|
|||
},
|
||||
previewUrl() {
|
||||
try {
|
||||
const url = new URL(window.location.origin + this.source.attributes.previewUrl)
|
||||
const previewUrl = this.source.attributes.previewUrl
|
||||
|| generateUrl('/core/preview?fileId={fileid}', {
|
||||
fileid: this.source.fileid,
|
||||
})
|
||||
const url = new URL(window.location.origin + previewUrl)
|
||||
|
||||
// Request tiny previews
|
||||
url.searchParams.set('x', '32')
|
||||
url.searchParams.set('y', '32')
|
||||
|
||||
// Handle cropping
|
||||
url.searchParams.set('a', this.cropPreviews === true ? '0' : '1')
|
||||
return url.href
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import './actions/downloadAction'
|
|||
import './actions/editLocallyAction'
|
||||
import './actions/favoriteAction'
|
||||
import './actions/openFolderAction'
|
||||
import './actions/openInFilesAction.js'
|
||||
import './actions/renameAction'
|
||||
import './actions/sidebarAction'
|
||||
import './actions/viewInFolderAction'
|
||||
|
|
@ -18,6 +19,7 @@ import NavigationService from './services/Navigation'
|
|||
import NavigationView from './views/Navigation.vue'
|
||||
import processLegacyFilesViews from './legacy/navigationMapper.js'
|
||||
import registerFavoritesView from './views/favorites'
|
||||
import registerRecentView from './views/recent'
|
||||
import registerPreviewServiceWorker from './services/ServiceWorker.js'
|
||||
import router from './router/router.js'
|
||||
import RouterService from './services/RouterService'
|
||||
|
|
@ -78,6 +80,7 @@ FilesList.$mount('#app-content-vue')
|
|||
// Init legacy and new files views
|
||||
processLegacyFilesViews()
|
||||
registerFavoritesView()
|
||||
registerRecentView()
|
||||
|
||||
// Register preview service worker
|
||||
registerPreviewServiceWorker()
|
||||
|
|
|
|||
148
apps/files/src/services/Recent.ts
Normal file
148
apps/files/src/services/Recent.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* @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 { File, Folder, Permission, parseWebdavPermissions } from '@nextcloud/files'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getClient, rootPath } from './WebdavClient'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { getDavNameSpaces, getDavProperties } from './DavProperties'
|
||||
import type { ContentsWithRoot } from './Navigation'
|
||||
import type { FileStat, ResponseDataDetailed, DAVResultResponseProps } from 'webdav'
|
||||
|
||||
const client = getClient(generateRemoteUrl('dav'))
|
||||
|
||||
const lastTwoWeeksTimestamp = Math.round((Date.now() / 1000) - (60 * 60 * 24 * 14))
|
||||
const searchPayload = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:searchrequest ${getDavNameSpaces()}
|
||||
xmlns:ns="https://github.com/icewind1991/SearchDAV/ns">
|
||||
<d:basicsearch>
|
||||
<d:select>
|
||||
<d:prop>
|
||||
${getDavProperties()}
|
||||
</d:prop>
|
||||
</d:select>
|
||||
<d:from>
|
||||
<d:scope>
|
||||
<d:href>/files/${getCurrentUser()?.uid}/</d:href>
|
||||
<d:depth>infinity</d:depth>
|
||||
</d:scope>
|
||||
</d:from>
|
||||
<d:where>
|
||||
<d:and>
|
||||
<d:or>
|
||||
<d:not>
|
||||
<d:eq>
|
||||
<d:prop>
|
||||
<d:getcontenttype/>
|
||||
</d:prop>
|
||||
<d:literal>httpd/unix-directory</d:literal>
|
||||
</d:eq>
|
||||
</d:not>
|
||||
<d:eq>
|
||||
<d:prop>
|
||||
<oc:size/>
|
||||
</d:prop>
|
||||
<d:literal>0</d:literal>
|
||||
</d:eq>
|
||||
</d:or>
|
||||
<d:gt>
|
||||
<d:prop>
|
||||
<d:getlastmodified/>
|
||||
</d:prop>
|
||||
<d:literal>${lastTwoWeeksTimestamp}</d:literal>
|
||||
</d:gt>
|
||||
</d:and>
|
||||
</d:where>
|
||||
<d:orderby>
|
||||
<d:order>
|
||||
<d:prop>
|
||||
<d:getlastmodified/>
|
||||
</d:prop>
|
||||
<d:descending/>
|
||||
</d:order>
|
||||
</d:orderby>
|
||||
<d:limit>
|
||||
<d:nresults>100</d:nresults>
|
||||
<ns:firstresult>0</ns:firstresult>
|
||||
</d:limit>
|
||||
</d:basicsearch>
|
||||
</d:searchrequest>`
|
||||
|
||||
interface ResponseProps extends DAVResultResponseProps {
|
||||
permissions: string,
|
||||
fileid: number,
|
||||
size: number,
|
||||
}
|
||||
|
||||
const resultToNode = function(node: FileStat): File | Folder {
|
||||
const props = node.props as ResponseProps
|
||||
const permissions = parseWebdavPermissions(props?.permissions)
|
||||
const owner = getCurrentUser()?.uid as string
|
||||
|
||||
const nodeData = {
|
||||
id: props?.fileid as number || 0,
|
||||
source: generateRemoteUrl('dav' + node.filename),
|
||||
mtime: new Date(node.lastmod),
|
||||
mime: node.mime as string,
|
||||
size: props?.size as number || 0,
|
||||
permissions,
|
||||
owner,
|
||||
root: rootPath,
|
||||
attributes: {
|
||||
...node,
|
||||
...props,
|
||||
hasPreview: props?.['has-preview'],
|
||||
},
|
||||
}
|
||||
|
||||
delete nodeData.attributes.props
|
||||
|
||||
return node.type === 'file'
|
||||
? new File(nodeData)
|
||||
: new Folder(nodeData)
|
||||
}
|
||||
|
||||
export const getContents = async (path = '/'): Promise<ContentsWithRoot> => {
|
||||
const contentsResponse = await client.getDirectoryContents(path, {
|
||||
details: true,
|
||||
data: searchPayload,
|
||||
headers: {
|
||||
// Patched in WebdavClient.ts
|
||||
method: 'SEARCH',
|
||||
// Somehow it's needed to get the correct response
|
||||
'Content-Type': 'application/xml; charset=utf-8',
|
||||
},
|
||||
deep: true,
|
||||
}) as ResponseDataDetailed<FileStat[]>
|
||||
|
||||
const contents = contentsResponse.data
|
||||
|
||||
return {
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: generateRemoteUrl('dav' + rootPath),
|
||||
root: rootPath,
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: Permission.READ,
|
||||
}),
|
||||
contents: contents.map(resultToNode),
|
||||
}
|
||||
}
|
||||
47
apps/files/src/views/recent.ts
Normal file
47
apps/files/src/views/recent.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* @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 type NavigationService from '../services/Navigation'
|
||||
import type { Navigation } from '../services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import HistorySvg from '@mdi/svg/svg/history.svg?raw'
|
||||
|
||||
import { getContents } from '../services/Recent'
|
||||
|
||||
export default () => {
|
||||
const Navigation = window.OCP.Files.Navigation as NavigationService
|
||||
Navigation.register({
|
||||
id: 'recent',
|
||||
name: t('files', 'Recent'),
|
||||
caption: t('files', 'List of recently modified files and folders.'),
|
||||
|
||||
emptyTitle: t('files', 'No recently modified files'),
|
||||
emptyCaption: t('files', 'Files and folders you recently modified will show up here.'),
|
||||
|
||||
icon: HistorySvg,
|
||||
order: 2,
|
||||
|
||||
defaultSortKey: 'mtime',
|
||||
|
||||
getContents,
|
||||
} as Navigation)
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?php /** @var \OCP\IL10N $l */ ?>
|
||||
|
||||
<div class="emptyfilelist emptycontent hidden"></div>
|
||||
|
||||
<div class="nofilterresults emptycontent hidden">
|
||||
<div class="icon-search"></div>
|
||||
<h2><?php p($l->t('No entries found in this folder')); ?></h2>
|
||||
<p></p>
|
||||
</div>
|
||||
|
||||
<table class="files-filestable list-container <?php p($_['showgridview'] ? 'view-grid' : '') ?>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hidden column-name">
|
||||
<div class="column-name-container">
|
||||
<a class="name sort columntitle" href="#" onclick="event.preventDefault()"
|
||||
data-sort="name"><span><?php p($l->t('Name')); ?></span></a>
|
||||
</div>
|
||||
</th>
|
||||
<th class="hidden column-size">
|
||||
<a class="size sort columntitle" href="#" onclick="event.preventDefault()"
|
||||
data-sort="size"><span><?php p($l->t('Size')); ?></span></a>
|
||||
</th>
|
||||
<th class="hidden column-mtime">
|
||||
<a class="columntitle" href="#" onclick="event.preventDefault()"
|
||||
data-sort="mtime"><span><?php p($l->t('Modified')); ?></span><span
|
||||
class="sort-indicator"></span></a>
|
||||
<span class="selectedActions">
|
||||
<a href="#" onclick="event.preventDefault()" class="delete-selected">
|
||||
<span class="icon icon-delete"></span>
|
||||
<span><?php p($l->t('Delete')) ?></span>
|
||||
</a>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="files-fileList">
|
||||
</tbody>
|
||||
<tfoot>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
|
@ -185,19 +185,6 @@ class ViewControllerTest extends TestCase {
|
|||
'expanded' => false,
|
||||
'unread' => 0,
|
||||
],
|
||||
'recent' => [
|
||||
'id' => 'recent',
|
||||
'appname' => 'files',
|
||||
'script' => 'recentlist.php',
|
||||
'order' => 2,
|
||||
'name' => \OC::$server->getL10N('files')->t('Recent'),
|
||||
'active' => false,
|
||||
'icon' => '',
|
||||
'type' => 'link',
|
||||
'classes' => '',
|
||||
'expanded' => false,
|
||||
'unread' => 0,
|
||||
],
|
||||
'systemtagsfilter' => [
|
||||
'id' => 'systemtagsfilter',
|
||||
'appname' => 'systemtags',
|
||||
|
|
@ -233,10 +220,6 @@ class ViewControllerTest extends TestCase {
|
|||
'id' => 'files',
|
||||
'content' => null,
|
||||
],
|
||||
'recent' => [
|
||||
'id' => 'recent',
|
||||
'content' => null,
|
||||
],
|
||||
'systemtagsfilter' => [
|
||||
'id' => 'systemtagsfilter',
|
||||
'content' => null,
|
||||
|
|
|
|||
|
|
@ -110,12 +110,27 @@ describe('Enter credentials action enabled tests', () => {
|
|||
},
|
||||
})
|
||||
|
||||
const notAStorage = new Folder({
|
||||
const missingConfig = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const notAStorage = new File({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/test.txt',
|
||||
mime: 'text/plain',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
test('Disabled with on success storage', () => {
|
||||
|
|
@ -138,6 +153,11 @@ describe('Enter credentials action enabled tests', () => {
|
|||
expect(action.enabled!([globalAuthUserStorage], view)).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for missing config', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([missingConfig], view)).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for normal nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([notAStorage], view)).toBe(false)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
.files-list__row-status {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 44px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
|
|
@ -33,4 +33,4 @@
|
|||
&--warning {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const rootPath = `/files/${getCurrentUser()?.uid}`
|
|||
|
||||
export type StorageConfig = {
|
||||
applicableUsers?: string[]
|
||||
applicableGroups?: string[]
|
||||
applicableGroups?: string[]
|
||||
authMechanism: string
|
||||
backend: string
|
||||
backendOptions: Record<string, string>
|
||||
|
|
|
|||
91
apps/files_external/src/utils/externalStorageUtils.spec.ts
Normal file
91
apps/files_external/src/utils/externalStorageUtils.spec.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* @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 { File, Folder, Permission } from '@nextcloud/files'
|
||||
import { isNodeExternalStorage } from './externalStorageUtils'
|
||||
import { expect } from '@jest/globals'
|
||||
|
||||
describe('Is node an external storage', () => {
|
||||
test('A Folder with a backend and a valid scope is an external storage', () => {
|
||||
const folder = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'personal',
|
||||
backend: 'SFTP',
|
||||
},
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(true)
|
||||
})
|
||||
|
||||
test('a File is not a valid storage', () => {
|
||||
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(isNodeExternalStorage(file)).toBe(false)
|
||||
})
|
||||
|
||||
test('A Folder without a backend is not a storage', () => {
|
||||
const folder = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'personal',
|
||||
},
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
|
||||
test('A Folder without a scope is not a storage', () => {
|
||||
const folder = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
backend: 'SFTP',
|
||||
},
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
|
||||
test('A Folder with an invalid scope is not a storage', () => {
|
||||
const folder = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'null',
|
||||
backend: 'SFTP',
|
||||
},
|
||||
})
|
||||
expect(isNodeExternalStorage(folder)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -313,7 +313,6 @@ describe('SharingService share to Node mapping', () => {
|
|||
expect(file.root).toBe('/files/test')
|
||||
expect(file.attributes).toBeInstanceOf(Object)
|
||||
expect(file.attributes['has-preview']).toBe(true)
|
||||
expect(file.attributes.previewUrl).toBe('/index.php/core/preview?fileId=530936&x=32&y=32&forceIcon=0')
|
||||
expect(file.attributes.favorite).toBe(0)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
|
|||
const Node = isFolder ? Folder : File
|
||||
|
||||
const fileid = ocsEntry.file_source
|
||||
const previewUrl = hasPreview ? generateUrl('/core/preview?fileId={fileid}&x=32&y=32&forceIcon=0', { fileid }) : undefined
|
||||
|
||||
// Generate path and strip double slashes
|
||||
const path = ocsEntry?.path || ocsEntry.file_target
|
||||
|
|
@ -76,7 +75,6 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
|
|||
root: rootPath,
|
||||
attributes: {
|
||||
...ocsEntry,
|
||||
previewUrl,
|
||||
'has-preview': hasPreview,
|
||||
favorite: ocsEntry?.tags?.includes(window.OC.TAG_FAVORITE) ? 1 : 0,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
|
|||
import AccountSvg from '@mdi/svg/svg/account.svg?raw'
|
||||
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
|
||||
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
|
||||
import AccouontPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
|
||||
import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
|
||||
|
||||
import { getContents } from '../services/SharingService'
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export default () => {
|
|||
emptyTitle: t('files_sharing', 'No shares'),
|
||||
emptyCaption: t('files_sharing', 'Files and folders you shared or have been shared with you will show up here'),
|
||||
|
||||
icon: AccouontPlusSvg,
|
||||
icon: AccountPlusSvg,
|
||||
order: 20,
|
||||
|
||||
columns: [],
|
||||
|
|
|
|||
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_external-main.js
vendored
4
dist/files_external-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_external-main.js.map
vendored
2
dist/files_external-main.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-files_sharing.js
vendored
4
dist/files_sharing-files_sharing.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-files_sharing.js.map
vendored
2
dist/files_sharing-files_sharing.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue