Add Feature: system timelines will be showing any modification for duat startat endat receivedat, also notification to the watchers and if card is due, watchers will be notified

This commit is contained in:
Sam X. Chen 2019-07-09 16:36:50 -04:00
parent a545f8c1a2
commit 2c44f83453
5 changed files with 848 additions and 716 deletions

View file

@ -201,6 +201,19 @@ template(name="cardActivities")
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklistItem.title
if(currentData.timeKey)
| {{{_ activityType }}}
= ' '
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
if (currentData.timeOldValue)
= ' '
| {{{_ "previous_as" }}}
= ' '
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
= ' @'
else if(currentData.timeValue)
| {{{_ activityType currentData.timeValue}}}
if($eq activityType 'addComment')
+inlinedForm(classNames='js-edit-comment')

View file

@ -165,7 +165,7 @@
"cardTemplatePopup-title": "Create template",
"cards": "Cards",
"cards-count": "Cards",
"casSignIn" : "Sign In with CAS",
"casSignIn": "Sign In with CAS",
"cardType-card": "Card",
"cardType-linkedCard": "Linked Card",
"cardType-linkedBoard": "Linked Board",
@ -593,7 +593,7 @@
"r-removed-from": "Removed from",
"r-the-board": "the board",
"r-list": "list",
"set-filter":"Set Filter",
"set-filter": "Set Filter",
"r-moved-to": "Moved to",
"r-moved-from": "Moved from",
"r-archived": "Moved to Archive",
@ -705,5 +705,21 @@
"swimlane-delete-pop": "All actions will be removed from the activity feed and you won't be able to recover the swimlane. There is no undo.",
"restore-all": "Restore all",
"delete-all": "Delete all",
"loading": "Loading, please wait."
"loading": "Loading, please wait.",
"previous_as": "last time was",
"act-a-dueAt": "modified due time to \nWhen: __timeValue__\nWhere: __card__\n previous due was __timeOldValue__",
"act-a-endAt": "modified ending time to __timeValue__ from (__timeOldValue__)",
"act-a-startAt": "modified starting time to __timeValue__ from (__timeOldValue__)",
"act-a-receivedAt": "modified received time to __timeValue__ from (__timeOldValue__)",
"a-dueAt": "modified due time to be",
"a-endAt": "modified ending time to be",
"a-startAt": "modified starting time to be",
"a-receivedAt": "modified received time to be",
"almostdue": "current due time %s is approaching",
"pastdue": "current due time %s is past",
"duenow": "current due time %s is today",
"act-withDue": "__card__ due reminders [__board__]",
"act-almostdue": "was reminding the current due (__timeValue__) of __card__ is approaching",
"act-pastdue": "was reminding the current due (__timeValue__) of __card__ is past",
"act-duenow": "was reminding the current due (__timeValue__) of __card__ is now"
}

View file

