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:
parent
bbae651bdd
commit
c4726b85f0
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
|||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
13
db/note.php
13
db/note.php
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue