Merge pull request #8 from v1r0x/simple-sharing

Simple sharing
This commit is contained in:
matiasdelellis
2016-06-23 10:58:26 -03:00
committed by GitHub
13 changed files with 453 additions and 14 deletions

View File

@@ -108,4 +108,37 @@
</field> </field>
</declaration> </declaration>
</table> </table>
<table>
<name>*dbprefix*quicknotes_shares</name>
<declaration>
<field>
<name>id</name>
<type>integer</type>
<notnull>true</notnull>
<autoincrement>true</autoincrement>
<unsigned>true</unsigned>
<primary>true</primary>
<length>8</length>
</field>
<field>
<name>note_id</name>
<type>integer</type>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>8</length>
</field>
<field>
<name>shared_user</name>
<type>text</type>
<length>200</length>
<default></default>
</field>
<field>
<name>shared_group</name>
<type>text</type>
<length>200</length>
<default></default>
</field>
</declaration>
</table>
</database> </database>

View File

@@ -5,7 +5,7 @@
<description>Quick notes with text, check lists and pictures</description> <description>Quick notes with text, check lists and pictures</description>
<licence>AGPL</licence> <licence>AGPL</licence>
<author>Matias De lellis</author> <author>Matias De lellis</author>
<version>0.1.0</version> <version>0.1.1</version>
<namespace>QuickNotes</namespace> <namespace>QuickNotes</namespace>
<category>tool</category> <category>tool</category>
<website>https://github.com/matiasdelellis</website> <website>https://github.com/matiasdelellis</website>

View File

@@ -7,6 +7,11 @@ return [
'routes' => [ 'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'note_api#preflighted_cors', 'url' => '/api/0.1/{path}', ['name' => 'note_api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']] 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']],
['name' => 'note#get_user_groups_and_users_with_share', 'url' => '/api/0.1/getusergroups', 'verb' => 'POST'],
['name' => 'note#add_group_share', 'url' => '/api/0.1/groups/addshare', 'verb' => 'POST'],
['name' => 'note#remove_group_share', 'url' => '/api/0.1/groups/removeshare', 'verb' => 'POST'],
['name' => 'note#add_user_share', 'url' => '/api/0.1/users/addshare', 'verb' => 'POST'],
['name' => 'note#remove_user_share', 'url' => '/api/0.1/users/removeshare', 'verb' => 'POST']
] ]
]; ];

View File

