1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 13:19:43 +02:00

pin spaces and documents to sidebar

This commit is contained in:
Harvey Kandola 2016-11-21 19:27:18 -08:00
parent e0d2dd47df
commit 8cc798990a
27 changed files with 2943 additions and 1589 deletions

View file

@ -8,7 +8,7 @@ The mission is to bring software dev inspired features (refactoring, testing, li
## Latest version ## Latest version
v0.33.0 v0.34.0
## OS Support ## OS Support

View file

@ -17,7 +17,7 @@
"Tooltip", "Tooltip",
"Drop", "Drop",
"Dropzone", "Dropzone",
"dragula", "Sortable",
"datetimepicker", "datetimepicker",
"Waypoint" "Waypoint"
], ],

View file

@ -24,6 +24,12 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
name: "", name: "",
description: "" description: ""
}, },
pinned: Ember.inject.service(),
pinState : {
isPinned: false,
pinId: '',
newName: '',
},
didReceiveAttrs() { didReceiveAttrs() {
this.set('saveTemplate.name', this.get('document.name')); this.set('saveTemplate.name', this.get('document.name'));
@ -32,6 +38,10 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
let doc = this.get('document'); let doc = this.get('document');
this.set('layoutLabel', doc.get('layout') === 'doc' ? 'Wiki style' : 'Document style'); this.set('layoutLabel', doc.get('layout') === 'doc' ? 'Wiki style' : 'Document style');
this.set('pinState.pinId', this.get('pinned').isDocumentPinned(doc.get('id')));
this.set('pinState.isPinned', this.get('pinState.pinId') !== '');
this.set('pinState.newName', doc.get('name').substring(0,3).toUpperCase());
}, },
didRender() { didRender() {
@ -106,5 +116,34 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
this.attrs.onSaveMeta(doc); this.attrs.onSaveMeta(doc);
return true; return true;
}, },
unpin() {
this.get('pinned').unpinItem(this.get('pinState.pinId')).then(() => {
this.set('pinState.isPinned', false);
this.set('pinState.pinId', '');
this.eventBus.publish('pinChange');
});
},
pin() {
let pin = {
pin: this.get('pinState.newName'),
documentId: this.get('document.id'),
folderId: this.get('folder.id')
};
if (is.empty(pin.pin)) {
$("#pin-document-name").addClass("error").focus();
return false;
}
this.get('pinned').pinItem(pin).then((pin) => {
this.set('pinState.isPinned', true);
this.set('pinState.pinId', pin.get('id'));
this.eventBus.publish('pinChange');
});
return true;
}
} }
}); });

View file

@ -21,13 +21,18 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
folderService: Ember.inject.service('folder'), folderService: Ember.inject.service('folder'),
session: Ember.inject.service(), session: Ember.inject.service(),
appMeta: Ember.inject.service(), appMeta: Ember.inject.service(),
showToolbar: false, showToolbar: false,
folder: {}, folder: {},
busy: false, busy: false,
isFolderOwner: computed.equal('folder.userId', 'session.user.id'), isFolderOwner: computed.equal('folder.userId', 'session.user.id'),
moveFolderId: "", moveFolderId: "",
drop: null, drop: null,
pinned: Ember.inject.service(),
pinState : {
isPinned: false,
pinId: '',
newName: '',
},
didReceiveAttrs() { didReceiveAttrs() {
this.set('isFolderOwner', this.get('folder.userId') === this.get("session.user.id")); this.set('isFolderOwner', this.get('folder.userId') === this.get("session.user.id"));
@ -40,6 +45,11 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
}); });
this.set('movedFolderOptions', targets); this.set('movedFolderOptions', targets);
let folder = this.get('folder');
this.set('pinState.pinId', this.get('pinned').isSpacePinned(folder.get('id')));
this.set('pinState.isPinned', this.get('pinState.pinId') !== '');
this.set('pinState.newName', folder.get('name').substring(0,3).toUpperCase());
}, },
didRender() { didRender() {
@ -87,6 +97,35 @@ export default Ember.Component.extend(NotifierMixin, TooltipMixin, {
this.set("moveFolderId", ""); this.set("moveFolderId", "");
return true; return true;
} },
unpin() {
this.get('pinned').unpinItem(this.get('pinState.pinId')).then(() => {
this.set('pinState.isPinned', false);
this.set('pinState.pinId', '');
this.eventBus.publish('pinChange');
});
},
pin() {
let pin = {
pin: this.get('pinState.newName'),
documentId: '',
folderId: this.get('folder.id')
};
if (is.empty(pin.pin)) {
$("#pin-space-name").addClass("error").focus();
return false;
}
this.get('pinned').pinItem(pin).then((pin) => {
this.set('pinState.isPinned', true);
this.set('pinState.pinId', pin.get('id'));
this.eventBus.publish('pinChange');
});
return true;
}
} }
}); });

View file

@ -19,15 +19,18 @@ const {
export default Ember.Component.extend(TooltipMixin, { export default Ember.Component.extend(TooltipMixin, {
folderService: service('folder'), folderService: service('folder'),
folder: null,
appMeta: service(), appMeta: service(),
session: service(), session: service(),
store: service(),
folder: null,
view: { view: {
folder: false, folder: false,
search: false, search: false,
settings: false, settings: false,
profile: false profile: false
}, },
pinned: service(),
pins: [],
init() { init() {
this._super(...arguments); this._super(...arguments);
@ -38,6 +41,8 @@ export default Ember.Component.extend(TooltipMixin, {
account.active = account.orgId === this.get("appMeta.orgId"); account.active = account.orgId === this.get("appMeta.orgId");
}); });
} }
this.set('pins', this.get('pinned').get('pins'));
}, },
didReceiveAttrs() { didReceiveAttrs() {
@ -52,6 +57,31 @@ export default Ember.Component.extend(TooltipMixin, {
this.set('view.search', (route === 'search') ? true : false); this.set('view.search', (route === 'search') ? true : false);
}, },
didInsertElement() {
this._super(...arguments);
// Size the pinned items zone
if (this.get("session.authenticated")) {
this.eventBus.subscribe('resized', this, 'sizePinnedZone');
this.eventBus.subscribe('pinChange', this, 'setupPins');
this.sizePinnedZone();
this.setupPins();
let self = this;
var sortable = Sortable.create(document.getElementById('pinned-zone'), {
animation: 150,
onEnd: function () {
self.get('pinned').updateSequence(this.toArray()).then((pins) => {
self.set('pins', pins);
});
}
});
this.set('sortable', sortable);
}
},
didRender() { didRender() {
if (this.get('session.isAdmin')) { if (this.get('session.isAdmin')) {
this.addTooltip(document.getElementById("workspace-settings")); this.addTooltip(document.getElementById("workspace-settings"));
@ -63,10 +93,53 @@ export default Ember.Component.extend(TooltipMixin, {
} }
}, },
setupPins() {
this.get('pinned').getUserPins().then((pins) => {
this.set('pins', pins);
pins.forEach((pin) => {
this.addTooltip(document.getElementById(`pin-${pin.id}`));
});
});
},
// set height for pinned zone so ti scrolls on spill
sizePinnedZone() {
let topofBottomZone = parseInt($('#bottom-zone').css("top").replace("px", ""));
let heightOfTopZone = parseInt($('#top-zone').css("height").replace("px", ""));
let size = topofBottomZone - heightOfTopZone - 40;
$('#pinned-zone').css('height', size + "px");
},
willDestroyElement() {
let sortable = this.get('sortable');
if (!_.isUndefined(sortable)) {
sortable.destroy();
}
this.destroyTooltips();
},
actions: { actions: {
switchAccount(domain) { switchAccount(domain) {
this.audit.record('switched-account'); this.audit.record('switched-account');
window.location.href = netUtil.getAppUrl(domain); window.location.href = netUtil.getAppUrl(domain);
},
jumpToPin(pin) {
let folderId = pin.get('folderId');
let documentId = pin.get('documentId');
if (_.isEmpty(documentId)) {
// jump to space
let folder = this.get('store').peekRecord('folder', folderId);
this.get('router').transitionTo('folder', folderId, folder.get('slug'));
} else {
// jump to doc
let folder = this.get('store').peekRecord('folder', folderId);
this.get('router').transitionTo('document', folderId, folder.get('slug'), documentId, 'document');
}
} }
} }
}); });

24
app/app/models/pin.js Normal file
View file

@ -0,0 +1,24 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
orgId: attr('string'),
userId: attr('string'),
folderId: attr('string'),
documentId: attr('string'),
sequence: attr('number', { defaultValue: 99 }),
pin: attr('string'),
created: attr(),
revised: attr()
});

