mirror of
https://github.com/wekan/wekan.git
synced 2026-02-03 07:01:47 +01:00
Merge branch 'master' of https://github.com/wekan/wekan
This commit is contained in:
commit
0ce2f9ea43
204 changed files with 13619 additions and 2379 deletions
125
models/boards.js
125
models/boards.js
|
|
@ -109,6 +109,8 @@ Boards.attachSchema(
|
|||
* List of labels attached to a board
|
||||
*/
|
||||
type: [Object],
|
||||
optional: true,
|
||||
/* Commented out, so does not create labels to new boards.
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert && !this.isSet) {
|
||||
|
|
@ -122,6 +124,7 @@ Boards.attachSchema(
|
|||
}));
|
||||
}
|
||||
},
|
||||
*/
|
||||
},
|
||||
'labels.$._id': {
|
||||
/**
|
||||
|
|
@ -224,6 +227,56 @@ Boards.attachSchema(
|
|||
type: String,
|
||||
allowedValues: ['public', 'private'],
|
||||
},
|
||||
orgs: {
|
||||
/**
|
||||
* the list of organizations that a board belongs to
|
||||
*/
|
||||
type: [Object],
|
||||
optional: true,
|
||||
},
|
||||
'orgs.$.orgId':{
|
||||
/**
|
||||
* The uniq ID of the organization
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'orgs.$.orgDisplayName':{
|
||||
/**
|
||||
* The display name of the organization
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'orgs.$.isActive': {
|
||||
/**
|
||||
* Is the organization active?
|
||||
*/
|
||||
type: Boolean,
|
||||
},
|
||||
teams: {
|
||||
/**
|
||||
* the list of teams that a board belongs to
|
||||
*/
|
||||
type: [Object],
|
||||
optional: true,
|
||||
},
|
||||
'teams.$.teamId':{
|
||||
/**
|
||||
* The uniq ID of the team
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'teams.$.teamDisplayName':{
|
||||
/**
|
||||
* The display name of the team
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'teams.$.isActive': {
|
||||
/**
|
||||
* Is the team active?
|
||||
*/
|
||||
type: Boolean,
|
||||
},
|
||||
color: {
|
||||
/**
|
||||
* The color of the board.
|
||||
|
|
@ -370,6 +423,14 @@ Boards.attachSchema(
|
|||
defaultValue: true,
|
||||
},
|
||||
|
||||
allowsCardSortingByNumber: {
|
||||
/**
|
||||
* Does the board allows card sorting by number?
|
||||
*/
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
|
||||
allowsAssignedBy: {
|
||||
/**
|
||||
* Does the board allows requested by?
|
||||
|
|
@ -684,6 +745,23 @@ Boards.helpers({
|
|||
return _.where(this.members, { isActive: true });
|
||||
},
|
||||
|
||||
|
||||
activeOrgs() {
|
||||
return _.where(this.orgs, { isActive: true });
|
||||
},
|
||||
|
||||
// hasNotAnyOrg(){
|
||||
// return this.orgs === undefined || this.orgs.length <= 0;
|
||||
// },
|
||||
|
||||
activeTeams() {
|
||||
return _.where(this.teams, { isActive: true });
|
||||
},
|
||||
|
||||
// hasNotAnyTeam(){
|
||||
// return this.teams === undefined || this.teams.length <= 0;
|
||||
// },
|
||||
|
||||
activeAdmins() {
|
||||
return _.where(this.members, { isActive: true, isAdmin: true });
|
||||
},
|
||||
|
|
@ -1187,6 +1265,10 @@ Boards.mutations({
|
|||
return { $set: { allowsRequestedBy } };
|
||||
},
|
||||
|
||||
setAllowsCardSortingByNumber(allowsCardSortingByNumber) {
|
||||
return { $set: { allowsCardSortingByNumber } };
|
||||
},
|
||||
|
||||
setAllowsAttachments(allowsAttachments) {
|
||||
return { $set: { allowsAttachments } };
|
||||
},
|
||||
|
|
@ -1417,13 +1499,18 @@ if (Meteor.isServer) {
|
|||
myLabelNames() {
|
||||
let names = [];
|
||||
Boards.userBoards(Meteor.userId()).forEach(board => {
|
||||
names = names.concat(
|
||||
board.labels
|
||||
.filter(label => !!label.name)
|
||||
.map(label => {
|
||||
return label.name;
|
||||
}),
|
||||
);
|
||||
// Only return labels when they exist.
|
||||
if (board.labels !== undefined) {
|
||||
names = names.concat(
|
||||
board.labels
|
||||
.filter(label => !!label.name)
|
||||
.map(label => {
|
||||
return label.name;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
return _.uniq(names).sort();
|
||||
},
|
||||
|
|
@ -1449,6 +1536,24 @@ if (Meteor.isServer) {
|
|||
} else throw new Meteor.Error('error-board-notAMember');
|
||||
} else throw new Meteor.Error('error-board-doesNotExist');
|
||||
},
|
||||
setBoardOrgs(boardOrgsArray, currBoardId){
|
||||
check(boardOrgsArray, Array);
|
||||
check(currBoardId, String);
|
||||
Boards.update(currBoardId, {
|
||||
$set: {
|
||||
orgs: boardOrgsArray,
|
||||
},
|
||||
});
|
||||
},
|
||||
setBoardTeams(boardTeamsArray, currBoardId){
|
||||
check(boardTeamsArray, Array);
|
||||
check(currBoardId, String);
|
||||
Boards.update(currBoardId, {
|
||||
$set: {
|
||||
teams: boardTeamsArray,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1673,7 +1778,8 @@ if (Meteor.isServer) {
|
|||
*/
|
||||
JsonRoutes.add('GET', '/api/boards', function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: Boards.find(
|
||||
|
|
@ -1847,7 +1953,8 @@ if (Meteor.isServer) {
|
|||
* @return_type string
|
||||
*/
|
||||
JsonRoutes.add('PUT', '/api/boards/:boardId/labels', function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const id = req.params.boardId;
|
||||
try {
|
||||
if (req.body.hasOwnProperty('label')) {
|
||||
|
|
|
|||
|
|
@ -192,8 +192,8 @@ if (Meteor.isServer) {
|
|||
res,
|
||||
) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCardId = req.params.cardId;
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
|
|
@ -230,8 +230,8 @@ if (Meteor.isServer) {
|
|||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||
function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCommentId = req.params.commentId;
|
||||
const paramCardId = req.params.cardId;
|
||||
JsonRoutes.sendResult(res, {
|
||||
|
|
@ -266,8 +266,8 @@ if (Meteor.isServer) {
|
|||
'/api/boards/:boardId/cards/:cardId/comments',
|
||||
function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCardId = req.params.cardId;
|
||||
const id = CardComments.direct.insert({
|
||||
userId: req.body.authorId,
|
||||
|
|
@ -312,8 +312,8 @@ if (Meteor.isServer) {
|
|||
'/api/boards/:boardId/cards/:cardId/comments/:commentId',
|
||||
function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCommentId = req.params.commentId;
|
||||
const paramCardId = req.params.cardId;
|
||||
CardComments.remove({
|
||||
|
|
|
|||
738
models/cards.js
738
models/cards.js
|
|
@ -338,6 +338,138 @@ Cards.attachSchema(
|
|||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
poker: {
|
||||
/**
|
||||
* poker object, see below
|
||||
*/
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
'poker.question': {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
'poker.one': {
|
||||
/**
|
||||
* poker card one
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.two': {
|
||||
/**
|
||||
* poker card two
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.three': {
|
||||
/**
|
||||
* poker card three
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.five': {
|
||||
/**
|
||||
* poker card five
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.eight': {
|
||||
/**
|
||||
* poker card eight
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.thirteen': {
|
||||
/**
|
||||
* poker card thirteen
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.twenty': {
|
||||
/**
|
||||
* poker card twenty
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.forty': {
|
||||
/**
|
||||
* poker card forty
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.oneHundred': {
|
||||
/**
|
||||
* poker card oneHundred
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.unsure': {
|
||||
/**
|
||||
* poker card unsure
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
'poker.end': {
|
||||
type: Date,
|
||||
optional: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
'poker.allowNonBoardMembers': {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
'poker.estimation': {
|
||||
/**
|
||||
* poker estimation value
|
||||
*/
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
targetId_gantt: {
|
||||
/**
|
||||
* ID of card which is the child link in gantt view
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
linkType_gantt: {
|
||||
/**
|
||||
* ID of card which is the parent link in gantt view
|
||||
*/
|
||||
type: [Number],
|
||||
decimal: false,
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
linkId_gantt: {
|
||||
/**
|
||||
* ID of card which is the parent link in gantt view
|
||||
*/
|
||||
type: [String],
|
||||
optional: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -361,6 +493,27 @@ Cards.allow({
|
|||
});
|
||||
|
||||
Cards.helpers({
|
||||
// Gantt https://github.com/wekan/wekan/issues/2870#issuecomment-857171127
|
||||
setGanttTargetId(sourceId, targetId, linkType, linkId){
|
||||
return Cards.update({ _id: sourceId}, {
|
||||
$push: {
|
||||
targetId_gantt: targetId,
|
||||
linkType_gantt : linkType,
|
||||
linkId_gantt: linkId
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeGanttTargetId(sourceId, targetId, linkType, linkId){
|
||||
return Cards.update({ _id: sourceId}, {
|
||||
$pull: {
|
||||
targetId_gantt: targetId,
|
||||
linkType_gantt : linkType,
|
||||
linkId_gantt: linkId
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
mapCustomFieldsToBoard(boardId) {
|
||||
// Map custom fields to new board
|
||||
return this.customFields.map(cf => {
|
||||
|
|
@ -1279,6 +1432,191 @@ Cards.helpers({
|
|||
return null;
|
||||
},
|
||||
|
||||
getPokerQuestion() {
|
||||
if (this.isLinkedCard()) {
|
||||
const card = Cards.findOne({ _id: this.linkedId });
|
||||
if (card === undefined) {
|
||||
return null;
|
||||
} else if (card && card.poker) {
|
||||
return card.poker.question;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (this.isLinkedBoard()) {
|
||||
const board = Boards.findOne({ _id: this.linkedId });
|
||||
if (board === undefined) {
|
||||
return null;
|
||||
} else if (board && board.poker) {
|
||||
return board.poker.question;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (this.poker) {
|
||||
return this.poker.question;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getPokerEstimation() {
|
||||
if (this.poker) {
|
||||
return this.poker.estimation;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
getPokerEnd() {
|
||||
if (this.isLinkedCard()) {
|
||||
const card = Cards.findOne({ _id: this.linkedId });
|
||||
if (card === undefined) {
|
||||
return null;
|
||||
} else if (card && card.poker) {
|
||||
return card.poker.end;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (this.isLinkedBoard()) {
|
||||
const board = Boards.findOne({ _id: this.linkedId });
|
||||
if (board === undefined) {
|
||||
return null;
|
||||
} else if (board && board.poker) {
|
||||
return board.poker.end;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (this.poker) {
|
||||
return this.poker.end;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
expiredPoker() {
|
||||
let end = this.getPokerEnd();
|
||||
if (end) {
|
||||
end = moment(end);
|
||||
return end.isBefore(new Date());
|
||||
}
|
||||
return false;
|
||||
},
|
||||
pokerMemberOne() {
|
||||
if (this.poker && this.poker.one)
|
||||
return Users.find({ _id: { $in: this.poker.one } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberTwo() {
|
||||
if (this.poker && this.poker.two)
|
||||
return Users.find({ _id: { $in: this.poker.two } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberThree() {
|
||||
if (this.poker && this.poker.three)
|
||||
return Users.find({ _id: { $in: this.poker.three } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberFive() {
|
||||
if (this.poker && this.poker.five)
|
||||
return Users.find({ _id: { $in: this.poker.five } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberEight() {
|
||||
if (this.poker && this.poker.eight)
|
||||
return Users.find({ _id: { $in: this.poker.eight } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberThirteen() {
|
||||
if (this.poker && this.poker.thirteen)
|
||||
return Users.find({ _id: { $in: this.poker.thirteen } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberTwenty() {
|
||||
if (this.poker && this.poker.twenty)
|
||||
return Users.find({ _id: { $in: this.poker.twenty } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberForty() {
|
||||
if (this.poker && this.poker.forty)
|
||||
return Users.find({ _id: { $in: this.poker.forty } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberOneHundred() {
|
||||
if (this.poker && this.poker.oneHundred)
|
||||
return Users.find({ _id: { $in: this.poker.oneHundred } });
|
||||
return [];
|
||||
},
|
||||
pokerMemberUnsure() {
|
||||
if (this.poker && this.poker.unsure)
|
||||
return Users.find({ _id: { $in: this.poker.unsure } });
|
||||
return [];
|
||||
},
|
||||
pokerState() {
|
||||
const userId = Meteor.userId();
|
||||
let state;
|
||||
if (this.poker) {
|
||||
if (this.poker.one) {
|
||||
state = _.contains(this.poker.one, userId);
|
||||
if (state === true) {
|
||||
return 'one';
|
||||
}
|
||||
}
|
||||
if (this.poker.two) {
|
||||
state = _.contains(this.poker.two, userId);
|
||||
if (state === true) {
|
||||
return 'two';
|
||||
}
|
||||
}
|
||||
if (this.poker.three) {
|
||||
state = _.contains(this.poker.three, userId);
|
||||
if (state === true) {
|
||||
return 'three';
|
||||
}
|
||||
}
|
||||
if (this.poker.five) {
|
||||
state = _.contains(this.poker.five, userId);
|
||||
if (state === true) {
|
||||
return 'five';
|
||||
}
|
||||
}
|
||||
if (this.poker.eight) {
|
||||
state = _.contains(this.poker.eight, userId);
|
||||
if (state === true) {
|
||||
return 'eight';
|
||||
}
|
||||
}
|
||||
if (this.poker.thirteen) {
|
||||
state = _.contains(this.poker.thirteen, userId);
|
||||
if (state === true) {
|
||||
return 'thirteen';
|
||||
}
|
||||
}
|
||||
if (this.poker.twenty) {
|
||||
state = _.contains(this.poker.twenty, userId);
|
||||
if (state === true) {
|
||||
return 'twenty';
|
||||
}
|
||||
}
|
||||
if (this.poker.forty) {
|
||||
state = _.contains(this.poker.forty, userId);
|
||||
if (state === true) {
|
||||
return 'forty';
|
||||
}
|
||||
}
|
||||
if (this.poker.oneHundred) {
|
||||
state = _.contains(this.poker.oneHundred, userId);
|
||||
if (state === true) {
|
||||
return 'oneHundred';
|
||||
}
|
||||
}
|
||||
if (this.poker.unsure) {
|
||||
state = _.contains(this.poker.unsure, userId);
|
||||
if (state === true) {
|
||||
return 'unsure';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
getId() {
|
||||
if (this.isLinked()) {
|
||||
return this.linkedId;
|
||||
|
|
@ -1433,6 +1771,101 @@ Cards.helpers({
|
|||
voteCount() {
|
||||
return this.voteCountPositive() + this.voteCountNegative();
|
||||
},
|
||||
|
||||
pokerAllowNonBoardMembers() {
|
||||
if (this.poker) return this.poker.allowNonBoardMembers;
|
||||
return null;
|
||||
},
|
||||
pokerCountOne() {
|
||||
if (this.poker && this.poker.one) return this.poker.one.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountTwo() {
|
||||
if (this.poker && this.poker.two) return this.poker.two.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountThree() {
|
||||
if (this.poker && this.poker.three) return this.poker.three.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountFive() {
|
||||
if (this.poker && this.poker.five) return this.poker.five.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountEight() {
|
||||
if (this.poker && this.poker.eight) return this.poker.eight.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountThirteen() {
|
||||
if (this.poker && this.poker.thirteen) return this.poker.thirteen.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountTwenty() {
|
||||
if (this.poker && this.poker.twenty) return this.poker.twenty.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountForty() {
|
||||
if (this.poker && this.poker.forty) return this.poker.forty.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountOneHundred() {
|
||||
if (this.poker && this.poker.oneHundred) return this.poker.oneHundred.length;
|
||||
return null;
|
||||
},
|
||||
pokerCountUnsure() {
|
||||
if (this.poker && this.poker.unsure) return this.poker.unsure.length;
|
||||
return null;
|
||||
},
|
||||
pokerCount() {
|
||||
return (
|
||||
this.pokerCountOne() +
|
||||
this.pokerCountTwo() +
|
||||
this.pokerCountThree() +
|
||||
this.pokerCountFive() +
|
||||
this.pokerCountEight() +
|
||||
this.pokerCountThirteen() +
|
||||
this.pokerCountTwenty() +
|
||||
this.pokerCountForty() +
|
||||
this.pokerCountOneHundred() +
|
||||
this.pokerCountUnsure()
|
||||
);
|
||||
},
|
||||
pokerWinner() {
|
||||
const pokerListMaps = [];
|
||||
let pokerWinnersListMap = [];
|
||||
if (this.expiredPoker()) {
|
||||
const one = { count: this.pokerCountOne(), pokerCard: 1 };
|
||||
const two = { count: this.pokerCountTwo(), pokerCard: 2 };
|
||||
const three = { count: this.pokerCountThree(), pokerCard: 3 };
|
||||
const five = { count: this.pokerCountFive(), pokerCard: 5 };
|
||||
const eight = { count: this.pokerCountEight(), pokerCard: 8 };
|
||||
const thirteen = { count: this.pokerCountThirteen(), pokerCard: 13 };
|
||||
const twenty = { count: this.pokerCountTwenty(), pokerCard: 20 };
|
||||
const forty = { count: this.pokerCountForty(), pokerCard: 40 };
|
||||
const oneHundred = { count: this.pokerCountOneHundred(), pokerCard: 100 };
|
||||
const unsure = { count: this.pokerCountUnsure(), pokerCard: 'Unsure' };
|
||||
pokerListMaps.push(one);
|
||||
pokerListMaps.push(two);
|
||||
pokerListMaps.push(three);
|
||||
pokerListMaps.push(five);
|
||||
pokerListMaps.push(eight);
|
||||
pokerListMaps.push(thirteen);
|
||||
pokerListMaps.push(twenty);
|
||||
pokerListMaps.push(forty);
|
||||
pokerListMaps.push(oneHundred);
|
||||
pokerListMaps.push(unsure);
|
||||
|
||||
pokerListMaps.sort(function(a, b) {
|
||||
return b.count - a.count;
|
||||
});
|
||||
const max = pokerListMaps[0].count;
|
||||
pokerWinnersListMap = pokerListMaps.filter(task => task.count >= max);
|
||||
pokerWinnersListMap.sort(function(a, b) {
|
||||
return b.pokerCard - a.pokerCard;
|
||||
});
|
||||
}
|
||||
return pokerWinnersListMap[0].pokerCard;
|
||||
},
|
||||
});
|
||||
|
||||
Cards.mutations({
|
||||
|
|
@ -1870,6 +2303,279 @@ Cards.mutations({
|
|||
};
|
||||
}
|
||||
},
|
||||
|
||||
setPokerQuestion(question, allowNonBoardMembers) {
|
||||
return {
|
||||
$set: {
|
||||
poker: {
|
||||
question,
|
||||
allowNonBoardMembers,
|
||||
one: [],
|
||||
two: [],
|
||||
three: [],
|
||||
five: [],
|
||||
eight: [],
|
||||
thirteen: [],
|
||||
twenty: [],
|
||||
forty: [],
|
||||
oneHundred: [],
|
||||
unsure: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
setPokerEstimation(estimation) {
|
||||
return {
|
||||
$set: { 'poker.estimation': estimation },
|
||||
};
|
||||
},
|
||||
unsetPokerEstimation() {
|
||||
return {
|
||||
$unset: { 'poker.estimation': '' },
|
||||
};
|
||||
},
|
||||
unsetPoker() {
|
||||
return {
|
||||
$unset: {
|
||||
poker: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
setPokerEnd(end) {
|
||||
return {
|
||||
$set: { 'poker.end': end },
|
||||
};
|
||||
},
|
||||
unsetPokerEnd() {
|
||||
return {
|
||||
$unset: { 'poker.end': '' },
|
||||
};
|
||||
},
|
||||
setPoker(userId, state) {
|
||||
switch (state) {
|
||||
case 'one':
|
||||
// poker one
|
||||
return {
|
||||
$pull: {
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.one': userId,
|
||||
},
|
||||
};
|
||||
case 'two':
|
||||
// poker two
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.two': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'three':
|
||||
// poker three
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.three': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'five':
|
||||
// poker five
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.five': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'eight':
|
||||
// poker eight
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.eight': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'thirteen':
|
||||
// poker thirteen
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.thirteen': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'twenty':
|
||||
// poker twenty
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.twenty': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'forty':
|
||||
// poker forty
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.forty': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'oneHundred':
|
||||
// poker one hundred
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.oneHundred': userId,
|
||||
},
|
||||
};
|
||||
|
||||
case 'unsure':
|
||||
// poker unsure
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
},
|
||||
$addToSet: {
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
// Remove pokers
|
||||
return {
|
||||
$pull: {
|
||||
'poker.one': userId,
|
||||
'poker.two': userId,
|
||||
'poker.three': userId,
|
||||
'poker.five': userId,
|
||||
'poker.eight': userId,
|
||||
'poker.thirteen': userId,
|
||||
'poker.twenty': userId,
|
||||
'poker.forty': userId,
|
||||
'poker.oneHundred': userId,
|
||||
'poker.unsure': userId,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
replayPoker() {
|
||||
return {
|
||||
$set: {
|
||||
'poker.one': [],
|
||||
'poker.two': [],
|
||||
'poker.three': [],
|
||||
'poker.five': [],
|
||||
'poker.eight': [],
|
||||
'poker.thirteen': [],
|
||||
'poker.twelve': [],
|
||||
'poker.forty': [],
|
||||
'poker.oneHundred': [],
|
||||
'poker.unsure': [],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
//FUNCTIONS FOR creation of Activities
|
||||
|
|
@ -2593,14 +3299,17 @@ if (Meteor.isServer) {
|
|||
* @param {string} vote.question the vote question
|
||||
* @param {boolean} vote.public show who voted what
|
||||
* @param {boolean} vote.allowNonBoardMembers allow all logged in users to vote?
|
||||
* @param {Object} [poker] the poker object
|
||||
* @param {string} poker.question the vote question
|
||||
* @param {boolean} poker.allowNonBoardMembers allow all logged in users to vote?
|
||||
* @return_type {_id: string}
|
||||
*/
|
||||
JsonRoutes.add(
|
||||
'PUT',
|
||||
'/api/boards/:boardId/lists/:listId/cards/:cardId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCardId = req.params.cardId;
|
||||
const paramListId = req.params.listId;
|
||||
|
||||
|
|
@ -2698,6 +3407,31 @@ if (Meteor.isServer) {
|
|||
{ $set: { vote: newVote } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('poker')) {
|
||||
const newPoker = req.body.poker;
|
||||
newPoker.one = [];
|
||||
newPoker.two = [];
|
||||
newPoker.three = [];
|
||||
newPoker.five = [];
|
||||
newPoker.eight = [];
|
||||
newPoker.thirteen = [];
|
||||
newPoker.twenty = [];
|
||||
newPoker.forty = [];
|
||||
newPoker.oneHundred = [];
|
||||
newPoker.unsure = [];
|
||||
if (!newPoker.hasOwnProperty('allowNonBoardMembers'))
|
||||
newPoker.allowNonBoardMembers = false;
|
||||
|
||||
Cards.direct.update(
|
||||
{
|
||||
_id: paramCardId,
|
||||
listId: paramListId,
|
||||
boardId: paramBoardId,
|
||||
archived: false,
|
||||
},
|
||||
{ $set: { poker: newPoker } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('labelIds')) {
|
||||
let newlabelIds = req.body.labelIds;
|
||||
if (_.isString(newlabelIds)) {
|
||||
|
|
@ -2932,8 +3666,8 @@ if (Meteor.isServer) {
|
|||
'DELETE',
|
||||
'/api/boards/:boardId/lists/:listId/cards/:cardId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramListId = req.params.listId;
|
||||
const paramCardId = req.params.cardId;
|
||||
|
||||
|
|
|
|||
|
|
@ -265,7 +265,8 @@ if (Meteor.isServer) {
|
|||
'GET',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramItemId = req.params.itemId;
|
||||
const checklistItem = ChecklistItems.findOne({ _id: paramItemId });
|
||||
if (checklistItem) {
|
||||
|
|
@ -298,7 +299,8 @@ if (Meteor.isServer) {
|
|||
'PUT',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
||||
const paramItemId = req.params.itemId;
|
||||
|
||||
|
|
@ -349,7 +351,8 @@ if (Meteor.isServer) {
|
|||
'DELETE',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId/items/:itemId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramItemId = req.params.itemId;
|
||||
ChecklistItems.direct.remove({ _id: paramItemId });
|
||||
JsonRoutes.sendResult(res, {
|
||||
|
|
|
|||
|
|
@ -204,7 +204,8 @@ if (Meteor.isServer) {
|
|||
'GET',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCardId = req.params.cardId;
|
||||
const checklists = Checklists.find({ cardId: paramCardId }).map(function(
|
||||
doc,
|
||||
|
|
@ -247,7 +248,8 @@ if (Meteor.isServer) {
|
|||
'GET',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramChecklistId = req.params.checklistId;
|
||||
const paramCardId = req.params.cardId;
|
||||
const checklist = Checklists.findOne({
|
||||
|
|
@ -351,7 +353,8 @@ if (Meteor.isServer) {
|
|||
'DELETE',
|
||||
'/api/boards/:boardId/cards/:cardId/checklists/:checklistId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramChecklistId = req.params.checklistId;
|
||||
Checklists.remove({ _id: paramChecklistId });
|
||||
JsonRoutes.sendResult(res, {
|
||||
|
|
|
|||
|
|
@ -294,8 +294,8 @@ if (Meteor.isServer) {
|
|||
req,
|
||||
res,
|
||||
) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: CustomFields.find({ boardIds: { $in: [paramBoardId] } }).map(
|
||||
|
|
@ -323,8 +323,8 @@ if (Meteor.isServer) {
|
|||
'GET',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramCustomFieldId = req.params.customFieldId;
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
|
|
@ -353,8 +353,8 @@ if (Meteor.isServer) {
|
|||
req,
|
||||
res,
|
||||
) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const board = Boards.findOne({ _id: paramBoardId });
|
||||
const id = CustomFields.direct.insert({
|
||||
name: req.body.name,
|
||||
|
|
@ -396,7 +396,8 @@ if (Meteor.isServer) {
|
|||
'PUT',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
||||
const paramFieldId = req.params.customFieldId;
|
||||
|
||||
|
|
@ -461,7 +462,8 @@ if (Meteor.isServer) {
|
|||
'POST',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
||||
const paramCustomFieldId = req.params.customFieldId;
|
||||
const paramItems = req.body.items;
|
||||
|
|
@ -504,7 +506,8 @@ if (Meteor.isServer) {
|
|||
'PUT',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
||||
const paramDropdownItemId = req.params.dropdownItemId;
|
||||
const paramCustomFieldId = req.params.customFieldId;
|
||||
|
|
@ -545,7 +548,8 @@ if (Meteor.isServer) {
|
|||
'DELETE',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
|
||||
paramCustomFieldId = req.params.customFieldId;
|
||||
paramDropdownItemId = req.params.dropdownItemId;
|
||||
|
|
@ -580,8 +584,8 @@ if (Meteor.isServer) {
|
|||
'DELETE',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId',
|
||||
function(req, res) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const id = req.params.customFieldId;
|
||||
CustomFields.remove({ _id: id, boardIds: { $in: [paramBoardId] } });
|
||||
JsonRoutes.sendResult(res, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
if (Meteor.isServer) {
|
||||
import { runOnServer } from './runOnServer';
|
||||
|
||||
runOnServer(function() {
|
||||
// the ExporterExcel class is only available on server and in order to import
|
||||
// it here we use runOnServer to have it inside a function instead of an
|
||||
// if (Meteor.isServer) block
|
||||
import { ExporterExcel } from './server/ExporterExcel';
|
||||
|
||||
// todo XXX once we have a real API in place, move that route there
|
||||
// todo XXX also share the route definition between the client and the server
|
||||
// so that we could use something like
|
||||
|
|
@ -20,7 +27,6 @@ if (Meteor.isServer) {
|
|||
* @param {string} boardId the ID of the board we are exporting
|
||||
* @param {string} authToken the loginToken
|
||||
*/
|
||||
const Excel = require('exceljs');
|
||||
Picker.route('/api/boards/:boardId/exportExcel', function (params, req, res) {
|
||||
const boardId = params.boardId;
|
||||
let user = null;
|
||||
|
|
@ -43,6 +49,7 @@ if (Meteor.isServer) {
|
|||
isAdmin: true,
|
||||
});
|
||||
}
|
||||
|
||||
const exporterExcel = new ExporterExcel(boardId);
|
||||
if (exporterExcel.canExport(user) || impersonateDone) {
|
||||
if (impersonateDone) {
|
||||
|
|
@ -57,619 +64,4 @@ if (Meteor.isServer) {
|
|||
res.end(TAPi18n.__('user-can-not-export-excel'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
|
||||
export class ExporterExcel {
|
||||
constructor(boardId) {
|
||||
this._boardId = boardId;
|
||||
}
|
||||
|
||||
build(res) {
|
||||
const fs = Npm.require('fs');
|
||||
const os = Npm.require('os');
|
||||
const path = Npm.require('path');
|
||||
|
||||
const byBoard = {
|
||||
boardId: this._boardId,
|
||||
};
|
||||
const byBoardNoLinked = {
|
||||
boardId: this._boardId,
|
||||
linkedId: {
|
||||
$in: ['', null],
|
||||
},
|
||||
};
|
||||
// we do not want to retrieve boardId in related elements
|
||||
const noBoardId = {
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
};
|
||||
const result = {
|
||||
_format: 'wekan-board-1.0.0',
|
||||
};
|
||||
_.extend(
|
||||
result,
|
||||
Boards.findOne(this._boardId, {
|
||||
fields: {
|
||||
stars: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
result.lists = Lists.find(byBoard, noBoardId).fetch();
|
||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
||||
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
|
||||
result.customFields = CustomFields.find(
|
||||
{
|
||||
boardIds: {
|
||||
$in: [this.boardId],
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
},
|
||||
).fetch();
|
||||
result.comments = CardComments.find(byBoard, noBoardId).fetch();
|
||||
result.activities = Activities.find(byBoard, noBoardId).fetch();
|
||||
result.rules = Rules.find(byBoard, noBoardId).fetch();
|
||||
result.checklists = [];
|
||||
result.checklistItems = [];
|
||||
result.subtaskItems = [];
|
||||
result.triggers = [];
|
||||
result.actions = [];
|
||||
result.cards.forEach((card) => {
|
||||
result.checklists.push(
|
||||
...Checklists.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.checklistItems.push(
|
||||
...ChecklistItems.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.subtaskItems.push(
|
||||
...Cards.find({
|
||||
parentId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
});
|
||||
result.rules.forEach((rule) => {
|
||||
result.triggers.push(
|
||||
...Triggers.find(
|
||||
{
|
||||
_id: rule.triggerId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
result.actions.push(
|
||||
...Actions.find(
|
||||
{
|
||||
_id: rule.actionId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
});
|
||||
|
||||
// we also have to export some user data - as the other elements only
|
||||
// include id but we have to be careful:
|
||||
// 1- only exports users that are linked somehow to that board
|
||||
// 2- do not export any sensitive information
|
||||
const users = {};
|
||||
result.members.forEach((member) => {
|
||||
users[member.userId] = true;
|
||||
});
|
||||
result.lists.forEach((list) => {
|
||||
users[list.userId] = true;
|
||||
});
|
||||
result.cards.forEach((card) => {
|
||||
users[card.userId] = true;
|
||||
if (card.members) {
|
||||
card.members.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
if (card.assignees) {
|
||||
card.assignees.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
result.comments.forEach((comment) => {
|
||||
users[comment.userId] = true;
|
||||
});
|
||||
result.activities.forEach((activity) => {
|
||||
users[activity.userId] = true;
|
||||
});
|
||||
result.checklists.forEach((checklist) => {
|
||||
users[checklist.userId] = true;
|
||||
});
|
||||
const byUserIds = {
|
||||
_id: {
|
||||
$in: Object.getOwnPropertyNames(users),
|
||||
},
|
||||
};
|
||||
// we use whitelist to be sure we do not expose inadvertently
|
||||
// some secret fields that gets added to User later.
|
||||
const userFields = {
|
||||
fields: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
'profile.initials': 1,
|
||||
'profile.avatarUrl': 1,
|
||||
},
|
||||
};
|
||||
result.users = Users.find(byUserIds, userFields)
|
||||
.fetch()
|
||||
.map((user) => {
|
||||
// user avatar is stored as a relative url, we export absolute
|
||||
if ((user.profile || {}).avatarUrl) {
|
||||
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
|
||||
//init exceljs workbook
|
||||
const Excel = require('exceljs');
|
||||
const workbook = new Excel.Workbook();
|
||||
workbook.creator = TAPi18n.__('export-board');
|
||||
workbook.lastModifiedBy = TAPi18n.__('export-board');
|
||||
workbook.created = new Date();
|
||||
workbook.modified = new Date();
|
||||
workbook.lastPrinted = new Date();
|
||||
const filename = `${result.title}.xlsx`;
|
||||
//init worksheet
|
||||
const worksheet = workbook.addWorksheet(result.title, {
|
||||
properties: {
|
||||
tabColor: {
|
||||
argb: 'FFC0000',
|
||||
},
|
||||
},
|
||||
pageSetup: {
|
||||
paperSize: 9,
|
||||
orientation: 'landscape',
|
||||
},
|
||||
});
|
||||
//get worksheet
|
||||
const ws = workbook.getWorksheet(result.title);
|
||||
ws.properties.defaultRowHeight = 20;
|
||||
//init columns
|
||||
//Excel font. Western: Arial. zh-CN: 宋体
|
||||
ws.columns = [
|
||||
{
|
||||
key: 'a',
|
||||
width: 14,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
key: 'd',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'e',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'f',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'g',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'h',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'i',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'j',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'k',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'l',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'm',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'n',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'o',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'q',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'r',
|
||||
width: 20,
|
||||
},
|
||||
];
|
||||
|
||||
//add title line
|
||||
ws.mergeCells('A1:H1');
|
||||
ws.getCell('A1').value = result.title;
|
||||
ws.getCell('A1').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '20',
|
||||
},
|
||||
};
|
||||
ws.getCell('A1').alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
};
|
||||
ws.getRow(1).height = 40;
|
||||
//get member and assignee info
|
||||
let jmem = '';
|
||||
let jassig = '';
|
||||
const jmeml = {};
|
||||
const jassigl = {};
|
||||
for (const i in result.users) {
|
||||
jmem = `${jmem + result.users[i].username},`;
|
||||
jmeml[result.users[i]._id] = result.users[i].username;
|
||||
}
|
||||
jmem = jmem.substr(0, jmem.length - 1);
|
||||
for (const ia in result.users) {
|
||||
jassig = `${jassig + result.users[ia].username},`;
|
||||
jassigl[result.users[ia]._id] = result.users[ia].username;
|
||||
}
|
||||
jassig = jassig.substr(0, jassig.length - 1);
|
||||
//get kanban list info
|
||||
const jlist = {};
|
||||
for (const klist in result.lists) {
|
||||
jlist[result.lists[klist]._id] = result.lists[klist].title;
|
||||
}
|
||||
//get kanban swimlanes info
|
||||
const jswimlane = {};
|
||||
for (const kswimlane in result.swimlanes) {
|
||||
jswimlane[result.swimlanes[kswimlane]._id] =
|
||||
result.swimlanes[kswimlane].title;
|
||||
}
|
||||
//get kanban label info
|
||||
const jlabel = {};
|
||||
var isFirst = 1;
|
||||
for (const klabel in result.labels) {
|
||||
// console.log(klabel);
|
||||
if (isFirst == 0) {
|
||||
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
|
||||
} else {
|
||||
isFirst = 0;
|
||||
jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
|
||||
}
|
||||
}
|
||||
//add data +8 hours
|
||||
function addTZhours(jdate) {
|
||||
const curdate = new Date(jdate);
|
||||
const checkCorrectDate = moment(curdate);
|
||||
if (checkCorrectDate.isValid()) {
|
||||
return curdate;
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
////Do not add 8 hours to GMT. Use GMT instead.
|
||||
////Could not yet figure out how to get localtime.
|
||||
//return new Date(curdate.setHours(curdate.getHours() + 8));
|
||||
//return curdate;
|
||||
}
|
||||
//add blank row
|
||||
ws.addRow().values = ['', '', '', '', '', ''];
|
||||
//add kanban info
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('createdAt'),
|
||||
addTZhours(result.createdAt),
|
||||
TAPi18n.__('modifiedAt'),
|
||||
addTZhours(result.modifiedAt),
|
||||
TAPi18n.__('members'),
|
||||
jmem,
|
||||
];
|
||||
ws.getRow(3).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 10,
|
||||
bold: true,
|
||||
};
|
||||
ws.mergeCells('F3:R3');
|
||||
ws.getCell('B3').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
bold: true,
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
};
|
||||
//cell center
|
||||
function cellCenter(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
function cellLeft(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'left',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
cellCenter('A3');
|
||||
cellCenter('B3');
|
||||
cellCenter('C3');
|
||||
cellCenter('D3');
|
||||
cellCenter('E3');
|
||||
cellLeft('F3');
|
||||
ws.getRow(3).height = 20;
|
||||
//all border
|
||||
function allBorder(cellno) {
|
||||
ws.getCell(cellno).border = {
|
||||
top: {
|
||||
style: 'thin',
|
||||
},
|
||||
left: {
|
||||
style: 'thin',
|
||||
},
|
||||
bottom: {
|
||||
style: 'thin',
|
||||
},
|
||||
right: {
|
||||
style: 'thin',
|
||||
},
|
||||
};
|
||||
}
|
||||
allBorder('A3');
|
||||
allBorder('B3');
|
||||
allBorder('C3');
|
||||
allBorder('D3');
|
||||
allBorder('E3');
|
||||
allBorder('F3');
|
||||
//add blank row
|
||||
ws.addRow().values = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
//add card title
|
||||
//ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
|
||||
//this is where order in which the excel file generates
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('number'),
|
||||
TAPi18n.__('title'),
|
||||
TAPi18n.__('description'),
|
||||
TAPi18n.__('parent-card'),
|
||||
TAPi18n.__('owner'),
|
||||
TAPi18n.__('createdAt'),
|
||||
TAPi18n.__('last-modified-at'),
|
||||
TAPi18n.__('card-received'),
|
||||
TAPi18n.__('card-start'),
|
||||
TAPi18n.__('card-due'),
|
||||
TAPi18n.__('card-end'),
|
||||
TAPi18n.__('list'),
|
||||
TAPi18n.__('swimlane'),
|
||||
TAPi18n.__('assignee'),
|
||||
TAPi18n.__('members'),
|
||||
TAPi18n.__('labels'),
|
||||
TAPi18n.__('overtime-hours'),
|
||||
TAPi18n.__('spent-time-hours'),
|
||||
];
|
||||
ws.getRow(5).height = 20;
|
||||
allBorder('A5');
|
||||
allBorder('B5');
|
||||
allBorder('C5');
|
||||
allBorder('D5');
|
||||
allBorder('E5');
|
||||
allBorder('F5');
|
||||
allBorder('G5');
|
||||
allBorder('H5');
|
||||
allBorder('I5');
|
||||
allBorder('J5');
|
||||
allBorder('K5');
|
||||
allBorder('L5');
|
||||
allBorder('M5');
|
||||
allBorder('N5');
|
||||
allBorder('O5');
|
||||
allBorder('P5');
|
||||
allBorder('Q5');
|
||||
allBorder('R5');
|
||||
cellCenter('A5');
|
||||
cellCenter('B5');
|
||||
cellCenter('C5');
|
||||
cellCenter('D5');
|
||||
cellCenter('E5');
|
||||
cellCenter('F5');
|
||||
cellCenter('G5');
|
||||
cellCenter('H5');
|
||||
cellCenter('I5');
|
||||
cellCenter('J5');
|
||||
cellCenter('K5');
|
||||
cellCenter('L5');
|
||||
cellCenter('M5');
|
||||
cellCenter('N5');
|
||||
cellCenter('O5');
|
||||
cellCenter('P5');
|
||||
cellCenter('Q5');
|
||||
cellCenter('R5');
|
||||
ws.getRow(5).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 12,
|
||||
bold: true,
|
||||
};
|
||||
//add blank row
|
||||
//add card info
|
||||
for (const i in result.cards) {
|
||||
const jcard = result.cards[i];
|
||||
//get member info
|
||||
let jcmem = '';
|
||||
for (const j in jcard.members) {
|
||||
jcmem += jmeml[jcard.members[j]];
|
||||
jcmem += ' ';
|
||||
}
|
||||
//get assignee info
|
||||
let jcassig = '';
|
||||
for (const ja in jcard.assignees) {
|
||||
jcassig += jassigl[jcard.assignees[ja]];
|
||||
jcassig += ' ';
|
||||
}
|
||||
//get card label info
|
||||
let jclabel = '';
|
||||
for (const jl in jcard.labelIds) {
|
||||
jclabel += jlabel[jcard.labelIds[jl]];
|
||||
jclabel += ' ';
|
||||
}
|
||||
//get parent name
|
||||
if (jcard.parentId) {
|
||||
const parentCard = result.cards.find(
|
||||
(card) => card._id === jcard.parentId,
|
||||
);
|
||||
jcard.parentCardTitle = parentCard ? parentCard.title : '';
|
||||
}
|
||||
|
||||
//add card detail
|
||||
const t = Number(i) + 1;
|
||||
ws.addRow().values = [
|
||||
t.toString(),
|
||||
jcard.title,
|
||||
jcard.description,
|
||||
jcard.parentCardTitle,
|
||||
jmeml[jcard.userId],
|
||||
addTZhours(jcard.createdAt),
|
||||
addTZhours(jcard.dateLastActivity),
|
||||
addTZhours(jcard.receivedAt),
|
||||
addTZhours(jcard.startAt),
|
||||
addTZhours(jcard.dueAt),
|
||||
addTZhours(jcard.endAt),
|
||||
jlist[jcard.listId],
|
||||
jswimlane[jcard.swimlaneId],
|
||||
jcassig,
|
||||
jcmem,
|
||||
jclabel,
|
||||
jcard.isOvertime ? 'true' : 'false',
|
||||
jcard.spentTime,
|
||||
];
|
||||
const y = Number(i) + 6;
|
||||
//ws.getRow(y).height = 25;
|
||||
allBorder(`A${y}`);
|
||||
allBorder(`B${y}`);
|
||||
allBorder(`C${y}`);
|
||||
allBorder(`D${y}`);
|
||||
allBorder(`E${y}`);
|
||||
allBorder(`F${y}`);
|
||||
allBorder(`G${y}`);
|
||||
allBorder(`H${y}`);
|
||||
allBorder(`I${y}`);
|
||||
allBorder(`J${y}`);
|
||||
allBorder(`K${y}`);
|
||||
allBorder(`L${y}`);
|
||||
allBorder(`M${y}`);
|
||||
allBorder(`N${y}`);
|
||||
allBorder(`O${y}`);
|
||||
allBorder(`P${y}`);
|
||||
allBorder(`Q${y}`);
|
||||
allBorder(`R${y}`);
|
||||
cellCenter(`A${y}`);
|
||||
ws.getCell(`B${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`C${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`M${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`N${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`O${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
workbook.xlsx.write(res).then(function () {});
|
||||
}
|
||||
|
||||
canExport(user) {
|
||||
const board = Boards.findOne(this._boardId);
|
||||
return board && board.isVisibleBy(user);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
70
models/exportPDF.js
Normal file
70
models/exportPDF.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { runOnServer } from './runOnServer';
|
||||
|
||||
runOnServer(function() {
|
||||
// the ExporterCardPDF class is only available on server and in order to import
|
||||
// it here we use runOnServer to have it inside a function instead of an
|
||||
// if (Meteor.isServer) block
|
||||
import { ExporterCardPDF } from './server/ExporterCardPDF';
|
||||
|
||||
// todo XXX once we have a real API in place, move that route there
|
||||
// todo XXX also share the route definition between the client and the server
|
||||
// so that we could use something like
|
||||
// `ApiRoutes.path('boards/exportExcel', boardId)``
|
||||
// on the client instead of copy/pasting the route path manually between the
|
||||
// client and the server.
|
||||
/**
|
||||
* @operation exportExcel
|
||||
* @tag Boards
|
||||
*
|
||||
* @summary This route is used to export the board Excel.
|
||||
*
|
||||
* @description If user is already logged-in, pass loginToken as param
|
||||
* "authToken": '/api/boards/:boardId/exportExcel?authToken=:token'
|
||||
*
|
||||
* See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
|
||||
* for detailed explanations
|
||||
*
|
||||
* @param {string} boardId the ID of the board we are exporting
|
||||
* @param {string} authToken the loginToken
|
||||
*/
|
||||
Picker.route('/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF', function (params, req, res) {
|
||||
const boardId = params.boardId;
|
||||
const paramListId = req.params.listId;
|
||||
const paramCardId = req.params.cardId;
|
||||
let user = null;
|
||||
let impersonateDone = false;
|
||||
let adminId = null;
|
||||
const loginToken = params.query.authToken;
|
||||
if (loginToken) {
|
||||
const hashToken = Accounts._hashLoginToken(loginToken);
|
||||
user = Meteor.users.findOne({
|
||||
'services.resume.loginTokens.hashedToken': hashToken,
|
||||
});
|
||||
adminId = user._id.toString();
|
||||
impersonateDone = ImpersonatedUsers.findOne({
|
||||
adminId: adminId,
|
||||
});
|
||||
} else if (!Meteor.settings.public.sandstorm) {
|
||||
Authentication.checkUserId(req.userId);
|
||||
user = Users.findOne({
|
||||
_id: req.userId,
|
||||
isAdmin: true,
|
||||
});
|
||||
}
|
||||
|
||||
const exporterCardPDF = new ExporterCardPDF(boardId);
|
||||
if (exporterCardPDF.canExport(user) || impersonateDone) {
|
||||
if (impersonateDone) {
|
||||
ImpersonatedUsers.insert({
|
||||
adminId: adminId,
|
||||
boardId: boardId,
|
||||
reason: 'exportCardPDF',
|
||||
});
|
||||
}
|
||||
|
||||
exporterCardPDF.build(res);
|
||||
} else {
|
||||
res.end(TAPi18n.__('user-can-not-export-card-to-pdf'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -531,8 +531,8 @@ if (Meteor.isServer) {
|
|||
*/
|
||||
JsonRoutes.add('POST', '/api/boards/:boardId/lists', function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const board = Boards.findOne(paramBoardId);
|
||||
const id = Lists.insert({
|
||||
title: req.body.title,
|
||||
|
|
@ -569,8 +569,8 @@ if (Meteor.isServer) {
|
|||
res,
|
||||
) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const paramListId = req.params.listId;
|
||||
Lists.remove({ _id: paramListId, boardId: paramBoardId });
|
||||
JsonRoutes.sendResult(res, {
|
||||
|
|
|
|||
|
|
@ -36,11 +36,19 @@ Org.attachSchema(
|
|||
optional: true,
|
||||
max: 255,
|
||||
},
|
||||
orgIsActive: {
|
||||
/**
|
||||
* status of the organization
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
createdAt: {
|
||||
/**
|
||||
* creation date of the organization
|
||||
*/
|
||||
type: Date,
|
||||
denyUpdate: false,
|
||||
// eslint-disable-next-line consistent-return
|
||||
autoValue() {
|
||||
if (this.isInsert) {
|
||||
|
|
@ -68,6 +76,44 @@ Org.attachSchema(
|
|||
);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Org.allow({
|
||||
insert(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
update(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
remove(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
fetch: [],
|
||||
});
|
||||
|
||||
|
||||
Meteor.methods({
|
||||
setCreateOrg(
|
||||
orgDisplayName,
|
||||
|
|
@ -81,7 +127,7 @@ if (Meteor.isServer) {
|
|||
check(orgDesc, String);
|
||||
check(orgShortName, String);
|
||||
check(orgWebsite, String);
|
||||
check(orgIsActive, String);
|
||||
check(orgIsActive, Boolean);
|
||||
|
||||
const nOrgNames = Org.find({ orgShortName }).count();
|
||||
if (nOrgNames > 0) {
|
||||
|
|
@ -100,17 +146,17 @@ if (Meteor.isServer) {
|
|||
|
||||
setOrgDisplayName(org, orgDisplayName) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(org, String);
|
||||
check(org, Object);
|
||||
check(orgDisplayName, String);
|
||||
Org.update(org, {
|
||||
$set: { orgDisplayName: orgDisplayName },
|
||||
$set: { orgDisplayName: orgDisplayNameorgShortName },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setOrgDesc(org, orgDesc) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(org, String);
|
||||
check(org, Object);
|
||||
check(orgDesc, String);
|
||||
Org.update(org, {
|
||||
$set: { orgDesc: orgDesc },
|
||||
|
|
@ -120,7 +166,7 @@ if (Meteor.isServer) {
|
|||
|
||||
setOrgShortName(org, orgShortName) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(org, String);
|
||||
check(org, Object);
|
||||
check(orgShortName, String);
|
||||
Org.update(org, {
|
||||
$set: { orgShortName: orgShortName },
|
||||
|
|
@ -130,20 +176,48 @@ if (Meteor.isServer) {
|
|||
|
||||
setOrgIsActive(org, orgIsActive) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(org, String);
|
||||
check(orgIsActive, String);
|
||||
check(org, Object);
|
||||
check(orgIsActive, Boolean);
|
||||
Org.update(org, {
|
||||
$set: { orgIsActive: orgIsActive },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setOrgAllFields(
|
||||
org,
|
||||
orgDisplayName,
|
||||
orgDesc,
|
||||
orgShortName,
|
||||
orgWebsite,
|
||||
orgIsActive,
|
||||
) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(org, Object);
|
||||
check(orgDisplayName, String);
|
||||
check(orgDesc, String);
|
||||
check(orgShortName, String);
|
||||
check(orgWebsite, String);
|
||||
check(orgIsActive, Boolean);
|
||||
Org.update(org, {
|
||||
$set: {
|
||||
orgDisplayName: orgDisplayName,
|
||||
orgDesc: orgDesc,
|
||||
orgShortName: orgShortName,
|
||||
orgWebsite: orgWebsite,
|
||||
orgIsActive: orgIsActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// Index for Organization name.
|
||||
Meteor.startup(() => {
|
||||
Org._collection._ensureIndex({ name: -1 });
|
||||
// Org._collection._ensureIndex({ name: -1 });
|
||||
Org._collection._ensureIndex({ orgDisplayName: -1 });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
8
models/runOnServer.js
Normal file
8
models/runOnServer.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Executes a function only if we are on the server. Use in combination
|
||||
* with package-sepcific loader functions to create a "nested" import that
|
||||
* prevents leakage of server-dependencies to the client.
|
||||
* @param fct {function} the function to be executed on the server
|
||||
* @return {*} a return value from the function, if there is any
|
||||
*/
|
||||
export const runOnServer = fct => Meteor.isServer && fct();
|
||||
629
models/server/ExporterCardPDF.js
Normal file
629
models/server/ExporterCardPDF.js
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
import { createWorkbook } from './createWorkbook';
|
||||
|
||||
class ExporterCardPDF {
|
||||
constructor(boardId) {
|
||||
this._boardId = boardId;
|
||||
}
|
||||
|
||||
build(res) {
|
||||
|
||||
/*
|
||||
const fs = Npm.require('fs');
|
||||
const os = Npm.require('os');
|
||||
const path = Npm.require('path');
|
||||
|
||||
const byBoard = {
|
||||
boardId: this._boardId,
|
||||
};
|
||||
const byBoardNoLinked = {
|
||||
boardId: this._boardId,
|
||||
linkedId: {
|
||||
$in: ['', null],
|
||||
},
|
||||
};
|
||||
// we do not want to retrieve boardId in related elements
|
||||
const noBoardId = {
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
};
|
||||
const result = {
|
||||
_format: 'wekan-board-1.0.0',
|
||||
};
|
||||
_.extend(
|
||||
result,
|
||||
Boards.findOne(this._boardId, {
|
||||
fields: {
|
||||
stars: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
result.lists = Lists.find(byBoard, noBoardId).fetch();
|
||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
||||
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
|
||||
result.customFields = CustomFields.find(
|
||||
{
|
||||
boardIds: {
|
||||
$in: [this.boardId],
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
},
|
||||
).fetch();
|
||||
result.comments = CardComments.find(byBoard, noBoardId).fetch();
|
||||
result.activities = Activities.find(byBoard, noBoardId).fetch();
|
||||
result.rules = Rules.find(byBoard, noBoardId).fetch();
|
||||
result.checklists = [];
|
||||
result.checklistItems = [];
|
||||
result.subtaskItems = [];
|
||||
result.triggers = [];
|
||||
result.actions = [];
|
||||
result.cards.forEach((card) => {
|
||||
result.checklists.push(
|
||||
...Checklists.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.checklistItems.push(
|
||||
...ChecklistItems.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.subtaskItems.push(
|
||||
...Cards.find({
|
||||
parentId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
});
|
||||
result.rules.forEach((rule) => {
|
||||
result.triggers.push(
|
||||
...Triggers.find(
|
||||
{
|
||||
_id: rule.triggerId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
result.actions.push(
|
||||
...Actions.find(
|
||||
{
|
||||
_id: rule.actionId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
});
|
||||
|
||||
// we also have to export some user data - as the other elements only
|
||||
// include id but we have to be careful:
|
||||
// 1- only exports users that are linked somehow to that board
|
||||
// 2- do not export any sensitive information
|
||||
const users = {};
|
||||
result.members.forEach((member) => {
|
||||
users[member.userId] = true;
|
||||
});
|
||||
result.lists.forEach((list) => {
|
||||
users[list.userId] = true;
|
||||
});
|
||||
result.cards.forEach((card) => {
|
||||
users[card.userId] = true;
|
||||
if (card.members) {
|
||||
card.members.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
if (card.assignees) {
|
||||
card.assignees.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
result.comments.forEach((comment) => {
|
||||
users[comment.userId] = true;
|
||||
});
|
||||
result.activities.forEach((activity) => {
|
||||
users[activity.userId] = true;
|
||||
});
|
||||
result.checklists.forEach((checklist) => {
|
||||
users[checklist.userId] = true;
|
||||
});
|
||||
const byUserIds = {
|
||||
_id: {
|
||||
$in: Object.getOwnPropertyNames(users),
|
||||
},
|
||||
};
|
||||
// we use whitelist to be sure we do not expose inadvertently
|
||||
// some secret fields that gets added to User later.
|
||||
const userFields = {
|
||||
fields: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
'profile.initials': 1,
|
||||
'profile.avatarUrl': 1,
|
||||
},
|
||||
};
|
||||
result.users = Users.find(byUserIds, userFields)
|
||||
.fetch()
|
||||
.map((user) => {
|
||||
// user avatar is stored as a relative url, we export absolute
|
||||
if ((user.profile || {}).avatarUrl) {
|
||||
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
|
||||
//init exceljs workbook
|
||||
const workbook = createWorkbook();
|
||||
workbook.creator = TAPi18n.__('export-board');
|
||||
workbook.lastModifiedBy = TAPi18n.__('export-board');
|
||||
workbook.created = new Date();
|
||||
workbook.modified = new Date();
|
||||
workbook.lastPrinted = new Date();
|
||||
const filename = `${result.title}.xlsx`;
|
||||
//init worksheet
|
||||
const worksheet = workbook.addWorksheet(result.title, {
|
||||
properties: {
|
||||
tabColor: {
|
||||
argb: 'FFC0000',
|
||||
},
|
||||
},
|
||||
pageSetup: {
|
||||
paperSize: 9,
|
||||
orientation: 'landscape',
|
||||
},
|
||||
});
|
||||
//get worksheet
|
||||
const ws = workbook.getWorksheet(result.title);
|
||||
ws.properties.defaultRowHeight = 20;
|
||||
//init columns
|
||||
//Excel font. Western: Arial. zh-CN: 宋体
|
||||
ws.columns = [
|
||||
{
|
||||
key: 'a',
|
||||
width: 14,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
key: 'd',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'e',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'f',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'g',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'h',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'i',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'j',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'k',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'l',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'm',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'n',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'o',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'q',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'r',
|
||||
width: 20,
|
||||
},
|
||||
];
|
||||
|
||||
//add title line
|
||||
ws.mergeCells('A1:H1');
|
||||
ws.getCell('A1').value = result.title;
|
||||
ws.getCell('A1').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '20',
|
||||
},
|
||||
};
|
||||
ws.getCell('A1').alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
};
|
||||
ws.getRow(1).height = 40;
|
||||
//get member and assignee info
|
||||
let jmem = '';
|
||||
let jassig = '';
|
||||
const jmeml = {};
|
||||
const jassigl = {};
|
||||
for (const i in result.users) {
|
||||
jmem = `${jmem + result.users[i].username},`;
|
||||
jmeml[result.users[i]._id] = result.users[i].username;
|
||||
}
|
||||
jmem = jmem.substr(0, jmem.length - 1);
|
||||
for (const ia in result.users) {
|
||||
jassig = `${jassig + result.users[ia].username},`;
|
||||
jassigl[result.users[ia]._id] = result.users[ia].username;
|
||||
}
|
||||
jassig = jassig.substr(0, jassig.length - 1);
|
||||
//get kanban list info
|
||||
const jlist = {};
|
||||
for (const klist in result.lists) {
|
||||
jlist[result.lists[klist]._id] = result.lists[klist].title;
|
||||
}
|
||||
//get kanban swimlanes info
|
||||
const jswimlane = {};
|
||||
for (const kswimlane in result.swimlanes) {
|
||||
jswimlane[result.swimlanes[kswimlane]._id] =
|
||||
result.swimlanes[kswimlane].title;
|
||||
}
|
||||
//get kanban label info
|
||||
const jlabel = {};
|
||||
var isFirst = 1;
|
||||
for (const klabel in result.labels) {
|
||||
// console.log(klabel);
|
||||
if (isFirst == 0) {
|
||||
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
|
||||
} else {
|
||||
isFirst = 0;
|
||||
jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
|
||||
}
|
||||
}
|
||||
//add data +8 hours
|
||||
function addTZhours(jdate) {
|
||||
const curdate = new Date(jdate);
|
||||
const checkCorrectDate = moment(curdate);
|
||||
if (checkCorrectDate.isValid()) {
|
||||
return curdate;
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
////Do not add 8 hours to GMT. Use GMT instead.
|
||||
////Could not yet figure out how to get localtime.
|
||||
//return new Date(curdate.setHours(curdate.getHours() + 8));
|
||||
//return curdate;
|
||||
}
|
||||
//add blank row
|
||||
ws.addRow().values = ['', '', '', '', '', ''];
|
||||
//add kanban info
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('createdAt'),
|
||||
addTZhours(result.createdAt),
|
||||
TAPi18n.__('modifiedAt'),
|
||||
addTZhours(result.modifiedAt),
|
||||
TAPi18n.__('members'),
|
||||
jmem,
|
||||
];
|
||||
ws.getRow(3).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 10,
|
||||
bold: true,
|
||||
};
|
||||
ws.mergeCells('F3:R3');
|
||||
ws.getCell('B3').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
bold: true,
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
};
|
||||
//cell center
|
||||
function cellCenter(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
function cellLeft(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'left',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
cellCenter('A3');
|
||||
cellCenter('B3');
|
||||
cellCenter('C3');
|
||||
cellCenter('D3');
|
||||
cellCenter('E3');
|
||||
cellLeft('F3');
|
||||
ws.getRow(3).height = 20;
|
||||
//all border
|
||||
function allBorder(cellno) {
|
||||
ws.getCell(cellno).border = {
|
||||
top: {
|
||||
style: 'thin',
|
||||
},
|
||||
left: {
|
||||
style: 'thin',
|
||||
},
|
||||
bottom: {
|
||||
style: 'thin',
|
||||
},
|
||||
right: {
|
||||
style: 'thin',
|
||||
},
|
||||
};
|
||||
}
|
||||
allBorder('A3');
|
||||
allBorder('B3');
|
||||
allBorder('C3');
|
||||
allBorder('D3');
|
||||
allBorder('E3');
|
||||
allBorder('F3');
|
||||
//add blank row
|
||||
ws.addRow().values = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
//add card title
|
||||
//ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
|
||||
//this is where order in which the excel file generates
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('number'),
|
||||
TAPi18n.__('title'),
|
||||
TAPi18n.__('description'),
|
||||
TAPi18n.__('parent-card'),
|
||||
TAPi18n.__('owner'),
|
||||
TAPi18n.__('createdAt'),
|
||||
TAPi18n.__('last-modified-at'),
|
||||
TAPi18n.__('card-received'),
|
||||
TAPi18n.__('card-start'),
|
||||
TAPi18n.__('card-due'),
|
||||
TAPi18n.__('card-end'),
|
||||
TAPi18n.__('list'),
|
||||
TAPi18n.__('swimlane'),
|
||||
TAPi18n.__('assignee'),
|
||||
TAPi18n.__('members'),
|
||||
TAPi18n.__('labels'),
|
||||
TAPi18n.__('overtime-hours'),
|
||||
TAPi18n.__('spent-time-hours'),
|
||||
];
|
||||
ws.getRow(5).height = 20;
|
||||
allBorder('A5');
|
||||
allBorder('B5');
|
||||
allBorder('C5');
|
||||
allBorder('D5');
|
||||
allBorder('E5');
|
||||
allBorder('F5');
|
||||
allBorder('G5');
|
||||
allBorder('H5');
|
||||
allBorder('I5');
|
||||
allBorder('J5');
|
||||
allBorder('K5');
|
||||
allBorder('L5');
|
||||
allBorder('M5');
|
||||
allBorder('N5');
|
||||
allBorder('O5');
|
||||
allBorder('P5');
|
||||
allBorder('Q5');
|
||||
allBorder('R5');
|
||||
cellCenter('A5');
|
||||
cellCenter('B5');
|
||||
cellCenter('C5');
|
||||
cellCenter('D5');
|
||||
cellCenter('E5');
|
||||
cellCenter('F5');
|
||||
cellCenter('G5');
|
||||
cellCenter('H5');
|
||||
cellCenter('I5');
|
||||
cellCenter('J5');
|
||||
cellCenter('K5');
|
||||
cellCenter('L5');
|
||||
cellCenter('M5');
|
||||
cellCenter('N5');
|
||||
cellCenter('O5');
|
||||
cellCenter('P5');
|
||||
cellCenter('Q5');
|
||||
cellCenter('R5');
|
||||
ws.getRow(5).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 12,
|
||||
bold: true,
|
||||
};
|
||||
//add blank row
|
||||
//add card info
|
||||
for (const i in result.cards) {
|
||||
const jcard = result.cards[i];
|
||||
//get member info
|
||||
let jcmem = '';
|
||||
for (const j in jcard.members) {
|
||||
jcmem += jmeml[jcard.members[j]];
|
||||
jcmem += ' ';
|
||||
}
|
||||
//get assignee info
|
||||
let jcassig = '';
|
||||
for (const ja in jcard.assignees) {
|
||||
jcassig += jassigl[jcard.assignees[ja]];
|
||||
jcassig += ' ';
|
||||
}
|
||||
//get card label info
|
||||
let jclabel = '';
|
||||
for (const jl in jcard.labelIds) {
|
||||
jclabel += jlabel[jcard.labelIds[jl]];
|
||||
jclabel += ' ';
|
||||
}
|
||||
//get parent name
|
||||
if (jcard.parentId) {
|
||||
const parentCard = result.cards.find(
|
||||
(card) => card._id === jcard.parentId,
|
||||
);
|
||||
jcard.parentCardTitle = parentCard ? parentCard.title : '';
|
||||
}
|
||||
|
||||
//add card detail
|
||||
const t = Number(i) + 1;
|
||||
ws.addRow().values = [
|
||||
t.toString(),
|
||||
jcard.title,
|
||||
jcard.description,
|
||||
jcard.parentCardTitle,
|
||||
jmeml[jcard.userId],
|
||||
addTZhours(jcard.createdAt),
|
||||
addTZhours(jcard.dateLastActivity),
|
||||
addTZhours(jcard.receivedAt),
|
||||
addTZhours(jcard.startAt),
|
||||
addTZhours(jcard.dueAt),
|
||||
addTZhours(jcard.endAt),
|
||||
jlist[jcard.listId],
|
||||
jswimlane[jcard.swimlaneId],
|
||||
jcassig,
|
||||
jcmem,
|
||||
jclabel,
|
||||
jcard.isOvertime ? 'true' : 'false',
|
||||
jcard.spentTime,
|
||||
];
|
||||
const y = Number(i) + 6;
|
||||
//ws.getRow(y).height = 25;
|
||||
allBorder(`A${y}`);
|
||||
allBorder(`B${y}`);
|
||||
allBorder(`C${y}`);
|
||||
allBorder(`D${y}`);
|
||||
allBorder(`E${y}`);
|
||||
allBorder(`F${y}`);
|
||||
allBorder(`G${y}`);
|
||||
allBorder(`H${y}`);
|
||||
allBorder(`I${y}`);
|
||||
allBorder(`J${y}`);
|
||||
allBorder(`K${y}`);
|
||||
allBorder(`L${y}`);
|
||||
allBorder(`M${y}`);
|
||||
allBorder(`N${y}`);
|
||||
allBorder(`O${y}`);
|
||||
allBorder(`P${y}`);
|
||||
allBorder(`Q${y}`);
|
||||
allBorder(`R${y}`);
|
||||
cellCenter(`A${y}`);
|
||||
ws.getCell(`B${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`C${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`M${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`N${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`O${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
workbook.xlsx.write(res).then(function () {});
|
||||
*/
|
||||
|
||||
var doc = new PDFDocument({size: 'A4', margin: 50});
|
||||
doc.fontSize(12);
|
||||
doc.text('Some test text', 10, 30, {align: 'center', width: 200});
|
||||
this.response.writeHead(200, {
|
||||
'Content-type': 'application/pdf',
|
||||
'Content-Disposition': "attachment; filename=test.pdf"
|
||||
});
|
||||
this.response.end( doc.outputSync() );
|
||||
|
||||
}
|
||||
|
||||
canExport(user) {
|
||||
const board = Boards.findOne(this._boardId);
|
||||
return board && board.isVisibleBy(user);
|
||||
}
|
||||
}
|
||||
|
||||
export { ExporterCardPDF };
|
||||
617
models/server/ExporterExcel.js
Normal file
617
models/server/ExporterExcel.js
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
import { createWorkbook } from './createWorkbook';
|
||||
|
||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||
|
||||
class ExporterExcel {
|
||||
constructor(boardId) {
|
||||
this._boardId = boardId;
|
||||
}
|
||||
|
||||
build(res) {
|
||||
const fs = Npm.require('fs');
|
||||
const os = Npm.require('os');
|
||||
const path = Npm.require('path');
|
||||
|
||||
const byBoard = {
|
||||
boardId: this._boardId,
|
||||
};
|
||||
const byBoardNoLinked = {
|
||||
boardId: this._boardId,
|
||||
linkedId: {
|
||||
$in: ['', null],
|
||||
},
|
||||
};
|
||||
// we do not want to retrieve boardId in related elements
|
||||
const noBoardId = {
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
};
|
||||
const result = {
|
||||
_format: 'wekan-board-1.0.0',
|
||||
};
|
||||
_.extend(
|
||||
result,
|
||||
Boards.findOne(this._boardId, {
|
||||
fields: {
|
||||
stars: 0,
|
||||
},
|
||||
}),
|
||||
);
|
||||
result.lists = Lists.find(byBoard, noBoardId).fetch();
|
||||
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
|
||||
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
|
||||
result.customFields = CustomFields.find(
|
||||
{
|
||||
boardIds: {
|
||||
$in: [this.boardId],
|
||||
},
|
||||
},
|
||||
{
|
||||
fields: {
|
||||
boardId: 0,
|
||||
},
|
||||
},
|
||||
).fetch();
|
||||
result.comments = CardComments.find(byBoard, noBoardId).fetch();
|
||||
result.activities = Activities.find(byBoard, noBoardId).fetch();
|
||||
result.rules = Rules.find(byBoard, noBoardId).fetch();
|
||||
result.checklists = [];
|
||||
result.checklistItems = [];
|
||||
result.subtaskItems = [];
|
||||
result.triggers = [];
|
||||
result.actions = [];
|
||||
result.cards.forEach((card) => {
|
||||
result.checklists.push(
|
||||
...Checklists.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.checklistItems.push(
|
||||
...ChecklistItems.find({
|
||||
cardId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
result.subtaskItems.push(
|
||||
...Cards.find({
|
||||
parentId: card._id,
|
||||
}).fetch(),
|
||||
);
|
||||
});
|
||||
result.rules.forEach((rule) => {
|
||||
result.triggers.push(
|
||||
...Triggers.find(
|
||||
{
|
||||
_id: rule.triggerId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
result.actions.push(
|
||||
...Actions.find(
|
||||
{
|
||||
_id: rule.actionId,
|
||||
},
|
||||
noBoardId,
|
||||
).fetch(),
|
||||
);
|
||||
});
|
||||
|
||||
// we also have to export some user data - as the other elements only
|
||||
// include id but we have to be careful:
|
||||
// 1- only exports users that are linked somehow to that board
|
||||
// 2- do not export any sensitive information
|
||||
const users = {};
|
||||
result.members.forEach((member) => {
|
||||
users[member.userId] = true;
|
||||
});
|
||||
result.lists.forEach((list) => {
|
||||
users[list.userId] = true;
|
||||
});
|
||||
result.cards.forEach((card) => {
|
||||
users[card.userId] = true;
|
||||
if (card.members) {
|
||||
card.members.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
if (card.assignees) {
|
||||
card.assignees.forEach((memberId) => {
|
||||
users[memberId] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
result.comments.forEach((comment) => {
|
||||
users[comment.userId] = true;
|
||||
});
|
||||
result.activities.forEach((activity) => {
|
||||
users[activity.userId] = true;
|
||||
});
|
||||
result.checklists.forEach((checklist) => {
|
||||
users[checklist.userId] = true;
|
||||
});
|
||||
const byUserIds = {
|
||||
_id: {
|
||||
$in: Object.getOwnPropertyNames(users),
|
||||
},
|
||||
};
|
||||
// we use whitelist to be sure we do not expose inadvertently
|
||||
// some secret fields that gets added to User later.
|
||||
const userFields = {
|
||||
fields: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
'profile.initials': 1,
|
||||
'profile.avatarUrl': 1,
|
||||
},
|
||||
};
|
||||
result.users = Users.find(byUserIds, userFields)
|
||||
.fetch()
|
||||
.map((user) => {
|
||||
// user avatar is stored as a relative url, we export absolute
|
||||
if ((user.profile || {}).avatarUrl) {
|
||||
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
|
||||
}
|
||||
return user;
|
||||
});
|
||||
|
||||
//init exceljs workbook
|
||||
const workbook = createWorkbook();
|
||||
workbook.creator = TAPi18n.__('export-board');
|
||||
workbook.lastModifiedBy = TAPi18n.__('export-board');
|
||||
workbook.created = new Date();
|
||||
workbook.modified = new Date();
|
||||
workbook.lastPrinted = new Date();
|
||||
const filename = `${result.title}.xlsx`;
|
||||
//init worksheet
|
||||
const worksheet = workbook.addWorksheet(result.title, {
|
||||
properties: {
|
||||
tabColor: {
|
||||
argb: 'FFC0000',
|
||||
},
|
||||
},
|
||||
pageSetup: {
|
||||
paperSize: 9,
|
||||
orientation: 'landscape',
|
||||
},
|
||||
});
|
||||
//get worksheet
|
||||
const ws = workbook.getWorksheet(result.title);
|
||||
ws.properties.defaultRowHeight = 20;
|
||||
//init columns
|
||||
//Excel font. Western: Arial. zh-CN: 宋体
|
||||
ws.columns = [
|
||||
{
|
||||
key: 'a',
|
||||
width: 14,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
key: 'd',
|
||||
width: 40,
|
||||
},
|
||||
{
|
||||
key: 'e',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'f',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'g',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'h',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'i',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'j',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'k',
|
||||
width: 20,
|
||||
style: {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'l',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'm',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'n',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'o',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'q',
|
||||
width: 20,
|
||||
},
|
||||
{
|
||||
key: 'r',
|
||||
width: 20,
|
||||
},
|
||||
];
|
||||
|
||||
//add title line
|
||||
ws.mergeCells('A1:H1');
|
||||
ws.getCell('A1').value = result.title;
|
||||
ws.getCell('A1').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '20',
|
||||
},
|
||||
};
|
||||
ws.getCell('A1').alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
};
|
||||
ws.getRow(1).height = 40;
|
||||
//get member and assignee info
|
||||
let jmem = '';
|
||||
let jassig = '';
|
||||
const jmeml = {};
|
||||
const jassigl = {};
|
||||
for (const i in result.users) {
|
||||
jmem = `${jmem + result.users[i].username},`;
|
||||
jmeml[result.users[i]._id] = result.users[i].username;
|
||||
}
|
||||
jmem = jmem.substr(0, jmem.length - 1);
|
||||
for (const ia in result.users) {
|
||||
jassig = `${jassig + result.users[ia].username},`;
|
||||
jassigl[result.users[ia]._id] = result.users[ia].username;
|
||||
}
|
||||
jassig = jassig.substr(0, jassig.length - 1);
|
||||
//get kanban list info
|
||||
const jlist = {};
|
||||
for (const klist in result.lists) {
|
||||
jlist[result.lists[klist]._id] = result.lists[klist].title;
|
||||
}
|
||||
//get kanban swimlanes info
|
||||
const jswimlane = {};
|
||||
for (const kswimlane in result.swimlanes) {
|
||||
jswimlane[result.swimlanes[kswimlane]._id] =
|
||||
result.swimlanes[kswimlane].title;
|
||||
}
|
||||
//get kanban label info
|
||||
const jlabel = {};
|
||||
var isFirst = 1;
|
||||
for (const klabel in result.labels) {
|
||||
// console.log(klabel);
|
||||
if (isFirst == 0) {
|
||||
jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
|
||||
} else {
|
||||
isFirst = 0;
|
||||
jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
|
||||
}
|
||||
}
|
||||
//add data +8 hours
|
||||
function addTZhours(jdate) {
|
||||
const curdate = new Date(jdate);
|
||||
const checkCorrectDate = moment(curdate);
|
||||
if (checkCorrectDate.isValid()) {
|
||||
return curdate;
|
||||
} else {
|
||||
return ' ';
|
||||
}
|
||||
////Do not add 8 hours to GMT. Use GMT instead.
|
||||
////Could not yet figure out how to get localtime.
|
||||
//return new Date(curdate.setHours(curdate.getHours() + 8));
|
||||
//return curdate;
|
||||
}
|
||||
//add blank row
|
||||
ws.addRow().values = ['', '', '', '', '', ''];
|
||||
//add kanban info
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('createdAt'),
|
||||
addTZhours(result.createdAt),
|
||||
TAPi18n.__('modifiedAt'),
|
||||
addTZhours(result.modifiedAt),
|
||||
TAPi18n.__('members'),
|
||||
jmem,
|
||||
];
|
||||
ws.getRow(3).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 10,
|
||||
bold: true,
|
||||
};
|
||||
ws.mergeCells('F3:R3');
|
||||
ws.getCell('B3').style = {
|
||||
font: {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: '10',
|
||||
bold: true,
|
||||
},
|
||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
||||
};
|
||||
//cell center
|
||||
function cellCenter(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'center',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
function cellLeft(cellno) {
|
||||
ws.getCell(cellno).alignment = {
|
||||
vertical: 'middle',
|
||||
horizontal: 'left',
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
cellCenter('A3');
|
||||
cellCenter('B3');
|
||||
cellCenter('C3');
|
||||
cellCenter('D3');
|
||||
cellCenter('E3');
|
||||
cellLeft('F3');
|
||||
ws.getRow(3).height = 20;
|
||||
//all border
|
||||
function allBorder(cellno) {
|
||||
ws.getCell(cellno).border = {
|
||||
top: {
|
||||
style: 'thin',
|
||||
},
|
||||
left: {
|
||||
style: 'thin',
|
||||
},
|
||||
bottom: {
|
||||
style: 'thin',
|
||||
},
|
||||
right: {
|
||||
style: 'thin',
|
||||
},
|
||||
};
|
||||
}
|
||||
allBorder('A3');
|
||||
allBorder('B3');
|
||||
allBorder('C3');
|
||||
allBorder('D3');
|
||||
allBorder('E3');
|
||||
allBorder('F3');
|
||||
//add blank row
|
||||
ws.addRow().values = [
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
];
|
||||
//add card title
|
||||
//ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
|
||||
//this is where order in which the excel file generates
|
||||
ws.addRow().values = [
|
||||
TAPi18n.__('number'),
|
||||
TAPi18n.__('title'),
|
||||
TAPi18n.__('description'),
|
||||
TAPi18n.__('parent-card'),
|
||||
TAPi18n.__('owner'),
|
||||
TAPi18n.__('createdAt'),
|
||||
TAPi18n.__('last-modified-at'),
|
||||
TAPi18n.__('card-received'),
|
||||
TAPi18n.__('card-start'),
|
||||
TAPi18n.__('card-due'),
|
||||
TAPi18n.__('card-end'),
|
||||
TAPi18n.__('list'),
|
||||
TAPi18n.__('swimlane'),
|
||||
TAPi18n.__('assignee'),
|
||||
TAPi18n.__('members'),
|
||||
TAPi18n.__('labels'),
|
||||
TAPi18n.__('overtime-hours'),
|
||||
TAPi18n.__('spent-time-hours'),
|
||||
];
|
||||
ws.getRow(5).height = 20;
|
||||
allBorder('A5');
|
||||
allBorder('B5');
|
||||
allBorder('C5');
|
||||
allBorder('D5');
|
||||
allBorder('E5');
|
||||
allBorder('F5');
|
||||
allBorder('G5');
|
||||
allBorder('H5');
|
||||
allBorder('I5');
|
||||
allBorder('J5');
|
||||
allBorder('K5');
|
||||
allBorder('L5');
|
||||
allBorder('M5');
|
||||
allBorder('N5');
|
||||
allBorder('O5');
|
||||
allBorder('P5');
|
||||
allBorder('Q5');
|
||||
allBorder('R5');
|
||||
cellCenter('A5');
|
||||
cellCenter('B5');
|
||||
cellCenter('C5');
|
||||
cellCenter('D5');
|
||||
cellCenter('E5');
|
||||
cellCenter('F5');
|
||||
cellCenter('G5');
|
||||
cellCenter('H5');
|
||||
cellCenter('I5');
|
||||
cellCenter('J5');
|
||||
cellCenter('K5');
|
||||
cellCenter('L5');
|
||||
cellCenter('M5');
|
||||
cellCenter('N5');
|
||||
cellCenter('O5');
|
||||
cellCenter('P5');
|
||||
cellCenter('Q5');
|
||||
cellCenter('R5');
|
||||
ws.getRow(5).font = {
|
||||
name: TAPi18n.__('excel-font'),
|
||||
size: 12,
|
||||
bold: true,
|
||||
};
|
||||
//add blank row
|
||||
//add card info
|
||||
for (const i in result.cards) {
|
||||
const jcard = result.cards[i];
|
||||
//get member info
|
||||
let jcmem = '';
|
||||
for (const j in jcard.members) {
|
||||
jcmem += jmeml[jcard.members[j]];
|
||||
jcmem += ' ';
|
||||
}
|
||||
//get assignee info
|
||||
let jcassig = '';
|
||||
for (const ja in jcard.assignees) {
|
||||
jcassig += jassigl[jcard.assignees[ja]];
|
||||
jcassig += ' ';
|
||||
}
|
||||
//get card label info
|
||||
let jclabel = '';
|
||||
for (const jl in jcard.labelIds) {
|
||||
jclabel += jlabel[jcard.labelIds[jl]];
|
||||
jclabel += ' ';
|
||||
}
|
||||
//get parent name
|
||||
if (jcard.parentId) {
|
||||
const parentCard = result.cards.find(
|
||||
(card) => card._id === jcard.parentId,
|
||||
);
|
||||
jcard.parentCardTitle = parentCard ? parentCard.title : '';
|
||||
}
|
||||
|
||||
//add card detail
|
||||
const t = Number(i) + 1;
|
||||
ws.addRow().values = [
|
||||
t.toString(),
|
||||
jcard.title,
|
||||
jcard.description,
|
||||
jcard.parentCardTitle,
|
||||
jmeml[jcard.userId],
|
||||
addTZhours(jcard.createdAt),
|
||||
addTZhours(jcard.dateLastActivity),
|
||||
addTZhours(jcard.receivedAt),
|
||||
addTZhours(jcard.startAt),
|
||||
addTZhours(jcard.dueAt),
|
||||
addTZhours(jcard.endAt),
|
||||
jlist[jcard.listId],
|
||||
jswimlane[jcard.swimlaneId],
|
||||
jcassig,
|
||||
jcmem,
|
||||
jclabel,
|
||||
jcard.isOvertime ? 'true' : 'false',
|
||||
jcard.spentTime,
|
||||
];
|
||||
const y = Number(i) + 6;
|
||||
//ws.getRow(y).height = 25;
|
||||
allBorder(`A${y}`);
|
||||
allBorder(`B${y}`);
|
||||
allBorder(`C${y}`);
|
||||
allBorder(`D${y}`);
|
||||
allBorder(`E${y}`);
|
||||
allBorder(`F${y}`);
|
||||
allBorder(`G${y}`);
|
||||
allBorder(`H${y}`);
|
||||
allBorder(`I${y}`);
|
||||
allBorder(`J${y}`);
|
||||
allBorder(`K${y}`);
|
||||
allBorder(`L${y}`);
|
||||
allBorder(`M${y}`);
|
||||
allBorder(`N${y}`);
|
||||
allBorder(`O${y}`);
|
||||
allBorder(`P${y}`);
|
||||
allBorder(`Q${y}`);
|
||||
allBorder(`R${y}`);
|
||||
cellCenter(`A${y}`);
|
||||
ws.getCell(`B${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`C${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`M${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`N${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
ws.getCell(`O${y}`).alignment = {
|
||||
wrapText: true,
|
||||
};
|
||||
}
|
||||
workbook.xlsx.write(res).then(function () {});
|
||||
}
|
||||
|
||||
canExport(user) {
|
||||
const board = Boards.findOne(this._boardId);
|
||||
return board && board.isVisibleBy(user);
|
||||
}
|
||||
}
|
||||
|
||||
export { ExporterExcel };
|
||||
5
models/server/createWorkbook.js
Normal file
5
models/server/createWorkbook.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Excel from 'exceljs';
|
||||
|
||||
export const createWorkbook = function() {
|
||||
return new Excel.Workbook();
|
||||
};
|
||||
|
|
@ -46,6 +46,10 @@ Settings.attachSchema(
|
|||
type: String,
|
||||
optional: false,
|
||||
},
|
||||
spinnerName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
hideLogo: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
|
|
|||
|
|
@ -454,8 +454,8 @@ if (Meteor.isServer) {
|
|||
*/
|
||||
JsonRoutes.add('POST', '/api/boards/:boardId/swimlanes', function(req, res) {
|
||||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const paramBoardId = req.params.boardId;
|
||||
Authentication.checkBoardAccess(req.userId, paramBoardId);
|
||||
const board = Boards.findOne(paramBoardId);
|
||||
const id = Swimlanes.insert({
|
||||
title: req.body.title,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,13 @@ Team.attachSchema(
|
|||
optional: true,
|
||||
max: 255,
|
||||
},
|
||||
teamIsActive: {
|
||||
/**
|
||||
* status of the team
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
createdAt: {
|
||||
/**
|
||||
* creation date of the team
|
||||
|
|
@ -68,6 +75,43 @@ Team.attachSchema(
|
|||
);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Team.allow({
|
||||
insert(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
update(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
remove(userId, doc) {
|
||||
const user = Users.findOne({
|
||||
_id: userId,
|
||||
});
|
||||
if ((user && user.isAdmin) || (Meteor.user() && Meteor.user().isAdmin))
|
||||
return true;
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
return doc._id === userId;
|
||||
},
|
||||
fetch: [],
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
setCreateTeam(
|
||||
teamDisplayName,
|
||||
|
|
@ -81,7 +125,7 @@ if (Meteor.isServer) {
|
|||
check(teamDesc, String);
|
||||
check(teamShortName, String);
|
||||
check(teamWebsite, String);
|
||||
check(teamIsActive, String);
|
||||
check(teamIsActive, Boolean);
|
||||
|
||||
const nTeamNames = Team.find({ teamShortName }).count();
|
||||
if (nTeamNames > 0) {
|
||||
|
|
@ -100,7 +144,7 @@ if (Meteor.isServer) {
|
|||
|
||||
setTeamDisplayName(team, teamDisplayName) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(team, String);
|
||||
check(team, Object);
|
||||
check(teamDisplayName, String);
|
||||
Team.update(team, {
|
||||
$set: { teamDisplayName: teamDisplayName },
|
||||
|
|
@ -110,7 +154,7 @@ if (Meteor.isServer) {
|
|||
|
||||
setTeamDesc(team, teamDesc) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(team, String);
|
||||
check(team, Object);
|
||||
check(teamDesc, String);
|
||||
Team.update(team, {
|
||||
$set: { teamDesc: teamDesc },
|
||||
|
|
@ -120,7 +164,7 @@ if (Meteor.isServer) {
|
|||
|
||||
setTeamShortName(team, teamShortName) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(team, String);
|
||||
check(team, Object);
|
||||
check(teamShortName, String);
|
||||
Team.update(team, {
|
||||
$set: { teamShortName: teamShortName },
|
||||
|
|
@ -130,20 +174,47 @@ if (Meteor.isServer) {
|
|||
|
||||
setTeamIsActive(team, teamIsActive) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(team, String);
|
||||
check(teamIsActive, String);
|
||||
check(team, Object);
|
||||
check(teamIsActive, Boolean);
|
||||
Team.update(team, {
|
||||
$set: { teamIsActive: teamIsActive },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setTeamAllFields(
|
||||
team,
|
||||
teamDisplayName,
|
||||
teamDesc,
|
||||
teamShortName,
|
||||
teamWebsite,
|
||||
teamIsActive,
|
||||
) {
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(team, Object);
|
||||
check(teamDisplayName, String);
|
||||
check(teamDesc, String);
|
||||
check(teamShortName, String);
|
||||
check(teamWebsite, String);
|
||||
check(teamIsActive, Boolean);
|
||||
Team.update(team, {
|
||||
$set: {
|
||||
teamDisplayName: teamDisplayName,
|
||||
teamDesc: teamDesc,
|
||||
teamShortName: teamShortName,
|
||||
teamWebsite: teamWebsite,
|
||||
teamIsActive: teamIsActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// Index for Team name.
|
||||
Meteor.startup(() => {
|
||||
Team._collection._ensureIndex({ name: -1 });
|
||||
Team._collection._ensureIndex({ teamDisplayName: -1 });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export class TrelloCreator {
|
|||
check(
|
||||
trelloBoard,
|
||||
Match.ObjectIncluding({
|
||||
closed: Boolean,
|
||||
// closed: Boolean, // issue #3840, should import closed Trello boards
|
||||
name: String,
|
||||
prefs: Match.ObjectIncluding({
|
||||
// XXX refine control by validating 'background' against a list of
|
||||
|
|
|
|||
190
models/users.js
190
models/users.js
|
|
@ -38,6 +38,44 @@ Users.attachSchema(
|
|||
}
|
||||
},
|
||||
},
|
||||
orgs: {
|
||||
/**
|
||||
* the list of organizations that a user belongs to
|
||||
*/
|
||||
type: [Object],
|
||||
optional: true,
|
||||
},
|
||||
'orgs.$.orgId':{
|
||||
/**
|
||||
* The uniq ID of the organization
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'orgs.$.orgDisplayName':{
|
||||
/**
|
||||
* The display name of the organization
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
teams: {
|
||||
/**
|
||||
* the list of teams that a user belongs to
|
||||
*/
|
||||
type: [Object],
|
||||
optional: true,
|
||||
},
|
||||
'teams.$.teamId':{
|
||||
/**
|
||||
* The uniq ID of the team
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
'teams.$.teamDisplayName':{
|
||||
/**
|
||||
* The display name of the team
|
||||
*/
|
||||
type: String,
|
||||
},
|
||||
emails: {
|
||||
/**
|
||||
* the list of emails attached to a user
|
||||
|
|
@ -126,7 +164,7 @@ Users.attachSchema(
|
|||
},
|
||||
'profile.showDesktopDragHandles': {
|
||||
/**
|
||||
* does the user want to hide system messages?
|
||||
* does the user want to show desktop drag handles?
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
|
@ -138,6 +176,13 @@ Users.attachSchema(
|
|||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.cardMaximized': {
|
||||
/**
|
||||
* has user clicked maximize card?
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.hiddenSystemMessages': {
|
||||
/**
|
||||
* does the user want to hide system messages?
|
||||
|
|
@ -329,13 +374,7 @@ Users.attachSchema(
|
|||
},
|
||||
'sessionData.totalHits': {
|
||||
/**
|
||||
* Total hits from last search
|
||||
*/
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
'sessionData.lastHit': {
|
||||
/**
|
||||
* Total hits from last searchquery['members.userId'] = Meteor.userId();
|
||||
* last hit that was returned
|
||||
*/
|
||||
type: Number,
|
||||
|
|
@ -403,6 +442,8 @@ Users.safeFields = {
|
|||
'profile.fullname': 1,
|
||||
'profile.avatarUrl': 1,
|
||||
'profile.initials': 1,
|
||||
orgs: 1,
|
||||
teams: 1,
|
||||
};
|
||||
|
||||
if (Meteor.isClient) {
|
||||
|
|
@ -464,7 +505,30 @@ Users.helpers({
|
|||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
orgsUserBelongs() {
|
||||
if (this.orgs) {
|
||||
return this.orgs.map(function(org){return org.orgDisplayName}).join(',');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
orgIdsUserBelongs() {
|
||||
if (this.orgs) {
|
||||
return this.orgs.map(function(org){return org.orgId}).join(',');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
teamsUserBelongs() {
|
||||
if (this.teams) {
|
||||
return this.teams.map(function(team){ return team.teamDisplayName}).join(',');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
teamIdsUserBelongs() {
|
||||
if (this.teams) {
|
||||
return this.teams.map(function(team){ return team.teamId}).join(',');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
boards() {
|
||||
return Boards.find(
|
||||
{
|
||||
|
|
@ -586,6 +650,11 @@ Users.helpers({
|
|||
return profile.hiddenSystemMessages || false;
|
||||
},
|
||||
|
||||
hasCardMaximized() {
|
||||
const profile = this.profile || {};
|
||||
return profile.cardMaximized || false;
|
||||
},
|
||||
|
||||
hasHiddenMinicardLabelText() {
|
||||
const profile = this.profile || {};
|
||||
return profile.hiddenMinicardLabelText || false;
|
||||
|
|
@ -738,6 +807,14 @@ Users.mutations({
|
|||
};
|
||||
},
|
||||
|
||||
toggleCardMaximized(value = false) {
|
||||
return {
|
||||
$set: {
|
||||
'profile.cardMaximized': !value,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
toggleLabelText(value = false) {
|
||||
return {
|
||||
$set: {
|
||||
|
|
@ -832,6 +909,10 @@ Meteor.methods({
|
|||
const user = Meteor.user();
|
||||
user.toggleSystem(user.hasHiddenSystemMessages());
|
||||
},
|
||||
toggleCardMaximized() {
|
||||
const user = Meteor.user();
|
||||
user.toggleCardMaximized(user.hasCardMaximized());
|
||||
},
|
||||
toggleMinicardLabelText() {
|
||||
const user = Meteor.user();
|
||||
user.toggleLabelText(user.hasHiddenMinicardLabelText());
|
||||
|
|
@ -894,17 +975,20 @@ if (Meteor.isServer) {
|
|||
isActive,
|
||||
email,
|
||||
importUsernames,
|
||||
userOrgsArray,
|
||||
userTeamsArray,
|
||||
) {
|
||||
check(fullname, String);
|
||||
check(username, String);
|
||||
check(initials, String);
|
||||
check(password, String);
|
||||
check(isAdmin, String);
|
||||
check(isActive, String);
|
||||
check(email, String);
|
||||
check(importUsernames, Array);
|
||||
check(userOrgsArray, Array);
|
||||
check(userTeamsArray, Array);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(fullname, String);
|
||||
check(username, String);
|
||||
check(initials, String);
|
||||
check(password, String);
|
||||
check(isAdmin, String);
|
||||
check(isActive, String);
|
||||
check(email, String);
|
||||
check(importUsernames, Array);
|
||||
|
||||
const nUsersWithUsername = Users.find({
|
||||
username,
|
||||
}).count();
|
||||
|
|
@ -935,6 +1019,8 @@ if (Meteor.isServer) {
|
|||
'profile.fullname': fullname,
|
||||
importUsernames,
|
||||
'profile.initials': initials,
|
||||
orgs: userOrgsArray,
|
||||
teams: userTeamsArray,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -942,9 +1028,9 @@ if (Meteor.isServer) {
|
|||
}
|
||||
},
|
||||
setUsername(username, userId) {
|
||||
check(username, String);
|
||||
check(userId, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(username, String);
|
||||
check(userId, String);
|
||||
const nUsersWithUsername = Users.find({
|
||||
username,
|
||||
}).count();
|
||||
|
|
@ -960,11 +1046,12 @@ if (Meteor.isServer) {
|
|||
}
|
||||
},
|
||||
setEmail(email, userId) {
|
||||
check(email, String);
|
||||
check(username, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
if (Array.isArray(email)) {
|
||||
email = email.shift();
|
||||
}
|
||||
check(email, String);
|
||||
const existingUser = Users.findOne(
|
||||
{
|
||||
'emails.address': email,
|
||||
|
|
@ -992,31 +1079,31 @@ if (Meteor.isServer) {
|
|||
}
|
||||
},
|
||||
setUsernameAndEmail(username, email, userId) {
|
||||
check(username, String);
|
||||
check(email, String);
|
||||
check(userId, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(username, String);
|
||||
if (Array.isArray(email)) {
|
||||
email = email.shift();
|
||||
}
|
||||
check(email, String);
|
||||
check(userId, String);
|
||||
Meteor.call('setUsername', username, userId);
|
||||
Meteor.call('setEmail', email, userId);
|
||||
}
|
||||
},
|
||||
setPassword(newPassword, userId) {
|
||||
check(userId, String);
|
||||
check(newPassword, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(userId, String);
|
||||
check(newPassword, String);
|
||||
if (Meteor.user().isAdmin) {
|
||||
Accounts.setPassword(userId, newPassword);
|
||||
}
|
||||
}
|
||||
},
|
||||
setEmailVerified(email, verified, userId) {
|
||||
check(email, String);
|
||||
check(verified, Boolean);
|
||||
check(userId, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(email, String);
|
||||
check(verified, Boolean);
|
||||
check(userId, String);
|
||||
Users.update(userId, {
|
||||
$set: {
|
||||
emails: [
|
||||
|
|
@ -1030,9 +1117,9 @@ if (Meteor.isServer) {
|
|||
}
|
||||
},
|
||||
setInitials(initials, userId) {
|
||||
check(initials, String);
|
||||
check(userId, String);
|
||||
if (Meteor.user() && Meteor.user().isAdmin) {
|
||||
check(initials, String);
|
||||
check(userId, String);
|
||||
Users.update(userId, {
|
||||
$set: {
|
||||
'profile.initials': initials,
|
||||
|
|
@ -1405,24 +1492,26 @@ if (Meteor.isServer) {
|
|||
|
||||
fakeUserId.withValue(doc._id, () => {
|
||||
/*
|
||||
// Insert the Welcome Board
|
||||
Boards.insert({
|
||||
title: TAPi18n.__('welcome-board'),
|
||||
permission: 'private',
|
||||
}, fakeUser, (err, boardId) => {
|
||||
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('welcome-swimlane'),
|
||||
boardId,
|
||||
sort: 1,
|
||||
}, fakeUser);
|
||||
// Insert the Welcome Board
|
||||
Boards.insert({
|
||||
title: TAPi18n.__('welcome-board'),
|
||||
permission: 'private',
|
||||
}, fakeUser, (err, boardId) => {
|
||||
|
||||
['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
|
||||
Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
|
||||
});
|
||||
});
|
||||
*/
|
||||
Swimlanes.insert({
|
||||
title: TAPi18n.__('welcome-swimlane'),
|
||||
boardId,
|
||||
sort: 1,
|
||||
}, fakeUser);
|
||||
|
||||
['welcome-list1', 'welcome-list2'].forEach((title, titleIndex) => {
|
||||
Lists.insert({title: TAPi18n.__(title), boardId, sort: titleIndex}, fakeUser);
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
// Insert Template Container
|
||||
const Future = require('fibers/future');
|
||||
const future1 = new Future();
|
||||
const future2 = new Future();
|
||||
|
|
@ -1507,6 +1596,7 @@ if (Meteor.isServer) {
|
|||
future1.wait();
|
||||
future2.wait();
|
||||
future3.wait();
|
||||
// End of Insert Template Container
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -1977,16 +2067,18 @@ if (Meteor.isServer) {
|
|||
try {
|
||||
Authentication.checkUserId(req.userId);
|
||||
const id = req.params.userId;
|
||||
// Delete is not enabled yet, because it does leave empty user avatars
|
||||
// Delete user is enabled, but is still has bug of leaving empty user avatars
|
||||
// to boards: boards members, card members and assignees have
|
||||
// empty users. See:
|
||||
// empty users. So it would be better to delete user from all boards before
|
||||
// deleting user.
|
||||
// See:
|
||||
// - wekan/client/components/settings/peopleBody.jade deleteButton
|
||||
// - wekan/client/components/settings/peopleBody.js deleteButton
|
||||
// - wekan/client/components/sidebar/sidebar.js Popup.afterConfirm('removeMember'
|
||||
// that does now remove member from board, card members and assignees correctly,
|
||||
// but that should be used to remove user from all boards similarly
|
||||
// - wekan/models/users.js Delete is not enabled
|
||||
// Meteor.users.remove({ _id: id });
|
||||
Meteor.users.remove({ _id: id });
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue