diff options
Diffstat (limited to 'models')
-rw-r--r-- | models/accountSettings.js | 2 | ||||
-rw-r--r-- | models/actions.js | 10 | ||||
-rw-r--r-- | models/activities.js | 70 | ||||
-rw-r--r-- | models/announcements.js | 2 | ||||
-rw-r--r-- | models/boards.js | 30 | ||||
-rw-r--r-- | models/cardComments.js | 2 | ||||
-rw-r--r-- | models/cards.js | 230 | ||||
-rw-r--r-- | models/checklistItems.js | 2 | ||||
-rw-r--r-- | models/checklists.js | 15 | ||||
-rw-r--r-- | models/customFields.js | 2 | ||||
-rw-r--r-- | models/export.js | 17 | ||||
-rw-r--r-- | models/integrations.js | 24 | ||||
-rw-r--r-- | models/invitationCodes.js | 2 | ||||
-rw-r--r-- | models/lists.js | 28 | ||||
-rw-r--r-- | models/org.js | 2 | ||||
-rw-r--r-- | models/orgUser.js | 2 | ||||
-rw-r--r-- | models/rules.js | 2 | ||||
-rw-r--r-- | models/settings.js | 2 | ||||
-rw-r--r-- | models/swimlanes.js | 21 | ||||
-rw-r--r-- | models/triggers.js | 10 | ||||
-rw-r--r-- | models/unsavedEdits.js | 2 | ||||
-rw-r--r-- | models/users.js | 85 |
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( { |