Added favorites feature to the files app

This commit is contained in:
Vincent Petry 2014-11-18 18:53:45 +01:00
parent c6be491a89
commit a5bb66f4a7
18 changed files with 1146 additions and 8 deletions

View file

@ -11,6 +11,8 @@ namespace OCA\Files\Appinfo;
use OC\AppFramework\Utility\SimpleContainer;
use OCA\Files\Controller\ApiController;
use OCP\AppFramework\App;
use \OCA\Files\Service\TagService;
use \OCP\IContainer;
class Application extends App {
public function __construct(array $urlParams=array()) {
@ -21,10 +23,44 @@ class Application extends App {
/**
* Controllers
*/
$container->registerService('APIController', function (SimpleContainer $c) {
$container->registerService('APIController', function (IContainer $c) {
return new ApiController(
$c->query('AppName'),
$c->query('Request')
$c->query('Request'),
$c->query('TagService')
);
});
/**
* Core
*/
$container->registerService('L10N', function(IContainer $c) {
return $c->query('ServerContainer')->getL10N($c->query('AppName'));
});
/**
* Services
*/
$container->registerService('Tagger', function(IContainer $c) {
return $c->query('ServerContainer')->getTagManager()->load('files');
});
$container->registerService('TagService', function(IContainer $c) {
$homeFolder = $c->query('ServerContainer')->getUserFolder();
return new TagService(
$c->query('ServerContainer')->getUserSession(),
$c->query('Tagger'),
$homeFolder
);
});
/**
* Controllers
*/
$container->registerService('APIController', function (IContainer $c) {
return new ApiController(
$c->query('AppName'),
$c->query('Request'),
$c->query('TagService')
);
});
}

View file

@ -9,10 +9,31 @@
namespace OCA\Files\Appinfo;
$application = new Application();
$application->registerRoutes($this, array('routes' => array(
array('name' => 'API#getThumbnail', 'url' => '/api/v1/thumbnail/{x}/{y}/{file}', 'verb' => 'GET', 'requirements' => array('file' => '.+')),
)));
$application->registerRoutes(
$this,
array(
'routes' => array(
array(
'name' => 'API#getThumbnail',
'url' => '/api/v1/thumbnail/{x}/{y}/{file}',
'verb' => 'GET',
'requirements' => array('file' => '.+')
),
array(
'name' => 'API#updateFileTags',
'url' => '/api/v1/files/{path}',
'verb' => 'POST',
'requirements' => array('path' => '.+'),
),
array(
'name' => 'API#getFilesByTag',
'url' => '/api/v1/tags/{tagName}/files',
'verb' => 'GET',
'requirements' => array('tagName' => '.+'),
),
)
)
);
/** @var $this \OC\Route\Router */

View file

@ -12,13 +12,16 @@ use OCP\AppFramework\Http;
use OCP\AppFramework\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\DownloadResponse;
use OC\Preview;
use OCA\Files\Service\TagService;
class ApiController extends Controller {
public function __construct($appName, IRequest $request){
public function __construct($appName, IRequest $request, TagService $tagService){
parent::__construct($appName, $request);
$this->tagService = $tagService;
}
@ -49,4 +52,50 @@ class ApiController extends Controller {
}
}
/**
* Updates the info of the specified file path
* The passed tags are absolute, which means they will
* replace the actual tag selection.
*
* @NoAdminRequired
* @CORS
*
* @param string $path path
* @param array $tags array of tags
*/
public function updateFileTags($path, $tags = null) {
$result = array();
// if tags specified or empty array, update tags
if (!is_null($tags)) {
try {
$this->tagService->updateFileTags($path, $tags);
} catch (\OCP\Files\NotFoundException $e) {
return new DataResponse($e->getMessage(), Http::STATUS_NOT_FOUND);
}
$result['tags'] = $tags;
}
return new DataResponse($result, Http::STATUS_OK);
}
/**
* Returns a list of all files tagged with the given tag.
*
* @NoAdminRequired
* @CORS
*
* @param array $tagName tag name to filter by
*/
public function getFilesByTag($tagName) {
$files = array();
$fileInfos = $this->tagService->getFilesByTag($tagName);
foreach ($fileInfos as &$fileInfo) {
$file = \OCA\Files\Helper::formatFileInfo($fileInfo);
$parts = explode('/', dirname($fileInfo->getPath()), 4);
$file['path'] = '/' . $parts[3];
$file['tags'] = array($tagName);
$files[] = $file;
}
return new DataResponse(array('files' => $files), Http::STATUS_OK);
}
}

View file

@ -286,6 +286,9 @@ table td.filename .nametext {
max-width: 800px;
height: 100%;
}
#fileList.has-favorites td.filename a.name {
left: 50px;
}
table td.filename .nametext .innernametext {
text-overflow: ellipsis;
@ -417,6 +420,18 @@ table td.filename .uploadtext {
height: 50px;
}
#fileList tr td.filename .favorite {
display: inline-block;
float: left;
}
#fileList tr td.filename .action-favorite {
display: block;
float: left;
width: 30px;
line-height: 100%;
text-align: center;
}
#uploadsize-message,#delete-confirm { display:none; }
/* File actions */
@ -442,7 +457,7 @@ table td.filename .uploadtext {
padding: 17px 14px;
}
#fileList .action.action-share-notification span, #fileList a {
#fileList .action.action-share-notification span, #fileList a.name {
cursor: default !important;
}

View file

@ -20,6 +20,7 @@
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
use OCA\Files\Appinfo\Application;
// Check if we are a user
OCP\User::checkLoggedIn();
@ -38,8 +39,14 @@ OCP\Util::addscript('files', 'filesummary');
OCP\Util::addscript('files', 'breadcrumb');
OCP\Util::addscript('files', 'filelist');
\OCP\Util::addScript('files', 'favoritesfilelist');
\OCP\Util::addScript('files', 'tagsplugin');
\OCP\Util::addScript('files', 'favoritesplugin');
OCP\App::setActiveNavigationEntry('files_index');
$l = \OC::$server->getL10N('files');
$isIE8 = false;
preg_match('/MSIE (.*?);/', $_SERVER['HTTP_USER_AGENT'], $matches);
if (count($matches) > 0 && $matches[1] <= 9) {
@ -79,6 +86,16 @@ function sortNavigationItems($item1, $item2) {
return $item1['order'] - $item2['order'];
}
\OCA\Files\App::getNavigationManager()->add(
array(
"id" => 'favorites',
"appname" => 'files',
"script" => 'simplelist.php',
"order" => 50,
"name" => $l->t('Favorites')
)
);
$navItems = \OCA\Files\App::getNavigationManager()->getAll();
usort($navItems, 'sortNavigationItems');
$nav->assign('navigationItems', $navItems);

View file

@ -80,6 +80,8 @@
// refer to the one of the "files" view
window.FileList = this.fileList;
OC.Plugins.attach('OCA.Files.App', this);
this._setupEvents();
// trigger URL change event handlers
this._onPopState(urlParams);

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
// HACK: this piece needs to be loaded AFTER the files app (for unit tests)
$(document).ready(function() {
(function(OCA) {
/**
* @class OCA.Files.FavoritesFileList
* @augments OCA.Files.FavoritesFileList
*
* @classdesc Favorites file list.
* Displays the list of files marked as favorites
*
* @param $el container element with existing markup for the #controls
* and a table
* @param [options] map of options, see other parameters
*/
var FavoritesFileList = function($el, options) {
this.initialize($el, options);
};
FavoritesFileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Files.FavoritesFileList.prototype */ {
id: 'favorites',
appName: 'Favorites',
_clientSideSort: true,
_allowSelection: false,
/**
* @private
*/
initialize: function($el, options) {
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
if (this.initialized) {
return;
}
OC.Plugins.attach('OCA.Files.FavoritesFileList', this);
},
updateEmptyContent: function() {
var dir = this.getCurrentDirectory();
if (dir === '/') {
// root has special permissions
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
}
else {
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
}
},
getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
},
updateStorageStatistics: function() {
// no op because it doesn't have
// storage info like free space / used space
},
reload: function() {
var tagName = OC.TAG_FAVORITE;
this.showMask();
if (this._reloadCall) {
this._reloadCall.abort();
}
this._reloadCall = $.ajax({
url: OC.generateUrl('/apps/files/api/v1/tags/{tagName}/files', {tagName: tagName}),
type: 'GET',
dataType: 'json'
});
var callBack = this.reloadCallback.bind(this);
return this._reloadCall.then(callBack, callBack);
},
reloadCallback: function(result) {
delete this._reloadCall;
this.hideMask();
if (result.files) {
this.setFiles(result.files.sort(this._sortComparator));
}
else {
// TODO: error handling
}
}
});
OCA.Files.FavoritesFileList = FavoritesFileList;
})(OCA);
});

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function(OCA) {
/**
* @namespace OCA.Files.FavoritesPlugin
*
* Registers the favorites file list from the files app sidebar.
*/
OCA.Files.FavoritesPlugin = {
name: 'Favorites',
/**
* @type OCA.Files.FavoritesFileList
*/
favoritesFileList: null,
attach: function() {
var self = this;
$('#app-content-favorites').on('show.plugin-favorites', function(e) {
self.showFileList($(e.target));
});
$('#app-content-favorites').on('hide.plugin-favorites', function() {
self.hideFileList();
});
},
detach: function() {
if (this.favoritesFileList) {
this.favoritesFileList.destroy();
OCA.Files.fileActions.off('setDefault.plugin-favorites', this._onActionsUpdated);
OCA.Files.fileActions.off('registerAction.plugin-favorites', this._onActionsUpdated);
$('#app-content-favorites').off('.plugin-favorites');
this.favoritesFileList = null;
}
},
showFileList: function($el) {
if (!this.favoritesFileList) {
this.favoritesFileList = this._createFavoritesFileList($el);
}
return this.favoritesFileList;
},
hideFileList: function() {
if (this.favoritesFileList) {
this.favoritesFileList.$fileList.empty();
}
},
/**
* Creates the favorites file list.
*
* @param $el container for the file list
* @return {OCA.Files.FavoritesFileList} file list
*/
_createFavoritesFileList: function($el) {
var fileActions = this._createFileActions();
// register favorite list for sidebar section
return new OCA.Files.FavoritesFileList(
$el, {
fileActions: fileActions,
scrollContainer: $('#app-content')
}
);
},
_createFileActions: function() {
// inherit file actions from the files app
var fileActions = new OCA.Files.FileActions();
// note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions();
fileActions.merge(OCA.Files.fileActions);
if (!this._globalActionsInitialized) {
// in case actions are registered later
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
OCA.Files.fileActions.on('setDefault.plugin-favorites', this._onActionsUpdated);
OCA.Files.fileActions.on('registerAction.plugin-favorites', this._onActionsUpdated);
this._globalActionsInitialized = true;
}
// when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
OCA.Files.App.setActiveView('files', {silent: true});
OCA.Files.App.fileList.changeDirectory(context.$file.attr('data-path') + '/' + filename, true, true);
});
fileActions.setDefault('dir', 'Open');
return fileActions;
},
_onActionsUpdated: function(ev) {
if (ev.action) {
this.favoritesFileList.fileActions.registerAction(ev.action);
} else if (ev.defaultAction) {
this.favoritesFileList.fileActions.setDefault(
ev.defaultAction.mime,
ev.defaultAction.name
);
}
}
};
})(OCA);
OC.Plugins.register('OCA.Files.App', OCA.Files.FavoritesPlugin);

