Merge branch 'master' into lib-change

This commit is contained in:
Romulus Tsai 蔡仲明 2020-05-08 10:13:11 +08:00
commit c3458855bd
425 changed files with 15176 additions and 28903 deletions

View file

@ -108,7 +108,7 @@ if (Meteor.isServer) {
let participants = [];
let watchers = [];
let title = 'act-activity-notify';
let board = null;
const board = Boards.findOne(activity.boardId);
const description = `act-${activity.activityType}`;
const params = {
activityId: activity._id,
@ -122,8 +122,11 @@ if (Meteor.isServer) {
params.userId = activity.userId;
}
if (activity.boardId) {
board = activity.board();
params.board = board.title;
if (board.title.length > 0) {
params.board = board.title;
} else {
params.board = '';
}
title = 'act-withBoardTitle';
params.url = board.absoluteUrl();
params.boardId = activity.boardId;
@ -283,7 +286,10 @@ if (Meteor.isServer) {
);
}
Notifications.getUsers(watchers).forEach(user => {
Notifications.notify(user, title, description, params);
// don't notify a user of their own behavior
if (user._id !== userId) {
Notifications.notify(user, title, description, params);
}
});
const integrations = Integrations.find({

View file

@ -41,6 +41,9 @@ function onAttachmentUploaded(fileRef) {
type: 'card',
activityType: 'addAttachment',
attachmentId: fileRef._id,
// this preserves the name so that notifications can be meaningful after
// this file is removed
attachmentName: fileRef.versions.original.name,
boardId: fileRef.meta.boardId,
cardId: fileRef.meta.cardId,
listId: fileRef.meta.listId,
@ -70,6 +73,9 @@ function onAttachmentRemoving(cursor) {
type: 'card',
activityType: 'deleteAttachment',
attachmentId: file._id,
// this preserves the name so that notifications can be meaningful after
// this file is removed
attachmentName: file.versions.original.name,
boardId: meta.boardId,
cardId: meta.cardId,
listId: meta.listId,

View file

@ -493,6 +493,14 @@ Boards.attachSchema(
type: String,
defaultValue: 'board',
},
sort: {
/**
* Sort value
*/
type: Number,
decimal: true,
defaultValue: -1,
},
}),
);
@ -806,7 +814,11 @@ Boards.helpers({
if (term) {
const regex = new RegExp(term, 'i');
query.$or = [{ title: regex }, { description: regex }];
query.$or = [
{ title: regex },
{ description: regex },
{ customFields: { $elemMatch: { value: regex } } },
];
}
return Cards.find(query, projection);
@ -1182,6 +1194,10 @@ Boards.mutations({
setPresentParentTask(presentParentTask) {
return { $set: { presentParentTask } };
},
move(sortIndex) {
return { $set: { sort: sortIndex } };
},
});
function boardRemover(userId, doc) {
@ -1279,6 +1295,17 @@ if (Meteor.isServer) {
});
}
// Insert new board at last position in sort order.
Boards.before.insert((userId, doc) => {
const lastBoard = Boards.findOne(
{ sort: { $exists: true } },
{ sort: { sort: -1 } },
);
if (lastBoard && typeof lastBoard.sort !== 'undefined') {
doc.sort = lastBoard.sort + 1;
}
});
if (Meteor.isServer) {
// Let MongoDB ensure that a member is not included twice in the same board
Meteor.startup(() => {
@ -1462,7 +1489,7 @@ if (Meteor.isServer) {
'members.userId': paramUserId,
},
{
sort: ['title'],
sort: { sort: 1 /* boards default sorting */ },
},
).map(function(board) {
return {
@ -1492,7 +1519,12 @@ if (Meteor.isServer) {
Authentication.checkUserId(req.userId);
JsonRoutes.sendResult(res, {
code: 200,
data: Boards.find({ permission: 'public' }).map(function(doc) {
data: Boards.find(
{ permission: 'public' },
{
sort: { sort: 1 /* boards default sorting */ },
},
).map(function(doc) {
return {
_id: doc._id,
title: doc.title,

View file

@ -304,6 +304,42 @@ Cards.attachSchema(
optional: true,
defaultValue: '',
},
vote: {
/**
* vote object, see below
*/
type: Object,
optional: true,
},
'vote.question': {
type: String,
defaultValue: '',
},
'vote.positive': {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
'vote.negative': {
/**
* list of members (user IDs)
*/
type: [String],
optional: true,
defaultValue: [],
},
'vote.end': {
type: Date,
optional: true,
defaultValue: null,
},
'vote.public': {
type: Boolean,
defaultValue: false,
},
}),
);
@ -981,6 +1017,50 @@ Cards.helpers({
}
},
getVoteQuestion() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card && card.vote) return card.vote.question;
else return null;
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board && board.vote) return board.vote.question;
else return null;
} else if (this.vote) {
return this.vote.question;
} else {
return null;
}
},
getVotePublic() {
if (this.isLinkedCard()) {
const card = Cards.findOne({ _id: this.linkedId });
if (card && card.vote) return card.vote.public;
else return null;
} else if (this.isLinkedBoard()) {
const board = Boards.findOne({ _id: this.linkedId });
if (board && board.vote) return board.vote.public;
else return null;
} else if (this.vote) {
return this.vote.public;
} else {
return null;
}
},
voteMemberPositive() {
if (this.vote && this.vote.positive)
return Users.find({ _id: { $in: this.vote.positive } });
return [];
},
voteMemberNegative() {
if (this.vote && this.vote.negative)
return Users.find({ _id: { $in: this.vote.negative } });
return [];
},
getId() {
if (this.isLinked()) {
return this.linkedId;
@ -1397,6 +1477,58 @@ Cards.mutations({
},
};
},
setVoteQuestion(question, publicVote) {
return {
$set: {
vote: {
question,
public: publicVote,
positive: [],
negative: [],
},
},
};
},
unsetVote() {
return {
$unset: {
vote: '',
},
};
},
setVote(userId, forIt) {
switch (forIt) {
case true:
// vote for it
return {
$pull: {
'vote.negative': userId,
},
$addToSet: {
'vote.positive': userId,
},
};
case false:
// vote against
return {
$pull: {
'vote.positive': userId,
},
$addToSet: {
'vote.negative': userId,
},
};
default:
// Remove votes
return {
$pull: {
'vote.positive': userId,
'vote.negative': userId,
},
};
}
},
});
//FUNCTIONS FOR creation of Activities

View file

@ -369,6 +369,9 @@ if (Meteor.isServer) {
activityType: 'createList',
boardId: doc.boardId,
listId: doc._id,
// this preserves the name so that the activity can be useful after the
// list is deleted
title: doc.title,
});
});
@ -397,6 +400,9 @@ if (Meteor.isServer) {
activityType: 'archivedList',
listId: doc._id,
boardId: doc.boardId,
// this preserves the name so that the activity can be useful after the
// list is deleted
title: doc.title,
});
}
});

View file

@ -198,6 +198,10 @@ if (Meteor.isServer) {
return process.env.CAS_ENABLED === 'true';
}
function isApiEnabled() {
return process.env.WITH_API === 'true';
}
Meteor.methods({
sendInvitation(emails, boards) {
check(emails, [String]);
@ -314,6 +318,10 @@ if (Meteor.isServer) {
return isCasEnabled();
},
_isApiEnabled() {
return isApiEnabled();
},
// Gets all connection methods to use it in the Template
getAuthenticationsEnabled() {
return {
@ -326,6 +334,10 @@ if (Meteor.isServer) {
getDefaultAuthenticationMethod() {
return process.env.DEFAULT_AUTHENTICATION_METHOD;
},
isPasswordLoginDisabled() {
return process.env.PASSWORD_LOGIN_ENABLED === 'false';
},
});
}

View file

@ -285,6 +285,30 @@ export class TrelloCreator {
cardToCreate.members = wekanMembers;
}
}
// add vote
if (card.idMembersVoted) {
// Trello only know's positive votes
const positiveVotes = [];
card.idMembersVoted.forEach(trelloId => {
if (this.members[trelloId]) {
const wekanId = this.members[trelloId];
// we may map multiple Trello members to the same wekan user
// in which case we risk adding the same user multiple times
if (!positiveVotes.find(wId => wId === wekanId)) {
positiveVotes.push(wekanId);
}
}
return true;
});
if (positiveVotes.length > 0) {
cardToCreate.vote = {
question: cardToCreate.title,
public: true,
positive: positiveVotes,
};
}
}
// insert card
const cardId = Cards.direct.insert(cardToCreate);
// keep track of Trello id => Wekan id

View file

@ -1,3 +1,5 @@
import { SyncedCron } from 'meteor/percolate:synced-cron';
// Sandstorm context is detected using the METEOR_SETTINGS environment variable
// in the package definition.
const isSandstorm =
@ -165,7 +167,20 @@ Users.attachSchema(
/**
* enabled notifications for the user
*/
type: [String],
type: [Object],
optional: true,
},
'profile.notifications.$.activity': {
/**
* The id of the activity this notification references
*/
type: String,
},
'profile.notifications.$.read': {
/**
* the date on which this notification was read
*/
type: Date,
optional: true,
},
'profile.showCardsCountAt': {
@ -175,6 +190,13 @@ Users.attachSchema(
type: Number,
optional: true,
},
'profile.startDayOfWeek': {
/**
* startDayOfWeek field of the user
*/
type: Number,
optional: true,
},
'profile.starredBoards': {
/**
* list of starred board IDs
@ -362,8 +384,8 @@ if (Meteor.isClient) {
return board && board.hasWorker(this._id);
},
isBoardAdmin() {
const board = Boards.findOne(Session.get('currentBoard'));
isBoardAdmin(boardId = Session.get('currentBoard')) {
const board = Boards.findOne(boardId);
return board && board.hasAdmin(this._id);
},
});
@ -371,12 +393,20 @@ if (Meteor.isClient) {
Users.helpers({
boards() {
return Boards.find({ 'members.userId': this._id });
return Boards.find(
{ 'members.userId': this._id },
{ sort: { sort: 1 /* boards default sorting */ } },
);
},
starredBoards() {
const { starredBoards = [] } = this.profile || {};
return Boards.find({ archived: false, _id: { $in: starredBoards } });
return Boards.find(
{ archived: false, _id: { $in: starredBoards } },
{
sort: { sort: 1 /* boards default sorting */ },
},
);
},
hasStarred(boardId) {
@ -386,7 +416,12 @@ Users.helpers({
invitedBoards() {
const { invitedBoards = [] } = this.profile || {};
return Boards.find({ archived: false, _id: { $in: invitedBoards } });
return Boards.find(
{ archived: false, _id: { $in: invitedBoards } },
{
sort: { sort: 1 /* boards default sorting */ },
},
);
},
isInvitedTo(boardId) {
@ -429,6 +464,20 @@ Users.helpers({
return _.contains(notifications, activityId);
},
notifications() {
const { notifications = [] } = this.profile || {};
for (const index in notifications) {
if (!notifications.hasOwnProperty(index)) continue;
const notification = notifications[index];
// this preserves their db sort order for editing
notification.dbIndex = index;
notification.activity = Activities.findOne(notification.activity);
}
// this sorts them newest to oldest to match Trello's behavior
notifications.reverse();
return notifications;
},
hasShowDesktopDragHandles() {
const profile = this.profile || {};
return profile.showDesktopDragHandles || false;
@ -479,6 +528,15 @@ Users.helpers({
return profile.language || 'en';
},
getStartDayOfWeek() {
const profile = this.profile || {};
if (typeof profile.startDayOfWeek === 'undefined') {
// default is 'Monday' (1)
return 1;
}
return profile.startDayOfWeek;
},
getTemplatesBoardId() {
return (this.profile || {}).templatesBoardId;
},
@ -573,7 +631,7 @@ Users.mutations({
addNotification(activityId) {
return {
$addToSet: {
'profile.notifications': activityId,
'profile.notifications': { activity: activityId },
},
};
},
@ -581,7 +639,7 @@ Users.mutations({
removeNotification(activityId) {
return {
$pull: {
'profile.notifications': activityId,
'profile.notifications': { activity: activityId },
},
};
},
@ -610,6 +668,10 @@ Users.mutations({
return { $set: { 'profile.showCardsCountAt': limit } };
},
setStartDayOfWeek(startDay) {
return { $set: { 'profile.startDayOfWeek': startDay } };
},
setBoardView(view) {
return {
$set: {
@ -620,16 +682,6 @@ 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');
} else {
Users.update(userId, { $set: { username } });
}
},
setListSortBy(value) {
check(value, String);
Meteor.user().setListSortBy(value);
@ -650,51 +702,101 @@ Meteor.methods({
check(limit, Number);
Meteor.user().setShowCardsCountAt(limit);
},
setEmail(email, userId) {
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } },
);
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
emails: [
{
address: email,
verified: false,
},
],
},
});
}
},
setUsernameAndEmail(username, email, userId) {
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().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
changeStartDayOfWeek(startDay) {
check(startDay, Number);
Meteor.user().setStartDayOfWeek(startDay);
},
});
if (Meteor.isServer) {
Meteor.methods({
setCreateUser(fullname, username, password, isAdmin, isActive, email) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(fullname, String);
check(username, String);
check(password, String);
check(isAdmin, String);
check(isActive, String);
check(email, String);
const nUsersWithUsername = Users.find({ username }).count();
const nUsersWithEmail = Users.find({ email }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else if (nUsersWithEmail > 0) {
throw new Meteor.Error('email-already-taken');
} else {
Accounts.createUser({
fullname,
username,
password,
isAdmin,
isActive,
email: email.toLowerCase(),
from: 'admin',
});
}
}
},
setUsername(username, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(username, String);
check(userId, String);
const nUsersWithUsername = Users.find({ username }).count();
if (nUsersWithUsername > 0) {
throw new Meteor.Error('username-already-taken');
} else {
Users.update(userId, { $set: { username } });
}
}
},
setEmail(email, userId) {
if (Meteor.user() && Meteor.user().isAdmin) {
if (Array.isArray(email)) {
email = email.shift();
}
check(email, String);
const existingUser = Users.findOne(
{ 'emails.address': email },
{ fields: { _id: 1 } },
);
if (existingUser) {
throw new Meteor.Error('email-already-taken');
} else {
Users.update(userId, {
$set: {
emails: [
{
address: email,
verified: false,
},
],
},
});
}
}
},
setUsernameAndEmail(username, email, userId) {
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) {
if (Meteor.user() && Meteor.user().isAdmin) {
check(userId, String);
check(newPassword, String);
if (Meteor.user().isAdmin) {
Accounts.setPassword(userId, newPassword);
}
}
},
// we accept userId, username, email
inviteUserToBoard(username, boardId) {
check(username, String);
@ -726,8 +828,9 @@ if (Meteor.isServer) {
throw new Meteor.Error('error-user-notAllowSelf');
} else {
if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
if (Settings.findOne().disableRegistration)
if (Settings.findOne({ disableRegistration: true })) {
throw new Meteor.Error('error-user-notCreated');
}
// Set in lowercase email before creating account
const email = username.toLowerCase();
username = email.substring(0, posAt);
@ -748,6 +851,16 @@ if (Meteor.isServer) {
board.addMember(user._id);
user.addInvite(boardId);
//Check if there is a subtasks board
if (board.subtasksDefaultBoardId) {
const subBoard = Boards.findOne(board.subtasksDefaultBoardId);
//If there is, also add user to that board
if (subBoard) {
subBoard.addMember(user._id);
user.addInvite(subBoard._id);
}
}
try {
const params = {
user: user.username,
@ -862,6 +975,39 @@ if (Meteor.isServer) {
});
}
const addCronJob = _.debounce(
Meteor.bindEnvironment(function notificationCleanupDebounced() {
// passed in the removeAge has to be a number standing for the number of days after a notification is read before we remove it
const envRemoveAge =
process.env.NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE;
// default notifications will be removed 2 days after they are read
const defaultRemoveAge = 2;
const removeAge = parseInt(envRemoveAge, 10) || defaultRemoveAge;
SyncedCron.add({
name: 'notification_cleanup',
schedule: parser => parser.text('every 1 days'),
job: () => {
for (const user of Users.find()) {
if (!user.profile || !user.profile.notifications) continue;
for (const notification of user.profile.notifications) {
if (notification.read) {
const removeDate = new Date(notification.read);
removeDate.setDate(removeDate.getDate() + removeAge);
if (removeDate <= new Date()) {
user.removeNotification(notification.activity);
}
}
}
}
},
});
SyncedCron.start();
}),
500,
);
if (Meteor.isServer) {
// Let mongoDB ensure username unicity
Meteor.startup(() => {
@ -875,6 +1021,9 @@ if (Meteor.isServer) {
},
{ unique: true },
);
Meteor.defer(() => {
addCronJob();
});
});
// OLD WAY THIS CODE DID WORK: When user is last admin of board,
@ -1180,10 +1329,13 @@ if (Meteor.isServer) {
let data = Meteor.users.findOne({ _id: id });
if (data !== undefined) {
if (action === 'takeOwnership') {
data = Boards.find({
'members.userId': id,
'members.isAdmin': true,
}).map(function(board) {
data = Boards.find(
{
'members.userId': id,
'members.isAdmin': true,
},
{ sort: { sort: 1 /* boards default sorting */ } },
).map(function(board) {
if (board.hasMember(req.userId)) {
board.removeMember(req.userId);
}