Collections (#1294)

Collections
This commit is contained in:
Julius Härtl
2019-10-24 17:20:54 +02:00
committed by GitHub
67 changed files with 534 additions and 16432 deletions

8
.gitignore vendored
View File

@@ -1,11 +1,5 @@
node_modules/*
js/*deck.js
js/*deck.js.map
js/node_modules/*
js/vendor/
js/public/
js/build/
js/
build/
css/style.css
css/vendor.css

View File

@@ -1,12 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['last 2 versions', 'ie >= 11']
}
}
]
]
}

View File

@@ -1,49 +0,0 @@
{
"esversion": 6,
"globals": {
"jasmine" : false,
"spyOn" : false,
"it" : false,
"describe" : false,
"expect" : false,
"beforeEach" : false,
"waits" : false,
"waitsFor" : false,
"runs" : false,
"require" : false,
"module": true
},
"asi" : true,
"boss" : true,
"browser" : true,
"curly" : true,
"debug" : true,
"devel" : true,
"eqeqeq" : true,
"eqnull" : false,
"evil" : false,
"forin" : true,
"immed" : true,
"indent" : 4,
"jquery" : true,
"latedef" : true,
"laxbreak" : false,
"newcap" : true,
"noarg" : true,
"node" : false,
"noempty" : false,
"nomen" : false,
"nonew" : true,
"onevar" : true,
"plusplus" : false,
"quotmark" : "single",
"regexp" : false,
"sub" : true,
"trailing" : true,
"undef" : true,
"unused" : true,
"white" : false,
"scripturl" : true
}

View File

@@ -1,64 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global angular */
angular.module('markdown', [])
.provider('markdown', [function () {
var opts = {};
return {
config: function (newOpts) {
opts = newOpts;
},
$get: function () {
return new window.showdown.Converter(opts);
}
};
}])
.filter('markdown', ['markdown', function (markdown) {
return function (text) {
return markdown.makeHtml(text || '');
};
}]);
import uirouter from '@uirouter/angularjs';
import ngsanitize from 'angular-sanitize';
import angularuiselect from 'ui-select';
import ngsortable from 'ng-sortable';
import md from 'angular-markdown-it';
import nganimate from 'angular-animate';
import 'angular-file-upload';
import ngInfiniteScroll from 'ng-infinite-scroll';
import '../legacy/jquery.atwho.min';
import '../legacy/jquery.caret.min';
var app = angular.module('Deck', [
ngsanitize,
uirouter,
angularuiselect,
ngsortable, md, nganimate,
'angularFileUpload',
ngInfiniteScroll
]);
export default app;

View File

@@ -1,117 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global app oc_requesttoken markdownitLinkTarget */
import app from './App.js';
import md from 'angular-markdown-it';
import markdownitLinkTarget from 'markdown-it-link-target';
import markdownitCheckbox from 'legacy/markdown-it-checkbox.js';
app.config(function ($provide, $interpolateProvider, $httpProvider, $urlRouterProvider, $stateProvider, $compileProvider, markdownItConverterProvider) {
'use strict';
$httpProvider.defaults.headers.common.requesttoken = oc_requesttoken;
$compileProvider.debugInfoEnabled(true);
// This should fix adding "unsafe:" prefix to ui-select href links containing javascript
// inline JS is blocked by CSP anyway and filtered out by our markdown renderer as well
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|javascript):/);
markdownItConverterProvider.config({
breaks: true,
linkify: true,
xhtmlOut: true
});
markdownItConverterProvider.use(markdownitLinkTarget).use(markdownitCheckbox);
$urlRouterProvider.otherwise('/');
$stateProvider
.state('list', {
url: '/:filter',
templateUrl: '/boardlist.mainView.html',
controller: 'ListController',
reloadOnSearch: false,
params: {
filter: {value: '', dynamic: true}
}
})
.state('board', {
url: '/board/:boardId/:filter',
templateUrl: '/board.html',
controller: 'BoardController',
params: {
filter: {value: '', dynamic: true}
}
})
.state('board.detail', {
url: '/detail/',
reloadOnSearch: false,
params: {
tab: {value: 0, dynamic: true},
},
views: {
'sidebarView@': {
templateUrl: '/board.sidebarView.html',
controller: 'BoardController'
}
}
})
.state('board.card', {
url: '/card/:cardId',
params: {
tab: {value: 0, dynamic: true},
},
views: {
'sidebarView@': {
templateUrl: '/card.sidebarView.html',
controller: 'CardController'
}
}
});
$provide.decorator('nvFileOverDirective', function ($delegate) {
var directive = $delegate[0],
link = directive.link;
directive.compile = function () {
return function (scope, element, attrs) {
var overClass = attrs.overClass || 'nv-file-over';
link.apply(this, arguments);
let counter = 0;
element.on('dragenter', function (event) {
counter++;
});
element.on('dragleave', function (event) {
counter--;
if (counter <= 0) {
$('.' + overClass).removeClass(overClass);
}
});
};
};
return $delegate;
});
});

View File

@@ -1,64 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from './App.js';
/* global Snap */
app.run(function ($document, $rootScope, $transitions, BoardService) {
'use strict';
$document.click(function (event) {
$rootScope.$broadcast('documentClicked', event);
});
$transitions.onEnter({from: 'list'}, function ($state, $transition$) {
BoardService.unsetCurrrent();
});
$transitions.onEnter({to: 'list'}, function ($state, $transition$) {
BoardService.unsetCurrrent();
document.title = "Deck - " + oc_defaults.name;
});
$transitions.onEnter({to: 'board.card'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board.detail'}, function ($state, $transition$) {
$rootScope.sidebar.show = true;
});
$transitions.onEnter({to: 'board'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.card'}, function ($state) {
$rootScope.sidebar.show = false;
});
$transitions.onExit({from: 'board.detail'}, function ($state) {
$rootScope.sidebar.show = false;
});
$('link[rel="shortcut icon"]').attr(
'href',
OC.filePath('deck', 'img', 'app-512.png')
);
// Select all elements with data-toggle="tooltips" in the document
$('body').tooltip({
selector: '[data-toggle="tooltip"]'
});
});

View File

@@ -1,350 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global OC OCA OCP t escapeHTML Handlebars moment */
import CommentCollection from '../legacy/commentcollection';
import CommentModel from '../legacy/commentmodel';
class ActivityController {
constructor ($scope, CardService, ActivityService, BoardService) {
'ngInject';
this.cardservice = CardService;
this.boardservice = BoardService;
this.activityservice = ActivityService;
this.$scope = $scope;
this.type = '';
this.loading = false;
this.status = {
commentCreateLoading: false
};
this.$scope.newComment = '';
this.$scope.newCommentString = 'New comment…';
this.currentUser = OC.getCurrentUser();
const self = this;
this.$scope.$watch(function () {
return self.element.id;
}, function (params) {
if (self.type === 'deck_card') {
self.activityservice.loadComments(self.element.id);
}
if (self.getData(self.element.id).length === 0) {
self.loading = true;
self.fetchUntilResults();
}
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
if (self.type === 'deck_card') {
self.cardservice.getCurrent().commentsUnread = 0;
}
}, true);
let $target = $('.newCommentForm .message');
this.applyAtWho($target);
this.activityservice.subscribe(this.$scope, function() {
self.$scope.$apply();
});
if (typeof OCA.Activity.Templates !== 'undefined') {
OCA.Activity.Templates.userLocal = Handlebars.template({"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
var helper;
// Compiled handlesbars template
// '<span class="avatar-name-wrapper"><avatar ng-attr-contactsmenu ng-attr-tooltip ng-attr-user="{{ id }}" ng-attr-displayname="{{name}}" ng-attr-size="16"></avatar> {{ name }}</span>';
return "<span class=\"avatar-name-wrapper\"><avatar ng-attr-contactsmenu ng-attr-tooltip ng-attr-user=\""
+ container.escapeExpression(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"id","hash":{},"data":data}) : helper)))
+ "\" ng-attr-displayname=\""
+ container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"name","hash":{},"data":data}) : helper)))
+ "\" ng-attr-size=\"16\"></avatar> "
+ container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : {},{"name":"name","hash":{},"data":data}) : helper)))
+ "</span>";
},"useData":true});
} else {
OCA.Activity.RichObjectStringParser._userLocalTemplate = '<span class="avatar-name-wrapper"><avatar ng-attr-contactsmenu ng-attr-tooltip ng-attr-user="{{ id }}" ng-attr-displayname="{{name}}" ng-attr-size="16"></avatar> {{ name }}</span>';
}
}
applyAtWho($target) {
const self = this;
if (!$target) {
return;
}
$target.atwho({
at: '@',
callbacks: {
remoteFilter: function(query, callback) {
let uids = self.boardservice.getUsers();
uids = uids.filter((x) => x.uid.toLowerCase().includes(query.toLowerCase()) || x.displayname.toLowerCase().includes(query.toLowerCase()));
callback(uids);
},
highlighter: function (li) {
// misuse the highlighter callback to instead of
// highlighting loads the avatars.
var $li = $(li);
$li.find('.avatar').avatar(undefined, 32);
return $li;
},
sorter: function (q, items) { return items; }
},
displayTpl: function (item) {
return '<li>' +
'<span class="avatar-name-wrapper">' +
'<span class="avatar" ' +
'data-username="' + escapeHTML(item.uid) + '" ' + // for avatars
'data-user="' + escapeHTML(item.uid) + '" ' + // for contactsmenu
'data-user-display-name="' + escapeHTML(item.displayname) + '">' +
'</span>' +
'<strong>' + escapeHTML(item.displayname) + '</strong>' +
'</span></li>';
},
insertTpl: function (item) {
return '' +
'<span class="avatar-name-wrapper">' +
'<span class="avatar" ' +
'data-username="' + escapeHTML(item.uid) + '" ' + // for avatars
'data-user="' + escapeHTML(item.uid) + '" ' + // for contactsmenu
'data-user-display-name="' + escapeHTML(item.displayname) + '">' +
'</span>' +
'<strong>' + escapeHTML(item.displayname) + '</strong>' +
'</span>';
},
searchKey: 'displayname'
});
$target.on('inserted.atwho', function (je, $el) {
$(je.target).find(
'span[data-username="' + $el.find('[data-username]').data('username') + '"]'
).avatar(undefined, 16);
});
$target.on('shown.atwho', function (je) {
$target.find('.avatar').avatar(undefined, 16);
});
}
commentBodyToPlain(content) {
let $comment = $('<div/>').html(content);
$comment.find('.avatar-name-wrapper').each(function () {
var $this = $(this);
var $inserted = $this.parent();
$inserted.html('@' + $this.find('.avatar').data('username'));
});
$comment.html(OCP.Comments.richToPlain($comment.html()));
$comment.html($comment.html().replace(/<br\s*[\/]?>/gi, '\n'));
return $comment.text();
}
static _composeHTMLMention(uid, displayName) {
var avatar = '' +
'<span class="avatar" data-username="' + escapeHTML(uid) + '" data-user="' + escapeHTML(uid) + '" ng-attr-size="16" ' +
'ng-attr-user="' + escapeHTML(uid) + '" ' +
'ng-attr-displayname="' + escapeHTML(displayName) + '" ng-attr-contactsmenu="true">' +
'</span>';
var isCurrentUser = (uid === OC.getCurrentUser().uid);
return '' +
'<span class="atwho-inserted" contenteditable="false">' +
'<span class="avatar-name-wrapper' + (isCurrentUser ? ' currentUser' : '') + '">' +
avatar +
'<strong>' + escapeHTML(displayName) + '</strong>' +
'</span>' +
'</span>';
}
formatMessage(activity) {
let message = activity.message;
let mentions = activity.commentModel.get('mentions');
const editMode = false;
message = escapeHTML(message).replace(/\n/g, '<br/>');
for(var i in mentions) {
if(!mentions.hasOwnProperty(i)) {
return;
}
var mention = '@' + mentions[i].mentionId;
// escape possible regex characters in the name
mention = mention.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const displayName = ActivityController._composeHTMLMention(mentions[i].mentionId, mentions[i].mentionDisplayName);
// replace every mention either at the start of the input or after a whitespace
// followed by a non-word character.
message = message.replace(new RegExp('(^|\\s)(' + mention + ')\\b', 'g'),
function(match, p1) {
// to get number of whitespaces (0 vs 1) right
return p1+displayName;
}
);
}
if(editMode !== true) {
message = OCP.Comments.plainToRich(message);
}
return message;
}
postComment() {
const self = this;
this.status.commentCreateLoading = true;
let content = this.commentBodyToPlain(self.$scope.newComment);
if (content.length < 1) {
self.status.commentCreateLoading = false;
OC.Notification.showTemporary(t('deck', 'Please provide a content for your comment.'));
return;
}
var model = this.activityservice.commentCollection.create({
actorId: OC.getCurrentUser().uid,
actorDisplayName: OC.getCurrentUser().displayName,
actorType: 'users',
verb: 'comment',
message: content,
creationDateTime: (new Date()).toUTCString()
}, {
at: 0,
// wait for real creation before adding
wait: true,
success: function() {
self.$scope.newComment = '';
self.activityservice.fetchNewerActivities(self.type, self.element.id).then(function () {});
self.status.commentCreateLoading = false;
},
error: function() {
self.status.commentCreateLoading = false;
OC.Notification.showTemporary(t('deck', 'Posting the comment failed.'));
}
});
}
updateComment(item) {
item.commentEdit = this.formatMessage(item);
let $target = $('.newCommentForm .message');
this.applyAtWho($target);
/** Workaround to trigger avatar rendering after the view has been updated */
window.setTimeout(function () {
$target.find('.avatar').avatar(undefined, 16);
}, 0);
}
editComment(item) {
const self = this;
let content = this.commentBodyToPlain(item.commentEdit);
if (content.length < 1) {
OC.Notification.showTemporary(t('deck', 'Please provide a content for your comment.'));
return;
}
/** We need to save the model and afterwards run a fetch to update the mentions
* and call apply to propagate the changes to angular
*/
item.commentModel.on('sync', function() {
item.commentModel.off('sync');
item.commentModel.fetch({
success: function() {
self.$scope.$apply();
}
});
});
item.commentModel.save({
message: content,
});
item.message = content;
item.commentEdit = undefined;
}
deleteComment(item) {
item.commentModel.destroy();
item.deleted = true;
item.commentModel = undefined;
item.message = t('deck', 'The comment has been deleted');
}
getData(id) {
return this.activityservice.getData(this.type, id);
}
parseMessage(activity) {
let subject = activity.subject_rich[0];
let parameters = activity.subject_rich[1];
if (parameters.after && typeof parameters.after.id === 'string' && parameters.after.id.startsWith('dt:')) {
let dateTime = parameters.after.id.substr(3);
parameters.after.name = moment(dateTime).format('L LTS');
}
return OCA.Activity.RichObjectStringParser.parseMessage(subject, parameters);
}
fetchUntilResults () {
const self = this;
let dataLengthBefore = self.getData(self.element.id).length;
let _executeFetch = function() {
let promise = self.activityservice.fetchMoreActivities(self.type, self.element.id);
promise.then(function (data) {
let dataLengthAfter = self.getData(self.element.id).length;
if (data !== null && (dataLengthAfter <= dataLengthBefore || dataLengthAfter < self.activityservice.RESULT_PER_PAGE)) {
_executeFetch();
} else {
self.loading = false;
}
}, function () {
self.loading = false;
self.$scope.$apply();
});
};
_executeFetch();
}
getComments() {
return this.activityservice.comments;
}
getActivityStream() {
let activities = this.activityservice.getData(this.type, this.element.id);
return activities;
}
page() {
if (!this.activityservice.since[this.type][this.element.id].finished) {
this.loading = true;
this.fetchUntilResults();
} else {
this.loading = false;
}
}
loadingNewer() {
return this.activityservice.runningNewer;
}
t(text) {
return t('deck', text);
}
}
let activityComponent = {
templateUrl: OC.linkTo('deck', 'templates/part.card.activity.html'),
controller: ActivityController,
bindings: {
type: '@',
element: '='
}
};
export default activityComponent;

View File