View file

@ -21,6 +21,7 @@ const {
export default Ember.Route.extend(ApplicationRouteMixin, TooltipMixin, { export default Ember.Route.extend(ApplicationRouteMixin, TooltipMixin, {
appMeta: service(), appMeta: service(),
session: service(), session: service(),
pinned: service(),
beforeModel(transition) { beforeModel(transition) {
return this.get('appMeta').boot(transition.targetName).then(data => { return this.get('appMeta').boot(transition.targetName).then(data => {

130
app/app/services/pinned.js Normal file
View file

@ -0,0 +1,130 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
const {
inject: { service }
} = Ember;
export default Ember.Service.extend({
session: service('session'),
ajax: service(),
appMeta: service(),
store: service(),
pins: [],
getUserPins() {
let userId = this.get('session.user.id');
return this.get('ajax').request(`pin/${userId}`, {
method: 'GET'
}).then((response) => {
if (is.not.array(response)) {
response = [];
}
let pins = Ember.ArrayProxy.create({
content: Ember.A([])
});
pins = response.map((pin) => {
let data = this.get('store').normalize('pin', pin);
return this.get('store').push(data);
});
this.set('pins', pins);
return pins;
});
},
// Pin an item.
pinItem(data) {
let userId = this.get('session.user.id');
if(this.get('session.authenticated')) {
return this.get('ajax').request(`pin/${userId}`, {
method: 'POST',
data: JSON.stringify(data)
}).then((response) => {
let data = this.get('store').normalize('pin', response);
return this.get('store').push(data);
});
}
},
// Unpin an item.
unpinItem(pinId) {
let userId = this.get('session.user.id');
if(this.get('session.authenticated')) {
return this.get('ajax').request(`pin/${userId}/${pinId}`, {
method: 'DELETE'
});
}
},
// updateSequence persists order after use drag-drop sorting.
updateSequence(data) {
let userId = this.get('session.user.id');
if(this.get('session.authenticated')) {
return this.get('ajax').request(`pin/${userId}/sequence`, {
method: 'POST',
data: JSON.stringify(data)
}).then((response) => {
if (is.not.array(response)) {
response = [];
}
let pins = Ember.ArrayProxy.create({
content: Ember.A([])
});
pins = response.map((pin) => {
let data = this.get('store').normalize('pin', pin);
return this.get('store').push(data);
});
this.set('pins', pins);
return pins;
});
}
},
isDocumentPinned(documentId) {
let userId = this.get('session.user.id');
let pins = this.get('pins');
let pinId = '';
pins.forEach((pin) => {
if (pin.get('userId') === userId && pin.get('documentId') === documentId) {
pinId = pin.get('id');
}
});
return pinId;
},
isSpacePinned(spaceId) {
let userId = this.get('session.user.id');
let pins = this.get('pins');
let pinId = '';
pins.forEach((pin) => {
if (pin.get('userId') === userId && pin.get('documentId') === '' && pin.get('folderId') === spaceId) {
pinId = pin.get('id');
}
});
return pinId;
}
});

View file

@ -83,6 +83,10 @@
&:hover { &:hover {
color: $color-link; color: $color-link;
} }
> .pinned {
color: $color-primary;
}
} }
} }
} }

View file

@ -98,6 +98,50 @@
color: $color-white; color: $color-white;
} }
} }
.pinned-zone {
position: relative;
top: 220px;
padding: 0;
margin: 0;
width: 100%;
overflow: scroll;
> .pin {
cursor: pointer;
margin: 20px 0 20px 9px;
padding: 11px 3px;
height: 40px;
width: 40px;
text-align: center;
overflow: hidden;
text-transform: uppercase;
@include ease-in();
@include border-radius(3px);
font-family: $font-semibold;
font-size: 12px;
letter-spacing: -1px;
background-color: $color-off-white;
color: $color-primary;
> .key {
width: 30px;
text-align: center;
display: inline-block;
overflow: hidden;
}
&:hover {
background-color: $color-link;
color: $color-off-white;
}
}
> .sortable-ghost {
background-color: $color-gray;
color: $color-off-white;
}
}
} }
.zone-sidebar { .zone-sidebar {

View file

@ -29,9 +29,16 @@
</ul> </ul>
<ul class="options"> <ul class="options">
{{#if session.authenticated}}
{{#if pinState.isPinned}}
<li class="option" {{action 'unpin'}}><i class="material-icons pinned">star</i></li>
{{else}}
<li class="option" id="pin-document-button"><i class="material-icons">star_border</i></li>
{{/if}}
{{/if}}
{{#if isEditor}} {{#if isEditor}}
<li class="option" id="set-meta-button"><i class="material-icons">settings</i></li>
<li class="option" id="save-template-button"><i class="material-icons">content_copy</i></li> <li class="option" id="save-template-button"><i class="material-icons">content_copy</i></li>
<li class="option" id="set-meta-button"><i class="material-icons">settings</i></li>
{{/if}} {{/if}}
<li class="option" id="document-toolbar-menu"><i class="material-icons">more_horiz</i></li> <li class="option" id="document-toolbar-menu"><i class="material-icons">more_horiz</i></li>
</ul> </ul>
@ -59,6 +66,20 @@
{{/dropdown-dialog}} {{/dropdown-dialog}}
{{/if}} {{/if}}
{{#if session.authenticated}}
{{#unless pinState.isPinned}}
{{#dropdown-dialog target="pin-document-button" position="bottom right" button="Pin" color="flat-green" onAction=(action 'pin') focusOn="pin-document-name" }}
<div>
<div class="input-control">
<label>Pin Document</label>
<div class="tip">A 3 or 4 character name</div>
{{input type='text' id="pin-document-name" value=pinState.newName}}
</div>
</div>
{{/dropdown-dialog}}
{{/unless}}
{{/if}}
{{#if isEditor}} {{#if isEditor}}
{{#dropdown-dialog target="save-template-button" position="bottom right" button="Save as Template" color="flat-green" onAction=(action 'saveTemplate') focusOn="new-template-name" }} {{#dropdown-dialog target="save-template-button" position="bottom right" button="Save as Template" color="flat-green" onAction=(action 'saveTemplate') focusOn="new-template-name" }}
<div> <div>

View file

@ -28,6 +28,20 @@
</ul> </ul>
{{/dropdown-dialog}} {{/dropdown-dialog}}
{{else}} {{else}}
{{#if session.authenticated}}
{{#if pinState.isPinned}}
<div class="round-button-mono" {{action 'unpin'}}>
<i class="material-icons color-primary">star</i>
</div>
{{else}}
<div class="round-button-mono" id="pin-space-button">
<i class="material-icons color-gray">star_border</i>
</div>
{{/if}}
{{#if isFolderOwner}}
<div class="button-gap"></div>
{{/if}}
{{/if}}
{{#if isFolderOwner}} {{#if isFolderOwner}}
{{#link-to 'settings' folder.id folder.slug (query-params tab="tabShare")}} {{#link-to 'settings' folder.id folder.slug (query-params tab="tabShare")}}
<div class="round-button-mono" id="folder-share-button" data-tooltip="Share" data-tooltip-position="top center"> <div class="round-button-mono" id="folder-share-button" data-tooltip="Share" data-tooltip-position="top center">
@ -41,6 +55,17 @@
</div> </div>
{{/link-to}} {{/link-to}}
{{/if}} {{/if}}
{{#if session.authenticated}}
{{#unless pinState.isPinned}}
{{#dropdown-dialog target="pin-space-button" position="bottom right" button="Pin" color="flat-green" onAction=(action 'pin') focusOn="pin-space-name" }}
<div class="input-control">
<label>Pin Space</label>
<div class="tip">A 3 or 4 character name</div>
{{input type='text' id="pin-space-name" value=pinState.newName}}
</div>
{{/dropdown-dialog}}
{{/unless}}
{{/if}}
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}

View file

@ -1,61 +1,67 @@
<div id="zone-navigation" class="zone-navigation"> <div id="zone-navigation" class="zone-navigation">
<ul class="top-zone"> <ul id="top-zone" class="top-zone">
{{#if session.authenticated}} {{#if session.authenticated}}
<li> <li>
<div id="accounts-button" class="filled-tool"> <div id="accounts-button" class="filled-tool">
<i class="material-icons">apps</i> <i class="material-icons">apps</i>
</div> </div>
</li> </li>
{{else}} {{else}}
<li> <li>
{{#link-to 'folders' class='title'}} {{#link-to 'folders' class='title'}}
<div class="filled-tool" title={{appMeta.title}}> <div class="filled-tool" title={{appMeta.title}}>
<i class="material-icons">apps</i> <i class="material-icons">apps</i>
</div> </div>
{{/link-to}}
</li>
{{/if}}
<li class="{{if view.folder 'selected'}}">
{{#link-to 'folders'}}
<i class="material-icons icon-tool">home</i>
{{/link-to}} {{/link-to}}
</li> </li>
<li class="{{if view.search 'selected'}}"> {{/if}}
{{#link-to 'search'}} <li class="{{if view.folder 'selected'}}">
<i class="material-icons icon-tool">search</i> {{#link-to 'folders'}}
{{/link-to}} <i class="material-icons icon-tool">home</i>
</li> {{/link-to}}
</ul> </li>
<li class="{{if view.search 'selected'}}">
{{#link-to 'search'}}
<i class="material-icons icon-tool">search</i>
{{/link-to}}
</li>
</ul>
<ul class="bottom-zone"> <ul id="pinned-zone" class="pinned-zone">
{{#if session.session.content.authenticated.user.admin}} {{#each pins as |pin|}}
<li id="workspace-settings" class="{{if view.settings 'selected'}}" data-tooltip="Settings" data-tooltip-position="right center"> <li {{action 'jumpToPin' pin}} data-id={{pin.id}} id="pin-{{pin.id}}" data-tooltip="{{pin.pin}}" data-tooltip-position="right middle" class="pin"><span class="key">{{pin.pin}}</span></li>
{{#link-to 'customize.general'}} {{/each}}
<i class="material-icons icon-tool">tune</i> </ul>
{{/link-to}}
</li> <ul id="bottom-zone" class="bottom-zone">
{{/if}} {{#if session.session.content.authenticated.user.admin}}
{{#if session.authenticated}} <li id="workspace-settings" class="{{if view.settings 'selected'}}" data-tooltip="Settings" data-tooltip-position="right center">
<li id="workspace-logout" data-tooltip="Logout" data-tooltip-position="right center"> {{#link-to 'customize.general'}}
{{#link-to 'auth.logout'}} <i class="material-icons icon-tool">tune</i>
<i class="material-icons icon-tool">exit_to_app</i> {{/link-to}}
{{/link-to}} </li>
</li> {{/if}}
<li class="{{if view.profile 'selected'}}"> {{#if session.authenticated}}
{{#link-to 'profile'}} <li id="workspace-logout" data-tooltip="Logout" data-tooltip-position="right center">
<div class="filled-tool"> {{#link-to 'auth.logout'}}
<i class="initials">{{session.user.initials}}</i> <i class="material-icons icon-tool">exit_to_app</i>
</div> {{/link-to}}
{{/link-to}} </li>
</li> <li class="{{if view.profile 'selected'}}">
{{else}} {{#link-to 'profile'}}
<li id="workspace-login" data-tooltip="Login" data-tooltip-position="right center"> <div class="filled-tool">
{{#link-to 'auth.login'}} <i class="initials">{{session.user.initials}}</i>
<i class="material-icons icon-tool">lock_open</i> </div>
{{/link-to}} {{/link-to}}
</li> </li>
{{/if}} {{else}}
</ul> <li id="workspace-login" data-tooltip="Login" data-tooltip-position="right center">
{{#link-to 'auth.login'}}
<i class="material-icons icon-tool">lock_open</i>
{{/link-to}}
</li>
{{/if}}
</ul>
{{#if session.authenticated}} {{#if session.authenticated}}
{{#dropdown-menu target="accounts-button" position="bottom left" open="click" }} {{#dropdown-menu target="accounts-button" position="bottom left" open="click" }}

View file

@ -53,7 +53,7 @@ module.exports = function (defaults) {
app.import('vendor/drop.js'); app.import('vendor/drop.js');
app.import('vendor/tooltip.js'); app.import('vendor/tooltip.js');
app.import('vendor/markdown-it.min.js'); app.import('vendor/markdown-it.min.js');
app.import('vendor/dragula.js'); app.import('vendor/sortable.js');
app.import('vendor/datetimepicker.min.js'); app.import('vendor/datetimepicker.min.js');
app.import('vendor/hoverIntent.js'); app.import('vendor/hoverIntent.js');
app.import('vendor/waypoints.js'); app.import('vendor/waypoints.js');

907
app/vendor/dragula.js vendored
View file

@ -1,907 +0,0 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.dragula = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
var cache = {};
var start = '(?:^|\\s)';
var end = '(?:\\s|$)';
function lookupClass (className) {
var cached = cache[className];
if (cached) {
cached.lastIndex = 0;
} else {
cache[className] = cached = new RegExp(start + className + end, 'g');
}
return cached;
}
function addClass (el, className) {
var current = el.className;
if (!current.length) {
el.className = className;
} else if (!lookupClass(className).test(current)) {
el.className += ' ' + className;
}
}
function rmClass (el, className) {
el.className = el.className.replace(lookupClass(className), ' ').trim();
}
module.exports = {
add: addClass,
rm: rmClass
};
},{}],2:[function(require,module,exports){
(function (global){
'use strict';
var emitter = require('contra/emitter');
var crossvent = require('crossvent');
var classes = require('./classes');
var doc = document;
var documentElement = doc.documentElement;
function dragula (initialContainers, options) {
var len = arguments.length;
if (len === 1 && Array.isArray(initialContainers) === false) {
options = initialContainers;
initialContainers = [];
}
var _mirror; // mirror image
var _source; // source container
var _item; // item being dragged
var _offsetX; // reference x
var _offsetY; // reference y
var _moveX; // reference move x
var _moveY; // reference move y
var _initialSibling; // reference sibling when grabbed
var _currentSibling; // reference sibling now
var _copy; // item used for copying
var _renderTimer; // timer for setTimeout renderMirrorImage
var _lastDropTarget = null; // last container item was over
var _grabbed; // holds mousedown context until first mousemove
var o = options || {};
if (o.moves === void 0) { o.moves = always; }
if (o.accepts === void 0) { o.accepts = always; }
if (o.invalid === void 0) { o.invalid = invalidTarget; }
if (o.containers === void 0) { o.containers = initialContainers || []; }
if (o.isContainer === void 0) { o.isContainer = never; }
if (o.copy === void 0) { o.copy = false; }
if (o.copySortSource === void 0) { o.copySortSource = false; }
if (o.revertOnSpill === void 0) { o.revertOnSpill = false; }
if (o.removeOnSpill === void 0) { o.removeOnSpill = false; }
if (o.direction === void 0) { o.direction = 'vertical'; }
if (o.ignoreInputTextSelection === void 0) { o.ignoreInputTextSelection = true; }
if (o.mirrorContainer === void 0) { o.mirrorContainer = doc.body; }
var drake = emitter({
containers: o.containers,
start: manualStart,
end: end,
cancel: cancel,
remove: remove,
destroy: destroy,
canMove: canMove,
dragging: false
});
if (o.removeOnSpill === true) {
drake.on('over', spillOver).on('out', spillOut);
}
events();
return drake;
function isContainer (el) {
return drake.containers.indexOf(el) !== -1 || o.isContainer(el);
}
function events (remove) {
var op = remove ? 'remove' : 'add';
touchy(documentElement, op, 'mousedown', grab);
touchy(documentElement, op, 'mouseup', release);
}
function eventualMovements (remove) {
var op = remove ? 'remove' : 'add';
touchy(documentElement, op, 'mousemove', startBecauseMouseMoved);
}
function movements (remove) {
var op = remove ? 'remove' : 'add';
crossvent[op](documentElement, 'selectstart', preventGrabbed); // IE8
crossvent[op](documentElement, 'click', preventGrabbed);
}
function destroy () {
events(true);
release({});
}
function preventGrabbed (e) {
if (_grabbed) {
e.preventDefault();
}
}
function grab (e) {
_moveX = e.clientX;
_moveY = e.clientY;
var ignore = whichMouseButton(e) !== 1 || e.metaKey || e.ctrlKey;
if (ignore) {
return; // we only care about honest-to-god left clicks and touch events
}
var item = e.target;
var context = canStart(item);
if (!context) {
return;
}
_grabbed = context;
eventualMovements();
if (e.type === 'mousedown') {
if (isInput(item)) { // see also: https://github.com/bevacqua/dragula/issues/208
item.focus(); // fixes https://github.com/bevacqua/dragula/issues/176
} else {
e.preventDefault(); // fixes https://github.com/bevacqua/dragula/issues/155
}
}
}
function startBecauseMouseMoved (e) {
if (!_grabbed) {
return;
}
if (whichMouseButton(e) === 0) {
release({});
return; // when text is selected on an input and then dragged, mouseup doesn't fire. this is our only hope
}
// truthy check fixes #239, equality fixes #207
if (e.clientX !== void 0 && e.clientX === _moveX && e.clientY !== void 0 && e.clientY === _moveY) {
return;
}
if (o.ignoreInputTextSelection) {
var clientX = getCoord('clientX', e);
var clientY = getCoord('clientY', e);
var elementBehindCursor = doc.elementFromPoint(clientX, clientY);
if (isInput(elementBehindCursor)) {
return;
}
}
var grabbed = _grabbed; // call to end() unsets _grabbed
eventualMovements(true);
movements();
end();
start(grabbed);
var offset = getOffset(_item);
_offsetX = getCoord('pageX', e) - offset.left;
_offsetY = getCoord('pageY', e) - offset.top;
classes.add(_copy || _item, 'gu-transit');
renderMirrorImage();
drag(e);
}
function canStart (item) {
if (drake.dragging && _mirror) {
return;
}
if (isContainer(item)) {
return; // don't drag container itself
}
var handle = item;
while (getParent(item) && isContainer(getParent(item)) === false) {
if (o.invalid(item, handle)) {
return;
}
item = getParent(item); // drag target should be a top element
if (!item) {
return;
}
}
var source = getParent(item);
if (!source) {
return;
}
if (o.invalid(item, handle)) {
return;
}
var movable = o.moves(item, source, handle, nextEl(item));
if (!movable) {
return;
}
return {
item: item,
source: source
};
}
function canMove (item) {
return !!canStart(item);
}
function manualStart (item) {
var context = canStart(item);
if (context) {
start(context);
}
}
function start (context) {
if (isCopy(context.item, context.source)) {
_copy = context.item.cloneNode(true);
drake.emit('cloned', _copy, context.item, 'copy');
}
_source = context.source;
_item = context.item;
_initialSibling = _currentSibling = nextEl(context.item);
drake.dragging = true;
drake.emit('drag', _item, _source);
}
function invalidTarget () {
return false;
}
function end () {
if (!drake.dragging) {
return;
}
var item = _copy || _item;
drop(item, getParent(item));
}
function ungrab () {
_grabbed = false;
eventualMovements(true);
movements(true);
}
function release (e) {
ungrab();
if (!drake.dragging) {
return;
}
var item = _copy || _item;
var clientX = getCoord('clientX', e);
var clientY = getCoord('clientY', e);
var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
if (dropTarget && ((_copy && o.copySortSource) || (!_copy || dropTarget !== _source))) {
drop(item, dropTarget);
} else if (o.removeOnSpill) {
remove();
} else {
cancel();
}
}
function drop (item, target) {
var parent = getParent(item);
if (_copy && o.copySortSource && target === _source) {
parent.removeChild(_item);
}
if (isInitialPlacement(target)) {
drake.emit('cancel', item, _source, _source);
} else {
drake.emit('drop', item, target, _source, _currentSibling);
}
cleanup();
}
function remove () {
if (!drake.dragging) {
return;
}
var item = _copy || _item;
var parent = getParent(item);
if (parent) {
parent.removeChild(item);
}
drake.emit(_copy ? 'cancel' : 'remove', item, parent, _source);
cleanup();
}
function cancel (revert) {
if (!drake.dragging) {
return;
}
var reverts = arguments.length > 0 ? revert : o.revertOnSpill;
var item = _copy || _item;
var parent = getParent(item);
var initial = isInitialPlacement(parent);
if (initial === false && reverts) {
if (_copy) {
if (parent) {
parent.removeChild(_copy);
}
} else {
_source.insertBefore(item, _initialSibling);
}
}
if (initial || reverts) {
drake.emit('cancel', item, _source, _source);
} else {
drake.emit('drop', item, parent, _source, _currentSibling);
}
cleanup();
}
function cleanup () {
var item = _copy || _item;
ungrab();
removeMirrorImage();
if (item) {
classes.rm(item, 'gu-transit');
}
if (_renderTimer) {
clearTimeout(_renderTimer);
}
drake.dragging = false;
if (_lastDropTarget) {
drake.emit('out', item, _lastDropTarget, _source);
}
drake.emit('dragend', item);
_source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null;
}
function isInitialPlacement (target, s) {
var sibling;
if (s !== void 0) {
sibling = s;
} else if (_mirror) {
sibling = _currentSibling;
} else {
sibling = nextEl(_copy || _item);
}
return target === _source && sibling === _initialSibling;
}
function findDropTarget (elementBehindCursor, clientX, clientY) {
var target = elementBehindCursor;
while (target && !accepted()) {
target = getParent(target);
}
return target;
function accepted () {
var droppable = isContainer(target);
if (droppable === false) {
return false;
}
var immediate = getImmediateChild(target, elementBehindCursor);
var reference = getReference(target, immediate, clientX, clientY);
var initial = isInitialPlacement(target, reference);
if (initial) {
return true; // should always be able to drop it right back where it was
}
return o.accepts(_item, target, _source, reference);
}
}
function drag (e) {
if (!_mirror) {
return;
}
e.preventDefault();
var clientX = getCoord('clientX', e);
var clientY = getCoord('clientY', e);
var x = clientX - _offsetX;
var y = clientY - _offsetY;
_mirror.style.left = x + 'px';
_mirror.style.top = y + 'px';
var item = _copy || _item;
var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY);
var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY);
var changed = dropTarget !== null && dropTarget !== _lastDropTarget;
if (changed || dropTarget === null) {
out();
_lastDropTarget = dropTarget;
over();
}
var parent = getParent(item);
if (dropTarget === _source && _copy && !o.copySortSource) {
if (parent) {
parent.removeChild(item);
}
return;
}
var reference;
var immediate = getImmediateChild(dropTarget, elementBehindCursor);
if (immediate !== null) {
reference = getReference(dropTarget, immediate, clientX, clientY);
} else if (o.revertOnSpill === true && !_copy) {
reference = _initialSibling;
dropTarget = _source;
} else {
if (_copy && parent) {
parent.removeChild(item);
}
return;
}
if (
(reference === null && changed) ||
reference !== item &&
reference !== nextEl(item)
) {
_currentSibling = reference;
dropTarget.insertBefore(item, reference);
drake.emit('shadow', item, dropTarget, _source);
}
function moved (type) { drake.emit(type, item, _lastDropTarget, _source); }
function over () { if (changed) { moved('over'); } }
function out () { if (_lastDropTarget) { moved('out'); } }
}
function spillOver (el) {
classes.rm(el, 'gu-hide');
}
function spillOut (el) {
if (drake.dragging) { classes.add(el, 'gu-hide'); }
}
function renderMirrorImage () {
if (_mirror) {
return;
}
var rect = _item.getBoundingClientRect();
_mirror = _item.cloneNode(true);
_mirror.style.width = getRectWidth(rect) + 'px';
_mirror.style.height = getRectHeight(rect) + 'px';
classes.rm(_mirror, 'gu-transit');
classes.add(_mirror, 'gu-mirror');
o.mirrorContainer.appendChild(_mirror);
touchy(documentElement, 'add', 'mousemove', drag);
classes.add(o.mirrorContainer, 'gu-unselectable');
drake.emit('cloned', _mirror, _item, 'mirror');
}
function removeMirrorImage () {
if (_mirror) {
classes.rm(o.mirrorContainer, 'gu-unselectable');
touchy(documentElement, 'remove', 'mousemove', drag);
getParent(_mirror).removeChild(_mirror);
_mirror = null;
}
}
function getImmediateChild (dropTarget, target) {
var immediate = target;
while (immediate !== dropTarget && getParent(immediate) !== dropTarget) {
immediate = getParent(immediate);
}
if (immediate === documentElement) {
return null;
}
return immediate;
}
function getReference (dropTarget, target, x, y) {
var horizontal = o.direction === 'horizontal';
var reference = target !== dropTarget ? inside() : outside();
return reference;
function outside () { // slower, but able to figure out any position
var len = dropTarget.children.length;
var i;
var el;
var rect;
for (i = 0; i < len; i++) {
el = dropTarget.children[i];
rect = el.getBoundingClientRect();
if (horizontal && (rect.left + rect.width / 2) > x) { return el; }
if (!horizontal && (rect.top + rect.height / 2) > y) { return el; }
}
return null;
}
function inside () { // faster, but only available if dropped inside a child element
var rect = target.getBoundingClientRect();
if (horizontal) {
return resolve(x > rect.left + getRectWidth(rect) / 2);
}
return resolve(y > rect.top + getRectHeight(rect) / 2);
}
function resolve (after) {
return after ? nextEl(target) : target;
}
}
function isCopy (item, container) {
return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container);
}
}
function touchy (el, op, type, fn) {
var touch = {
mouseup: 'touchend',
mousedown: 'touchstart',
mousemove: 'touchmove'
};
var pointers = {
mouseup: 'pointerup',
mousedown: 'pointerdown',
mousemove: 'pointermove'
};
var microsoft = {
mouseup: 'MSPointerUp',
mousedown: 'MSPointerDown',
mousemove: 'MSPointerMove'
};
if (global.navigator.pointerEnabled) {
crossvent[op](el, pointers[type], fn);
} else if (global.navigator.msPointerEnabled) {
crossvent[op](el, microsoft[type], fn);
} else {
crossvent[op](el, touch[type], fn);
crossvent[op](el, type, fn);
}
}
function whichMouseButton (e) {
if (e.touches !== void 0) { return e.touches.length; }
if (e.which !== void 0 && e.which !== 0) { return e.which; } // see https://github.com/bevacqua/dragula/issues/261
if (e.buttons !== void 0) { return e.buttons; }
var button = e.button;
if (button !== void 0) { // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575
return button & 1 ? 1 : button & 2 ? 3 : (button & 4 ? 2 : 0);
}
}
function getOffset (el) {
var rect = el.getBoundingClientRect();
return {
left: rect.left + getScroll('scrollLeft', 'pageXOffset'),
top: rect.top + getScroll('scrollTop', 'pageYOffset')
};
}
function getScroll (scrollProp, offsetProp) {
if (typeof global[offsetProp] !== 'undefined') {
return global[offsetProp];
}
if (documentElement.clientHeight) {
return documentElement[scrollProp];
}
return doc.body[scrollProp];
}
function getElementBehindPoint (point, x, y) {
var p = point || {};
var state = p.className;
var el;
p.className += ' gu-hide';
el = doc.elementFromPoint(x, y);
p.className = state;
return el;
}
function never () { return false; }
function always () { return true; }
function getRectWidth (rect) { return rect.width || (rect.right - rect.left); }
function getRectHeight (rect) { return rect.height || (rect.bottom - rect.top); }
function getParent (el) { return el.parentNode === doc ? null : el.parentNode; }
function isInput (el) { return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el); }
function isEditable (el) {
if (!el) { return false; } // no parents were editable
if (el.contentEditable === 'false') { return false; } // stop the lookup
if (el.contentEditable === 'true') { return true; } // found a contentEditable element in the chain
return isEditable(getParent(el)); // contentEditable is set to 'inherit'
}
function nextEl (el) {
return el.nextElementSibling || manually();
function manually () {
var sibling = el;
do {
sibling = sibling.nextSibling;
} while (sibling && sibling.nodeType !== 1);
return sibling;
}
}
function getEventHost (e) {
// on touchend event, we have to use `e.changedTouches`
// see http://stackoverflow.com/questions/7192563/touchend-event-properties
// see https://github.com/bevacqua/dragula/issues/34
if (e.targetTouches && e.targetTouches.length) {
return e.targetTouches[0];
}
if (e.changedTouches && e.changedTouches.length) {
return e.changedTouches[0];
}
return e;
}
function getCoord (coord, e) {
var host = getEventHost(e);
var missMap = {
pageX: 'clientX', // IE8
pageY: 'clientY' // IE8
};
if (coord in missMap && !(coord in host) && missMap[coord] in host) {
coord = missMap[coord];
}
return host[coord];
}
module.exports = dragula;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./classes":1,"contra/emitter":5,"crossvent":6}],3:[function(require,module,exports){
module.exports = function atoa (a, n) { return Array.prototype.slice.call(a, n); }
},{}],4:[function(require,module,exports){
'use strict';
var ticky = require('ticky');
module.exports = function debounce (fn, args, ctx) {
if (!fn) { return; }
ticky(function run () {
fn.apply(ctx || null, args || []);
});
};
},{"ticky":9}],5:[function(require,module,exports){
'use strict';
var atoa = require('atoa');
var debounce = require('./debounce');
module.exports = function emitter (thing, options) {
var opts = options || {};
var evt = {};
if (thing === undefined) { thing = {}; }
thing.on = function (type, fn) {
if (!evt[type]) {
evt[type] = [fn];
} else {
evt[type].push(fn);
}
return thing;
};
thing.once = function (type, fn) {
fn._once = true; // thing.off(fn) still works!
thing.on(type, fn);
return thing;
};
thing.off = function (type, fn) {
var c = arguments.length;
if (c === 1) {
delete evt[type];
} else if (c === 0) {
evt = {};
} else {
var et = evt[type];
if (!et) { return thing; }
et.splice(et.indexOf(fn), 1);
}
return thing;
};
thing.emit = function () {
var args = atoa(arguments);
return thing.emitterSnapshot(args.shift()).apply(this, args);
};
thing.emitterSnapshot = function (type) {
var et = (evt[type] || []).slice(0);
return function () {
var args = atoa(arguments);
var ctx = this || thing;
if (type === 'error' && opts.throws !== false && !et.length) { throw args.length === 1 ? args[0] : args; }
et.forEach(function emitter (listen) {
if (opts.async) { debounce(listen, args, ctx); } else { listen.apply(ctx, args); }
if (listen._once) { thing.off(type, listen); }
});
return thing;
};
};
return thing;
};
},{"./debounce":4,"atoa":3}],6:[function(require,module,exports){
(function (global){
'use strict';
var customEvent = require('custom-event');
var eventmap = require('./eventmap');
var doc = global.document;
var addEvent = addEventEasy;
var removeEvent = removeEventEasy;
var hardCache = [];
if (!global.addEventListener) {
addEvent = addEventHard;
removeEvent = removeEventHard;
}
module.exports = {
add: addEvent,
remove: removeEvent,
fabricate: fabricateEvent
};
function addEventEasy (el, type, fn, capturing) {
return el.addEventListener(type, fn, capturing);
}
function addEventHard (el, type, fn) {
return el.attachEvent('on' + type, wrap(el, type, fn));
}
function removeEventEasy (el, type, fn, capturing) {
return el.removeEventListener(type, fn, capturing);
}
function removeEventHard (el, type, fn) {
var listener = unwrap(el, type, fn);
if (listener) {
return el.detachEvent('on' + type, listener);
}
}
function fabricateEvent (el, type, model) {
var e = eventmap.indexOf(type) === -1 ? makeCustomEvent() : makeClassicEvent();
if (el.dispatchEvent) {
el.dispatchEvent(e);
} else {
el.fireEvent('on' + type, e);
}
function makeClassicEvent () {
var e;
if (doc.createEvent) {
e = doc.createEvent('Event');
e.initEvent(type, true, true);
} else if (doc.createEventObject) {
e = doc.createEventObject();
}
return e;
}
function makeCustomEvent () {
return new customEvent(type, { detail: model });
}
}
function wrapperFactory (el, type, fn) {
return function wrapper (originalEvent) {
var e = originalEvent || global.event;
e.target = e.target || e.srcElement;
e.preventDefault = e.preventDefault || function preventDefault () { e.returnValue = false; };
e.stopPropagation = e.stopPropagation || function stopPropagation () { e.cancelBubble = true; };
e.which = e.which || e.keyCode;
fn.call(el, e);
};
}
function wrap (el, type, fn) {
var wrapper = unwrap(el, type, fn) || wrapperFactory(el, type, fn);
hardCache.push({
wrapper: wrapper,
element: el,
type: type,
fn: fn
});
return wrapper;
}
function unwrap (el, type, fn) {
var i = find(el, type, fn);
if (i) {
var wrapper = hardCache[i].wrapper;
hardCache.splice(i, 1); // free up a tad of memory
return wrapper;
}
}
function find (el, type, fn) {
var i, item;
for (i = 0; i < hardCache.length; i++) {
item = hardCache[i];
if (item.element === el && item.type === type && item.fn === fn) {
return i;
}
}
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./eventmap":7,"custom-event":8}],7:[function(require,module,exports){
(function (global){
'use strict';
var eventmap = [];
var eventname = '';
var ron = /^on/;
for (eventname in global) {
if (ron.test(eventname)) {
eventmap.push(eventname.slice(2));
}
}
module.exports = eventmap;
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],8:[function(require,module,exports){
(function (global){
var NativeCustomEvent = global.CustomEvent;
function useNative () {
try {
var p = new NativeCustomEvent('cat', { detail: { foo: 'bar' } });
return 'cat' === p.type && 'bar' === p.detail.foo;
} catch (e) {
}
return false;
}
/**
* Cross-browser `CustomEvent` constructor.
*
* https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent.CustomEvent
*
* @public
*/
module.exports = useNative() ? NativeCustomEvent :
// IE >= 9
'function' === typeof document.createEvent ? function CustomEvent (type, params) {
var e = document.createEvent('CustomEvent');
if (params) {
e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail);
} else {
e.initCustomEvent(type, false, false, void 0);
}
return e;
} :
// IE <= 8
function CustomEvent (type, params) {
var e = document.createEventObject();
e.type = type;
if (params) {
e.bubbles = Boolean(params.bubbles);
e.cancelable = Boolean(params.cancelable);
e.detail = params.detail;
} else {
e.bubbles = false;
e.cancelable = false;
e.detail = void 0;
}
return e;
}
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{}],9:[function(require,module,exports){
var si = typeof setImmediate === 'function', tick;
if (si) {
tick = function (fn) { setImmediate(fn); };
} else {
tick = function (fn) { setTimeout(fn, 0); };
}
module.exports = tick;
},{}]},{},[2])(2)
});

1384
app/vendor/sortable.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -276,6 +276,14 @@ func DeleteDocument(w http.ResponseWriter, r *http.Request) {
return return
} }
_, err = p.DeletePinnedDocument(documentID)
if err != nil && err != sql.ErrNoRows {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
log.IfErr(tx.Commit()) log.IfErr(tx.Commit())
writeSuccessEmptyJSON(w) writeSuccessEmptyJSON(w)

View file

@ -319,6 +319,14 @@ func RemoveFolder(w http.ResponseWriter, r *http.Request) {
return return
} }
_, err = p.DeletePinnedSpace(id)
if err != nil && err != sql.ErrNoRows {
log.IfErr(tx.Rollback())
writeServerError(w, method, err)
return
}
log.IfErr(tx.Commit()) log.IfErr(tx.Commit())
writeSuccessString(w, "{}") writeSuccessString(w, "{}")

View file

@ -0,0 +1,235 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package endpoint
import (
"database/sql"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util"
"github.com/documize/community/core/log"
"github.com/gorilla/mux"
)
// AddPin saves pinned item.
func AddPin(w http.ResponseWriter, r *http.Request) {
method := "AddPin"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if !p.Context.Authenticated {
writeForbiddenError(w)
return
}
if len(userID) == 0 {
writeMissingDataError(w, method, "userID")
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var pin entity.Pin
err = json.Unmarshal(body, &pin)
if err != nil {
writePayloadError(w, method, err)
return
}
pin.RefID = util.UniqueID()
pin.OrgID = p.Context.OrgID
pin.UserID = p.Context.UserID
pin.Pin = strings.TrimSpace(pin.Pin)
if len(pin.Pin) > 20 {
pin.Pin = pin.Pin[0:20]
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
err = p.AddPin(pin)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
newPin, err := p.GetPin(pin.RefID)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
util.WriteJSON(w, newPin)
}
// GetUserPins returns users' pins.
func GetUserPins(w http.ResponseWriter, r *http.Request) {
method := "GetUserPins"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if len(userID) == 0 {
writeMissingDataError(w, method, "userID")
return
}
if p.Context.UserID != userID {
writeForbiddenError(w)
return
}
pins, err := p.GetUserPins(userID)
if err != nil && err != sql.ErrNoRows {
writeGeneralSQLError(w, method, err)
return
}
if err == sql.ErrNoRows {
pins = []entity.Pin{}
}
json, err := json.Marshal(pins)
if err != nil {
writeJSONMarshalError(w, method, "pin", err)
return
}
writeSuccessBytes(w, json)
}
// DeleteUserPin removes saved user pin.
func DeleteUserPin(w http.ResponseWriter, r *http.Request) {
method := "DeleteUserPin"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
pinID := params["pinID"]
if len(userID) == 0 {
writeMissingDataError(w, method, "userID")
return
}
if len(pinID) == 0 {
writeMissingDataError(w, method, "pinID")
return
}
if p.Context.UserID != userID {
writeForbiddenError(w)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
_, err = p.DeletePin(pinID)
if err != nil && err != sql.ErrNoRows {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
log.IfErr(tx.Commit())
util.WriteSuccessEmptyJSON(w)
}
// UpdatePinSequence records order of pinned items.
func UpdatePinSequence(w http.ResponseWriter, r *http.Request) {
method := "UpdatePinSequence"
p := request.GetPersister(r)
params := mux.Vars(r)
userID := params["userID"]
if !p.Context.Authenticated {
writeForbiddenError(w)
return
}
if len(userID) == 0 {
writeMissingDataError(w, method, "userID")
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writePayloadError(w, method, err)
return
}
var pins []string
err = json.Unmarshal(body, &pins)
if err != nil {
writePayloadError(w, method, err)
return
}
tx, err := request.Db.Beginx()
if err != nil {
writeTransactionError(w, method, err)
return
}
p.Context.Transaction = tx
for k, v := range pins {
err = p.UpdatePinSequence(v, k+1)
if err != nil {
log.IfErr(tx.Rollback())
writeGeneralSQLError(w, method, err)
return
}
}
log.IfErr(tx.Commit())
newPins, err := p.GetUserPins(userID)
if err != nil {
writeGeneralSQLError(w, method, err)
return
}
util.WriteJSON(w, newPins)
}

View file

@ -221,6 +221,12 @@ func init() {
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig)) log.IfErr(Add(RoutePrefixPrivate, "global", []string{"GET", "OPTIONS"}, nil, GetGlobalConfig))
log.IfErr(Add(RoutePrefixPrivate, "global", []string{"PUT", "OPTIONS"}, nil, SaveGlobalConfig)) log.IfErr(Add(RoutePrefixPrivate, "global", []string{"PUT", "OPTIONS"}, nil, SaveGlobalConfig))
// Pinned items
log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}", []string{"POST", "OPTIONS"}, nil, AddPin))
log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}", []string{"GET", "OPTIONS"}, nil, GetUserPins))
log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, UpdatePinSequence))
log.IfErr(Add(RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, DeleteUserPin))
// Single page app handler // Single page app handler
log.IfErr(Add(RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, GetRobots)) log.IfErr(Add(RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, GetRobots))
log.IfErr(Add(RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, GetSitemap)) log.IfErr(Add(RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, GetSitemap))

View file

@ -385,3 +385,14 @@ type LinkCandidate struct {
Title string `json:"title"` // what we label the link Title string `json:"title"` // what we label the link
Context string `json:"context"` // additional context (e.g. excerpt, parent, file extension) Context string `json:"context"` // additional context (e.g. excerpt, parent, file extension)
} }
// Pin defines a saved link to a document or space
type Pin struct {
BaseEntity
OrgID string `json:"orgId"`
UserID string `json:"userId"`
FolderID string `json:"folderId"`
DocumentID string `json:"documentId"`
Pin string `json:"pin"`
Sequence int `json:"sequence"`
}

View file

@ -16,13 +16,12 @@ import (
"strings" "strings"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/documize/community/core/api/endpoint/models" "github.com/documize/community/core/api/endpoint/models"
"github.com/documize/community/core/api/entity" "github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/util" "github.com/documize/community/core/api/util"
"github.com/documize/community/core/log" "github.com/documize/community/core/log"
"github.com/documize/community/core/utility" "github.com/documize/community/core/utility"
"github.com/jmoiron/sqlx"
) )
// AddPage inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added. // AddPage inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added.

145
core/api/request/pin.go Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package request
import (
"fmt"
"time"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/log"
"github.com/documize/community/core/utility"
"github.com/jmoiron/sqlx"
)
// AddPin saves pinned item.
func (p *Persister) AddPin(pin entity.Pin) (err error) {
row := Db.QueryRow("SELECT max(sequence) FROM pin WHERE orgid=? AND userid=?", p.Context.OrgID, p.Context.UserID)
var maxSeq int
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 99
}
pin.Created = time.Now().UTC()
pin.Revised = time.Now().UTC()
pin.Sequence = maxSeq + 1
stmt, err := p.Context.Transaction.Preparex("INSERT INTO pin (refid, orgid, userid, labelid, documentid, pin, sequence, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer utility.Close(stmt)
if err != nil {
log.Error("Unable to prepare insert for pin", err)
return
}
_, err = stmt.Exec(pin.RefID, pin.OrgID, pin.UserID, pin.FolderID, pin.DocumentID, pin.Pin, pin.Sequence, pin.Created, pin.Revised)
if err != nil {
log.Error("Unable to execute insert for pin", err)
return
}
return
}
// GetPin returns requested pinned item.
func (p *Persister) GetPin(id string) (pin entity.Pin, err error) {
err = nil
stmt, err := Db.Preparex("SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND refid=?")
defer utility.Close(stmt)
if err != nil {
log.Error(fmt.Sprintf("Unable to prepare select for pin %s", id), err)
return
}
err = stmt.Get(&pin, p.Context.OrgID, id)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute select for pin %s", id), err)
return
}
return
}
// GetUserPins returns pinned items for specified user.
func (p *Persister) GetUserPins(userID string) (pins []entity.Pin, err error) {
err = Db.Select(&pins, "SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND userid=? ORDER BY sequence", p.Context.OrgID, userID)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute select pin for org %s and user %s", p.Context.OrgID, userID), err)
return
}
return
}
// UpdatePin updates existing pinned item.
func (p *Persister) UpdatePin(pin entity.Pin) (err error) {
err = nil
pin.Revised = time.Now().UTC()
var stmt *sqlx.NamedStmt
stmt, err = p.Context.Transaction.PrepareNamed("UPDATE pin SET labelid=:folderid, documentid=:documentid, pin=:pin, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid")
defer utility.Close(stmt)
if err != nil {
log.Error(fmt.Sprintf("Unable to prepare update for pin %s", pin.RefID), err)
return
}
_, err = stmt.Exec(&pin)
if err != nil {
log.Error(fmt.Sprintf("Unable to execute update for pin %s", pin.RefID), err)
return
}
return
}
// UpdatePinSequence updates existing pinned item sequence number
func (p *Persister) UpdatePinSequence(pinID string, sequence int) (err error) {
err = nil
stmt, err := p.Context.Transaction.Preparex("UPDATE pin SET sequence=?, revised=? WHERE orgid=? AND userid=? AND refid=?")
defer utility.Close(stmt)
if err != nil {
log.Error(fmt.Sprintf("Unable to prepare update for pin sequence %s", pinID), err)
return
}
_, err = stmt.Exec(sequence, time.Now().UTC(), p.Context.OrgID, p.Context.UserID, pinID)
return
}
// DeletePin removes folder from the store.
func (p *Persister) DeletePin(id string) (rows int64, err error) {
return p.Base.DeleteConstrained(p.Context.Transaction, "pin", p.Context.OrgID, id)
}
// DeletePinnedSpace removes any pins for specified space.
func (p *Persister) DeletePinnedSpace(spaceID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND labelid=\"%s\"", p.Context.OrgID, spaceID))
}
// DeletePinnedDocument removes any pins for specified document.
func (p *Persister) DeletePinnedDocument(documentID string) (rows int64, err error) {
return p.Base.DeleteWhere(p.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND documentid=\"%s\"", p.Context.OrgID, documentID))
}

View file

@ -351,3 +351,21 @@ CREATE TABLE IF NOT EXISTS `participant` (
INDEX `idx_participant_documentid` (`documentid` ASC)) INDEX `idx_participant_documentid` (`documentid` ASC))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB; ENGINE = InnoDB;
DROP TABLE IF EXISTS `pin`;
CREATE TABLE IF NOT EXISTS `pin` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`labelid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`documentid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`sequence` INT UNSIGNED NOT NULL DEFAULT 99,
`pin` CHAR(20) NOT NULL DEFAULT '' COLLATE utf8_bin,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id),
INDEX `idx_pin_userid` (`userid` ASC))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;

View file

@ -0,0 +1,18 @@
/* community edition */
DROP TABLE IF EXISTS `pin`;
CREATE TABLE IF NOT EXISTS `pin` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`userid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`labelid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`documentid` CHAR(16) DEFAULT '' COLLATE utf8_bin,
`sequence` INT UNSIGNED NOT NULL DEFAULT 99,
`pin` CHAR(20) NOT NULL DEFAULT '' COLLATE utf8_bin,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT pk_id PRIMARY KEY (id),
INDEX `idx_pin_userid` (`userid` ASC))
DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci
ENGINE = InnoDB;

View file

@ -26,7 +26,7 @@ type ProdInfo struct {
// Product returns product edition details // Product returns product edition details
func Product() (p ProdInfo) { func Product() (p ProdInfo) {
p.Major = "0" p.Major = "0"
p.Minor = "33" p.Minor = "34"
p.Patch = "0" p.Patch = "0"
p.Version = fmt.Sprintf("%s.%s.%s", p.Major, p.Minor, p.Patch) p.Version = fmt.Sprintf("%s.%s.%s", p.Major, p.Minor, p.Patch)
p.Edition = "Community" p.Edition = "Community"

File diff suppressed because one or more lines are too long