New feature: set a note as favorite (star/unstar) (#248)

* New feature: set note as favorite (star/unstar)

* fix use of hidden classes

* minor optimizations requested by Henni

* remove array annotation

* check if tagger is null

* adjust tests for new favorite feature

* allow setting favorite over API
This commit is contained in:
korelstar 2016-10-21 16:05:33 +02:00 committed by Hendrik Leppelsack
parent bbae651bdd
commit c4726b85f0
12 changed files with 113 additions and 18 deletions

View File

@ -18,6 +18,7 @@ return ['routes' => [
['name' => 'notes#get', 'url' => '/notes/{id}', 'verb' => 'GET'],
['name' => 'notes#create', 'url' => '/notes', 'verb' => 'POST'],
['name' => 'notes#update', 'url' => '/notes/{id}', 'verb' => 'PUT'],
['name' => 'notes#favorite', 'url' => '/notes/{id}/favorite', 'verb' => 'PUT'],
['name' => 'notes#destroy', 'url' => '/notes/{id}', 'verb' => 'DELETE'],
// api

View File

@ -125,11 +125,19 @@ class NotesApiController extends ApiController {
*
* @param int $id
* @param string $content
* @param boolean $favorite
* @return DataResponse
*/
public function update($id, $content) {
public function update($id, $content=null, $favorite=null) {
if($favorite!==null) {
$this->service->favorite($id, $favorite, $this->userId);
}
return $this->respond(function () use ($id, $content) {
return $this->service->update($id, $content, $this->userId);
if($content===null) {
return $this->service->get($id, $this->userId);
} else {
return $this->service->update($id, $content, $this->userId);
}
});
}

View File

@ -105,6 +105,20 @@ class NotesController extends Controller {
}
/**
* @NoAdminRequired
*
* @param int $id
* @param boolean $favorite
* @return DataResponse
*/
public function favorite($id, $favorite) {
return $this->respond(function () use ($id, $favorite) {
return $this->notesService->favorite($id, $favorite, $this->userId);
});
}
/**
* @NoAdminRequired
*
@ -118,4 +132,4 @@ class NotesController extends Controller {
});
}
}
}

View File

@ -42,10 +42,15 @@
display: none;
}
#app-navigation .active > .utils .icon-delete {
display: block;
#app-navigation .active > .utils .icon-delete,
#app-navigation .active > .utils .icon-star {
display: inline-block;
opacity: .3;
}
#app-navigation .utils .icon-starred {
display: inline-block;
opacity: 1 !important;
}
.tooltip {
text-shadow: none;

View File

@ -24,6 +24,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setTitle(string $value)
* @method string getContent()
* @method void setContent(string $value)
* @method boolean getFavorite()
* @method void setFavorite(boolean $value)
* @package OCA\Notes\Db
*/
class Note extends Entity {
@ -31,24 +33,29 @@ class Note extends Entity {
public $modified;
public $title;
public $content;
public $favorite = false;
public function __construct() {
$this->addType('modified', 'integer');
$this->addType('favorite', 'boolean');
}
/**
* @param File $file
* @return static
*/
public static function fromFile(File $file){
public static function fromFile(File $file, $tags=[]){
$note = new static();
$note->setId($file->getId());
$note->setContent($file->getContent());
$note->setModified($file->getMTime());
$note->setTitle(pathinfo($file->getName(),PATHINFO_FILENAME)); // remove extension
if(is_array($tags) && in_array(\OC\Tags::TAG_FAVORITE, $tags)) {
$note->setFavorite(true);
//unset($tags[array_search(\OC\Tags::TAG_FAVORITE, $tags)]);
}
$note->resetUpdatedFields();
return $note;
}
}
}

View File

@ -35,4 +35,12 @@ app.controller('NotesController', function($routeParams, $scope, $location,
});
};
$scope.toggleFavorite = function (noteId) {
var note = NotesModel.get(noteId);
note.customPUT({favorite: !note.favorite},
'favorite', {}, {}).then(function (favorite) {
note.favorite = favorite ? true : false;
});
};
});

View File

@ -35,6 +35,7 @@ app.factory('NotesModel', function () {
note.title = updated.title;
note.modified = updated.modified;
note.content = updated.content;
note.favorite = updated.favorite;
} else {
this.notes.push(updated);
this.notesIds[updated.id] = updated;
@ -53,4 +54,4 @@ app.factory('NotesModel', function () {
};
return new NotesModel();
});
});

View File

@ -1,2 +1,2 @@
!function(e,n,o,i,r){"use strict";var s=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(t,e,n,i,r){i.defaults.headers.common.requesttoken=o,t.value("Constants",{saveInterval:5e3}),e.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(t,e,n,o){var i=e.defer(),r=t.current.params.noteId;return n.loading=!0,o.one("notes",r).get().then(function(t){n.loading=!1,i.resolve(t)},function(){n.loading=!1,i.reject()}),i.promise}]}}).otherwise({redirectTo:"/"});var s=OC.generateUrl("/apps/notes");n.setBaseUrl(s)}]).run(["$rootScope","$location","NotesModel",function(t,e,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),t.$on("$routeChangeError",function(){var t=o.getAll();if(t.length>0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified<e.modified?-1:0}),i=t[n.length-1];e.path("/notes/"+i.id)}else e.path("/")})}]);s.controller("AppController",["$scope","$location","is",function(t,e,n){t.is=n,t.init=function(t){0!==t&&e.path("/notes/"+t)}}]),s.controller("NoteController",["$routeParams","$scope","NotesModel","SaveQueue","note",function(e,n,o,i,r){o.updateIfExists(r),n.note=o.get(e.noteId),n.isSaving=function(){return i.isSaving()},n.updateTitle=function(){n.note.title=n.note.content.split("\n")[0]||t("notes","New note")},n.save=function(){var t=n.note;i.add(t)}}]),s.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel",function(t,e,n,o,i){e.route=t,e.notes=i.getAll();var r=o.all("notes");r.getList().then(function(t){i.addAll(t)}),e.create=function(){r.post().then(function(t){i.add(t),n.path("/notes/"+t.id)})},e["delete"]=function(t){var n=i.get(t);n.remove().then(function(){i.remove(t),e.$emit("$routeChangeError")})}}]),s.directive("notesAutofocus",function(){return{restrict:"A",link:function(t,e){e.focus()}}}),s.directive("editor",["$timeout",function(t){return{restrict:"A",link:function(e,o){var r=i(o[0],{change:function(n){t(function(){e.$apply(function(){e.note.content=n,e.updateTitle()})})}});r.setValue(e.note.content),o.on("click",".link",function(t){if(t.ctrlKey){var e=n(this).find(".link-params-inner").text();window.open(e,"_blank")}})}}}]),s.directive("notesIsSaving",["$window",function(e){return{restrict:"A",scope:{notesIsSaving:"="},link:function(n){e.onbeforeunload=function(){return n.notesIsSaving?t("notes","Note is currently saving. Leaving the page will delete all changes!"):null}}}}]),s.directive("notesTimeoutChange",["$timeout",function(t){return{restrict:"A",link:function(e,o,i){var r,s=300;n(o).bind("input propertychange paste",function(){t.cancel(r),r=t(function(){e.$apply(i.notesTimeoutChange)},s)})}}}]),s.directive("notesTooltip",function(){return{restrict:"A",link:function(t,e){e.tooltip()}}}),s.filter("noteTitle",function(){return function(t){return t=t.split("\n")[0]||"newNote",t.trim().replace(/^#+/g,"")}}),s.filter("wordCount",function(){return function(t){if(t&&"string"==typeof t){var e=t.split(/\s+/).filter(function(t){return-1!==t.search(/[A-Za-z0-9]/)}).length;return window.n("notes","%n word","%n words",e)}return 0}}),s.factory("is",function(){return{loading:!1}}),s.factory("NotesModel",function(){var t=function(){this.notes=[],this.notesIds={}};return t.prototype={addAll:function(t){for(var e=0;e<t.length;e+=1)this.add(t[e])},add:function(t){this.updateIfExists(t)},getAll:function(){return this.notes},get:function(t){return this.notesIds[t]},updateIfExists:function(t){var n=this.notesIds[t.id];e.isDefined(n)?(n.title=t.title,n.modified=t.modified,n.content=t.content):(this.notes.push(t),this.notesIds[t.id]=t)},remove:function(t){for(var e=0;e<this.notes.length;e+=1){var n=this.notes[e];if(n.id===t){this.notes.splice(e,1),delete this.notesIds[t];break}}}},new t}),s.factory("SaveQueue",["$q",function(t){var e=function(){this._queue={},this._flushLock=!1};return e.prototype={add:function(t){this._queue[t.id]=t,this._flush()},_flush:function(){var e=Object.keys(this._queue);if(0!==e.length&&!this._flushLock){this._flushLock=!0;for(var n=this,o=[],i=0;i<e.length;i+=1){var r=this._queue[e[i]];o.push(r.put().then(this._noteUpdateRequest.bind(null,r)))}this._queue={},t.all(o).then(function(){n._flushLock=!1,n._flush()})}},_noteUpdateRequest:function(t,e){t.title=e.title,t.modified=e.modified},isSaving:function(){return this._flushLock}},new e}])}(angular,jQuery,oc_requesttoken,mdEdit);
!function(e,n,o,i,r){"use strict";var s=e.module("Notes",["restangular","ngRoute"]).config(["$provide","$routeProvider","RestangularProvider","$httpProvider","$windowProvider",function(t,e,n,i,r){i.defaults.headers.common.requesttoken=o,t.value("Constants",{saveInterval:5e3}),e.when("/notes/:noteId",{templateUrl:"note.html",controller:"NoteController",resolve:{note:["$route","$q","is","Restangular",function(t,e,n,o){var i=e.defer(),r=t.current.params.noteId;return n.loading=!0,o.one("notes",r).get().then(function(t){n.loading=!1,i.resolve(t)},function(){n.loading=!1,i.reject()}),i.promise}]}}).otherwise({redirectTo:"/"});var s=OC.generateUrl("/apps/notes");n.setBaseUrl(s)}]).run(["$rootScope","$location","NotesModel",function(t,e,o){n('link[rel="shortcut icon"]').attr("href",OC.filePath("notes","img","favicon.png")),t.$on("$routeChangeError",function(){var t=o.getAll();if(t.length>0){var n=t.sort(function(t,e){return t.modified>e.modified?1:t.modified<e.modified?-1:0}),i=t[n.length-1];e.path("/notes/"+i.id)}else e.path("/")})}]);s.controller("AppController",["$scope","$location","is",function(t,e,n){t.is=n,t.init=function(t){0!==t&&e.path("/notes/"+t)}}]),s.controller("NoteController",["$routeParams","$scope","NotesModel","SaveQueue","note",function(e,n,o,i,r){o.updateIfExists(r),n.note=o.get(e.noteId),n.isSaving=function(){return i.isSaving()},n.updateTitle=function(){n.note.title=n.note.content.split("\n")[0]||t("notes","New note")},n.save=function(){var t=n.note;i.add(t)}}]),s.controller("NotesController",["$routeParams","$scope","$location","Restangular","NotesModel",function(t,e,n,o,i){e.route=t,e.notes=i.getAll();var r=o.all("notes");r.getList().then(function(t){i.addAll(t)}),e.create=function(){r.post().then(function(t){i.add(t),n.path("/notes/"+t.id)})},e["delete"]=function(t){var n=i.get(t);n.remove().then(function(){i.remove(t),e.$emit("$routeChangeError")})},e.toggleFavorite=function(t){var e=i.get(t);e.customPUT({favorite:!e.favorite},"favorite",{},{}).then(function(t){e.favorite=!!t})}}]),s.directive("notesAutofocus",function(){return{restrict:"A",link:function(t,e){e.focus()}}}),s.directive("editor",["$timeout",function(t){return{restrict:"A",link:function(e,o){var r=i(o[0],{change:function(n){t(function(){e.$apply(function(){e.note.content=n,e.updateTitle()})})}});r.setValue(e.note.content),o.on("click",".link",function(t){if(t.ctrlKey){var e=n(this).find(".link-params-inner").text();window.open(e,"_blank")}})}}}]),s.directive("notesIsSaving",["$window",function(e){return{restrict:"A",scope:{notesIsSaving:"="},link:function(n){e.onbeforeunload=function(){return n.notesIsSaving?t("notes","Note is currently saving. Leaving the page will delete all changes!"):null}}}}]),s.directive("notesTimeoutChange",["$timeout",function(t){return{restrict:"A",link:function(e,o,i){var r,s=300;n(o).bind("input propertychange paste",function(){t.cancel(r),r=t(function(){e.$apply(i.notesTimeoutChange)},s)})}}}]),s.directive("notesTooltip",function(){return{restrict:"A",link:function(t,e){e.tooltip()}}}),s.filter("noteTitle",function(){return function(t){return t=t.split("\n")[0]||"newNote",t.trim().replace(/^#+/g,"")}}),s.filter("wordCount",function(){return function(t){if(t&&"string"==typeof t){var e=t.split(/\s+/).filter(function(t){return t.search(/[A-Za-z0-9]/)!==-1}).length;return window.n("notes","%n word","%n words",e)}return 0}}),s.factory("is",function(){return{loading:!1}}),s.factory("NotesModel",function(){var t=function(){this.notes=[],this.notesIds={}};return t.prototype={addAll:function(t){for(var e=0;e<t.length;e+=1)this.add(t[e])},add:function(t){this.updateIfExists(t)},getAll:function(){return this.notes},get:function(t){return this.notesIds[t]},updateIfExists:function(t){var n=this.notesIds[t.id];e.isDefined(n)?(n.title=t.title,n.modified=t.modified,n.content=t.content,n.favorite=t.favorite):(this.notes.push(t),this.notesIds[t.id]=t)},remove:function(t){for(var e=0;e<this.notes.length;e+=1){var n=this.notes[e];if(n.id===t){this.notes.splice(e,1),delete this.notesIds[t];break}}}},new t}),s.factory("SaveQueue",["$q",function(t){var e=function(){this._queue={},this._flushLock=!1};return e.prototype={add:function(t){this._queue[t.id]=t,this._flush()},_flush:function(){var e=Object.keys(this._queue);if(0!==e.length&&!this._flushLock){this._flushLock=!0;for(var n=this,o=[],i=0;i<e.length;i+=1){var r=this._queue[e[i]];o.push(r.put().then(this._noteUpdateRequest.bind(null,r)))}this._queue={},t.all(o).then(function(){n._flushLock=!1,n._flush()})}},_noteUpdateRequest:function(t,e){t.title=e.title,t.modified=e.modified},isSaving:function(){return this._flushLock}},new e}])}(angular,jQuery,oc_requesttoken,mdEdit);
//# sourceMappingURL=app.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -44,13 +44,23 @@ class NotesService {
public function getAll ($userId){
$folder = $this->getFolderForUser($userId);
$files = $folder->getDirectoryListing();
$notes = [];
$filesById = [];
foreach($files as $file) {
if($this->isNote($file)) {
$notes[] = Note::fromFile($file);
$filesById[$file->getId()] = $file;
}
}
$tagger = \OC::$server->getTagManager()->load('files');
if($tagger==null) {
$tags = [];
} else {
$tags = $tagger->getTagsForObjects(array_keys($filesById));
}
$notes = [];
foreach($filesById as $id=>$file) {
$notes[] = Note::fromFile($file, array_key_exists($id, $tags) ? $tags[$id] : []);
}
return $notes;
}
@ -65,9 +75,18 @@ class NotesService {
*/
public function get ($id, $userId) {
$folder = $this->getFolderForUser($userId);
return Note::fromFile($this->getFileById($folder, $id));
return Note::fromFile($this->getFileById($folder, $id), $this->getTags($id));
}
private function getTags ($id) {
$tagger = \OC::$server->getTagManager()->load('files');
if($tagger==null) {
$tags = [];
} else {
$tags = $tagger->getTagsForObjects([$id]);
}
return array_key_exists($id, $tags) ? $tags[$id] : [];
}
/**
* Creates a note and returns the empty note
@ -131,7 +150,31 @@ class NotesService {
$file->putContent($content);
return Note::fromFile($file);
return Note::fromFile($file, $this->getTags($id));
}
/**
* Set or unset a note as favorite.
* @param int $id the id of the note used to update
* @param boolean $favorite whether the note should be a favorite or not
* @throws NoteDoesNotExistException if note does not exist
* @return boolean the new favorite state of the note
*/
public function favorite ($id, $favorite, $userId){
$folder = $this->getFolderForUser($userId);
$file = $this->getFileById($folder, $id);
if(!$this->isNote($file)) {
throw new NoteDoesNotExistException();
}
$tagger = \OC::$server->getTagManager()->load('files');
if($favorite)
$tagger->addToFavorites($id);
else
$tagger->removeFromFavorites($id);
$tags = $tagger->getTagsForObjects([$id]);
return in_array(\OC\Tags::TAG_FAVORITE, $tags[$id]);
}
@ -161,7 +204,6 @@ class NotesService {
if(count($file) <= 0 || !$this->isNote($file[0])) {
throw new NoteDoesNotExistException();
}
return $file[0];
}

View File

@ -42,7 +42,7 @@ style('notes', [
<a href='#'>+ <span><?php p($l->t('New note')); ?></span></a>
</li>
<!-- notes list -->
<li ng-repeat="note in notes|orderBy:'modified':'reverse'"
<li ng-repeat="note in notes|orderBy:['-favorite','-modified']"
ng-class="{ active: note.id == route.noteId }">
<a href="#/notes/{{ note.id }}">
{{ note.title | noteTitle }}
@ -53,6 +53,12 @@ style('notes', [
notes-tooltip
data-placement="bottom"
ng-click="delete(note.id)"></button>
<button class="svg action icon-star"
title="<?php p($l->t('Favorite')); ?>"
notes-tooltip
data-placement="bottom"
ng-click="toggleFavorite(note.id)"
ng-class="{'icon-starred': note.favorite}"></button>
</span>
</li>

View File

@ -88,10 +88,12 @@ class NotesApiControllerTest extends PHPUnit_Framework_TestCase {
$this->assertEquals(json_encode([
[
'modified' => 123,
'favorite' => false,
'id' => 3,
],
[
'modified' => 111,
'favorite' => false,
'id' => 4,
]
]), json_encode($response->getData()));
@ -136,6 +138,7 @@ class NotesApiControllerTest extends PHPUnit_Framework_TestCase {
$this->assertEquals(json_encode([
'modified' => 123,
'favorite' => false,
'id' => 3,
]), json_encode($response->getData()));
$this->assertTrue($response instanceof DataResponse);