@ -197,6 +197,18 @@ if (Meteor.isServer) {
// params.label = label.name;
// params.labelId = activity.labelId;
//}
if (
(!activity.timeKey || activity.timeKey === 'dueAt') &&
activity.timeValue
) {
// due time reminder
title = 'act-withDue';
}
['timeValue', 'timeOldValue'].forEach(key => {
// copy time related keys & values to params
const value = activity[key];
if (value) params[key] = value;
});
if (board) {
const watchingUsers = _.pluck(
_.where(board.watchers, { level: 'watching' }),
@ -212,7 +224,6 @@ if (Meteor.isServer) {
_.intersection(participants, trackingUsers),
);
}
Notifications.getUsers(watchers).forEach(user => {
Notifications.notify(user, title, description, params);
});

View file

@ -1553,6 +1553,60 @@ function cardRemover(userId, doc) {
});
}
const findDueCards = days => {
const seekDue = ($from, $to, activityType) => {
Cards.find({
dueAt: { $gte: $from, $lt: $to },
}).forEach(card => {
const username = Users.findOne(card.userId).username;
const activity = {
userId: card.userId,
username,
activityType,
boardId: card.boardId,
cardId: card._id,
cardTitle: card.title,
listId: card.listId,
timeValue: card.dueAt,
swimlaneId: card.swimlaneId,
};
Activities.insert(activity);
});
};
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');
};
const addCronJob = _.debounce(
Meteor.bindEnvironment(function findDueCardsDebounced() {
const notifydays = parseInt(process.env.NOTIFY_DUE_DAYS, 10) || 2; // default as 2 days b4 and after
if (!(notifydays > 0 && notifydays < 15)) {
// notifying due is disabled
return;
}
const notifyitvl = process.env.NOTIFY_DUE_ITVL; //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;
const scheduler = (job => () => {
const now = new Date();
const hour = 3600 * 1e3;
if (now.getHours() === itvl) {
if (typeof job === 'function') {
job();
}
}
Meteor.setTimeout(scheduler, hour);
})(() => {
findDueCards(notifydays);
});
scheduler();
}),
500,
);
if (Meteor.isServer) {
// Cards are often fetched within a board, so we create an index to make these
// queries more efficient.
@ -1565,12 +1619,17 @@ if (Meteor.isServer) {
// With a huge database, this result in a very slow app and high CPU on the mongodb side.
// To correct it, add Index to parentId:
Cards._collection._ensureIndex({ parentId: 1 });
/*let notifydays = parseInt(process.env.NOTIFY_DUE_DAYS) || 2; // default as 2 days b4 and after
let notifyitvl = parseInt(process.env.NOTIFY_DUE_ITVL) || 3600 * 24 * 1e3; // default interval as one day
Meteor.call("findDueCards",notifydays,notifyitvl);*/
Meteor.defer(() => {
addCronJob();
});
});
Cards.after.insert((userId, doc) => {
cardCreation(userId, doc);
});
// New activity for card (un)archivage
Cards.after.update((userId, doc, fieldNames) => {
cardState(userId, doc, fieldNames);
@ -1600,6 +1659,35 @@ if (Meteor.isServer) {
cardCustomFields(userId, doc, fieldNames, modifier);
});
// Add a new activity if modify time related field like dueAt startAt etc
Cards.before.update((userId, doc, fieldNames, modifier) => {
const dla = 'dateLastActivity';
const fields = fieldNames.filter(name => name !== dla);
const timingaction = ['receivedAt', 'dueAt', 'startAt', 'endAt'];
const action = fields[0];
if (fields.length > 0 && _.contains(timingaction, action)) {
// add activities for user change these attributes
const value = modifier.$set[action];
const oldvalue = doc[action] || '';
const activityType = `a-${action}`;
const card = Cards.findOne(doc._id);
const username = Users.findOne(userId).username;
const activity = {
userId,
username,
activityType,
boardId: doc.boardId,
cardId: doc._id,
cardTitle: doc.title,
timeKey: action,
timeValue: value,
timeOldValue: oldvalue,
listId: card.listId,
swimlaneId: card.swimlaneId,
};
Activities.insert(activity);
}
});
// Remove all activities associated with a card if we remove the card
// Remove also card_comments / checklists / attachments
Cards.before.remove((userId, doc) => {

View file

@ -6,12 +6,17 @@ Meteor.startup(() => {
['card', 'list', 'oldList', 'board', 'comment'].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
});
['timeValue', 'timeOldValue'].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `${params[key]}`;
});
const lan = user.getLanguage();
const subject = TAPi18n.__(title, params, lan); // the original function has a fault, i believe the title should be used according to original author
const existing = user.getEmailBuffer().length > 0;
const text = `${existing ? `\n${subject}\n` : ''}${
params.user
} ${TAPi18n.__(description, quoteParams, lan)}\n${params.url}`;
const text = `${params.user} ${TAPi18n.__(
description,
quoteParams,
user.getLanguage(),
)}\n${params.url}`;
user.addEmailBuffer(text);
// unlike setTimeout(func, delay, args),
@ -29,12 +34,11 @@ Meteor.startup(() => {
// merge the cached content into single email and flush
const text = texts.join('\n\n');
user.clearEmailBuffer();
try {
Email.send({
to: user.emails[0].address.toLowerCase(),
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('act-activity-notify', {}, user.getLanguage()),
subject,
text,
});
} catch (e) {