135
apps/files/js/tagsplugin.js Normal file
View file

@ -0,0 +1,135 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
(function(OCA) {
OCA.Files = OCA.Files || {};
/**
* @namespace OCA.Files.TagsPlugin
*
* Extends the file actions and file list to include a favorite action icon
* and addition "data-tags" and "data-favorite" attributes.
*/
OCA.Files.TagsPlugin = {
name: 'Tags',
allowedLists: [
'files',
'favorites'
],
_extendFileActions: function(fileActions) {
var self = this;
// register "star" action
fileActions.registerAction({
name: 'favorite',
displayName: 'Favorite',
mime: 'all',
permissions: OC.PERMISSION_READ,
render: function(actionSpec, isDefault, context) {
// TODO: use proper icon
var $file = context.$file;
var isFavorite = $file.data('favorite') === true;
var starState = isFavorite ? '&#x2605' : '&#x2606;';
var $icon = $(
'<a href="#" class="action action-favorite ' + (isFavorite ? 'permanent' : '') + '">' +
starState + '</a>'
);
$file.find('td:first>.favorite').prepend($icon);
return $icon;
},
actionHandler: function(fileName, context) {
var $actionEl = context.$file.find('.action-favorite');
var $file = context.$file;
var dir = context.dir || context.fileList.getCurrentDirectory();
var tags = $file.attr('data-tags');
if (_.isUndefined(tags)) {
tags = '';
}
tags = tags.split('|');
tags = _.without(tags, '');
var isFavorite = tags.indexOf(OC.TAG_FAVORITE) >= 0;
if (isFavorite) {
// remove tag from list
tags = _.without(tags, OC.TAG_FAVORITE);
} else {
tags.push(OC.TAG_FAVORITE);
}
if ($actionEl.hasClass('icon-loading')) {
// do nothing
return;
}
$actionEl.addClass('icon-loading permanent');
self.applyFileTags(
dir + '/' + fileName,
tags
).then(function() {
// TODO: read from result
$actionEl.removeClass('icon-loading');
$actionEl.html(isFavorite ? '&#x2606;' : '&#x2605;');
$actionEl.toggleClass('permanent', !isFavorite);
$file.attr('data-tags', tags.join('|'));
$file.attr('data-favorite', !isFavorite);
});
}
});
},
_extendFileList: function(fileList) {
// extend row prototype
fileList.$fileList.addClass('has-favorites');
var oldCreateRow = fileList._createRow;
fileList._createRow = function(fileData) {
var $tr = oldCreateRow.apply(this, arguments);
if (fileData.tags) {
$tr.attr('data-tags', fileData.tags.join('|'));
if (fileData.tags.indexOf(OC.TAG_FAVORITE) >= 0) {
$tr.attr('data-favorite', true);
}
}
$tr.find('td:first').prepend('<div class="favorite"></div>');
return $tr;
};
},
attach: function(fileList) {
if (this.allowedLists.indexOf(fileList.id) < 0) {
return;
}
this._extendFileActions(fileList.fileActions);
this._extendFileList(fileList);
},
/**
* Replaces the given files' tags with the specified ones.
*
* @param {String} fileName path to the file or folder to tag
* @param {Array.<String>} tagNames array of tag names
*/
applyFileTags: function(fileName, tagNames) {
var encodedPath = OC.encodePath(fileName);
while (encodedPath[0] === '/') {
encodedPath = encodedPath.substr(1);
}
return $.ajax({
url: OC.generateUrl('/apps/files/api/v1/files/') + encodedPath,
contentType: 'application/json',
data: JSON.stringify({
tags: tagNames || []
}),
dataType: 'json',
type: 'POST'
});
}
};
})(OCA);
OC.Plugins.register('OCA.Files.FileList', OCA.Files.TagsPlugin);