@@ -1,44 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* globals oc_current_user: false */
app.controller('AppController', function ($scope, $location, $http, $log, $rootScope, $attrs) {
$rootScope.sidebar = {
show: false
};
$scope.sidebar = $rootScope.sidebar;
$scope.user = oc_current_user;
$rootScope.config = JSON.parse($attrs.config);
$rootScope.compactMode = localStorage.getItem('deck.compactMode') === 'true';
$scope.appNavigationHide = localStorage.getItem('deck.appNavigationHide') === 'true';
$scope.toggleSidebar = function() {
if ($(window).width() > 768) {
$log.debug($scope.appNavigationHide);
$scope.appNavigationHide = !$scope.appNavigationHide;
localStorage.setItem('deck.appNavigationHide', JSON.stringify($scope.appNavigationHide));
}
};
});

View File

@@ -1,78 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global OC */
class AttachmentListController {
constructor ($scope, CardService, FileService) {
'ngInject';
this.cardservice = CardService;
this.fileservice = FileService;
this.attachments = CardService.getCurrent().attachments;
}
mimetypeForAttachment(attachment) {
let url = OC.MimeType.getIconUrl(attachment.extendedData.mimetype);
let styles = {
'background-image': `url("${url}")`,
};
return styles;
}
attachmentUrl(attachment) {
let cardId = this.cardservice.getCurrent().id;
let attachmentId = attachment.id;
return OC.generateUrl(`/apps/deck/cards/${cardId}/attachment/${attachmentId}`);
}
getAttachmentMarkdown(attachment) {
const inlineMimetypes = ['image/png', 'image/jpg', 'image/jpeg'];
let url = this.attachmentUrl(attachment);
let filename = attachment.data;
let insertText = `[📎 ${filename}](${url})`;
if (inlineMimetypes.indexOf(attachment.extendedData.mimetype) > -1) {
insertText = `![📎 ${filename}](${url})`;
}
return insertText;
}
select(attachment) {
this.onSelect({attachment: this.getAttachmentMarkdown(attachment)});
}
abort() {
this.onAbort();
}
}
let attachmentListComponent = {
templateUrl: '/card.attachments.html',
controller: AttachmentListController,
bindings: {
isFileSelector: '<',
attachments: '=',
onSelect: '&',
onAbort: '&'
}
};
export default attachmentListComponent;

View File

@@ -1,568 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global oc_defaults oc_config OC OCP OCA t n */
import app from '../app/App.js';
import Vue from 'vue';
Vue.prototype.t = t;
Vue.prototype.n = n;
Vue.prototype.OC = OC;
import CollaborationView from '../views/CollaborationView';
app.controller('BoardController', function ($rootScope, $scope, $element, $stateParams, StatusService, BoardService, StackService, CardService, LabelService, $state, $transitions, $filter, FileService) {
$scope.sidebar = $rootScope.sidebar;
$scope.id = $stateParams.boardId;
$scope.status = {
addCard: [],
};
$scope.newLabel = {};
$scope.OC = OC;
$scope.stackservice = StackService;
$scope.boardservice = BoardService;
$scope.cardservice = CardService;
$scope.statusservice = StatusService.getInstance();
$scope.labelservice = LabelService;
$scope.defaultColors = ['31CC7C', '317CCC', 'FF7A66', 'F1DB50', '7C31CC', 'CC317C', '3A3B3D', 'CACBCD'];
$scope.board = BoardService.getCurrent();
$scope.uploader = FileService.uploader;
$scope.searchText = '';
$scope.startTitleEdit = function(card) {
card.renameTitle = card.title;
card.status = card.status || {};
card.status.editCard = true;
};
$scope.finishTitleEdit = function(card) {
var newTitle;
if (!card.renameTitle || !card.renameTitle.trim()) {
newTitle = '';
} else {
newTitle = card.renameTitle.trim();
}
if (newTitle === card.title) {
// title unchanged
card.status.editCard = false;
delete card.renameTitle;
} else if (newTitle !== '') {
// title changed
card.title = newTitle;
CardService.update(card).then(function (data) {
card.status.editCard = false;
delete card.renameTitle;
});
} else {
// empty title
card.status.editCard = false;
delete card.renameTitle;
}
};
$scope.$watch(function() {
return $scope.params.tab;
}, function (newTab, oldTab) {
if (newTab === 2 && oldTab !== 2) {
CardService.fetchDeleted($scope.id);
StackService.fetchDeleted($scope.id);
}
});
// workaround for $stateParams changes not being propagated
$scope.$watch(function() {
return $state.params;
}, function (params) {
$scope.params = params;
}, true);
$scope.params = $state.params;
/**
* Check for markdown checkboxes in description to render the counter
*
* This should probably be moved to the backend at some point
*
* @param text
* @returns array of [finished, total] checkboxes
*/
$scope.getCheckboxes = function(text) {
const regTotal = /\[(X|\s|\_|\-)\]/igm;
const regFinished = /\[(X|\_|\-)\]/igm;
return [
((text || '').match(regFinished) || []).length,
((text || '').match(regTotal) || []).length
];
};
$scope.search = function (searchText) {
$scope.searchText = searchText;
$scope.refreshData();
};
$scope.$watch(function () {
if (typeof BoardService.getCurrent() !== 'undefined') {
return BoardService.getCurrent().title;
} else {
return null;
}
}, function () {
$scope.setPageTitle();
});
$scope.setPageTitle = function () {
if (BoardService.getCurrent()) {
document.title = BoardService.getCurrent().title + ' | Deck - ' + oc_defaults.name;
} else {
document.title = 'Deck - ' + oc_defaults.name;
}
};
$scope.statusservice.retainWaiting();
$scope.statusservice.retainWaiting();
// handle filter parameter for switching between archived/unarchived cards
$scope.switchFilter = function (filter) {
$state.go('.', {filter: filter});
};
$scope.$watch(function() {
return $scope.params.filter;
}, function (filter) {
if (filter === 'archive') {
$scope.loadArchived();
} else {
$scope.loadDefault();
}
});
if (parseInt(oc_config.version.split('.')[0]) >= 16) {
const ComponentVM = new Vue({
render: h => h(CollaborationView),
data: {
model: BoardService.getCurrent()
},
});
$scope.mountCollections = function () {
const MountingPoint = document.getElementById('collaborationResources');
if (MountingPoint) {
ComponentVM.model = BoardService.getCurrent();
ComponentVM.$mount(MountingPoint);
}
};
$scope.$$postDigest($scope.mountCollections);
$scope.$watch(function () {
return BoardService.getCurrent();
}, function () {
ComponentVM.model = BoardService.getCurrent();
if ($scope.sidebar.show) {
$scope.$$postDigest($scope.mountCollections);
}
});
}
$scope.toggleCompactMode = function() {
$rootScope.compactMode = !$rootScope.compactMode;
localStorage.setItem('deck.compactMode', JSON.stringify($rootScope.compactMode));
};
$scope.stacksData = StackService;
$scope.stacks = [];
$scope.$watch('stacksData', function () {
$scope.refreshData();
}, true);
$scope.refreshData = function () {
if ($scope.params.filter === 'archive') {
$scope.filterData('-lastModified', $scope.searchText);
} else {
$scope.filterData('order', $scope.searchText);
}
};
$scope.checkCanEdit = function () {
return !BoardService.getCurrent().archived;
};
// filter cards here, as ng-sortable will not work nicely with html-inline filters
$scope.filterData = function (order, text) {
if ($scope.stacks === undefined) {
return;
}
angular.copy(StackService.getData(), $scope.stacks);
$scope.stacks = $filter('orderBy')($scope.stacks, 'order');
angular.forEach($scope.stacks, function (value, key) {
var cards = $filter('cardSearchFilter')(value.cards, text);
cards = $filter('orderBy')(cards, order);
$scope.stacks[key].cards = cards;
});
};
$scope.loadDefault = function () {
StackService.fetchAll($scope.id).then(function (data) {
$scope.statusservice.releaseWaiting();
}, function (error) {
$scope.statusservice.setError('Error occured', error);
});
};
$scope.loadArchived = function () {
StackService.fetchArchived($scope.id).then(function (data) {
$scope.statusservice.releaseWaiting();
}, function (error) {
$scope.statusservice.setError('Error occured', error);
});
};
// Handle initial Loading
BoardService.fetchOne($scope.id).then(function (data) {
$scope.statusservice.releaseWaiting();
$scope.setPageTitle();
}, function (error) {
$scope.statusservice.setError('Error occured', error);
});
$scope.searchForUser = function (search) {
BoardService.searchUsers(search);
};
$scope.newStack = {'boardId': $scope.id};
$scope.newCard = {};
// Create a new Stack
$scope.createStack = function () {
StackService.create($scope.newStack).then(function (data) {
$scope.newStack.title = '';
});
};
$scope.createCard = function (stack, title) {
if (this['addCardForm' + stack].$valid) {
var newCard = {
'title': title,
'stackId': stack,
'type': 'plain'
};
CardService.create(newCard).then(function (data) {
$scope.stackservice.addCard(data);
$scope.newCard.title = '';
});
}
};
$scope.stackDelete = function (stack) {
$scope.stackservice.delete(stack.id);
};
$scope.stackUndoDelete = function (deletedStack) {
return StackService.undoDelete(deletedStack);
};
$scope.cardDelete = function (card) {
CardService.delete(card.id).then(function () {
StackService.removeCard(card);
$scope.sidebar.show = false;
});
};
$scope.cardOrCardAndStackUndoDelete = function (deletedCard) {
var associatedDeletedStack = $scope.stackservice.deleted[deletedCard.stackId];
if(associatedDeletedStack !== undefined) {
$scope.cardAndStackUndoDeleteAskForConfirmation(deletedCard, associatedDeletedStack);
} else {
$scope.cardUndoDelete(deletedCard);
}
};
$scope.cardAndStackUndoDeleteAskForConfirmation = function(deletedCard, associatedDeletedStack) {
OC.dialogs.confirm(
t('deck', 'The associated stack is deleted as well, it will be restored as well.'),
t('deck', 'Restore associated stack'),
function(state) {
if (state) {
$scope.cardAndStackUndoDelete(deletedCard, associatedDeletedStack);
}
}
);
};
$scope.cardAndStackUndoDelete = function(deletedCard, associatedDeletedStack) {
$scope.stackUndoDelete(associatedDeletedStack).then(function() {
$scope.cardUndoDelete(deletedCard);
});
};
$scope.cardUndoDelete = function(deletedCard) {
CardService.undoDelete(deletedCard).then(function() {
StackService.addCard(deletedCard);
});
};
$scope.cardArchive = function (card) {
CardService.archive(card);
StackService.removeCard(card);
};
$scope.isCurrentUserAssigned = function (card) {
if (! CardService.get(card.id).assignedUsers) {
return false;
}
var userList = CardService.get(card.id).assignedUsers.filter(function (obj) {
return obj.participant.uid === OC.getCurrentUser().uid;
});
return userList.length === 1;
};
$scope.cardAssignToMe = function (card) {
CardService.assignUser(card, OC.getCurrentUser().uid)
.then(
function() {StackService.updateCard(card);}
);
// TODO: remove this jquery call. Fix and use appPopoverMenuUtils instead
$('.popovermenu').addClass('hidden');
};
$scope.cardUnassignFromMe = function (card) {
CardService.unassignUser(card, OC.getCurrentUser().uid);
StackService.updateCard(card);
// TODO: remove this jquery call.Fix and use appPopoverMenuUtils instead
$('.popovermenu').addClass('hidden');
};
$scope.cardUnarchive = function (card) {
CardService.unarchive(card);
StackService.removeCard(card);
};
$scope.labelDelete = function (label) {
LabelService.delete(label.id);
// remove from board data
var i = BoardService.getCurrent().labels.indexOf(label);
BoardService.getCurrent().labels.splice(i, 1);
// remove from cards
var cards = CardService.data;
for (var card in cards) {
if (Object.prototype.hasOwnProperty.call(cards, card)) {
var labelsFromCard = cards[card].labels;
labelsFromCard.forEach(function (labelFromCard, index) {
if (labelFromCard.id === label.id) {
cards[card].labels.splice(index, 1);
}
});
}
}
};
$scope.labelCreate = function (label) {
label.boardId = $scope.id;
LabelService.create(label).then(function (data) {
$scope.newStack.title = '';
BoardService.getCurrent().labels.push(data);
$scope.status.createLabel = false;
$scope.newLabel = {};
}).catch((err) => {
OC.Notification.showTemporary(err);
});
};
$scope.labelUpdateBefore = function (label) {
label.renameTitle = label.title;
};
$scope.labelUpdate = function (label) {
label.edit = false;
LabelService.update(label).catch((err) => {
label.title = label.renameTitle;
OC.Notification.showTemporary(err);
});
// update labels in UI
var cards = CardService.data;
for (var card in cards) {
if (Object.prototype.hasOwnProperty.call(cards, card)) {
var labelsFromCard = cards[card].labels;
labelsFromCard.forEach(function (labelFromCard, index) {
if (labelFromCard.id === label.id) {
cards[card].labels[index] = label;
}
});
}
}
};
$scope.aclAdd = function (sharee) {
sharee.boardId = $scope.id;
BoardService.addAcl(sharee);
$scope.status.addSharee = null;
};
$scope.aclDelete = function (acl) {
BoardService.deleteAcl(acl).then(function(data) {
$scope.loadDefault();
$scope.refreshData();
});
};
$scope.aclUpdate = function (acl) {
BoardService.updateAcl(acl);
};
$scope.aclTypeString = function (acl) {
if (typeof acl === 'undefined') {
return '';
}
switch (acl.type) {
case OC.Share.SHARE_TYPE_USER:
return 'user';
case OC.Share.SHARE_TYPE_GROUP:
return 'group';
case OC.Share.SHARE_TYPE_CIRCLE:
return 'circles';
default:
return '';
}
};
// settings for card sorting
$scope.sortOptions = {
id: 'card',
itemMoved: function (event) {
event.source.itemScope.modelValue.status = event.dest.sortableScope.$parent.column;
var order = event.dest.index;
var card = $scope.cardservice.get(event.source.itemScope.c.id);
var newStack = event.dest.sortableScope.$parent.s.id;
var oldStack = card.stackId;
card.stackId = newStack;
CardService.update(card);
CardService.reorder(card, order).then(function (data) {
StackService.addCard(card);
StackService.reorderCard(card, order);
StackService.removeCard({
id: card.id,
stackId: oldStack
});
});
},
orderChanged: function (event) {
var order = event.dest.index;
var card = $scope.cardservice.get(event.source.itemScope.c.id);
var stack = event.dest.sortableScope.$parent.s.id;
CardService.reorder(card, order).then(function (data) {
StackService.reorderCard(card, order);
$scope.refreshData();
});
},
scrollableContainer: '#innerBoard',
containerPositioning: 'relative',
containment: '#innerBoard',
longTouch: true,
// auto scroll on drag
dragMove: function (itemPosition, containment, eventObj) {
if (eventObj) {
var container = $('#board');
var offset = container.offset();
var targetX = eventObj.pageX - (offset.left || container.scrollLeft());
var targetY = eventObj.pageY - (offset.top || container.scrollTop());
if (targetX < offset.left) {
container.scrollLeft(container.scrollLeft() - 25);
} else if (targetX > container.width()) {
container.scrollLeft(container.scrollLeft() + 25);
}
if (targetY < offset.top) {
container.scrollTop(container.scrollTop() - 25);
} else if (targetY > container.height()) {
container.scrollTop(container.scrollTop() + 25);
}
}
},
accept: function (sourceItemHandleScope, destSortableScope, destItemScope) {
return sourceItemHandleScope.sortableScope.options.id === 'card';
}
};
$scope.sortOptionsStack = {
id: 'stack',
orderChanged: function (event) {
var order = event.dest.index;
var stack = event.source.itemScope.s;
StackService.reorder(stack, order).then(function (data) {
$scope.refreshData();
});
},
scrollableContainer: '#board',
containerPositioning: 'relative',
containment: '#innerBoard',
dragMove: function (itemPosition, containment, eventObj) {
if (eventObj) {
var container = $('#board');
var offset = container.offset();
var targetX = eventObj.pageX - (offset.left || container.scrollLeft());
var targetY = eventObj.pageY - (offset.top || container.scrollTop());
if (targetX < offset.left) {
container.scrollLeft(container.scrollLeft() - 50);
} else if (targetX > container.width()) {
container.scrollLeft(container.scrollLeft() + 50);
}
if (targetY < offset.top) {
container.scrollTop(container.scrollTop() - 50);
} else if (targetY > container.height()) {
container.scrollTop(container.scrollTop() + 50);
}
}
},
accept: function (sourceItemHandleScope, destSortableScope, destItemScope) {
return sourceItemHandleScope.sortableScope.options.id === 'stack';
}
};
$scope.labelStyle = function (color) {
return {
'background-color': '#' + color,
'color': $filter('textColorFilter')(color)
};
};
$scope.colorValue = function(color) {
const re = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/;
if (re.test(color)) {
return color;
}
return '';
};
$scope.attachmentCount = function(card) {
if (Array.isArray(card.attachments)) {
return card.attachments.filter((obj) => obj.deletedAt === 0).length;
}
return card.attachmentCount;
};
$scope.unreadCommentCount = function(card) {
return card.commentsUnread;
};
$scope.isTimelineEnabled = function() {
return OCP.Comments && OCA.Activity;
};
});

View File

@@ -1,286 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global app moment angular OC OCP OCA */
import app from '../app/App.js';
app.controller('CardController', function ($scope, $rootScope, $sce, $location, $stateParams, $state, $interval, $timeout, $filter, BoardService, CardService, StackService, StatusService, markdownItConverter, FileService) {
$scope.sidebar = $rootScope.sidebar;
$scope.status = {
renameTitle: '',
lastEdit: 0,
lastSave: Date.now()
};
$scope.cardservice = CardService;
$scope.fileservice = FileService;
$scope.cardId = $stateParams.cardId;
$scope.statusservice = StatusService.getInstance();
$scope.boardservice = BoardService;
$scope.isArray = angular.isArray;
// workaround for $stateParams changes not being propagated
$scope.$watch(function() {
return $state.params;
}, function (params) {
$scope.params = params;
$scope.fileservice.reset();
}, true);
$scope.params = $state.params;
$scope.addAttachmentToDescription = function(insertText) {
let el = document.querySelectorAll('textarea')[0];
let start = el.selectionStart;
let end = el.selectionEnd;
let text = $scope.status.edit.description || '';
let before = text.substring(0, start);
let after = text.substring(end, text.length);
let newText = before + '\n' + insertText + '\n' + after;
$scope.status.edit.description = newText;
el.selectionStart = el.selectionEnd = start + newText.length;
el.focus();
$scope.status.continueEdit = false;
$scope.cardEditDescriptionChanged();
$scope.status.selectAttachment = false;
};
$scope.abortAttachmentSelection = function() {
$scope.status.continueEdit = false;
$scope.status.selectAttachment = false;
let el = document.querySelectorAll('textarea')[0];
el.focus();
};
$scope.statusservice.retainWaiting();
$scope.description = function() {
return $scope.rendered;
};
$scope.updateMarkdown = function(content) {
// only trust the html from markdown-it-checkbox
$scope.rendered = $sce.trustAsHtml(markdownItConverter.render(content || ''));
};
CardService.fetchOne($scope.cardId).then(function (data) {
$scope.statusservice.releaseWaiting();
$scope.archived = CardService.getCurrent().archived;
$scope.updateMarkdown(CardService.getCurrent().description);
}, function (error) {
});
$scope.cardRenameShow = function () {
if ($scope.archived || !BoardService.canEdit()) {
return false;
} else {
$scope.status.renameTitle = CardService.getCurrent().title;
$scope.status.cardRename = true;
}
};
$scope.toggleCheckbox = function (id) {
$('#markdown input[type=checkbox]').attr('disabled', true);
$scope.status.edit = angular.copy(CardService.getCurrent());
var reg = /\[(X|\s|\_|\-)\]/ig;
var nth = 0;
$scope.status.edit.description = $scope.status.edit.description.replace(reg, function (match, i, original) {
var result = match;
if ('' + nth++ === '' + id) {
if (match.match(/^\[\s\]/i)) {
result = match.replace(/\[\s\]/i, '[x]');
}
if (match.match(/^\[x\]/i)) {
result = match.replace(/\[x\]/i, '[ ]');
}
return result;
}
return match;
});
CardService.update($scope.status.edit).then(function (data) {
var header = $('.tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
});
$('#markdown input[type=checkbox]').removeAttr('disabled');
};
$scope.clickCardDescription = function ($event) {
var checkboxId = $($event.target).data('id');
if ($event.target.tagName === 'LABEL') {
$scope.toggleCheckbox(checkboxId);
$event.stopPropagation();
return false;
}
if ($event.target.tagName === 'INPUT') {
$event.stopPropagation();
return;
}
if (BoardService.isArchived() || CardService.getCurrent().archived) {
return false;
}
if ($scope.card.archived || !$scope.boardservice.canEdit()) {
return false;
}
$scope.status.cardEditDescription = true;
$scope.status.edit = angular.copy(CardService.getCurrent());
return true;
};
$scope.cardEditDescriptionChanged = function ($event) {
$scope.status.lastEdit = Date.now();
var header = $('.tabDetails');
header.find('.save-indicator.unsaved').show();
header.find('.save-indicator.saved').hide();
};
$interval(function() {
var currentTime = Date.now();
var timeSinceEdit = currentTime-$scope.status.lastEdit;
if (timeSinceEdit > 1000 && $scope.status.lastEdit > $scope.status.lastSave && !$scope.status.saving) {
$scope.status.lastSave = currentTime;
$scope.status.saving = true;
var header = $('.tabDetails');
header.find('.save-indicator.unsaved').fadeIn(500);
CardService.update($scope.status.edit).then(function (data) {
var header = $('.tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(250).fadeOut(1000);
$scope.status.saving = false;
});
}
}, 500, 0, false);
// handle rename to update information on the board as well
$scope.cardRename = function (card) {
var newTitle;
if (!$scope.status.renameTitle || !$scope.status.renameTitle.trim()) {
newTitle = '';
} else {
newTitle = $scope.status.renameTitle.trim();
}
if (newTitle === card.title) {
// title unchanged
$scope.status.renameCard = false;
} else if (newTitle !== '') {
// title changed
card.title = newTitle;
CardService.rename(card).then(function (data) {
$scope.status.renameCard = false;
});
} else {
// empty title
$scope.status.renameTitle = card.title;
$scope.status.renameCard = false;
}
};
$scope.cardUpdate = function (card) {
CardService.update(card).then(function (data) {
$scope.status.cardEditDescription = false;
$scope.updateMarkdown($scope.status.edit.description);
var header = $('.tabDetails');
header.find('.save-indicator.unsaved').hide();
header.find('.save-indicator.saved').fadeIn(500).fadeOut(1000);
});
};
$scope.labelAssign = function (element, model) {
CardService.assignLabel($scope.cardId, element.id).then(function (data) {
});
};
$scope.labelRemove = function (element, model) {
CardService.removeLabel($scope.cardId, element.id).then(function (data) {
});
};
$scope.setDuedate = function (duedate) {
var element = CardService.getCurrent();
var newDate = moment(element.duedate);
if(!newDate.isValid()) {
newDate = moment();
}
newDate.date(duedate.date());
newDate.month(duedate.month());
newDate.year(duedate.year());
element.duedate = newDate.toISOString();
CardService.update(element);
};
$scope.setDuedateTime = function (time) {
var element = CardService.getCurrent();
var newDate = moment(element.duedate);
if(!newDate.isValid()) {
newDate = moment();
}
newDate.hour(time.hour());
newDate.minute(time.minute());
element.duedate = newDate.toISOString();
CardService.update(element);
};
$scope.resetDuedate = function () {
var element = CardService.getCurrent();
element.duedate = null;
CardService.update(element);
};
/**
* Show ui-select field when clicking the add button
*/
$scope.toggleAssignUser = function() {
$scope.status.showAssignUser = !$scope.status.showAssignUser;
if ($scope.status.showAssignUser === true) {
$timeout(function () {
$('#assignUserSelect').find('a').click();
});
}
};
/**
* Hide ui-select when select list is closed
*/
$scope.assingUserOpenClose = function(isOpen) {
$scope.status.showAssignUser = isOpen;
};
$scope.addAssignedUser = function(item) {
CardService.assignUser(CardService.getCurrent(), item.uid).then(function (data) {
});
$scope.status.showAssignUser = false;
};
$scope.removeAssignedUser = function(uid) {
CardService.unassignUser(CardService.getCurrent(), uid).then(function (data) {
});
};
$scope.labelStyle = function (color) {
return {
'background-color': '#' + color,
'color': $filter('textColorFilter')(color)
};
};
$scope.isTimelineEnabled = function() {
return OCP.Comments && OCA.Activity;
};
});

View File

@@ -1,44 +0,0 @@
/*
* @copyright Copyright (c) 2018 Oskar Kurz <oskar.kurz@gmail.com>
*
* @author Oskar Kurz <oskar.kurz@gmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global oc_defaults OC */
app.controller('ColorPickerController', ['$scope', function ($scope) {
$scope.hashedColor = '';
$scope.setColor = function (object, color) {
object.color = color;
object.hashedColor = '#' + color;
return object;
};
$scope.setHashedColor = function (object) {
object.color = object.hashedColor.substr(1);
return object;
};
$scope.getCustomBackground = function (color) {
return {'background-color': color};
};
}]);

View File

@@ -1,255 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/* global app angular oc_isadmin */
var ListController = function ($scope, $location, $filter, BoardService, $element, $timeout, $stateParams, $state, StatusService, $http, $q, $rootScope) {
function calculateNewColor() {
var boards = BoardService.getAll();
var boardKeys = Object.keys(boards);
var colorOccurrences = [];
for (var i = 0; i < $scope.colors.length; i++) {
colorOccurrences.push(0);
}
for (var j = 0; j < boardKeys.length; j++) {
var key = boardKeys[j];
var board = boards[key];
if (board && $scope.colors.indexOf(board.color) !== -1) {
colorOccurrences[$scope.colors.indexOf(board.color)]++;
}
}
return $scope.colors[colorOccurrences.indexOf(Math.min.apply(Math, colorOccurrences))];
}
$scope.boards = [];
$scope.newBoard = {};
$scope.status = {
deleteUndo: [],
filter: $stateParams.filter ? $stateParams.filter : '',
sidebar: false
};
$scope.colors = ['0082c9', '00c9c6','00c906', 'c92b00', 'F1DB50', '7C31CC', '3A3B3D', 'CACBCD'];
$scope.boardservice = BoardService;
$scope.updatingBoard = null;
$scope.isAdmin = oc_isadmin;
$scope.canCreate = $rootScope.config.canCreate;
if ($scope.isAdmin) {
OC.Apps.enableDynamicSlideToggle();
$scope.groups = [];
$scope.groupLimit = [];
$scope.groupLimitDisabled = true;
let fetchGroups = function () {
var deferred = $q.defer();
// TODO: move to groups/details once 15 is min version
$http.get(OC.linkToOCS('cloud', 2) + 'groups').then(function (response) {
$scope.groups = response.data.ocs.data.groups.reduce((obj, item) => {
obj.push({
id: item,
displayname: item,
});
return obj;
}, []);
deferred.resolve($scope.groups);
}, function (error) {
deferred.reject('Error while loading groups');
});
$http.get(OC.generateUrl('apps/deck/config')).then(function (response) {
$scope.groupLimit = response.data.groupLimit;
$scope.groupLimitDisabled = false;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while loading groupLimit');
});
return deferred.promise;
};
let updateConfig = function() {
$scope.groupLimitDisabled = true;
var deferred = $q.defer();
$http.post(OC.generateUrl('apps/deck/config/groupLimit'), {value: $scope.groupLimit}).then(function (response) {
$scope.groupLimitDisabled = false;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while saving groupLimit');
});
return deferred.promise;
};
$scope.groupLimitAdd = function (element, model) {
$scope.groupLimit.push(element);
updateConfig();
};
$scope.groupLimitRemove = function (element, model) {
$scope.groupLimit = $scope.groupLimit.filter((el) => {
return el.id !== element.id;
});
updateConfig();
};
fetchGroups();
}
var filterData = function () {
if($element.attr('id') === 'app-navigation') {
$scope.boardservice.sidebar = $scope.boardservice.getData();
$scope.boardservice.sidebar = $filter('orderBy')($scope.boardservice.sidebar, 'title');
$scope.boardservice.sidebar = $filter('cardFilter')($scope.boardservice.sidebar, {archived: false});
} else {
$scope.boardservice.sorted = $scope.boardservice.getData();
if ($scope.status.filter === 'archived') {
var filter = {};
filter[$scope.status.filter] = true;
$scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, filter);
} else if ($scope.status.filter === 'shared') {
$scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, {archived: false});
$scope.boardservice.sorted = $filter('boardFilterAcl')($scope.boardservice.sorted);
} else {
$scope.boardservice.sorted = $filter('cardFilter')($scope.boardservice.sorted, {archived: false});
}
$scope.boardservice.sorted = $filter('orderBy')($scope.boardservice.sorted, ['deletedAt', 'title']);
}
};
var finishedLoading = function() {
filterData();
$scope.newBoard.color = calculateNewColor();
};
var initialize = function () {
$scope.statusservice = StatusService.listStatus;
if($element.attr('id') === 'app-navigation') {
$scope.statusservice.retainWaiting();
BoardService.fetchAll().then(function(data) {
finishedLoading();
$scope.statusservice.releaseWaiting();
BoardService.loaded = true;
}, function (error) {
$scope.statusservice.setError('Error occured', error);
});
} else {
/* initialize main list controller when board list is loaded */
var boardDataWatch = $scope.$watch(function () {
return $scope.boardservice.loaded;
}, function () {
if (BoardService.loaded === true) {
boardDataWatch();
finishedLoading();
}
});
}
$scope.$watch(function () {
return $scope.boardservice.data;
}, function () {
filterData();
}, true);
/* Watch for board filter change */
$scope.$watchCollection(function(){
return $state.params;
}, function(){
$scope.status.filter = $state.params.filter;
filterData();
});
};
initialize();
$scope.selectColor = function(color) {
$scope.newBoard.color = color;
};
$scope.gotoBoard = function(board) {
if(board.deletedAt > 0) {
return false;
}
return $state.go('board', {boardId: board.id});
};
$scope.boardCreate = function() {
if(!$scope.newBoard.title || !$scope.newBoard.color) {
$scope.status.addBoard=false;
return;
}
BoardService.create($scope.newBoard)
.then(function (response) {
$scope.newBoard = {};
$scope.newBoard.color = calculateNewColor();
$scope.status.addBoard=false;
filterData();
}, function(error) {
$scope.status.createBoard = 'Unable to insert board: ' + error.message;
});
};
$scope.boardUpdate = function(board) {
BoardService.update(board).then(function(data) {
board.status.edit = false;
filterData();
});
};
$scope.boardUpdateBegin = function(board) {
$scope.updatingBoard = angular.copy(board);
};
$scope.boardUpdateReset = function(board) {
board.title = $scope.updatingBoard.title;
board.color = $scope.updatingBoard.color;
filterData();
board.status.edit = false;
};
$scope.boardArchive = function (board) {
board.archived = true;
BoardService.update(board).then(function(data) {
filterData();
});
};
$scope.boardUnarchive = function (board) {
board.archived = false;
BoardService.update(board).then(function(data) {
filterData();
});
};
$scope.boardDelete = function(board) {
BoardService.delete(board.id).then(function (data) {
filterData();
});
};
$scope.boardDeleteUndo = function (board) {
BoardService.deleteUndo(board.id).then(function (data) {
filterData();
});
};
};
export default ListController;

View File

@@ -1,48 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('appPopoverMenuUtils', function () {
'use strict';
return {
restrict: 'C',
link: function (scope, elm) {
var menu = elm.find('.popovermenu');
var button = elm.find('button');
button.click(function (e) {
var popovermenus = $('.popovermenu');
var shouldShow = menu.hasClass('hidden');
popovermenus.addClass('hidden');
if (shouldShow) {
menu.toggleClass('hidden');
}
e.stopPropagation();
});
scope.$on('documentClicked', function (scope, event) {
/* prevent closing popover if target has no-close class */
if (event.target !== button && !$(event.target).hasClass('no-close')) {
menu.addClass('hidden');
}
});
}
};
});

