Merge pull request #35772 from nextcloud/feat/files2vue-navigation

Port Files navigation to vue
pull/35995/head
John Molakvoæ 1 year ago committed by GitHub
commit e235c6438c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -55,7 +55,7 @@ jobs:
fail-fast: false
matrix:
# run multiple copies of the current job in parallel
containers: [1, 2]
containers: ['component', 1, 2]
name: runner ${{ matrix.containers }}
@ -66,14 +66,18 @@ jobs:
key: cypress-context-${{ github.run_id }}
path: /home/runner/work/server
- name: Run E2E cypress tests
uses: cypress-io/github-action@v4
- name: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }} cypress tests
uses: cypress-io/github-action@v5
with:
record: true
parallel: true
# cypress run type
component: ${{ matrix.containers == 'component' }}
group: Run ${{ matrix.containers == 'component' && 'component' || 'E2E' }}
# cypress env
ci-build-id: ${{ github.sha }}-${{ github.run_number }}
tag: ${{ github.event_name }}
tag: ${{ github.event_name }}t
env:
# Needs to be prefixed with CYPRESS_
CYPRESS_BRANCH: ${{ env.BRANCH }}

@ -39,19 +39,47 @@ namespace OCA\Files\AppInfo;
use OCA\Files\Controller\OpenLocalEditorController;
// Legacy routes above
/** @var $this \OC\Route\Router */
$this->create('files_ajax_download', 'apps/files/ajax/download.php')
->actionInclude('files/ajax/download.php');
$this->create('files_ajax_list', 'apps/files/ajax/list.php')
->actionInclude('files/ajax/list.php');
/** @var Application $application */
$application = \OC::$server->query(Application::class);
$application->registerRoutes(
$this,
[
'routes' => [
[
'name' => 'view#index',
'url' => '/',
'verb' => 'GET',
],
[
'name' => 'view#index',
'url' => '/{view}',
'verb' => 'GET',
'postfix' => 'view',
],
[
'name' => 'view#index',
'url' => '/{view}/{fileid}',
'verb' => 'GET',
'postfix' => 'fileid',
],
[
'name' => 'View#showFile',
'url' => '/f/{fileid}',
'verb' => 'GET',
'root' => '',
],
[
'name' => 'ajax#getStorageStats',
'url' => '/ajax/getstoragestats',
'verb' => 'GET',
],
[
'name' => 'API#getThumbnail',
'url' => '/api/v1/thumbnail/{x}/{y}/{file}',
@ -69,6 +97,16 @@ $application->registerRoutes(
'url' => '/api/v1/recent/',
'verb' => 'GET'
],
[
'name' => 'API#setConfig',
'url' => '/api/v1/config/{key}',
'verb' => 'POST'
],
[
'name' => 'API#getConfigs',
'url' => '/api/v1/configs',
'verb' => 'GET'
],
[
'name' => 'API#updateFileSorting',
'url' => '/api/v1/sorting',
@ -94,16 +132,6 @@ $application->registerRoutes(
'url' => '/api/v1/showgridview',
'verb' => 'GET'
],
[
'name' => 'view#index',
'url' => '/',
'verb' => 'GET',
],
[
'name' => 'ajax#getStorageStats',
'url' => '/ajax/getstoragestats',
'verb' => 'GET',
],
[
'name' => 'API#toggleShowFolder',
'url' => '/api/v1/toggleShowFolder/{key}',
@ -186,10 +214,3 @@ $application->registerRoutes(
],
]
);
/** @var $this \OC\Route\Router */
$this->create('files_ajax_download', 'apps/files/ajax/download.php')
->actionInclude('files/ajax/download.php');
$this->create('files_ajax_list', 'apps/files/ajax/list.php')
->actionInclude('files/ajax/list.php');

@ -58,5 +58,6 @@ return array(
'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php',
'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php',
'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php',
'OCA\\Files\\Service\\UserConfig' => $baseDir . '/../lib/Service/UserConfig.php',
'OCA\\Files\\Settings\\PersonalSettings' => $baseDir . '/../lib/Settings/PersonalSettings.php',
);

@ -73,6 +73,7 @@ class ComposerStaticInitFiles
'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php',
'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php',
'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php',
'OCA\\Files\\Service\\UserConfig' => __DIR__ . '/..' . '/../lib/Service/UserConfig.php',
'OCA\\Files\\Settings\\PersonalSettings' => __DIR__ . '/..' . '/../lib/Settings/PersonalSettings.php',
);

@ -480,7 +480,9 @@ table td.filename .thumbnail {
display: inline-block;
width: 32px;
height: 32px;
background-size: 32px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-left: 9px;
margin-top: 9px;
border-radius: var(--border-radius);

File diff suppressed because one or more lines are too long

@ -380,7 +380,9 @@ table td.filename .thumbnail {
display: inline-block;
width: 32px;
height: 32px;
background-size: 32px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-left: 9px;
margin-top: 9px;
border-radius: var(--border-radius);

@ -480,7 +480,9 @@ table td.filename .thumbnail {
display: inline-block;
width: 32px;
height: 32px;
background-size: 32px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin-left: 9px;
margin-top: 9px;
border-radius: var(--border-radius);

File diff suppressed because one or more lines are too long

@ -27,9 +27,9 @@
*/
OCA.Files.App = {
/**
* Navigation control
* Navigation instance
*
* @member {OCA.Files.Navigation}
* @member {OCP.Files.Navigation}
*/
navigation: null,
@ -51,16 +51,11 @@
* Initializes the files app
*/
initialize: function() {
this.navigation = new OCA.Files.Navigation($('#app-navigation'));
this.navigation = OCP.Files.Navigation;
this.$showHiddenFiles = $('input#showhiddenfilesToggle');
var showHidden = $('#showHiddenFiles').val() === "1";
this.$showHiddenFiles.prop('checked', showHidden);
// crop image previews
this.$cropImagePreviews = $('input#cropimagepreviewsToggle');
var cropImagePreviews = $('#cropImagePreviews').val() === "1";
this.$cropImagePreviews.prop('checked', cropImagePreviews);
// Toggle for grid view
this.$showGridView = $('input#showgridview');
this.$showGridView.on('change', _.bind(this._onGridviewChange, this));
@ -69,12 +64,9 @@
OC.Notification.show(t('files', 'File could not be found'), {type: 'error'});
}
this._filesConfig = new OC.Backbone.Model({
showhidden: showHidden,
cropimagepreviews: cropImagePreviews,
});
this._filesConfig = OCP.InitialState.loadState('files', 'config', {})
var urlParams = OC.Util.History.parseUrlQuery();
var { fileid, scrollto, openfile } = OC.Util.History.parseUrlQuery();
var fileActions = new OCA.Files.FileActions();
// default actions
fileActions.registerDefaultActions();
@ -94,8 +86,8 @@
folderDropOptions: folderDropOptions,
fileActions: fileActions,
allowLegacyActions: true,
scrollTo: urlParams.scrollto,
openFile: urlParams.openfile,
scrollTo: scrollto,
openFile: openfile,
filesClient: OC.Files.getClient(),
multiSelectMenu: [
{
@ -144,7 +136,7 @@
this._setupEvents();
// trigger URL change event handlers
this._onPopState(urlParams);
this._onPopState({ ...OC.Util.History.parseUrlQuery(), view: this.navigation?.active?.id });
this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200);
this._debouncedPersistCropImagePreviewsState = _.debounce(this._persistCropImagePreviewsState, 1200);
@ -159,7 +151,6 @@
* Destroy the app
*/
destroy: function() {
this.navigation = null;
this.fileList.destroy();
this.fileList = null;
this.files = null;
@ -216,15 +207,17 @@
* @return app container
*/
getCurrentAppContainer: function() {
return this.navigation.getActiveContainer();
var viewId = this.getActiveView();
return $('#app-content-' + viewId);
},
/**
* Sets the currently active view
* @param viewId view id
*/
setActiveView: function(viewId, options) {
this.navigation.setActiveItem(viewId, options);
setActiveView: function(viewId) {
// The Navigation API will handle the final event
window._nc_event_bus.emit('files:legacy-navigation:changed', { id: viewId })
},
/**
@ -232,7 +225,8 @@
* @return view id
*/
getActiveView: function() {
return this.navigation.getActiveItem();
return this.navigation.active
&& this.navigation.active.id;
},
/**
@ -253,71 +247,29 @@
$('#app-content').delegate('>div', 'changeDirectory', _.bind(this._onDirectoryChanged, this));
$('#app-content').delegate('>div', 'afterChangeDirectory', _.bind(this._onAfterDirectoryChanged, this));
$('#app-content').delegate('>div', 'changeViewerMode', _.bind(this._onChangeViewerMode, this));
$('#app-navigation').on('itemChanged', _.bind(this._onNavigationChanged, this));
this.$showHiddenFiles.on('change', _.bind(this._onShowHiddenFilesChange, this));
this.$cropImagePreviews.on('change', _.bind(this._onCropImagePreviewsChange, this));
},
/**
* Toggle showing hidden files according to the settings checkbox
*
* @returns {undefined}
*/
_onShowHiddenFilesChange: function() {
var show = this.$showHiddenFiles.is(':checked');
this._filesConfig.set('showhidden', show);
this._debouncedPersistShowHiddenFilesState();
},
/**
* Persist show hidden preference on the server
*
* @returns {undefined}
*/
_persistShowHiddenFilesState: function() {
var show = this._filesConfig.get('showhidden');
$.post(OC.generateUrl('/apps/files/api/v1/showhidden'), {
show: show
});
},
/**
* Toggle cropping image previews according to the settings checkbox
*
* @returns void
*/
_onCropImagePreviewsChange: function() {
var crop = this.$cropImagePreviews.is(':checked');
this._filesConfig.set('cropimagepreviews', crop);
this._debouncedPersistCropImagePreviewsState();
},
/**
* Persist crop image previews preference on the server
*
* @returns void
*/
_persistCropImagePreviewsState: function() {
var crop = this._filesConfig.get('cropimagepreviews');
$.post(OC.generateUrl('/apps/files/api/v1/cropimagepreviews'), {
crop: crop
});
},
/**
* Event handler for when the current navigation item has changed
*/
_onNavigationChanged: function(e) {
_onNavigationChanged: function(view) {
var params;
if (e && e.itemId) {
params = {
view: typeof e.view === 'string' && e.view !== '' ? e.view : e.itemId,
dir: e.dir ? e.dir : '/'
};
if (view && (view.itemId || view.id)) {
if (view.id) {
params = {
view: view.id,
dir: '/',
}
} else {
// Legacy handling
params = {
view: typeof view.view === 'string' && view.view !== '' ? view.view : view.itemId,
dir: view.dir ? view.dir : '/'
}
}
this._changeUrl(params.view, params.dir);
OCA.Files.Sidebar.close();
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params));
window._nc_event_bus.emit('files:navigation:changed')
}
},
@ -327,7 +279,7 @@
*/
_onDirectoryChanged: function(e) {
if (e.dir && !e.changedThroughUrl) {
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
this._changeUrl(this.getActiveView(), e.dir, e.fileId);
}
},
@ -336,7 +288,7 @@
*/
_onAfterDirectoryChanged: function(e) {
if (e.dir && e.fileId) {
this._changeUrl(this.navigation.getActiveItem(), e.dir, e.fileId);
this._changeUrl(this.getActiveView(), e.dir, e.fileId);
}
},
@ -361,16 +313,20 @@
dir: '/',
view: 'files'
}, params);
var lastId = this.navigation.getActiveItem();
if (!this.navigation.itemExists(params.view)) {
var lastId = this.navigation.active;
if (!this.navigation.views.find(view => view.id === params.view)) {
params.view = 'files';
}
this.navigation.setActiveItem(params.view, {silent: true});
if (lastId !== this.navigation.getActiveItem()) {
this.navigation.getActiveContainer().trigger(new $.Event('show'));
this.setActiveView(params.view, {silent: true});
if (lastId !== this.getActiveView()) {
this.getCurrentAppContainer().trigger(new $.Event('show', params));
}
this.navigation.getActiveContainer().trigger(new $.Event('urlChanged', params));
this.getCurrentAppContainer().trigger(new $.Event('urlChanged', params));
window._nc_event_bus.emit('files:navigation:changed')
},
/**
@ -390,7 +346,7 @@
* Change the URL to point to the given dir and view
*/
_changeUrl: function(view, dir, fileId) {
var params = {dir: dir};
var params = { dir: dir };
if (view !== 'files') {
params.view = view;
} else if (fileId) {
@ -401,9 +357,11 @@
if (currentParams.fileid !== params.fileid) {
// if only fileid changed or was added, replace instead of push
OC.Util.History.replaceState(this._makeUrlParams(params));
return
}
} else {
OC.Util.History.pushState(this._makeUrlParams(params));
return
}
},

@ -67,7 +67,7 @@ window.addEventListener('DOMContentLoaded', function() {
reload: function() {
this.showMask();
if (this._reloadCall) {
if (this._reloadCall?.abort) {
this._reloadCall.abort();
}

@ -173,7 +173,8 @@
_filter: '',
/**
* @type Backbone.Model
* @type UserConfig
* @see /apps/files/lib/Service/UserConfig.php
*/
_filesConfig: undefined,
@ -252,10 +253,7 @@
} else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) {
this._filesConfig = OCA.Files.App.getFilesConfig();
} else {
this._filesConfig = new OC.Backbone.Model({
'showhidden': false,
'cropimagepreviews': true
});
this._filesConfig = OCP.InitialState.loadState('files', 'config', {})
}
if (options.dragOptions) {
@ -281,26 +279,30 @@
this.$header = $el.find('.filelist-header');
this.$footer = $el.find('.filelist-footer');
if (!_.isUndefined(this._filesConfig)) {
this._filesConfig.on('change:showhidden', function() {
var showHidden = this.get('showhidden');
self.$el.toggleClass('hide-hidden-files', !showHidden);
// Legacy mapper for new vue components
window._nc_event_bus.subscribe('files:config:updated', ({ key, value }) => {
// Replace existing config with new one
Object.assign(this._filesConfig, { [key]: value })
if (key === 'show_hidden') {
self.$el.toggleClass('hide-hidden-files', !value);
self.updateSelectionSummary();
if (!showHidden) {
// hiding files could make the page too small, need to try rendering next page
// hiding files could make the page too small, need to try rendering next page
if (!value) {
self._onScroll();
}
});
this._filesConfig.on('change:cropimagepreviews', function() {
}
if (key === 'crop_image_previews') {
self.reload();
});
}
})
this.$el.toggleClass('hide-hidden-files', !this._filesConfig.get('showhidden'));
var config = OCP.InitialState.loadState('files', 'config', {})
if (config.show_hidden === false) {
this.$el.addClass('hide-hidden-files');
}
if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
this._detailsView = new OCA.Files.DetailsView();
this._detailsView.$el.addClass('disappear');
@ -413,7 +415,7 @@
this._setCurrentDir(options.dir || '/', false);
}
if(options.openFile) {
if (options.openFile) {
// Wait for some initialisation process to be over before triggering the default action.
_.defer(() => {
try {
@ -755,16 +757,13 @@
*/
_onShow: function(e) {
OCA.Files.App && OCA.Files.App.updateCurrentFileList(this);
if (this.shown) {
if (e.itemId === this.id) {
this._setCurrentDir('/', false);
}
// Only reload if we don't navigate to a different directory
if (typeof e.dir === 'undefined' || e.dir === this.getCurrentDirectory()) {
this.reload();
}
if (e.itemId === this.id) {
this._setCurrentDir('/', false);
}
// Only reload if we don't navigate to a different directory
if (typeof e.dir === 'undefined' || e.dir === this.getCurrentDirectory()) {
this.reload();
}
this.shown = true;
},
/**
@ -1407,7 +1406,7 @@
fileData,
newTrs = [],
isAllSelected = this.isAllSelected(),
showHidden = this._filesConfig.get('showhidden');
showHidden = this._filesConfig.show_hidden;
if (index >= this.files.length) {
return false;
@ -2371,7 +2370,7 @@
* Images are cropped to a square by default. Append a=1 to the URL
* if the user wants to see images with original aspect ratio.
*/
urlSpec.a = this._filesConfig.get('cropimagepreviews') ? 0 : 1;
urlSpec.a = this._filesConfig.crop_image_previews ? 0 : 1;
if (typeof urlSpec.fileId !== 'undefined') {
delete urlSpec.file;
@ -3295,7 +3294,7 @@
this.$el.find('tfoot').append($tr);
return new OCA.Files.FileSummary($tr, {config: this._filesConfig});
return new OCA.Files.FileSummary($tr, { config: this._filesConfig });
},
updateEmptyContent: function() {
var permissions = this.getDirectoryPermissions();
@ -3443,7 +3442,7 @@
var summary = this._selectionSummary.summary;
var selection;
var showHidden = !!this._filesConfig.get('showhidden');
var showHidden = !!this._filesConfig.show_hidden;
if (summary.totalFiles === 0 && summary.totalDirs === 0) {
this.$el.find('.column-name a.name>span:first').text(t('files','Name'));
this.$el.find('.column-size a>span:first').text(t('files','Size'));

@ -36,10 +36,12 @@
this.$el = $tr;
var filesConfig = options.config;
if (filesConfig) {
this._showHidden = !!filesConfig.get('showhidden');
filesConfig.on('change:showhidden', function() {
self._showHidden = !!this.get('showhidden');
self.update();
this._showHidden = !!filesConfig.show_hidden;
window._nc_event_bus.subscribe('files:config:updated', ({ key, value }) => {
if (key === 'show_hidden') {
self._showHidden = !!value;
self.update();
}
});
}
this.clear();

@ -19,7 +19,6 @@
"jquery.fileupload.js",
"keyboardshortcuts.js",
"mainfileinfodetailview.js",
"navigation.js",
"newfilemenu.js",
"operationprogressbar.js",
"recentfilelist.js",

@ -72,7 +72,7 @@ window.addEventListener('DOMContentLoaded', function () {
reload: function () {
this.showMask();
if (this._reloadCall) {
if (this._reloadCall?.abort) {
this._reloadCall.abort();
}

@ -47,6 +47,7 @@ use OCA\Files\Listener\LoadSidebarListener;
use OCA\Files\Notification\Notifier;
use OCA\Files\Search\FilesSearchProvider;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
use OCP\Activity\IManager as IActivityManager;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@ -88,7 +89,8 @@ class Application extends App implements IBootstrap {
$c->get(IPreview::class),
$c->get(IShareManager::class),
$c->get(IConfig::class),
$server->getUserFolder()
$server->getUserFolder(),
$c->get(UserConfig::class),
);
});
@ -172,7 +174,6 @@ class Application extends App implements IBootstrap {
'script' => 'simplelist.php',
'order' => 5,
'name' => $l10n->t('Favorites'),
'expandedState' => 'show_Quick_Access'
];
});
}

@ -39,6 +39,7 @@ namespace OCA\Files\Controller;
use OC\Files\Node\Node;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
@ -61,18 +62,13 @@ use OCP\Share\IShare;
* @package OCA\Files\Controller
*/
class ApiController extends Controller {
/** @var TagService */
private $tagService;
/** @var IManager * */
private $shareManager;
/** @var IPreview */
private $previewManager;
/** @var IUserSession */
private $userSession;
/** @var IConfig */
private $config;
/** @var Folder */
private $userFolder;
private TagService $tagService;
private IManager $shareManager;
private IPreview $previewManager;
private IUserSession $userSession;
private IConfig $config;
private Folder $userFolder;
private UserConfig $userConfig;
/**
* @param string $appName
@ -91,7 +87,8 @@ class ApiController extends Controller {
IPreview $previewManager,
IManager $shareManager,
IConfig $config,
Folder $userFolder) {
Folder $userFolder,
UserConfig $userConfig) {
parent::__construct($appName, $request);
$this->userSession = $userSession;
$this->tagService = $tagService;
@ -99,6 +96,7 @@ class ApiController extends Controller {
$this->shareManager = $shareManager;
$this->config = $config;
$this->userFolder = $userFolder;
$this->userConfig = $userConfig;
}
/**
@ -282,17 +280,48 @@ class ApiController extends Controller {
return new Response();
}
/**
* Toggle default files user config
*
* @NoAdminRequired
*
* @param string $key
* @param string|bool $value
* @return JSONResponse
*/
public function setConfig(string $key, $value): JSONResponse {
try {
$this->userConfig->setConfig($key, (string)$value);
} catch (\InvalidArgumentException $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
return new JSONResponse(['message' => 'ok', 'data' => ['key' => $key, 'value' => $value]]);
}
/**
* Get the user config
*
* @NoAdminRequired
*
* @return JSONResponse
*/
public function getConfigs(): JSONResponse {
return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]);
}
/**
* Toggle default for showing/hiding hidden files
*
* @NoAdminRequired
*
* @param bool $show
* @param bool $value
* @return Response
* @throws \OCP\PreConditionNotMetException
*/
public function showHiddenFiles(bool $show): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $show ? '1' : '0');
public function showHiddenFiles(bool $value): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0');
return new Response();
}
@ -301,12 +330,12 @@ class ApiController extends Controller {
*
* @NoAdminRequired
*
* @param bool $crop
* @param bool $value
* @return Response
* @throws \OCP\PreConditionNotMetException
*/
public function cropImagePreviews(bool $crop): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $crop ? '1' : '0');
public function cropImagePreviews(bool $value): Response {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0');
return new Response();
}
@ -346,18 +375,18 @@ class ApiController extends Controller {
* @throws \OCP\PreConditionNotMetException
*/
public function toggleShowFolder(int $show, string $key): Response {
// ensure the edited key exists
$navItems = \OCA\Files\App::getNavigationManager()->getAll();
foreach ($navItems as $item) {
// check if data is valid
if (($show === 0 || $show === 1) && isset($item['expandedState']) && $key === $item['expandedState']) {
$this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', $key, (string)$show);
return new Response();
}
if ($show !== 0 && $show !== 1) {
return new DataResponse([
'message' => 'Invalid show value. Only 0 and 1 are allowed.'
], Http::STATUS_BAD_REQUEST);
}
$response = new Response();
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
$userId = $this->userSession->getUser()->getUID();
// Set the new value and return it
// Using a prefix prevents the user from setting arbitrary keys
$this->config->setUserValue($userId, 'files', 'show_' . $key, (string)$show);
return new JSONResponse([$key => $show]);
}
/**

@ -36,8 +36,10 @@
namespace OCA\Files\Controller;
use OCA\Files\Activity\Helper;
use OCA\Files\AppInfo\Application;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files\Event\LoadSidebar;
use OCA\Files\Service\UserConfig;
use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
@ -65,32 +67,18 @@ use OCP\Share\IManager;
* @package OCA\Files\Controller
*/
class ViewController extends Controller {
/** @var string */
protected $appName;
/** @var IRequest */
protected $request;
/** @var IURLGenerator */
protected $urlGenerator;
/** @var IL10N */
protected $l10n;
/** @var IConfig */
protected $config;
/** @var IEventDispatcher */
protected $eventDispatcher;
/** @var IUserSession */
protected $userSession;
/** @var IAppManager */
protected $appManager;
/** @var IRootFolder */
protected $rootFolder;
/** @var Helper */
protected $activityHelper;
/** @var IInitialState */
private $initialState;
/** @var ITemplateManager */
private $templateManager;
/** @var IManager */
private $shareManager;
private IURLGenerator $urlGenerator;
private IL10N $l10n;
private IConfig $config;
private IEventDispatcher $eventDispatcher;
private IUserSession $userSession;
private IAppManager $appManager;
private IRootFolder $rootFolder;
private Helper $activityHelper;
private IInitialState $initialState;
private ITemplateManager $templateManager;
private IManager $shareManager;
private UserConfig $userConfig;
public function __construct(string $appName,
IRequest $request,
@ -104,11 +92,10 @@ class ViewController extends Controller {
Helper $activityHelper,
IInitialState $initialState,
ITemplateManager $templateManager,
IManager $shareManager
IManager $shareManager,
UserConfig $userConfig
) {
parent::__construct($appName, $request);
$this->appName = $appName;
$this->request = $request;
$this->urlGenerator = $urlGenerator;
$this->l10n = $l10n;
$this->config = $config;
@ -120,6 +107,7 @@ class ViewController extends Controller {
$this->initialState = $initialState;
$this->templateManager = $templateManager;
$this->shareManager = $shareManager;
$this->userConfig = $userConfig;
}
/**
@ -186,6 +174,7 @@ class ViewController extends Controller {
* @throws NotFoundException
*/
public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false, $openfile = null) {
if ($fileid !== null && $dir === '') {
try {
return $this->redirectToFile($fileid);
@ -205,11 +194,11 @@ class ViewController extends Controller {
// FIXME: Make non static
$storageInfo = $this->getStorageInfo();
$user = $this->userSession->getUser()->getUID();
$userId = $this->userSession->getUser()->getUID();
// Get all the user favorites to create a submenu
try {
$favElements = $this->activityHelper->getFavoriteFilePaths($this->userSession->getUser()->getUID());
$favElements = $this->activityHelper->getFavoriteFilePaths($userId);
} catch (\RuntimeException $e) {
$favElements['folders'] = [];
}
@ -222,20 +211,17 @@ class ViewController extends Controller {
$favoritesSublistArray = [];
$navBarPositionPosition = 6;
$currentCount = 0;
foreach ($favElements['folders'] as $favElement) {
$link = $this->urlGenerator->linkToRoute('files.view.index', ['dir' => $favElement, 'view' => 'files']);
$sortingValue = ++$currentCount;
$element = [
'id' => str_replace('/', '-', $favElement),
'view' => 'files',
'href' => $link,
'dir' => $favElement,
'order' => $navBarPositionPosition,
'folderPosition' => $sortingValue,
'name' => basename($favElement),
'icon' => 'files',
'quickaccesselement' => 'true'
'icon' => 'folder',
'params' => [
'view' => 'files',
'dir' => $favElement,
],
];
array_push($favoritesSublistArray, $element);
@ -248,11 +234,9 @@ class ViewController extends Controller {
$navItems['favorites']['sublist'] = $favoritesSublistArray;
$navItems['favorites']['classes'] = $collapseClasses;
// parse every menu and add the expandedState user value
// parse every menu and add the expanded user value
foreach ($navItems as $key => $item) {
if (isset($item['expandedState'])) {
$navItems[$key]['defaultExpandedState'] = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', $item['expandedState'], '0') === '1';
}
$navItems[$key]['expanded'] = $this->config->getUserValue($userId, 'files', 'show_' . $item['id'], '0') === '1';
}
$nav->assign('navigationItems', $navItems);
@ -267,10 +251,11 @@ class ViewController extends Controller {
$nav->assign('quota', $storageInfo['quota']);
$nav->assign('usage_relative', $storageInfo['relative']);
$nav->assign('webdav_url', \OCP\Util::linkToRemote('dav/files/' . rawurlencode($user)));
$contentItems = [];
$this->initialState->provideInitialState('navigation', $navItems);
$this->initialState->provideInitialState('config', $this->userConfig->getConfigs());
// render the container content for every navigation item
foreach ($navItems as $item) {
$content = '';
@ -314,12 +299,12 @@ class ViewController extends Controller {
$params['ownerDisplayName'] = $storageInfo['ownerDisplayName'] ?? '';
$params['isPublic'] = false;
$params['allowShareWithLink'] = $this->shareManager->shareApiAllowLinks() ? 'yes' : 'no';
$params['defaultFileSorting'] = $this->config->getUserValue($user, 'files', 'file_sorting', 'name');
$params['defaultFileSortingDirection'] = $this->config->getUserValue($user, 'files', 'file_sorting_direction', 'asc');
$params['showgridview'] = $this->config->getUserValue($user, 'files', 'show_grid', false);
$showHidden = (bool) $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', false);
$params['defaultFileSorting'] = $this->config->getUserValue($userId, 'files', 'file_sorting', 'name');
$params['defaultFileSortingDirection'] = $this->config->getUserValue($userId, 'files', 'file_sorting_direction', 'asc');
$params['showgridview'] = $this->config->getUserValue($userId, 'files', 'show_grid', false);
$showHidden = (bool) $this->config->getUserValue($userId, 'files', 'show_hidden', false);
$params['showHiddenFiles'] = $showHidden ? 1 : 0;
$cropImagePreviews = (bool) $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', true);
$cropImagePreviews = (bool) $this->config->getUserValue($userId, 'files', 'crop_image_previews', true);
$params['cropImagePreviews'] = $cropImagePreviews ? 1 : 0;
$params['fileNotFound'] = $fileNotFound ? 1 : 0;
$params['appNavigation'] = $nav;
@ -327,7 +312,7 @@ class ViewController extends Controller {
$params['hiddenFields'] = $event->getHiddenFields();
$response = new TemplateResponse(
$this->appName,
Application::APP_ID,
'index',
$params
);

@ -0,0 +1,144 @@
<?php
/**
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @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/>.
*
*/
namespace OCA\Files\Service;
use OCA\Files\AppInfo\Application;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserSession;
class UserConfig {
const ALLOWED_CONFIGS = [
[
// Whether to crop the files previews or not in the files list
'key' => 'crop_image_previews',
'default' => true,
'allowed' => [true, false],
],
[
// Whether to show the hidden files or not in the files list
'key' => 'show_hidden',
'default' => false,
'allowed' => [true, false],
],
];
protected IConfig $config;
protected ?IUser $user = null;
public function __construct(IConfig $config, IUserSession $userSession) {
$this->config = $config;
$this->user = $userSession->getUser();
}
/**
* Get the list of all allowed user config keys
* @return string[]
*/
public function getAllowedConfigKeys(): array {
return array_map(function($config) {
return $config['key'];
}, self::ALLOWED_CONFIGS);
}
/**
* Get the list of allowed config values for a given key
*
* @param string $key a valid config key
* @return array
*/
private function getAllowedConfigValues(string $key): array {
foreach (self::ALLOWED_CONFIGS as $config) {
if ($config['key'] === $key) {
return $config['allowed'];
}
}
return [];
}
/**
* Get the default config value for a given key
*
* @param string $key a valid config key
* @return string|bool
*/
private function getDefaultConfigValue(string $key) {
foreach (self::ALLOWED_CONFIGS as $config) {
if ($config['key'] === $key) {
return $config['default'];
}
}
return '';
}
/**
* Set a user config
*
* @param string $key
* @param string|bool $value
* @throws \Exception
* @throws \InvalidArgumentException
*/
public function setConfig(string $key, $value): void {
if ($this->user === null) {
throw new \Exception('No user logged in');
}
if (!in_array($key, $this->getAllowedConfigKeys())) {
throw new \InvalidArgumentException('Unknown config key');
}
if (!in_array($value, $this->getAllowedConfigValues($key))) {
throw new \InvalidArgumentException('Invalid config value');
}
if (is_bool($value)) {
$value = $value ? '1' : '0';
}
$this->config->setUserValue($this->user->getUID(), Application::APP_ID, $key, $value);
}
/**
* Get the current user configs array
*
* @return array
*/
public function getConfigs(): array {
if ($this->user === null) {
throw new \Exception('No user logged in');
}
$userId = $this->user->getUID();
$userConfigs = array_map(function(string $key) use ($userId) {
$value = $this->config->getUserValue($userId, Application::APP_ID, $key, $this->getDefaultConfigValue($key));
// If the default is expected to be a boolean, we need to cast the value
if (is_bool($this->getDefaultConfigValue($key))) {
return $value === '1';
}
return $value;
}, $this->getAllowedConfigKeys());
return array_combine($this->getAllowedConfigKeys(), $userConfigs);
}
}

@ -37,5 +37,3 @@ export default {
},
}
</script>
<style>
</style>

@ -1,57 +0,0 @@
/**
* @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Gary Kim <gary@garykim.dev>
* @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 Vue from 'vue'
import Settings from './services/Settings'
import SettingsView from './views/Settings'
import Setting from './models/Setting'
Vue.prototype.t = t
// Init Files App Settings Service
if (!window.OCA.Files) {
window.OCA.Files = {}
}
Object.assign(window.OCA.Files, { Settings: new Settings() })
Object.assign(window.OCA.Files.Settings, { Setting })
window.addEventListener('DOMContentLoaded', function() {
if (window.TESTING) {
return
}
// Init Vue app
// eslint-disable-next-line
new Vue({
el: '#files-app-settings',
render: h => h(SettingsView),
})
const appSettingsHeader = document.getElementById('app-settings-header')
if (appSettingsHeader) {
appSettingsHeader.addEventListener('click', e => {
const opened = e.currentTarget.children[0].classList.contains('opened')
OCA.Files.Settings.settings.forEach(e => opened ? e.close() : e.open())
})
}
})

@ -0,0 +1,55 @@
/**
* @copyright Copyright (c) 2022 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 { loadState } from '@nextcloud/initial-state'
import logger from '../logger.js'
/**
* Fetch and register the legacy files views
*/
export default function() {
const legacyViews = Object.values(loadState('files', 'navigation', {}))
if (legacyViews.length > 0) {
logger.debug('Legacy files views detected. Processing...', legacyViews)
legacyViews.forEach(view => {
registerLegacyView(view)
if (view.sublist) {
view.sublist.forEach(subview => registerLegacyView({ ...subview, parent: view.id }))
}
})
}
}
const registerLegacyView = function({ id, name, order, icon, parent, classes = '', expanded, params }) {
OCP.Files.Navigation.register({
id,
name,
order,
params,
parent,
expanded: expanded === true,
iconClass: icon ? `icon-${icon}` : 'nav-icon-' + id,
legacy: true,
sticky: classes.includes('pinned'),
})
}

@ -1,8 +1,7 @@
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
@ -20,20 +19,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import { getCurrentUser } from '@nextcloud/auth'
import { getLoggerBuilder } from '@nextcloud/logger'
const getLogger = user => {
if (user === null) {
return getLoggerBuilder()
.setApp('files')
.build()
}
return getLoggerBuilder()
.setApp('files')
.setUid(user.uid)
.build()
}
export default getLogger(getCurrentUser())
export default getLoggerBuilder()
.setApp('files')
.detectUser()
.build()

@ -1,3 +1,39 @@
import './files-app-settings'
import './templates'
import './legacy/filelistSearch'
import './templates.js'
import './legacy/filelistSearch.js'
import processLegacyFilesViews from './legacy/navigationMapper.js'
import Vue from 'vue'
import NavigationService from './services/Navigation.ts'
import NavigationView from './views/Navigation.vue'
import SettingsService from './services/Settings.js'
import SettingsModel from './models/Setting.js'
import router from './router/router.js'
// Init private and public Files namespace
window.OCA.Files = window.OCA.Files ?? {}
window.OCP.Files = window.OCP.Files ?? {}
// Init Navigation Service
const Navigation = new NavigationService()
Object.assign(window.OCP.Files, { Navigation })
// Init Files App Settings Service
const Settings = new SettingsService()
Object.assign(window.OCA.Files, { Settings })
Object.assign(window.OCA.Files.Settings, { Setting: SettingsModel })
// Init Navigation View
const View = Vue.extend(NavigationView)
const FilesNavigationRoot = new View({
name: 'FilesNavigationRoot',
propsData: {
Navigation,
},
router,
})
FilesNavigationRoot.$mount('#app-navigation-files')
// Init legacy files views
processLegacyFilesViews()

@ -0,0 +1,57 @@
/**
* @copyright Copyright (c) 2022 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 Vue from 'vue'
import Router from 'vue-router'
import { generateUrl } from '@nextcloud/router'
import { stringify } from 'query-string'
Vue.use(Router)
const router = new Router({
mode: 'history',
// if index.php is in the url AND we got this far, then it's working:
// let's keep using index.php in the url
base: generateUrl('/apps/files', ''),
linkActiveClass: 'active',
routes: [
{
path: '/',
// Pretending we're using the default view
alias: '/files',
},
{
path: '/:view/:fileid?',
name: 'filelist',
props: true,
},
],
// Custom stringifyQuery to prevent encoding of slashes in the url
stringifyQuery(query) {
const result = stringify(query).replace(/%2F/gmi, '/')
return result ? ('?' + result) : ''
},
})
export default router

@ -0,0 +1,217 @@
/**
* @copyright Copyright (c) 2022 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 Node from '@nextcloud/files/dist/files/node'
import isSvg from 'is-svg'
import logger from '../logger'
export interface Column {
/** Unique column ID */
id: string
/** Translated column title */
title: string
/** Property key from Node main or additional attributes.
Will be used if no custom sort function is provided.
Sorting will be done by localCompare */
property: string
/** Special function used to sort Nodes between them */
sortFunction?: (nodeA: Node, nodeB: Node) => number;
/** Custom summary of the column to display at the end of the list.
Will not be displayed if nothing is provided */
summary?: (node: Node[]) => string
}
export interface Navigation {
/** Unique view ID */
id: string
/** Translated view name */
name: string
/** Method return the content of the provided path */
getFiles: (path: string) => Node[]
/** The view icon as an inline svg */
icon: string
/** The view order */
order: number
/** This view column(s). Name and actions are
by default always included */
columns?: Column[]
/** The empty view element to render your empty content into */
emptyView?: (div: HTMLDivElement) => void
/** The parent unique ID */
parent?: string
/** This view is sticky (sent at the bottom) */
sticky?: boolean
/** This view has children and is expanded or not */
expanded?: boolean
/**
* This view is sticky a legacy view.
* Here until all the views are migrated to Vue.
* @deprecated It will be removed in a near future
*/
legacy?: boolean
/**
* An icon class.
* @deprecated It will be removed in a near future
*/
iconClass?: string
}
export default class {
private _views: Navigation[] = []
private _currentView: Navigation | null = null
constructor() {
logger.debug('Navigation service initialized')
}
register(view: Navigation) {
try {
isValidNavigation(view)
isUniqueNavigation(view, this._views)
} catch (e) {
if (e instanceof Error) {
logger.error(e.message, { view })
}
throw e
}
if (view.legacy) {
logger.warn('Legacy view detected, please migrate to Vue')
}
if (view.iconClass) {
view.legacy = true
}
this._views.push(view)
}
get views(): Navigation[] {
return this._views
}
setActive(view: Navigation | null) {
this._currentView = view
}
get active(): Navigation | null {
return this._currentView
}
}
/**
* Make sure the given view is unique
* and not already registered.
*/
const isUniqueNavigation = function(view: Navigation, views: Navigation[]): boolean {
if (views.find(search => search.id === view.id)) {
throw new Error(`Navigation id ${view.id} is already registered`)
}
return true
}
/**
* Typescript cannot validate an interface.
* Please keep in sync with the Navigation interface requirements.
*/
const isValidNavigation = function(view: Navigation): boolean {
if (!view.id || typeof view.id !== 'string') {
throw new Error('Navigation id is required and must be a string')
}
if (!view.name || typeof view.name !== 'string') {
throw new Error('Navigation name is required and must be a string')
}
/**
* Legacy handle their content and icon differently
* TODO: remove when support for legacy views is removed
*/
if (!view.legacy) {
if (!view.getFiles || typeof view.getFiles !== 'function') {
throw new Error('Navigation getFiles is required and must be a function')
}
if (!view.icon || typeof view.icon !== 'string' || !isSvg(view.icon)) {
throw new Error('Navigation icon is required and must be a valid svg string')
}
}
if (!('order' in view) || typeof view.order !== 'number') {
throw new Error('Navigation order is required and must be a number')
}
// Optional properties
if (view.columns) {
view.columns.forEach(isValidColumn)
}
if (view.emptyView && typeof view.emptyView !== 'function') {
throw new Error('Navigation emptyView must be a function')
}
if (view.parent && typeof view.parent !== 'string') {
throw new Error('Navigation parent must be a string')
}
if ('sticky' in view && typeof view.sticky !== 'boolean') {
throw new Error('Navigation sticky must be a boolean')
}
if ('expanded' in view && typeof view.expanded !== 'boolean') {
throw new Error('Navigation expanded must be a boolean')
}
return true
}
/**
* Typescript cannot validate an interface.
* Please keep in sync with the Column interface requirements.
*/
const isValidColumn = function(column: Column): boolean {
if (!column.id || typeof column.id !== 'string') {
throw new Error('Column id is required')
}
if (!column.title || typeof column.title !== 'string') {
throw new Error('Column title is required')
}
if (!column.property || typeof column.property !== 'string') {
throw new Error('Column property is required')
}
// Optional properties
if (column.sortFunction && typeof column.sortFunction !== 'function') {
throw new Error('Column sortFunction must be a function')
}
if (column.summary && typeof column.summary !== 'function') {
throw new Error('Column summary must be a function')
}
return true
}

@ -0,0 +1,116 @@
/* eslint-disable import/first */
import FolderSvg from '@mdi/svg/svg/folder.svg'
import ShareSvg from '@mdi/svg/svg/share-variant.svg'
import NavigationService from '../services/Navigation'
import NavigationView from './Navigation.vue'
import router from '../router/router.js'
const Navigation = new NavigationService()
describe('Navigation renders', () => {
it('renders', () => {
cy.mount(NavigationView, {
propsData: {
Navigation,
},
})
cy.get('[data-cy-files-navigation]').should('be.visible')
cy.get('[data-cy-files-navigation-settings-button]').should('be.visible')
})
})
describe('Navigation API', () => {
it('Check API entries rendering', () => {
Navigation.register({
id: 'files',
name: 'Files',
getFiles: () => [],
icon: FolderSvg,
order: 1,
})
cy.mount(NavigationView, {
propsData: {
Navigation,
},
router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
cy.get('[data-cy-files-navigation-item]').should('have.length', 1)
cy.get('[data-cy-files-navigation-item="files"]').should('be.visible')
cy.get('[data-cy-files-navigation-item="files"]').should('contain.text', 'Files')
})
it('Adds a new entry and render', () => {
Navigation.register({
id: 'sharing',
name: 'Sharing',
getFiles: () => [],
icon: ShareSvg,
order: 2,
})
cy.mount(NavigationView, {
propsData: {
Navigation,
},
router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
cy.get('[data-cy-files-navigation-item]').should('have.length', 2)
cy.get('[data-cy-files-navigation-item="sharing"]').should('be.visible')
cy.get('[data-cy-files-navigation-item="sharing"]').should('contain.text', 'Sharing')
})
it('Adds a new children, render and open menu', () => {
Navigation.register({
id: 'sharingin',
name: 'Shared with me',
getFiles: () => [],
parent: 'sharing',
icon: ShareSvg,
order: 1,
})
cy.mount(NavigationView, {
propsData: {
Navigation,
},
router,
})
cy.get('[data-cy-files-navigation]').should('be.visible')
cy.get('[data-cy-files-navigation-item]').should('have.length', 3)
// Intercept collapse preference request
cy.intercept('POST', '*/apps/files/api/v1/toggleShowFolder/*', {
statusCode: 200,
}).as('toggleShowFolder')
// Toggle the sharing entry children
cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').should('exist')
cy.get('[data-cy-files-navigation-item="sharing"] button.icon-collapse').click({ force: true })
cy.wait('@toggleShowFolder')
// Validate children
cy.get('[data-cy-files-navigation-item="sharingin"]').should('be.visible')
cy.get('[data-cy-files-navigation-item="sharingin"]').should('contain.text', 'Shared with me')
})
it('Throws when adding a duplicate entry', () => {
expect(() => {
Navigation.register({
id: 'files',
name: 'Files',
getFiles: () => [],
icon: FolderSvg,
order: 1,
})
}).to.throw('Navigation id files is already registered')
})
})

@ -0,0 +1,264 @@
<!--
- @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
-
- @author Gary Kim <gary@garykim.dev>
-
- @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/>.
-
-->
<template>
<NcAppNavigation data-cy-files-navigation>
<template #list>
<NcAppNavigationItem v-for="view in parentViews"
:key="view.id"
:allow-collapse="true"
:data-cy-files-navigation-item="view.id"
:icon="view.iconClass"
:open="view.expanded"
:pinned="view.sticky"
:title="view.name"
:to="generateToNavigation(view)"
@update:open="onToggleExpand(view)">
<NcAppNavigationItem v-for="child in childViews[view.id]"
:key="child.id"
:data-cy-files-navigation-item="child.id"
:exact="true"
:icon="child.iconClass"
:title="child.name"
:to="generateToNavigation(child)" />
</NcAppNavigationItem>
</template>
<!-- Settings toggle -->
<template #footer>
<ul class="app-navigation-entry__settings">
<NcAppNavigationItem :aria-label="t('files', 'Open the files app settings')"
:title="t('files', 'Files settings')"
data-cy-files-navigation-settings-button
@click.prevent.stop="openSettings">
<Cog slot="icon" :size="20" />
</NcAppNavigationItem>
</ul>
</template>
<!-- Settings modal-->
<SettingsModal :open="settingsOpened"
data-cy-files-navigation-settings
@close="onSettingsClose" />
</NcAppNavigation>
</template>
<script>
import { emit, subscribe } from '@nextcloud/event-bus'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import Cog from 'vue-material-design-icons/Cog.vue'
import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
import logger from '../logger.js'
import Navigation from '../services/Navigation.ts'
import SettingsModal from './Settings.vue'
import { translate } from '@nextcloud/l10n'
export default {
name: 'Navigation',
components: {
Cog,
NcAppNavigation,
NcAppNavigationItem,
SettingsModal,
},
props: {
// eslint-disable-next-line vue/prop-name-casing
Navigation: {
type: Navigation,
required: true,
},
},
data() {
return {
settingsOpened: false,
}
},
computed: {
currentViewId() {
return this.$route?.params?.view || 'files'
},
currentView() {
return this.views.find(view => view.id === this.currentViewId)
},
/** @return {Navigation[]} */
views() {
return this.Navigation.views
},
parentViews() {
return this.views
// filter child views
.filter(view => !view.parent)
// sort views by order
.sort((a, b) => {
return a.order - b.order
})
},
childViews() {
return this.views
// filter parent views
.filter(view => !!view.parent)
// create a map of parents and their children
.reduce((list, view) => {
list[view.parent] = [...(list[view.parent] || []), view]
// Sort children by order
list[view.parent].sort((a, b) => {
return a.order - b.order
})
return list
}, {})
},
},
watch: {
currentView(view, oldView) {
logger.debug('View changed', { id: view.id, view })
this.showView(view, oldView)
},
},
beforeMount() {
if (this.currentView) {
logger.debug('Navigation mounted. Showing requested view', { view: this.currentView })
this.showView(this.currentView)
}
subscribe('files:legacy-navigation:changed', this.onLegacyNavigationChanged)
},
methods: {
/**
* @param {Navigation} view the new active view
* @param {Navigation} oldView the old active view
*/
showView(view, oldView) {
// Closing any opened sidebar
window?.OCA?.Files?.Sidebar?.close?.()
if (view.legacy) {
const newAppContent = document.querySelector('#app-content #app-content-' + this.currentView.id + '.viewcontainer')
document.querySelectorAll('#app-content .viewcontainer').forEach(el => {
el.classList.add('hidden')
})
newAppContent.classList.remove('hidden')
// Triggering legacy navigation events
const { dir = '/' } = OC.Util.History.parseUrlQuery()
const params = { itemId: view.id, dir }
logger.debug('Triggering legacy navigation event', params)
window.jQuery(newAppContent).trigger(new window.jQuery.Event('show', params))
window.jQuery(newAppContent).trigger(new window.jQuery.Event('urlChanged', params))
}
this.Navigation.setActive(view)
emit('files:navigation:changed', view)
},
/**
* Coming from the legacy files app.
* TODO: remove when all views are migrated.
*
* @param {Navigation} view the new active view
*/
onLegacyNavigationChanged({ id } = { id: 'files' }) {
const view = this.Navigation.views.find(view => view.id === id)
if (view && view.legacy && view.id !== this.currentView.id) {
// Force update the current route as the request comes
// from the legacy files app router
this.$router.replace({ ...this.$route, params: { view: view.id } })
this.Navigation.setActive(view)
this.showView(view)
}
},
/**
* Expand/collapse a a view with children and permanently
* save this setting in the server.
*
* @param {Navigation} view the view to toggle
*/
onToggleExpand(view) {
// Invert state
view.expanded = !view.expanded
axios.post(generateUrl(`/apps/files/api/v1/toggleShowFolder/${view.id}`), { show: view.expanded })
},
/**
* Generate the route to a view
* @param {Navigation} view the view to toggle
*/
generateToNavigation(view) {
if (view.params) {
const { dir, fileid } = view.params
return { name: 'filelist', params: view.params, query: { dir, fileid } }
}
return { name: 'filelist', params: { view: view.id } }
},
/**
* Open the settings modal
*/
openSettings() {
this.settingsOpened = true
},
/**
* Close the settings modal
*/
onSettingsClose() {
this.settingsOpened = false
},
t: translate,
},
}
</script>
<style scoped lang="scss">
// TODO: remove when https://github.com/nextcloud/nextcloud-vue/pull/3539 is in
.app-navigation::v-deep .app-navigation-entry-icon {
background-repeat: no-repeat;
background-position: center;
}
.app-navigation > ul.app-navigation__list {
// Use flex gap value for more elegant spacing
padding-bottom: var(--default-grid-baseline, 4px);
}
.app-navigation-entry__settings {
height: auto !important;
overflow: hidden !important;
padding-top: 0 !important;
// Prevent shrinking or growing
flex: 0 0 auto;
}
</style>

@ -20,26 +20,150 @@
-
-->
<template>
<div id="files-app-extra-settings">
<template v-for="setting in settings">
<Setting :key="setting.name" :el="setting.el" />
</template>
</div>
<NcAppSettingsDialog :open="open"
:show-navigation="true"
:title="t('files', 'Files settings')"
@update:open="onClose">
<!-- Settings API-->
<NcAppSettingsSection id="settings" :title="t('files', 'Files settings')">
<NcCheckboxRadioSwitch :checked.sync="show_hidden"
@update:checked="setConfig('show_hidden', $event)">
{{ t('files', 'Show hidden files') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="crop_image_previews"
@update:checked="setConfig('crop_image_previews', $event)">
{{ t('files', 'Crop image previews') }}
</NcCheckboxRadioSwitch>
</NcAppSettingsSection>
<!-- Settings API-->
<NcAppSettingsSection v-if="settings.length !== 0"
id="more-settings"
:title="t('files', 'Additional settings')">
<template v-for="setting in settings">
<Setting :key="setting.name" :el="setting.el" />
</template>
</NcAppSettingsSection>
<!-- Webdav URL-->
<NcAppSettingsSection id="webdav" :title="t('files', 'Webdav')">
<NcInputField id="webdav-url-input"
:show-trailing-button="true"
:success="webdavUrlCopied"
:trailing-button-label="t('files', 'Copy to clipboard')"
:value="webdavUrl"
readonly="readonly"
type="url"
@focus="$event.target.select()"
@trailing-button-click="copyCloudId">
<template #trailing-button-icon>
<Clipboard :size="20" />
</template>
</NcInputField>
<em>
<a :href="webdavDocs" target="_blank" rel="noreferrer noopener">
{{ t('files', 'Use this address to access your Files via WebDAV') }}
</a>
</em>
</NcAppSettingsSection>
</NcAppSettingsDialog>
</template>
<script>
import Setting from '../components/Setting'
import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDialog.js'
import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js'
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import Clipboard from 'vue-material-design-icons/Clipboard.vue'
import NcInputField from '@nextcloud/vue/dist/Components/NcInputField'
import Setting from '../components/Setting.vue'
import { emit } from '@nextcloud/event-bus'
import { generateRemoteUrl, generateUrl } from '@nextcloud/router'
import { getCurrentUser } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import { showError, showSuccess } from '@nextcloud/dialogs'
import { translate } from '@nextcloud/l10n'
import axios from '@nextcloud/axios'
const userConfig = loadState('files', 'config', {
show_hidden: false,
crop_image_previews: true,
})
export default {
name: 'Settings',
components: {
Clipboard,
NcAppSettingsDialog,
NcAppSettingsSection,
NcCheckboxRadioSwitch,
NcInputField,
Setting,
},
props: {
open: {
type: Boolean,
default: false,
},
},
data() {
return {
settings: OCA.Files.Settings.settings,
...userConfig,
// Settings API
settings: window.OCA?.Files?.Settings?.settings || [],
// Webdav infos
webdavUrl: generateRemoteUrl('dav/files/' + encodeURIComponent(getCurrentUser()?.uid)),
webdavDocs: 'https://docs.nextcloud.com/server/stable/go.php?to=user-webdav',
webdavUrlCopied: false,
}
},
beforeMount() {
// Update the settings API entries state
this.settings.forEach(setting => setting.open())
},
beforeDestroy() {
// Update the settings API entries state
this.settings.forEach(setting => setting.close())
},
methods: {
onClose() {
this.$emit('close')
},
setConfig(key, value) {
emit('files:config:updated', { key, value })
axios.post(generateUrl('/apps/files/api/v1/config/' + key), {
value,
})
},
async copyCloudId() {
document.querySelector('input#webdav-url-input').select()
if (!navigator.clipboard) {
// Clipboard API not available
showError(t('files', 'Clipboard is not available'))
return
}
await navigator.clipboard.writeText(this.webdavUrl)
this.webdavUrlCopied = true
showSuccess(t('files', 'Webdav URL copied to clipboard'))
setTimeout(() => {
this.webdavUrlCopied = false
}, 5000)
},
t: translate,
},
}
</script>

@ -1,4 +1,5 @@
<div id="app-navigation" role="navigation">
<div id="app-navigation-files" role="navigation"></div>
<div class="hidden">
<ul class="with-icon" tabindex="0">
<?php

@ -8,6 +8,10 @@
<label id="view-toggle" for="showgridview" tabindex="0" class="button <?php p($_['showgridview'] ? 'icon-toggle-filelist' : 'icon-toggle-pictures') ?>"
title="<?php p($_['showgridview'] ? $l->t('Show list view') : $l->t('Show grid view'))?>"></label>
<!-- New files vue container -->
<div id="app-content-vue" class="hidden"></div>
<!-- Legacy views -->
<?php foreach ($_['appContents'] as $content) { ?>
<div id="app-content-<?php p($content['id']) ?>" class="hidden viewcontainer">
<?php print_unescaped($content['content']) ?>

@ -28,6 +28,7 @@
namespace OCA\Files\Controller;
use OCA\Files\Service\TagService;
use OCA\Files\Service\UserConfig;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Files\File;
@ -67,6 +68,8 @@ class ApiControllerTest extends TestCase {
private $config;
/** @var Folder|\PHPUnit\Framework\MockObject\MockObject */
private $userFolder;
/** @var UserConfig|\PHPUnit\Framework\MockObject\MockObject */
private $userConfig;
protected function setUp(): void {
parent::setUp();
@ -95,6 +98,7 @@ class ApiControllerTest extends TestCase {
$this->userFolder = $this->getMockBuilder(Folder::class)
->disableOriginalConstructor()
->getMock();
$this->userConfig = $this->createMock(UserConfig::class);
$this->apiController = new ApiController(
$this->appName,
@ -104,7 +108,8 @@ class ApiControllerTest extends TestCase {
$this->preview,
$this->shareManager,
$this->config,
$this->userFolder
$this->userFolder,
$this->userConfig
);
}

@ -34,6 +34,7 @@ namespace OCA\Files\Tests\Controller;
use OCA\Files\Activity\Helper;
use OCA\Files\Controller\ViewController;
use OCA\Files\Service\UserConfig;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Services\IInitialState;
@ -87,6 +88,8 @@ class ViewControllerTest extends TestCase {
private $templateManager;
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
private $shareManager;
/** @var UserConfig|\PHPUnit\Framework\MockObject\MockObject */
private $userConfig;
protected function setUp(): void {
parent::setUp();
@ -109,6 +112,7 @@ class ViewControllerTest extends TestCase {
$this->initialState = $this->createMock(IInitialState::class);
$this->templateManager = $this->createMock(ITemplateManager::class);
$this->shareManager = $this->createMock(IManager::class);
$this->userConfig = $this->createMock(UserConfig::class);
$this->viewController = $this->getMockBuilder('\OCA\Files\Controller\ViewController')
->setConstructorArgs([
'files',
@ -124,6 +128,7 @@ class ViewControllerTest extends TestCase {
$this->initialState,
$this->templateManager,
$this->shareManager,
$this->userConfig,
])
->setMethods([
'getStorageInfo',
@ -166,7 +171,6 @@ class ViewControllerTest extends TestCase {
$nav->assign('usage', '123 B');
$nav->assign('quota', 100);
$nav->assign('total_space', '100 B');
$nav->assign('webdav_url', 'http://localhost/remote.php/dav/files/testuser1/');
$nav->assign('navigationItems', [
'files' => [
'id' => 'files',
@ -178,6 +182,7 @@ class ViewControllerTest extends TestCase {
'icon' => '',
'type' => 'link',
'classes' => '',
'expanded' => false,
'unread' => 0,
],
'recent' => [
@ -190,6 +195,7 @@ class ViewControllerTest extends TestCase {
'icon' => '',
'type' => 'link',
'classes' => '',
'expanded' => false,
'unread' => 0,
],
'favorites' => [
@ -205,51 +211,50 @@ class ViewControllerTest extends TestCase {
'sublist' => [
[
'id' => '-test1',
'view' => 'files',
'href' => '',
'dir' => '/test1',
'order' => 6,
'folderPosition' => 1,
'name' => 'test1',
'icon' => 'files',
'quickaccesselement' => 'true',
'icon' => 'folder',
'params' => [
'view' => 'files',
'dir' => '/test1',
],
],
[
'name' => 'test2',
'id' => '-test2-',
'view' => 'files',
'href' => '',
'dir' => '/test2/',
'order' => 7,
'folderPosition' => 2,
'icon' => 'files',
'quickaccesselement' => 'true',
'icon' => 'folder',
'params' => [
'view' => 'files',
'dir' => '/test2/',
],
],
[
'name' => 'sub4',
'id' => '-test3-sub4',
'view' => 'files',
'href' => '',
'dir' => '/test3/sub4',
'order' => 8,
'folderPosition' => 3,
'icon' => 'files',
'quickaccesselement' => 'true',
'icon' => 'folder',
'params' => [
'view' => 'files',
'dir' => '/test3/sub4',
],
],
[
'name' => 'sub6',
'id' => '-test5-sub6-',
'view' => 'files',
'href' => '',
'dir' => '/test5/sub6/',
'order' => 9,
'folderPosition' => 4,
'icon' => 'files',
'quickaccesselement' => 'true',
'icon' => 'folder',
'params' => [
'view' => 'files',
'dir' => '/test5/sub6/',
],
],
],
'defaultExpandedState' => false,
'expandedState' => 'show_Quick_Access',
'expanded' => false,
'unread' => 0,
],
'systemtagsfilter' => [
@ -262,6 +267,7 @@ class ViewControllerTest extends TestCase {
'icon' => '',
'type' => 'link',
'classes' => '',
'expanded' => false,
'unread' => 0,
],
'trashbin' => [
@ -274,6 +280,7 @@ class ViewControllerTest extends TestCase {
'icon' => '',
'type' => 'link',
'classes' => 'pinned',
'expanded' => false,
'unread' => 0,
],
'shareoverview' => [
@ -323,8 +330,7 @@ class ViewControllerTest extends TestCase {
'active' => false,
'icon' => '',
'type' => 'link',
'expandedState' => 'show_sharing_menu',
'defaultExpandedState' => false,
'expanded' => false,
'unread' => 0,
]
]);
@ -407,7 +413,7 @@ class ViewControllerTest extends TestCase {
],
],
'hiddenFields' => [],
'showgridview' => false
'showgridview' => null
]
);
$policy = new Http\ContentSecurityPolicy();

@ -1,244 +0,0 @@
/**
* @copyright 2014 Vincent Petry <pvince81@owncloud.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Vincent Petry <vincent@nextcloud.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/>.
*
*/
describe('OCA.Files.App tests', function() {
var App = OCA.Files.App;
var pushStateStub;
var replaceStateStub;
var parseUrlQueryStub;
beforeEach(function() {
$('#testArea').append(
'<div id="content" class="app-files">' +
'<div id="app-navigation">' +
'<ul><li data-id="files"><a>Files</a></li>' +
'<li data-id="other"><a>Other</a></li>' +
'</div>' +
'<div id="app-content">' +
'<div id="app-content-files" class="hidden">' +
'</div>' +
'<div id="app-content-other" class="hidden">' +
'</div>' +
'</div>' +
'</div>' +
'</div>'
);
OCA.Files.fileActions = new OCA.Files.FileActions();
pushStateStub = sinon.stub(OC.Util.History, 'pushState');
replaceStateStub = sinon.stub(OC.Util.History, 'replaceState');
parseUrlQueryStub = sinon.stub(OC.Util.History, 'parseUrlQuery');
parseUrlQueryStub.returns({});
App.initialize();
});
afterEach(function() {
App.destroy();
pushStateStub.restore();
replaceStateStub.restore();
parseUrlQueryStub.restore();
});
describe('initialization', function() {
it('initializes the default file list with the default file actions', function() {
expect(App.fileList).toBeDefined();
expect(App.fileList.fileActions.actions.all).toBeDefined();
expect(App.fileList.$el.is('#app-content-files')).toEqual(true);
});
});
describe('URL handling', function() {
it('pushes the state to the URL when current app changed directory', function() {
$('#app-content-files').trigger(new $.Event('changeDirectory', {dir: 'sub dir'}));
expect(pushStateStub.calledOnce).toEqual(true);
var params = OC.parseQueryString(pushStateStub.getCall(0).args[0]);
expect(params.dir).toEqual('sub dir');
expect(params.view).not.toBeDefined();
$('li[data-id=other]>a').click();
pushStateStub.reset();
$('#app-content-other').trigger(new $.Event('changeDirectory', {dir: 'sub dir'}));
expect(pushStateStub.calledOnce).toEqual(true);
params = OC.parseQueryString(pushStateStub.getCall(0).args[0]);
expect(params.dir).toEqual('sub dir');
expect(params.view).toEqual('other');
});
it('replaces the state to the URL when fileid is known', function() {
$('#app-content-files').trigger(new $.Event('changeDirectory', {dir: 'sub dir'}));
expect(pushStateStub.calledOnce).toEqual(true);
var params = OC.parseQueryString(pushStateStub.getCall(0).args[0]);
expect(params.dir).toEqual('sub dir');
expect(params.view).not.toBeDefined();
expect(replaceStateStub.notCalled).toEqual(true);
parseUrlQueryStub.returns({dir: 'sub dir'});
$('#app-content-files').trigger(new $.Event('afterChangeDirectory', {dir: 'sub dir', fileId: 123}));
expect(pushStateStub.calledOnce).toEqual(true);
expect(replaceStateStub.calledOnce).toEqual(true);
params = OC.parseQueryString(replaceStateStub.getCall(0).args[0]);
expect(params.dir).toEqual('sub dir');
expect(params.view).not.toBeDefined();
expect(params.fileid).toEqual('123');
});
describe('onpopstate', function() {
it('sends "urlChanged" event to current app', function() {
var handler = sinon.stub();
$('#app-content-files').on('urlChanged', handler);
App._onPopState({view: 'files', dir: '/somedir'});
expect(handler.calledOnce).toEqual(true);
expect(handler.getCall(0).args[0].view).toEqual('files');
expect(handler.getCall(0).args[0].dir).toEqual('/somedir');
});
it('sends "show" event to current app and sets navigation', function() {
var showHandlerFiles = sinon.stub();
var showHandlerOther = sinon.stub();
var hideHandlerFiles = sinon.stub();
var hideHandlerOther = sinon.stub();
$('#app-content-files').on('show', showHandlerFiles);
$('#app-content-files').on('hide', hideHandlerFiles);
$('#app-content-other').on('show', showHandlerOther);
$('#app-content-other').on('hide', hideHandlerOther);
App._onPopState({view: 'other', dir: '/somedir'});
expect(showHandlerFiles.notCalled).toEqual(true);
expect(hideHandlerFiles.calledOnce).toEqual(true);
expect(showHandlerOther.calledOnce).toEqual(true);
expect(hideHandlerOther.notCalled).toEqual(true);
showHandlerFiles.reset();
showHandlerOther.reset();
hideHandlerFiles.reset();
hideHandlerOther.reset();
App._onPopState({view: 'files', dir: '/somedir'});
expect(showHandlerFiles.calledOnce).toEqual(true);
expect(hideHandlerFiles.notCalled).toEqual(true);
expect(showHandlerOther.notCalled).toEqual(true);
expect(hideHandlerOther.calledOnce).toEqual(true);
expect(App.navigation.getActiveItem()).toEqual('files');
expect($('#app-content-files').hasClass('hidden')).toEqual(false);
expect($('#app-content-other').hasClass('hidden')).toEqual(true);
});
it('does not send "show" or "hide" event to current app when already visible', function() {
var showHandler = sinon.stub();
var hideHandler = sinon.stub();
$('#app-content-files').on('show', showHandler);
$('#app-content-files').on('hide', hideHandler);
App._onPopState({view: 'files', dir: '/somedir'});
expect(showHandler.notCalled).toEqual(true);
expect(hideHandler.notCalled).toEqual(true);
});
it('state defaults to files app with root dir', function() {
var handler = sinon.stub();
parseUrlQueryStub.returns({});
$('#app-content-files').on('urlChanged', handler);
App._onPopState();
expect(handler.calledOnce).toEqual(true);
expect(handler.getCall(0).args[0].view).toEqual('files');
expect(handler.getCall(0).args[0].dir).toEqual('/');
});
it('activates files app if invalid view is passed', function() {
App._onPopState({view: 'invalid', dir: '/somedir'});
expect(App.navigation.getActiveItem()).toEqual('files');
expect($('#app-content-files').hasClass('hidden')).toEqual(false);
});
});
describe('navigation', function() {
it('switches the navigation item and panel visibility when onpopstate', function() {
App._onPopState({view: 'other', dir: '/somedir'});
expect(App.navigation.getActiveItem()).toEqual('other');
expect($('#app-content-files').hasClass('hidden')).toEqual(true);
expect($('#app-content-other').hasClass('hidden')).toEqual(false);
expect($('li[data-id=files] > a').hasClass('active')).toEqual(false);
expect($('li[data-id=other] > a').hasClass('active')).toEqual(true);
App._onPopState({view: 'files', dir: '/somedir'});
expect(App.navigation.getActiveItem()).toEqual('files');
expect($('#app-content-files').hasClass('hidden')).toEqual(false);
expect($('#app-content-other').hasClass('hidden')).toEqual(true);
expect($('li[data-id=files] > a').hasClass('active')).toEqual(true);
expect($('li[data-id=other] > a').hasClass('active')).toEqual(false);
});
it('clicking on navigation switches the panel visibility', function() {
$('li[data-id=other] > a').click();
expect(App.navigation.getActiveItem()).toEqual('other');
expect($('#app-content-files').hasClass('hidden')).toEqual(true);
expect($('#app-content-other').hasClass('hidden')).toEqual(false);
expect($('li[data-id=files] > a').hasClass('active')).toEqual(false);
expect($('li[data-id=other] > a').hasClass('active')).toEqual(true);
$('li[data-id=files] > a').click();
expect(App.navigation.getActiveItem()).toEqual('files');
expect($('#app-content-files').hasClass('hidden')).toEqual(false);
expect($('#app-content-other').hasClass('hidden')).toEqual(true);
expect($('li[data-id=files] > a').hasClass('active')).toEqual(true);
expect($('li[data-id=other] > a').hasClass('active')).toEqual(false);
});
it('clicking on navigation sends "show" and "urlChanged" event', function() {
var handler = sinon.stub();
var showHandler = sinon.stub();
$('#app-content-other').on('urlChanged', handler);
$('#app-content-other').on('show', showHandler);
$('li[data-id=other] > a').click();
expect(handler.calledOnce).toEqual(true);
expect(handler.getCall(0).args[0].view).toEqual('other');
expect(handler.getCall(0).args[0].dir).toEqual('/');
expect(showHandler.calledOnce).toEqual(true);
});
it('clicking on activate navigation only sends "urlChanged" event', function() {
var handler = sinon.stub();
var showHandler = sinon.stub();
$('#app-content-files').on('urlChanged', handler);
$('#app-content-files').on('show', showHandler);
$('li[data-id=files] > a').click();
expect(handler.calledOnce).toEqual(true);
expect(handler.getCall(0).args[0].view).toEqual('files');
expect(handler.getCall(0).args[0].dir).toEqual('/');
expect(showHandler.notCalled).toEqual(true);
});
});
describe('viewer mode', function() {
it('toggles the sidebar when viewer mode is enabled', function() {
$('#app-content-files').trigger(
new $.Event('changeViewerMode', {viewerModeEnabled: true}
));
expect($('#app-navigation').hasClass('hidden')).toEqual(true);
expect($('.app-files').hasClass('viewer-mode no-sidebar')).toEqual(true);
$('#app-content-files').trigger(
new $.Event('changeViewerMode', {viewerModeEnabled: false}
));
expect($('#app-navigation').hasClass('hidden')).toEqual(false);
expect($('.app-files').hasClass('viewer-mode no-sidebar')).toEqual(false);
});
});
});
});

@ -515,9 +515,9 @@ describe('OCA.Files.FileList tests', function() {
});
it('toggles the list\'s class when toggling hidden files', function() {
expect(fileList.$el.hasClass('hide-hidden-files')).toEqual(false);
filesConfig.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
expect(fileList.$el.hasClass('hide-hidden-files')).toEqual(true);
filesConfig.set('showhidden', true);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: true })
expect(fileList.$el.hasClass('hide-hidden-files')).toEqual(false);
});
});
@ -1371,7 +1371,7 @@ describe('OCA.Files.FileList tests', function() {
expect($('.files-fileList tr').length).toEqual(20);
});
it('renders the full first page despite hidden rows', function() {
filesConfig.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
var files = _.map(generateFiles(0, 23), function(data) {
return _.extend(data, {
name: '.' + data.name
@ -1385,7 +1385,7 @@ describe('OCA.Files.FileList tests', function() {
expect($('.files-fileList tr').length).toEqual(25);
});
it('renders the full first page despite hidden rows', function() {
filesConfig.set('showhidden', true);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: true });
var files = _.map(generateFiles(0, 23), function(data) {
return _.extend(data, {
name: '.' + data.name
@ -1817,18 +1817,6 @@ describe('OCA.Files.FileList tests', function() {
$('#app-content-files').trigger(new $.Event('urlChanged', {view: 'files', dir: '/somedir'}));
expect(fileList.getCurrentDirectory()).toEqual('/somedir');
});
it('reloads the list when leaving hidden state', function() {
var reloadStub = sinon.stub(fileList, 'reload');
// First show should not trigger
$('#app-content-files').trigger(new $.Event('show'));
expect(reloadStub.calledOnce).toEqual(false);
// Second show should!
$('#app-content-files').trigger(new $.Event('show'));
expect(reloadStub.calledOnce).toEqual(true);
reloadStub.restore();
});
it('refreshes breadcrumb after update', function() {
var setDirSpy = sinon.spy(fileList.breadcrumb, 'setDirectory');
fileList.changeDirectory('/anothersubdir');
@ -2014,7 +2002,7 @@ describe('OCA.Files.FileList tests', function() {
expect($('.select-all').prop('checked')).toEqual(false);
});
it('Selecting all files also selects hidden files when invisible', function() {
filesConfig.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
var $tr = fileList.add(new FileInfo({
name: '.hidden',
type: 'dir',
@ -2103,7 +2091,7 @@ describe('OCA.Files.FileList tests', function() {
expect($summary.text()).toEqual('Name');
});
it('Displays the number of hidden files in selection summary if hidden files are invisible', function() {
filesConfig.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
var $tr = fileList.add(new FileInfo({
name: '.hidden',
type: 'dir',
@ -2115,7 +2103,7 @@ describe('OCA.Files.FileList tests', function() {
expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)');
});
it('Does not displays the number of hidden files in selection summary if hidden files are visible', function() {
filesConfig.set('showhidden', true);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: true });
var $tr = fileList.add(new FileInfo({
name: '.hidden',
type: 'dir',
@ -2127,7 +2115,7 @@ describe('OCA.Files.FileList tests', function() {
expect($summary.text()).toEqual('2 folders and 3 files');
});
it('Toggling hidden file visibility updates selection summary', function() {
filesConfig.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
var $tr = fileList.add(new FileInfo({
name: '.hidden',
type: 'dir',
@ -2137,7 +2125,7 @@ describe('OCA.Files.FileList tests', function() {
$('.select-all').click();
var $summary = $('.column-name a.name>span:first');
expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)');
filesConfig.set('showhidden', true);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: true });
expect($summary.text()).toEqual('2 folders and 3 files');
});
it('Select/deselect files shows/hides file actions', function() {

@ -204,7 +204,8 @@ describe('OCA.Files.FileSummary tests', function() {
});
it('renders hidden count section when hidden files are hidden', function() {
config.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
summary.add({name: 'abc', type: 'file', size: 256000});
summary.add({name: 'def', type: 'dir', size: 100});
summary.add({name: '.hidden', type: 'dir', size: 512000});
@ -217,7 +218,8 @@ describe('OCA.Files.FileSummary tests', function() {
expect($container.find('.filesize').text()).toEqual('750 KB');
});
it('does not render hidden count section when hidden files exist but are visible', function() {
config.set('showhidden', true);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: true });
summary.add({name: 'abc', type: 'file', size: 256000});
summary.add({name: 'def', type: 'dir', size: 100});
summary.add({name: '.hidden', type: 'dir', size: 512000});
@ -229,7 +231,8 @@ describe('OCA.Files.FileSummary tests', function() {
expect($container.find('.filesize').text()).toEqual('750 KB');
});
it('does not render hidden count section when no hidden files exist', function() {
config.set('showhidden', false);
window._nc_event_bus.emit('files:config:updated', { key: 'show_hidden', value: false });
summary.add({name: 'abc', type: 'file', size: 256000});
summary.add({name: 'def', type: 'dir', size: 100});
summary.update();

@ -84,7 +84,7 @@
reload: function() {
this.showMask();
if (this._reloadCall) {
if (this._reloadCall?.abort) {
this._reloadCall.abort();
}

@ -179,7 +179,7 @@
reload: function() {
this.showMask()
if (this._reloadCall) {
if (this._reloadCall?.abort) {
this._reloadCall.abort()
}

@ -284,7 +284,6 @@ class Application extends App implements IBootstrap {
'name' => $l->t('Shares'),
'classes' => 'collapsible',
'sublist' => $sharingSublistArray,
'expandedState' => 'show_sharing_menu'
];
});
}

@ -292,7 +292,7 @@
this._selectionSummary.clear()
this.$el.find('.select-all').prop('checked', false)
this.showMask()
if (this._reloadCall) {
if (this._reloadCall?.abort) {
this._reloadCall.abort()
}
this._reloadCall = this.client.getFolderContents(

@ -4,6 +4,8 @@ module.exports = {
'@babel/plugin-proposal-class-properties',
],
presets: [
// https://babeljs.io/docs/en/babel-preset-typescript
'@babel/preset-typescript',
[
'@babel/preset-env',
{

@ -94,9 +94,8 @@ $expectedFiles = [
'vendor-bin',
'version.php',
'webpack.common.js',
'webpack.dev.js',
'webpack.config.js',
'webpack.modules.js',
'webpack.prod.js',
];
$actualFiles = [];

@ -1,5 +1,12 @@
/* eslint-disable node/no-unpublished-import */
import { applyChangesToNextcloud, configureNextcloud, preppingNextcloud, startNextcloud, stopNextcloud, waitOnNextcloud } from './cypress/dockerNode'
import {
applyChangesToNextcloud,
configureNextcloud,
startNextcloud,
stopNextcloud,
waitOnNextcloud,
} from './cypress/dockerNode'
import { defineConfig } from 'cypress'
import browserify from '@cypress/browserify-preprocessor'
@ -29,6 +36,7 @@ export default defineConfig({
failSilently: false,
type: 'actual',
},
screenshotsFolder: 'cypress/snapshots/actual',
trashAssetsBeforeRuns: true,
@ -82,4 +90,24 @@ export default defineConfig({
})
},
},
component: {
devServer: {
framework: 'vue',
bundler: 'webpack',
webpackConfig: async () => {
process.env.npm_package_name = 'NcCypress'
process.env.npm_package_version = '1.0.0'
process.env.NODE_ENV = 'development'
const config = require('@nextcloud/webpack-vue-config')
config.module.rules.push({
test: /\.svg$/,
type: 'asset/source',
})
return config
},
},
},
})

@ -117,10 +117,6 @@ export const configureNextcloud = async function() {
await runExec(container, ['php', 'occ', 'config:system:set', 'force_locale', '--value', 'en_US'], true)
await runExec(container, ['php', 'occ', 'config:system:set', 'enforce_theme', '--value', 'light'], true)
// Enable the app and give status
await runExec(container, ['php', 'occ', 'app:enable', '--force', 'viewer'], true)
// await runExec(container, ['php', 'occ', 'app:list'], true)
console.log('└─ Nextcloud is now ready to use 🎉')
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

@ -0,0 +1,35 @@
/**
* @copyright Copyright (c) 2022 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 { mount } from 'cypress/vue2'
type MountParams = Parameters<typeof mount>;
type OptionsParam = MountParams[1];
declare global {
namespace Cypress {
interface Chainable<Subject = any> {
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -52,13 +52,13 @@
*/
/*!
* Vue.js v2.7.10
* Vue.js v2.7.13
* (c) 2014-2022 Evan You
* Released under the MIT License.
*/
/*!
* Vue.js v2.7.13
* Vue.js v2.7.14
* (c) 2014-2022 Evan You
* Released under the MIT License.
*/
@ -306,21 +306,11 @@
* @license MIT
*/
/*!
* focus-trap 7.0.0
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
*/
/*!
* focus-trap 7.1.0
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
*/
/*!
* tabbable 6.0.0
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
*/
/*!
* tabbable 6.0.1
* @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
@ -354,6 +344,8 @@
/*! For license information please see NcAppNavigationSettings.js.LICENSE.txt */
/*! For license information please see NcAppSettingsDialog.js.LICENSE.txt */
/*! For license information please see NcAppSidebar.js.LICENSE.txt */
/*! For license information please see NcAvatar.js.LICENSE.txt */
@ -392,8 +384,6 @@
/*! For license information please see index.module.js.LICENSE.txt */
/*! For license information please see ncvuecomponents.js.LICENSE.txt */
/*! Hammer.JS - v2.0.7 - 2016-04-22
* http://hammerjs.github.io/
*
@ -423,6 +413,51 @@
*
*/
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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/>.
*
*/
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @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/>.
*
*/
/**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
*
@ -647,7 +682,7 @@
*/
/**
* @copyright Copyright (c) 2020 Georg Ehrke
* @copyright Copyright (c) 2020 Georg Ehrke <georg-nextcloud@ehrke.email>
*
* @author Georg Ehrke <georg-nextcloud@ehrke.email>
*
@ -659,7 +694,7 @@
* 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
* 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.
*
@ -669,9 +704,9 @@
*/
/**
* @copyright Copyright (c) 2020 Georg Ehrke <georg-nextcloud@ehrke.email>
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Georg Ehrke <georg-nextcloud@ehrke.email>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
@ -682,7 +717,7 @@
*
* 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
* 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
@ -691,9 +726,9 @@
*/
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2020 Raimund Schlüßler <raimund.schluessler@mailbox.org>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Raimund Schlüßler <raimund.schluessler@mailbox.org>
*
* @license GNU AGPL version 3 or any later version
*
@ -703,7 +738,7 @@
* 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
* 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.
*
@ -725,7 +760,7 @@
* 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
* 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.
*
@ -735,9 +770,9 @@
*/
/**
* @copyright Copyright (c) 2020 Raimund Schlüßler <raimund.schluessler@mailbox.org>
* @copyright Copyright (c) 2021 Christoph Wurst
*
* @author Raimund Schlüßler <raimund.schluessler@mailbox.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
@ -747,8 +782,8 @@
* 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
* 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
@ -757,11 +792,11 @@
*/
/**
* @copyright Copyright (c) 2021 Christoph Wurst
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
* @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
@ -769,7 +804,7 @@
* 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
* 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.
*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
{"version":3,"file":"core-files_fileinfo.js?v=d5c54f8e5b3834c089a0","mappings":";CA0BA,SAAUA,GAUT,IAAMC,EAAW,SAASC,GACzB,IAAMC,EAAOC,KACbC,EAAEC,KAAKJ,GAAM,SAASK,EAAOC,GACvBH,EAAEI,WAAWF,KACjBJ,EAAKK,GAAOD,EAEb,IAEIF,EAAEK,YAAYN,KAAKO,MACvBP,KAAKO,GAAKC,SAASV,EAAKS,GAAI,KAI7BP,KAAKS,KAAOX,EAAKW,MAAQ,GAEP,QAAdT,KAAKU,KACRV,KAAKW,SAAW,uBAEhBX,KAAKW,SAAWX,KAAKW,UAAY,2BAG7BX,KAAKU,OACa,yBAAlBV,KAAKW,SACRX,KAAKU,KAAO,MAEZV,KAAKU,KAAO,OAGd,EAKDb,EAASe,UAAY,CAMpBL,GAAI,KAOJM,KAAM,KAQNJ,KAAM,KAONE,SAAU,KASVG,KAAM,KAQNJ,KAAM,KAQNK,YAAa,KAObC,MAAO,KAOPC,KAAM,KASNC,UAAW,KAKXC,YAAY,EAKZC,iBAAkB,KAKlBC,gBAAiB,GAEjBC,qBAAsB,EAEtBC,YAAa,WACZ,IAAK,IAAMC,KAAKxB,KAAKqB,gBAAiB,CACrC,IAAMI,EAAOzB,KAAKqB,gBAAgBG,GAClC,GAAmB,gBAAfC,EAAKC,OAAwC,aAAbD,EAAKrB,IACxC,OAAOqB,EAAKE,OAEb,CAED,OAAO,CACP,GAGG/B,EAAGgC,QACPhC,EAAGgC,MAAQ,CAAC,GAEbhC,EAAGgC,MAAM/B,SAAWA,CAzJrB,EA0JGD","sources":["webpack:///nextcloud/core/src/files/fileinfo.js"],"sourcesContent":["/**\n * Copyright (c) 2015\n *\n * @author Julius Härtl <jus@bitgrid.net>\n * @author Robin Appelman <robin@icewind.nl>\n * @author Roeland Jago Douma <roeland@famdouma.nl>\n * @author Vincent Petry <vincent@nextcloud.com>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n/* eslint-disable */\n(function(OC) {\n\n\t/**\n\t * @class OC.Files.FileInfo\n\t * @classdesc File information\n\t *\n\t * @param {Object} data file data, see attributes for details\n\t *\n\t * @since 8.2\n\t */\n\tconst FileInfo = function(data) {\n\t\tconst self = this\n\t\t_.each(data, function(value, key) {\n\t\t\tif (!_.isFunction(value)) {\n\t\t\t\tself[key] = value\n\t\t\t}\n\t\t})\n\n\t\tif (!_.isUndefined(this.id)) {\n\t\t\tthis.id = parseInt(data.id, 10)\n\t\t}\n\n\t\t// TODO: normalize path\n\t\tthis.path = data.path || ''\n\n\t\tif (this.type === 'dir') {\n\t\t\tthis.mimetype = 'httpd/unix-directory'\n\t\t} else {\n\t\t\tthis.mimetype = this.mimetype || 'application/octet-stream'\n\t\t}\n\n\t\tif (!this.type) {\n\t\t\tif (this.mimetype === 'httpd/unix-directory') {\n\t\t\t\tthis.type = 'dir'\n\t\t\t} else {\n\t\t\t\tthis.type = 'file'\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @memberof OC.Files\n\t */\n\tFileInfo.prototype = {\n\t\t/**\n\t\t * File id\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tid: null,\n\n\t\t/**\n\t\t * File name\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tname: null,\n\n\t\t/**\n\t\t * Path leading to the file, without the file name,\n\t\t * and with a leading slash.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tpath: null,\n\n\t\t/**\n\t\t * Mime type\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tmimetype: null,\n\n\t\t/**\n\t\t * Icon URL.\n\t\t *\n\t\t * Can be used to override the mime type icon.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\ticon: null,\n\n\t\t/**\n\t\t * File type. 'file' for files, 'dir' for directories.\n\t\t *\n\t\t * @type String\n\t\t * @deprecated rely on mimetype instead\n\t\t */\n\t\ttype: null,\n\n\t\t/**\n\t\t * Permissions.\n\t\t *\n\t\t * @see OC#PERMISSION_ALL for permissions\n\t\t * @type int\n\t\t */\n\t\tpermissions: null,\n\n\t\t/**\n\t\t * Modification time\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tmtime: null,\n\n\t\t/**\n\t\t * Etag\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tetag: null,\n\n\t\t/**\n\t\t * Mount type.\n\t\t *\n\t\t * One of null, \"external-root\", \"shared\" or \"shared-root\"\n\t\t *\n\t\t * @type string\n\t\t */\n\t\tmountType: null,\n\n\t\t/**\n\t\t * @type boolean\n\t\t */\n\t\thasPreview: true,\n\n\t\t/**\n\t\t * @type int\n\t\t */\n\t\tsharePermissions: null,\n\n\t\t/**\n\t\t * @type Array\n\t\t */\n\t\tshareAttributes: [],\n\n\t\tquotaAvailableBytes: -1,\n\n\t\tcanDownload: function() {\n\t\t\tfor (const i in this.shareAttributes) {\n\t\t\t\tconst attr = this.shareAttributes[i]\n\t\t\t\tif (attr.scope === 'permissions' && attr.key === 'download') {\n\t\t\t\t\treturn attr.enabled\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t}\n\n\tif (!OC.Files) {\n\t\tOC.Files = {}\n\t}\n\tOC.Files.FileInfo = FileInfo\n})(OC)\n"],"names":["OC","FileInfo","data","self","this","_","each","value","key","isFunction","isUndefined","id","parseInt","path","type","mimetype","prototype","name","icon","permissions","mtime","etag","mountType","hasPreview","sharePermissions","shareAttributes","quotaAvailableBytes","canDownload","i","attr","scope","enabled","Files"],"sourceRoot":""}
{"version":3,"file":"core-files_fileinfo.js?v=d5c54f8e5b3834c089a0","mappings":";CA0BA,SAAUA,GAUT,IAAMC,EAAW,SAASC,GACzB,IAAMC,EAAOC,KACbC,EAAEC,KAAKJ,GAAM,SAASK,EAAOC,GACvBH,EAAEI,WAAWF,KACjBJ,EAAKK,GAAOD,EAEd,IAEKF,EAAEK,YAAYN,KAAKO,MACvBP,KAAKO,GAAKC,SAASV,EAAKS,GAAI,KAI7BP,KAAKS,KAAOX,EAAKW,MAAQ,GAEP,QAAdT,KAAKU,KACRV,KAAKW,SAAW,uBAEhBX,KAAKW,SAAWX,KAAKW,UAAY,2BAG7BX,KAAKU,OACa,yBAAlBV,KAAKW,SACRX,KAAKU,KAAO,MAEZV,KAAKU,KAAO,OAGf,EAKAb,EAASe,UAAY,CAMpBL,GAAI,KAOJM,KAAM,KAQNJ,KAAM,KAONE,SAAU,KASVG,KAAM,KAQNJ,KAAM,KAQNK,YAAa,KAObC,MAAO,KAOPC,KAAM,KASNC,UAAW,KAKXC,YAAY,EAKZC,iBAAkB,KAKlBC,gBAAiB,GAEjBC,qBAAsB,EAEtBC,YAAa,WACZ,IAAK,IAAMC,KAAKxB,KAAKqB,gBAAiB,CACrC,IAAMI,EAAOzB,KAAKqB,gBAAgBG,GAClC,GAAmB,gBAAfC,EAAKC,OAAwC,aAAbD,EAAKrB,IACxC,OAAOqB,EAAKE,OAEd,CAEA,OAAO,CACR,GAGI/B,EAAGgC,QACPhC,EAAGgC,MAAQ,CAAC,GAEbhC,EAAGgC,MAAM/B,SAAWA,CACpB,CA1JD,CA0JGD","sources":["webpack:///nextcloud/core/src/files/fileinfo.js"],"sourcesContent":["/**\n * Copyright (c) 2015\n *\n * @author Julius Härtl <jus@bitgrid.net>\n * @author Robin Appelman <robin@icewind.nl>\n * @author Roeland Jago Douma <roeland@famdouma.nl>\n * @author Vincent Petry <vincent@nextcloud.com>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n/* eslint-disable */\n(function(OC) {\n\n\t/**\n\t * @class OC.Files.FileInfo\n\t * @classdesc File information\n\t *\n\t * @param {Object} data file data, see attributes for details\n\t *\n\t * @since 8.2\n\t */\n\tconst FileInfo = function(data) {\n\t\tconst self = this\n\t\t_.each(data, function(value, key) {\n\t\t\tif (!_.isFunction(value)) {\n\t\t\t\tself[key] = value\n\t\t\t}\n\t\t})\n\n\t\tif (!_.isUndefined(this.id)) {\n\t\t\tthis.id = parseInt(data.id, 10)\n\t\t}\n\n\t\t// TODO: normalize path\n\t\tthis.path = data.path || ''\n\n\t\tif (this.type === 'dir') {\n\t\t\tthis.mimetype = 'httpd/unix-directory'\n\t\t} else {\n\t\t\tthis.mimetype = this.mimetype || 'application/octet-stream'\n\t\t}\n\n\t\tif (!this.type) {\n\t\t\tif (this.mimetype === 'httpd/unix-directory') {\n\t\t\t\tthis.type = 'dir'\n\t\t\t} else {\n\t\t\t\tthis.type = 'file'\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * @memberof OC.Files\n\t */\n\tFileInfo.prototype = {\n\t\t/**\n\t\t * File id\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tid: null,\n\n\t\t/**\n\t\t * File name\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tname: null,\n\n\t\t/**\n\t\t * Path leading to the file, without the file name,\n\t\t * and with a leading slash.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tpath: null,\n\n\t\t/**\n\t\t * Mime type\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tmimetype: null,\n\n\t\t/**\n\t\t * Icon URL.\n\t\t *\n\t\t * Can be used to override the mime type icon.\n\t\t *\n\t\t * @type String\n\t\t */\n\t\ticon: null,\n\n\t\t/**\n\t\t * File type. 'file' for files, 'dir' for directories.\n\t\t *\n\t\t * @type String\n\t\t * @deprecated rely on mimetype instead\n\t\t */\n\t\ttype: null,\n\n\t\t/**\n\t\t * Permissions.\n\t\t *\n\t\t * @see OC#PERMISSION_ALL for permissions\n\t\t * @type int\n\t\t */\n\t\tpermissions: null,\n\n\t\t/**\n\t\t * Modification time\n\t\t *\n\t\t * @type int\n\t\t */\n\t\tmtime: null,\n\n\t\t/**\n\t\t * Etag\n\t\t *\n\t\t * @type String\n\t\t */\n\t\tetag: null,\n\n\t\t/**\n\t\t * Mount type.\n\t\t *\n\t\t * One of null, \"external-root\", \"shared\" or \"shared-root\"\n\t\t *\n\t\t * @type string\n\t\t */\n\t\tmountType: null,\n\n\t\t/**\n\t\t * @type boolean\n\t\t */\n\t\thasPreview: true,\n\n\t\t/**\n\t\t * @type int\n\t\t */\n\t\tsharePermissions: null,\n\n\t\t/**\n\t\t * @type Array\n\t\t */\n\t\tshareAttributes: [],\n\n\t\tquotaAvailableBytes: -1,\n\n\t\tcanDownload: function() {\n\t\t\tfor (const i in this.shareAttributes) {\n\t\t\t\tconst attr = this.shareAttributes[i]\n\t\t\t\tif (attr.scope === 'permissions' && attr.key === 'download') {\n\t\t\t\t\treturn attr.enabled\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn true\n\t\t},\n\t}\n\n\tif (!OC.Files) {\n\t\tOC.Files = {}\n\t}\n\tOC.Files.FileInfo = FileInfo\n})(OC)\n"],"names":["OC","FileInfo","data","self","this","_","each","value","key","isFunction","isUndefined","id","parseInt","path","type","mimetype","prototype","name","icon","permissions","mtime","etag","mountType","hasPreview","sharePermissions","shareAttributes","quotaAvailableBytes","canDownload","i","attr","scope","enabled","Files"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-main.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1,3 +1,3 @@
/*! For license information please see core-unsupported-browser-redirect.js.LICENSE.txt */
!function(){var e,n={25714:function(e,n,r){"use strict";var o,t,i,u=r(79753),l=r(81655),c=r(31e3),a=r.n(c),d=r(77727),f=r.n(d),s=(0,l.ZI)({allowHigherVersions:!0,browsers:f()}),p=(a()(f()),(0,r(62556).getBuilder)("core").clearOnLogout().persist().build()),b=r(22200),h=r(17499),g=null===(o=(0,b.getCurrentUser)())?(0,h.IY)().setApp("core").build():(0,h.IY)().setApp("core").setUid(o.uid).build(),v=r(48764).Buffer,w=(0,u.generateUrl)("/unsupported"),O="true"===p.getItem("unsupported-browser-ignore");window.TESTING||null!==(t=OC)&&void 0!==t&&null!==(i=t.config)&&void 0!==i&&i.no_unsupported_browser_warning||function(){if(s.test(navigator.userAgent))g.debug("this browser is officially supported ! 🚀");else if(O)g.debug("this browser is NOT supported but has been manually overridden ! ⚠️");else if(-1===window.location.pathname.indexOf(w)){var e=window.location.href.replace(window.location.origin,""),n=v.from(e).toString("base64");history.pushState(null,null,"".concat(w,"?redirect_url=").concat(n)),window.location.reload()}}()},72950:function(){}},r={};function o(e){var t=r[e];if(void 0!==t)return t.exports;var i=r[e]={id:e,loaded:!1,exports:{}};return n[e].call(i.exports,i,i.exports,o),i.loaded=!0,i.exports}o.m=n,o.amdD=function(){throw new Error("define cannot be used indirect")},o.amdO={},e=[],o.O=function(n,r,t,i){if(!r){var u=1/0;for(d=0;d<e.length;d++){r=e[d][0],t=e[d][1],i=e[d][2];for(var l=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(o.O).every((function(e){return o.O[e](r[c])}))?r.splice(c--,1):(l=!1,i<u&&(u=i));if(l){e.splice(d--,1);var a=t();void 0!==a&&(n=a)}}return n}i=i||0;for(var d=e.length;d>0&&e[d-1][2]>i;d--)e[d]=e[d-1];e[d]=[r,t,i]},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,{a:n}),n},o.d=function(e,n){for(var r in n)o.o(n,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},o.j=8876,function(){o.b=document.baseURI||self.location.href;var e={8876:0};o.O.j=function(n){return 0===e[n]};var n=function(n,r){var t,i,u=r[0],l=r[1],c=r[2],a=0;if(u.some((function(n){return 0!==e[n]}))){for(t in l)o.o(l,t)&&(o.m[t]=l[t]);if(c)var d=c(o)}for(n&&n(r);a<u.length;a++)i=u[a],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(d)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(n.bind(null,0)),r.push=n.bind(null,r.push.bind(r))}(),o.nc=void 0;var t=o.O(void 0,[7874],(function(){return o(25714)}));t=o.O(t)}();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=22e284cc4a754bfd50a8
!function(){var e,n={25714:function(e,n,r){"use strict";var o,t,i,u=r(79753),l=r(81655),c=r(31e3),a=r.n(c),d=r(77727),f=r.n(d),s=(0,l.ZI)({allowHigherVersions:!0,browsers:f()}),p=(a()(f()),(0,r(62556).getBuilder)("core").clearOnLogout().persist().build()),b=r(45994),h=r(17499),v=null===(o=(0,b.ts)())?(0,h.IY)().setApp("core").build():(0,h.IY)().setApp("core").setUid(o.uid).build(),w=r(48764).Buffer,g=(0,u.generateUrl)("/unsupported"),O="true"===p.getItem("unsupported-browser-ignore");window.TESTING||null!==(t=OC)&&void 0!==t&&null!==(i=t.config)&&void 0!==i&&i.no_unsupported_browser_warning||function(){if(s.test(navigator.userAgent))v.debug("this browser is officially supported ! 🚀");else if(O)v.debug("this browser is NOT supported but has been manually overridden ! ⚠️");else if(-1===window.location.pathname.indexOf(g)){var e=window.location.href.replace(window.location.origin,""),n=w.from(e).toString("base64");history.pushState(null,null,"".concat(g,"?redirect_url=").concat(n)),window.location.reload()}}()},72950:function(){}},r={};function o(e){var t=r[e];if(void 0!==t)return t.exports;var i=r[e]={id:e,loaded:!1,exports:{}};return n[e].call(i.exports,i,i.exports,o),i.loaded=!0,i.exports}o.m=n,o.amdD=function(){throw new Error("define cannot be used indirect")},o.amdO={},e=[],o.O=function(n,r,t,i){if(!r){var u=1/0;for(d=0;d<e.length;d++){r=e[d][0],t=e[d][1],i=e[d][2];for(var l=!0,c=0;c<r.length;c++)(!1&i||u>=i)&&Object.keys(o.O).every((function(e){return o.O[e](r[c])}))?r.splice(c--,1):(l=!1,i<u&&(u=i));if(l){e.splice(d--,1);var a=t();void 0!==a&&(n=a)}}return n}i=i||0;for(var d=e.length;d>0&&e[d-1][2]>i;d--)e[d]=e[d-1];e[d]=[r,t,i]},o.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return o.d(n,{a:n}),n},o.d=function(e,n){for(var r in n)o.o(n,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:n[r]})},o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},o.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},o.j=8876,function(){o.b=document.baseURI||self.location.href;var e={8876:0};o.O.j=function(n){return 0===e[n]};var n=function(n,r){var t,i,u=r[0],l=r[1],c=r[2],a=0;if(u.some((function(n){return 0!==e[n]}))){for(t in l)o.o(l,t)&&(o.m[t]=l[t]);if(c)var d=c(o)}for(n&&n(r);a<u.length;a++)i=u[a],o.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return o.O(d)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(n.bind(null,0)),r.push=n.bind(null,r.push.bind(r))}(),o.nc=void 0;var t=o.O(void 0,[7874],(function(){return o(25714)}));t=o.O(t)}();
//# sourceMappingURL=core-unsupported-browser-redirect.js.map?v=0035fd90d8be1902df52

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -66,11 +66,10 @@
*/
/**
* @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author Gary Kim <gary@garykim.dev>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license AGPL-3.0-or-later
*
@ -90,10 +89,9 @@
*/
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
*
* @license AGPL-3.0-or-later
*
@ -113,7 +111,7 @@
*/
/**
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
{"version":3,"file":"files_sharing-collaboration.js?v=b736a508139452dd8b55","mappings":"AAwBoBA,KAAKC,GAAGC,cAE5BC,OAAOC,IAAIC,cAAcC,aAAa,OAAQ,CAC7CC,OAAQ,WACP,OAAO,IAAIC,SAAQ,SAACC,EAASC,GAC5BT,GAAGU,QAAQC,WAAWC,EAAE,gBAAiB,mBAAmB,SAASC,GACrDb,GAAGc,MAAMC,YACjBC,YAAYH,GAAGI,MAAK,SAACC,EAAQC,GACnCX,EAAQW,EAASC,GACjB,IAAEC,MAAK,WACPZ,EAAO,IAAIa,MAAM,uBACjB,GACD,IAAE,EAAO,MAAM,EAAOtB,GAAGU,QAAQa,uBAAwB,GAAI,CAAEC,uBAAuB,GACvF,GACD,EACDC,WAAYb,EAAE,gBAAiB,kBAC/Bc,cAAe","sources":["webpack:///nextcloud/apps/files_sharing/src/collaborationresourceshandler.js"],"sourcesContent":["/**\n * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>\n *\n * @author John Molakvoæ <skjnldsv@protonmail.com>\n * @author Julius Härtl <jus@bitgrid.net>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n// eslint-disable-next-line camelcase\n__webpack_nonce__ = btoa(OC.requestToken)\n\nwindow.OCP.Collaboration.registerType('file', {\n\taction: () => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tOC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function(f) {\n\t\t\t\tconst client = OC.Files.getClient()\n\t\t\t\tclient.getFileInfo(f).then((status, fileInfo) => {\n\t\t\t\t\tresolve(fileInfo.id)\n\t\t\t\t}).fail(() => {\n\t\t\t\t\treject(new Error('Cannot get fileinfo'))\n\t\t\t\t})\n\t\t\t}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true })\n\t\t})\n\t},\n\ttypeString: t('files_sharing', 'Link to a file'),\n\ttypeIconClass: 'icon-files-dark',\n})\n"],"names":["btoa","OC","requestToken","window","OCP","Collaboration","registerType","action","Promise","resolve","reject","dialogs","filepicker","t","f","Files","getClient","getFileInfo","then","status","fileInfo","id","fail","Error","FILEPICKER_TYPE_CHOOSE","allowDirectoryChooser","typeString","typeIconClass"],"sourceRoot":""}
{"version":3,"file":"files_sharing-collaboration.js?v=b736a508139452dd8b55","mappings":"AAwBoBA,KAAKC,GAAGC,cAE5BC,OAAOC,IAAIC,cAAcC,aAAa,OAAQ,CAC7CC,OAAQ,WACP,OAAO,IAAIC,SAAQ,SAACC,EAASC,GAC5BT,GAAGU,QAAQC,WAAWC,EAAE,gBAAiB,mBAAmB,SAASC,GACrDb,GAAGc,MAAMC,YACjBC,YAAYH,GAAGI,MAAK,SAACC,EAAQC,GACnCX,EAAQW,EAASC,GAClB,IAAGC,MAAK,WACPZ,EAAO,IAAIa,MAAM,uBAClB,GACD,IAAG,EAAO,MAAM,EAAOtB,GAAGU,QAAQa,uBAAwB,GAAI,CAAEC,uBAAuB,GACxF,GACD,EACAC,WAAYb,EAAE,gBAAiB,kBAC/Bc,cAAe","sources":["webpack:///nextcloud/apps/files_sharing/src/collaborationresourceshandler.js"],"sourcesContent":["/**\n * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com>\n *\n * @author John Molakvoæ <skjnldsv@protonmail.com>\n * @author Julius Härtl <jus@bitgrid.net>\n *\n * @license AGPL-3.0-or-later\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Affero General Public License as\n * published by the Free Software Foundation, either version 3 of the\n * License, or (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU Affero General Public License for more details.\n *\n * You should have received a copy of the GNU Affero General Public License\n * along with this program. If not, see <http://www.gnu.org/licenses/>.\n *\n */\n\n// eslint-disable-next-line camelcase\n__webpack_nonce__ = btoa(OC.requestToken)\n\nwindow.OCP.Collaboration.registerType('file', {\n\taction: () => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tOC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function(f) {\n\t\t\t\tconst client = OC.Files.getClient()\n\t\t\t\tclient.getFileInfo(f).then((status, fileInfo) => {\n\t\t\t\t\tresolve(fileInfo.id)\n\t\t\t\t}).fail(() => {\n\t\t\t\t\treject(new Error('Cannot get fileinfo'))\n\t\t\t\t})\n\t\t\t}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true })\n\t\t})\n\t},\n\ttypeString: t('files_sharing', 'Link to a file'),\n\ttypeIconClass: 'icon-files-dark',\n})\n"],"names":["btoa","OC","requestToken","window","OCP","Collaboration","registerType","action","Promise","resolve","reject","dialogs","filepicker","t","f","Files","getClient","getFileInfo","then","status","fileInfo","id","fail","Error","FILEPICKER_TYPE_CHOOSE","allowDirectoryChooser","typeString","typeIconClass"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save