summaryrefslogtreecommitdiffstats
path: root/models
diff options
context:
space:
mode:
author蔡仲明 (Romulus Urakagi Tsai) <urakagi@gmail.com>2019-11-21 11:25:56 +0800
committerGitHub <noreply@github.com>2019-11-21 11:25:56 +0800
commit3e0bedd8c7a6dec97352212adb1cbde1ade44190 (patch)
tree651ff30d25ddb0416444370368d699e597c142d7 /models
parent9bbeb73db1cd0ce1caaaca8dfb14ea92131bbf9d (diff)
parent4f5de87cc4c2281bd576548693de7c94e6a988c6 (diff)
downloadwekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.tar.gz
wekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.tar.bz2
wekan-3e0bedd8c7a6dec97352212adb1cbde1ade44190.zip
Merge pull request #1 from wekan/master
Update master
Diffstat (limited to 'models')
-rw-r--r--models/accountSettings.js2
-rw-r--r--models/actions.js10
-rw-r--r--models/activities.js70
-rw-r--r--models/announcements.js2
-rw-r--r--models/boards.js30
-rw-r--r--models/cardComments.js2
-rw-r--r--models/cards.js230
-rw-r--r--models/checklistItems.js2
-rw-r--r--models/checklists.js15
-rw-r--r--models/customFields.js2
-rw-r--r--models/export.js17
-rw-r--r--models/integrations.js24
-rw-r--r--models/invitationCodes.js2
-rw-r--r--models/lists.js28
-rw-r--r--models/org.js2
-rw-r--r--models/orgUser.js2
-rw-r--r--models/rules.js2
-rw-r--r--models/settings.js2
-rw-r--r--models/swimlanes.js21
-rw-r--r--models/triggers.js10
-rw-r--r--models/unsavedEdits.js2
-rw-r--r--models/users.js85
22 files changed, 522 insertions, 40 deletions
diff --git a/models/accountSettings.js b/models/accountSettings.js
index ed1087ca..f61614b8 100644
--- a/models/accountSettings.js
+++ b/models/accountSettings.js
@@ -20,6 +20,8 @@ AccountSettings.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/actions.js b/models/actions.js
index e9fa9114..8995d101 100644
--- a/models/actions.js
+++ b/models/actions.js
@@ -14,6 +14,16 @@ Actions.allow({
},
});
+Actions.before.insert((userId, doc) => {
+ doc.createdAt = new Date();
+ doc.modifiedAt = doc.createdAt;
+});
+
+Actions.before.update((userId, doc, fieldNames, modifier) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = new Date();
+});
+
Actions.helpers({
description() {
return this.desc;
diff --git a/models/activities.js b/models/activities.js
index 3ecd5c8c..19e3fb7d 100644
--- a/models/activities.js
+++ b/models/activities.js
@@ -62,8 +62,14 @@ Activities.helpers({
//},
});
+Activities.before.update((userId, doc, fieldNames, modifier) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.modifiedAt = new Date();
+});
+
Activities.before.insert((userId, doc) => {
doc.createdAt = new Date();
+ doc.modifiedAt = doc.createdAt;
});
Activities.after.insert((userId, doc) => {
@@ -174,25 +180,34 @@ if (Meteor.isServer) {
const comment = activity.comment();
params.comment = comment.text;
if (board) {
- const atUser = /(?:^|>|\b|\s)@(\S+)(?:\s|$|<|\b)/g;
const comment = params.comment;
- if (comment.match(atUser)) {
- const commenter = params.user;
- while (atUser.exec(comment)) {
- const username = RegExp.$1;
- if (commenter === username) {
- // it's person at himself, ignore it?
- continue;
- }
- const user = Users.findOne(username) || Users.findOne({ username });
- const uid = user && user._id;
- params.atUsername = username;
- params.atEmails = user.emails;
- if (board.hasMember(uid)) {
- title = 'act-atUserComment';
- watchers = _.union(watchers, [uid]);
- }
+ const knownUsers = board.members.map(member => {
+ const u = Users.findOne(member.userId);
+ if (u) {
+ member.username = u.username;
+ member.emails = u.emails;
+ }
+ return member;
+ });
+ const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
+ let currentMention;
+ while ((currentMention = mentionRegex.exec(comment)) !== null) {
+ /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/
+ const [ignored, quoteduser, simple] = currentMention;
+ const username = quoteduser || simple;
+ if (username === params.user) {
+ // ignore commenter mention himself?
+ continue;
+ }
+ const atUser = _.findWhere(knownUsers, { username });
+ if (!atUser) {
+ continue;
}
+ const uid = atUser.userId;
+ params.atUsername = username;
+ params.atEmails = atUser.emails;
+ title = 'act-atUserComment';
+ watchers = _.union(watchers, [uid]);
}
}
params.commentId = comment._id;
@@ -227,8 +242,8 @@ if (Meteor.isServer) {
(!activity.timeKey || activity.timeKey === 'dueAt') &&
activity.timeValue
) {
- // due time reminder
- title = 'act-withDue';
+ // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation
+ title = activity.timeOldValue ? 'act-withDue' : 'act-newDue';
}
['timeValue', 'timeOldValue'].forEach(key => {
// copy time related keys & values to params
@@ -268,13 +283,24 @@ if (Meteor.isServer) {
});
const integrations = Integrations.find({
- boardId: board._id,
- type: 'outgoing-webhooks',
+ boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
+ // type: 'outgoing-webhooks', // all types
enabled: true,
activities: { $in: [description, 'all'] },
}).fetch();
if (integrations.length > 0) {
- Meteor.call('outgoingWebhooks', integrations, description, params);
+ params.watchers = watchers;
+ integrations.forEach(integration => {
+ Meteor.call(
+ 'outgoingWebhooks',
+ integration,
+ description,
+ params,
+ () => {
+ return;
+ },
+ );
+ });
}
});
}
diff --git a/models/announcements.js b/models/announcements.js
index c08710b8..7fdf8d8b 100644
--- a/models/announcements.js
+++ b/models/announcements.js
@@ -25,6 +25,8 @@ Announcements.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/boards.js b/models/boards.js
index b5f8b01b..857aa963 100644
--- a/models/boards.js
+++ b/models/boards.js
@@ -55,6 +55,8 @@ Boards.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -407,6 +409,27 @@ Boards.helpers({
},
lists() {
+ //currentUser = Meteor.user();
+ //if (currentUser) {
+ // enabled = Meteor.user().hasSortBy();
+ //}
+ //return enabled ? this.newestLists() : this.draggableLists();
+ return this.draggableLists();
+ },
+
+ newestLists() {
+ // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
+ const value = Meteor.user()._getListSortBy();
+ const sortKey = { starred: -1, [value[0]]: value[1] }; // [["starred",-1],value];
+ return Lists.find(
+ {
+ boardId: this._id,
+ archived: false,
+ },
+ { sort: sortKey },
+ );
+ },
+ draggableLists() {
return Lists.find({ boardId: this._id }, { sort: { sort: 1 } });
},
@@ -697,6 +720,13 @@ Boards.helpers({
return result;
},
+ cardsDueInBetween(start, end) {
+ return Cards.find({
+ boardId: this._id,
+ dueAt: { $gte: start, $lte: end },
+ });
+ },
+
cardsInInterval(start, end) {
return Cards.find({
boardId: this._id,
diff --git a/models/cardComments.js b/models/cardComments.js
index 40723582..39477e14 100644
--- a/models/cardComments.js
+++ b/models/cardComments.js
@@ -34,6 +34,8 @@ CardComments.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/cards.js b/models/cards.js
index d92d003c..816132fe 100644
--- a/models/cards.js
+++ b/models/cards.js
@@ -107,6 +107,8 @@ Cards.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -201,6 +203,15 @@ Cards.attachSchema(
optional: true,
defaultValue: [],
},
+ assignees: {
+ /**
+ * who is assignee of the card (user ID),
+ * maximum one ID of assignee in array.
+ */
+ type: [String],
+ optional: true,
+ defaultValue: [],
+ },
receivedAt: {
/**
* Date the card was received
@@ -409,6 +420,10 @@ Cards.helpers({
return _.contains(this.getMembers(), memberId);
},
+ isAssignee(assigneeId) {
+ return _.contains(this.getAssignees(), assigneeId);
+ },
+
activities() {
if (this.isLinkedCard()) {
return Activities.find(
@@ -743,6 +758,20 @@ Cards.helpers({
}
},
+ getAssignees() {
+ if (this.isLinkedCard()) {
+ const card = Cards.findOne({ _id: this.linkedId });
+ return card.assignees;
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.activeMembers().map(assignee => {
+ return assignee.userId;
+ });
+ } else {
+ return this.assignees;
+ }
+ },
+
assignMember(memberId) {
if (this.isLinkedCard()) {
return Cards.update(
@@ -760,6 +789,23 @@ Cards.helpers({
}
},
+ assignAssignee(assigneeId) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $addToSet: { assignees: assigneeId } },
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.addAssignee(assigneeId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $addToSet: { assignees: assigneeId } },
+ );
+ }
+ },
+
unassignMember(memberId) {
if (this.isLinkedCard()) {
return Cards.update(
@@ -774,6 +820,23 @@ Cards.helpers({
}
},
+ unassignAssignee(assigneeId) {
+ if (this.isLinkedCard()) {
+ return Cards.update(
+ { _id: this.linkedId },
+ { $pull: { assignees: assigneeId } },
+ );
+ } else if (this.isLinkedBoard()) {
+ const board = Boards.findOne({ _id: this.linkedId });
+ return board.removeAssignee(assigneeId);
+ } else {
+ return Cards.update(
+ { _id: this._id },
+ { $pull: { assignees: assigneeId } },
+ );
+ }
+ },
+
toggleMember(memberId) {
if (this.getMembers() && this.getMembers().indexOf(memberId) > -1) {
return this.unassignMember(memberId);
@@ -782,6 +845,14 @@ Cards.helpers({
}
},
+ toggleAssignee(assigneeId) {
+ if (this.getAssignees() && this.getAssignees().indexOf(assigneeId) > -1) {
+ return this.unassignAssignee(assigneeId);
+ } else {
+ return this.assignAssignee(assigneeId);
+ }
+ },
+
getReceived() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
@@ -1124,6 +1195,19 @@ Cards.mutations({
};
},
+ assignAssignee(assigneeId) {
+ // If there is not any assignee, allow one assignee, not more.
+ if (this.getAssignees().length === 0) {
+ return {
+ $addToSet: {
+ assignees: assigneeId,
+ },
+ };
+ } else {
+ return false;
+ }
+ },
+
unassignMember(memberId) {
return {
$pull: {
@@ -1132,6 +1216,14 @@ Cards.mutations({
};
},
+ unassignAssignee(assigneeId) {
+ return {
+ $pull: {
+ assignees: assigneeId,
+ },
+ };
+ },
+
toggleMember(memberId) {
if (this.members && this.members.indexOf(memberId) > -1) {
return this.unassignMember(memberId);
@@ -1140,6 +1232,14 @@ Cards.mutations({
}
},
+ toggleAssignee(assigneeId) {
+ if (this.assignees && this.assignees.indexOf(assigneeId) > -1) {
+ return this.unassignAssignee(assigneeId);
+ } else {
+ return this.assignAssignee(assigneeId);
+ }
+ },
+
assignCustomField(customFieldId) {
return {
$addToSet: {
@@ -1434,6 +1534,46 @@ function cardMembers(userId, doc, fieldNames, modifier) {
}
}
+function cardAssignees(userId, doc, fieldNames, modifier) {
+ if (!_.contains(fieldNames, 'assignees')) return;
+ let assigneeId;
+ // Say hello to the new assignee
+ if (modifier.$addToSet && modifier.$addToSet.assignees) {
+ assigneeId = modifier.$addToSet.assignees;
+ const username = Users.findOne(assigneeId).username;
+ if (!_.contains(doc.assignees, assigneeId)) {
+ Activities.insert({
+ userId,
+ username,
+ activityType: 'joinAssignee',
+ boardId: doc.boardId,
+ cardId: doc._id,
+ assigneeId,
+ listId: doc.listId,
+ swimlaneId: doc.swimlaneId,
+ });
+ }
+ }
+ // Say goodbye to the former assignee
+ if (modifier.$pull && modifier.$pull.assignees) {
+ assigneeId = modifier.$pull.assignees;
+ const username = Users.findOne(assigneeId).username;
+ // Check that the former assignee is assignee of the card
+ if (_.contains(doc.assignees, assigneeId)) {
+ Activities.insert({
+ userId,
+ username,
+ activityType: 'unjoinAssignee',
+ boardId: doc.boardId,
+ cardId: doc._id,
+ assigneeId,
+ listId: doc.listId,
+ swimlaneId: doc.swimlaneId,
+ });
+ }
+ }
+}
+
function cardLabels(userId, doc, fieldNames, modifier) {
if (!_.contains(fieldNames, 'labelIds')) return;
let labelId;
@@ -1556,6 +1696,7 @@ function cardRemover(userId, doc) {
const findDueCards = days => {
const seekDue = ($from, $to, activityType) => {
Cards.find({
+ archived: false,
dueAt: { $gte: $from, $lt: $to },
}).forEach(card => {
const username = Users.findOne(card.userId).username;
@@ -1576,18 +1717,38 @@ const findDueCards = days => {
const now = new Date(),
aday = 3600 * 24 * 1e3,
then = day => new Date(now.setHours(0, 0, 0, 0) + day * aday);
- seekDue(then(1), then(days), 'almostdue');
- seekDue(then(0), then(1), 'duenow');
- seekDue(then(-days), now, 'pastdue');
+ if (!days) return;
+ if (!days.map) days = [days];
+ days.map(day => {
+ let args = [];
+ if (day === 0) {
+ args = [then(0), then(1), 'duenow'];
+ } else if (day > 0) {
+ args = [then(1), then(day), 'almostdue'];
+ } else {
+ args = [then(day), now, 'pastdue'];
+ }
+ seekDue(...args);
+ });
};
const addCronJob = _.debounce(
Meteor.bindEnvironment(function findDueCardsDebounced() {
- const notifydays =
- parseInt(process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER, 10) || 2; // default as 2 days before and after
- if (!(notifydays > 0 && notifydays < 15)) {
- // notifying due is disabled
+ const envValue = process.env.NOTIFY_DUE_DAYS_BEFORE_AND_AFTER;
+ if (!envValue) {
return;
}
+ const notifydays = envValue
+ .split(',')
+ .map(value => {
+ const iValue = parseInt(value, 10);
+ if (!(iValue > 0 && iValue < 15)) {
+ // notifying due is disabled
+ return false;
+ } else {
+ return iValue;
+ }
+ })
+ .filter(Boolean);
const notifyitvl = process.env.NOTIFY_DUE_AT_HOUR_OF_DAY; //passed in the itvl has to be a number standing for the hour of current time
const defaultitvl = 8; // default every morning at 8am, if the passed env variable has parsing error use default
const itvl = parseInt(notifyitvl, 10) || defaultitvl;
@@ -1650,6 +1811,12 @@ if (Meteor.isServer) {
updateActivities(doc, fieldNames, modifier);
});
+ // Add a new activity if we add or remove a assignee to the card
+ Cards.before.update((userId, doc, fieldNames, modifier) => {
+ cardAssignees(userId, doc, fieldNames, modifier);
+ updateActivities(doc, fieldNames, modifier);
+ });
+
// Add a new activity if we add or remove a label to the card
Cards.before.update((userId, doc, fieldNames, modifier) => {
cardLabels(userId, doc, fieldNames, modifier);
@@ -1672,6 +1839,26 @@ if (Meteor.isServer) {
const oldvalue = doc[action] || '';
const activityType = `a-${action}`;
const card = Cards.findOne(doc._id);
+ const list = card.list();
+ if (list) {
+ // change list modifiedAt, when user modified the key values in timingaction array, if it's endAt, put the modifiedAt of list back to one year ago for sorting purpose
+ const modifiedAt = new Date(
+ new Date(value).getTime() -
+ (action === 'endAt' ? 365 * 24 * 3600 * 1e3 : 0),
+ ); // set it as 1 year before
+ const boardId = list.boardId;
+ Lists.direct.update(
+ {
+ _id: list._id,
+ },
+ {
+ $set: {
+ modifiedAt,
+ boardId,
+ },
+ },
+ );
+ }
const username = Users.findOne(userId).username;
const activity = {
userId,
@@ -1809,6 +1996,7 @@ if (Meteor.isServer) {
* @param {string} description the description of the new card
* @param {string} swimlaneId the swimlane ID of the new card
* @param {string} [members] the member IDs list of the new card
+ * @param {string} [assignees] the array of maximum one ID of assignee of the new card
* @return_type {_id: string}
*/
JsonRoutes.add('POST', '/api/boards/:boardId/lists/:listId/cards', function(
@@ -1830,6 +2018,7 @@ if (Meteor.isServer) {
_id: req.body.authorId,
});
const members = req.body.members || [req.body.authorId];
+ const assignees = req.body.assignees;
if (typeof check !== 'undefined') {
const id = Cards.direct.insert({
title: req.body.title,
@@ -1841,6 +2030,7 @@ if (Meteor.isServer) {
swimlaneId: req.body.swimlaneId,
sort: currentCards.count(),
members,
+ assignees,
});
JsonRoutes.sendResult(res, {
code: 200,
@@ -1892,6 +2082,7 @@ if (Meteor.isServer) {
* @param {string} [labelIds] the new list of label IDs attached to the card
* @param {string} [swimlaneId] the new swimlane ID of the card
* @param {string} [members] the new list of member IDs attached to the card
+ * @param {string} [assignees] the array of maximum one ID of assignee attached to the card
* @param {string} [requestedBy] the new requestedBy field of the card
* @param {string} [assignedBy] the new assignedBy field of the card
* @param {string} [receivedAt] the new receivedAt field of the card
@@ -2152,6 +2343,25 @@ if (Meteor.isServer) {
{ $set: { members: newmembers } },
);
}
+ if (req.body.hasOwnProperty('assignees')) {
+ let newassignees = req.body.assignees;
+ if (_.isString(newassignees)) {
+ if (newassignees === '') {
+ newassignees = null;
+ } else {
+ newassignees = [newassignees];
+ }
+ }
+ Cards.direct.update(
+ {
+ _id: paramCardId,
+ listId: paramListId,
+ boardId: paramBoardId,
+ archived: false,
+ },
+ { $set: { assignees: newassignees } },
+ );
+ }
if (req.body.hasOwnProperty('swimlaneId')) {
const newParamSwimlaneId = req.body.swimlaneId;
Cards.direct.update(
@@ -2194,14 +2404,14 @@ if (Meteor.isServer) {
const paramListId = req.params.listId;
const paramCardId = req.params.cardId;
+ const card = Cards.findOne({
+ _id: paramCardId,
+ });
Cards.direct.remove({
_id: paramCardId,
listId: paramListId,
boardId: paramBoardId,
});
- const card = Cards.find({
- _id: paramCardId,
- });
cardRemover(req.body.authorId, card);
JsonRoutes.sendResult(res, {
code: 200,
diff --git a/models/checklistItems.js b/models/checklistItems.js
index e6451fbf..7f3ab095 100644
--- a/models/checklistItems.js
+++ b/models/checklistItems.js
@@ -44,6 +44,8 @@ ChecklistItems.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/checklists.js b/models/checklists.js
index f139192e..3b50cda6 100644
--- a/models/checklists.js
+++ b/models/checklists.js
@@ -35,6 +35,8 @@ Checklists.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -274,6 +276,7 @@ if (Meteor.isServer) {
* @param {string} boardId the board ID
* @param {string} cardId the card ID
* @param {string} title the title of the new checklist
+ * @param {string} [items] the list of items on the new checklist
* @return_type {_id: string}
*/
JsonRoutes.add(
@@ -289,11 +292,19 @@ if (Meteor.isServer) {
sort: 0,
});
if (id) {
- req.body.items.forEach(function(item, idx) {
+ let items = req.body.items || [];
+ if (_.isString(items)) {
+ if (items === '') {
+ items = [];
+ } else {
+ items = [items];
+ }
+ }
+ items.forEach(function(item, idx) {
ChecklistItems.insert({
cardId: paramCardId,
checklistId: id,
- title: item.title,
+ title: item,
sort: idx,
});
});
diff --git a/models/customFields.js b/models/customFields.js
index 6b5697c1..cc798b16 100644
--- a/models/customFields.js
+++ b/models/customFields.js
@@ -78,6 +78,8 @@ CustomFields.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/export.js b/models/export.js
index a69be970..cc979ce0 100644
--- a/models/export.js
+++ b/models/export.js
@@ -50,12 +50,18 @@ if (Meteor.isServer) {
});
}
+// exporter maybe is broken since Gridfs introduced, add fs and path
+
export class Exporter {
constructor(boardId) {
this._boardId = boardId;
}
build() {
+ 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,
@@ -106,7 +112,7 @@ export class Exporter {
);
result.subtaskItems.push(
...Cards.find({
- parentid: card._id,
+ parentId: card._id,
}).fetch(),
);
});
@@ -134,6 +140,11 @@ export class Exporter {
const getBase64Data = function(doc, callback) {
let buffer = new Buffer(0);
// callback has the form function (err, res) {}
+ const tmpFile = path.join(
+ os.tmpdir(),
+ `tmpexport${process.pid}${Math.random()}`,
+ );
+ const tmpWriteable = fs.createWriteStream(tmpFile);
const readStream = doc.createReadStream();
readStream.on('data', function(chunk) {
buffer = Buffer.concat([buffer, chunk]);
@@ -143,8 +154,12 @@ export class Exporter {
});
readStream.on('end', function() {
// done
+ fs.unlink(tmpFile, () => {
+ //ignored
+ });
callback(null, buffer.toString('base64'));
});
+ readStream.pipe(tmpWriteable);
};
const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
result.attachments = Attachments.find(byBoard)
diff --git a/models/integrations.js b/models/integrations.js
index 0b2e08c6..dbf53b8e 100644
--- a/models/integrations.js
+++ b/models/integrations.js
@@ -63,6 +63,8 @@ Integrations.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -88,16 +90,30 @@ Integrations.attachSchema(
},
}),
);
-
+Integrations.Const = {
+ GLOBAL_WEBHOOK_ID: '_global',
+ ONEWAY: 'outgoing-webhooks',
+ TWOWAY: 'bidirectional-webhooks',
+ get WEBHOOK_TYPES() {
+ return [this.ONEWAY, this.TWOWAY];
+ },
+};
+const permissionHelper = {
+ allow(userId, doc) {
+ const user = Users.findOne(userId);
+ const isAdmin = user && Meteor.user().isAdmin;
+ return isAdmin || allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
+ },
+};
Integrations.allow({
insert(userId, doc) {
- return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
+ return permissionHelper.allow(userId, doc);
},
update(userId, doc) {
- return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
+ return permissionHelper.allow(userId, doc);
},
remove(userId, doc) {
- return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
+ return permissionHelper.allow(userId, doc);
},
fetch: ['boardId'],
});
diff --git a/models/invitationCodes.js b/models/invitationCodes.js
index 75db5708..abb30f32 100644
--- a/models/invitationCodes.js
+++ b/models/invitationCodes.js
@@ -18,6 +18,8 @@ InvitationCodes.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/lists.js b/models/lists.js
index e57849d7..f06b15b1 100644
--- a/models/lists.js
+++ b/models/lists.js
@@ -11,6 +11,15 @@ Lists.attachSchema(
*/
type: String,
},
+ starred: {
+ /**
+ * if a list is stared
+ * then we put it on the top
+ */
+ type: Boolean,
+ optional: true,
+ defaultValue: false,
+ },
archived: {
/**
* is the list archived
@@ -45,6 +54,8 @@ Lists.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -79,10 +90,14 @@ Lists.attachSchema(
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
- if (this.isInsert || this.isUpsert || this.isUpdate) {
+ // this is redundant with updatedAt
+ /*if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
+ }*/
+ if (!this.isSet) {
+ return new Date();
}
},
},
@@ -250,6 +265,14 @@ Lists.helpers({
return this.type === 'template-list';
},
+ isStarred() {
+ return this.starred === true;
+ },
+
+ absoluteUrl() {
+ const card = Cards.findOne({ listId: this._id });
+ return card && card.absoluteUrl();
+ },
remove() {
Lists.remove({ _id: this._id });
},
@@ -259,6 +282,9 @@ Lists.mutations({
rename(title) {
return { $set: { title } };
},
+ star(enable = true) {
+ return { $set: { starred: !!enable } };
+ },
archive() {
if (this.isTemplateList()) {
diff --git a/models/org.js b/models/org.js
index ce6f377e..a24d829d 100644
--- a/models/org.js
+++ b/models/org.js
@@ -98,6 +98,8 @@ Org.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/orgUser.js b/models/orgUser.js
index b671cb41..f310fa9c 100644
--- a/models/orgUser.js
+++ b/models/orgUser.js
@@ -49,6 +49,8 @@ OrgUser.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/rules.js b/models/rules.js
index 202071fc..2e6729cc 100644
--- a/models/rules.js
+++ b/models/rules.js
@@ -27,6 +27,8 @@ Rules.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/settings.js b/models/settings.js
index 4a0359d5..8eb02c5b 100644
--- a/models/settings.js
+++ b/models/settings.js
@@ -60,6 +60,8 @@ Settings.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/swimlanes.js b/models/swimlanes.js
index 769aaed3..aa7016f7 100644
--- a/models/swimlanes.js
+++ b/models/swimlanes.js
@@ -38,6 +38,8 @@ Swimlanes.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -172,6 +174,25 @@ Swimlanes.helpers({
},
lists() {
+ //currentUser = Meteor.user();
+ //if (currentUser) {
+ // enabled = Meteor.user().hasSortBy();
+ //}
+ //return enabled ? this.newestLists() : this.draggableLists();
+ return this.draggableLists();
+ },
+ newestLists() {
+ // sorted lists from newest to the oldest, by its creation date or its cards' last modification date
+ return Lists.find(
+ {
+ boardId: this.boardId,
+ swimlaneId: { $in: [this._id, ''] },
+ archived: false,
+ },
+ { sort: { modifiedAt: -1 } },
+ );
+ },
+ draggableLists() {
return Lists.find(
{
boardId: this.boardId,
diff --git a/models/triggers.js b/models/triggers.js
index 45f5e6fc..a95b1235 100644
--- a/models/triggers.js
+++ b/models/triggers.js
@@ -12,6 +12,16 @@ Triggers.mutations({
},
});
+Triggers.before.insert((userId, doc) => {
+ doc.createdAt = new Date();
+ doc.updatedAt = doc.createdAt;
+});
+
+Triggers.before.update((userId, doc, fieldNames, modifier) => {
+ modifier.$set = modifier.$set || {};
+ modifier.$set.updatedAt = new Date();
+});
+
Triggers.allow({
insert(userId, doc) {
return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId));
diff --git a/models/unsavedEdits.js b/models/unsavedEdits.js
index 89418bfb..81331598 100644
--- a/models/unsavedEdits.js
+++ b/models/unsavedEdits.js
@@ -29,6 +29,8 @@ UnsavedEditCollection.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
diff --git a/models/users.js b/models/users.js
index 55d85e07..83a224ba 100644
--- a/models/users.js
+++ b/models/users.js
@@ -4,6 +4,16 @@ const isSandstorm =
Meteor.settings && Meteor.settings.public && Meteor.settings.public.sandstorm;
Users = Meteor.users;
+const allowedSortValues = [
+ '-modifiedAt',
+ 'modifiedAt',
+ '-title',
+ 'title',
+ '-sort',
+ 'sort',
+];
+const defaultSortBy = allowedSortValues[0];
+
/**
* A User in wekan
*/
@@ -54,6 +64,8 @@ Users.attachSchema(
autoValue() {
if (this.isInsert) {
return new Date();
+ } else if (this.isUpsert) {
+ return { $setOnInsert: new Date() };
} else {
this.unset();
}
@@ -107,6 +119,13 @@ Users.attachSchema(
type: String,
optional: true,
},
+ 'profile.showDesktopDragHandles': {
+ /**
+ * does the user want to hide system messages?
+ */
+ type: Boolean,
+ optional: true,
+ },
'profile.hiddenSystemMessages': {
/**
* does the user want to hide system messages?
@@ -182,6 +201,15 @@ Users.attachSchema(
'board-view-cal',
],
},
+ 'profile.listSortBy': {
+ /**
+ * default sort list for user
+ */
+ type: String,
+ optional: true,
+ defaultValue: defaultSortBy,
+ allowedValues: allowedSortValues,
+ },
'profile.templatesBoardId': {
/**
* Reference to the templates board
@@ -356,6 +384,31 @@ Users.helpers({
return _.contains(invitedBoards, boardId);
},
+ _getListSortBy() {
+ const profile = this.profile || {};
+ const sortBy = profile.listSortBy || defaultSortBy;
+ const keyPattern = /^(-{0,1})(.*$)/;
+ const ret = [];
+ if (keyPattern.exec(sortBy)) {
+ ret[0] = RegExp.$2;
+ ret[1] = RegExp.$1 ? -1 : 1;
+ }
+ return ret;
+ },
+ hasSortBy() {
+ // if use doesn't have dragHandle, then we can let user to choose sort list by different order
+ return !this.hasShowDesktopDragHandles();
+ },
+ getListSortBy() {
+ return this._getListSortBy()[0];
+ },
+ getListSortTypes() {
+ return allowedSortValues;
+ },
+ getListSortByDirection() {
+ return this._getListSortBy()[1];
+ },
+
hasTag(tag) {
const { tags = [] } = this.profile || {};
return _.contains(tags, tag);
@@ -366,6 +419,11 @@ Users.helpers({
return _.contains(notifications, activityId);
},
+ hasShowDesktopDragHandles() {
+ const profile = this.profile || {};
+ return profile.showDesktopDragHandles || false;
+ },
+
hasHiddenSystemMessages() {
const profile = this.profile || {};
return profile.hiddenSystemMessages || false;
@@ -471,6 +529,21 @@ Users.mutations({
else this.addTag(tag);
},
+ setListSortBy(value) {
+ return {
+ $set: {
+ 'profile.listSortBy': value,
+ },
+ };
+ },
+ toggleDesktopHandles(value = false) {
+ return {
+ $set: {
+ 'profile.showDesktopDragHandles': !value,
+ },
+ };
+ },
+
toggleSystem(value = false) {
return {
$set: {
@@ -539,6 +612,7 @@ Users.mutations({
Meteor.methods({
setUsername(username, userId) {
check(username, String);
+ check(userId, String);
const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
@@ -546,6 +620,14 @@ Meteor.methods({
Users.update(userId, { $set: { username } });
}
},
+ setListSortBy(value) {
+ check(value, String);
+ Meteor.user().setListSortBy(value);
+ },
+ toggleDesktopDragHandles() {
+ const user = Meteor.user();
+ user.toggleDesktopHandles(user.hasShowDesktopDragHandles());
+ },
toggleSystemMessages() {
const user = Meteor.user();
user.toggleSystem(user.hasHiddenSystemMessages());
@@ -773,6 +855,9 @@ if (Meteor.isServer) {
if (Meteor.isServer) {
// Let mongoDB ensure username unicity
Meteor.startup(() => {
+ allowedSortValues.forEach(value => {
+ Lists._collection._ensureIndex(value);
+ });
Users._collection._ensureIndex({ modifiedAt: -1 });
Users._collection._ensureIndex(
{