View File

@@ -1,47 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
// OwnCloud Click Handling
// https://doc.owncloud.org/server/8.0/developer_manual/app/css.html
app.directive('appNavigationEntryUtils', function () {
'use strict';
return {
restrict: 'C',
link: function (scope, elm) {
var menu = elm.siblings('.app-navigation-entry-menu');
var button = $(elm)
.find('.app-navigation-entry-utils-menu-button button');
button.click(function () {
menu.toggleClass('open');
});
scope.$on('documentClicked', function (scope, event) {
if (event.target !== button[0]) {
menu.removeClass('open');
}
});
}
};
});

View File

@@ -1,29 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('autofocusOnInsert', function () {
'use strict';
return function (scope, elm) {
elm.focus();
};
});

View File

@@ -1,55 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('avatar', function() {
'use strict';
return {
restrict: 'AEC',
transclude: true,
replace: true,
template: '<div class="avatardiv-container"><div class="avatardiv" data-toggle="tooltip" ng-transclude></div></div>',
scope: { attr: '=' },
link: function(scope, element, attr){
scope.uid = attr.displayname;
scope.displayname = attr.displayname;
scope.size = attr.size;
if (typeof scope.size === 'undefined') {
scope.size = 32;
}
var value = attr.user;
var avatardiv = $(element).find('.avatardiv');
if(typeof attr.contactsmenu !== 'undefined' && attr.contactsmenu !== 'false') {
avatardiv.contactsMenu(value, 0, $(element));
avatardiv.addClass('has-contactsmenu');
}
if(typeof attr.tooltip !== 'undefined' && attr.tooltip !== 'false') {
$(element).tooltip({
title: scope.displayname,
placement: 'top'
});
}
avatardiv.avatar(value, scope.size, false, false, false, attr.displayname);
},
controller: function () {}
};
});

View File

@@ -1,38 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('bindHtmlCompile', function ($compile) {
'use strict';
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
element.html(value);
$compile(element.contents())(scope);
});
}
};
});

View File

@@ -1,42 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('contactsmenudelete', function() {
'use strict';
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attr){
var user = attr.user;
var menu = $(element).parent().find('.contactsmenu-popover');
if (oc_current_user === user) {
menu.children(':first').remove();
}
var menuEntry = $('<li><a><span class="icon icon-delete"></span><span>' + t('deck', 'Remove user from card') + '</span></a></li>');
menuEntry.on('click', function () {
scope.removeAssignedUser(user);
});
$(menu).append(menuEntry);
}
};
});

View File

@@ -1,59 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App';
app.directive('ngContenteditable', function($compile) {
return {
require: 'ngModel',
restrict: 'A',
scope: {
submit: '&ngSubmit'
},
link: function(scope, element, attrs, ngModel) {
//read the text typed in the div (syncing model with the view)
function read() {
ngModel.$setViewValue(element.html());
}
//render the data now in your model into your view
//$render is invoked when the modelvalue differs from the viewvalue
//see documentation: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#
ngModel.$render = function() {
element.html(ngModel.$viewValue || '');
};
//do this whenever someone starts typing
element.bind('blur keyup change', function(event) {
scope.$apply(read);
});
element.bind('keydown', function(event) {
if(event.which === 13 && event.shiftKey) {
scope.submit();
}
});
}
};
});

View File

@@ -1,55 +0,0 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global app */
/* gloabl t */
/* global moment */
app.directive('datepicker', function () {
'use strict';
return {
link: function (scope, elm, attr) {
return elm.datepicker({
dateFormat: moment.localeData().longDateFormat('L').replace('YYYY', 'YY').toLowerCase(),
onSelect: function(date, inst) {
var selectedDate = $(this).datepicker('getDate');
scope.setDuedate(moment(selectedDate));
scope.$apply();
},
beforeShow: function(input, inst) {
var dp, marginLeft;
dp = $(inst).datepicker('widget');
marginLeft = -Math.abs($(input).outerWidth() - dp.outerWidth()) / 2 + 'px';
dp.css({
'margin-left': marginLeft
});
$('div.ui-datepicker:before').css({
'left': 100 + 'px'
});
return $('.hasDatepicker').datepicker();
},
minDate: null
});
}
};
});

View File

@@ -1,41 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
// original idea from blockloop: http://stackoverflow.com/a/24090733
app.directive('elastic', [
'$timeout',
function($timeout) {
return {
restrict: 'A',
link: function($scope, element) {
$scope.initialHeight = $scope.initialHeight || element[0].style.height;
var resize = function() {
element[0].style.height = $scope.initialHeight;
element[0].style.height = "" + element[0].scrollHeight + "px";
};
element.on("input change", resize);
$timeout(resize, 0);
}
};
}
]);

View File

@@ -1,61 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.directive('search', function ($document, $location) {
'use strict';
return {
restrict: 'E',
scope: {
'onSearch': '='
},
link: function (scope) {
if (OCA.Search && OCA.Search.Core) {
// eslint-disable-next-line no-unused-vars
const search = new OCA.Search((term) => {
scope.$apply(function () {
scope.onSearch(term);
});
}, () => {
scope.$apply(function () {
scope.onSearch('');
});
});
} else {
const box = $('#searchbox');
box.val($location.search().search);
var doSearch = function () {
var value = box.val();
scope.$apply(function () {
scope.onSearch(value);
});
};
box.on('search keyup', function (event) {
doSearch();
});
}
}
};
});

View File

@@ -1,48 +0,0 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
import '../legacy/jquery.ui.timepicker.js';
import 'legacy/jquery.ui.timepicker.css';
/* global app */
/* global t */
/* global moment */
app.directive('timepicker', function() {
'use strict';
return {
restrict: 'A',
link: function(scope, elm, attr) {
return $(elm).timepicker({
onSelect: function(date, inst) {
scope.setDuedateTime(moment('2000-01-01 ' + date));
scope.$apply();
},
myPosition: 'center top',
atPosition: 'center bottom',
hourText: t('deck', 'Hours'),
minuteText: t('deck', 'Minutes'),
showPeriodLabels: false
});
}
};
});

View File

@@ -1,34 +0,0 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('boardFilterAcl', function() {
return function(boards) {
var _result = [];
angular.forEach(boards, function(board){
if(board.acl !== null && Object.keys(board.acl).length > 0) {
_result.push(board);
}
});
return _result;
};
});

View File

@@ -1,37 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('bytes', function () {
return function (bytes, precision) {
if (isNaN(parseFloat(bytes, 10)) || !isFinite(bytes)) {
return '-';
}
if (typeof precision === 'undefined') {
precision = 2;
}
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'],
number = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
};
});

View File

@@ -1,40 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
// usage | cardFilter({ member: 'admin'})
app.filter('cardFilter', function() {
return function(cards, rules) {
var _result = [];
angular.forEach(cards, function(card){
var _card = card;
var keys = Object.keys(rules);
keys.some(function(key, condition) {
if(_card[key]===rules[key]) {
_result.push(_card);
}
});
});
return _result;
};
});

View File

@@ -1,44 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('cardSearchFilter', function() {
return function(cards, searchString) {
var _result = {};
var rules = {
title: searchString,
//owner: searchString,
};
angular.forEach(cards, function(card){
var _card = card;
Object.keys(rules).some(function(rule) {
if(_card[rule].search(rules[rule])>=0) {
_result[_card.id] = _card;
}
});
});
return $.map(_result, function(value, index) {
return [value];
});
};
});

View File

@@ -1,63 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global app */
/* global OC */
/* global moment */
app.filter('relativeDateFilter', function() {
return function (timestamp) {
return OC.Util.relativeModifiedDate(timestamp*1000);
};
});
app.filter('relativeDateFilterString', function() {
return function (date) {
return OC.Util.relativeModifiedDate(Date.parse(date));
};
});
app.filter('dateToTimestamp', function() {
return function (date) {
return Date.parse(date);
};
});
app.filter('parseDate', function() {
return function (date) {
if(moment(date).isValid()) {
var dateFormat = moment.localeData().longDateFormat('L');
return moment(date).format(dateFormat);
}
return '';
};
});
app.filter('parseTime', function() {
return function (date) {
if(moment(date).isValid()) {
return moment(date).format('HH:mm');
}
return '';
};
});

View File

@@ -1,67 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('iconWhiteFilter', function () {
return function (hex) {
// RGB2HLS by Garry Tan
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
var color = result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
if (result === null) {
return '';
}
var r = color.r / 255;
var g = color.g / 255;
var b = color.b / 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
if (l < 0.5) {
return '-white';
} else {
return '';
}
};
});

View File

@@ -1,38 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('lightenColorFilter', function() {
return function (hex) {
var result = /^([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
var color = result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
if (result !== null) {
return 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',0.7)';
} else {
return '#' + hex;
}
};
});

View File

@@ -1,43 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('orderObjectBy', function(){
return function(input, attribute) {
if (!angular.isObject(input)) {
return input;
}
var array = [];
for(var objectKey in input) {
if ({}.hasOwnProperty.call(input, objectKey)) {
array.push(input[objectKey]);
}
}
array.sort(function(a, b){
a = parseInt(a[attribute]);
b = parseInt(b[attribute]);
return a < b;
});
return array;
};
});

View File

@@ -1,69 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.filter('textColorFilter', function () {
return function (hex) {
// RGB2HLS by Garry Tan
// http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
var result = /^#?([A-Fa-f\d]{2})([A-Fa-f\d]{2})([A-Fa-f\d]{2})$/i.exec(hex);
var color = result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
if (result !== null) {
var r = color.r / 255;
var g = color.g / 255;
var b = color.b / 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
if (l < 0.5) {
return '#ffffff';
} else {
return '#000000';
}
} else {
return '#000000';
}
};
});

View File

@@ -1,46 +0,0 @@
/*
* @copyright Copyright (c) 2017 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global app */
/* global angular */
/*
* Remove all assignedUsers from users list
*/
app.filter('withoutAssignedUsers', function () {
return function (users, assignedUsers) {
var _result = [];
angular.forEach(users, function (user) {
var _found = false;
angular.forEach(assignedUsers, function (assignedUser) {
if (assignedUser.participant.uid === user.uid) {
_found = true;
}
});
if (_found === false) {
_result.push(user);
}
});
return _result;
};
});

View File

@@ -1,69 +0,0 @@
/*
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
'use strict';
/* global __webpack_nonce__ __webpack_public_path__ OC t n */
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('deck', 'js/build/');
import Vue from 'vue';
Vue.prototype.t = t;
Vue.prototype.n = n;
Vue.prototype.OC = OC;
import BoardSelector from './views/BoardSelector';
import './../css/collections.css';
((function(OCP) {
OCP.Collaboration.registerType('deck', {
action: () => {
return new Promise((resolve, reject) => {
const container = document.createElement('div');
container.id = 'deck-board-select';
const body = document.getElementById('body-user');
body.append(container);
const ComponentVM = new Vue({
render: h => h(BoardSelector),
});
ComponentVM.$mount(container);
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove();
ComponentVM.$destroy();
reject();
});
ComponentVM.$root.$on('select', (id) => {
resolve(id);
ComponentVM.$el.remove();
ComponentVM.$destroy();
});
});
},
typeString: t('deck', 'Link to a board'),
typeIconClass: 'icon-deck'
});
})(window.OCP));

View File

@@ -1,38 +0,0 @@
'use strict';
import "@babel/polyfill";
/* global __webpack_nonce__ __webpack_public_path__ OC t n */
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('deck', 'js/build/');
// used for building a vendor stylesheet
import 'ng-sortable/dist/ng-sortable.css';
import angular from 'angular';
import markdownit from 'markdown-it';
global.markdownit = markdownit;
import app from './app/App.js';
import './app/Config.js';
import './app/Run.js';
import ListController from 'controller/ListController.js';
import attachmentListComponent from './controller/AttachmentController.js';
import activityComponent from './controller/ActivityController.js';
app.controller('ListController', ListController);
app.component('attachmentListComponent', attachmentListComponent);
app.component('activityComponent', activityComponent);
// require all the js files from subdirectories
var context = require.context('.', true, /(controller|service|filters|directive)\/(.*)\.js$/);
context.keys().forEach(function (key) {
context(key);
});

View File

@@ -1,161 +0,0 @@
/**
* @licence
*/
import CommentModel from './commentmodel.js';
import CommentSummaryModel from './commentsummarymodel.js';
/**
* @class CommentCollection
* @classdesc
*
* Collection of comments assigned to a file
*
*/
var CommentCollection = OC.Backbone.Collection.extend(
/** @lends OCA.AnnouncementCenter.Comments.CommentCollection.prototype */ {
sync: OC.Backbone.davSync,
model: CommentModel,
/**
* Object type
*
* @type string
*/
_objectType: 'deckCard',
/**
* Object id
*
* @type string
*/
_objectId: null,
/**
* True if there are no more page results left to fetch
*
* @type bool
*/
_endReached: false,
/**
* Number of comments to fetch per page
*
* @type int
*/
_limit : 5,
/**
* Initializes the collection
*
* @param {string} [options.objectType] object type
* @param {string} [options.objectId] object id
*/
initialize: function(models, options) {
options = options || {};
if (options.objectType) {
this._objectType = options.objectType;
}
if (options.objectId) {
this._objectId = options.objectId;
}
},
url: function() {
return OC.linkToRemote('dav') + '/comments/' +
encodeURIComponent(this._objectType) + '/' +
encodeURIComponent(this._objectId) + '/';
},
setObjectId: function(objectId) {
this._objectId = objectId;
},
hasMoreResults: function() {
return !this._endReached;
},
reset: function() {
this._endReached = false;
this._summaryModel = null;
return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
},
/**
* Fetch the next set of results
*/
fetchNext: function(options) {
var self = this;
if (!this.hasMoreResults()) {
return null;
}
var body = '<?xml version="1.0" encoding="utf-8" ?>\n' +
'<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n' +
// load one more so we know there is more
' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n' +
' <oc:offset>' + this.length + '</oc:offset>\n' +
'</oc:filter-comments>\n';
options = options || {};
var success = options.success;
options = _.extend({
remove: false,
parse: true,
data: body,
davProperties: CommentCollection.prototype.model.prototype.davProperties,
success: function(resp) {
if (resp.length <= self._limit) {
// no new entries, end reached
self._endReached = true;
} else {
// remove last entry, for next page load
resp = _.initial(resp);
}
if (!self.set(resp, options)) {
return false;
}
if (success) {
success.apply(null, arguments);
}
self.trigger('sync', 'REPORT', self, options);
}
}, options);
return this.sync('REPORT', this, options);
},
/**
* Returns the matching summary model
*
* @return {OCA.AnnouncementCenter.Comments.CommentSummaryModel} summary model
*/
getSummaryModel: function() {
if (!this._summaryModel) {
this._summaryModel = new CommentSummaryModel({
id: this._objectId,
objectType: this._objectType
});
}
return this._summaryModel;
},
/**
* Updates the read marker for this comment thread
*
* @param {Date} [date] optional date, defaults to now
* @param {Object} [options] backbone options
*/
updateReadMarker: function(date, options) {
options = options || {};
return this.getSummaryModel().save({
readMarker: (date || new Date()).toUTCString()
}, options);
}
});
export default CommentCollection;

View File

@@ -1,119 +0,0 @@
/*
* Copyright (c) 2016
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
var NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* @class CommentModel
* @classdesc
*
* Comment
*
*/
var CommentModel = OC.Backbone.Model.extend(
/** @lends OCA.Comments.CommentModel.prototype */ {
sync: OC.Backbone.davSync,
/**
* Object type
*
* @type string
*/
_objectType: 'deckCard',
/**
* Object id
*
* @type string
*/
_objectId: null,
initialize: function(model, options) {
options = options || {};
if (options.objectType) {
this._objectType = options.objectType;
}
if (options.objectId) {
this._objectId = options.objectId;
}
},
defaults: {
actorType: 'users',
objectType: 'deckCard'
},
davProperties: {
'id': '{' + NS_OWNCLOUD + '}id',
'message': '{' + NS_OWNCLOUD + '}message',
'actorType': '{' + NS_OWNCLOUD + '}actorType',
'actorId': '{' + NS_OWNCLOUD + '}actorId',
'actorDisplayName': '{' + NS_OWNCLOUD + '}actorDisplayName',
'creationDateTime': '{' + NS_OWNCLOUD + '}creationDateTime',
'objectType': '{' + NS_OWNCLOUD + '}objectType',
'objectId': '{' + NS_OWNCLOUD + '}objectId',
'isUnread': '{' + NS_OWNCLOUD + '}isUnread',
'mentions': '{' + NS_OWNCLOUD + '}mentions'
},
parse: function(data) {
return {
id: data.id,
message: data.message,
actorType: data.actorType,
actorId: data.actorId,
actorDisplayName: data.actorDisplayName,
creationDateTime: data.creationDateTime,
objectType: data.objectType,
objectId: data.objectId,
isUnread: (data.isUnread === 'true'),
mentions: this._parseMentions(data.mentions)
};
},
_parseMentions: function(mentions) {
if(_.isUndefined(mentions)) {
return {};
}
var result = {};
for(var i in mentions) {
var mention = mentions[i];
if(_.isUndefined(mention.localName) || mention.localName !== 'mention') {
continue;
}
result[i] = {};
for (var child = mention.firstChild; child; child = child.nextSibling) {
if(_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
continue;
}
result[i][child.localName] = child.textContent;
}
}
return result;
},
url: function() {
let baseUrl;
if (typeof this.collection === 'undefined') {
baseUrl = OC.linkToRemote('dav') + '/comments/' +
encodeURIComponent(this.get('objectType')) + '/' +
encodeURIComponent(this.get('objectId')) + '/';
} else {
baseUrl = this.collection.url();
}
if (typeof this.get('id') !== 'undefined') {
return baseUrl + this.get('id');
} else {
return baseUrl;
}
}
});
export default CommentModel;

View File

@@ -1,54 +0,0 @@
var NS_OWNCLOUD = 'http://owncloud.org/ns';
/**
* @class OCA.AnnouncementCenter.Comments.CommentSummaryModel
* @classdesc
*
* Model containing summary information related to comments
* like the read marker.
*
*/
var CommentSummaryModel = OC.Backbone.Model.extend(
/** @lends OCA.AnnouncementCenter.Comments.CommentSummaryModel.prototype */ {
sync: OC.Backbone.davSync,
/**
* Object type
*
* @type string
*/
_objectType: 'deckCard',
/**
* Object id
*
* @type string
*/
_objectId: null,
davProperties: {
'readMarker': '{' + NS_OWNCLOUD + '}readMarker'
},
/**
* Initializes the summary model
*
* @param {string} [options.objectType] object type
* @param {string} [options.objectId] object id
*/
initialize: function(attrs, options) {
options = options || {};
if (options.objectType) {
this._objectType = options.objectType;
}
},
url: function() {
return OC.linkToRemote('dav') + '/comments/' +
encodeURIComponent(this._objectType) + '/' +
encodeURIComponent(this.id) + '/';
}
});
export default CommentSummaryModel;

File diff suppressed because one or more lines are too long

View File

@@ -1,561 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
(function($, undefined) {
var _input = document.createElement('input');
var _support = {
setSelectionRange: ('setSelectionRange' in _input) || ('selectionStart' in _input),
createTextRange: ('createTextRange' in _input) || ('selection' in document)
};
var _rNewlineIE = /\r\n/g,
_rCarriageReturn = /\r/g;
var _getValue = function(input) {
if (typeof(input.value) !== 'undefined') {
return input.value;
}
return $(input).text();
};
var _setValue = function(input, value) {
if (typeof(input.value) !== 'undefined') {
input.value = value;
} else {
$(input).text(value);
}
};
var _getIndex = function(input, pos) {
var norm = _getValue(input).replace(_rCarriageReturn, '');
var len = norm.length;
if (typeof(pos) === 'undefined') {
pos = len;
}
pos = Math.floor(pos);
// Negative index counts backward from the end of the input/textarea's value
if (pos < 0) {
pos = len + pos;
}
// Enforce boundaries
if (pos < 0) { pos = 0; }
if (pos > len) { pos = len; }
return pos;
};
var _hasAttr = function(input, attrName) {
return input.hasAttribute ? input.hasAttribute(attrName) : (typeof(input[attrName]) !== 'undefined');
};
/**
* @class
* @constructor
*/
var Range = function(start, end, length, text) {
this.start = start || 0;
this.end = end || 0;
this.length = length || 0;
this.text = text || '';
};
Range.prototype.toString = function() {
return JSON.stringify(this, null, ' ');
};
var _getCaretW3 = function(input) {
return input.selectionStart;
};
/**
* @see http://stackoverflow.com/q/6943000/467582
*/
var _getCaretIE = function(input) {
var caret, range, textInputRange, rawValue, len, endRange;
// Yeah, you have to focus twice for IE 7 and 8. *cries*
input.focus();
input.focus();
range = document.selection.createRange();
if (range && range.parentElement() === input) {
rawValue = _getValue(input);
len = rawValue.length;
// Create a working TextRange that lives only in the input
textInputRange = input.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = input.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
caret = rawValue.replace(_rNewlineIE, '\n').length;
} else {
caret = -textInputRange.moveStart("character", -len);
}
return caret;
}
// NOTE: This occurs when you highlight part of a textarea and then click in the middle of the highlighted portion in IE 6-10.
// There doesn't appear to be anything we can do about it.
// alert("Your browser is incredibly stupid. I don't know what else to say.");
// alert(range + '\n\n' + range.parentElement().tagName + '#' + range.parentElement().id);
return 0;
};
/**
* Gets the position of the caret in the given input.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @returns {Number}
* @see http://stackoverflow.com/questions/263743/how-to-get-cursor-position-in-textarea/263796#263796
*/
var _getCaret = function(input) {
if (!input) {
return undefined;
}
// Mozilla, et al.
if (_support.setSelectionRange) {
return _getCaretW3(input);
}
// IE
else if (_support.createTextRange) {
return _getCaretIE(input);
}
return undefined;
};
var _setCaretW3 = function(input, pos) {
input.setSelectionRange(pos, pos);
};
var _setCaretIE = function(input, pos) {
var range = input.createTextRange();
range.move('character', pos);
range.select();
};
/**
* Sets the position of the caret in the given input.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @param {Number} pos
* @see http://parentnode.org/javascript/working-with-the-cursor-position/
*/
var _setCaret = function(input, pos) {
input.focus();
pos = _getIndex(input, pos);
// Mozilla, et al.
if (_support.setSelectionRange) {
_setCaretW3(input, pos);
}
// IE
else if (_support.createTextRange) {
_setCaretIE(input, pos);
}
};
/**
* Inserts the specified text at the current caret position in the given input.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @param {String} text
* @see http://parentnode.org/javascript/working-with-the-cursor-position/
*/
var _insertAtCaret = function(input, text) {
var curPos = _getCaret(input);
var oldValueNorm = _getValue(input).replace(_rCarriageReturn, '');
var newLength = +(curPos + text.length + (oldValueNorm.length - curPos));
var maxLength = +input.getAttribute('maxlength');
if(_hasAttr(input, 'maxlength') && newLength > maxLength) {
var delta = text.length - (newLength - maxLength);
text = text.substr(0, delta);
}
_setValue(input, oldValueNorm.substr(0, curPos) + text + oldValueNorm.substr(curPos));
_setCaret(input, curPos + text.length);
};
var _getInputRangeW3 = function(input) {
var range = new Range();
range.start = input.selectionStart;
range.end = input.selectionEnd;
var min = Math.min(range.start, range.end);
var max = Math.max(range.start, range.end);
range.length = max - min;
range.text = _getValue(input).substring(min, max);
return range;
};
/** @see http://stackoverflow.com/a/3648244/467582 */
var _getInputRangeIE = function(input) {
var range = new Range();
input.focus();
var selection = document.selection.createRange();
if (selection && selection.parentElement() === input) {
var len, normalizedValue, textInputRange, endRange, start = 0, end = 0;
var rawValue = _getValue(input);
len = rawValue.length;
normalizedValue = rawValue.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = input.createTextRange();
textInputRange.moveToBookmark(selection.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
endRange = input.createTextRange();
endRange.collapse(false);
if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
/// normalize newlines
start -= (rawValue.substring(0, start).split('\r\n').length - 1);
end -= (rawValue.substring(0, end).split('\r\n').length - 1);
/// normalize newlines
range.start = start;
range.end = end;
range.length = range.end - range.start;
range.text = normalizedValue.substr(range.start, range.length);
}
return range;
};
/**
* Gets the selected text range of the given input.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @returns {Range}
* @see http://stackoverflow.com/a/263796/467582
* @see http://stackoverflow.com/a/2966703/467582
*/
var _getInputRange = function(input) {
if (!input) {
return undefined;
}
// Mozilla, et al.
if (_support.setSelectionRange) {
return _getInputRangeW3(input);
}
// IE
else if (_support.createTextRange) {
return _getInputRangeIE(input);
}
return undefined;
};
var _setInputRangeW3 = function(input, startPos, endPos) {
input.setSelectionRange(startPos, endPos);
};
var _setInputRangeIE = function(input, startPos, endPos) {
var tr = input.createTextRange();
tr.moveEnd('textedit', -1);
tr.moveStart('character', startPos);
tr.moveEnd('character', endPos - startPos);
tr.select();
};
/**
* Sets the selected text range of (i.e., highlights text in) the given input.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @param {Number} startPos Zero-based index
* @param {Number} endPos Zero-based index
* @see http://parentnode.org/javascript/working-with-the-cursor-position/
* @see http://stackoverflow.com/a/2966703/467582
*/
var _setInputRange = function(input, startPos, endPos) {
startPos = _getIndex(input, startPos);
endPos = _getIndex(input, endPos);
// Mozilla, et al.
if (_support.setSelectionRange) {
_setInputRangeW3(input, startPos, endPos);
}
// IE
else if (_support.createTextRange) {
_setInputRangeIE(input, startPos, endPos);
}
};
/**
* Replaces the currently selected text with the given string.
* @param {HTMLInputElement|HTMLTextAreaElement} input input or textarea element
* @param {String} text New text that will replace the currently selected text.
* @see http://parentnode.org/javascript/working-with-the-cursor-position/
*/
var _replaceInputRange = function(input, text) {
var $input = $(input);
var oldValue = $input.val();
var selection = _getInputRange(input);
var newLength = +(selection.start + text.length + (oldValue.length - selection.end));
var maxLength = +$input.attr('maxlength');
if($input.is('[maxlength]') && newLength > maxLength) {
var delta = text.length - (newLength - maxLength);
text = text.substr(0, delta);
}
// Now that we know what the user selected, we can replace it
var startText = oldValue.substr(0, selection.start);
var endText = oldValue.substr(selection.end);
$input.val(startText + text + endText);
// Reset the selection
var startPos = selection.start;
var endPos = startPos + text.length;
_setInputRange(input, selection.length ? startPos : endPos, endPos);
};
var _selectAllW3 = function(elem) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(elem);
selection.removeAllRanges();
selection.addRange(range);
};
var _selectAllIE = function(elem) {
var range = document.body.createTextRange();
range.moveToElementText(elem);
range.select();
};
/**
* Select all text in the given element.
* @param {HTMLElement} elem Any block or inline element other than a form element.
*/
var _selectAll = function(elem) {
var $elem = $(elem);
if ($elem.is('input, textarea') || elem.select) {
$elem.select();
return;
}
// Mozilla, et al.
if (_support.setSelectionRange) {
_selectAllW3(elem);
}
// IE
else if (_support.createTextRange) {
_selectAllIE(elem);
}
};
var _deselectAll = function() {
if (document.selection) {
document.selection.empty();
}
else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
};
$.extend($.fn, {
/**
* Gets or sets the position of the caret or inserts text at the current caret position in an input or textarea element.
* @returns {Number|jQuery} The current caret position if invoked as a getter (with no arguments)
* or this jQuery object if invoked as a setter or inserter.
* @see http://web.archive.org/web/20080704185920/http://parentnode.org/javascript/working-with-the-cursor-position/
* @since 1.0.0
* @example
* <pre>
* // Get position
* var pos = $('input:first').caret();
* </pre>
* @example
* <pre>
* // Set position
* $('input:first').caret(15);
* $('input:first').caret(-3);
* </pre>
* @example
* <pre>
* // Insert text at current position
* $('input:first').caret('Some text');
* </pre>
*/
caret: function() {
var $inputs = this.filter('input, textarea');
// getCaret()
if (arguments.length === 0) {
var input = $inputs.get(0);
return _getCaret(input);
}
// setCaret(position)
else if (typeof arguments[0] === 'number') {
var pos = arguments[0];
$inputs.each(function(_i, input) {
_setCaret(input, pos);
});
}
// insertAtCaret(text)
else {
var text = arguments[0];
$inputs.each(function(_i, input) {
_insertAtCaret(input, text);
});
}
return this;
},
/**
* Gets or sets the selection range or replaces the currently selected text in an input or textarea element.
* @returns {Range|jQuery} The current selection range if invoked as a getter (with no arguments)
* or this jQuery object if invoked as a setter or replacer.
* @see http://stackoverflow.com/a/2966703/467582
* @since 1.0.0
* @example
* <pre>
* // Get selection range
* var range = $('input:first').range();
* </pre>
* @example
* <pre>
* // Set selection range
* $('input:first').range(15);
* $('input:first').range(15, 20);
* $('input:first').range(-3);
* $('input:first').range(-8, -3);
* </pre>
* @example
* <pre>
* // Replace the currently selected text
* $('input:first').range('Replacement text');
* </pre>
*/
range: function() {
var $inputs = this.filter('input, textarea');
// getRange() = { start: pos, end: pos }
if (arguments.length === 0) {
var input = $inputs.get(0);
return _getInputRange(input);
}
// setRange(startPos, endPos)
else if (typeof arguments[0] === 'number') {
var startPos = arguments[0];
var endPos = arguments[1];
$inputs.each(function(_i, input) {
_setInputRange(input, startPos, endPos);
});
}
// replaceRange(text)
else {
var text = arguments[0];
$inputs.each(function(_i, input) {
_replaceInputRange(input, text);
});
}
return this;
},
/**
* Selects all text in each element of this jQuery object.
* @returns {jQuery} This jQuery object
* @see http://stackoverflow.com/a/11128179/467582
* @since 1.5.0
* @example
* <pre>
* // Select the contents of span elements when clicked
* $('span').on('click', function() { $(this).highlight(); });
* </pre>
*/
selectAll: function() {
return this.each(function(_i, elem) {
_selectAll(elem);
});
}
});
$.extend($, {
/**
* Deselects all text on the page.
* @returns {jQuery} The jQuery function
* @since 1.5.0
* @example
* <pre>
* // Select some text
* $('span').selectAll();
*
* // Deselect the text
* $.deselectAll();
* </pre>
*/
deselectAll: function() {
_deselectAll();
return this;
}
});
}(window.jQuery || window.Zepto || window.$));

View File