@@ -14,23 +14,28 @@ namespace OCA\QuickNotes\Controller;
use OCP\IRequest; use OCP\IRequest;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCA\QuickNotes\Db\Note; use OCA\QuickNotes\Db\Note;
use OCA\QuickNotes\Db\Color; use OCA\QuickNotes\Db\Color;
use OCA\QuickNotes\Db\NoteShare;
use OCA\QuickNotes\Db\NoteMapper; use OCA\QuickNotes\Db\NoteMapper;
use OCA\QuickNotes\Db\ColorMapper; use OCA\QuickNotes\Db\ColorMapper;
use OCA\QuickNotes\Db\NoteShareMapper;
class NoteController extends Controller { class NoteController extends Controller {
private $notemapper; private $notemapper;
private $colormapper; private $colormapper;
private $notesharemapper;
private $userId; private $userId;
public function __construct($AppName, IRequest $request, NoteMapper $notemapper, ColorMapper $colormapper, $UserId) { public function __construct($AppName, IRequest $request, NoteMapper $notemapper, NoteShareMapper $notesharemapper, ColorMapper $colormapper, $UserId) {
parent::__construct($AppName, $request); parent::__construct($AppName, $request);
$this->notemapper = $notemapper; $this->notemapper = $notemapper;
$this->colormapper = $colormapper; $this->colormapper = $colormapper;
$this->notesharemapper = $notesharemapper;
$this->userId = $UserId; $this->userId = $UserId;
} }
@@ -39,6 +44,33 @@ class NoteController extends Controller {
*/ */
public function index() { public function index() {
$notes = $this->notemapper->findAll($this->userId); $notes = $this->notemapper->findAll($this->userId);
foreach($notes as $note) {
$note->setIsShared(false);
$sharedWith = $this->notesharemapper->getSharesForNote($note->getId());
if(count($sharedWith) > 0) {
$shareList = array();
foreach($sharedWith as $share) {
$shareList[] = $share->getSharedUser();
}
$note->setSharedWith(implode(", ", $shareList));
} else {
$note->setSharedWith(null);
}
}
$shareEntries = $this->notesharemapper->findForUser($this->userId);
$shares = array();
foreach($shareEntries as $entry) {
try {
//find is only to check if current user is owner
$this->notemapper->find($entry->getNoteId(), $this->userId);
//user is owner, nothing to do
} catch(\OCP\AppFramework\Db\DoesNotExistException $e) {
$share = $this->notemapper->findById($entry->getNoteId());
$share->setIsShared(true);
$shares[] = $share;
}
}
$notes = array_merge($notes, $shares);
// Insert true color to response // Insert true color to response
foreach ($notes as $note) { foreach ($notes as $note) {
$note->setColor($this->colormapper->find($note->getColorId())->getColor()); $note->setColor($this->colormapper->find($note->getColorId())->getColor());
@@ -151,6 +183,8 @@ class NoteController extends Controller {
} }
$oldcolorid = $note->getColorId(); $oldcolorid = $note->getColorId();
$this->notesharemapper->deleteByNoteId($note->getId());
// Delete note. // Delete note.
$this->notemapper->delete($note); $this->notemapper->delete($note);
@@ -162,4 +196,94 @@ class NoteController extends Controller {
return new DataResponse($note); return new DataResponse($note);
} }
/**
* @NoAdminRequired
*/
public function getUserGroupsAndUsersWithShare($noteId) {
$userMgr = \OC::$server->getUserManager();
$grpMgr = \OC::$server->getGroupManager();
$users = array();
$groups = array();
if($grpMgr->isAdmin($this->userId)) {
$igroups = $grpMgr->search("");
$iusers = $userMgr->search("");
foreach($igroups as $g) {
$groups[] = $g->getGID();
}
foreach($iusers as $u) {
$users[] = $u->getUID();
}
} else {
$igroups = $grpMgr->getUserGroups($userMgr->get($this->userId));
foreach($igroups as $g) {
$iusers = $g->getUsers();
foreach($iusers as $u) {
$users[] = $u->getUID();
}
$groups[] = $g->getGID();
}
}
$users = array_unique($users);
if(($i = array_search($this->userId, $users)) !== false) {
unset($users[$i]);
}
$pos_users = array();
$pos_groups = array();
$shares = $this->notesharemapper->getSharesForNote($noteId);
foreach($shares as $s) {
$shareType = $s->getSharedUser();
if(strlen($shareType) != 0) {
if(($i = array_search($shareType, $users)) !== false) {
unset($users[$i]);
$pos_users[] = $shareType;
}
} else {
$shareType = $s->getSharedGroup();
if(($i = array_search($shareType, $groups)) !== false) {
unset($groups[$i]);
$pos_groups[] = $shareType;
}
}
}
$params = array('groups' => $groups, 'users' => $users, 'posGroups' => $pos_groups, 'posUsers' => $pos_users);
return new JSONResponse($params);
}
/**
* @NoAdminRequired
*/
public function addGroupShare($groupId, $noteId) {
$share = new NoteShare();
$share->setSharedGroup($groupId);
$share->setNoteId($noteId);
$this->notesharemapper->insert($share);
}
/**
* @NoAdminRequired
*/
public function removeGroupShare($groupId, $noteId) {
$share = $this->notesharemapper->findByNoteAndGroup($noteId, $groupId);
$this->notesharemapper->delete($share);
}
/**
* @NoAdminRequired
*/
public function addUserShare($userId, $noteId) {
$share = new NoteShare();
$share->setSharedUser($userId);
$share->setNoteId($noteId);
$this->notesharemapper->insert($share);
}
/**
* @NoAdminRequired
*/
public function removeUserShare($userId, $noteId) {
$share = $this->notesharemapper->findByNoteAndUser($noteId, $userId);
$this->notesharemapper->delete($share);
}
} }

View File

@@ -31,6 +31,10 @@
top: 6px; top: 6px;
} }
.save-button #unshare-button {
display: none;
}
#div-content .save-button { #div-content .save-button {
float: right; float: right;
padding-right: 8px; padding-right: 8px;
@@ -47,8 +51,6 @@
/* Grid Note */ /* Grid Note */
#div-content .note-title { #div-content .note-title {
height: 28px;
width: calc(100% - 20px);
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -56,6 +58,16 @@
overflow: hidden; overflow: hidden;
} }
#div-content .shared-title, #div-content .shared-title-owner {
float: right;
margin: 2px;
opacity: 0.5;
}
#div-content .shared-title-owner {
margin-right: 25px;
}
.noselect { .noselect {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */ -webkit-user-select: none; /* Chrome/Safari/Opera */
@@ -141,6 +153,19 @@ div[data-placeholder]:not([data-placeholder=""]):empty::before {
content: attr(data-placeholder); content: attr(data-placeholder);
} }
#note-share-options {
display: none;
padding-bottom: 5px;
}
.selected-share:hover, .unselected-share:hover, .selected-share span:hover, .unselected-share span:hover {
cursor: pointer;
}
.selected-share, .unselected-share {
padding-left: 5px;
}
/* Modal Content */ /* Modal Content */
.modal-content { .modal-content {

View File

@@ -12,6 +12,8 @@ class Note extends Entity implements JsonSerializable {
protected $timestamp; protected $timestamp;
protected $colorId; protected $colorId;
protected $userId; protected $userId;
protected $sharedWith;
protected $isShared;
protected $color; protected $color;
@@ -26,7 +28,10 @@ class Note extends Entity implements JsonSerializable {
'content' => $this->content, 'content' => $this->content,
'timestamp' => $this->timestamp, 'timestamp' => $this->timestamp,
'colorid' => $this->colorId, 'colorid' => $this->colorId,
'color' => $this->color 'color' => $this->color,
'userid' => $this->userId,
'sharedwith' => $this->sharedWith,
'isshared' => $this->isShared
]; ];
} }
} }

