diff options
Diffstat (limited to 'apps/files')
52 files changed, 2997 insertions, 756 deletions
diff --git a/apps/files/admin.php b/apps/files/admin.php index 70f537d0db9..349c27ff742 100644 --- a/apps/files/admin.php +++ b/apps/files/admin.php @@ -42,9 +42,10 @@ if($_POST && OC_Util::isCallRegistered()) { } $htaccessWritable=is_writable(OC::$SERVERROOT.'/.htaccess'); +$userIniWritable=is_writable(OC::$SERVERROOT.'/.user.ini'); $tmpl = new OCP\Template( 'files', 'admin' ); -$tmpl->assign( 'uploadChangable', $htaccessWorking and $htaccessWritable ); +$tmpl->assign( 'uploadChangable', ($htaccessWorking and $htaccessWritable) or $userIniWritable ); $tmpl->assign( 'uploadMaxFilesize', $maxUploadFilesize); // max possible makes only sense on a 32 bit system $tmpl->assign( 'displayMaxPossibleUploadSize', PHP_INT_SIZE===4); diff --git a/apps/files/ajax/download.php b/apps/files/ajax/download.php index e67635ab853..26bab8837b4 100644 --- a/apps/files/ajax/download.php +++ b/apps/files/ajax/download.php @@ -39,4 +39,15 @@ if (!is_array($files_list)) { $files_list = array($files); } +/** + * this sets a cookie to be able to recognize the start of the download + * the content must not be longer than 32 characters and must only contain + * alphanumeric characters + */ +if(isset($_GET['downloadStartSecret']) + && !isset($_GET['downloadStartSecret'][32]) + && preg_match('!^[a-zA-Z0-9]+$!', $_GET['downloadStartSecret']) === 1) { + setcookie('ocDownloadStarted', $_GET['downloadStartSecret'], time() + 20, '/'); +} + OC_Files::get($dir, $files_list, $_SERVER['REQUEST_METHOD'] == 'HEAD'); diff --git a/apps/files/ajax/mimeicon.php b/apps/files/ajax/mimeicon.php deleted file mode 100644 index 008ad41953e..00000000000 --- a/apps/files/ajax/mimeicon.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Robin Appelman <icewind@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ -\OC::$server->getSession()->close(); - -$mime = isset($_GET['mime']) ? (string)$_GET['mime'] : ''; - -print OC_Helper::mimetypeIcon($mime); diff --git a/apps/files/appinfo/application.php b/apps/files/appinfo/application.php index c8aaf375d96..6ba77e09556 100644 --- a/apps/files/appinfo/application.php +++ b/apps/files/appinfo/application.php @@ -1,6 +1,5 @@ <?php /** - * @author Morris Jobke <hey@morrisjobke.de> * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tobias Kaminsky <tobias@kaminsky.me> * @author Vincent Petry <pvince81@owncloud.com> @@ -21,8 +20,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ - -namespace OCA\Files\Appinfo; +namespace OCA\Files\AppInfo; use OCA\Files\Controller\ApiController; use OCP\AppFramework\App; @@ -68,5 +66,10 @@ class Application extends App { $homeFolder ); }); + + /* + * Register capabilities + */ + $container->registerCapability('OCA\Files\Capabilities'); } } diff --git a/apps/files/appinfo/remote.php b/apps/files/appinfo/remote.php index fff3332ef49..36479ae13d0 100644 --- a/apps/files/appinfo/remote.php +++ b/apps/files/appinfo/remote.php @@ -33,51 +33,23 @@ set_time_limit(0); // Turn off output buffering to prevent memory problems \OC_Util::obEnd(); +$serverFactory = new \OC\Connector\Sabre\ServerFactory( + \OC::$server->getConfig(), + \OC::$server->getLogger(), + \OC::$server->getDatabaseConnection(), + \OC::$server->getUserSession(), + \OC::$server->getMountManager(), + \OC::$server->getTagManager() +); + // Backends $authBackend = new \OC\Connector\Sabre\Auth(); +$requestUri = \OC::$server->getRequest()->getRequestUri(); -// Fire up server -$objectTree = new \OC\Connector\Sabre\ObjectTree(); -$server = new \OC\Connector\Sabre\Server($objectTree); -// Set URL explicitly due to reverse-proxy situations -$server->httpRequest->setUrl(\OC::$server->getRequest()->getRequestUri()); -$server->setBaseUri($baseuri); - -// Load plugins -$defaults = new OC_Defaults(); -$server->addPlugin(new \OC\Connector\Sabre\BlockLegacyClientPlugin(\OC::$server->getConfig())); -$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, $defaults->getName())); -// FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / -$server->addPlugin(new \OC\Connector\Sabre\DummyGetResponsePlugin()); -$server->addPlugin(new \OC\Connector\Sabre\FilesPlugin($objectTree)); -$server->addPlugin(new \OC\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig())); -$server->addPlugin(new \OC\Connector\Sabre\ExceptionLoggerPlugin('webdav', \OC::$server->getLogger())); - -// wait with registering these until auth is handled and the filesystem is setup -$server->on('beforeMethod', function () use ($server, $objectTree) { - $view = \OC\Files\Filesystem::getView(); - $rootInfo = $view->getFileInfo(''); - - // Create ownCloud Dir - $mountManager = \OC\Files\Filesystem::getMountManager(); - $rootDir = new \OC\Connector\Sabre\Directory($view, $rootInfo); - $objectTree->init($rootDir, $view, $mountManager); - - $server->addPlugin(new \OC\Connector\Sabre\TagsPlugin($objectTree, \OC::$server->getTagManager())); - $server->addPlugin(new \OC\Connector\Sabre\QuotaPlugin($view)); - - // custom properties plugin must be the last one - $server->addPlugin( - new \Sabre\DAV\PropertyStorage\Plugin( - new \OC\Connector\Sabre\CustomPropertiesBackend( - $objectTree, - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserSession()->getUser() - ) - ) - ); - $server->addPlugin(new \OC\Connector\Sabre\CopyEtagHeaderPlugin()); -}, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request +$server = $serverFactory->createServer($baseuri, $requestUri, $authBackend, function() { + // use the view for the logged in user + return \OC\Files\Filesystem::getView(); +}); // And off we go! $server->exec(); diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php index ceeb3cdcc8a..d1b8954d5ce 100644 --- a/apps/files/appinfo/routes.php +++ b/apps/files/appinfo/routes.php @@ -1,9 +1,9 @@ <?php /** * @author Bart Visscher <bartv@thisnet.nl> - * @author Joas Schilling <nickvergessen@owncloud.com> * @author Lukas Reschke <lukas@owncloud.com> * @author Morris Jobke <hey@morrisjobke.de> + * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tobias Kaminsky <tobias@kaminsky.me> * @author Tom Needham <tom@owncloud.com> * @author Vincent Petry <pvince81@owncloud.com> @@ -24,8 +24,7 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ - -namespace OCA\Files\Appinfo; +namespace OCA\Files\AppInfo; $application = new Application(); $application->registerRoutes( @@ -66,8 +65,6 @@ $this->create('files_ajax_getstoragestats', 'ajax/getstoragestats.php') ->actionInclude('files/ajax/getstoragestats.php'); $this->create('files_ajax_list', 'ajax/list.php') ->actionInclude('files/ajax/list.php'); -$this->create('files_ajax_mimeicon', 'ajax/mimeicon.php') - ->actionInclude('files/ajax/mimeicon.php'); $this->create('files_ajax_move', 'ajax/move.php') ->actionInclude('files/ajax/move.php'); $this->create('files_ajax_newfile', 'ajax/newfile.php') @@ -84,6 +81,4 @@ $this->create('files_ajax_upload', 'ajax/upload.php') $this->create('download', 'download{file}') ->requirements(array('file' => '.*')) ->actionInclude('files/download.php'); - -// Register with the capabilities API -\OCP\API::register('get', '/cloud/capabilities', array('OCA\Files\Capabilities', 'getCapabilities'), 'files', \OCP\API::USER_AUTH); + diff --git a/apps/files/appinfo/update.php b/apps/files/appinfo/update.php new file mode 100644 index 00000000000..2691c05c348 --- /dev/null +++ b/apps/files/appinfo/update.php @@ -0,0 +1,96 @@ +<?php +/** + * @author Björn Schießle <schiessle@owncloud.com> + * @author Joas Schilling <nickvergessen@owncloud.com> + * + * @copyright Copyright (c) 2015, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +$installedVersion = \OC::$server->getConfig()->getAppValue('files', 'installed_version'); +$ocVersion = explode('.', \OC::$server->getSystemConfig()->getValue('version')); + +/** + * In case encryption was not enabled, we accidently set encrypted = 1 for + * files inside mount points, since 8.1.0. This breaks opening the files in + * 8.1.1 because we fixed the code that checks if a file is encrypted. + * In order to fix the file, we need to reset the flag of the file. However, + * the flag might be set because the file is in fact encrypted because it was + * uploaded at a time where encryption was enabled. + * + * So we can only do this when: + * - Current version of ownCloud before the update is 8.1.0 or 8.2.0.(0-2) + * - Encryption is disabled + * - files_encryption is not known in the app config + * + * If the first two are not the case, we are save. However, if files_encryption + * values exist in the config, we might have a false negative here. + * Now if there is no file with unencrypted size greater 0, that means there are + * no files that are still encrypted with "files_encryption" encryption. So we + * can also safely reset the flag here. + * + * If this is not the case, we go with "better save then sorry" and don't change + * the flag but write a message to the ownCloud log file. + */ + +/** + * @param \OCP\IDBConnection $conn + */ +function owncloud_reset_encrypted_flag(\OCP\IDBConnection $conn) { + $conn->executeUpdate('UPDATE `*PREFIX*filecache` SET `encrypted` = 0 WHERE `encrypted` = 1'); +} + +// Current version of ownCloud before the update is 8.1.0 or 8.2.0.(0-2) +if ($installedVersion === '1.1.9' && ( + // 8.1.0.x + (((int) $ocVersion[0]) === 8 && ((int) $ocVersion[1]) === 1 && ((int) $ocVersion[2]) === 0) + || + // < 8.2.0.3 + (((int) $ocVersion[0]) === 8 && ((int) $ocVersion[1]) === 2 && ((int) $ocVersion[2]) === 0 && ((int) $ocVersion[3]) < 3) + )) { + + // Encryption is not enabled + if (!\OC::$server->getEncryptionManager()->isEnabled()) { + $conn = \OC::$server->getDatabaseConnection(); + + // Old encryption is not known in app config + $oldEncryption = \OC::$server->getConfig()->getAppKeys('files_encryption'); + if (empty($oldEncryption)) { + owncloud_reset_encrypted_flag($conn); + } else { + $query = $conn->prepare('SELECT * FROM `*PREFIX*filecache` WHERE `encrypted` = 1 AND `unencrypted_size` > 0', 1); + $query->execute(); + $empty = $query->fetch(); + + if (empty($empty)) { + owncloud_reset_encrypted_flag($conn); + } else { + /** + * Sorry in case you are a false positive, but we are not 100% that + * you don't have any encrypted files anymore, so we can not reset + * the value safely + */ + \OC::$server->getLogger()->warning( + 'If you have a problem with files not being accessible and ' + . 'you are not using encryption, please have a look at the following' + . 'issue: {issue}', + [ + 'issue' => 'https://github.com/owncloud/core/issues/17846', + ] + ); + } + } + } +} diff --git a/apps/files/appinfo/version b/apps/files/appinfo/version index 512a1faa680..5ed5faa5f16 100644 --- a/apps/files/appinfo/version +++ b/apps/files/appinfo/version @@ -1 +1 @@ -1.1.9 +1.1.10 diff --git a/apps/files/command/scan.php b/apps/files/command/scan.php index 599dc603c7b..99ce64e09cc 100644 --- a/apps/files/command/scan.php +++ b/apps/files/command/scan.php @@ -92,10 +92,10 @@ class Scan extends Command { } protected function execute(InputInterface $input, OutputInterface $output) { - $path = $input->getOption('path'); - if ($path) { - $path = '/'.trim($path, '/'); - list (, $user, ) = explode('/', $path, 3); + $inputPath = $input->getOption('path'); + if ($inputPath) { + $inputPath = '/' . trim($inputPath, '/'); + list (, $user,) = explode('/', $inputPath, 3); $users = array($user); } else if ($input->getOption('all')) { $users = $this->userManager->search(''); @@ -114,6 +114,7 @@ class Scan extends Command { if (is_object($user)) { $user = $user->getUID(); } + $path = $inputPath ? $inputPath : '/' . $user; if ($this->userManager->userExists($user)) { $this->scanFiles($user, $path, $quiet, $output); } else { diff --git a/apps/files/controller/apicontroller.php b/apps/files/controller/apicontroller.php index 0cc222d7ce9..494d97db7a6 100644 --- a/apps/files/controller/apicontroller.php +++ b/apps/files/controller/apicontroller.php @@ -78,7 +78,7 @@ class ApiController extends Controller { return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST); } - $preview = $this->previewManager->createPreview('files/'.urldecode($file), $x, $y, true); + $preview = $this->previewManager->createPreview('files/'.$file, $x, $y, true); if ($preview->valid()) { return new DataDisplayResponse($preview->data(), Http::STATUS_OK, ['Content-Type' => 'image/png']); } else { diff --git a/apps/files/css/detailsView.css b/apps/files/css/detailsView.css new file mode 100644 index 00000000000..76629cb790f --- /dev/null +++ b/apps/files/css/detailsView.css @@ -0,0 +1,55 @@ +#app-sidebar .detailFileInfoContainer { + min-height: 50px; + padding: 15px; +} + +#app-sidebar .detailFileInfoContainer > div { + clear: both; +} + +#app-sidebar .mainFileInfoView { + margin-right: 20px; /* accomodate for close icon */ +} + +#app-sidebar .thumbnail { + width: 50px; + height: 50px; + float: left; + margin-right: 10px; + background-size: 50px; +} + +#app-sidebar .ellipsis { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +#app-sidebar .fileName { + font-size: 16px; + padding-top: 3px; +} + +#app-sidebar .file-details { + margin-top: 3px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; +} +#app-sidebar .action-favorite { + vertical-align: text-bottom; + padding: 10px; + margin: -10px; +} + +#app-sidebar .detailList { + float: left; +} + +#app-sidebar .close { + position: absolute; + top: 0; + right: 0; + padding: 15px; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + opacity: .5; +} diff --git a/apps/files/css/files.css b/apps/files/css/files.css index e4bf791761d..d66eece94d9 100644 --- a/apps/files/css/files.css +++ b/apps/files/css/files.css @@ -103,6 +103,10 @@ min-height: 100%; } +.app-files #app-content { + overflow-x: hidden; +} + /* icons for sidebar */ .nav-icon-files { background-image: url('../img/folder.svg'); @@ -143,6 +147,7 @@ #filestable tbody tr:active { background-color: rgb(240,240,240); } +#filestable tbody tr.highlighted, #filestable tbody tr.selected { background-color: rgb(230,230,230); } @@ -244,8 +249,8 @@ table th.column-last, table td.column-last { box-sizing: border-box; position: relative; /* this can not be just width, both need to be set … table styling */ - min-width: 176px; - max-width: 176px; + min-width: 130px; + max-width: 130px; } /* Multiselect bar */ @@ -270,6 +275,11 @@ table.multiselect thead th { font-weight: bold; border-bottom: 0; } + +#app-content.with-app-sidebar table.multiselect thead{ + margin-right: 27%; +} + table.multiselect #headerName { position: relative; width: 9999px; /* when we use 100%, the styling breaks on mobile … table styling */ @@ -321,14 +331,7 @@ table td.filename .nametext, .uploadtext, .modified, .column-last>span:first-chi position: relative; overflow: hidden; text-overflow: ellipsis; - width: 90%; -} -/* ellipsize long modified dates to make room for showing delete button */ -#fileList tr:hover .modified, -#fileList tr:focus .modified, -#fileList tr:hover .column-last>span:first-child, -#fileList tr:focus .column-last>span:first-child { - width: 75%; + width: 110px; } /* TODO fix usability bug (accidental file/folder selection) */ @@ -367,45 +370,27 @@ table td.filename .nametext .innernametext { @media only screen and (min-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 760px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 480px; + max-width: 660px; } } - @media only screen and (min-width: 1200px) and (max-width: 1366px) { table td.filename .nametext .innernametext { - max-width: 600px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 320px; + max-width: 500px; } } - -@media only screen and (min-width: 1000px) and (max-width: 1200px) { +@media only screen and (min-width: 1100px) and (max-width: 1200px) { table td.filename .nametext .innernametext { max-width: 400px; } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 120px; +} +@media only screen and (min-width: 1000px) and (max-width: 1100px) { + table td.filename .nametext .innernametext { + max-width: 310px; } } - @media only screen and (min-width: 768px) and (max-width: 1000px) { table td.filename .nametext .innernametext { - max-width: 320px; - } - - table tr:hover td.filename .nametext .innernametext, - table tr:focus td.filename .nametext .innernametext { - max-width: 40px; + max-width: 240px; } } @@ -512,6 +497,23 @@ table td.filename .uploadtext { font-size: 11px; } +.busy .fileactions, .busy .action { + visibility: hidden; +} + +/* fix position of bubble pointer for Files app */ +.bubble, +#app-navigation .app-navigation-entry-menu { + border-top-right-radius: 3px; +} +.bubble:after, +#app-navigation .app-navigation-entry-menu:after { + right: 6px; +} +.bubble:before, +#app-navigation .app-navigation-entry-menu:before { + right: 6px; +} /* force show the loading icon, not only on hover */ #fileList .icon-loading-small { @@ -522,21 +524,15 @@ table td.filename .uploadtext { } #fileList img.move2trash { display:inline; margin:-8px 0; padding:16px 8px 16px 8px !important; float:right; } -#fileList a.action.delete { - position: absolute; - right: 15px; - padding: 17px 14px; -} #fileList .action.action-share-notification span, #fileList a.name { cursor: default !important; } -a.action>img { - max-height:16px; - max-width:16px; - vertical-align:text-bottom; - margin-bottom: -1px; +a.action > img { + max-height: 16px; + max-width: 16px; + vertical-align: text-bottom; } /* Actions for selected files */ @@ -573,24 +569,29 @@ a.action>img { display:none; } -#fileList a.action[data-action="Rename"] { - padding: 16px 14px 17px !important; -} - .ie8 #fileList a.action img, #fileList tr:hover a.action, #fileList a.action.permanent, #fileList tr:focus a.action, #fileList a.action.permanent, #fileList tr:hover a.action.no-permission:hover, -#fileList tr:focus a.action.no-permission:focus -/*#fileList .name:focus .action*/ { +#fileList tr:focus a.action.no-permission:focus, +/*#fileList .name:focus .action,*/ +/* also enforce the low opacity for disabled links that are hovered/focused */ +.ie8 #fileList a.action.disabled:hover img, +#fileList tr:hover a.action.disabled:hover, +#fileList tr:focus a.action.disabled:focus, +#fileList .name:focus a.action.disabled:focus, +#fileList a.action.disabled img { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; filter: alpha(opacity=50); opacity: .5; display:inline; } .ie8 #fileList a.action:hover img, +#fileList tr a.action.disabled.action-download, +#fileList tr:hover a.action.disabled.action-download:hover, +#fileList tr:focus a.action.disabled.action-download:focus, #fileList tr:hover a.action:hover, #fileList tr:focus a.action:focus, #fileList .name:focus a.action:focus { @@ -599,6 +600,18 @@ a.action>img { opacity: 1; display:inline; } +#fileList tr a.action.disabled { + background: none; +} + +#selectedActionsList a.download.disabled, +#fileList tr a.action.action-download.disabled { + color: #000000; +} + +#fileList tr:hover a.action.disabled:hover * { + cursor: default; +} .summary { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; @@ -667,3 +680,44 @@ table.dragshadow td.size { .mask.transparent{ opacity: 0; } + +.fileActionsMenu { + padding: 4px 12px; +} +.fileActionsMenu li { + padding: 5px 0; +} +#fileList .fileActionsMenu a.action img { + padding: initial; +} +#fileList .fileActionsMenu a.action { + padding: 10px; + margin: -10px; +} + +.fileActionsMenu.hidden { + display: none; +} + +#fileList .fileActionsMenu .action { + display: block; + line-height: 30px; + padding-left: 5px; + color: #000; + padding: 0; +} + +.fileActionsMenu .action img, +.fileActionsMenu .action .no-icon { + display: inline-block; + width: 16px; + margin-right: 5px; +} + +.fileActionsMenu .action { + opacity: 0.5; +} + +.fileActionsMenu li:hover .action { + opacity: 1; +} diff --git a/apps/files/css/mobile.css b/apps/files/css/mobile.css index 4881f7c70e4..dd8244a2913 100644 --- a/apps/files/css/mobile.css +++ b/apps/files/css/mobile.css @@ -5,11 +5,6 @@ min-width: initial !important; } -/* do not show Deleted Files on mobile, not optimized yet and button too long */ -#controls #trash { - display: none; -} - /* hide size and date columns */ table th#headerSize, table td.filesize, @@ -38,7 +33,8 @@ table td.filename .nametext { } /* always show actions on mobile, not only on hover */ -#fileList a.action { +#fileList a.action, +#fileList a.action.action-menu.permanent { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)" !important; filter: alpha(opacity=20) !important; opacity: .2 !important; @@ -50,17 +46,19 @@ table td.filename .nametext { filter: alpha(opacity=70) !important; opacity: .7 !important; } -/* do not show Rename or Versions on mobile */ -#fileList .action.action-rename, -#fileList .action.action-versions { - display: none !important; +#fileList a.action.action-menu img { + padding-left: 2px; +} + +#fileList .fileActionsMenu { + margin-right: 5px; } /* some padding for better clickability */ #fileList a.action img { padding: 0 6px 0 12px; } -/* hide text of the actions on mobile */ -#fileList a.action span { +/* hide text of the share action on mobile */ +#fileList a.action-share span { display: none; } diff --git a/apps/files/index.php b/apps/files/index.php index 4f103f975cb..a73caa50fbe 100644 --- a/apps/files/index.php +++ b/apps/files/index.php @@ -41,6 +41,7 @@ OCP\Util::addscript('files', 'file-upload'); OCP\Util::addscript('files', 'jquery.iframe-transport'); OCP\Util::addscript('files', 'jquery.fileupload'); OCP\Util::addscript('files', 'jquery-visibility'); +OCP\Util::addscript('files', 'fileinfomodel'); OCP\Util::addscript('files', 'filesummary'); OCP\Util::addscript('files', 'breadcrumb'); OCP\Util::addscript('files', 'filelist'); @@ -50,6 +51,12 @@ OCP\Util::addscript('files', 'search'); \OCP\Util::addScript('files', 'tagsplugin'); \OCP\Util::addScript('files', 'favoritesplugin'); +\OCP\Util::addScript('files', 'detailfileinfoview'); +\OCP\Util::addScript('files', 'detailtabview'); +\OCP\Util::addScript('files', 'mainfileinfodetailview'); +\OCP\Util::addScript('files', 'detailsview'); +\OCP\Util::addStyle('files', 'detailsView'); + \OC_Util::addVendorScript('core', 'handlebars/handlebars'); OCP\App::setActiveNavigationEntry('files_index'); @@ -132,9 +139,13 @@ foreach ($navItems as $item) { } OCP\Util::addscript('files', 'fileactions'); +OCP\Util::addscript('files', 'fileactionsmenu'); OCP\Util::addscript('files', 'files'); OCP\Util::addscript('files', 'navigation'); OCP\Util::addscript('files', 'keyboardshortcuts'); + +\OC::$server->getEventDispatcher()->dispatch('OCA\Files::loadAdditionalScripts'); + $tmpl = new OCP\Template('files', 'index', 'user'); $tmpl->assign('usedSpacePercent', (int)$storageInfo['relative']); $tmpl->assign('owner', $storageInfo['owner']); diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js new file mode 100644 index 00000000000..43595001212 --- /dev/null +++ b/apps/files/js/detailfileinfoview.js @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + /** + * @class OCA.Files.DetailFileInfoView + * @classdesc + * + * Displays a block of details about the file info. + * + */ + var DetailFileInfoView = OC.Backbone.View.extend({ + tagName: 'div', + className: 'detailFileInfoView', + + _template: null, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfo} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this.model = fileInfo; + this.render(); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfo} file info + */ + getFileInfo: function() { + return this.model; + } + }); + + OCA.Files.DetailFileInfoView = DetailFileInfoView; +})(); + diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js new file mode 100644 index 00000000000..a4ebe90cd64 --- /dev/null +++ b/apps/files/js/detailsview.js @@ -0,0 +1,271 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var TEMPLATE = + '<div>' + + ' <div class="detailFileInfoContainer">' + + ' </div>' + + ' <div>' + + ' {{#if tabHeaders}}' + + ' <ul class="tabHeaders">' + + ' {{#each tabHeaders}}' + + ' <li class="tabHeader" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}">' + + ' <a href="#">{{label}}</a>' + + ' </li>' + + ' {{/each}}' + + ' </ul>' + + ' {{/if}}' + + ' <div class="tabsContainer">' + + ' </div>' + + ' </div>' + + ' <a class="close icon-close" href="#" alt="{{closeLabel}}"></a>' + + '</div>'; + + /** + * @class OCA.Files.DetailsView + * @classdesc + * + * The details view show details about a selected file. + * + */ + var DetailsView = OC.Backbone.View.extend({ + id: 'app-sidebar', + tabName: 'div', + className: 'detailsView', + + _template: null, + + /** + * List of detail tab views + * + * @type Array<OCA.Files.DetailTabView> + */ + _tabViews: [], + + /** + * List of detail file info views + * + * @type Array<OCA.Files.DetailFileInfoView> + */ + _detailFileInfoViews: [], + + /** + * Id of the currently selected tab + * + * @type string + */ + _currentTabId: null, + + /** + * Dirty flag, whether the view needs to be rerendered + */ + _dirty: false, + + events: { + 'click a.close': '_onClose', + 'click .tabHeaders .tabHeader': '_onClickTab' + }, + + /** + * Initialize the details view + */ + initialize: function() { + this._tabViews = []; + this._detailFileInfoViews = []; + + this._dirty = true; + + // uncomment to add some dummy tabs for testing + //this._addTestTabs(); + }, + + _onClose: function(event) { + OC.Apps.hideAppSidebar(); + event.preventDefault(); + }, + + _onClickTab: function(e) { + var $target = $(e.target); + e.preventDefault(); + if (!$target.hasClass('tabHeader')) { + $target = $target.closest('.tabHeader'); + } + var tabId = $target.attr('data-tabid'); + if (_.isUndefined(tabId)) { + return; + } + + this.selectTab(tabId); + }, + + _addTestTabs: function() { + for (var j = 0; j < 2; j++) { + var testView = new OCA.Files.DetailTabView({id: 'testtab' + j}); + testView.index = j; + testView.getLabel = function() { return 'Test tab ' + this.index; }; + testView.render = function() { + this.$el.empty(); + for (var i = 0; i < 100; i++) { + this.$el.append('<div>Test tab ' + this.index + ' row ' + i + '</div>'); + } + }; + this._tabViews.push(testView); + } + }, + + template: function(vars) { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + return this._template(vars); + }, + + /** + * Renders this details view + */ + render: function() { + var templateVars = { + closeLabel: t('files', 'Close') + }; + + if (this._tabViews.length > 1) { + // only render headers if there is more than one available + templateVars.tabHeaders = _.map(this._tabViews, function(tabView, i) { + return { + tabId: tabView.id, + tabIndex: i, + label: tabView.getLabel() + }; + }); + } + + this.$el.html(this.template(templateVars)); + + var $detailsContainer = this.$el.find('.detailFileInfoContainer'); + + // render details + _.each(this._detailFileInfoViews, function(detailView) { + $detailsContainer.append(detailView.get$()); + }); + + if (!this._currentTabId && this._tabViews.length > 0) { + this._currentTabId = this._tabViews[0].id; + } + + this.selectTab(this._currentTabId); + + this._dirty = false; + }, + + /** + * Selects the given tab by id + * + * @param {string} tabId tab id + */ + selectTab: function(tabId) { + if (!tabId) { + return; + } + + var tabView = _.find(this._tabViews, function(tab) { + return tab.id === tabId; + }); + + if (!tabView) { + console.warn('Details view tab with id "' + tabId + '" not found'); + return; + } + + this._currentTabId = tabId; + + var $tabsContainer = this.$el.find('.tabsContainer'); + var $tabEl = $tabsContainer.find('#' + tabId); + + // hide other tabs + $tabsContainer.find('.tab').addClass('hidden'); + + // tab already rendered ? + if (!$tabEl.length) { + // render tab + $tabsContainer.append(tabView.$el); + $tabEl = tabView.$el; + } + + // this should trigger tab rendering + tabView.setFileInfo(this.model); + + $tabEl.removeClass('hidden'); + + // update tab headers + var $tabHeaders = this.$el.find('.tabHeaders li'); + $tabHeaders.removeClass('selected'); + $tabHeaders.filterAttr('data-tabid', tabView.id).addClass('selected'); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfoModel} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + this.model = fileInfo; + + if (this._dirty) { + this.render(); + } + + if (this._currentTabId) { + // only update current tab, others will be updated on-demand + var tabId = this._currentTabId; + var tabView = _.find(this._tabViews, function(tab) { + return tab.id === tabId; + }); + tabView.setFileInfo(fileInfo); + } + + _.each(this._detailFileInfoViews, function(detailView) { + detailView.setFileInfo(fileInfo); + }); + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfoModel} file info + */ + getFileInfo: function() { + return this.model; + }, + + /** + * Adds a tab in the tab view + * + * @param {OCA.Files.DetailTabView} tab view + */ + addTabView: function(tabView) { + this._tabViews.push(tabView); + this._dirty = true; + }, + + /** + * Adds a detail view for file info. + * + * @param {OCA.Files.DetailFileInfoView} detail view + */ + addDetailView: function(detailView) { + this._detailFileInfoViews.push(detailView); + this._dirty = true; + } + }); + + OCA.Files.DetailsView = DetailsView; +})(); + diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js new file mode 100644 index 00000000000..b0e170bc4e7 --- /dev/null +++ b/apps/files/js/detailtabview.js @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + /** + * @class OCA.Files.DetailTabView + * @classdesc + * + * Base class for tab views to display file information. + * + */ + var DetailTabView = OC.Backbone.View.extend({ + tag: 'div', + + className: 'tab', + + /** + * Tab label + */ + _label: null, + + _template: null, + + initialize: function() { + if (!this.id) { + this.id = 'detailTabView' + DetailTabView._TAB_COUNT; + DetailTabView._TAB_COUNT++; + } + }, + + /** + * Returns the tab label + * + * @return {String} label + */ + getLabel: function() { + return 'Tab ' + this.id; + }, + + /** + * returns the jQuery object for HTML output + * + * @returns {jQuery} + */ + get$: function() { + return this.$el; + }, + + /** + * Renders this details view + * + * @abstract + */ + render: function() { + // to be implemented in subclass + // FIXME: code is only for testing + this.$el.html('<div>Hello ' + this.id + '</div>'); + }, + + /** + * Sets the file info to be displayed in the view + * + * @param {OCA.Files.FileInfoModel} fileInfo file info to set + */ + setFileInfo: function(fileInfo) { + if (this.model !== fileInfo) { + this.model = fileInfo; + this.render(); + } + }, + + /** + * Returns the file info. + * + * @return {OCA.Files.FileInfoModel} file info + */ + getFileInfo: function() { + return this.model; + } + }); + DetailTabView._TAB_COUNT = 0; + + OCA.Files = OCA.Files || {}; + + OCA.Files.DetailTabView = DetailTabView; +})(); + diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js index 1956fda0077..f3f137a0537 100644 --- a/apps/files/js/fileactions.js +++ b/apps/files/js/fileactions.js @@ -10,6 +10,12 @@ (function() { + var TEMPLATE_FILE_ACTION_TRIGGER = + '<a class="action action-{{nameLowerCase}}" href="#" data-action="{{name}}">' + + '{{#if icon}}<img class="svg" alt="{{altText}}" src="{{icon}}" />{{/if}}' + + '{{#if displayName}}<span> {{displayName}}</span>{{/if}}' + + '</a>'; + /** * Construct a new FileActions instance * @constructs FileActions @@ -18,11 +24,17 @@ var FileActions = function() { this.initialize(); }; + FileActions.TYPE_DROPDOWN = 0; + FileActions.TYPE_INLINE = 1; FileActions.prototype = { /** @lends FileActions.prototype */ actions: {}, defaults: {}, icons: {}, + + /** + * @deprecated + */ currentFile: null, /** @@ -38,6 +50,8 @@ */ _updateListeners: {}, + _fileActionTriggerTemplate: null, + /** * @private */ @@ -46,6 +60,8 @@ // abusing jquery for events until we get a real event lib this.$el = $('<div class="dummy-fileactions hidden"></div>'); $('body').append(this.$el); + + this._showMenuClosure = _.bind(this._showMenu, this); }, /** @@ -111,6 +127,7 @@ displayName: displayName || name }); }, + /** * Register action * @@ -125,15 +142,14 @@ displayName: action.displayName, mime: mime, icon: action.icon, - permissions: action.permissions + permissions: action.permissions, + type: action.type || FileActions.TYPE_DROPDOWN }; if (_.isUndefined(action.displayName)) { actionSpec.displayName = t('files', name); } if (_.isFunction(action.render)) { actionSpec.render = action.render; - } else { - actionSpec.render = _.bind(this._defaultRenderAction, this); } if (!this.actions[mime]) { this.actions[mime] = {}; @@ -162,6 +178,16 @@ this.defaults[mime] = name; this._notifyUpdateListeners('setDefault', {defaultAction: {mime: mime, name: name}}); }, + + /** + * Returns a map of file actions handlers matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Object.<string,OCA.Files.FileActions~actionHandler>} map of action name to action spec + */ get: function (mime, type, permissions) { var actions = this.getActions(mime, type, permissions); var filteredActions = {}; @@ -170,6 +196,16 @@ }); return filteredActions; }, + + /** + * Returns an array of file actions matching the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {Array.<OCA.Files.FileAction>} array of action specs + */ getActions: function (mime, type, permissions) { var actions = {}; if (this.actions.all) { @@ -197,7 +233,37 @@ }); return filteredActions; }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * + * @deprecated use getDefaultFileAction instead + */ getDefault: function (mime, type, permissions) { + var defaultActionSpec = this.getDefaultFileAction(mime, type, permissions); + if (defaultActionSpec) { + return defaultActionSpec.action; + } + return undefined; + }, + + /** + * Returns the default file action handler for the given conditions + * + * @param {string} mime mime type + * @param {string} type "dir" or "file" + * @param {int} permissions permissions + * + * @return {OCA.Files.FileActions~actionHandler} action handler + * @since 8.2 + */ + getDefaultFileAction: function(mime, type, permissions) { var mimePart; if (mime) { mimePart = mime.substr(0, mime.indexOf('/')); @@ -212,9 +278,10 @@ } else { name = this.defaults.all; } - var actions = this.get(mime, type, permissions); + var actions = this.getActions(mime, type, permissions); return actions[name]; }, + /** * Default function to render actions * @@ -224,87 +291,84 @@ * @param {OCA.Files.FileActionContext} context action context */ _defaultRenderAction: function(actionSpec, isDefault, context) { - var name = actionSpec.name; - if (name === 'Download' || !isDefault) { - var $actionLink = this._makeActionLink(actionSpec, context); + if (!isDefault) { + var params = { + name: actionSpec.name, + nameLowerCase: actionSpec.name.toLowerCase(), + displayName: actionSpec.displayName, + icon: actionSpec.icon, + altText: actionSpec.altText, + }; + if (_.isFunction(actionSpec.icon)) { + params.icon = actionSpec.icon(context.$file.attr('data-file')); + } + + var $actionLink = this._makeActionLink(params, context); context.$file.find('a.name>span.fileactions').append($actionLink); + $actionLink.addClass('permanent'); return $actionLink; } }, + /** * Renders the action link element * - * @param {OCA.Files.FileAction} actionSpec action object - * @param {OCA.Files.FileActionContext} context action context + * @param {Object} params action params */ - _makeActionLink: function(actionSpec, context) { - var img = actionSpec.icon; - if (img && img.call) { - img = img(context.$file.attr('data-file')); - } - var html = '<a href="#">'; - if (img) { - html += '<img class="svg" alt="" src="' + img + '" />'; + _makeActionLink: function(params) { + if (!this._fileActionTriggerTemplate) { + this._fileActionTriggerTemplate = Handlebars.compile(TEMPLATE_FILE_ACTION_TRIGGER); } - if (actionSpec.displayName) { - html += '<span> ' + actionSpec.displayName + '</span>'; - } - html += '</a>'; - return $(html); + return $(this._fileActionTriggerTemplate(params)); }, + /** - * Custom renderer for the "Rename" action. - * Displays the rename action as an icon behind the file name. + * Displays the file actions dropdown menu * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {string} fileName file name + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderRenameAction: function(actionSpec, isDefault, context) { - var $actionEl = this._makeActionLink(actionSpec, context); - var $container = context.$file.find('a.name span.nametext'); - $actionEl.find('img').attr('alt', t('files', 'Rename')); - $container.find('.action-rename').remove(); - $container.append($actionEl); - return $actionEl; + _showMenu: function(fileName, context) { + var menu; + var $trigger = context.$file.closest('tr').find('.fileactions .action-menu'); + $trigger.addClass('open'); + + menu = new OCA.Files.FileActionsMenu(); + + context.$file.find('td.filename').append(menu.$el); + + menu.$el.on('afterHide', function() { + context.$file.removeClass('mouseOver'); + $trigger.removeClass('open'); + menu.remove(); + }); + + context.$file.addClass('mouseOver'); + menu.show(context); }, + /** - * Custom renderer for the "Delete" action. - * Displays the "Delete" action as a trash icon at the end of - * the table row. - * - * @param {OCA.Files.FileAction} actionSpec file action to render - * @param {boolean} isDefault true if the action is a default action, - * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * Renders the menu trigger on the given file list row + * + * @param {Object} $tr file list row element + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderDeleteAction: function(actionSpec, isDefault, context) { - var mountType = context.$file.attr('data-mounttype'); - var deleteTitle = t('files', 'Delete'); - if (mountType === 'external-root') { - deleteTitle = t('files', 'Disconnect storage'); - } else if (mountType === 'shared-root') { - deleteTitle = t('files', 'Unshare'); - } - var cssClasses = 'action delete icon-delete'; - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { - // add css class no-permission to delete icon - cssClasses += ' no-permission'; - deleteTitle = t('files', 'No permission to delete'); - } - var $actionLink = $('<a href="#" original-title="' + - escapeHTML(deleteTitle) + - '" class="' +cssClasses + '">' + - '<span class="hidden-visually">' + escapeHTML(deleteTitle) + '</span>' + - '</a>' - ); - var $container = context.$file.find('td:last'); - $container.find('.delete').remove(); - $container.append($actionLink); - return $actionLink; + _renderMenuTrigger: function($tr, context) { + // remove previous + $tr.find('.action-menu').remove(); + + var $el = this._renderInlineAction({ + name: 'menu', + displayName: '', + icon: OC.imagePath('core', 'actions/more'), + altText: t('files', 'Actions'), + action: this._showMenuClosure + }, false, context); + + $el.addClass('permanent'); }, + /** * Renders the action element by calling actionSpec.render() and * registers the click event to process the action. @@ -312,40 +376,117 @@ * @param {OCA.Files.FileAction} actionSpec file action to render * @param {boolean} isDefault true if the action is a default action, * false otherwise - * @param {OCAFiles.FileActionContext} context rendering context + * @param {OCA.Files.FileActionContext} context rendering context */ - _renderAction: function(actionSpec, isDefault, context) { - var $actionEl = actionSpec.render(actionSpec, isDefault, context); + _renderInlineAction: function(actionSpec, isDefault, context) { + var renderFunc = actionSpec.render || _.bind(this._defaultRenderAction, this); + var $actionEl = renderFunc(actionSpec, isDefault, context); if (!$actionEl || !$actionEl.length) { return; } - $actionEl.addClass('action action-' + actionSpec.name.toLowerCase()); - $actionEl.attr('data-action', actionSpec.name); $actionEl.on( 'click', { a: null }, function(event) { + event.stopPropagation(); + event.preventDefault(); + + if ($actionEl.hasClass('open')) { + return; + } + var $file = $(event.target).closest('tr'); + if ($file.hasClass('busy')) { + return; + } var currentFile = $file.find('td.filename'); var fileName = $file.attr('data-file'); - event.stopPropagation(); - event.preventDefault(); context.fileActions.currentFile = currentFile; // also set on global object for legacy apps window.FileActions.currentFile = currentFile; + var callContext = _.extend({}, context); + + if (!context.dir && context.fileList) { + callContext.dir = $file.attr('data-path') || context.fileList.getCurrentDirectory(); + } + + if (!context.fileInfoModel && context.fileList) { + callContext.fileInfoModel = context.fileList.getModelForFile(fileName); + if (!callContext.fileInfoModel) { + console.warn('No file info model found for file "' + fileName + '"'); + } + } + actionSpec.action( fileName, - _.extend(context, { - dir: $file.attr('data-path') || context.fileList.getCurrentDirectory() - }) + callContext ); } ); + $actionEl.tooltip({placement:'top'}); return $actionEl; }, + + /** + * Trigger the given action on the given file. + * + * @param {string} actionName action name + * @param {OCA.Files.FileInfoModel} fileInfoModel file info model + * @param {OCA.Files.FileList} [fileList] file list, for compatibility with older action handlers [DEPRECATED] + * + * @return {boolean} true if the action handler was called, false otherwise + * + * @since 8.2 + */ + triggerAction: function(actionName, fileInfoModel, fileList) { + var actionFunc; + var actions = this.get( + fileInfoModel.get('mimetype'), + fileInfoModel.isDirectory() ? 'dir' : 'file', + fileInfoModel.get('permissions') + ); + + if (actionName) { + actionFunc = actions[actionName]; + } else { + actionFunc = this.getDefault( + fileInfoModel.get('mimetype'), + fileInfoModel.isDirectory() ? 'dir' : 'file', + fileInfoModel.get('permissions') + ); + } + + if (!actionFunc) { + actionFunc = actions['Download']; + } + + if (!actionFunc) { + return false; + } + + var context = { + fileActions: this, + fileInfoModel: fileInfoModel, + dir: fileInfoModel.get('path') + }; + + var fileName = fileInfoModel.get('name'); + this.currentFile = fileName; + // also set on global object for legacy apps + window.FileActions.currentFile = fileName; + + if (fileList) { + // compatibility with action handlers that expect these + context.fileList = fileList; + context.$file = fileList.findFileEl(fileName); + } + + actionFunc(fileName, context); + }, + /** * Display file actions for the given element * @param parent "td" element of the file for which to display actions @@ -376,36 +517,29 @@ nameLinks = parent.children('a.name'); nameLinks.find('.fileactions, .nametext .action').remove(); nameLinks.append('<span class="fileactions" />'); - var defaultAction = this.getDefault( + var defaultAction = this.getDefaultFileAction( this.getCurrentMimeType(), this.getCurrentType(), this.getCurrentPermissions() ); + var context = { + $file: $tr, + fileActions: this, + fileList: fileList + }; + $.each(actions, function (name, actionSpec) { - if (name !== 'Share') { - self._renderAction( + if (actionSpec.type === FileActions.TYPE_INLINE) { + self._renderInlineAction( actionSpec, - actionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList : fileList - } + defaultAction && actionSpec.name === defaultAction.name, + context ); } }); - // added here to make sure it's always the last action - var shareActionSpec = actions.Share; - if (shareActionSpec){ - this._renderAction( - shareActionSpec, - shareActionSpec.action === defaultAction, { - $file: $tr, - fileActions: this, - fileList: fileList - } - ); - } + + this._renderMenuTrigger($tr, context); if (triggerEvent){ fileList.$fileList.trigger(jQuery.Event("fileActionsReady", {fileList: fileList, $files: $tr})); @@ -429,35 +563,42 @@ */ registerDefaultActions: function() { this.registerAction({ - name: 'Delete', - displayName: '', + name: 'Download', + displayName: t('files', 'Download'), mime: 'all', - // permission is READ because we show a hint instead if there is no permission permissions: OC.PERMISSION_READ, - icon: function() { - return OC.imagePath('core', 'actions/delete'); + icon: function () { + return OC.imagePath('core', 'actions/download'); }, - render: _.bind(this._renderDeleteAction, this), - actionHandler: function(fileName, context) { - // if there is no permission to delete do nothing - if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { + actionHandler: function (filename, context) { + var dir = context.dir || context.fileList.getCurrentDirectory(); + var url = context.fileList.getDownloadUrl(filename, dir); + + var downloadFileaction = $(context.$file).find('.fileactions .action-download'); + + // don't allow a second click on the download action + if(downloadFileaction.hasClass('disabled')) { return; } - context.fileList.do_delete(fileName, context.dir); - $('.tipsy').remove(); + + if (url) { + var disableLoadingState = function() { + context.fileList.showFileBusyState(filename, false); + }; + + context.fileList.showFileBusyState(downloadFileaction, true); + OCA.Files.Files.handleDownload(url, disableLoadingState); + } } }); - // t('files', 'Rename') this.registerAction({ name: 'Rename', - displayName: '', mime: 'all', permissions: OC.PERMISSION_UPDATE, icon: function() { return OC.imagePath('core', 'actions/rename'); }, - render: _.bind(this._renderRenameAction, this), actionHandler: function (filename, context) { context.fileList.rename(filename); } @@ -471,23 +612,51 @@ context.fileList.changeDirectory(dir + filename); }); - this.setDefault('dir', 'Open'); - - this.register('all', 'Download', OC.PERMISSION_READ, function () { - return OC.imagePath('core', 'actions/download'); - }, function (filename, context) { - var dir = context.dir || context.fileList.getCurrentDirectory(); - var url = context.fileList.getDownloadUrl(filename, dir); - if (url) { - OC.redirect(url); + this.registerAction({ + name: 'Delete', + mime: 'all', + // permission is READ because we show a hint instead if there is no permission + permissions: OC.PERMISSION_DELETE, + icon: function() { + return OC.imagePath('core', 'actions/delete'); + }, + actionHandler: function(fileName, context) { + // if there is no permission to delete do nothing + if((context.$file.data('permissions') & OC.PERMISSION_DELETE) === 0) { + return; + } + context.fileList.do_delete(fileName, context.dir); + $('.tipsy').remove(); } - }, t('files', 'Download')); + }); + + this.setDefault('dir', 'Open'); } }; OCA.Files.FileActions = FileActions; /** + * Replaces the download icon with a loading spinner and vice versa + * - also adds the class disabled to the passed in element + * + * @param downloadButtonElement download fileaction + * @param {boolean} showIt whether to show the spinner(true) or to hide it(false) + */ + OCA.Files.FileActions.updateFileActionSpinner = function(downloadButtonElement, showIt) { + var icon = downloadButtonElement.find('img'), + sourceImage = icon.attr('src'); + + if(showIt) { + downloadButtonElement.addClass('disabled'); + icon.attr('src', sourceImage.replace('actions/download.svg', 'loading-small.gif')); + } else { + downloadButtonElement.removeClass('disabled'); + icon.attr('src', sourceImage.replace('loading-small.gif', 'actions/download.svg')); + } + }; + + /** * File action attributes. * * @todo make this a real class in the future @@ -532,11 +701,12 @@ * Action handler function for file actions * * @callback OCA.Files.FileActions~actionHandler - * @param {String} fileName name of the clicked file + * @param {String} fileName name of the file on which the action must be performed * @param context context * @param {String} context.dir directory of the file - * @param context.$file jQuery element of the file - * @param {OCA.Files.FileList} context.fileList the FileList instance on which the action occurred + * @param {OCA.Files.FileInfoModel} fileInfoModel file info model + * @param {Object} [context.$file] jQuery element of the file [DEPRECATED] + * @param {OCA.Files.FileList} [context.fileList] the FileList instance on which the action occurred [DEPRECATED] * @param {OCA.Files.FileActions} context.fileActions the FileActions instance on which the action occurred */ diff --git a/apps/files/js/fileactionsmenu.js b/apps/files/js/fileactionsmenu.js new file mode 100644 index 00000000000..623ebde5442 --- /dev/null +++ b/apps/files/js/fileactionsmenu.js @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2014 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + + var TEMPLATE_MENU = + '<ul>' + + '{{#each items}}' + + '<li>' + + '<a href="#" class="action action-{{nameLowerCase}} permanent" data-action="{{name}}">{{#if icon}}<img src="{{icon}}"/>{{else}}<span class="no-icon"></span>{{/if}}<span>{{displayName}}</span></a>' + + '</li>' + + '{{/each}}' + + '</ul>'; + + /** + * Construct a new FileActionsMenu instance + * @constructs FileActionsMenu + * @memberof OCA.Files + */ + var FileActionsMenu = OC.Backbone.View.extend({ + tagName: 'div', + className: 'fileActionsMenu bubble hidden open menu', + + /** + * Current context + * + * @type OCA.Files.FileActionContext + */ + _context: null, + + events: { + 'click a.action': '_onClickAction' + }, + + template: function(data) { + if (!OCA.Files.FileActionsMenu._TEMPLATE) { + OCA.Files.FileActionsMenu._TEMPLATE = Handlebars.compile(TEMPLATE_MENU); + } + return OCA.Files.FileActionsMenu._TEMPLATE(data); + }, + + /** + * Event handler whenever an action has been clicked within the menu + * + * @param {Object} event event object + */ + _onClickAction: function(event) { + var $target = $(event.target); + if (!$target.is('a')) { + $target = $target.closest('a'); + } + var fileActions = this._context.fileActions; + var actionName = $target.attr('data-action'); + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + var actionSpec = actions[actionName]; + var fileName = this._context.$file.attr('data-file'); + + event.stopPropagation(); + event.preventDefault(); + + OC.hideMenus(); + + actionSpec.action( + fileName, + this._context + ); + }, + + /** + * Renders the menu with the currently set items + */ + render: function() { + var fileActions = this._context.fileActions; + var actions = fileActions.getActions( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var defaultAction = fileActions.getDefaultFileAction( + fileActions.getCurrentMimeType(), + fileActions.getCurrentType(), + fileActions.getCurrentPermissions() + ); + + var items = _.filter(actions, function(actionSpec) { + return ( + actionSpec.type === OCA.Files.FileActions.TYPE_DROPDOWN && + (!defaultAction || actionSpec.name !== defaultAction.name) + ); + }); + items = _.map(items, function(item) { + item.nameLowerCase = item.name.toLowerCase(); + return item; + }); + + this.$el.html(this.template({ + items: items + })); + }, + + /** + * Displays the menu under the given element + * + * @param {OCA.Files.FileActionContext} context context + * @param {Object} $trigger trigger element + */ + show: function(context) { + this._context = context; + + this.render(); + this.$el.removeClass('hidden'); + + OC.showMenu(null, this.$el); + } + }); + + OCA.Files.FileActionsMenu = FileActionsMenu; + +})(); + diff --git a/apps/files/js/fileinfomodel.js b/apps/files/js/fileinfomodel.js new file mode 100644 index 00000000000..05060854fba --- /dev/null +++ b/apps/files/js/fileinfomodel.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function(OC, OCA) { + + /** + * @class OC.Files.FileInfo + * @classdesc File information + * + * @param {Object} attributes file data + * @param {int} attributes.id file id + * @param {string} attributes.name file name + * @param {string} attributes.path path leading to the file, + * without the file name and with a leading slash + * @param {int} attributes.size size + * @param {string} attributes.mimetype mime type + * @param {string} attributes.icon icon URL + * @param {int} attributes.permissions permissions + * @param {Date} attributes.mtime modification time + * @param {string} attributes.etag etag + * @param {string} mountType mount type + * + * @since 8.2 + */ + var FileInfoModel = OC.Backbone.Model.extend({ + + initialize: function(data) { + if (!_.isUndefined(data.id)) { + data.id = parseInt(data.id, 10); + } + + // TODO: normalize path + data.path = data.path || ''; + data.name = data.name; + + data.mimetype = data.mimetype || 'application/octet-stream'; + }, + + /** + * Returns whether this file is a directory + * + * @return {boolean} true if this is a directory, false otherwise + */ + isDirectory: function() { + return this.get('mimetype') === 'httpd/unix-directory'; + }, + + /** + * Returns the full path to this file + * + * @return {string} full path + */ + getFullPath: function() { + return OC.joinPaths(this.get('path'), this.get('name')); + } + }); + + if (!OCA.Files) { + OCA.Files = {}; + } + OCA.Files.FileInfoModel = FileInfoModel; + +})(OC, OCA); + diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 417c4b9fe99..eb46f155269 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -23,6 +23,7 @@ * @param [options.scrollContainer] scrollable container, defaults to $(window) * @param [options.dragOptions] drag options, disabled by default * @param [options.folderDropOptions] folder drop options, disabled by default + * @param [options.detailsViewEnabled=true] whether to enable details view */ var FileList = function($el, options) { this.initialize($el, options); @@ -65,6 +66,11 @@ fileSummary: null, /** + * @type OCA.Files.DetailsView + */ + _detailsView: null, + + /** * Whether the file list was initialized already. * @type boolean */ @@ -205,6 +211,13 @@ } this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions); + if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) { + this._detailsView = new OCA.Files.DetailsView(); + this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView({fileList: this, fileActions: this.fileActions})); + this._detailsView.$el.insertBefore(this.$el); + this._detailsView.$el.addClass('disappear'); + } + this.$el.find('#controls').prepend(this.breadcrumb.$el); this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this)); @@ -216,6 +229,13 @@ this.updateSearch(); + this.$el.on('click', function(event) { + var $target = $(event.target); + // click outside file row ? + if (!$target.closest('tbody').length && !$target.closest('#app-sidebar').length) { + self._updateDetailsView(null); + } + }); this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this)); this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this)); this.$el.on('urlChanged', _.bind(this._onUrlChanged, this)); @@ -223,6 +243,8 @@ this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this)); this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this)); + this.$el.find('.selectedActions a').tooltip({placement:'top'}); + this.setupUploadEvents(); this.$container.on('scroll', _.bind(this._onScroll, this)); @@ -263,6 +285,79 @@ }, /** + * Returns a unique model for the given file name. + * + * @param {string|object} fileName file name or jquery row + * @return {OCA.Files.FileInfoModel} file info model + */ + getModelForFile: function(fileName) { + var $tr; + // jQuery object ? + if (fileName.is) { + $tr = fileName; + fileName = $tr.attr('data-file'); + } else { + $tr = this.findFileEl(fileName); + } + + if (!$tr || !$tr.length) { + return null; + } + + // if requesting the selected model, return it + if (this._currentFileModel && this._currentFileModel.get('name') === fileName) { + return this._currentFileModel; + } + + // TODO: note, this is a temporary model required for synchronising + // state between different views. + // In the future the FileList should work with Backbone.Collection + // and contain existing models that can be used. + // This method would in the future simply retrieve the matching model from the collection. + var model = new OCA.Files.FileInfoModel(this.elementToFile($tr)); + if (!model.has('path')) { + model.set('path', this.getCurrentDirectory(), {silent: true}); + } + return model; + }, + + /** + * Update the details view to display the given file + * + * @param {string} fileName file name from the current list + */ + _updateDetailsView: function(fileName) { + if (!this._detailsView) { + return; + } + + var oldFileInfo = this._detailsView.getFileInfo(); + if (oldFileInfo) { + // TODO: use more efficient way, maybe track the highlight + this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.get('id')).removeClass('highlighted'); + oldFileInfo.off('change', this._onSelectedModelChanged, this); + } + + if (!fileName) { + OC.Apps.hideAppSidebar(); + this._detailsView.setFileInfo(null); + this._currentFileModel = null; + return; + } + + var $tr = this.findFileEl(fileName); + var model = this.getModelForFile($tr); + + this._currentFileModel = model; + + $tr.addClass('highlighted'); + + this._detailsView.setFileInfo(model); + this._detailsView.$el.scrollTop(0); + _.defer(OC.Apps.showAppSidebar); + }, + + /** * Event handler for when the window size changed */ _onResize: function() { @@ -315,6 +410,12 @@ delete this._selectedFiles[$tr.data('id')]; this._selectionSummary.remove(data); } + if (this._selectionSummary.getTotal() === 1) { + this._updateDetailsView(_.values(this._selectedFiles)[0].name); + } else { + // show nothing when multiple files are selected + this._updateDetailsView(null); + } this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length); }, @@ -350,27 +451,33 @@ this._selectFileEl($tr, !$checkbox.prop('checked')); this.updateSelectionSummary(); } else { - var filename = $tr.attr('data-file'); - var renaming = $tr.data('renaming'); - if (!renaming) { - this.fileActions.currentFile = $tr.find('td'); - var mime = this.fileActions.getCurrentMimeType(); - var type = this.fileActions.getCurrentType(); - var permissions = this.fileActions.getCurrentPermissions(); - var action = this.fileActions.getDefault(mime,type, permissions); - if (action) { - event.preventDefault(); - // also set on global object for legacy apps - window.FileActions.currentFile = this.fileActions.currentFile; - action(filename, { - $file: $tr, - fileList: this, - fileActions: this.fileActions, - dir: $tr.attr('data-path') || this.getCurrentDirectory() - }); + // clicked directly on the name + if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) { + var filename = $tr.attr('data-file'); + var renaming = $tr.data('renaming'); + if (!renaming) { + this.fileActions.currentFile = $tr.find('td'); + var mime = this.fileActions.getCurrentMimeType(); + var type = this.fileActions.getCurrentType(); + var permissions = this.fileActions.getCurrentPermissions(); + var action = this.fileActions.getDefault(mime,type, permissions); + if (action) { + event.preventDefault(); + // also set on global object for legacy apps + window.FileActions.currentFile = this.fileActions.currentFile; + action(filename, { + $file: $tr, + fileList: this, + fileActions: this.fileActions, + dir: $tr.attr('data-path') || this.getCurrentDirectory() + }); + } + // deselect row + $(event.target).closest('a').blur(); } - // deselect row - $(event.target).closest('a').blur(); + } else { + this._updateDetailsView($tr.attr('data-file')); + event.preventDefault(); } } }, @@ -417,7 +524,21 @@ else { files = _.pluck(this.getSelectedFiles(), 'name'); } - OC.redirect(this.getDownloadUrl(files, dir)); + + var downloadFileaction = $('#selectedActionsList').find('.download'); + + // don't allow a second click on the download action + if(downloadFileaction.hasClass('disabled')) { + event.preventDefault(); + return; + } + + var disableLoadingState = function(){ + OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, false); + }; + + OCA.Files.FileActions.updateFileActionSpinner(downloadFileaction, true); + OCA.Files.Files.handleDownload(this.getDownloadUrl(files, dir), disableLoadingState); return false; }, @@ -487,6 +608,7 @@ * Event handler when dropping on a breadcrumb */ _onDropOnBreadCrumb: function( event, ui ) { + var self = this; var $target = $(event.target); if (!$target.is('.crumb')) { $target = $target.closest('.crumb'); @@ -508,7 +630,9 @@ var files = this.getSelectedFiles(); if (files.length === 0) { // single one selected without checkbox? - files = _.map(ui.helper.find('tr'), this.elementToFile); + files = _.map(ui.helper.find('tr'), function(el) { + return self.elementToFile($(el)); + }); } this.move(_.pluck(files, 'name'), targetPath); @@ -670,7 +794,7 @@ */ _createRow: function(fileData, options) { var td, simpleSize, basename, extension, sizeColor, - icon = OC.Util.replaceSVGIcon(fileData.icon), + icon = OC.MimeType.getIconUrl(fileData.mimetype), name = fileData.name, type = fileData.type || 'file', mtime = parseInt(fileData.mtime, 10), @@ -685,6 +809,10 @@ if (type === 'dir') { mime = mime || 'httpd/unix-directory'; + + if (fileData.mountType && fileData.mountType.indexOf('external') === 0) { + icon = OC.MimeType.getIconUrl('dir-external'); + } } //containing tr @@ -772,6 +900,7 @@ fileData.extraData = fileData.extraData.substr(1); } nameSpan.addClass('extra-data').attr('title', fileData.extraData); + nameSpan.tooltip({placement: 'right'}); } // dirs can show the number of uploaded files if (type === 'dir') { @@ -807,7 +936,7 @@ var formatted; var text; if (mtime > 0) { - formatted = formatDate(mtime); + formatted = OC.Util.formatDate(mtime); text = OC.Util.relativeModifiedDate(mtime); } else { formatted = t('files', 'Unable to determine date'); @@ -818,7 +947,9 @@ "class": "modified", "title": formatted, "style": 'color:rgb('+modifiedColor+','+modifiedColor+','+modifiedColor+')' - }).text(text)); + }).text(text) + .tooltip({placement: 'top'}) + ); tr.find('.filesize').text(simpleSize); tr.append(td); return tr; @@ -1104,6 +1235,8 @@ sortdirection: this._sortDirection } }); + // close sidebar + this._updateDetailsView(null); var callBack = this.reloadCallback.bind(this); return this._reloadCall.then(callBack, callBack); }, @@ -1213,36 +1346,40 @@ var etag = options.etag; // get mime icon url - OCA.Files.Files.getMimeIcon(mime, function(iconURL) { - var previewURL, - urlSpec = {}; - ready(iconURL); // set mimeicon URL + var iconURL = OC.MimeType.getIconUrl(mime); + var previewURL, + urlSpec = {}; + ready(iconURL); // set mimeicon URL - urlSpec.file = OCA.Files.Files.fixPath(path); + urlSpec.file = OCA.Files.Files.fixPath(path); + if (options.x) { + urlSpec.x = options.x; + } + if (options.y) { + urlSpec.y = options.y; + } - if (etag){ - // use etag as cache buster - urlSpec.c = etag; - } - else { - console.warn('OCA.Files.FileList.lazyLoadPreview(): missing etag argument'); - } + if (etag){ + // use etag as cache buster + urlSpec.c = etag; + } else { + console.warn('OCA.Files.FileList.lazyLoadPreview(): missing etag argument'); + } - previewURL = self.generatePreviewUrl(urlSpec); - previewURL = previewURL.replace('(', '%28'); - previewURL = previewURL.replace(')', '%29'); - - // preload image to prevent delay - // this will make the browser cache the image - var img = new Image(); - img.onload = function(){ - // if loading the preview image failed (no preview for the mimetype) then img.width will < 5 - if (img.width > 5) { - ready(previewURL); - } - }; - img.src = previewURL; - }); + previewURL = self.generatePreviewUrl(urlSpec); + previewURL = previewURL.replace('(', '%28'); + previewURL = previewURL.replace(')', '%29'); + + // preload image to prevent delay + // this will make the browser cache the image + var img = new Image(); + img.onload = function(){ + // if loading the preview image failed (no preview for the mimetype) then img.width will < 5 + if (img.width > 5) { + ready(previewURL); + } + }; + img.src = previewURL; }, setDirectoryPermissions: function(permissions) { @@ -1356,9 +1493,7 @@ } _.each(fileNames, function(fileName) { var $tr = self.findFileEl(fileName); - var $thumbEl = $tr.find('.thumbnail'); - var oldBackgroundImage = $thumbEl.css('background-image'); - $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + self.showFileBusyState($tr, true); // TODO: improve performance by sending all file names in a single call $.post( OC.filePath('files', 'ajax', 'move.php'), @@ -1400,7 +1535,7 @@ } else { OC.dialogs.alert(t('files', 'Error moving file'), t('files', 'Error')); } - $thumbEl.css('background-image', oldBackgroundImage); + self.showFileBusyState($tr, false); } ); }); @@ -1446,7 +1581,7 @@ }; function restore() { - input.tipsy('hide'); + input.tooltip('hide'); tr.data('renaming',false); form.remove(); td.children('a.name').show(); @@ -1461,14 +1596,13 @@ try { var newName = input.val(); - var $thumbEl = tr.find('.thumbnail'); - input.tipsy('hide'); + input.tooltip('hide'); form.remove(); if (newName !== oldname) { checkInput(); // mark as loading (temp element) - $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + self.showFileBusyState(tr, true); tr.attr('data-file', newName); var basename = newName; if (newName.indexOf('.') > 0 && tr.data('type') !== 'dir') { @@ -1476,7 +1610,6 @@ } td.find('a.name span.nametext').text(basename); td.children('a.name').show(); - tr.find('.fileactions, .action').addClass('hidden'); $.ajax({ url: OC.filePath('files','ajax','rename.php'), @@ -1503,6 +1636,7 @@ tr.remove(); tr = self.add(fileInfo, {updateSummary: false, silent: true}); self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)})); + self._updateDetailsView(fileInfo.name); } }); } else { @@ -1514,8 +1648,8 @@ } } catch (error) { input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); + input.tooltip({placement: 'left', trigger: 'manual'}); + input.tooltip('show'); input.addClass('error'); } return false; @@ -1524,12 +1658,12 @@ // verify filename on typing try { checkInput(); - input.tipsy('hide'); + input.tooltip('hide'); input.removeClass('error'); } catch (error) { input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); + input.tooltip({placement: 'left', trigger: 'manual'}); + input.tooltip('show'); input.addClass('error'); } if (event.keyCode === 27) { @@ -1547,6 +1681,44 @@ inList:function(file) { return this.findFileEl(file).length; }, + + /** + * Shows busy state on a given file row or multiple + * + * @param {string|Array.<string>} files file name or array of file names + * @param {bool} [busy=true] busy state, true for busy, false to remove busy state + * + * @since 8.2 + */ + showFileBusyState: function(files, state) { + var self = this; + if (!_.isArray(files)) { + files = [files]; + } + + if (_.isUndefined(state)) { + state = true; + } + + _.each(files, function($tr) { + // jquery element already ? + if (!$tr.is) { + $tr = self.findFileEl($tr); + } + + var $thumbEl = $tr.find('.thumbnail'); + $tr.toggleClass('busy', state); + + if (state) { + $thumbEl.attr('data-oldimage', $thumbEl.css('background-image')); + $thumbEl.css('background-image', 'url('+ OC.imagePath('core', 'loading.gif') + ')'); + } else { + $thumbEl.css('background-image', $thumbEl.attr('data-oldimage')); + $thumbEl.removeAttr('data-oldimage'); + } + }); + }, + /** * Delete the given files from the given dir * @param files file names list (without path) @@ -1560,9 +1732,8 @@ files=[files]; } if (files) { + this.showFileBusyState(files, true); for (var i=0; i<files.length; i++) { - var deleteAction = this.findFileEl(files[i]).children("td.date").children(".action.delete"); - deleteAction.removeClass('icon-delete').addClass('icon-loading-small'); } } // Finish any existing actions @@ -1580,7 +1751,7 @@ // no files passed, delete all in current dir params.allfiles = true; // show spinner for all files - this.$fileList.find('tr>td.date .action.delete').removeClass('icon-delete').addClass('icon-loading-small'); + this.showFileBusyState(this.$fileList.find('tr'), true); } $.post(OC.filePath('files', 'ajax', 'delete.php'), @@ -1623,8 +1794,7 @@ } else { $.each(files,function(index,file) { - var deleteAction = self.findFileEl(file).find('.action.delete'); - deleteAction.removeClass('icon-loading-small').addClass('icon-delete'); + self.showFileBusyState(file, false); }); } } @@ -1659,6 +1829,7 @@ } this.$table.addClass('hidden'); + this.$el.find('#emptycontent').addClass('hidden'); $mask = $('<div class="mask transparent"></div>'); @@ -1762,6 +1933,8 @@ updateSelectionSummary: function() { var summary = this._selectionSummary.summary; var canDelete; + var selection; + if (summary.totalFiles === 0 && summary.totalDirs === 0) { this.$el.find('#headerName a.name>span:first').text(t('files','Name')); this.$el.find('#headerSize a>span:first').text(t('files','Size')); @@ -1773,16 +1946,22 @@ canDelete = (this.getDirectoryPermissions() & OC.PERMISSION_DELETE) && this.isSelectedDeletable(); this.$el.find('.selectedActions').removeClass('hidden'); this.$el.find('#headerSize a>span:first').text(OC.Util.humanFileSize(summary.totalSize)); - var selection = ''; - if (summary.totalDirs > 0) { - selection += n('files', '%n folder', '%n folders', summary.totalDirs); - if (summary.totalFiles > 0) { - selection += ' & '; - } - } - if (summary.totalFiles > 0) { - selection += n('files', '%n file', '%n files', summary.totalFiles); + + var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs); + var fileInfo = n('files', '%n file', '%n files', summary.totalFiles); + + if (summary.totalDirs > 0 && summary.totalFiles > 0) { + var selectionVars = { + dirs: directoryInfo, + files: fileInfo + }; + selection = t('files', '{dirs} and {files}', selectionVars); + } else if (summary.totalDirs > 0) { + selection = directoryInfo; + } else { + selection = fileInfo; } + this.$el.find('#headerName a.name>span:first').text(selection); this.$el.find('#modified a>span:first').text(''); this.$el.find('table').addClass('multiselect'); @@ -2158,6 +2337,20 @@ } }); + }, + + /** + * Register a tab view to be added to all views + */ + registerTabView: function(tabView) { + this._detailsView.addTabView(tabView); + }, + + /** + * Register a detail view to be added to all views + */ + registerDetailView: function(detailView) { + this._detailsView.addDetailView(detailView); } }; diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 44868e78bd0..245648a79e2 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -61,7 +61,7 @@ if (response.data !== undefined && response.data.uploadMaxFilesize !== undefined) { $('#max_upload').val(response.data.uploadMaxFilesize); $('#free_space').val(response.data.freeSpace); - $('#upload.button').attr('original-title', response.data.maxHumanFilesize); + $('#upload.button').attr('data-original-title', response.data.maxHumanFilesize); $('#usedSpacePercent').val(response.data.usedSpacePercent); $('#owner').val(response.data.owner); $('#ownerDisplayName').val(response.data.ownerDisplayName); @@ -72,7 +72,7 @@ } if (response[0].uploadMaxFilesize !== undefined) { $('#max_upload').val(response[0].uploadMaxFilesize); - $('#upload.button').attr('original-title', response[0].maxHumanFilesize); + $('#upload.button').attr('data-original-title', response[0].maxHumanFilesize); $('#usedSpacePercent').val(response[0].usedSpacePercent); Files.displayStorageWarnings(); } @@ -116,7 +116,7 @@ ownerDisplayName = $('#ownerDisplayName').val(); if (usedSpacePercent > 98) { if (owner !== oc_current_user) { - OC.Notification.show(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!', + OC.Notification.showTemporary(t('files', 'Storage of {owner} is full, files can not be updated or synced anymore!', { owner: ownerDisplayName })); return; } @@ -125,7 +125,7 @@ } if (usedSpacePercent > 90) { if (owner !== oc_current_user) { - OC.Notification.show(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)', + OC.Notification.showTemporary(t('files', 'Storage of {owner} is almost full ({usedSpacePercent}%)', { usedSpacePercent: usedSpacePercent, owner: ownerDisplayName })); return; } @@ -163,18 +163,14 @@ return OC.filePath('files', 'ajax', action + '.php') + q; }, + /** + * Fetch the icon url for the mimetype + * @param {string} mime The mimetype + * @param {Files~mimeicon} ready Function to call when mimetype is retrieved + * @deprecated use OC.MimeType.getIconUrl(mime) + */ getMimeIcon: function(mime, ready) { - if (Files.getMimeIcon.cache[mime]) { - ready(Files.getMimeIcon.cache[mime]); - } else { - $.get( OC.filePath('files','ajax','mimeicon.php'), {mime: mime}, function(path) { - if(OC.Util.hasSVGSupport()){ - path = path.substr(0, path.length-4) + '.svg'; - } - Files.getMimeIcon.cache[mime]=path; - ready(Files.getMimeIcon.cache[mime]); - }); - } + ready(OC.MimeType.getIconUrl(mime)); }, /** @@ -211,7 +207,6 @@ * Initialize the files view */ initialize: function() { - Files.getMimeIcon.cache = {}; Files.bindKeyboardShortcuts(document, $); // TODO: move file list related code (upload) to OCA.Files.FileList @@ -239,7 +234,6 @@ // display storage warnings setTimeout(Files.displayStorageWarnings, 100); - OC.Notification.setDefault(Files.displayStorageWarnings); // only possible at the moment if user is logged in or the files app is loaded if (OC.currentUser && OCA.Files.App) { @@ -252,12 +246,12 @@ // Use jquery-visibility to de-/re-activate file stats sync if ($.support.pageVisibility) { $(document).on({ - 'show.visibility': function() { + 'show': function() { if (!updateStorageStatisticsIntervalId) { updateStorageStatisticsIntervalId = setInterval(func, updateStorageStatisticsInterval); } }, - 'hide.visibility': function() { + 'hide': function() { clearInterval(updateStorageStatisticsIntervalId); updateStorageStatisticsIntervalId = 0; } @@ -270,14 +264,41 @@ $('#webdavurl').select(); }); + $('#upload').tooltip({placement:'right'}); + //FIXME scroll to and highlight preselected file /* if (getURLParameter('scrollto')) { FileList.scrollTo(getURLParameter('scrollto')); } */ + }, + + /** + * Handles the download and calls the callback function once the download has started + * - browser sends download request and adds parameter with a token + * - server notices this token and adds a set cookie to the download response + * - browser now adds this cookie for the domain + * - JS periodically checks for this cookie and then knows when the download has started to call the callback + * + * @param {string} url download URL + * @param {function} callback function to call once the download has started + */ + handleDownload: function(url, callback) { + var randomToken = Math.random().toString(36).substring(2), + checkForDownloadCookie = function() { + if (!OC.Util.isCookieSetToValue('ocDownloadStarted', randomToken)){ + return false; + } else { + callback(); + return true; + } + }; + + OC.redirect(url + '&downloadStartSecret=' + randomToken); + OC.Util.waitFor(checkForDownloadCookie, 500); } - } + }; Files._updateStorageStatisticsDebounced = _.debounce(Files._updateStorageStatistics, 250); OCA.Files.Files = Files; @@ -432,7 +453,9 @@ var folderDropOptions = { var files = FileList.getSelectedFiles(); if (files.length === 0) { // single one selected without checkbox? - files = _.map(ui.helper.find('tr'), FileList.elementToFile); + files = _.map(ui.helper.find('tr'), function(el) { + return FileList.elementToFile($(el)); + }); } FileList.move(_.pluck(files, 'name'), targetPath); diff --git a/apps/files/js/jquery-visibility.js b/apps/files/js/jquery-visibility.js index 18f57d1f2bd..4d65e7771f9 100644 --- a/apps/files/js/jquery-visibility.js +++ b/apps/files/js/jquery-visibility.js @@ -1,31 +1,88 @@ -/*! http://mths.be/visibility v1.0.5 by @mathias */ -(function (window, document, $, undefined) { +/*! + * jquery-visibility v1.0.11 + * Page visibility shim for jQuery. + * + * Project Website: http://mths.be/visibility + * + * @version 1.0.11 + * @license MIT. + * @author Mathias Bynens - @mathias + * @author Jan Paepke - @janpaepke + */ +;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], function ($) { + return factory(root, $); + }); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(root, require('jquery')); + } else { + // Browser globals + factory(root, jQuery); + } +}(this, function(window, $, undefined) { + "use strict"; - var prefix, - property, - // In Opera, `'onfocusin' in document == true`, hence the extra `hasFocus` check to detect IE-like behavior - eventName = 'onfocusin' in document && 'hasFocus' in document ? 'focusin focusout' : 'focus blur', - prefixes = ['', 'moz', 'ms', 'o', 'webkit'], - $support = $.support, - $event = $.event; + var + document = window.document, + property, // property name of document, that stores page visibility + vendorPrefixes = ['webkit', 'o', 'ms', 'moz', ''], + $support = $.support || {}, + // In Opera, `'onfocusin' in document == true`, hence the extra `hasFocus` check to detect IE-like behavior + eventName = 'onfocusin' in document && 'hasFocus' in document ? + 'focusin focusout' : + 'focus blur'; - while ((property = prefix = prefixes.pop()) != undefined) { - property = (prefix ? prefix + 'H' : 'h') + 'idden'; - if ($support.pageVisibility = typeof document[property] == 'boolean') { + var prefix; + while ((prefix = vendorPrefixes.pop()) !== undefined) { + property = (prefix ? prefix + 'H': 'h') + 'idden'; + $support.pageVisibility = document[property] !== undefined; + if ($support.pageVisibility) { eventName = prefix + 'visibilitychange'; break; } } - $(/blur$/.test(eventName) ? window : document).on(eventName, function (event) { - var type = event.type, - originalEvent = event.originalEvent; - // If it’s a `{focusin,focusout}` event (IE), `fromElement` and `toElement` should both be `null` or `undefined`; - // else, the page visibility hasn’t changed, but the user just clicked somewhere in the doc. - // In IE9, we need to check the `relatedTarget` property instead. - if (!/^focus./.test(type) || originalEvent == undefined || (originalEvent.toElement == undefined && originalEvent.fromElement == undefined && originalEvent.relatedTarget == undefined)) { - $event.trigger((property && document[property] || /^(?:blur|focusout)$/.test(type) ? 'hide' : 'show') + '.visibility'); + // normalize to and update document hidden property + function updateState() { + if (property !== 'hidden') { + document.hidden = $support.pageVisibility ? document[property] : undefined; } - }); + } + updateState(); -}(this, document, jQuery)); + $(/blur$/.test(eventName) ? window : document).on(eventName, function(event) { + var type = event.type; + var originalEvent = event.originalEvent; + + // Avoid errors from triggered native events for which `originalEvent` is + // not available. + if (!originalEvent) { + return; + } + + var toElement = originalEvent.toElement; + + // If it’s a `{focusin,focusout}` event (IE), `fromElement` and `toElement` + // should both be `null` or `undefined`; else, the page visibility hasn’t + // changed, but the user just clicked somewhere in the doc. In IE9, we need + // to check the `relatedTarget` property instead. + if ( + !/^focus./.test(type) || ( + toElement === undefined && + originalEvent.fromElement === undefined && + originalEvent.relatedTarget === undefined + ) + ) { + $(document).triggerHandler( + property && document[property] || /^(?:blur|focusout)$/.test(type) ? + 'hide' : + 'show' + ); + } + // and update the current state + updateState(); + }); +})); diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js new file mode 100644 index 00000000000..6910e5f2be5 --- /dev/null +++ b/apps/files/js/mainfileinfodetailview.js @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +(function() { + var TEMPLATE = + '<a href="#" class="thumbnail action-default"></a><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' + + '<div class="file-details ellipsis">' + + ' <a href="#" ' + + ' alt="{{starAltText}}"' + + ' class="action action-favorite favorite">' + + ' <img class="svg" src="{{starIcon}}" />' + + ' </a>' + + ' <span class="size" title="{{altSize}}">{{size}}</span>, <span class="date" title="{{altDate}}">{{date}}</span>' + + '</div>'; + + /** + * @class OCA.Files.MainFileInfoDetailView + * @classdesc + * + * Displays main details about a file + * + */ + var MainFileInfoDetailView = OCA.Files.DetailFileInfoView.extend( + /** @lends OCA.Files.MainFileInfoDetailView.prototype */ { + + className: 'mainFileInfoView', + + /** + * Associated file list instance, for file actions + * + * @type {OCA.Files.FileList} + */ + _fileList: null, + + /** + * File actions + * + * @type {OCA.Files.FileActions} + */ + _fileActions: null, + + events: { + 'click a.action-favorite': '_onClickFavorite', + 'click a.action-default': '_onClickDefaultAction' + }, + + template: function(data) { + if (!this._template) { + this._template = Handlebars.compile(TEMPLATE); + } + return this._template(data); + }, + + initialize: function(options) { + options = options || {}; + this._fileList = options.fileList; + this._fileActions = options.fileActions; + if (!this._fileList) { + throw 'Missing requird parameter "fileList"'; + } + if (!this._fileActions) { + throw 'Missing requird parameter "fileActions"'; + } + }, + + _onClickFavorite: function(event) { + event.preventDefault(); + this._fileActions.triggerAction('Favorite', this.model, this._fileList); + }, + + _onClickDefaultAction: function(event) { + event.preventDefault(); + this._fileActions.triggerAction(null, this.model, this._fileList); + }, + + _onModelChanged: function() { + // simply re-render + this.render(); + }, + + setFileInfo: function(fileInfo) { + if (this.model) { + this.model.off('change', this._onModelChanged, this); + } + this.model = fileInfo; + if (this.model) { + this.model.on('change', this._onModelChanged, this); + } + this.render(); + }, + + /** + * Renders this details view + */ + render: function() { + if (this.model) { + var isFavorite = (this.model.get('tags') || []).indexOf(OC.TAG_FAVORITE) >= 0; + this.$el.html(this.template({ + nameLabel: t('files', 'Name'), + name: this.model.get('name'), + pathLabel: t('files', 'Path'), + path: this.model.get('path'), + sizeLabel: t('files', 'Size'), + size: OC.Util.humanFileSize(this.model.get('size'), true), + altSize: n('files', '%n byte', '%n bytes', this.model.get('size')), + dateLabel: t('files', 'Modified'), + altDate: OC.Util.formatDate(this.model.get('mtime')), + date: OC.Util.relativeModifiedDate(this.model.get('mtime')), + starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'), + starIcon: OC.imagePath('core', isFavorite ? 'actions/starred' : 'actions/star') + })); + + // TODO: we really need OC.Previews + var $iconDiv = this.$el.find('.thumbnail'); + if (!this.model.isDirectory()) { + // TODO: inject utility class? + FileList.lazyLoadPreview({ + path: this.model.getFullPath(), + mime: this.model.get('mimetype'), + etag: this.model.get('etag'), + x: 50, + y: 50, + callback: function(previewUrl) { + $iconDiv.css('background-image', 'url("' + previewUrl + '")'); + } + }); + } else { + // TODO: special icons / shared / external + $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")'); + } + this.$el.find('[title]').tooltip({placement: 'bottom'}); + } else { + this.$el.empty(); + } + this.delegateEvents(); + } + }); + + OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView; +})(); diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js index 293e25176f3..ed1105a1706 100644 --- a/apps/files/js/tagsplugin.js +++ b/apps/files/js/tagsplugin.js @@ -77,10 +77,11 @@ var self = this; // register "star" action fileActions.registerAction({ - name: 'favorite', + name: 'Favorite', displayName: 'Favorite', mime: 'all', permissions: OC.PERMISSION_READ, + type: OCA.Files.FileActions.TYPE_INLINE, render: function(actionSpec, isDefault, context) { var $file = context.$file; var isFavorite = $file.data('favorite') === true; @@ -107,6 +108,8 @@ } toggleStar($actionEl, !isFavorite); + context.fileInfoModel.set('tags', tags); + self.applyFileTags( dir + '/' + fileName, tags, @@ -144,6 +147,12 @@ $tr.find('td:first').prepend('<div class="favorite"></div>'); return $tr; }; + var oldElementToFile = fileList.elementToFile; + fileList.elementToFile = function($el) { + var fileInfo = oldElementToFile.apply(this, arguments); + fileInfo.tags = $el.attr('data-tags') || []; + return fileInfo; + }; }, attach: function(fileList) { diff --git a/apps/files/l10n/hu_HU.js b/apps/files/l10n/hu_HU.js index 52800fc6cee..0f7d09bc123 100644 --- a/apps/files/l10n/hu_HU.js +++ b/apps/files/l10n/hu_HU.js @@ -42,10 +42,13 @@ OC.L10N.register( "Delete" : "Törlés", "Disconnect storage" : "Tároló leválasztása", "Unshare" : "A megosztás visszavonása", + "No permission to delete" : "Nincs törlési jogosultsága", "Download" : "Letöltés", "Select" : "Kiválaszt", "Pending" : "Folyamatban", "Unable to determine date" : "Nem lehet meghatározni a dátumot", + "This operation is forbidden" : "Tiltott művelet", + "This directory is unavailable, please check the logs or contact the administrator" : "Ez a könyvtár nem elérhető, kérem nézze meg a naplófájlokat vagy keresse az adminisztrátort", "Error moving file." : "Hiba történt a fájl áthelyezése közben.", "Error moving file" : "Az állomány áthelyezése nem sikerült.", "Error" : "Hiba", @@ -61,7 +64,9 @@ OC.L10N.register( "_Uploading %n file_::_Uploading %n files_" : ["%n állomány feltöltése","%n állomány feltöltése"], "\"{name}\" is an invalid file name." : "\"{name}\" érvénytelen, mint fájlnév.", "File name cannot be empty." : "A fájlnév nem lehet semmi.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "A {owner} felhasználó tárolója betelt, a fájlok nem frissíthetők és szinkronizálhatók többet!", "Your storage is full, files can not be updated or synced anymore!" : "A tároló tele van, a fájlok nem frissíthetőek vagy szinkronizálhatóak a jövőben.", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "A {owner} felhasználó tárolója majdnem betelt ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "A tároló majdnem tele van ({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["egyezések '{filter}'","egyezés '{filter}'"], "{dirs} and {files}" : "{dirs} és {files}", @@ -70,6 +75,7 @@ OC.L10N.register( "An error occurred while trying to update the tags" : "Hiba történt, miközben megpróbálta frissíteni a címkéket", "A new file or folder has been <strong>created</strong>" : "Új fájl vagy könyvtár <strong>létrehozása</strong>", "A file or folder has been <strong>changed</strong>" : "Fájl vagy könyvtár <strong>módosítása</strong>", + "Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>" : "Szűkítse le az értesítéseket a létrehozásról és a változásokról a <strong>kedvenc fájlok</strong> <em>(Stream only)</em> -ra", "A file or folder has been <strong>deleted</strong>" : "Fájl vagy könyvtár <strong>törlése</strong>", "A file or folder has been <strong>restored</strong>" : "Fájl vagy könyvtár <strong>visszatöltése</strong>", "You created %1$s" : "Létrehoztam: %1$s", @@ -99,6 +105,7 @@ OC.L10N.register( "Folder" : "Mappa", "Upload" : "Feltöltés", "Cancel upload" : "A feltöltés megszakítása", + "No files in here" : "Itt nincsenek fájlok", "Upload some content or sync with your devices!" : "Tölts fel néhány tartalmat, vagy szinkronizálj az eszközöddel!", "No entries found in this folder" : "Nincsenek bejegyzések ebben a könyvtárban", "Select all" : "Összes kijelölése", diff --git a/apps/files/l10n/hu_HU.json b/apps/files/l10n/hu_HU.json index 3f2d8e133f2..bf98eef627b 100644 --- a/apps/files/l10n/hu_HU.json +++ b/apps/files/l10n/hu_HU.json @@ -40,10 +40,13 @@ "Delete" : "Törlés", "Disconnect storage" : "Tároló leválasztása", "Unshare" : "A megosztás visszavonása", + "No permission to delete" : "Nincs törlési jogosultsága", "Download" : "Letöltés", "Select" : "Kiválaszt", "Pending" : "Folyamatban", "Unable to determine date" : "Nem lehet meghatározni a dátumot", + "This operation is forbidden" : "Tiltott művelet", + "This directory is unavailable, please check the logs or contact the administrator" : "Ez a könyvtár nem elérhető, kérem nézze meg a naplófájlokat vagy keresse az adminisztrátort", "Error moving file." : "Hiba történt a fájl áthelyezése közben.", "Error moving file" : "Az állomány áthelyezése nem sikerült.", "Error" : "Hiba", @@ -59,7 +62,9 @@ "_Uploading %n file_::_Uploading %n files_" : ["%n állomány feltöltése","%n állomány feltöltése"], "\"{name}\" is an invalid file name." : "\"{name}\" érvénytelen, mint fájlnév.", "File name cannot be empty." : "A fájlnév nem lehet semmi.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "A {owner} felhasználó tárolója betelt, a fájlok nem frissíthetők és szinkronizálhatók többet!", "Your storage is full, files can not be updated or synced anymore!" : "A tároló tele van, a fájlok nem frissíthetőek vagy szinkronizálhatóak a jövőben.", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "A {owner} felhasználó tárolója majdnem betelt ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "A tároló majdnem tele van ({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["egyezések '{filter}'","egyezés '{filter}'"], "{dirs} and {files}" : "{dirs} és {files}", @@ -68,6 +73,7 @@ "An error occurred while trying to update the tags" : "Hiba történt, miközben megpróbálta frissíteni a címkéket", "A new file or folder has been <strong>created</strong>" : "Új fájl vagy könyvtár <strong>létrehozása</strong>", "A file or folder has been <strong>changed</strong>" : "Fájl vagy könyvtár <strong>módosítása</strong>", + "Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>" : "Szűkítse le az értesítéseket a létrehozásról és a változásokról a <strong>kedvenc fájlok</strong> <em>(Stream only)</em> -ra", "A file or folder has been <strong>deleted</strong>" : "Fájl vagy könyvtár <strong>törlése</strong>", "A file or folder has been <strong>restored</strong>" : "Fájl vagy könyvtár <strong>visszatöltése</strong>", "You created %1$s" : "Létrehoztam: %1$s", @@ -97,6 +103,7 @@ "Folder" : "Mappa", "Upload" : "Feltöltés", "Cancel upload" : "A feltöltés megszakítása", + "No files in here" : "Itt nincsenek fájlok", "Upload some content or sync with your devices!" : "Tölts fel néhány tartalmat, vagy szinkronizálj az eszközöddel!", "No entries found in this folder" : "Nincsenek bejegyzések ebben a könyvtárban", "Select all" : "Összes kijelölése", diff --git a/apps/files/l10n/id.js b/apps/files/l10n/id.js index 97190ddf47a..d11e9b7a4f6 100644 --- a/apps/files/l10n/id.js +++ b/apps/files/l10n/id.js @@ -42,10 +42,13 @@ OC.L10N.register( "Delete" : "Hapus", "Disconnect storage" : "Memutuskan penyimpaan", "Unshare" : "Batalkan berbagi", + "No permission to delete" : "Tidak memiliki hak untuk menghapus", "Download" : "Unduh", "Select" : "Pilih", - "Pending" : "Menunggu", + "Pending" : "Tertunda", "Unable to determine date" : "Tidak dapat menentukan tanggal", + "This operation is forbidden" : "Operasi ini dilarang", + "This directory is unavailable, please check the logs or contact the administrator" : "Direktori ini tidak tersedia, silakan periksa log atau hubungi kontak", "Error moving file." : "Kesalahan saat memindahkan berkas.", "Error moving file" : "Kesalahan saat memindahkan berkas", "Error" : "Kesalahan ", @@ -61,7 +64,9 @@ OC.L10N.register( "_Uploading %n file_::_Uploading %n files_" : ["Mengunggah %n berkas"], "\"{name}\" is an invalid file name." : "\"{name}\" adalah nama berkas yang tidak sah.", "File name cannot be empty." : "Nama berkas tidak boleh kosong.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "Penyimpanan {owner} penuh, berkas tidak dapat diperbarui atau disinkronisasikan lagi!", "Your storage is full, files can not be updated or synced anymore!" : "Ruang penyimpanan Anda penuh, berkas tidak dapat diperbarui atau disinkronkan lagi!", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "Penyimpanan {owner} hampir penuh ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "Ruang penyimpanan hampir penuh ({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["cocok dengan '{filter}'"], "{dirs} and {files}" : "{dirs} dan {files}", diff --git a/apps/files/l10n/id.json b/apps/files/l10n/id.json index c3906da3c3d..f2b557205b7 100644 --- a/apps/files/l10n/id.json +++ b/apps/files/l10n/id.json @@ -40,10 +40,13 @@ "Delete" : "Hapus", "Disconnect storage" : "Memutuskan penyimpaan", "Unshare" : "Batalkan berbagi", + "No permission to delete" : "Tidak memiliki hak untuk menghapus", "Download" : "Unduh", "Select" : "Pilih", - "Pending" : "Menunggu", + "Pending" : "Tertunda", "Unable to determine date" : "Tidak dapat menentukan tanggal", + "This operation is forbidden" : "Operasi ini dilarang", + "This directory is unavailable, please check the logs or contact the administrator" : "Direktori ini tidak tersedia, silakan periksa log atau hubungi kontak", "Error moving file." : "Kesalahan saat memindahkan berkas.", "Error moving file" : "Kesalahan saat memindahkan berkas", "Error" : "Kesalahan ", @@ -59,7 +62,9 @@ "_Uploading %n file_::_Uploading %n files_" : ["Mengunggah %n berkas"], "\"{name}\" is an invalid file name." : "\"{name}\" adalah nama berkas yang tidak sah.", "File name cannot be empty." : "Nama berkas tidak boleh kosong.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "Penyimpanan {owner} penuh, berkas tidak dapat diperbarui atau disinkronisasikan lagi!", "Your storage is full, files can not be updated or synced anymore!" : "Ruang penyimpanan Anda penuh, berkas tidak dapat diperbarui atau disinkronkan lagi!", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "Penyimpanan {owner} hampir penuh ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "Ruang penyimpanan hampir penuh ({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["cocok dengan '{filter}'"], "{dirs} and {files}" : "{dirs} dan {files}", diff --git a/apps/files/l10n/ja.js b/apps/files/l10n/ja.js index 154fc6d2106..5a0b4247e93 100644 --- a/apps/files/l10n/ja.js +++ b/apps/files/l10n/ja.js @@ -64,7 +64,9 @@ OC.L10N.register( "_Uploading %n file_::_Uploading %n files_" : ["%n 個のファイルをアップロード中"], "\"{name}\" is an invalid file name." : "\"{name}\" は無効なファイル名です。", "File name cannot be empty." : "ファイル名を空にすることはできません。", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner} のストレージは一杯です。ファイルの更新と同期はもうできません!", "Your storage is full, files can not be updated or synced anymore!" : "あなたのストレージは一杯です。ファイルの更新と同期はもうできません!", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner} のストレージはほぼ一杯です。({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "ストレージがほぼ一杯です({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : [" '{filter}' にマッチ"], "{dirs} and {files}" : "{dirs} と {files}", diff --git a/apps/files/l10n/ja.json b/apps/files/l10n/ja.json index e82b9dfc986..bcc9c1c77a1 100644 --- a/apps/files/l10n/ja.json +++ b/apps/files/l10n/ja.json @@ -62,7 +62,9 @@ "_Uploading %n file_::_Uploading %n files_" : ["%n 個のファイルをアップロード中"], "\"{name}\" is an invalid file name." : "\"{name}\" は無効なファイル名です。", "File name cannot be empty." : "ファイル名を空にすることはできません。", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner} のストレージは一杯です。ファイルの更新と同期はもうできません!", "Your storage is full, files can not be updated or synced anymore!" : "あなたのストレージは一杯です。ファイルの更新と同期はもうできません!", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner} のストレージはほぼ一杯です。({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "ストレージがほぼ一杯です({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : [" '{filter}' にマッチ"], "{dirs} and {files}" : "{dirs} と {files}", diff --git a/apps/files/l10n/ko.js b/apps/files/l10n/ko.js index b49fcd33bc5..a29ef613da3 100644 --- a/apps/files/l10n/ko.js +++ b/apps/files/l10n/ko.js @@ -47,6 +47,8 @@ OC.L10N.register( "Select" : "선택", "Pending" : "대기 중", "Unable to determine date" : "날짜를 결정할 수 없음", + "This operation is forbidden" : "이 작업이 금지됨", + "This directory is unavailable, please check the logs or contact the administrator" : "디렉터리를 사용할 수 없습니다. 로그를 확인하거나 관리자에게 연락하십시오", "Error moving file." : "파일 이동 오류.", "Error moving file" : "파일 이동 오류", "Error" : "오류", @@ -62,8 +64,10 @@ OC.L10N.register( "_Uploading %n file_::_Uploading %n files_" : ["파일 %n개 업로드 중"], "\"{name}\" is an invalid file name." : "\"{name}\"은(는) 잘못된 파일 이름입니다.", "File name cannot be empty." : "파일 이름이 비어 있을 수 없습니다.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner}의 저장소가 가득 찼습니다. 파일을 더 이상 업데이트하거나 동기화할 수 없습니다!", "Your storage is full, files can not be updated or synced anymore!" : "저장 공간이 가득 찼습니다. 파일을 업데이트하거나 동기화할 수 없습니다!", - "Your storage is almost full ({usedSpacePercent}%)" : "저장 공간이 거의 가득 찼습니다 ({usedSpacePercent}%)", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner}의 저장 공간이 거의 가득 찼습니다({usedSpacePercent}%)", + "Your storage is almost full ({usedSpacePercent}%)" : "저장 공간이 거의 가득 찼습니다({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["'{filter}'와(과) 일치"], "{dirs} and {files}" : "{dirs} 그리고 {files}", "Favorited" : "책갈피에 추가됨", diff --git a/apps/files/l10n/ko.json b/apps/files/l10n/ko.json index b9611b185b5..fd001630161 100644 --- a/apps/files/l10n/ko.json +++ b/apps/files/l10n/ko.json @@ -45,6 +45,8 @@ "Select" : "선택", "Pending" : "대기 중", "Unable to determine date" : "날짜를 결정할 수 없음", + "This operation is forbidden" : "이 작업이 금지됨", + "This directory is unavailable, please check the logs or contact the administrator" : "디렉터리를 사용할 수 없습니다. 로그를 확인하거나 관리자에게 연락하십시오", "Error moving file." : "파일 이동 오류.", "Error moving file" : "파일 이동 오류", "Error" : "오류", @@ -60,8 +62,10 @@ "_Uploading %n file_::_Uploading %n files_" : ["파일 %n개 업로드 중"], "\"{name}\" is an invalid file name." : "\"{name}\"은(는) 잘못된 파일 이름입니다.", "File name cannot be empty." : "파일 이름이 비어 있을 수 없습니다.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner}의 저장소가 가득 찼습니다. 파일을 더 이상 업데이트하거나 동기화할 수 없습니다!", "Your storage is full, files can not be updated or synced anymore!" : "저장 공간이 가득 찼습니다. 파일을 업데이트하거나 동기화할 수 없습니다!", - "Your storage is almost full ({usedSpacePercent}%)" : "저장 공간이 거의 가득 찼습니다 ({usedSpacePercent}%)", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner}의 저장 공간이 거의 가득 찼습니다({usedSpacePercent}%)", + "Your storage is almost full ({usedSpacePercent}%)" : "저장 공간이 거의 가득 찼습니다({usedSpacePercent}%)", "_matches '{filter}'_::_match '{filter}'_" : ["'{filter}'와(과) 일치"], "{dirs} and {files}" : "{dirs} 그리고 {files}", "Favorited" : "책갈피에 추가됨", diff --git a/apps/files/l10n/lt_LT.js b/apps/files/l10n/lt_LT.js index 376e0d84f67..2106a985350 100644 --- a/apps/files/l10n/lt_LT.js +++ b/apps/files/l10n/lt_LT.js @@ -42,15 +42,19 @@ OC.L10N.register( "Delete" : "Ištrinti", "Disconnect storage" : "Atjungti saugyklą", "Unshare" : "Nebesidalinti", + "No permission to delete" : "Neturite leidimų ištrinti", "Download" : "Atsisiųsti", "Select" : "Pasirinkiti", "Pending" : "Laukiantis", "Unable to determine date" : "Nepavyksta nustatyti datos", + "This operation is forbidden" : "Ši operacija yra uždrausta", + "This directory is unavailable, please check the logs or contact the administrator" : "Katalogas nepasiekiamas, prašome peržiūrėti žurnalo įrašus arba susisiekti su administratoriumi", "Error moving file." : "Klaida perkeliant failą.", "Error moving file" : "Klaida perkeliant failą", "Error" : "Klaida", "Could not rename file" : "Neįmanoma pervadinti failo", "Error deleting file." : "Klaida trinant failą.", + "No entries in this folder match '{filter}'" : "Nėra įrašų šiame aplanko atitikmeniui „{filter}“", "Name" : "Pavadinimas", "Size" : "Dydis", "Modified" : "Pakeista", @@ -60,13 +64,18 @@ OC.L10N.register( "_Uploading %n file_::_Uploading %n files_" : ["Įkeliamas %n failas","Įkeliami %n failai","Įkeliama %n failų"], "\"{name}\" is an invalid file name." : "„{name}“ yra netinkamas failo pavadinime.", "File name cannot be empty." : "Failo pavadinimas negali būti tuščias.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner} saugykla yra pilna, failai daugiau nebegali būti atnaujinti arba sinchronizuojami!", "Your storage is full, files can not be updated or synced anymore!" : "Jūsų visa vieta serveryje užimta", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner} saugykla yra beveik pilna ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "Jūsų vieta serveryje beveik visa užimta ({usedSpacePercent}%)", + "_matches '{filter}'_::_match '{filter}'_" : ["atitikmuo „{filter}“","atitikmenys „{filter}“","atitikmenų „{filter}“"], "{dirs} and {files}" : "{dirs} ir {files}", "Favorited" : "Pažymėta mėgstamu", "Favorite" : "Mėgiamas", + "An error occurred while trying to update the tags" : "Bandant atnaujinti žymes įvyko klaida", "A new file or folder has been <strong>created</strong>" : "Naujas failas ar aplankas buvo <strong>sukurtas</strong>", "A file or folder has been <strong>changed</strong>" : "Failas ar aplankas buvo <strong>pakeistas</strong>", + "Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>" : "Riboti pranešimus apie sukūrimą ir pokyčius jūsų <strong>mėgiamuose failuose</strong> <em>(Tik srautas)</em>", "A file or folder has been <strong>deleted</strong>" : "Failas ar aplankas buvo <strong>ištrintas</strong>", "A file or folder has been <strong>restored</strong>" : "Failas ar aplankas buvo <strong>atkurtas</strong>", "You created %1$s" : "Jūs sukūrėte %1$s", @@ -85,6 +94,7 @@ OC.L10N.register( "Maximum upload size" : "Maksimalus įkeliamo failo dydis", "max. possible: " : "maks. galima:", "Save" : "Išsaugoti", + "Can not be edited from here due to insufficient permissions." : "Negali būti redaguojamas iš čia dėl leidimų trūkumo.", "Settings" : "Nustatymai", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Naudokite šį adresą, kad <a href=\"%s\" target=\"_blank\">pasiektumėte savo failus per WebDAV</a>", @@ -95,7 +105,9 @@ OC.L10N.register( "Folder" : "Katalogas", "Upload" : "Įkelti", "Cancel upload" : "Atšaukti siuntimą", + "No files in here" : "Čia nėra failų", "Upload some content or sync with your devices!" : "Įkelkite kokį nors turinį, arba sinchronizuokite su savo įrenginiais!", + "No entries found in this folder" : "Nerasta įrašų šiame aplanke", "Select all" : "Pažymėti viską", "Upload too large" : "Įkėlimui failas per didelis", "The files you are trying to upload exceed the maximum size for file uploads on this server." : "Bandomų įkelti failų dydis viršija maksimalų, kuris leidžiamas šiame serveryje", diff --git a/apps/files/l10n/lt_LT.json b/apps/files/l10n/lt_LT.json index b8e296a0d43..b4758b4a9bb 100644 --- a/apps/files/l10n/lt_LT.json +++ b/apps/files/l10n/lt_LT.json @@ -40,15 +40,19 @@ "Delete" : "Ištrinti", "Disconnect storage" : "Atjungti saugyklą", "Unshare" : "Nebesidalinti", + "No permission to delete" : "Neturite leidimų ištrinti", "Download" : "Atsisiųsti", "Select" : "Pasirinkiti", "Pending" : "Laukiantis", "Unable to determine date" : "Nepavyksta nustatyti datos", + "This operation is forbidden" : "Ši operacija yra uždrausta", + "This directory is unavailable, please check the logs or contact the administrator" : "Katalogas nepasiekiamas, prašome peržiūrėti žurnalo įrašus arba susisiekti su administratoriumi", "Error moving file." : "Klaida perkeliant failą.", "Error moving file" : "Klaida perkeliant failą", "Error" : "Klaida", "Could not rename file" : "Neįmanoma pervadinti failo", "Error deleting file." : "Klaida trinant failą.", + "No entries in this folder match '{filter}'" : "Nėra įrašų šiame aplanko atitikmeniui „{filter}“", "Name" : "Pavadinimas", "Size" : "Dydis", "Modified" : "Pakeista", @@ -58,13 +62,18 @@ "_Uploading %n file_::_Uploading %n files_" : ["Įkeliamas %n failas","Įkeliami %n failai","Įkeliama %n failų"], "\"{name}\" is an invalid file name." : "„{name}“ yra netinkamas failo pavadinime.", "File name cannot be empty." : "Failo pavadinimas negali būti tuščias.", + "Storage of {owner} is full, files can not be updated or synced anymore!" : "{owner} saugykla yra pilna, failai daugiau nebegali būti atnaujinti arba sinchronizuojami!", "Your storage is full, files can not be updated or synced anymore!" : "Jūsų visa vieta serveryje užimta", + "Storage of {owner} is almost full ({usedSpacePercent}%)" : "{owner} saugykla yra beveik pilna ({usedSpacePercent}%)", "Your storage is almost full ({usedSpacePercent}%)" : "Jūsų vieta serveryje beveik visa užimta ({usedSpacePercent}%)", + "_matches '{filter}'_::_match '{filter}'_" : ["atitikmuo „{filter}“","atitikmenys „{filter}“","atitikmenų „{filter}“"], "{dirs} and {files}" : "{dirs} ir {files}", "Favorited" : "Pažymėta mėgstamu", "Favorite" : "Mėgiamas", + "An error occurred while trying to update the tags" : "Bandant atnaujinti žymes įvyko klaida", "A new file or folder has been <strong>created</strong>" : "Naujas failas ar aplankas buvo <strong>sukurtas</strong>", "A file or folder has been <strong>changed</strong>" : "Failas ar aplankas buvo <strong>pakeistas</strong>", + "Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>" : "Riboti pranešimus apie sukūrimą ir pokyčius jūsų <strong>mėgiamuose failuose</strong> <em>(Tik srautas)</em>", "A file or folder has been <strong>deleted</strong>" : "Failas ar aplankas buvo <strong>ištrintas</strong>", "A file or folder has been <strong>restored</strong>" : "Failas ar aplankas buvo <strong>atkurtas</strong>", "You created %1$s" : "Jūs sukūrėte %1$s", @@ -83,6 +92,7 @@ "Maximum upload size" : "Maksimalus įkeliamo failo dydis", "max. possible: " : "maks. galima:", "Save" : "Išsaugoti", + "Can not be edited from here due to insufficient permissions." : "Negali būti redaguojamas iš čia dėl leidimų trūkumo.", "Settings" : "Nustatymai", "WebDAV" : "WebDAV", "Use this address to <a href=\"%s\" target=\"_blank\">access your Files via WebDAV</a>" : "Naudokite šį adresą, kad <a href=\"%s\" target=\"_blank\">pasiektumėte savo failus per WebDAV</a>", @@ -93,7 +103,9 @@ "Folder" : "Katalogas", "Upload" : "Įkelti", "Cancel upload" : "Atšaukti siuntimą", + "No files in here" : "Čia nėra failų", "Upload some content or sync with your devices!" : "Įkelkite kokį nors turinį, arba sinchronizuokite su savo įrenginiais!", + "No entries found in this folder" : "Nerasta įrašų šiame aplanke", "Select all" : "Pažymėti viską", "Upload too large" : "Įkėlimui failas per didelis", "The files you are trying to upload exceed the maximum size for file uploads on this server." : "Bandomų įkelti failų dydis viršija maksimalų, kuris leidžiamas šiame serveryje", diff --git a/apps/files/l10n/ro.js b/apps/files/l10n/ro.js index 16699e084f6..541ff6e444a 100644 --- a/apps/files/l10n/ro.js +++ b/apps/files/l10n/ro.js @@ -30,7 +30,7 @@ OC.L10N.register( "Favorites" : "Favorite", "Home" : "Acasă", "Unable to upload {filename} as it is a directory or has 0 bytes" : "Nu se poate încărca {filename} deoarece este un director sau are mărimea de 0 octeți", - "Total file size {size1} exceeds upload limit {size2}" : "Mărimea fișierului este {size1} ce depășește limita de incarcare de {size2}", + "Total file size {size1} exceeds upload limit {size2}" : "Mărimea fișierului este {size1} ce depășește limita de încărcare de {size2}", "Not enough free space, you are uploading {size1} but only {size2} is left" : "Spațiu liber insuficient, încărcați {size1} însă doar {size2} disponibil rămas", "Upload cancelled." : "Încărcare anulată.", "Could not get result from server." : "Nu se poate obține rezultatul de la server.", @@ -55,7 +55,7 @@ OC.L10N.register( "Modified" : "Modificat", "_%n folder_::_%n folders_" : ["%n director","%n directoare","%n directoare"], "_%n file_::_%n files_" : ["%n fișier","%n fișiere","%n fișiere"], - "You don’t have permission to upload or create files here" : "Nu aveti permisiunea de a incarca sau crea fisiere aici", + "You don’t have permission to upload or create files here" : "Nu aveți permisiunea de a încărca sau crea fișiere aici", "_Uploading %n file_::_Uploading %n files_" : ["Se încarcă %n fișier.","Se încarcă %n fișiere.","Se încarcă %n fișiere."], "\"{name}\" is an invalid file name." : "\"{name}\" este un nume de fișier nevalid.", "File name cannot be empty." : "Numele fișierului nu poate rămâne gol.", diff --git a/apps/files/l10n/ro.json b/apps/files/l10n/ro.json index d09af6ba759..6c4536d5357 100644 --- a/apps/files/l10n/ro.json +++ b/apps/files/l10n/ro.json @@ -28,7 +28,7 @@ "Favorites" : "Favorite", "Home" : "Acasă", "Unable to upload {filename} as it is a directory or has 0 bytes" : "Nu se poate încărca {filename} deoarece este un director sau are mărimea de 0 octeți", - "Total file size {size1} exceeds upload limit {size2}" : "Mărimea fișierului este {size1} ce depășește limita de incarcare de {size2}", + "Total file size {size1} exceeds upload limit {size2}" : "Mărimea fișierului este {size1} ce depășește limita de încărcare de {size2}", "Not enough free space, you are uploading {size1} but only {size2} is left" : "Spațiu liber insuficient, încărcați {size1} însă doar {size2} disponibil rămas", "Upload cancelled." : "Încărcare anulată.", "Could not get result from server." : "Nu se poate obține rezultatul de la server.", @@ -53,7 +53,7 @@ "Modified" : "Modificat", "_%n folder_::_%n folders_" : ["%n director","%n directoare","%n directoare"], "_%n file_::_%n files_" : ["%n fișier","%n fișiere","%n fișiere"], - "You don’t have permission to upload or create files here" : "Nu aveti permisiunea de a incarca sau crea fisiere aici", + "You don’t have permission to upload or create files here" : "Nu aveți permisiunea de a încărca sau crea fișiere aici", "_Uploading %n file_::_Uploading %n files_" : ["Se încarcă %n fișier.","Se încarcă %n fișiere.","Se încarcă %n fișiere."], "\"{name}\" is an invalid file name." : "\"{name}\" este un nume de fișier nevalid.", "File name cannot be empty." : "Numele fișierului nu poate rămâne gol.", diff --git a/apps/files/l10n/sr.js b/apps/files/l10n/sr.js index 7a173fe694b..060ac4d506e 100644 --- a/apps/files/l10n/sr.js +++ b/apps/files/l10n/sr.js @@ -47,6 +47,8 @@ OC.L10N.register( "Select" : "Изабери", "Pending" : "На чекању", "Unable to determine date" : "Не могу да одредим датум", + "This operation is forbidden" : "Ова радња је забрањена", + "This directory is unavailable, please check the logs or contact the administrator" : "Овај директоријум није доступан, проверите записе или контактирајте администратора", "Error moving file." : "Грешка при премештању фајла.", "Error moving file" : "Грешка при премештању фајла", "Error" : "Грешка", diff --git a/apps/files/l10n/sr.json b/apps/files/l10n/sr.json index f535f4fb013..69fefcaf2b2 100644 --- a/apps/files/l10n/sr.json +++ b/apps/files/l10n/sr.json @@ -45,6 +45,8 @@ "Select" : "Изабери", "Pending" : "На чекању", "Unable to determine date" : "Не могу да одредим датум", + "This operation is forbidden" : "Ова радња је забрањена", + "This directory is unavailable, please check the logs or contact the administrator" : "Овај директоријум није доступан, проверите записе или контактирајте администратора", "Error moving file." : "Грешка при премештању фајла.", "Error moving file" : "Грешка при премештању фајла", "Error" : "Грешка", diff --git a/apps/files/l10n/tr.js b/apps/files/l10n/tr.js index a92d45e2481..dbc91ba15dc 100644 --- a/apps/files/l10n/tr.js +++ b/apps/files/l10n/tr.js @@ -47,6 +47,8 @@ OC.L10N.register( "Select" : "Seç", "Pending" : "Bekliyor", "Unable to determine date" : "Tarih tespit edilemedi", + "This operation is forbidden" : "Bu işlem yasak", + "This directory is unavailable, please check the logs or contact the administrator" : "Bu dizine yazılamıyor, lütfen günlüğü kontrol edin veya yönetici ile iletişime geçin", "Error moving file." : "Dosya taşıma hatası.", "Error moving file" : "Dosya taşıma hatası", "Error" : "Hata", diff --git a/apps/files/l10n/tr.json b/apps/files/l10n/tr.json index 1eb5534accb..eb967e2e3ec 100644 --- a/apps/files/l10n/tr.json +++ b/apps/files/l10n/tr.json @@ -45,6 +45,8 @@ "Select" : "Seç", "Pending" : "Bekliyor", "Unable to determine date" : "Tarih tespit edilemedi", + "This operation is forbidden" : "Bu işlem yasak", + "This directory is unavailable, please check the logs or contact the administrator" : "Bu dizine yazılamıyor, lütfen günlüğü kontrol edin veya yönetici ile iletişime geçin", "Error moving file." : "Dosya taşıma hatası.", "Error moving file" : "Dosya taşıma hatası", "Error" : "Hata", diff --git a/apps/files/lib/activity.php b/apps/files/lib/activity.php index fff49ea4ea5..bf80d0cfd7c 100644 --- a/apps/files/lib/activity.php +++ b/apps/files/lib/activity.php @@ -30,6 +30,7 @@ use OCP\IL10N; use OCP\IURLGenerator; class Activity implements IExtension { + const APP_FILES = 'files'; const FILTER_FILES = 'files'; const FILTER_FAVORITES = 'files_favorites'; @@ -78,7 +79,7 @@ class Activity implements IExtension { * @return IL10N */ protected function getL10N($languageCode = null) { - return $this->languageFactory->get('files', $languageCode); + return $this->languageFactory->get(self::APP_FILES, $languageCode); } /** @@ -86,14 +87,21 @@ class Activity implements IExtension { * If no additional types are to be added false is to be returned * * @param string $languageCode - * @return array|false + * @return array|false Array "stringID of the type" => "translated string description for the setting" + * or Array "stringID of the type" => [ + * 'desc' => "translated string description for the setting" + * 'methods' => [self::METHOD_*], + * ] */ public function getNotificationTypes($languageCode) { $l = $this->getL10N($languageCode); return [ self::TYPE_SHARE_CREATED => (string) $l->t('A new file or folder has been <strong>created</strong>'), self::TYPE_SHARE_CHANGED => (string) $l->t('A file or folder has been <strong>changed</strong>'), - self::TYPE_FAVORITES => (string) $l->t('Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>'), + self::TYPE_FAVORITES => [ + 'desc' => (string) $l->t('Limit notifications about creation and changes to your <strong>favorite files</strong> <em>(Stream only)</em>'), + 'methods' => [self::METHOD_STREAM], + ], self::TYPE_SHARE_DELETED => (string) $l->t('A file or folder has been <strong>deleted</strong>'), self::TYPE_SHARE_RESTORED => (string) $l->t('A file or folder has been <strong>restored</strong>'), ]; @@ -107,7 +115,7 @@ class Activity implements IExtension { * @return array|false */ public function getDefaultTypes($method) { - if ($method === 'stream') { + if ($method === self::METHOD_STREAM) { $settings = array(); $settings[] = self::TYPE_SHARE_CREATED; $settings[] = self::TYPE_SHARE_CHANGED; @@ -132,29 +140,30 @@ class Activity implements IExtension { * @return string|false */ public function translate($app, $text, $params, $stripPath, $highlightParams, $languageCode) { - if ($app !== 'files') { + if ($app !== self::APP_FILES) { return false; } + $l = $this->getL10N($languageCode); switch ($text) { case 'created_self': - return (string) $this->l->t('You created %1$s', $params); + return (string) $l->t('You created %1$s', $params); case 'created_by': - return (string) $this->l->t('%2$s created %1$s', $params); + return (string) $l->t('%2$s created %1$s', $params); case 'created_public': - return (string) $this->l->t('%1$s was created in a public folder', $params); + return (string) $l->t('%1$s was created in a public folder', $params); case 'changed_self': - return (string) $this->l->t('You changed %1$s', $params); + return (string) $l->t('You changed %1$s', $params); case 'changed_by': - return (string) $this->l->t('%2$s changed %1$s', $params); + return (string) $l->t('%2$s changed %1$s', $params); case 'deleted_self': - return (string) $this->l->t('You deleted %1$s', $params); + return (string) $l->t('You deleted %1$s', $params); case 'deleted_by': - return (string) $this->l->t('%2$s deleted %1$s', $params); + return (string) $l->t('%2$s deleted %1$s', $params); case 'restored_self': - return (string) $this->l->t('You restored %1$s', $params); + return (string) $l->t('You restored %1$s', $params); case 'restored_by': - return (string) $this->l->t('%2$s restored %1$s', $params); + return (string) $l->t('%2$s restored %1$s', $params); default: return false; @@ -173,7 +182,7 @@ class Activity implements IExtension { * @return array|false */ function getSpecialParameterList($app, $text) { - if ($app === 'files') { + if ($app === self::APP_FILES) { switch ($text) { case 'created_self': case 'created_by': @@ -223,7 +232,7 @@ class Activity implements IExtension { * @return integer|false */ public function getGroupParameter($activity) { - if ($activity['app'] === 'files') { + if ($activity['app'] === self::APP_FILES) { switch ($activity['subject']) { case 'created_self': case 'created_by': @@ -309,7 +318,7 @@ class Activity implements IExtension { $user = $this->activityManager->getCurrentUserId(); // Display actions from all files if ($filter === self::FILTER_FILES) { - return ['`app` = ?', ['files']]; + return ['`app` = ?', [self::APP_FILES]]; } if (!$user) { @@ -323,7 +332,7 @@ class Activity implements IExtension { $favorites = $this->helper->getFavoriteFilePaths($user); } catch (\RuntimeException $e) { // Too many favorites, can not put them into one query anymore... - return ['`app` = ?', ['files']]; + return ['`app` = ?', [self::APP_FILES]]; } /* @@ -331,7 +340,7 @@ class Activity implements IExtension { * or `file` is a favorite or in a favorite folder */ $parameters = $fileQueryList = []; - $parameters[] = 'files'; + $parameters[] = self::APP_FILES; $fileQueryList[] = '(`type` <> ? AND `type` <> ?)'; $parameters[] = self::TYPE_SHARE_CREATED; @@ -346,7 +355,7 @@ class Activity implements IExtension { $parameters[] = $favorite . '/%'; } - $parameters[] = 'files'; + $parameters[] = self::APP_FILES; return [ ' CASE WHEN `app` = ? THEN (' . implode(' OR ', $fileQueryList) . ') ELSE `app` <> ? END ', @@ -363,6 +372,6 @@ class Activity implements IExtension { * @return bool */ protected function userSettingFavoritesOnly($user) { - return (bool) $this->config->getUserValue($user, 'activity', 'notify_stream_' . self::TYPE_FAVORITES, false); + return (bool) $this->config->getUserValue($user, 'activity', 'notify_' . self::METHOD_STREAM . '_' . self::TYPE_FAVORITES, false); } } diff --git a/apps/files/lib/capabilities.php b/apps/files/lib/capabilities.php index 05d12864dca..2e19283e4d6 100644 --- a/apps/files/lib/capabilities.php +++ b/apps/files/lib/capabilities.php @@ -1,7 +1,7 @@ <?php /** * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Morris Jobke <hey@morrisjobke.de> + * @author Roeland Jago Douma <roeland@famdouma.nl> * @author Tom Needham <tom@owncloud.com> * * @copyright Copyright (c) 2015, ownCloud, Inc. @@ -20,19 +20,28 @@ * along with this program. If not, see <http://www.gnu.org/licenses/> * */ - -namespace OCA\Files; -class Capabilities { - - public static function getCapabilities() { - return new \OC_OCS_Result(array( - 'capabilities' => array( - 'files' => array( - 'bigfilechunking' => true, - ), - ), - )); +namespace OCA\Files; + +use OCP\Capabilities\ICapability; + +/** + * Class Capabilities + * + * @package OCA\Files + */ +class Capabilities implements ICapability { + + /** + * Return this classes capabilities + * + * @return array + */ + public function getCapabilities() { + return [ + 'files' => [ + 'bigfilechunking' => true, + ], + ]; } - } diff --git a/apps/files/templates/admin.php b/apps/files/templates/admin.php index adf756a12be..822fc779bd3 100644 --- a/apps/files/templates/admin.php +++ b/apps/files/templates/admin.php @@ -10,6 +10,8 @@ <br/> <input type="hidden" value="<?php p($_['requesttoken']); ?>" name="requesttoken" /> <?php if($_['uploadChangable']): ?> + <?php p($l->t('With PHP-FPM this value may take up to 5 minutes to take effect after saving.')); ?> + <br/> <input type="submit" name="submitFilesAdminSettings" id="submitFilesAdminSettings" value="<?php p($l->t( 'Save' )); ?>"/> <?php else: ?> diff --git a/apps/files/templates/test.png b/apps/files/templates/test.png Binary files differnew file mode 100644 index 00000000000..2b90216f797 --- /dev/null +++ b/apps/files/templates/test.png diff --git a/apps/files/tests/activitytest.php b/apps/files/tests/activitytest.php index 4ab8ad11eae..cdb1d21bcd8 100644 --- a/apps/files/tests/activitytest.php +++ b/apps/files/tests/activitytest.php @@ -42,6 +42,9 @@ class ActivityTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $activityHelper; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $l10nFactory; + /** @var \OCA\Files\Activity */ protected $activityExtension; @@ -67,8 +70,28 @@ class ActivityTest extends TestCase { $this->config ); + $this->l10nFactory = $this->getMockBuilder('OC\L10N\Factory') + ->disableOriginalConstructor() + ->getMock(); + $deL10n = $this->getMockBuilder('OC_L10N') + ->disableOriginalConstructor() + ->getMock(); + $deL10n->expects($this->any()) + ->method('t') + ->willReturnCallback(function ($argument) { + return 'translate(' . $argument . ')'; + }); + + $this->l10nFactory->expects($this->any()) + ->method('get') + ->willReturnMap([ + ['files', null, new \OC_L10N('files', 'en')], + ['files', 'en', new \OC_L10N('files', 'en')], + ['files', 'de', $deL10n], + ]); + $this->activityExtension = $activityExtension = new Activity( - new \OC\L10N\Factory(), + $this->l10nFactory, $this->getMockBuilder('OCP\IURLGenerator')->disableOriginalConstructor()->getMock(), $this->activityManager, $this->activityHelper, @@ -111,6 +134,26 @@ class ActivityTest extends TestCase { $this->activityExtension->translate('files_sharing', '', [], false, false, 'en'), 'Asserting that no translations are set for files_sharing' ); + + // Test english + $this->assertNotFalse( + $this->activityExtension->translate('files', 'deleted_self', ['file'], false, false, 'en'), + 'Asserting that translations are set for files.deleted_self' + ); + $this->assertStringStartsWith( + 'You deleted ', + $this->activityExtension->translate('files', 'deleted_self', ['file'], false, false, 'en') + ); + + // Test translation + $this->assertNotFalse( + $this->activityExtension->translate('files', 'deleted_self', ['file'], false, false, 'de'), + 'Asserting that translations are set for files.deleted_self' + ); + $this->assertStringStartsWith( + 'translate(You deleted ', + $this->activityExtension->translate('files', 'deleted_self', ['file'], false, false, 'de') + ); } public function testGetSpecialParameterList() { diff --git a/apps/files/tests/js/detailsviewSpec.js b/apps/files/tests/js/detailsviewSpec.js new file mode 100644 index 00000000000..852f8b04293 --- /dev/null +++ b/apps/files/tests/js/detailsviewSpec.js @@ -0,0 +1,157 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library 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 library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +describe('OCA.Files.DetailsView tests', function() { + var detailsView; + + beforeEach(function() { + detailsView = new OCA.Files.DetailsView(); + }); + afterEach(function() { + detailsView.remove(); + detailsView = undefined; + }); + it('renders itself empty when nothing registered', function() { + detailsView.render(); + expect(detailsView.$el.find('.detailFileInfoContainer').length).toEqual(1); + expect(detailsView.$el.find('.tabsContainer').length).toEqual(1); + }); + describe('file info detail view', function() { + it('renders registered view', function() { + var testView = new OCA.Files.DetailFileInfoView(); + var testView2 = new OCA.Files.DetailFileInfoView(); + detailsView.addDetailView(testView); + detailsView.addDetailView(testView2); + detailsView.render(); + + expect(detailsView.$el.find('.detailFileInfoContainer .detailFileInfoView').length).toEqual(2); + }); + it('updates registered tabs when fileinfo is updated', function() { + var viewRenderStub = sinon.stub(OCA.Files.DetailFileInfoView.prototype, 'render'); + var testView = new OCA.Files.DetailFileInfoView(); + var testView2 = new OCA.Files.DetailFileInfoView(); + detailsView.addDetailView(testView); + detailsView.addDetailView(testView2); + detailsView.render(); + + var fileInfo = {id: 5, name: 'test.txt'}; + viewRenderStub.reset(); + detailsView.setFileInfo(fileInfo); + + expect(testView.getFileInfo()).toEqual(fileInfo); + expect(testView2.getFileInfo()).toEqual(fileInfo); + + expect(viewRenderStub.callCount).toEqual(2); + viewRenderStub.restore(); + }); + }); + describe('tabs', function() { + var testView, testView2; + + beforeEach(function() { + testView = new OCA.Files.DetailTabView({id: 'test1'}); + testView2 = new OCA.Files.DetailTabView({id: 'test2'}); + detailsView.addTabView(testView); + detailsView.addTabView(testView2); + detailsView.render(); + }); + it('initially renders only the selected tab', function() { + expect(detailsView.$el.find('.tab').length).toEqual(1); + expect(detailsView.$el.find('.tab').attr('id')).toEqual('test1'); + }); + it('updates tab model and rerenders on-demand as soon as it gets selected', function() { + var tab1RenderStub = sinon.stub(testView, 'render'); + var tab2RenderStub = sinon.stub(testView2, 'render'); + var fileInfo1 = new OCA.Files.FileInfoModel({id: 5, name: 'test.txt'}); + var fileInfo2 = new OCA.Files.FileInfoModel({id: 8, name: 'test2.txt'}); + + detailsView.setFileInfo(fileInfo1); + + // first tab renders, not the second one + expect(tab1RenderStub.calledOnce).toEqual(true); + expect(tab2RenderStub.notCalled).toEqual(true); + + // info got set only to the first visible tab + expect(testView.getFileInfo()).toEqual(fileInfo1); + expect(testView2.getFileInfo()).toBeUndefined(); + + // select second tab for first render + detailsView.$el.find('.tabHeader').eq(1).click(); + + // second tab got rendered + expect(tab2RenderStub.calledOnce).toEqual(true); + expect(testView2.getFileInfo()).toEqual(fileInfo1); + + // select the first tab again + detailsView.$el.find('.tabHeader').eq(0).click(); + + // no re-render + expect(tab1RenderStub.calledOnce).toEqual(true); + expect(tab2RenderStub.calledOnce).toEqual(true); + + tab1RenderStub.reset(); + tab2RenderStub.reset(); + + // switch to another file + detailsView.setFileInfo(fileInfo2); + + // only the visible tab was updated and rerendered + expect(tab1RenderStub.calledOnce).toEqual(true); + expect(testView.getFileInfo()).toEqual(fileInfo2); + + // second/invisible tab still has old info, not rerendered + expect(tab2RenderStub.notCalled).toEqual(true); + expect(testView2.getFileInfo()).toEqual(fileInfo1); + + // reselect the second one + detailsView.$el.find('.tabHeader').eq(1).click(); + + // second tab becomes visible, updated and rendered + expect(testView2.getFileInfo()).toEqual(fileInfo2); + expect(tab2RenderStub.calledOnce).toEqual(true); + + tab1RenderStub.restore(); + tab2RenderStub.restore(); + }); + it('selects the first tab by default', function() { + expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(true); + expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(false); + expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(false); + expect(detailsView.$el.find('.tab').eq(1).length).toEqual(0); + }); + it('switches the current tab when clicking on tab header', function() { + detailsView.$el.find('.tabHeader').eq(1).click(); + expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(false); + expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(true); + expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(true); + expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(false); + }); + it('does not render tab headers when only one tab exists', function() { + detailsView.remove(); + detailsView = new OCA.Files.DetailsView(); + testView = new OCA.Files.DetailTabView({id: 'test1'}); + detailsView.addTabView(testView); + detailsView.render(); + + expect(detailsView.$el.find('.tabHeader').length).toEqual(0); + }); + }); +}); diff --git a/apps/files/tests/js/favoritespluginspec.js b/apps/files/tests/js/favoritespluginspec.js index 90b40ede74b..1b144c28707 100644 --- a/apps/files/tests/js/favoritespluginspec.js +++ b/apps/files/tests/js/favoritespluginspec.js @@ -113,7 +113,7 @@ describe('OCA.Files.FavoritesPlugin tests', function() { shareOwner: 'user2' }]); - fileList.findFileEl('testdir').find('td a.name').click(); + fileList.findFileEl('testdir').find('td .nametext').click(); expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir'); diff --git a/apps/files/tests/js/fileactionsSpec.js b/apps/files/tests/js/fileactionsSpec.js index 53fa8707674..1254843e66a 100644 --- a/apps/files/tests/js/fileactionsSpec.js +++ b/apps/files/tests/js/fileactionsSpec.js @@ -20,220 +20,225 @@ */ describe('OCA.Files.FileActions tests', function() { - var $filesTable, fileList; - var FileActions; + var fileList, fileActions; beforeEach(function() { // init horrible parameters var $body = $('#testArea'); $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); $body.append('<input type="hidden" id="permissions" value="31"></input>'); + $body.append('<table id="filestable"><tbody id="fileList"></tbody></table>'); // dummy files table - $filesTable = $body.append('<table id="filestable"></table>'); - fileList = new OCA.Files.FileList($('#testArea')); - FileActions = new OCA.Files.FileActions(); - FileActions.registerDefaultActions(); + fileActions = new OCA.Files.FileActions(); + fileActions.registerAction({ + name: 'Testdropdown', + displayName: 'Testdropdowndisplay', + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: function () { + return OC.imagePath('core', 'actions/download'); + } + }); + + fileActions.registerAction({ + name: 'Testinline', + displayName: 'Testinlinedisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ + }); + + fileActions.registerAction({ + name: 'Testdefault', + displayName: 'Testdefaultdisplay', + mime: 'all', + permissions: OC.PERMISSION_READ + }); + fileActions.setDefault('all', 'Testdefault'); + fileList = new OCA.Files.FileList($body, { + fileActions: fileActions + }); }); afterEach(function() { - FileActions = null; + fileActions = null; fileList.destroy(); fileList = undefined; $('#dir, #permissions, #filestable').remove(); }); it('calling clear() clears file actions', function() { - FileActions.clear(); - expect(FileActions.actions).toEqual({}); - expect(FileActions.defaults).toEqual({}); - expect(FileActions.icons).toEqual({}); - expect(FileActions.currentFile).toBe(null); + fileActions.clear(); + expect(fileActions.actions).toEqual({}); + expect(fileActions.defaults).toEqual({}); + expect(fileActions.icons).toEqual({}); + expect(fileActions.currentFile).toBe(null); }); - it('calling display() sets file actions', function() { - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - - // note: FileActions.display() is called implicitly - var $tr = fileList.add(fileData); - - // actions defined after call - expect($tr.find('.action.action-download').length).toEqual(1); - expect($tr.find('.action.action-download').attr('data-action')).toEqual('Download'); - expect($tr.find('.nametext .action.action-rename').length).toEqual(1); - expect($tr.find('.nametext .action.action-rename').attr('data-action')).toEqual('Rename'); - expect($tr.find('.action.delete').length).toEqual(1); - }); - it('calling display() twice correctly replaces file actions', function() { - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - - FileActions.display($tr.find('td.filename'), true, fileList); - FileActions.display($tr.find('td.filename'), true, fileList); - - // actions defined after cal - expect($tr.find('.action.action-download').length).toEqual(1); - expect($tr.find('.nametext .action.action-rename').length).toEqual(1); - expect($tr.find('.action.delete').length).toEqual(1); - }); - it('redirects to download URL when clicking download', function() { - var redirectStub = sinon.stub(OC, 'redirect'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action-download').click(); - - expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toEqual( - OC.webroot + - '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fsubdir&files=testName.txt'); - redirectStub.restore(); - }); - it('takes the file\'s path into account when clicking download', function() { - var redirectStub = sinon.stub(OC, 'redirect'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/anotherpath/there', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action-download').click(); - - expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toEqual( - OC.webroot + '/index.php/apps/files/ajax/download.php' + - '?dir=%2Fanotherpath%2Fthere&files=testName.txt' - ); - redirectStub.restore(); - }); - it('deletes file when clicking delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - $tr.find('.action.delete').click(); - - expect(deleteStub.calledOnce).toEqual(true); - expect(deleteStub.getCall(0).args[0]).toEqual('testName.txt'); - expect(deleteStub.getCall(0).args[1]).toEqual('/somepath/dir'); - deleteStub.restore(); - }); - it('shows delete hint when no permission to delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456', - permissions: OC.PERMISSION_READ - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); + describe('displaying actions', function() { + var $tr; - var $action = $tr.find('.action.delete'); + beforeEach(function() { + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456', + permissions: OC.PERMISSION_READ | OC.PERMISSION_UPDATE + }; - expect($action.hasClass('no-permission')).toEqual(true); - deleteStub.restore(); - }); - it('shows delete hint not when permission to delete', function() { - var deleteStub = sinon.stub(fileList, 'do_delete'); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - path: '/somepath/dir', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456', - permissions: OC.PERMISSION_DELETE - }; - var $tr = fileList.add(fileData); - FileActions.display($tr.find('td.filename'), true, fileList); - - var $action = $tr.find('.action.delete'); - - expect($action.hasClass('no-permission')).toEqual(false); - deleteStub.restore(); + // note: FileActions.display() is called implicitly + $tr = fileList.add(fileData); + }); + it('renders inline file actions', function() { + // actions defined after call + expect($tr.find('.action.action-testinline').length).toEqual(1); + expect($tr.find('.action.action-testinline').attr('data-action')).toEqual('Testinline'); + }); + it('does not render dropdown actions', function() { + expect($tr.find('.action.action-testdropdown').length).toEqual(0); + }); + it('does not render default action', function() { + expect($tr.find('.action.action-testdefault').length).toEqual(0); + }); + it('replaces file actions when displayed twice', function() { + fileActions.display($tr.find('td.filename'), true, fileList); + fileActions.display($tr.find('td.filename'), true, fileList); + + expect($tr.find('.action.action-testinline').length).toEqual(1); + }); + it('renders actions menu trigger', function() { + expect($tr.find('.action.action-menu').length).toEqual(1); + expect($tr.find('.action.action-menu').attr('data-action')).toEqual('menu'); + }); + it('only renders actions relevant to the mime type', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_READ + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'application/octet-stream', + permissions: OC.PERMISSION_READ + }); + + fileActions.display($tr.find('td.filename'), true, fileList); + expect($tr.find('.action.action-match').length).toEqual(1); + expect($tr.find('.action.action-nomatch').length).toEqual(0); + }); + it('only renders actions relevant to the permissions', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_UPDATE + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'text/plain', + permissions: OC.PERMISSION_DELETE + }); + + fileActions.display($tr.find('td.filename'), true, fileList); + expect($tr.find('.action.action-match').length).toEqual(1); + expect($tr.find('.action.action-nomatch').length).toEqual(0); + }); }); - it('passes context to action handler', function() { - var actionStub = sinon.stub(); - var fileData = { - id: 18, - type: 'file', - name: 'testName.txt', - mimetype: 'text/plain', - size: '1234', - etag: 'a01234c', - mtime: '123456' - }; - var $tr = fileList.add(fileData); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.display($tr.find('td.filename'), true, fileList); - $tr.find('.action-test').click(); - expect(actionStub.calledOnce).toEqual(true); - expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); - var context = actionStub.getCall(0).args[1]; - expect(context.$file.is($tr)).toEqual(true); - expect(context.fileList).toBeDefined(); - expect(context.fileActions).toBeDefined(); - expect(context.dir).toEqual('/subdir'); - - // when data-path is defined - actionStub.reset(); - $tr.attr('data-path', '/somepath'); - $tr.find('.action-test').click(); - context = actionStub.getCall(0).args[1]; - expect(context.dir).toEqual('/somepath'); + describe('action handler', function() { + var actionStub, $tr, clock; + + beforeEach(function() { + clock = sinon.useFakeTimers(); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + actionStub = sinon.stub(); + fileActions.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + icon: OC.imagePath('core', 'actions/test'), + permissions: OC.PERMISSION_READ, + actionHandler: actionStub + }); + $tr = fileList.add(fileData); + }); + afterEach(function() { + OC.hideMenus(); + // jump past animations + clock.tick(1000); + clock.restore(); + }); + it('passes context to action handler', function() { + $tr.find('.action-test').click(); + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); + var context = actionStub.getCall(0).args[1]; + expect(context.$file.is($tr)).toEqual(true); + expect(context.fileList).toBeDefined(); + expect(context.fileActions).toBeDefined(); + expect(context.dir).toEqual('/subdir'); + expect(context.fileInfoModel.get('name')).toEqual('testName.txt'); + + // when data-path is defined + actionStub.reset(); + $tr.attr('data-path', '/somepath'); + $tr.find('.action-test').click(); + context = actionStub.getCall(0).args[1]; + expect(context.dir).toEqual('/somepath'); + }); + it('also triggers action handler when calling triggerAction()', function() { + var model = new OCA.Files.FileInfoModel({ + id: 1, + name: 'Test.txt', + path: '/subdir', + mime: 'text/plain', + permissions: 31 + }); + fileActions.triggerAction('Test', model, fileList); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('Test.txt'); + expect(actionStub.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionStub.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionStub.getCall(0).args[1].fileInfoModel).toEqual(model); + }); + describe('actions menu', function() { + it('shows actions menu inside row when clicking the menu trigger', function() { + expect($tr.find('td.filename .fileActionsMenu').length).toEqual(0); + $tr.find('.action-menu').click(); + expect($tr.find('td.filename .fileActionsMenu').length).toEqual(1); + }); + it('shows highlight on current row', function() { + $tr.find('.action-menu').click(); + expect($tr.hasClass('mouseOver')).toEqual(true); + }); + it('cleans up after hiding', function() { + var slideUpStub = sinon.stub($.fn, 'slideUp'); + $tr.find('.action-menu').click(); + expect($tr.find('.fileActionsMenu').length).toEqual(1); + OC.hideMenus(); + // sliding animation + expect(slideUpStub.calledOnce).toEqual(true); + slideUpStub.getCall(0).args[1](); + expect($tr.hasClass('mouseOver')).toEqual(false); + expect($tr.find('.fileActionsMenu').length).toEqual(0); + }); + }); }); describe('custom rendering', function() { var $tr; @@ -251,10 +256,11 @@ describe('OCA.Files.FileActions tests', function() { }); it('regular function', function() { var actionStub = sinon.stub(); - FileActions.registerAction({ + fileActions.registerAction({ name: 'Test', displayName: '', mime: 'all', + type: OCA.Files.FileActions.TYPE_INLINE, permissions: OC.PERMISSION_READ, render: function(actionSpec, isDefault, context) { expect(actionSpec.name).toEqual('Test'); @@ -266,13 +272,13 @@ describe('OCA.Files.FileActions tests', function() { expect(context.fileList).toEqual(fileList); expect(context.$file[0]).toEqual($tr[0]); - var $customEl = $('<a href="#"><span>blabli</span><span>blabla</span></a>'); + var $customEl = $('<a class="action action-test" href="#"><span>blabli</span><span>blabla</span></a>'); $tr.find('td:first').append($customEl); return $customEl; }, actionHandler: actionStub }); - FileActions.display($tr.find('td.filename'), true, fileList); + fileActions.display($tr.find('td.filename'), true, fileList); var $actionEl = $tr.find('td:first .action-test'); expect($actionEl.length).toEqual(1); @@ -306,20 +312,22 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); - actions2.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); + actions2.registerAction({ + name: 'Test2', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions2.merge(actions1); actions2.display($tr.find('td.filename'), true, fileList); @@ -342,20 +350,22 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); - actions2.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); + actions2.registerAction({ + name: 'Test', // override + mime: 'all', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions1.merge(actions2); actions1.display($tr.find('td.filename'), true, fileList); @@ -371,24 +381,26 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); actions1.merge(actions2); // late override - actions1.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', // override + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); actions1.display($tr.find('td.filename'), true, fileList); @@ -403,25 +415,27 @@ describe('OCA.Files.FileActions tests', function() { var actions2 = new OCA.Files.FileActions(); var actionStub1 = sinon.stub(); var actionStub2 = sinon.stub(); - actions1.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub1 - ); + actions1.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub1 + }); // copy the Test action to actions2 actions2.merge(actions1); // late override - actions2.register( - 'all', - 'Test', // override - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub2 - ); + actions2.registerAction({ + mime: 'all', + name: 'Test', // override + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub2 + }); // check if original actions still call the correct handler actions1.display($tr.find('td.filename'), true, fileList); @@ -444,42 +458,45 @@ describe('OCA.Files.FileActions tests', function() { it('notifies update event handlers once after multiple changes', function() { var actionStub = sinon.stub(); var handler = sinon.stub(); - FileActions.on('registerAction', handler); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); + fileActions.on('registerAction', handler); + fileActions.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); + fileActions.registerAction({ + mime: 'all', + name: 'Test2', + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); expect(handler.calledTwice).toEqual(true); }); it('does not notifies update event handlers after unregistering', function() { var actionStub = sinon.stub(); var handler = sinon.stub(); - FileActions.on('registerAction', handler); - FileActions.off('registerAction', handler); - FileActions.register( - 'all', - 'Test', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); - FileActions.register( - 'all', - 'Test2', - OC.PERMISSION_READ, - OC.imagePath('core', 'actions/test'), - actionStub - ); + fileActions.on('registerAction', handler); + fileActions.off('registerAction', handler); + fileActions.registerAction({ + mime: 'all', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); + fileActions.registerAction({ + mime: 'all', + name: 'Test2', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_READ, + icon: OC.imagePath('core', 'actions/test'), + actionHandler: actionStub + }); expect(handler.notCalled).toEqual(true); }); }); diff --git a/apps/files/tests/js/fileactionsmenuSpec.js b/apps/files/tests/js/fileactionsmenuSpec.js new file mode 100644 index 00000000000..0cfd12a2d04 --- /dev/null +++ b/apps/files/tests/js/fileactionsmenuSpec.js @@ -0,0 +1,273 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library 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 library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +describe('OCA.Files.FileActionsMenu tests', function() { + var fileList, fileActions, menu, actionStub, $tr; + + beforeEach(function() { + // init horrible parameters + var $body = $('#testArea'); + $body.append('<input type="hidden" id="dir" value="/subdir"></input>'); + $body.append('<input type="hidden" id="permissions" value="31"></input>'); + // dummy files table + actionStub = sinon.stub(); + fileActions = new OCA.Files.FileActions(); + fileList = new OCA.Files.FileList($body, { + fileActions: fileActions + }); + + fileActions.registerAction({ + name: 'Testdropdown', + displayName: 'Testdropdowndisplay', + mime: 'all', + permissions: OC.PERMISSION_READ, + icon: function () { + return OC.imagePath('core', 'actions/download'); + }, + actionHandler: actionStub + }); + + fileActions.registerAction({ + name: 'Testdropdownnoicon', + displayName: 'Testdropdowndisplaynoicon', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionStub + }); + + fileActions.registerAction({ + name: 'Testinline', + displayName: 'Testinlinedisplay', + type: OCA.Files.FileActions.TYPE_INLINE, + mime: 'all', + permissions: OC.PERMISSION_READ + }); + + fileActions.registerAction({ + name: 'Testdefault', + displayName: 'Testdefaultdisplay', + mime: 'all', + permissions: OC.PERMISSION_READ + }); + fileActions.setDefault('all', 'Testdefault'); + + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + $tr = fileList.add(fileData); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: fileList.getCurrentDirectory() + }; + menu = new OCA.Files.FileActionsMenu(); + menu.show(menuContext); + }); + afterEach(function() { + fileActions = null; + fileList.destroy(); + fileList = undefined; + menu.remove(); + $('#dir, #permissions, #filestable').remove(); + }); + + describe('rendering', function() { + it('renders dropdown actions in menu', function() { + var $action = menu.$el.find('a[data-action=Testdropdown]'); + expect($action.length).toEqual(1); + expect($action.find('img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/download')); + expect($action.find('.no-icon').length).toEqual(0); + + $action = menu.$el.find('a[data-action=Testdropdownnoicon]'); + expect($action.length).toEqual(1); + expect($action.find('img').length).toEqual(0); + expect($action.find('.no-icon').length).toEqual(1); + }); + it('does not render default actions', function() { + expect(menu.$el.find('a[data-action=Testdefault]').length).toEqual(0); + }); + it('does not render inline actions', function() { + expect(menu.$el.find('a[data-action=Testinline]').length).toEqual(0); + }); + it('only renders actions relevant to the mime type', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_READ + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + mime: 'application/octet-stream', + permissions: OC.PERMISSION_READ + }); + + menu.render(); + expect(menu.$el.find('a[data-action=Match]').length).toEqual(1); + expect(menu.$el.find('a[data-action=NoMatch]').length).toEqual(0); + }); + it('only renders actions relevant to the permissions', function() { + fileActions.registerAction({ + name: 'Match', + displayName: 'MatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_UPDATE + }); + fileActions.registerAction({ + name: 'Nomatch', + displayName: 'NoMatchDisplay', + mime: 'text/plain', + permissions: OC.PERMISSION_DELETE + }); + + menu.render(); + expect(menu.$el.find('a[data-action=Match]').length).toEqual(1); + expect(menu.$el.find('a[data-action=NoMatch]').length).toEqual(0); + }); + }); + + describe('action handler', function() { + it('calls action handler when clicking menu item', function() { + var $action = menu.$el.find('a[data-action=Testdropdown]'); + $action.click(); + + expect(actionStub.calledOnce).toEqual(true); + expect(actionStub.getCall(0).args[0]).toEqual('testName.txt'); + expect(actionStub.getCall(0).args[1].$file[0]).toEqual($tr[0]); + expect(actionStub.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionStub.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionStub.getCall(0).args[1].dir).toEqual('/subdir'); + }); + }); + describe('default actions from registerDefaultActions', function() { + beforeEach(function() { + fileActions.clear(); + fileActions.registerDefaultActions(); + }); + it('redirects to download URL when clicking download', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: fileList.getCurrentDirectory() + }; + menu = new OCA.Files.FileActionsMenu(); + menu.show(menuContext); + + menu.$el.find('.action-download').click(); + + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toContain( + OC.webroot + + '/index.php/apps/files/ajax/download.php' + + '?dir=%2Fsubdir&files=testName.txt'); + redirectStub.restore(); + }); + it('takes the file\'s path into account when clicking download', function() { + var redirectStub = sinon.stub(OC, 'redirect'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + path: '/anotherpath/there', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: '/anotherpath/there' + }; + menu = new OCA.Files.FileActionsMenu(); + menu.show(menuContext); + + menu.$el.find('.action-download').click(); + + expect(redirectStub.calledOnce).toEqual(true); + expect(redirectStub.getCall(0).args[0]).toContain( + OC.webroot + '/index.php/apps/files/ajax/download.php' + + '?dir=%2Fanotherpath%2Fthere&files=testName.txt' + ); + redirectStub.restore(); + }); + it('deletes file when clicking delete', function() { + var deleteStub = sinon.stub(fileList, 'do_delete'); + var fileData = { + id: 18, + type: 'file', + name: 'testName.txt', + path: '/somepath/dir', + mimetype: 'text/plain', + size: '1234', + etag: 'a01234c', + mtime: '123456' + }; + var $tr = fileList.add(fileData); + fileActions.display($tr.find('td.filename'), true, fileList); + + var menuContext = { + $file: $tr, + fileList: fileList, + fileActions: fileActions, + dir: '/somepath/dir' + }; + menu = new OCA.Files.FileActionsMenu(); + menu.show(menuContext); + + menu.$el.find('.action-delete').click(); + + expect(deleteStub.calledOnce).toEqual(true); + expect(deleteStub.getCall(0).args[0]).toEqual('testName.txt'); + expect(deleteStub.getCall(0).args[1]).toEqual('/somepath/dir'); + deleteStub.restore(); + }); + }); +}); + diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index b12ac2f2517..7ed60084fa9 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -77,8 +77,8 @@ describe('OCA.Files.FileList tests', function() { '<th id="headerName" class="hidden column-name">' + '<input type="checkbox" id="select_all_files" class="select-all">' + '<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' + - '<span class="selectedActions hidden">' + - '<a href class="download">Download</a>' + + '<span id="selectedActionsList" class="selectedActions hidden">' + + '<a href class="download"><img src="actions/download.svg">Download</a>' + '<a href class="delete-selected">Delete</a></span>' + '</th>' + '<th class="hidden column-size"><a class="columntitle" data-sort="size"><span class="sort-indicator"></span></a></th>' + @@ -456,19 +456,19 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.notCalled).toEqual(true); }); - it('shows spinner on files to be deleted', function() { + it('shows busy state on files to be deleted', function() { fileList.setFiles(testFiles); doDelete(); - expect(fileList.findFileEl('One.txt').find('.icon-loading-small:not(.icon-delete)').length).toEqual(1); - expect(fileList.findFileEl('Three.pdf').find('.icon-delete:not(.icon-loading-small)').length).toEqual(1); + expect(fileList.findFileEl('One.txt').hasClass('busy')).toEqual(true); + expect(fileList.findFileEl('Three.pdf').hasClass('busy')).toEqual(false); }); - it('shows spinner on all files when deleting all', function() { + it('shows busy state on all files when deleting all', function() { fileList.setFiles(testFiles); fileList.do_delete(); - expect(fileList.$fileList.find('tr .icon-loading-small:not(.icon-delete)').length).toEqual(4); + expect(fileList.$fileList.find('tr.busy').length).toEqual(4); }); it('updates summary when deleting last file', function() { var $summary; @@ -625,7 +625,7 @@ describe('OCA.Files.FileList tests', function() { doCancelRename(); expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); }); - it('Hides actions while rename in progress', function() { + it('Shows busy state while rename in progress', function() { var $tr; doRename(); @@ -634,8 +634,7 @@ describe('OCA.Files.FileList tests', function() { expect($tr.length).toEqual(1); expect(fileList.findFileEl('One.txt').length).toEqual(0); // file actions are hidden - expect($tr.find('.action').hasClass('hidden')).toEqual(true); - expect($tr.find('.fileactions').hasClass('hidden')).toEqual(true); + expect($tr.hasClass('busy')).toEqual(true); // input and form are gone expect(fileList.$fileList.find('input.filename').length).toEqual(0); @@ -697,7 +696,7 @@ describe('OCA.Files.FileList tests', function() { expect(fileList.findFileEl('One.txt').length).toEqual(1); expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) - .toEqual(OC.imagePath('core', 'filetypes/file.svg')); + .toEqual(OC.imagePath('core', 'filetypes/text.svg')); }); }); describe('Moving files', function() { @@ -816,7 +815,7 @@ describe('OCA.Files.FileList tests', function() { expect(notificationStub.getCall(0).args[0]).toEqual('Error while moving file'); expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail'))) - .toEqual(OC.imagePath('core', 'filetypes/file.svg')); + .toEqual(OC.imagePath('core', 'filetypes/text.svg')); }); }); describe('List rendering', function() { @@ -1161,7 +1160,8 @@ describe('OCA.Files.FileList tests', function() { var fileData = { type: 'file', name: 'test dir', - icon: OC.webroot + '/core/img/filetypes/application-pdf.svg' + icon: OC.webroot + '/core/img/filetypes/application-pdf.svg', + mimetype: 'application/pdf' }; var $tr = fileList.add(fileData); var $imgDiv = $tr.find('td.filename .thumbnail'); @@ -1606,7 +1606,7 @@ describe('OCA.Files.FileList tests', function() { fileList.findFileEl('One.txt').find('input:checkbox').click(); fileList.findFileEl('Three.pdf').find('input:checkbox').click(); fileList.findFileEl('somedir').find('input:checkbox').click(); - expect($summary.text()).toEqual('1 folder & 2 files'); + expect($summary.text()).toEqual('1 folder and 2 files'); }); it('Unselecting files hides selection summary', function() { var $summary = $('#headerName a.name>span:first'); @@ -1774,7 +1774,7 @@ describe('OCA.Files.FileList tests', function() { var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22One.txt%22%2C%22Three.pdf%22%2C%22somedir%22%5D'); + expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2Fsubdir&files=%5B%22One.txt%22%2C%22Three.pdf%22%2C%22somedir%22%5D'); redirectStub.restore(); }); it('Downloads root folder when all selected in root folder', function() { @@ -1783,7 +1783,7 @@ describe('OCA.Files.FileList tests', function() { var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files='); + expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files='); redirectStub.restore(); }); it('Downloads parent folder when all selected in subfolder', function() { @@ -1791,7 +1791,7 @@ describe('OCA.Files.FileList tests', function() { var redirectStub = sinon.stub(OC, 'redirect'); $('.selectedActions .download').click(); expect(redirectStub.calledOnce).toEqual(true); - expect(redirectStub.getCall(0).args[0]).toEqual(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=subdir'); + expect(redirectStub.getCall(0).args[0]).toContain(OC.webroot + '/index.php/apps/files/ajax/download.php?dir=%2F&files=subdir'); redirectStub.restore(); }); }); @@ -1869,8 +1869,13 @@ describe('OCA.Files.FileList tests', function() { }); }) }); - describe('File actions', function() { - it('Clicking on a file name will trigger default action', function() { + describe('Details sidebar', function() { + beforeEach(function() { + fileList.setFiles(testFiles); + }); + it('Clicking on a file row will trigger file action if no details view configured', function() { + fileList._detailsView = null; + var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView'); var actionStub = sinon.stub(); fileList.setFiles(testFiles); fileList.fileActions.register( @@ -1887,6 +1892,60 @@ describe('OCA.Files.FileList tests', function() { var $tr = fileList.findFileEl('One.txt'); $tr.find('td.filename>a.name').click(); expect(actionStub.calledOnce).toEqual(true); + expect(updateDetailsViewStub.notCalled).toEqual(true); + updateDetailsViewStub.restore(); + }); + it('Clicking on a file row will trigger details sidebar', function() { + fileList.fileActions.setDefault('text/plain', 'Test'); + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + expect($tr.hasClass('highlighted')).toEqual(true); + + expect(fileList._detailsView.getFileInfo().id).toEqual(1); + }); + it('Clicking outside to deselect a file row will trigger details sidebar', function() { + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + + fileList.$el.find('tfoot').click(); + + expect($tr.hasClass('highlighted')).toEqual(false); + expect(fileList._detailsView.getFileInfo()).toEqual(null); + }); + it('returns the currently selected model instance when calling getModelForFile', function() { + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename>a.name').click(); + + var model1 = fileList.getModelForFile('One.txt'); + var model2 = fileList.getModelForFile('One.txt'); + model1.set('test', true); + + // it's the same model + expect(model2).toEqual(model1); + + var model3 = fileList.getModelForFile($tr); + expect(model3).toEqual(model1); + }); + }); + describe('File actions', function() { + it('Clicking on a file name will trigger default action', function() { + var actionStub = sinon.stub(); + fileList.setFiles(testFiles); + fileList.fileActions.registerAction({ + mime: 'text/plain', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_ALL, + icon: function() { + // Specify icon for hitory button + return OC.imagePath('core','actions/history'); + }, + actionHandler: actionStub + }); + fileList.fileActions.setDefault('text/plain', 'Test'); + var $tr = fileList.findFileEl('One.txt'); + $tr.find('td.filename .nametext').click(); + expect(actionStub.calledOnce).toEqual(true); expect(actionStub.getCall(0).args[0]).toEqual('One.txt'); var context = actionStub.getCall(0).args[1]; expect(context.$file.is($tr)).toEqual(true); @@ -1913,16 +1972,17 @@ describe('OCA.Files.FileList tests', function() { fileList.$fileList.on('fileActionsReady', readyHandler); - fileList.fileActions.register( - 'text/plain', - 'Test', - OC.PERMISSION_ALL, - function() { + fileList.fileActions.registerAction({ + mime: 'text/plain', + name: 'Test', + type: OCA.Files.FileActions.TYPE_INLINE, + permissions: OC.PERMISSION_ALL, + icon: function() { // Specify icon for hitory button return OC.imagePath('core','actions/history'); }, - actionStub - ); + actionHandler: actionStub + }); var $tr = fileList.findFileEl('One.txt'); expect($tr.find('.action-test').length).toEqual(0); expect(readyHandler.notCalled).toEqual(true); @@ -2211,6 +2271,8 @@ describe('OCA.Files.FileList tests', function() { }); }); describe('Handeling errors', function () { + var redirectStub; + beforeEach(function () { redirectStub = sinon.stub(OC, 'redirect'); @@ -2236,4 +2298,36 @@ describe('OCA.Files.FileList tests', function() { expect(redirectStub.calledWith(OC.generateUrl('apps/files'))).toEqual(true); }); }); + describe('showFileBusyState', function() { + var $tr; + + beforeEach(function() { + fileList.setFiles(testFiles); + $tr = fileList.findFileEl('Two.jpg'); + }); + it('shows spinner on busy rows', function() { + fileList.showFileBusyState('Two.jpg', true); + expect($tr.hasClass('busy')).toEqual(true); + expect(OC.TestUtil.getImageUrl($tr.find('.thumbnail'))) + .toEqual(OC.imagePath('core', 'loading.gif')); + + fileList.showFileBusyState('Two.jpg', false); + expect($tr.hasClass('busy')).toEqual(false); + expect(OC.TestUtil.getImageUrl($tr.find('.thumbnail'))) + .toEqual(OC.imagePath('core', 'filetypes/image.svg')); + }); + it('accepts multiple input formats', function() { + _.each([ + 'Two.jpg', + ['Two.jpg'], + $tr, + [$tr] + ], function(testCase) { + fileList.showFileBusyState(testCase, true); + expect($tr.hasClass('busy')).toEqual(true); + fileList.showFileBusyState(testCase, false); + expect($tr.hasClass('busy')).toEqual(false); + }); + }); + }); }); diff --git a/apps/files/tests/js/mainfileinfodetailviewSpec.js b/apps/files/tests/js/mainfileinfodetailviewSpec.js new file mode 100644 index 00000000000..ca7384f6207 --- /dev/null +++ b/apps/files/tests/js/mainfileinfodetailviewSpec.js @@ -0,0 +1,175 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2015 Vincent Petry <pvince81@owncloud.com> +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library 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 library. If not, see <http://www.gnu.org/licenses/>. +* +*/ + +describe('OCA.Files.MainFileInfoDetailView tests', function() { + var view, tooltipStub, fileListMock, fileActions, fileList, testFileInfo; + + beforeEach(function() { + tooltipStub = sinon.stub($.fn, 'tooltip'); + fileListMock = sinon.mock(OCA.Files.FileList.prototype); + fileActions = new OCA.Files.FileActions(); + fileList = new OCA.Files.FileList($('<table></table>'), { + fileActions: fileActions + }); + view = new OCA.Files.MainFileInfoDetailView({ + fileList: fileList, + fileActions: fileActions + }); + testFileInfo = new OCA.Files.FileInfoModel({ + id: 5, + name: 'One.txt', + mimetype: 'text/plain', + permissions: 31, + path: '/subdir', + size: 123456789, + mtime: Date.UTC(2015, 6, 17, 1, 2, 0, 0) + }); + }); + afterEach(function() { + view.remove(); + view = undefined; + tooltipStub.restore(); + fileListMock.restore(); + + }); + describe('rendering', function() { + it('displays basic info', function() { + var clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3)); + var dateExpected = OC.Util.formatDate(Date(Date.UTC(2015, 6, 17, 1, 2, 0, 0))); + view.setFileInfo(testFileInfo); + expect(view.$el.find('.fileName').text()).toEqual('One.txt'); + expect(view.$el.find('.fileName').attr('title')).toEqual('One.txt'); + expect(view.$el.find('.size').text()).toEqual('117.7 MB'); + expect(view.$el.find('.size').attr('title')).toEqual('123456789 bytes'); + expect(view.$el.find('.date').text()).toEqual('a few seconds ago'); + expect(view.$el.find('.date').attr('title')).toEqual(dateExpected); + clock.restore(); + }); + it('displays favorite icon', function() { + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + view.setFileInfo(testFileInfo); + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/starred')); + + testFileInfo.set('tags', []); + view.setFileInfo(testFileInfo); + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + it('displays mime icon', function() { + // File + testFileInfo.set('mimetype', 'text/calendar'); + view.setFileInfo(testFileInfo); + + expect(view.$el.find('.thumbnail').css('background-image')) + .toContain('filetypes/text-calendar.svg'); + + // Folder + testFileInfo.set('mimetype', 'httpd/unix-directory'); + view.setFileInfo(testFileInfo); + + expect(view.$el.find('.thumbnail').css('background-image')) + .toContain('filetypes/folder.svg'); + }); + it('displays thumbnail', function() { + testFileInfo.set('mimetype', 'test/plain'); + view.setFileInfo(testFileInfo); + + var expectation = fileListMock.expects('lazyLoadPreview'); + expectation.once(); + + view.setFileInfo(testFileInfo); + + fileListMock.verify(); + }); + it('rerenders when changes are made on the model', function() { + view.setFileInfo(testFileInfo); + + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/starred')); + + testFileInfo.set('tags', []); + + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + it('unbinds change listener from model', function() { + view.setFileInfo(testFileInfo); + view.setFileInfo(new OCA.Files.FileInfoModel({ + id: 999, + name: 'test.txt', + path: '/' + })); + + // set value on old model + testFileInfo.set('tags', [OC.TAG_FAVORITE]); + + // no change + expect(view.$el.find('.favorite img').attr('src')) + .toEqual(OC.imagePath('core', 'actions/star')); + }); + }); + describe('events', function() { + it('triggers default action when clicking on the thumbnail', function() { + var actionHandler = sinon.stub(); + + fileActions.registerAction({ + name: 'Something', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionHandler + }); + fileActions.setDefault('text/plain', 'Something'); + + view.setFileInfo(testFileInfo); + + view.$el.find('.thumbnail').click(); + + expect(actionHandler.calledOnce).toEqual(true); + expect(actionHandler.getCall(0).args[0]).toEqual('One.txt'); + expect(actionHandler.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionHandler.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionHandler.getCall(0).args[1].fileInfoModel).toEqual(testFileInfo); + }); + it('triggers "Favorite" action when clicking on the star', function() { + var actionHandler = sinon.stub(); + + fileActions.registerAction({ + name: 'Favorite', + mime: 'all', + permissions: OC.PERMISSION_READ, + actionHandler: actionHandler + }); + + view.setFileInfo(testFileInfo); + + view.$el.find('.action-favorite').click(); + + expect(actionHandler.calledOnce).toEqual(true); + expect(actionHandler.getCall(0).args[0]).toEqual('One.txt'); + expect(actionHandler.getCall(0).args[1].fileList).toEqual(fileList); + expect(actionHandler.getCall(0).args[1].fileActions).toEqual(fileActions); + expect(actionHandler.getCall(0).args[1].fileInfoModel).toEqual(testFileInfo); + }); + }); +}); |