@@ -1,57 +0,0 @@
/*
* Timepicker stylesheet
* Highly inspired from datepicker
* FG - Nov 2010 - Web3R
*
* version 0.0.3 : Fixed some settings, more dynamic
* version 0.0.4 : Removed width:100% on tables
* version 0.1.1 : set width 0 on tables to fix an ie6 bug
*/
.ui-timepicker-inline { display: inline; }
#ui-timepicker-div { padding: 0.2em; }
.ui-timepicker-table { display: inline-table; width: 0; }
.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; }
.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; }
.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; }
.ui-timepicker-table td { padding: 0.1em; width: 2.2em; }
.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; }
/* span for disabled cells */
.ui-timepicker-table td span {
display:block;
padding:0.2em 0.3em 0.2em 0.5em;
width: 1.2em;
text-align:right;
text-decoration:none;
}
/* anchors for clickable cells */
.ui-timepicker-table td a {
display:block;
padding:0.2em 0.3em 0.2em 0.5em;
width: 1.2em;
cursor: pointer;
text-align:right;
text-decoration:none;
}
/* buttons and button pane styling */
.ui-timepicker .ui-timepicker-buttonpane {
background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0;
}
.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; }
/* The close button */
.ui-timepicker .ui-timepicker-close { float: right }
/* the now button */
.ui-timepicker .ui-timepicker-now { float: left; }
/* the deselect button */
.ui-timepicker .ui-timepicker-deselect { float: left; }

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +0,0 @@
/**
* Original source code from https://github.com/mcecot/markdown-it-checkbox
* © 2015 Markus Cecot
* licenced under MIT
* https://github.com/mcecot/markdown-it-checkbox/blob/master/LICENSE
*/
var checkboxReplace;
checkboxReplace = function(md, options, Token) {
"use strict";
var arrayReplaceAt, createTokens, defaults, lastId, pattern, splitTextToken;
arrayReplaceAt = md.utils.arrayReplaceAt;
lastId = 0;
defaults = {
divWrap: false,
divClass: 'checkbox',
idPrefix: 'checkbox'
};
options = Object.assign(defaults, options);
pattern = /(.*?)(\[(X|\s|\_|\-)\])(.*)/igm;
createTokens = function(checked, label, Token, before) {
var id, idNumeric, nodes, token;
nodes = [];
token = new Token("text", "", 0);
token.content = before;
nodes.push(token);
/**
* <div class="checkbox">
*/
if (options.divWrap) {
token = new Token("checkbox_open", "div", 1);
token.attrs = [["class", options.divClass]];
nodes.push(token);
}
/**
* <input type="checkbox" id="checkbox{n}" checked="true">
*/
id = options.idPrefix + lastId;
idNumeric = lastId;
lastId += 1;
token = new Token("checkbox_input", "input", 0);
token.attrs = [["type", "checkbox"], ["id", id], ["data-id", idNumeric]];
if (checked === true) {
token.attrs.push(["checked", "true"]);
}
token.attrs.push(["class", "checkbox"]);
nodes.push(token);
/**
* <label for="checkbox{n}">
*/
token = new Token("label_open", "label", 1);
token.attrs = [["for", id], ["data-id", idNumeric]];
nodes.push(token);
/**
* content of label tag
*/
token = new Token("text", "", 0);
token.content = label;
nodes.push(token);
/**
* closing tags
*/
nodes.push(new Token("label_close", "label", -1));
if (options.divWrap) {
nodes.push(new Token("checkbox_close", "div", -1));
}
return nodes;
};
splitTextToken = function(original, Token) {
var checked, label, matches, text, value, before;
text = original.content;
matches = pattern.exec(text);
if (matches === null) {
return original;
}
checked = false;
before = matches[1];
value = matches[3];
label = matches[4];
if (value === "X" || value === "x") {
checked = true;
}
return createTokens(checked, label, Token, before);
};
return function(state) {
lastId = 0;
var blockTokens, i, j, l, token, tokens;
blockTokens = state.tokens;
j = 0;
l = blockTokens.length;
while (j < l) {
if (blockTokens[j].type !== "inline") {
j++;
continue;
}
tokens = blockTokens[j].children;
i = 0;
while (i < tokens.length) {
token = tokens[i];
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, splitTextToken(token, state.Token));
i++;
}
j++;
}
};
};
/*global module */
module.exports = function(md, options) {
"use strict";
md.core.ruler.push("checkbox", checkboxReplace(md, options));
};

9243
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
{
"name": "deck",
"description": "Frontend for the Nextcloud Deck app",
"repository": "https://github.com/nextcloud/deck",
"version": "1.0.0",
"directories": {
"test": "tests"
},
"dependencies": {
"@uirouter/angularjs": "^1.0.22",
"angular": "^1.7.8",
"angular-animate": "^1.7.8",
"angular-file-upload": "^2.5.0",
"angular-markdown-it": "^0.6.1",
"angular-sanitize": "^1.7.8",
"babel-polyfill": "^6.26.0",
"markdown-it": "^9.1.0",
"markdown-it-link-target": "^1.0.2",
"nextcloud-axios": "^0.2.0",
"nextcloud-vue": "^0.12.1",
"nextcloud-vue-collections": "^0.5.6",
"ng-infinite-scroll": "^1.3.0",
"ng-sortable": "^1.3.8",
"ui-select": "^0.19.8",
"vue": "^2.6.10",
"vuex": "^3.1.1"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.5.5",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"karma": "^4.2.0",
"mini-css-extract-plugin": "^0.8.0",
"style-loader": "^1.0.0",
"uglifyjs-webpack-plugin": "^2.2.0",
"url-loader": "^2.1.0",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.39.2",
"webpack-cli": "^3.3.6",
"webpack-merge": "^4.2.1"
},
"scripts": {
"build": "NODE_ENV=production ./node_modules/webpack-cli/bin/cli.js --mode production --config webpack.prod.config.js",
"dev": "./node_modules/webpack-cli/bin/cli.js --mode development --config webpack.dev.config.js",
"watch": "./node_modules/webpack-cli/bin/cli.js --mode development --config webpack.dev.config.js --watch",
"test": "echo \"Warning: no test specified\" && exit 0"
},
"author": "",
"license": "AGPL-3.0",
"keywords": []
}

View File

@@ -1,256 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
import CommentCollection from '../legacy/commentcollection';
import CommentModel from '../legacy/commentmodel';
const DECK_ACTIVITY_TYPE_BOARD = 'deck_board';
const DECK_ACTIVITY_TYPE_CARD = 'deck_card';
/* global OC oc_requesttoken */
class ActivityService {
static get RESULT_PER_PAGE() { return 50; }
constructor ($rootScope, $filter, $http, $q) {
this.running = false;
this.runningNewer = false;
this.$filter = $filter;
this.$http = $http;
this.$q = $q;
this.$rootScope = $rootScope;
this.data = {};
this.data[DECK_ACTIVITY_TYPE_BOARD] = {};
this.data[DECK_ACTIVITY_TYPE_CARD] = {};
this.toEnhanceWithComments = [];
this.commentCollection = new CommentCollection();
this.commentCollection._limit = ActivityService.RESULT_PER_PAGE;
this.commentCollection.on('request', function() {
}, this);
this.commentCollection.on('sync', function(a) {
for (let index in this.toEnhanceWithComments) {
if (this.toEnhanceWithComments.hasOwnProperty(index)) {
let item = this.toEnhanceWithComments[index];
let commentId = Array.isArray(item.subject_rich[1].comment) ? item.subject_rich[1].comment.id : item.subject_rich[1].comment;
item.commentModel = this.commentCollection.get(commentId);
if (typeof item.commentModel !== 'undefined') {
this.toEnhanceWithComments = this.toEnhanceWithComments.filter((entry) => entry.activity_id !== item.activity_id);
}
}
}
var firstUnread = this.commentCollection.findWhere({isUnread: true});
if (typeof firstUnread !== 'undefined') {
this.commentCollection.updateReadMarker();
}
this.notify();
}, this);
this.commentCollection.on('add', function(model, collection, options) {
// we need to update the model, because it consists of client data
// only, but the server might add meta data, e.g. about mentions
model.fetch();
}, this);
this.since = {
deck_card: {
},
deck_board: {
},
};
}
/**
* We need a event here to properly update scope once the external data from
* the comments backbone js code has changed
*/
subscribe(scope, callback) {
let handler = this.$rootScope.$on('notify-comment-update', callback);
scope.$on('$destroy', handler);
}
notify() {
this.$rootScope.$emit('notify-comment-update');
}
static getUrl(type, id, since) {
if (type === DECK_ACTIVITY_TYPE_CARD) {
return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'filter?format=json&object_type=deck_card&object_id=' + id + '&limit=' + this.RESULT_PER_PAGE + '&since=' + since;
}
if (type === DECK_ACTIVITY_TYPE_BOARD) {
return OC.linkToOCS('apps/activity/api/v2/activity', 2) + 'deck?format=json&limit=' + this.RESULT_PER_PAGE + '&since=' + since;
}
}
fetchCardActivities(type, id, since) {
this.running = true;
this.checkData(type, id);
const self = this;
return this.$http.get(ActivityService.getUrl(type, id, since)).then(function (response) {
const objects = response.data.ocs.data;
for (let index in objects) {
if (objects.hasOwnProperty(index)) {
let item = objects[index];
self.addItem(type, id, item);
if (item.activity_id > self.since[type][id].latest) {
self.since[type][id].latest = item.activity_id;
}
}
}
self.data[type][id].sort(function(a, b) {
return b.activity_id - a.activity_id;
});
self.since[type][id].oldest = response.headers('X-Activity-Last-Given');
self.running = false;
return response;
}, function (error) {
if (error.status === 304 || error.status === 404) {
self.since[type][id].finished = true;
}
self.running = false;
});
}
fetchMoreActivities(type, id, success) {
const self = this;
this.checkData(type, id);
if (this.running === true) {
return this.runningPromise;
}
if (!this.since[type][id].finished) {
this.runningPromise = this.fetchCardActivities(type, id, this.since[type][id].oldest);
this.runningPromise.then(function() {
if (type === 'deck_card') {
self.commentCollection.fetchNext();
}
});
return this.runningPromise;
}
return Promise.reject();
}
checkData(type, id) {
if (!Array.isArray(this.data[type][id])) {
this.data[type][id] = [];
}
if (typeof this.since[type][id] === 'undefined') {
this.since[type][id] = {
latest: -1,
oldestCatchedUp: false,
oldest: '0',
finished: false,
};
}
}
addItem(type, id, item) {
const self = this;
const existingEntry = this.data[type][id].findIndex((entry) => { return entry.activity_id === item.activity_id; });
if (existingEntry !== -1) {
return;
}
/** check if the fetched item from all deck activities is actually related */
const isUnrelatedBoard = (item.object_type === DECK_ACTIVITY_TYPE_BOARD && item.object_id !== id);
const isUnrelatedCard = (item.object_type === DECK_ACTIVITY_TYPE_CARD && (
(item.subject_rich[1].board && item.subject_rich[1].board.id !== id) || (typeof item.subject_rich[1].board === 'undefined'))
);
if (type === DECK_ACTIVITY_TYPE_BOARD && (isUnrelatedBoard || isUnrelatedCard)) {
return;
}
item.timestamp = new Date(item.datetime).getTime();
item.type = 'activity';
if (item.subject_rich[1].comment) {
item.type = 'comment';
item.commentModel = this.commentCollection.get(item.subject_rich[1].comment);
if (typeof item.commentModel === 'undefined') {
this.toEnhanceWithComments.push(item);
}
}
this.data[type][id].push(item);
}
/**
* Fetch newer activities starting from the latest ones that are in cache
*
* @param type
* @param id
*/
fetchNewerActivities(type, id) {
if (this.since[type][id].latest === 0) {
return Promise.resolve();
}
let self = this;
return this.fetchNewer(type, id).then(function() {
return self.fetchNewerActivities(type, id);
});
}
fetchNewer(type, id) {
const deferred = this.$q.defer();
this.running = true;
this.runningNewer = true;
const self = this;
this.$http.get(ActivityService.getUrl(type, id, this.since[type][id].latest) + '&sort=asc').then(function (response) {
let objects = response.data.ocs.data;
let data = [];
for (let index in objects) {
if (objects.hasOwnProperty(index)) {
let item = objects[index];
self.addItem(type, id, item);
}
}
self.data[type][id].sort(function(a, b) {
return b.activity_id - a.activity_id;
});
self.since[type][id].latest = response.headers('X-Activity-Last-Given');
self.data[type][id] = data.concat(self.data[type][id]);
self.running = false;
self.runningNewer = false;
deferred.resolve(objects);
}, function (error) {
self.runningNewer = false;
self.running = false;
});
return deferred.promise;
}
getData(type, id) {
if (!Array.isArray(this.data[type][id])) {
return [];
}
return this.data[type][id];
}
loadComments(id) {
this.commentCollection.reset();
this.commentCollection.setObjectId(id);
}
}
app.service('ActivityService', ActivityService);
export default ActivityService;
export {DECK_ACTIVITY_TYPE_BOARD, DECK_ACTIVITY_TYPE_CARD};

View File

@@ -1,232 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/** global: oc_defaults */
app.factory('ApiService', function ($http, $q) {
var ApiService = function (http, endpoint) {
// Consider renaming endpoint to resource
this.endpoint = endpoint;
this.baseUrl = this.generateUrl(this.endpoint);
this.http = http;
this.q = $q;
this.data = {};
this.deleted = {};
this.id = null;
this.sorted = [];
};
ApiService.prototype.generateUrl = function(path) {
return OC.generateUrl('/apps/deck/' + path);
};
ApiService.prototype.tryAllThenDeleted = function(id) {
let object = this.data[id];
if (object === undefined) {
object = this.deleted[id];
}
return object;
};
ApiService.prototype.fetchAll = function () {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl).then(function (response) {
var objects = response.data;
objects.forEach(function (obj) {
self.data[obj.id] = obj;
});
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Fetching ' + self.endpoint + ' failed');
});
return deferred.promise;
};
ApiService.prototype.fetchDeleted = function (scopeId) {
var deferred = $q.defer();
var self = this;
self.deleted = {};
$http.get(this.generateUrl(scopeId + '/' + this.endpoint + '/deleted')).then(function (response) {
var objects = response.data;
objects.forEach(function (obj) {
if(self.deleted[obj.id] !== undefined) {
return;
}
self.deleted[obj.id] = obj;
if(self.afterFetch !== undefined) {
self.afterFetch(obj);
}
});
deferred.resolve(objects);
}, function (error) {
deferred.reject('Fetching ' + self.endpoint + ' failed');
});
return deferred.promise;
};
ApiService.prototype.fetchOne = function (id) {
this.id = id;
var deferred = $q.defer();
if (id === undefined) {
return deferred.promise;
}
var self = this;
$http.get(this.baseUrl + '/' + id).then(function (response) {
var data = response.data;
if (self.data[data.id] === undefined) {
self.data[data.id] = response.data;
}
$.each(response.data, function (key, value) {
self.data[data.id][key] = value;
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Fetching ' + self.endpoint + ' failed');
});
return deferred.promise;
};
ApiService.prototype.create = function (entity) {
var deferred = $q.defer();
var self = this;
$http.post(this.baseUrl, entity).then(function (response) {
self.add(response.data);
deferred.resolve(response.data);
}, function (error) {
deferred.reject(error.data.message);
});
return deferred.promise;
};
ApiService.prototype.update = function (entity) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + entity.id, entity).then(function (response) {
self.add(response.data);
deferred.resolve(response.data);
}, function (error) {
deferred.reject(error.data.message);
});
return deferred.promise;
};
ApiService.prototype.delete = function (id) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + id).then(function (response) {
self.deleted[id] = self.data[id];
delete self.data[id];
let deletedAt = response.data.deletedAt;
if (deletedAt !== undefined) {
self.deleted[id].deletedAt = deletedAt;
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Deleting ' + self.endpoint + ' failed');
});
return deferred.promise;
};
ApiService.prototype.undoDelete = function(entity) {
var self = this;
entity.deletedAt = 0;
var promise = this.update(entity);
promise.then(() => {
self.data[entity.id] = entity;
delete this.deleted[entity.id];
});
return promise;
};
// methods for managing data
ApiService.prototype.clear = function () {
this.data = {};
};
ApiService.prototype.add = function (entity) {
var element = this.data[entity.id];
if (element === undefined) {
this.data[entity.id] = entity;
} else {
Object.keys(entity).forEach(function (key) {
if (entity[key] !== null && element[key] !== entity[key]) {
element[key] = entity[key];
}
});
element.status = {};
}
};
ApiService.prototype.addAll = function (entities) {
var self = this;
angular.forEach(entities, function (entity) {
self.add(entity);
});
};
ApiService.prototype.getCurrent = function () {
return this.data[this.id];
};
ApiService.prototype.unsetCurrrent = function () {
this.id = null;
};
ApiService.prototype.getData = function () {
return $.map(this.data, function (value, index) {
return [value];
});
};
ApiService.prototype.getAll = function () {
return this.data;
};
ApiService.prototype.get = function (id) {
return this.data[id];
};
ApiService.prototype.getName = function () {
var funcNameRegex = /function (.{1,})\(/;
var results = (funcNameRegex).exec((this).constructor.toString());
return (results && results.length > 1) ? results[1] : '';
};
return ApiService;
});

View File

