summaryrefslogtreecommitdiffstats
path: root/apps/files
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files')
-rw-r--r--apps/files/admin.php3
-rw-r--r--apps/files/ajax/download.php11
-rw-r--r--apps/files/ajax/mimeicon.php27
-rw-r--r--apps/files/appinfo/application.php9
-rw-r--r--apps/files/appinfo/remote.php56
-rw-r--r--apps/files/appinfo/routes.php11
-rw-r--r--apps/files/appinfo/update.php96
-rw-r--r--apps/files/appinfo/version2
-rw-r--r--apps/files/command/scan.php9
-rw-r--r--apps/files/controller/apicontroller.php2
-rw-r--r--apps/files/css/detailsView.css55
-rw-r--r--apps/files/css/files.css158
-rw-r--r--apps/files/css/mobile.css22
-rw-r--r--apps/files/index.php11
-rw-r--r--apps/files/js/detailfileinfoview.js56
-rw-r--r--apps/files/js/detailsview.js271
-rw-r--r--apps/files/js/detailtabview.js95
-rw-r--r--apps/files/js/fileactions.js418
-rw-r--r--apps/files/js/fileactionsmenu.js132
-rw-r--r--apps/files/js/fileinfomodel.js71
-rw-r--r--apps/files/js/filelist.js353
-rw-r--r--apps/files/js/files.js65
-rw-r--r--apps/files/js/jquery-visibility.js101
-rw-r--r--apps/files/js/mainfileinfodetailview.js147
-rw-r--r--apps/files/js/tagsplugin.js11
-rw-r--r--apps/files/l10n/hu_HU.js7
-rw-r--r--apps/files/l10n/hu_HU.json7
-rw-r--r--apps/files/l10n/id.js7
-rw-r--r--apps/files/l10n/id.json7
-rw-r--r--apps/files/l10n/ja.js2
-rw-r--r--apps/files/l10n/ja.json2
-rw-r--r--apps/files/l10n/ko.js6
-rw-r--r--apps/files/l10n/ko.json6
-rw-r--r--apps/files/l10n/lt_LT.js12
-rw-r--r--apps/files/l10n/lt_LT.json12
-rw-r--r--apps/files/l10n/ro.js4
-rw-r--r--apps/files/l10n/ro.json4
-rw-r--r--apps/files/l10n/sr.js2
-rw-r--r--apps/files/l10n/sr.json2
-rw-r--r--apps/files/l10n/tr.js2
-rw-r--r--apps/files/l10n/tr.json2
-rw-r--r--apps/files/lib/activity.php51
-rw-r--r--apps/files/lib/capabilities.php37
-rw-r--r--apps/files/templates/admin.php2
-rw-r--r--apps/files/templates/test.pngbin0 -> 2302 bytes
-rw-r--r--apps/files/tests/activitytest.php45
-rw-r--r--apps/files/tests/js/detailsviewSpec.js157
-rw-r--r--apps/files/tests/js/favoritespluginspec.js2
-rw-r--r--apps/files/tests/js/fileactionsSpec.js589
-rw-r--r--apps/files/tests/js/fileactionsmenuSpec.js273
-rw-r--r--apps/files/tests/js/filelistSpec.js146
-rw-r--r--apps/files/tests/js/mainfileinfodetailviewSpec.js175
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
new file mode 100644
index 00000000000..2b90216f797
--- /dev/null
+++ b/apps/files/templates/test.png
Binary files differ
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);
+ });
+ });
+});