View file

@ -122,6 +122,9 @@ class Helper
$entry['size'] = $i['size'];
$entry['type'] = $i['type'];
$entry['etag'] = $i['etag'];
if (isset($i['tags'])) {
$entry['tags'] = $i['tags'];
}
if (isset($i['displayname_owner'])) {
$entry['shareOwner'] = $i['displayname_owner'];
}
@ -171,10 +174,32 @@ class Helper
*/
public static function getFiles($dir, $sortAttribute = 'name', $sortDescending = false) {
$content = \OC\Files\Filesystem::getDirectoryContent($dir);
$content = self::populateTags($content);
return self::sortFiles($content, $sortAttribute, $sortDescending);
}
/**
* Populate the result set with file tags
*
* @param array file list
* @return file list populated with tags
*/
public static function populateTags($fileList) {
$filesById = array();
foreach ($fileList as $fileData) {
$filesById[$fileData['fileid']] = $fileData;
}
$tagger = \OC::$server->getTagManager()->load('files');
$tags = $tagger->getTagsForObjects(array_keys($filesById));
if ($tags) {
foreach ($tags as $fileId => $fileTags) {
$filesById[$fileId]['tags'] = $fileTags;
}
}
return $fileList;
}
/**
* Sort the given file info array
*

View file

@ -0,0 +1,94 @@
<?php
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OCA\Files\Service;
/**
* Service class to manage tags on files.
*/
class TagService {
/**
* @var \OCP\IUserSession
*/
private $userSession;
/**
* @var \OCP\ITags
*/
private $tagger;
/**
* @var \OCP\Files\Folder
*/
private $homeFolder;
public function __construct(
\OCP\IUserSession $userSession,
\OCP\ITags $tagger,
\OCP\Files\Folder $homeFolder
) {
$this->userSession = $userSession;
$this->tagger = $tagger;
$this->homeFolder = $homeFolder;
}
/**
* Updates the tags of the specified file path.
* The passed tags are absolute, which means they will
* replace the actual tag selection.
*
* @param string $path path
* @param array $tags array of tags
* @return array list of tags
* @throws \OCP\NotFoundException if the file does not exist
*/
public function updateFileTags($path, $tags) {
$fileId = $this->homeFolder->get($path)->getId();
$currentTags = $this->tagger->getTagsForObjects(array($fileId));
if (!empty($currentTags)) {
$currentTags = current($currentTags);
}
$newTags = array_diff($tags, $currentTags);
foreach ($newTags as $tag) {
$this->tagger->tagAs($fileId, $tag);
}
$deletedTags = array_diff($currentTags, $tags);
foreach ($deletedTags as $tag) {
$this->tagger->unTag($fileId, $tag);
}
// TODO: re-read from tagger to make sure the
// list is up to date, in case of concurrent changes ?
return $tags;
}
/**
* Updates the tags of the specified file path.
* The passed tags are absolute, which means they will
* replace the actual tag selection.
*
* @param array $tagName tag name to filter by
* @return FileInfo[] list of matching files
* @throws \Exception if the tag does not exist
*/
public function getFilesByTag($tagName) {
$nodes = $this->homeFolder->searchByTag(
$tagName, $this->userSession->getUser()->getUId()
);
foreach ($nodes as &$node) {
$node = $node->getFileInfo();
}
return $nodes;
}
}