@@ -1,272 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global app OC angular */
app.factory('BoardService', function (ApiService, $http, $q) {
var BoardService = function ($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
BoardService.prototype = angular.copy(ApiService.prototype);
BoardService.prototype.delete = function (id) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + id).then(function (response) {
self.data[id].deletedAt = response.data.deletedAt;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Deleting ' + self.endpoint + ' failed');
});
return deferred.promise;
};
BoardService.prototype.deleteUndo = function (id) {
var deferred = $q.defer();
var self = this;
var _id = id;
$http.post(this.baseUrl + '/' + id + '/deleteUndo').then(function (response) {
self.data[_id].deletedAt = 0;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Deleting ' + self.endpoint + ' failed');
});
return deferred.promise;
};
BoardService.prototype.searchUsers = function (search) {
var deferred = $q.defer();
var self = this;
var searchData = {
format: 'json',
perPage: 4,
itemType: [0, 1, 7]
};
if (search !== "") {
searchData.search = search;
}
$http({
method: 'GET',
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'sharees',
params: searchData
})
.then(function (result) {
var response = result.data;
if (response.ocs.meta.statuscode !== 100) {
deferred.reject('Error while searching for sharees');
return;
}
self.sharees = [];
var users = response.ocs.data.exact.users.concat(response.ocs.data.users.slice(0, 4));
var groups = response.ocs.data.exact.groups.concat(response.ocs.data.groups.slice(0, 4));
var circles = response.ocs.data.exact.groups.concat(response.ocs.data.circles.slice(0, 4));
// filter out everyone who is already in the share list
angular.forEach(users, function (item) {
var acl = self.generateAcl(OC.Share.SHARE_TYPE_USER, item);
var exists = false;
angular.forEach(self.getCurrent().acl, function (acl) {
if (acl.participant.primaryKey === item.value.shareWith) {
exists = true;
}
});
if (!exists && OC.getCurrentUser().uid !== item.value.shareWith) {
self.sharees.push(acl);
}
});
angular.forEach(groups, function (item) {
var acl = self.generateAcl(OC.Share.SHARE_TYPE_GROUP, item);
var exists = false;
angular.forEach(self.getCurrent().acl, function (acl) {
if (acl.participant.primaryKey === item.value.shareWith) {
exists = true;
}
});
if (!exists) {
self.sharees.push(acl);
}
});
angular.forEach(circles, function (item) {
var acl = self.generateAcl(OC.Share.SHARE_TYPE_CIRCLE, item);
var exists = false;
angular.forEach(self.getCurrent().acl, function (acl) {
if (acl.participant.primaryKey === item.value.shareWith) {
exists = true;
}
});
if (!exists) {
self.sharees.push(acl);
}
});
deferred.resolve(self.sharees);
}, function () {
deferred.reject('Error while searching for sharees');
});
return deferred.promise;
};
BoardService.prototype.generateAcl = function (type, ocsItem) {
return {
boardId: null,
id: null,
owner: false,
participant: {
primaryKey: ocsItem.value.shareWith,
uid: ocsItem.value.shareWith,
displayname: ocsItem.label
},
permissionEdit: true,
permissionManage: false,
permissionShare: false,
type: type
};
};
BoardService.prototype.addAcl = function (acl) {
var board = this.getCurrent();
var deferred = $q.defer();
var self = this;
var _acl = acl;
$http.post(this.baseUrl + '/' + acl.boardId + '/acl', _acl).then(function (response) {
if (!board.acl || board.acl.length === 0) {
board.acl = {};
}
board.acl[response.data.id] = response.data;
if (response.data.type === OC.Share.SHARE_TYPE_USER) {
self._updateUsers();
} else {
self.fetchOne(response.data.boardId);
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error creating ACL ' + _acl);
});
acl = null;
return deferred.promise;
};
BoardService.prototype.deleteAcl = function (acl) {
var board = this.getCurrent();
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + acl.boardId + '/acl/' + acl.id).then(function (response) {
var index = board.acl.findIndex((item) => item.id === response.data.id);
delete board.acl[index];
if (response.data.type === OC.Share.SHARE_TYPE_USER) {
self._updateUsers();
} else {
self.fetchOne(response.data.boardId);
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error deleting ACL ' + acl.id);
});
acl = null;
return deferred.promise;
};
BoardService.prototype.updateAcl = function (acl) {
var board = this.getCurrent();
var deferred = $q.defer();
var self = this;
var _acl = acl;
$http.put(this.baseUrl + '/' + acl.boardId + '/acl', _acl).then(function (response) {
var index = board.acl.findIndex((item) => item.id === _acl.id);
board.acl[index] = response.data;
if (response.data.type === OC.Share.SHARE_TYPE_USER) {
self._updateUsers();
} else {
self.fetchOne(response.data.boardId);
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error updating ACL ' + _acl);
});
acl = null;
return deferred.promise;
};
BoardService.prototype._updateUsers = function () {
if (!this.getCurrent() || !this.getCurrent().acl) {
return [];
}
this.getCurrent().users = [this.getCurrent().owner];
let self = this;
angular.forEach(this.getCurrent().acl, function(value, key) {
if (value.type === OC.Share.SHARE_TYPE_USER) {
self.getCurrent().users.push(value.participant);
}
});
};
BoardService.prototype.getUsers = function () {
if (this.getCurrent() && !this.getCurrent().users) {
this._updateUsers();
}
return this.getCurrent().users;
};
BoardService.prototype.canRead = function () {
if (!this.getCurrent() || !this.getCurrent().permissions) {
return false;
}
return this.getCurrent().permissions['PERMISSION_READ'];
};
BoardService.prototype.canEdit = function () {
if (!this.getCurrent() || !this.getCurrent().permissions) {
return false;
}
return this.getCurrent().permissions['PERMISSION_EDIT'];
};
BoardService.prototype.canManage = function (board) {
if (board !== null && board !== undefined) {
return board.permissions['PERMISSION_MANAGE'];
}
if (!this.getCurrent() || !this.getCurrent().permissions) {
return false;
}
return this.getCurrent().permissions['PERMISSION_MANAGE'];
};
BoardService.prototype.canShare = function () {
if (!this.getCurrent() || !this.getCurrent().permissions) {
return false;
}
return this.getCurrent().permissions['PERMISSION_SHARE'];
};
BoardService.prototype.isArchived = function () {
if (!this.getCurrent() || this.getCurrent().archived) {
return true;
}
return false;
};
return new BoardService($http, 'boards', $q);
});

View File

@@ -1,177 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.factory('CardService', function (ApiService, $http, $q) {
var CardService = function ($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
CardService.prototype = angular.copy(ApiService.prototype);
CardService.prototype.reorder = function (card, order) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + card.id + '/reorder', {
cardId: card.id,
order: order,
stackId: card.stackId
}).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.rename = function (card) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + card.id + '/rename', {
cardId: card.id,
title: card.title
}).then(function (response) {
self.data[card.id].title = card.title;
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while renaming ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.assignLabel = function (card, label) {
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.post(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.removeLabel = function (card, label) {
var url = this.baseUrl + '/' + card + '/label/' + label;
var deferred = $q.defer();
var self = this;
$http.delete(url).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.archive = function (card) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + card.id + '/archive', {}).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.unarchive = function (card) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + card.id + '/unarchive', {}).then(function (response) {
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.assignUser = function (card, user) {
var deferred = $q.defer();
var self = this;
if (self.get(card.id).assignedUsers === null) {
self.get(card.id).assignedUsers = [];
}
$http.post(this.baseUrl + '/' + card.id + '/assign', {'userId': user}).then(function (response) {
self.get(card.id).assignedUsers.push(response.data);
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.unassignUser = function (card, user) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + card.id + '/assign/' + user, {}).then(function (response) {
self.get(card.id).assignedUsers = self.get(card.id).assignedUsers.filter(function (obj) {
return obj.participant.uid !== user;
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
CardService.prototype.attachmentRemove = function (attachment) {
var deferred = $q.defer();
var self = this;
$http.delete(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id, {}).then(function (response) {
if (response.data.deletedAt > 0) {
let currentAttachment = self.getCurrent().attachments.find(function (obj) {
if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
} else {
self.getCurrent().attachments = self.getCurrent().attachments.filter(function (obj) {
return obj.id !== attachment.id;
});
}
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error when removing the attachment');
});
return deferred.promise;
};
CardService.prototype.attachmentRemoveUndo = function (attachment) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id + '/restore', {}).then(function (response) {
let currentAttachment = self.getCurrent().attachments.find(function (obj) {
if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error when restoring the attachment');
});
return deferred.promise;
};
var service = new CardService($http, 'cards', $q);
return service;
});

View File

@@ -1,137 +0,0 @@
/*
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global OC oc_requesttoken */
export default class FileService {
constructor (FileUploader, CardService, $rootScope, $filter) {
this.$filter = $filter;
this.uploader = new FileUploader();
this.cardservice = CardService;
this.uploader.onAfterAddingFile = this.onAfterAddingFile.bind(this);
this.uploader.onSuccessItem = this.onSuccessItem.bind(this);
this.uploader.onErrorItem = this.onErrorItem.bind(this);
this.uploader.onCancelItem = this.onCancelItem.bind(this);
this.maxUploadSize = $rootScope.config.maxUploadSize;
this.progress = [];
this.status = null;
}
reset () {
this.status = null;
}
runUpload (fileItem, attachmentId) {
this.status = null;
fileItem.url = OC.generateUrl('/apps/deck/cards/' + fileItem.cardId + '/attachment?type=deck_file');
if (typeof attachmentId !== 'undefined') {
fileItem.url = OC.generateUrl('/apps/deck/cards/' + fileItem.cardId + '/attachment/' + attachmentId + '?type=deck_file');
} else {
fileItem.formData = [
{
requesttoken: oc_requesttoken,
type: 'deck_file',
}
];
}
fileItem.headers = {requesttoken: oc_requesttoken};
this.uploader.uploadItem(fileItem);
}
onAfterAddingFile (fileItem) {
if (this.maxUploadSize > 0 && fileItem.file.size > this.maxUploadSize) {
this.status = {
error: t('deck', `Failed to upload {name}`, {name: fileItem.file.name}),
message: t('deck', 'Maximum file size of {size} exceeded', {size: this.$filter('bytes')(this.maxUploadSize)})
};
return;
}
// Fetch card details before trying to upload so we can detect filename collisions properly
let self = this;
this.progress.push(fileItem);
this.cardservice.fetchOne(fileItem.cardId).then(function (data) {
let attachments = self.cardservice.get(fileItem.cardId).attachments;
let existingFile = attachments.find((attachment) => {
return attachment.data === fileItem.file.name;
});
if (typeof existingFile !== 'undefined') {
OC.dialogs.confirm(
t('deck', `A file with the name ${fileItem.file.name} already exists. Do you want to overwrite it?`),
t('deck', 'File already exists'),
function (result) {
if (result) {
self.runUpload(fileItem, existingFile.id);
} else {
let fileName = existingFile.extendedData.info.filename;
let foundFilesMatching = attachments.filter((attachment) => {
return attachment.extendedData.info.extension === existingFile.extendedData.info.extension
&& attachment.extendedData.info.filename.startsWith(fileName);
});
let nextIndex = foundFilesMatching.length + 1;
fileItem.file.name = fileName + ' (' + nextIndex + ').' + existingFile.extendedData.info.extension;
self.runUpload(fileItem);
}
}
);
} else {
self.runUpload(fileItem);
}
}, function (error) {
this.progress = this.progress.filter((item) => (fileItem.file.name !== item.file.name));
});
}
onSuccessItem (item, response) {
let attachments = this.cardservice.get(item.cardId).attachments;
let index = attachments.indexOf(attachments.find((attachment) => attachment.id === response.id));
if (~index) {
attachments = attachments.splice(index, 1);
}
this.cardservice.get(item.cardId).attachments.push(response);
this.progress = this.progress.filter((fileItem) => (fileItem.file.name !== item.file.name));
}
onErrorItem (item, response) {
this.progress = this.progress.filter((fileItem) => (fileItem.file.name !== item.file.name));
this.status = {
error: t('deck', `Failed to upload:`) + ' ' + item.file.name,
message: response.message
};
}
onCancelItem (item) {
this.progress = this.progress.filter((fileItem) => (fileItem.file.name !== item.file.name));
}
getProgressItemsForCard (cardId) {
return this.progress.filter((fileItem) => (fileItem.cardId === cardId));
}
}
app.service('FileService', FileService);

View File

@@ -1,30 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.factory('LabelService', function (ApiService, $http, $q) {
var LabelService = function ($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
LabelService.prototype = angular.copy(ApiService.prototype);
return new LabelService($http, 'labels', $q);
});

View File

@@ -1,139 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
/* global app angular */
app.factory('StackService', function (ApiService, CardService, $http, $q) {
var StackService = function ($http, ep, $q) {
ApiService.call(this, $http, ep, $q);
};
StackService.prototype = angular.copy(ApiService.prototype);
StackService.prototype.afterFetch = function(stack) {
CardService.addAll(stack.cards);
};
StackService.prototype.fetchAll = function (boardId) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/' + boardId).then(function (response) {
self.clear();
self.addAll(response.data);
// When loading a stack add cards to the CardService so we can fetch
// information from there. That way we don't need to refresh the whole
// stack data during digest if some value changes
angular.forEach(response.data, function (entity) {
CardService.addAll(entity.cards);
});
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Error while loading stacks');
});
return deferred.promise;
};
StackService.prototype.fetchArchived = function (boardId) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/' + boardId + '/archived').then(function (response) {
self.clear();
self.addAll(response.data);
angular.forEach(response.data, function (entity) {
CardService.addAll(entity.cards);
});
deferred.resolve(self.data);
}, function (error) {
deferred.reject('Error while loading stacks');
});
return deferred.promise;
};
StackService.prototype.addCard = function (entity) {
if (!this.data[entity.stackId].cards) {
this.data[entity.stackId].cards = [];
}
this.data[entity.stackId].cards.push(entity);
};
StackService.prototype.reorder = function (stack, order) {
var deferred = $q.defer();
var self = this;
$http.put(this.baseUrl + '/' + stack.id + '/reorder', {
stackId: stack.id,
order: order
}).then(function (response) {
angular.forEach(response.data, function (value, key) {
var id = value.id;
self.data[id].order = value.order;
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error while update ' + self.endpoint);
});
return deferred.promise;
};
StackService.prototype.reorderCard = function (entity, order) {
// assign new order
for (var i = 0, j = 0; i < this.data[entity.stackId].cards.length; i++) {
if (this.data[entity.stackId].cards[i].id === entity.id) {
this.data[entity.stackId].cards[i].order = order;
}
if (j === order) {
j++;
}
if (this.data[entity.stackId].cards[i].id !== entity.id) {
this.data[entity.stackId].cards[i].order = j++;
}
}
// sort array by order
this.data[entity.stackId].cards.sort(function (a, b) {
if (a.order < b.order)
{return -1;}
if (a.order > b.order)
{return 1;}
return 0;
});
};
StackService.prototype.updateCard = function (entity) {
var self = this;
var cards = this.data[entity.stackId].cards;
for (var i = 0; i < cards.length; i++) {
if (cards[i].id === entity.id) {
cards[i] = entity;
}
}
};
StackService.prototype.removeCard = function (entity) {
var self = this;
var cards = this.data[entity.stackId].cards;
for (var i = 0; i < cards.length; i++) {
if (cards[i].id === entity.id) {
cards.splice(i, 1);
}
}
};
var service = new StackService($http, 'stacks', $q);
return service;
});

View File

@@ -1,82 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import app from '../app/App.js';
app.factory('StatusService', function () {
// Status Helper
var StatusService = function () {
this.active = true;
this.icon = 'loading';
this.title = '';
this.text = '';
this.counter = 0;
};
StatusService.prototype.setStatus = function ($icon, $title, $text) {
this.active = true;
this.icon = $icon;
this.title = $title;
this.text = $text;
};
StatusService.prototype.setError = function ($title, $text) {
this.active = true;
this.icon = 'error';
this.title = $title;
this.text = $text;
this.counter = 0;
};
StatusService.prototype.releaseWaiting = function () {
if (this.counter > 0) {
this.counter--;
}
if (this.counter <= 0) {
this.active = false;
this.counter = 0;
}
};
StatusService.prototype.retainWaiting = function () {
this.active = true;
this.icon = 'loading';
this.title = '';
this.text = '';
this.counter++;
};
StatusService.prototype.unsetStatus = function () {
this.active = false;
};
return {
getInstance: function () {
return new StatusService();
},
/* Shared StatusService instance between both ListController instances */
listStatus: new StatusService()
};
});

View File

@@ -1,41 +0,0 @@
/*
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
describe('BoardController', function() {
'use strict';
var $controller;
beforeEach(inject(function(_$controller_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$controller = _$controller_;
}));
describe('$scope.rgblight', function() {
it('converts rbg color to a lighter color', function() {
var $scope = {};
var controller = $controller('BoardController', { $scope: $scope });
var hex = $scope.rgblight('red');
expect(hex).toEqual('#red');
});
});
});

View File

@@ -1,76 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
node: {
fs: 'empty',
},
entry: {
deck: ['./init.js'],
collections: ['./init-collections.js']
},
output: {
filename: '[name].js',
path: __dirname + '/build'
},
module: {
rules: [
{
test: /\.css$/,
use: ['vue-style-loader', 'style-loader', 'css-loader']
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['@babel/preset-env'],
plugins: ['@babel/plugin-syntax-dynamic-import']
}
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader',
options: {
name: '[name].[ext]?[hash]'
}
},
]
},
/* use external jQuery from server */
externals: {
'jquery': 'jQuery'
},
resolve: {
alias: {
vue$: 'vue/dist/vue.esm.js'
},
extensions: ['*', '.js', '.vue', '.json'],
modules: [
path.resolve(__dirname),
path.join(__dirname, 'node_modules'),
'node_modules'
]
},
plugins: [
new VueLoaderPlugin(),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
]
};

View File

@@ -1,7 +0,0 @@
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.js');
module.exports = merge(baseConfig, {
mode: 'development',
devtool: 'source-map',
});

View File

@@ -1,15 +0,0 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const baseConfig = require('./webpack.config.js');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = merge(baseConfig, {
mode: 'production',
optimization: {
minimizer: [
new UglifyJsPlugin({
test: /(vendor\.js)+/i
})
]
}});

View File

@@ -187,8 +187,9 @@ class Application extends App {
/** @var IManager $resourceManager */
$resourceManager = $this->getContainer()->query(IManager::class);
$resourceManager->registerResourceProvider(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
$resourceManager->registerResourceProvider(\OCA\Deck\Collaboration\Resources\ResourceProviderCard::class);
\OC::$server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', function () {
\OCP\Util::addScript('deck', 'build/collections');
\OCP\Util::addScript('deck', 'collections');
});
}

View File

@@ -0,0 +1,160 @@
<?php
/**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Deck\Collaboration\Resources;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Board;
use OCA\Deck\Db\BoardMapper;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\Service\PermissionService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\QueryException;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IProvider;
use OCP\Collaboration\Resources\IResource;
use OCP\Collaboration\Resources\ResourceException;
use OCP\IURLGenerator;
use OCP\IUser;
class ResourceProviderCard implements IProvider {
const RESOURCE_TYPE = 'deck-card';
/** @var CardMapper */
private $cardMapper;
/** @var BoardMapper */
private $boardMapper;
/** @var PermissionService */
private $permissionService;
/** @var IURLGenerator */
private $urlGenerator;
/** @var array */
protected $nodes = [];
public function __construct(CardMapper $cardMapper, BoardMapper $boardMapper, PermissionService $permissionService, IURLGenerator $urlGenerator) {
$this->cardMapper = $cardMapper;
$this->boardMapper = $boardMapper;
$this->permissionService = $permissionService;
$this->urlGenerator = $urlGenerator;
}
/**
* Get the type of a resource
*
* @return string
* @since 15.0.0
*/
public function getType(): string {
return self::RESOURCE_TYPE;
}
/**
* Get the rich object data of a resource
*
* @param IResource $resource
* @return array
* @since 16.0.0
*/
public function getResourceRichObject(IResource $resource): array {
try {
$card = $this->cardMapper->find($resource->getId());
$board = $this->getBoard($resource->getId());
} catch (DoesNotExistException $e) {
throw new ResourceException('No card found for resource');
} catch (MultipleObjectsReturnedException $e) {
throw new ResourceException('No unique card found for resource, this should never happen');
}
$link = $this->urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId() . '/cards/' . $resource->getId();
return [
'type' => self::RESOURCE_TYPE,
'id' => $resource->getId(),
'name' => $board->getTitle() . ': ' . $card->getTitle(),
'link' => $link,
'iconUrl' => $this->urlGenerator->imagePath('core', 'actions/toggle-pictures.svg')
];
}
/**
* Can a user/guest access the collection
*
* @param IResource $resource
* @param IUser|null $user
* @return bool
* @since 16.0.0
*/
public function canAccessResource(IResource $resource, ?IUser $user): bool {
if (!$user instanceof IUser || $resource->getType() !== self::RESOURCE_TYPE) {
return false;
}
try {
$board = $this->getBoard($resource->getId());
} catch (DoesNotExistException $e) {
return false;
} catch (MultipleObjectsReturnedException $e) {
return false;
}
if ($board === null) {
return false;
}
if ($board->getOwner() === $user->getUID()) {
return true;
}
return $this->permissionService->userCan($board->getAcl(), Acl::PERMISSION_READ, $user->getUID());
}
/**
* @param $cardId
* @return Board
* @throws DoesNotExistException
* @throws MultipleObjectsReturnedException
*/
private function getBoard($cardId) {
$boardId = $this->cardMapper->findBoardId($cardId);
return $this->boardMapper->find($boardId, false, true);
}
public function invalidateAccessCache($cardId = null) {
try {
/** @var IManager $resourceManager */
$resourceManager = \OC::$server->query(IManager::class);
} catch (QueryException $e) {
}
if ($cardId !== null) {
$resource = $resourceManager->getResourceForUser(self::RESOURCE_TYPE, (string)$cardId, null);
$resourceManager->invalidateAccessCacheForResource($resource);
} else {
$resourceManager->invalidateAccessCacheForProvider($this);
}
}
}

85
package-lock.json generated
View File

@@ -10672,6 +10672,12 @@
"brorand": "^1.0.1"
}
},
"mime": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"dev": true
},
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
@@ -16026,6 +16032,85 @@
}
}
},
"url-loader": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.2.0.tgz",
"integrity": "sha512-G8nk3np8ZAnwhHXas1JxJEwJyQdqFXAKJehfgZ/XrC48volFBRtO+FIKtF2u0Ma3bw+4vnDVjHPAQYlF9p2vsw==",
"dev": true,
"requires": {
"loader-utils": "^1.2.3",
"mime": "^2.4.4",
"schema-utils": "^2.4.1"
},
"dependencies": {
"ajv": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
"integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
"dev": true,
"requires": {
"fast-deep-equal": "^2.0.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"ajv-keywords": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz",
"integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==",
"dev": true
},
"big.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
"dev": true
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
},
"loader-utils": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz",
"integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^2.0.0",
"json5": "^1.0.1"
}
},
"schema-utils": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.5.0.tgz",
"integrity": "sha512-32ISrwW2scPXHUSusP8qMg5dLUawKkyV+/qIEV9JdXKx+rsM6mi8vZY8khg2M69Qom16rtroWXD3Ybtiws38gQ==",
"dev": true,
"requires": {
"ajv": "^6.10.2",
"ajv-keywords": "^3.4.1"
}
}
}
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",

