diff options
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | controller/foldercontroller.php | 24 | ||||
-rw-r--r-- | js/app/Run.js | 3 | ||||
-rw-r--r-- | js/build/app.js | 181 | ||||
-rw-r--r-- | js/controller/NavigationController.js | 7 | ||||
-rw-r--r-- | js/directive/NewsDraggable.js | 22 | ||||
-rw-r--r-- | js/directive/NewsDroppable.js | 34 | ||||
-rw-r--r-- | js/service/FeedResource.js | 106 | ||||
-rw-r--r-- | js/service/FolderResource.js | 84 | ||||
-rw-r--r-- | js/tests/unit/controller/NavigationControllerSpec.js | 13 | ||||
-rw-r--r-- | js/tests/unit/service/FeedResourceSpec.js | 103 | ||||
-rw-r--r-- | js/tests/unit/service/FolderResourceSpec.js | 133 | ||||
-rw-r--r-- | templates/main.php | 4 | ||||
-rw-r--r-- | templates/part.listfeed.php | 13 | ||||
-rw-r--r-- | templates/part.listfolder.php | 18 | ||||
-rw-r--r-- | tests/unit/controller/FolderControllerTest.php | 58 |
16 files changed, 711 insertions, 93 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 50b071302..3ca61814a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -32,7 +32,6 @@ $application->registerRoutes($this, ['routes' => [ ['name' => 'folder#rename', 'url' => '/folders/{folderId}/rename', 'verb' => 'POST'], ['name' => 'folder#read', 'url' => '/folders/{folderId}/read', 'verb' => 'POST'], ['name' => 'folder#open', 'url' => '/folders/{folderId}/open', 'verb' => 'POST'], - ['name' => 'folder#collapse', 'url' => '/folders/{folderId}/collapse', 'verb' => 'POST'], // feeds ['name' => 'feed#index', 'url' => '/feeds', 'verb' => 'GET'], diff --git a/controller/foldercontroller.php b/controller/foldercontroller.php index 1137a2528..576fee422 100644 --- a/controller/foldercontroller.php +++ b/controller/foldercontroller.php @@ -57,33 +57,15 @@ class FolderController extends Controller { } - private function setOpened($isOpened, $folderId) { - $this->folderService->open($folderId, $isOpened, $this->userId); - } - - - /** - * @NoAdminRequired - * - * @param int $folderId - */ - public function open($folderId) { - try { - $this->setOpened(true, $folderId); - } catch(ServiceNotFoundException $ex) { - return $this->error($ex, Http::STATUS_NOT_FOUND); - } - } - - /** * @NoAdminRequired * * @param int $folderId + * @param bool $open */ - public function collapse($folderId) { + public function open($folderId, $open) { try { - $this->setOpened(false, $folderId); + $this->folderService->open($folderId, $open, $this->userId); } catch(ServiceNotFoundException $ex) { return $this->error($ex, Http::STATUS_NOT_FOUND); } diff --git a/js/app/Run.js b/js/app/Run.js index e119f0464..cdb2ba6d0 100644 --- a/js/app/Run.js +++ b/js/app/Run.js @@ -54,8 +54,7 @@ app.run(($rootScope, $location, $http, $q, $interval, Loading, ItemResource, } // only redirect if url is empty or faulty - // TODO check for faulty url - if (path === '') { + if (!/^\/items(\/(starred|feeds\/\d+|folders\/\d+))?$/.test(path)) { $location.path(url); } diff --git a/js/build/app.js b/js/build/app.js index d6355a108..c482c3fb4 100644 --- a/js/build/app.js +++ b/js/build/app.js @@ -132,7 +132,7 @@ var $__build_47_app__ = function () { default: url = '/items'; } - if (path === '') { + if (!/^\/items(\/(starred|feeds\/\d+|folders\/\d+))?$/.test(path)) { $location.path(url); } activeFeedDeferred.resolve(); @@ -299,6 +299,8 @@ var $__build_47_app__ = function () { 'SettingsResource', function (FeedResource, FolderResource, ItemResource, SettingsResource) { 'use strict'; + this.feedError = ''; + this.folderError = ''; this.getFeeds = function () { return FeedResource.getAll(); }; @@ -329,6 +331,9 @@ var $__build_47_app__ = function () { this.isShowAll = function () { return SettingsResource.get('showAll'); }; + this.getFeedsOfFolder = function (folderId) { + return FeedResource.getByFolderId(folderId); + }; this.createFeed = function () { console.log('TBD'); }; @@ -418,12 +423,15 @@ var $__build_47_app__ = function () { this.ids = {}; this.unreadCount = 0; this.folderUnreadCount = {}; + this.folderIds = {}; + this.deleted = null; }; var $FeedResource = FeedResource; $traceurRuntime.createClass(FeedResource, { receive: function (data) { $traceurRuntime.superCall(this, $FeedResource.prototype, 'receive', [data]); this.updateUnreadCache(); + this.updateFolderCache(); }, updateUnreadCache: function () { var $__14, $__15, $__16, $__17, $__18; @@ -446,16 +454,34 @@ var $__build_47_app__ = function () { } } }, + updateFolderCache: function () { + this.folderIds = {}; + for (var $__3 = this.values[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) { + try { + throw undefined; + } catch (feed) { + feed = $__4.value; + { + $traceurRuntime.setProperty(this.folderIds, feed.folderId, this.folderIds[$traceurRuntime.toProperty(feed.folderId)] || []); + this.folderIds[$traceurRuntime.toProperty(feed.folderId)].push(feed); + } + } + } + }, add: function (value) { $traceurRuntime.superCall(this, $FeedResource.prototype, 'add', [value]); if (value.id !== undefined) { $traceurRuntime.setProperty(this.ids, value.id, this.hashMap[$traceurRuntime.toProperty(value.url)]); } }, - delete: function (id) { - var feed = this.get(id); + delete: function (url) { + var feed = this.get(url); + this.deleted = feed; delete this.ids[$traceurRuntime.toProperty(feed.id)]; - $traceurRuntime.superCall(this, $FeedResource.prototype, 'delete', [id]); + $traceurRuntime.superCall(this, $FeedResource.prototype, 'delete', [url]); + this.updateUnreadCache(); + this.updateFolderCache(); + return this.http.delete(this.BASE_URL + '/feeds/' + feed.id); }, markRead: function () { for (var $__3 = this.values[$traceurRuntime.toProperty(Symbol.iterator)](), $__4; !($__4 = $__3.next()).done;) { @@ -518,12 +544,61 @@ var $__build_47_app__ = function () { return this.folderUnreadCount[$traceurRuntime.toProperty(folderId)] || 0; }, getByFolderId: function (folderId) { - return this.values.filter(function (v) { - return v.folderId === folderId; - }); + return this.folderIds[$traceurRuntime.toProperty(folderId)] || []; }, getById: function (feedId) { return this.ids[$traceurRuntime.toProperty(feedId)]; + }, + rename: function (url, name) { + var feed = this.get(url); + feed.title = name; + return this.http({ + method: 'POST', + url: this.BASE_URL + '/feeds/' + feed.id + '/rename', + data: { feedTitle: name } + }); + }, + move: function (url, folderId) { + var feed = this.get(url); + feed.folderId = folderId; + this.updateFolderCache(); + return this.http({ + method: 'POST', + url: this.BASE_URL + '/feeds/' + feed.id + '/move', + data: { parentFolderId: folderId } + }); + }, + create: function (url, folderId) { + var title = arguments[2] !== void 0 ? arguments[2] : null; + if (title) { + title = title.toUpperCase(); + } + var feed = { + url: url, + folderId: folderId, + title: title + }; + if (!this.get(url)) { + this.add(feed); + } + this.updateFolderCache(); + return this.http({ + method: 'POST', + url: this.BASE_URL + '/feeds', + data: { + url: url, + parentFolderId: folderId, + title: title + } + }); + }, + undoDelete: function () { + if (this.deleted) { + this.add(this.deleted); + return this.http.post(this.BASE_URL + '/feeds/' + this.deleted.id + '/restore'); + } + this.updateFolderCache(); + this.updateUnreadCache(); } }, {}, Resource); return new FeedResource($http, BASE_URL); @@ -541,9 +616,65 @@ var $__build_47_app__ = function () { BASE_URL, 'name' ]); + this.deleted = null; }; var $FolderResource = FolderResource; - $traceurRuntime.createClass(FolderResource, {}, {}, Resource); + $traceurRuntime.createClass(FolderResource, { + delete: function (folderName) { + var folder = this.get(folderName); + this.deleted = folder; + $traceurRuntime.superCall(this, $FolderResource.prototype, 'delete', [folderName]); + return this.http.delete(this.BASE_URL + '/folders/' + folder.id); + }, + toggleOpen: function (folderName) { + var folder = this.get(folderName); + folder.opened = !folder.opened; + return this.http({ + url: this.BASE_URL + '/folders/' + folder.id + '/open', + method: 'POST', + data: { + folderId: folder.id, + open: folder.opened + } + }); + }, + rename: function (folderName, toFolderName) { + toFolderName = toFolderName.toUpperCase(); + var folder = this.get(folderName); + if (!this.get(toFolderName)) { + folder.name = toFolderName; + delete this.hashMap[$traceurRuntime.toProperty(folderName)]; + $traceurRuntime.setProperty(this.hashMap, toFolderName, folder); + } + return this.http({ + url: this.BASE_URL + '/folders/' + folder.id + '/rename', + method: 'POST', + data: { folderName: toFolderName } + }); + }, + create: function (folderName) { + folderName = folderName.toUpperCase(); + if (!this.get(folderName)) { + try { + throw undefined; + } catch (folder) { + folder = { name: folderName }; + this.add(folder); + } + } + return this.http({ + url: this.BASE_URL + '/folders', + method: 'POST', + data: { folderName: folderName } + }); + }, + undoDelete: function () { + if (this.deleted) { + this.add(this.deleted); + return this.http.post(this.BASE_URL + '/folders/' + this.deleted.id + '/restore'); + } + } + }, {}, Resource); return new FolderResource($http, BASE_URL); } ]); @@ -1383,6 +1514,40 @@ var $__build_47_app__ = function () { }); }; }); + app.directive('newsDraggable', function () { + 'use strict'; + return function (scope, elem, attr) { + var options = scope.$eval(attr.newsDraggable); + if (angular.isDefined(options)) { + elem.draggable(options); + } else { + elem.draggable(); + } + }; + }); + app.directive('newsDroppable', [ + '$rootScope', + function ($rootScope) { + 'use strict'; + return function (scope, elem, attr) { + var details = { + accept: '.feed', + hoverClass: 'drag-and-drop', + greedy: true, + drop: function (event, ui) { + $('.drag-and-drop').removeClass('drag-and-drop'); + var data = { + folderId: parseInt(elem.data('id'), 10), + feedId: parseInt($(ui.draggable).data('id'), 10) + }; + $rootScope.$broadcast('moveFeedToFolder', data); + scope.$apply(attr.droppable); + } + }; + elem.droppable(details); + }; + } + ]); app.directive('newsReadFile', function () { 'use strict'; return function (scope, elem, attr) { diff --git a/js/controller/NavigationController.js b/js/controller/NavigationController.js index 08443191b..e6cb14a93 100644 --- a/js/controller/NavigationController.js +++ b/js/controller/NavigationController.js @@ -11,6 +11,9 @@ app.controller('NavigationController', function (FeedResource, FolderResource, ItemResource, SettingsResource) { 'use strict'; + this.feedError = ''; + this.folderError = ''; + this.getFeeds = () => { return FeedResource.getAll(); }; @@ -41,6 +44,10 @@ function (FeedResource, FolderResource, ItemResource, SettingsResource) { return SettingsResource.get('showAll'); }; + this.getFeedsOfFolder = (folderId) => { + return FeedResource.getByFolderId(folderId); + }; + // TBD this.createFeed = () => { console.log('TBD'); diff --git a/js/directive/NewsDraggable.js b/js/directive/NewsDraggable.js new file mode 100644 index 000000000..76360ecb7 --- /dev/null +++ b/js/directive/NewsDraggable.js @@ -0,0 +1,22 @@ +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Bernhard Posselt 2014 + */ +app.directive('newsDraggable', () => { + 'use strict'; + + return (scope, elem, attr) => { + let options = scope.$eval(attr.newsDraggable); + + if (angular.isDefined(options)) { + elem.draggable(options); + } else { + elem.draggable(); + } + }; +});
\ No newline at end of file diff --git a/js/directive/NewsDroppable.js b/js/directive/NewsDroppable.js new file mode 100644 index 000000000..a68f26444 --- /dev/null +++ b/js/directive/NewsDroppable.js @@ -0,0 +1,34 @@ +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Bernhard Posselt 2014 + */ +app.directive('newsDroppable', ($rootScope) => { + 'use strict'; + + return (scope, elem, attr) => { + let details = { + accept: '.feed', + hoverClass: 'drag-and-drop', + greedy: true, + drop: (event, ui) => { + + $('.drag-and-drop').removeClass('drag-and-drop'); + + let data = { + folderId: parseInt(elem.data('id'), 10), + feedId: parseInt($(ui.draggable).data('id'), 10) + }; + + $rootScope.$broadcast('moveFeedToFolder', data); + scope.$apply(attr.droppable); + } + }; + + elem.droppable(details); + }; +});
\ No newline at end of file diff --git a/js/service/FeedResource.js b/js/service/FeedResource.js index ab5aec9b4..9759724ac 100644 --- a/js/service/FeedResource.js +++ b/js/service/FeedResource.js @@ -17,12 +17,15 @@ app.factory('FeedResource', (Resource, $http, BASE_URL) => { this.ids = {}; this.unreadCount = 0; this.folderUnreadCount = {}; + this.folderIds = {}; + this.deleted = null; } receive (data) { super.receive(data); this.updateUnreadCache(); + this.updateFolderCache(); } @@ -43,6 +46,17 @@ app.factory('FeedResource', (Resource, $http, BASE_URL) => { } + updateFolderCache () { + this.folderIds = {}; + + for (let feed of this.values) { + this.folderIds[feed.folderId] = + this.folderIds[feed.folderId] || []; + this.folderIds[feed.folderId].push(feed); + } + } + + add (value) { super.add(value); if (value.id !== undefined) { @@ -51,10 +65,17 @@ app.factory('FeedResource', (Resource, $http, BASE_URL) => { } - delete (id) { - let feed = this.get(id); + delete (url) { + let feed = this.get(url); + this.deleted = feed; delete this.ids[feed.id]; - super.delete(id); + + super.delete(url); + + this.updateUnreadCache(); + this.updateFolderCache(); + + return this.http.delete(`${this.BASE_URL}/feeds/${feed.id}`); } @@ -114,12 +135,89 @@ app.factory('FeedResource', (Resource, $http, BASE_URL) => { getByFolderId (folderId) { - return this.values.filter(v => v.folderId === folderId); + return this.folderIds[folderId] || []; } + getById (feedId) { return this.ids[feedId]; } + + + rename (url, name) { + let feed = this.get(url); + feed.title = name; + + return this.http({ + method: 'POST', + url: `${this.BASE_URL}/feeds/${feed.id}/rename`, + data: { + feedTitle: name + } + }); + } + + + move (url, folderId) { + let feed = this.get(url); + feed.folderId = folderId; + + this.updateFolderCache(); + + return this.http({ + method: 'POST', + url: `${this.BASE_URL}/feeds/${feed.id}/move`, + data: { + parentFolderId: folderId + } + }); + + } + + + create (url, folderId, title=null) { + if (title) { + title = title.toUpperCase(); + } + + let feed = { + url: url, + folderId: folderId, + title: title + }; + + if (!this.get(url)) { + this.add(feed); + } + + this.updateFolderCache(); + + return this.http({ + method: 'POST', + url: `${this.BASE_URL}/feeds`, + data: { + url: url, + parentFolderId: folderId, + title: title + } + }); + } + + + undoDelete () { + if (this.deleted) { + this.add(this.deleted); + + return this.http.post( + `${this.BASE_URL}/feeds/${this.deleted.id}/restore` + ); + } + + this.updateFolderCache(); + this.updateUnreadCache(); + } + + } return new FeedResource($http, BASE_URL); diff --git a/js/service/FolderResource.js b/js/service/FolderResource.js index 1ea48e0bb..2e67fa690 100644 --- a/js/service/FolderResource.js +++ b/js/service/FolderResource.js @@ -14,8 +14,92 @@ app.factory('FolderResource', (Resource, $http, BASE_URL) => { constructor ($http, BASE_URL) { super($http, BASE_URL, 'name'); + this.deleted = null; } + + delete (folderName) { + let folder = this.get(folderName); + this.deleted = folder; + + super.delete(folderName); + + return this.http.delete(`${this.BASE_URL}/folders/${folder.id}`); + } + + + toggleOpen (folderName) { + let folder = this.get(folderName); + folder.opened = !folder.opened; + + return this.http({ + url: `${this.BASE_URL}/folders/${folder.id}/open`, + method: 'POST', + data: { + folderId: folder.id, + open: folder.opened + } + }); + } + + + rename (folderName, toFolderName) { + toFolderName = toFolderName.toUpperCase(); + let folder = this.get(folderName); + + // still do http request if folder exists but dont change the name + // to have one point of failure + if (!this.get(toFolderName)) { + folder.name = toFolderName; + + delete this.hashMap[folderName]; + this.hashMap[toFolderName] = folder; + } + + return this.http({ + url: `${this.BASE_URL}/folders/${folder.id}/rename`, + method: 'POST', + data: { + folderName: toFolderName + } + }); + } + + + create (folderName) { + folderName = folderName.toUpperCase(); + + // still do http request if folder exists but dont change the name + // to have one point of failure + if (!this.get(folderName)) { + let folder = { + name: folderName + }; + + this.add(folder); + } + + return this.http({ + url: `${this.BASE_URL}/folders`, + method: 'POST', + data: { + folderName: folderName + } + }); + } + + + undoDelete () { + if (this.deleted) { + this.add(this.deleted); + + return this.http.post( + `${this.BASE_URL}/folders/${this.deleted.id}/restore` + ); + } + } + + } return new FolderResource($http, BASE_URL); diff --git a/js/tests/unit/controller/NavigationControllerSpec.js b/js/tests/unit/controller/NavigationControllerSpec.js index 433a40e3f..adefd157f 100644 --- a/js/tests/unit/controller/NavigationControllerSpec.js +++ b/js/tests/unit/controller/NavigationControllerSpec.js @@ -111,6 +111,19 @@ describe('NavigationController', () => { SettingsResource.set('showAll', true); expect(ctrl.isShowAll()).toBe(true); + })); + + it('should get all of folder', inject((FeedResource, $controller) => { + let ctrl = $controller('NavigationController', { + FeedResource: FeedResource, + }); + + FeedResource.getByFolderId = jasmine.createSpy('getByFolderId'); + ctrl.getFeedsOfFolder(3); + + expect(FeedResource.getByFolderId).toHaveBeenCalledWith(3); })); + + });
\ No newline at end of file diff --git a/js/tests/unit/service/FeedResourceSpec.js b/js/tests/unit/service/FeedResourceSpec.js index 5187435f0..a37ff42f3 100644 --- a/js/tests/unit/service/FeedResourceSpec.js +++ b/js/tests/unit/service/FeedResourceSpec.js @@ -10,19 +10,21 @@ describe('FeedResource', () => { 'use strict'; - let resource; + let resource, + http; beforeEach(module('News', ($provide) => { $provide.value('BASE_URL', 'base'); })); - beforeEach(inject((FeedResource) => { + beforeEach(inject((FeedResource, $httpBackend) => { resource = FeedResource; + http = $httpBackend; FeedResource.receive([ {id: 1, folderId: 3, url: 'ye', unreadCount: 45}, {id: 2, folderId: 4, url: 'sye', unreadCount: 25}, - {id: 3, folderId: 3, url: '1sye', unreadCount: 0} + {id: 3, folderId: 3, title: 'hore', url: '1sye', unreadCount: 0} ]); })); @@ -104,4 +106,99 @@ describe('FeedResource', () => { expect(FeedResource.getUnreadCount()).toBe(68); })); + + + it ('should delete a feed', inject((FeedResource) => { + http.expectDELETE('base/feeds/1').respond(200, {}); + + FeedResource.delete('ye'); + + http.flush(); + + expect(FeedResource.size()).toBe(2); + })); + + + it ('should rename a feed', inject((FeedResource) => { + http.expectPOST('base/feeds/3/rename', { + feedTitle: 'heho' + }).respond(200, {}); + + FeedResource.rename('1sye', 'heho'); + + http.flush(); + + expect(FeedResource.get('1sye').title).toBe('heho'); + })); + + + it ('should move a feed', inject((FeedResource) => { + http.expectPOST('base/feeds/3/move', { + parentFolderId: 5 + }).respond(200, {}); + + FeedResource.move('1sye', 5); + + http.flush(); + + expect(FeedResource.get('1sye').folderId).toBe(5); + })); + + + it ('should create a feed', inject((FeedResource) => { + http.expectPOST('base/feeds', { + parentFolderId: 5, + url: 'hey', + title: 'ABC' + }).respond(200, {}); + + FeedResource.create('hey', 5, 'abc'); + + http.flush(); + + expect(FeedResource.get('hey').folderId).toBe(5); + })); + + + it ('should not create a feed if it exists', inject((FeedResource) => { + http.expectPOST('base/feeds', { + parentFolderId: 5, + url: 'ye', + title: 'ABC' + }).respond(200, {}); + + FeedResource.create('ye', 5, 'abc'); + + http.flush(); + + expect(FeedResource.size()).toBe(3); + })); + + + it ('should undo a delete folder', inject((FeedResource) => { + http.expectDELETE('base/feeds/1').respond(200, {}); + + FeedResource.delete('ye'); + + http.flush(); + + + http.expectPOST('base/feeds/1/restore').respond(200, {}); + + FeedResource.undoDelete(); + + http.flush(); + + expect(FeedResource.get('ye').id).toBe(1); + })); + + + + + afterEach(() => { + http.verifyNoOutstandingExpectation(); + http.verifyNoOutstandingRequest(); + }); + + }); diff --git a/js/tests/unit/service/FolderResourceSpec.js b/js/tests/unit/service/FolderResourceSpec.js new file mode 100644 index 000000000..db8312232 --- /dev/null +++ b/js/tests/unit/service/FolderResourceSpec.js @@ -0,0 +1,133 @@ +/** + * ownCloud - News + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Bernhard Posselt <dev@bernhard-posselt.com> + * @copyright Bernhard Posselt 2014 + */ +describe('FolderResource', () => { + 'use strict'; + + let resource, + http; + + beforeEach(module('News', ($provide) => { + $provide.value('BASE_URL', 'base'); + })); + + + beforeEach(inject((FolderResource, $httpBackend) => { + resource = FolderResource; + http = $httpBackend; + FolderResource.receive([ + {id: 1, name: 'ye'}, + {id: 2, name: 'SYE'}, + {id: 3, name: 'hore', opened: true} + ]); + })); + + + it ('should delete a folder', inject((FolderResource) => { + http.expectDELETE('base/folders/1').respond(200, {}); + |