30
apps/files/simplelist.php Normal file
View file

@ -0,0 +1,30 @@
<?php
/**
* ownCloud - Simple files list
*
* @author Vincent Petry
* @copyright 2014 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/>.
*
*/
// Check if we are a user
OCP\User::checkLoggedIn();
// renders the controls and table headers template
$tmpl = new OCP\Template('files', 'simplelist', '');
$tmpl->printPage();

View file

@ -0,0 +1,36 @@
<div id="controls">
<div id="file_action_panel"></div>
</div>
<div id='notification'></div>
<div id="emptycontent" class="hidden"></div>
<input type="hidden" name="dir" value="" id="dir">
<table id="filestable">
<thead>
<tr>
<th id='headerName' class="hidden column-name">
<div id="headerName-container">
<a class="name sort columntitle" data-sort="name"><span><?php p($l->t( 'Name' )); ?></span><span class="sort-indicator"></span></a>
</div>
</th>
<th id="headerSize" class="hidden column-size">
<a class="size sort columntitle" data-sort="size"><span><?php p($l->t('Size')); ?></span><span class="sort-indicator"></span></a>
</th>
<th id="headerDate" class="hidden column-mtime">
<a id="modified" class="columntitle" data-sort="mtime"><span><?php p($l->t( 'Modified' )); ?></span><span class="sort-indicator"></span></a>
<span class="selectedActions"><a href="" class="delete-selected">
<?php p($l->t('Delete'))?>
<img class="svg" alt="<?php p($l->t('Delete'))?>"
src="<?php print_unescaped(OCP\image_path("core", "actions/delete.svg")); ?>" />
</a></span>
</th>
</tr>
</thead>
<tbody id="fileList">
</tbody>
<tfoot>
</tfoot>
</table>

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
describe('OCA.Files.FavoritesFileList tests', function() {
var fileList;
beforeEach(function() {
// init parameters and test table elements
$('#testArea').append(
'<div id="app-content-container">' +
// init horrible parameters
'<input type="hidden" id="dir" value="/"></input>' +
'<input type="hidden" id="permissions" value="31"></input>' +
// dummy controls
'<div id="controls">' +
' <div class="actions creatable"></div>' +
' <div class="notCreatable"></div>' +
'</div>' +
// dummy table
// TODO: at some point this will be rendered by the fileList class itself!
'<table id="filestable">' +
'<thead><tr>' +
'<th id="headerName" class="hidden column-name">' +
'<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' +
'</th>' +
'<th class="hidden column-mtime">' +
'<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' +
'</th>' +
'</tr></thead>' +
'<tbody id="fileList"></tbody>' +
'<tfoot></tfoot>' +
'</table>' +
'<div id="emptycontent">Empty content message</div>' +
'</div>'
);
});
afterEach(function() {
fileList.destroy();
fileList = undefined;
});
describe('loading file list', function() {
var response;
beforeEach(function() {
fileList = new OCA.Files.FavoritesFileList(
$('#app-content-container')
);
OCA.Files.FavoritesPlugin.attach(fileList);
fileList.reload();
/* jshint camelcase: false */
response = {
files: [{
id: 7,
name: 'test.txt',
path: '/somedir',
size: 123,
mtime: 11111000,
tags: [OC.TAG_FAVORITE],
permissions: OC.PERMISSION_ALL,
mimetype: 'text/plain'
}]
};
});
it('render files', function() {
var request;
expect(fakeServer.requests.length).toEqual(1);
request = fakeServer.requests[0];
expect(request.url).toEqual(
OC.generateUrl('apps/files/api/v1/tags/{tagName}/files', {tagName: OC.TAG_FAVORITE})
);
fakeServer.requests[0].respond(
200,
{ 'Content-Type': 'application/json' },
JSON.stringify(response)
);
var $rows = fileList.$el.find('tbody tr');
var $tr = $rows.eq(0);
expect($rows.length).toEqual(1);
expect($tr.attr('data-id')).toEqual('7');
expect($tr.attr('data-type')).toEqual('file');
expect($tr.attr('data-file')).toEqual('test.txt');
expect($tr.attr('data-path')).toEqual('/somedir');
expect($tr.attr('data-size')).toEqual('123');
expect(parseInt($tr.attr('data-permissions'), 10))
.toEqual(OC.PERMISSION_ALL);
expect($tr.attr('data-mime')).toEqual('text/plain');
expect($tr.attr('data-mtime')).toEqual('11111000');
expect($tr.find('a.name').attr('href')).toEqual(
OC.webroot +
'/index.php/apps/files/ajax/download.php' +
'?dir=%2Fsomedir&files=test.txt'
);
expect($tr.find('.nametext').text().trim()).toEqual('test.txt');
});
});
});

View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
describe('OCA.Files.FavoritesPlugin tests', function() {
var Plugin = OCA.Files.FavoritesPlugin;
var fileList;
beforeEach(function() {
$('#testArea').append(
'<div id="app-navigation">' +
'<ul><li data-id="files"><a>Files</a></li>' +
'<li data-id="sharingin"><a></a></li>' +
'<li data-id="sharingout"><a></a></li>' +
'</ul></div>' +
'<div id="app-content">' +
'<div id="app-content-files" class="hidden">' +
'</div>' +
'<div id="app-content-favorites" class="hidden">' +
'</div>' +
'</div>' +
'</div>'
);
OC.Plugins.attach('OCA.Files.App', Plugin);
fileList = Plugin.showFileList($('#app-content-favorites'));
});
afterEach(function() {
OC.Plugins.detach('OCA.Files.App', Plugin);
});
describe('initialization', function() {
it('inits favorites list on show', function() {
expect(fileList).toBeDefined();
});
});
describe('file actions', function() {
var oldLegacyFileActions;
beforeEach(function() {
oldLegacyFileActions = window.FileActions;
window.FileActions = new OCA.Files.FileActions();
});
afterEach(function() {
window.FileActions = oldLegacyFileActions;
});
it('provides default file actions', function() {
var fileActions = fileList.fileActions;
expect(fileActions.actions.all).toBeDefined();
expect(fileActions.actions.all.Delete).toBeDefined();
expect(fileActions.actions.all.Rename).toBeDefined();
expect(fileActions.actions.all.Download).toBeDefined();
expect(fileActions.defaults.dir).toEqual('Open');
});
it('provides custom file actions', function() {
var actionStub = sinon.stub();
// regular file action
OCA.Files.fileActions.register(
'all',
'RegularTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/shared'),
actionStub
);
Plugin.favoritesFileList = null;
fileList = Plugin.showFileList($('#app-content-favorites'));
expect(fileList.fileActions.actions.all.RegularTest).toBeDefined();
});
it('does not provide legacy file actions', function() {
var actionStub = sinon.stub();
// legacy file action
window.FileActions.register(
'all',
'LegacyTest',
OC.PERMISSION_READ,
OC.imagePath('core', 'actions/shared'),
actionStub
);
Plugin.favoritesFileList = null;
fileList = Plugin.showFileList($('#app-content-favorites'));
expect(fileList.fileActions.actions.all.LegacyTest).not.toBeDefined();
});
it('redirects to files app when opening a directory', function() {
var oldList = OCA.Files.App.fileList;
// dummy new list to make sure it exists
OCA.Files.App.fileList = new OCA.Files.FileList($('<table><thead></thead><tbody></tbody></table>'));
var setActiveViewStub = sinon.stub(OCA.Files.App, 'setActiveView');
// create dummy table so we can click the dom
var $table = '<table><thead></thead><tbody id="fileList"></tbody></table>';
$('#app-content-favorites').append($table);
Plugin.favoritesFileList = null;
fileList = Plugin.showFileList($('#app-content-favorites'));
fileList.setFiles([{
name: 'testdir',
type: 'dir',
path: '/somewhere/inside/subdir',
counterParts: ['user2'],
shareOwner: 'user2'
}]);
fileList.findFileEl('testdir').find('td a.name').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
expect(setActiveViewStub.calledOnce).toEqual(true);
expect(setActiveViewStub.calledWith('files')).toEqual(true);
setActiveViewStub.restore();
// restore old list
OCA.Files.App.fileList = oldList;
});
});
});

View file

@ -0,0 +1,84 @@
/*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
describe('OCA.Files.TagsPlugin tests', function() {
var fileList;
var testFiles;
beforeEach(function() {
var $content = $('<div id="content"></div>');
$('#testArea').append($content);
// dummy file list
var $div = $(
'<div>' +
'<table id="filestable">' +
'<thead></thead>' +
'<tbody id="fileList"></tbody>' +
'</table>' +
'</div>');
$('#content').append($div);
fileList = new OCA.Files.FileList($div);
OCA.Files.TagsPlugin.attach(fileList);
testFiles = [{
id: 1,
type: 'file',
name: 'One.txt',
path: '/subdir',
mimetype: 'text/plain',
size: 12,
permissions: OC.PERMISSION_ALL,
etag: 'abc',
shareOwner: 'User One',
isShareMountPoint: false,
tags: ['tag1', 'tag2']
}];
});
afterEach(function() {
fileList.destroy();
fileList = null;
});
describe('Favorites icon', function() {
it('renders favorite icon and extra data', function() {
var $action, $tr;
fileList.setFiles(testFiles);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-favorite');
expect($action.length).toEqual(1);
expect($action.hasClass('permanent')).toEqual(false);
expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2']);
expect($tr.attr('data-favorite')).not.toBeDefined();
});
it('renders permanent favorite icon and extra data', function() {
var $action, $tr;
testFiles[0].tags.push(OC.TAG_FAVORITE);
fileList.setFiles(testFiles);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-favorite');
expect($action.length).toEqual(1);
expect($action.hasClass('permanent')).toEqual(true);
expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', OC.TAG_FAVORITE]);
expect($tr.attr('data-favorite')).toEqual('true');
});
});
describe('Applying tags', function() {
it('sends request to server and updates icon', function() {
// TODO
fileList.setFiles(testFiles);
});
it('sends all tags to server when applyFileTags() is called ', function() {
// TODO
});
});
});

View file

@ -0,0 +1,121 @@
<?php
/**
* ownCloud
*
* @author Vincent Petry
* @copyright 2014 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/>.
*
*/
namespace OCA\Files;
use \OCA\Files\Service\TagService;
class TagServiceTest extends \Test\TestCase {
/**
* @var string
*/
private $user;
/**
* @var \OCP\Files\Folder
*/
private $root;
/**
* @var \OCA\Files\Service\TagService
*/
private $tagService;
/**
* @var \OCP\ITags
*/
private $tagger;
protected function setUp() {
parent::setUp();
$this->user = $this->getUniqueId('user');
\OC_User::createUser($this->user, 'test');
\OC_User::setUserId($this->user);
\OC_Util::setupFS($this->user);
/**
* @var \OCP\IUser
*/
$user = new \OC\User\User($this->user, null);
/**
* @var \OCP\IUserSession
*/
$userSession = $this->getMock('\OCP\IUserSession');
$userSession->expects($this->any())
->method('getUser')
->withAnyParameters()
->will($this->returnValue($user));
$this->root = \OC::$server->getUserFolder();
$this->tagger = \OC::$server->getTagManager()->load('files');
$this->tagService = new TagService(
$userSession,
$this->tagger,
$this->root
);
}
protected function tearDown() {
\OC_User::setUserId('');
\OC_User::deleteUser($this->user);
}
public function testUpdateFileTags() {
$tag1 = 'tag1';
$tag2 = 'tag2';
$subdir = $this->root->newFolder('subdir');
$testFile = $subdir->newFile('test.txt');
$testFile->putContent('test contents');
$fileId = $testFile->getId();
// set tags
$this->tagService->updateFileTags('subdir/test.txt', array($tag1, $tag2));
$this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag1));
$this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag2));
// remove tag
$result = $this->tagService->updateFileTags('subdir/test.txt', array($tag2));
$this->assertEquals(array(), $this->tagger->getIdsForTag($tag1));
$this->assertEquals(array($fileId), $this->tagger->getIdsForTag($tag2));
// clear tags
$result = $this->tagService->updateFileTags('subdir/test.txt', array());
$this->assertEquals(array(), $this->tagger->getIdsForTag($tag1));
$this->assertEquals(array(), $this->tagger->getIdsForTag($tag2));
// non-existing file
$caught = false;
try {
$this->tagService->updateFileTags('subdir/unexist.txt', array($tag1));
} catch (\OCP\Files\NotFoundException $e) {
$caught = true;
}
$this->assertTrue($caught);
$subdir->delete();
}
}

View file

@ -66,6 +66,7 @@ var OC={
PERMISSION_DELETE:8,
PERMISSION_SHARE:16,
PERMISSION_ALL:31,
TAG_FAVORITE: '_$!<Favorite>!$_',
/* jshint camelcase: false */
webroot:oc_webroot,
appswebroots:(typeof oc_appswebroots !== 'undefined') ? oc_appswebroots:false,
@ -211,6 +212,24 @@ var OC={
return OC.filePath(app,'img',file);
},
/**
* URI-Encodes a file path but keep the path slashes.
*
* @param path path
* @return encoded path
*/
encodePath: function(path) {
if (!path) {
return path;
}
var parts = path.split('/');
var result = [];
for (var i = 0; i < parts.length; i++) {
result.push(encodeURIComponent(parts[i]));
}
return result.join('/');
},
/**
* Load a script for the server and load it. If the script is already loaded,
* the event handler will be called directly