View File

@@ -80,6 +80,7 @@
"stylelint": "^8.4.0",
"stylelint-config-recommended-scss": "^3.3.0",
"stylelint-webpack-plugin": "^0.10.5",
"url-loader": "^2.2.0",
"vue-jest": "^3.0.5",
"vue-loader": "^15.7.1",
"vue-style-loader": "^4.1.1",

View File

@@ -25,12 +25,14 @@
<div id="modal-inner" :class="{ 'icon-loading': loading }">
<h1>{{ t('deck', 'Select the board to link to a project') }}</h1>
<ul v-if="!loading">
<li v-for="board in boards" v-if="!currentBoard || ''+board.id !== ''+currentBoard" @click="selectedBoard=board.id" :class="{'selected': (selectedBoard === board.id) }">
<span class="board-bullet" :style="{ 'backgroundColor': '#' + board.color }"></span>
<li v-for="board in boards" v-if="!currentBoard || ''+board.id !== ''+currentBoard" :key="board.id"
:class="{'selected': (selectedBoard === board.id) }"
@click="selectedBoard=board.id">
<span :style="{ 'backgroundColor': '#' + board.color }" class="board-bullet" />
<span>{{ board.title }}</span>
</li>
</ul>
<button v-if="!loading" @click="select" class="primary">{{ t('deck', 'Select board') }}</button>
<button v-if="!loading" class="primary" @click="select">{{ t('deck', 'Select board') }}</button>
</div>
</Modal>
</template>
@@ -69,50 +71,42 @@
</style>
<script>
/* global OC */
import { Modal } from 'nextcloud-vue/dist/Components/Modal'
import { Avatar } from 'nextcloud-vue/dist/Components/Avatar'
import axios from 'nextcloud-axios'
/* global OC */
import { Modal } from 'nextcloud-vue/dist/Components/Modal'
import { Avatar } from 'nextcloud-vue/dist/Components/Avatar'
import axios from 'nextcloud-axios'
export default {
name: 'CollaborationView',
components: {
Modal, Avatar
export default {
name: 'CollaborationView',
components: {
Modal, Avatar
},
data() {
return {
boards: [],
selectedBoard: null,
loading: true,
currentBoard: null
}
},
beforeMount() {
this.fetchBoards()
this.currentBoard = window.location.hash.match(/\/boards\/([0-9]+)/)[1] || null
},
methods: {
fetchBoards() {
axios.get(OC.generateUrl('/apps/deck/boards')).then((response) => {
this.boards = response.data
this.loading = false
})
},
data() {
return {
boards: [],
selectedBoard: null,
loading: true,
currentBoard: null,
}
close() {
this.$root.$emit('close')
},
beforeMount() {
this.fetchBoards();
if (typeof angular !== 'undefined' && angular.element('#board')) {
try {
this.currentBoard = angular.element('#board').scope().boardservice.id || null;
} catch (e) {}
}
},
methods: {
fetchBoards() {
axios.get(OC.generateUrl('/apps/deck/boards')).then((response) => {
this.boards = response.data
this.loading = false
})
},
close() {
this.$root.$emit('close');
},
select() {
this.$root.$emit('select', this.selectedBoard)
}
},
computed: {
},
select() {
this.$root.$emit('select', this.selectedBoard)
}
}
}
</script>

137
src/CardSelector.vue Normal file
View File

@@ -0,0 +1,137 @@
<!--
- @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
-
- @author Julius Härtl <jus@bitgrid.net>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<Modal :title="t('deck', 'Select the card to link to a project')" @close="close">
<div id="modal-inner" :class="{ 'icon-loading': loading }">
<Multiselect :placeholder="t('deck', 'Select a board')" v-model="selectedBoard" :options="boards"
label="title"
@select="fetchCardsFromBoard" />
<Multiselect :placeholder="t('deck', 'Select a card')" v-model="selectedCard" :options="cardsFromBoard"
label="title" />
<button :disabled="!isBoardAndStackChoosen" class="primary" @click="select">{{ t('deck', 'Link to card') }}</button>
<button @click="close">{{ t('deck', 'Cancel') }}</button>
</div>
</Modal>
</template>
<script>
import { Modal } from 'nextcloud-vue/dist/Components/Modal'
import { Multiselect } from 'nextcloud-vue/dist/Components/Multiselect'
import axios from 'nextcloud-axios'
export default {
name: 'CollaborationView',
components: {
Modal,
Multiselect
},
data() {
return {
boards: [],
selectedBoard: '',
cardsFromBoard: [],
selectedCard: '',
loading: true
}
},
computed: {
isBoardAndStackChoosen() {
if (this.selectedBoard === '' || this.selectedCard === '') {
return false
}
return true
}
},
beforeMount() {
this.fetchBoards()
},
methods: {
fetchBoards() {
axios.get(OC.generateUrl('/apps/deck/boards')).then((response) => {
this.boards = response.data
this.loading = false
})
},
async fetchCardsFromBoard(board) {
try {
this.cardsFromBoard = []
let url = OC.generateUrl('/apps/deck/stacks/' + board.id)
let response = await axios.get(url)
response.data.forEach(stack => {
this.cardsFromBoard.push(...stack.cards)
})
} catch (err) {
return err
}
},
close() {
this.$root.$emit('close')
},
select() {
this.$root.$emit('select', this.selectedCard.id)
}
}
}
</script>
<style scoped>
#modal-inner {
width: 90vw;
max-width: 400px;
padding: 20px;
height: 500px;
}
ul {
min-height: 100px;
}
li {
padding: 6px;
border: 1px solid transparent;
}
li:hover, li:focus {
background-color: var(--color-background-dark);
}
li.selected {
border: 1px solid var(--color-primary);
}
.board-bullet {
display: inline-block;
width: 12px;
height: 12px;
border: none;
border-radius: 50%;
cursor: pointer;
}
li > span,
.avatar {
vertical-align: middle;
}
button {
float: right;
}
</style>

View File

@@ -71,6 +71,11 @@
</div>
</div>
<div class="section-wrapper">
<collection-list v-if="currentCard.id" :id="`${currentCard.id}`" :name="currentCard.title"
type="deck-card" />
</div>
<h5>Description</h5>
<VueEasymde ref="markdownEditor" v-model="copiedCard.description" :configs="mdeConfig" />
</AppSidebarTab>
@@ -97,6 +102,7 @@ import { Actions } from 'nextcloud-vue/dist/Components/Actions'
import { ActionButton } from 'nextcloud-vue/dist/Components/ActionButton'
import ActivityEntry from '../ActivityEntry'
import Color from '../../mixins/color'
import { CollectionList } from 'nextcloud-vue-collections'
export default {
name: 'CardSidebar',
@@ -109,7 +115,8 @@ export default {
VueEasymde,
Actions,
ActionButton,
Avatar
Avatar,
CollectionList
},
mixins: [
Color

94
src/init-collections.js Normal file
View File

@@ -0,0 +1,94 @@
/*
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import BoardSelector from './BoardSelector'
import CardSelector from './CardSelector'
import './../css/collections.css'
// eslint-disable-next-line
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line
__webpack_public_path__ = OC.linkTo('deck', 'js/');
Vue.prototype.t = t
Vue.prototype.n = n
Vue.prototype.OC = OC;
((function(OCP) {
OCP.Collaboration.registerType('deck', {
action: () => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const ComponentVM = new Vue({
render: h => h(BoardSelector)
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Board selection canceled'))
})
ComponentVM.$root.$on('select', (id) => {
resolve(id)
ComponentVM.$el.remove()
ComponentVM.$destroy()
})
})
},
typeString: t('deck', 'Link to a board'),
typeIconClass: 'icon-deck'
})
OCP.Collaboration.registerType('deck-card', {
action: () => {
return new Promise((resolve, reject) => {
const container = document.createElement('div')
container.id = 'deck-board-select'
const body = document.getElementById('body-user')
body.append(container)
const ComponentVM = new Vue({
render: h => h(CardSelector)
})
ComponentVM.$mount(container)
ComponentVM.$root.$on('close', () => {
ComponentVM.$el.remove()
ComponentVM.$destroy()
reject(new Error('Card selection canceled'))
})
ComponentVM.$root.$on('select', (id) => {
resolve(id)
ComponentVM.$el.remove()
ComponentVM.$destroy()
})
})
},
typeString: t('deck', 'Link to a card'),
typeIconClass: 'icon-deck'
})
})(window.OCP))

View File

@@ -2,11 +2,14 @@ const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
entry: path.join(__dirname, 'src', 'main.js'),
entry: {
deck: path.join(__dirname, 'src', 'main.js'),
collections: path.join(__dirname, 'src', 'init-collections.js'),
},
output: {
path: path.resolve(__dirname, './js'),
filename: '[name].js',
path: __dirname + '/js',
publicPath: '/js/',
filename: 'deck.js',
jsonpFunction: 'webpackJsonpOCADeck'
},
module: {
@@ -34,7 +37,7 @@ module.exports = {
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'file-loader',
loader: 'url-loader',
options: {
name: '[name].[ext]?[hash]'
}