View File

@@ -11,11 +11,23 @@ class NoteMapper extends Mapper {
parent::__construct($db, 'quicknotes_notes', '\OCA\QuickNotes\Db\Note'); parent::__construct($db, 'quicknotes_notes', '\OCA\QuickNotes\Db\Note');
} }
/**
* @param int $id
* @param string $userId
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
* @return Note
*/
public function find($id, $userId) { public function find($id, $userId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_notes WHERE id = ? AND user_id = ?'; $sql = 'SELECT * FROM *PREFIX*quicknotes_notes WHERE id = ? AND user_id = ?';
return $this->findEntity($sql, [$id, $userId]); return $this->findEntity($sql, [$id, $userId]);
} }
public function findById($id) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_notes WHERE id = ?';
return $this->findEntity($sql, [$id]);
}
public function findAll($userId) { public function findAll($userId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_notes WHERE user_id = ?'; $sql = 'SELECT * FROM *PREFIX*quicknotes_notes WHERE user_id = ?';
return $this->findEntities($sql, [$userId]); return $this->findEntities($sql, [$userId]);

22
db/noteshare.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace OCA\QuickNotes\Db;
use JsonSerializable;
use OCP\AppFramework\Db\Entity;
class NoteShare extends Entity implements JsonSerializable {
protected $noteId;
protected $sharedUser;
protected $sharedGroup;
public function jsonSerialize() {
return [
'id' => $this->id,
'noteid' => $this->noteId,
'shareduser' => $this->sharedUser,
'sharedgroup' => $this->sharedGroup
];
}
}

48
db/notesharemapper.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
namespace OCA\QuickNotes\Db;
use OCP\IDb;
use OCP\AppFramework\Db\Mapper;
use OCP\AppFramework\Db\DoesNotExistException;
class NoteShareMapper extends Mapper {
public function __construct(IDb $db) {
parent::__construct($db, 'quicknotes_shares', '\OCA\QuickNotes\Db\NoteShare');
}
/*public function find($id, $userId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE id = ? AND user_id = ?';
return $this->findEntity($sql, [$id, $userId]);
}*/
public function findForUser($userId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE shared_user = ?';
return $this->findEntities($sql, [$userId]);
}
public function findForGroup($groupId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE shared_group = ?';
return $this->findEntities($sql, [$groupId]);
}
public function findByNoteAndUser($noteId, $userId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE shared_user = ? AND note_id = ?';
return $this->findEntity($sql, [$userId, $noteId]);
}
public function findByNoteAndGroup($noteId, $groupId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE shared_group = ? AND note_id = ?';
return $this->findEntity($sql, [$groupId, $noteId]);
}
public function getSharesForNote($noteId) {
$sql = 'SELECT * FROM *PREFIX*quicknotes_shares WHERE note_id = ?';
return $this->findEntities($sql, [$noteId]);
}
public function deleteByNoteId($noteId) {
$sql = 'DELETE FROM *PREFIX*quicknotes_shares WHERE note_id = ?';
$this->execute($sql, [$noteId]);
}
}

View File

@@ -24,6 +24,65 @@ var Notes = function (baseUrl) {
this._activeNote = undefined; this._activeNote = undefined;
}; };
var moveToUnselectedShare = function() {
var curr = $(this).clone();
var groupIndex = curr.html().indexOf('<span>(group)</span>');
var id = $('.note-active').data('id');
if(groupIndex >= 0) {
var groupId = curr.html().substring(0, groupIndex);
var formData = {
groupId : groupId,
noteId : id
};
$.post(OC.generateUrl('/apps/quicknotes/api/0.1/groups/removeshare'), formData, function(data){
});
} else {
var userId = curr.html();
var formData = {
userId : userId,
noteId : id
};
$.post(OC.generateUrl('/apps/quicknotes/api/0.1/users/removeshare'), formData, function(data){
});
}
curr.switchClass('selected-share', 'unselected-share', 0);
curr.hide();
curr.click(moveToSelectedShare);
$(curr).appendTo($('#share-neg'));
$(this).remove();
var pos = $('#share-pos');
if(pos.children().length == 0) pos.hide();
}
var moveToSelectedShare = function() {
var curr = $(this).clone();
var groupIndex = curr.html().indexOf('<span>(group)</span>');
var id = $('.note-active').data('id');
if(groupIndex >= 0) {
var groupId = curr.html().substring(0, groupIndex);
var formData = {
groupId : groupId,
noteId : id
};
$.post(OC.generateUrl('/apps/quicknotes/api/0.1/groups/addshare'), formData, function(data){
});
} else {
var userId = curr.html();
var formData = {
userId : userId,
noteId : id
};
$.post(OC.generateUrl('/apps/quicknotes/api/0.1/users/addshare'), formData, function(data){
});
}
curr.switchClass('unselected-share', 'selected-share', 0);
curr.click(moveToUnselectedShare);
$(curr).appendTo($('#share-pos'));
$(this).remove();
$('#share-pos').show();
$('#share-search').val('');
}
Notes.prototype = { Notes.prototype = {
load: function (id) { load: function (id) {
var self = this; var self = this;
@@ -269,6 +328,7 @@ View.prototype = {
$("#app-content").on("click", ".quicknote", function (event) { $("#app-content").on("click", ".quicknote", function (event) {
event.stopPropagation(); // Not work so need fix on next binding.. event.stopPropagation(); // Not work so need fix on next binding..
if($(this).hasClass('shared')) return; //shares don't allow editing
var modalnote = $("#modal-note-editable .quicknote"); var modalnote = $("#modal-note-editable .quicknote");
var modalid = modalnote.data('id'); var modalid = modalnote.data('id');
if (modalid > 0) return; if (modalid > 0) return;
@@ -335,6 +395,80 @@ View.prototype = {
modalnote.css("background-color", color); modalnote.css("background-color", color);
}); });
// handle share editing notes.
$('#modal-note-div #share-button').click(function (event) {
var id = $('.note-active').data('id');
var formData = {
noteId: id
}
$.post(OC.generateUrl('/apps/quicknotes/api/0.1/getusergroups'), formData, function(data) {
var shareOptions = $('#note-share-options');
var groups = data.groups;
var users = data.users;
var pos_groups = data.posGroups;
var pos_users = data.posUsers;
var neg = $('#share-neg');
var pos = $('#share-pos');
var sear = $('#share-search');
for(var i=0; i<groups.length; i++) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(groups[i]));
var sp = document.createElement('span');
sp.appendChild(document.createTextNode('(group)'));
li.className = "unselected-share";
li.appendChild(sp);
$(li).hide();
neg[0].appendChild(li);
}
for(var i=0; i<users.length; i++) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(users[i]));
li.className = "unselected-share";
$(li).hide();
neg[0].appendChild(li);
}
for(var i=0; i<pos_groups.length; i++) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(pos_groups[i]));
var sp = document.createElement('span');
sp.appendChild(document.createTextNode('(group)'));
li.className = "selected-share";
li.appendChild(sp);
pos[0].appendChild(li);
}
for(var i=0; i<pos_users.length; i++) {
var li = document.createElement('li');
li.appendChild(document.createTextNode(pos_users[i]));
li.className = "selected-share";
pos[0].appendChild(li);
}
$('.unselected-share').click(moveToSelectedShare);
$('.selected-share').click(moveToUnselectedShare);
shareOptions.show();
var modalNote = $('.note-active');
var startHeight = modalNote.outerHeight(true);
modalNote.outerHeight(startHeight + shareOptions.outerHeight(true));
sear.on('input', function() {
var val = $(this).val().toLowerCase().trim();
var lis = neg.children();
if(val.length == 0) {
lis.hide();
} else {
for(var i=0; i<lis.length; i++) {
if(lis[i].innerHTML.toLowerCase().indexOf(val) >= 0) {
$(lis[i]).show();
} else {
$(lis[i]).hide();
}
}
}
modalNote.outerHeight(startHeight + shareOptions.outerHeight(true));
});
});
});
// handle cancel editing notes. // handle cancel editing notes.
$('#modal-note-div #cancel-button').click(function (event) { $('#modal-note-div #cancel-button').click(function (event) {
self.cancelEdit(); self.cancelEdit();
@@ -384,6 +518,18 @@ View.prototype = {
$('#app-navigation .any-color').addClass('icon-checkmark'); $('#app-navigation .any-color').addClass('icon-checkmark');
}); });
$('#shared-with-you').click(function () {
$('.notes-grid').isotope({ filter: function() {
return $(this).children().hasClass('shared');
} });
});
$('#shared-by-you').click(function () {
$('.notes-grid').isotope({ filter: function() {
return $(this).children().hasClass('shareowner');
} });
});
// create a new note // create a new note
var self = this; var self = this;
$('#new-note').click(function () { $('#new-note').click(function () {

View File

@@ -4,6 +4,8 @@
<script id="navigation-tpl" type="text/x-handlebars-template"> <script id="navigation-tpl" type="text/x-handlebars-template">
<li id="new-note"><a href="#" class="icon-add svg"><?php p($l->t('New note')); ?></a></li> <li id="new-note"><a href="#" class="icon-add svg"><?php p($l->t('New note')); ?></a></li>
<li id="all-notes"><a href="#" class="icon-home svg"><?php p($l->t('All notes')); ?></a></li> <li id="all-notes"><a href="#" class="icon-home svg"><?php p($l->t('All notes')); ?></a></li>
<li id="shared-with-you"><a href="#" class="icon-share svg"><?php p($l->t('Shared with you')); ?></a></li>
<li id="shared-by-you"><a href="#" class="icon-share svg"><?php p($l->t('Shared by you')); ?></a></li>
<li class="collapsible open"> <li class="collapsible open">
<button class="collapse"></button> <button class="collapse"></button>

View File

@@ -5,9 +5,18 @@
<div contenteditable="true" id='content-editable' class='note-content' data-placeholder="No content"></div> <div contenteditable="true" id='content-editable' class='note-content' data-placeholder="No content"></div>
<div class="note-options"> <div class="note-options">
<div class="save-button"> <div class="save-button">
<button id='share-button'><?php p($l->t('Share'));?></button>
<button id='cancel-button'><?php p($l->t('Cancel')); ?></button> <button id='cancel-button'><?php p($l->t('Cancel')); ?></button>
<button id='save-button'><?php p($l->t('Save')); ?></button> <button id='save-button'><?php p($l->t('Save')); ?></button>
</div> </div>
<div style="clear: both;"></div>
<div id="note-share-options">
<ul id="share-pos">
</ul>
<input type="text" id="share-search" />
<ul id="share-neg">
</ul>
</div>
<div class="note-toolbar"> <div class="note-toolbar">
<a href="#" class="circle-toolbar" style="background-color: #F7EB96"></a> <a href="#" class="circle-toolbar" style="background-color: #F7EB96"></a>
<a href="#" class="circle-toolbar" style="background-color: #88B7E3"></a> <a href="#" class="circle-toolbar" style="background-color: #88B7E3"></a>

View File

@@ -1,7 +1,15 @@
<div class="note-grid-item"> <div class="note-grid-item">
<div class="quicknote noselect {{#if active}}note-active{{/if}}" style="background-color: {{color}}" data-id="{{ id }}" data-timestamp="{{ timestamp }}" > <div class="quicknote noselect {{#if active}}note-active{{/if}} {{#if isshared}}shared{{/if}} {{#if sharedwith}}shareowner{{/if}}" style="background-color: {{color}}" data-id="{{ id }}" data-timestamp="{{ timestamp }}" >
{{#if isshared}}
<div class='icon-share shared-title' title="shared with you by {{ userid }}"></div><div id='title' class='note-title'>{{{ title }}}</div>
<div id='content' class='note-content'>{{{ content }}}</div>
{{else}}
{{#if sharedwith}}
<div class='icon-share shared-title-owner' title="shared with {{ sharedwith }}"></div>
{{/if}}
<div id='title-editable' class='note-title'>{{{ title }}}</div> <div id='title-editable' class='note-title'>{{{ title }}}</div>
<button class="icon-delete hide-delete-icon icon-delete-note" title="Delete"></button> <button class="icon-delete hide-delete-icon icon-delete-note" title="Delete"></button>
<div id='content-editable' class='note-content'>{{{ content }}}</div> <div id='content-editable' class='note-content'>{{{ content }}}</div>
{{/if}}
</div> </div>
</div> </div>