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

@ -1,5 +1,7 @@
Meteor.startup(() => {
if (process.env.CARD_OPENED_WEBHOOK_ENABLED) {
if (process.env.CARD_OPENED_WEBHOOK_ENABLED === 'true') {
Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED = true;
} else {
Meteor.settings.public.CARD_OPENED_WEBHOOK_ENABLED = false;
}
});

View file

@ -1034,10 +1034,13 @@ Migrations.add('add-description-text-allowed', () => {
);
});
Migrations.add('fix-incorrect-dates', () => {
cas = CFSAttachments.find();
console.log('cas', cas);
});
Migrations.add('add-sort-field-to-boards', () => {
Boards.find().forEach((board, index) => {
if (!board.hasOwnProperty('sort')) {
Boards.direct.update(board._id, { $set: { sort: index } }, noValidate);
}
});
});
import { MongoInternals } from 'meteor/mongo';

View file

@ -1,192 +1,199 @@
const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
HTTP.post(url, options, (err, res) => {
if (err) {
resolve(null, err.response);
} else {
resolve(null, res);
}
if (Meteor.isServer) {
const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
HTTP.post(url, options, (err, res) => {
if (err) {
resolve(null, err.response);
} else {
resolve(null, res);
}
});
});
});
const Lock = {
_lock: {},
_timer: {},
echoDelay: 500, // echo should be happening much faster
normalDelay: 1e3, // normally user typed comment will be much slower
ECHO: 2,
NORMAL: 1,
NULL: 0,
has(id, value) {
const existing = this._lock[id];
let ret = this.NULL;
if (existing) {
ret = existing === value ? this.ECHO : this.NORMAL;
}
return ret;
},
clear(id, delay) {
const previous = this._timer[id];
if (previous) {
Meteor.clearTimeout(previous);
}
this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
},
set(id, value) {
const state = this.has(id, value);
let delay = this.normalDelay;
if (state === this.ECHO) {
delay = this.echoDelay;
}
if (!value) {
// user commented, we set a lock
value = 1;
}
this._lock[id] = value;
this.clear(id, delay); // always auto reset the locker after delay
},
unset(id) {
delete this._lock[id];
},
};
const Lock = {
_lock: {},
_timer: {},
echoDelay: 500, // echo should be happening much faster
normalDelay: 1e3, // normally user typed comment will be much slower
ECHO: 2,
NORMAL: 1,
NULL: 0,
has(id, value) {
const existing = this._lock[id];
let ret = this.NULL;
if (existing) {
ret = existing === value ? this.ECHO : this.NORMAL;
}
return ret;
},
clear(id, delay) {
const previous = this._timer[id];
if (previous) {
Meteor.clearTimeout(previous);
}
this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
},
set(id, value) {
const state = this.has(id, value);
let delay = this.normalDelay;
if (state === this.ECHO) {
delay = this.echoDelay;
}
if (!value) {
// user commented, we set a lock
value = 1;
}
this._lock[id] = value;
this.clear(id, delay); // always auto reset the locker after delay
},
unset(id) {
delete this._lock[id];
},
};
const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
'cardId',
'listId',
'oldListId',
'boardId',
'comment',
'user',
'card',
'commentId',
'swimlaneId',
];
const responseFunc = data => {
const paramCommentId = data.commentId;
const paramCardId = data.cardId;
const paramBoardId = data.boardId;
const newComment = data.comment;
if (paramCardId && paramBoardId && newComment) {
// only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data
const comment = CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
});
const board = Boards.findOne(paramBoardId);
const card = Cards.findOne(paramCardId);
if (board && card) {
if (comment) {
Lock.set(comment._id, newComment);
CardComments.direct.update(comment._id, {
$set: {
const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
'cardId',
'listId',
'oldListId',
'boardId',
'comment',
'user',
'card',
'commentId',
'swimlaneId',
];
const responseFunc = data => {
const paramCommentId = data.commentId;
const paramCardId = data.cardId;
const paramBoardId = data.boardId;
const newComment = data.comment;
if (paramCardId && paramBoardId && newComment) {
// only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data
const comment = CardComments.findOne({
_id: paramCommentId,
cardId: paramCardId,
boardId: paramBoardId,
});
const board = Boards.findOne(paramBoardId);
const card = Cards.findOne(paramCardId);
if (board && card) {
if (comment) {
Lock.set(comment._id, newComment);
CardComments.direct.update(comment._id, {
$set: {
text: newComment,
},
});
}
} else {
const userId = data.userId;
if (userId) {
const inserted = CardComments.direct.insert({
text: newComment,
},
});
}
} else {
const userId = data.userId;
if (userId) {
const inserted = CardComments.direct.insert({
text: newComment,
userId,
cardId,
boardId,
});
Lock.set(inserted._id, newComment);
}
}
}
};
Meteor.methods({
outgoingWebhooks(integration, description, params) {
check(integration, Object);
check(description, String);
check(params, Object);
this.unblock();
// label activity did not work yet, see wekan/models/activities.js
const quoteParams = _.clone(params);
const clonedParams = _.clone(params);
[
'card',
'list',
'oldList',
'board',
'oldBoard',
'comment',
'checklist',
'swimlane',
'oldSwimlane',
'label',
'attachment',
].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
});
const userId = params.userId ? params.userId : integrations[0].userId;
const user = Users.findOne(userId);
const text = `${params.user} ${TAPi18n.__(
description,
quoteParams,
user.getLanguage(),
)}\n${params.url}`;
if (text.length === 0) return;
const value = {
text: `${text}`,
};
webhooksAtbts.forEach(key => {
if (params[key]) value[key] = params[key];
});
value.description = description;
//integrations.forEach(integration => {
const is2way = integration.type === Integrations.Const.TWOWAY;
const token = integration.token || '';
const headers = {
'Content-Type': 'application/json',
};
if (token) headers['X-Wekan-Token'] = token;
const options = {
headers,
data: is2way ? { description, ...clonedParams } : value,
};
const url = integration.url;
if (is2way) {
const cid = params.commentId;
const comment = params.comment;
const lockState = cid && Lock.has(cid, comment);
if (cid && lockState !== Lock.NULL) {
// it's a comment and there is a previous lock
return;
} else if (cid) {
Lock.set(cid, comment); // set a lock here
}
}
const response = postCatchError(url, options);
if (
response &&
response.statusCode &&
response.statusCode >= 200 &&
response.statusCode < 300
) {
if (is2way) {
const data = response.data; // only an JSON encoded response will be actioned
if (data) {
try {
responseFunc(data);
} catch (e) {
throw new Meteor.Error('error-process-data');
}
userId,
cardId,
boardId,
});
Lock.set(inserted._id, newComment);
}
}
return response; // eslint-disable-line consistent-return
} else {
throw new Meteor.Error('error-invalid-webhook-response');
}
//});
},
});
};
Meteor.methods({
outgoingWebhooks(integration, description, params) {
if (Meteor.user()) {
check(integration, Object);
check(description, String);
check(params, Object);
this.unblock();
// label activity did not work yet, see wekan/models/activities.js
const quoteParams = _.clone(params);
const clonedParams = _.clone(params);
[
'card',
'list',
'oldList',
'board',
'oldBoard',
'comment',
'checklist',
'swimlane',
'oldSwimlane',
'label',
'attachment',
].forEach(key => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
});
const userId = params.userId ? params.userId : integrations[0].userId;
const user = Users.findOne(userId);
const text = `${params.user} ${TAPi18n.__(
description,
quoteParams,
user.getLanguage(),
)}\n${params.url}`;
if (text.length === 0) return;
const value = {
text: `${text}`,
};
webhooksAtbts.forEach(key => {
if (params[key]) value[key] = params[key];
});
value.description = description;
//integrations.forEach(integration => {
const is2way = integration.type === Integrations.Const.TWOWAY;
const token = integration.token || '';
const headers = {
'Content-Type': 'application/json',
};
if (token) headers['X-Wekan-Token'] = token;
const options = {
headers,
data: is2way ? { description, ...clonedParams } : value,
};
if (!Integrations.findOne({ url: integration.url })) return;
const url = integration.url;
if (is2way) {
const cid = params.commentId;
const comment = params.comment;
const lockState = cid && Lock.has(cid, comment);
if (cid && lockState !== Lock.NULL) {
// it's a comment and there is a previous lock
return;
} else if (cid) {
Lock.set(cid, comment); // set a lock here
}
}
const response = postCatchError(url, options);
if (
response &&
response.statusCode &&
response.statusCode >= 200 &&
response.statusCode < 300
) {
if (is2way) {
const data = response.data; // only an JSON encoded response will be actioned
if (data) {
try {
responseFunc(data);
} catch (e) {
throw new Meteor.Error('error-process-data');
}
}
}
return response; // eslint-disable-line consistent-return
} else {
throw new Meteor.Error('error-invalid-webhook-response');
}
}
},
});
}

View file

@ -1,9 +1,5 @@
Meteor.startup(() => {
// XXX: add activity id to profile.notifications,
// it can be displayed and rendered on web or mobile UI
// will uncomment the following code once UI implemented
//
// Notifications.subscribe('profile', (user, title, description, params) => {
// user.addNotification(params.activityId);
// });
Notifications.subscribe('profile', (user, title, description, params) => {
user.addNotification(params.activityId);
});
});

View file

@ -18,7 +18,7 @@ Meteor.publish('boards', function() {
archived: false,
$or: [
{
_id: { $in: starredBoards },
// _id: { $in: starredBoards }, // Commented out, to get a list of all public boards
permission: 'public',
},
{ members: { $elemMatch: { userId, isActive: true } } },
@ -35,7 +35,9 @@ Meteor.publish('boards', function() {
members: 1,
permission: 1,
type: 1,
sort: 1,
},
sort: { sort: 1 /* boards default sorting */ },
},
);
});
@ -61,6 +63,7 @@ Meteor.publish('archivedBoards', function() {
slug: 1,
title: 1,
},
sort: { sort: 1 /* boards default sorting */ },
},
);
});
@ -90,7 +93,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
$or,
// Sort required to ensure oplog usage
},
{ limit: 1, sort: { _id: 1 } },
{ limit: 1, sort: { sort: 1 /* boards default sorting */ } },
),
function(boardId, board) {
this.cursor(Lists.find({ boardId, archived: isArchived }));
@ -192,6 +195,7 @@ Meteor.publishRelations('board', function(boardId, isArchived) {
username: 1,
'profile.fullname': 1,
'profile.avatarUrl': 1,
'profile.initials': 1,
},
},
),

View file

@ -0,0 +1,101 @@
// We use these when displaying notifications in the notificationsDrawer
// gets all activities associated with the current user
Meteor.publish('notificationActivities', () => {
return activities();
});
// gets all attachments associated with activities associated with the current user
Meteor.publish('notificationAttachments', function() {
return Attachments.find({
_id: {
$in: activities()
.map(v => v.attachmentId)
.filter(v => !!v),
},
});
});
// gets all cards associated with activities associated with the current user
Meteor.publish('notificationCards', function() {
return Cards.find({
_id: {
$in: activities()
.map(v => v.cardId)
.filter(v => !!v),
},
});
});
// gets all checklistItems associated with activities associated with the current user
Meteor.publish('notificationChecklistItems', function() {
return ChecklistItems.find({
_id: {
$in: activities()
.map(v => v.checklistItemId)
.filter(v => !!v),
},
});
});
// gets all checklists associated with activities associated with the current user
Meteor.publish('notificationChecklists', function() {
return Checklists.find({
_id: {
$in: activities()
.map(v => v.checklistId)
.filter(v => !!v),
},
});
});
// gets all comments associated with activities associated with the current user
Meteor.publish('notificationComments', function() {
return CardComments.find({
_id: {
$in: activities()
.map(v => v.commentId)
.filter(v => !!v),
},
});
});
// gets all lists associated with activities associated with the current user
Meteor.publish('notificationLists', function() {
return Lists.find({
_id: {
$in: activities()
.map(v => v.listId)
.filter(v => !!v),
},
});
});
// gets all swimlanes associated with activities associated with the current user
Meteor.publish('notificationSwimlanes', function() {
return Swimlanes.find({
_id: {
$in: activities()
.map(v => v.swimlaneId)
.filter(v => !!v),
},
});
});
// gets all users associated with activities associated with the current user
Meteor.publish('notificationUsers', function() {
return Users.find({
_id: {
$in: activities()
.map(v => v.userId)
.filter(v => !!v),
},
});
});
function activities() {
const notifications = Meteor.user().profile.notifications || [];
return Activities.find({
_id: { $in: notifications.map(v => v.activity) },
});
}

View file

@ -6,6 +6,7 @@ Meteor.publish('user-miniprofile', function(userId) {
username: 1,
'profile.fullname': 1,
'profile.avatarUrl': 1,
'profile.initials': 1,
},
});
});

15
server/scroll.js Normal file
View file

@ -0,0 +1,15 @@
Meteor.startup(() => {
// Mouse Scroll Intertia, issue #2949. Integer.
if (process.env.SCROLLINERTIA !== '0') {
Meteor.settings.public.SCROLLINERTIA = process.env.SCROLLINERTIA;
} else {
Meteor.settings.public.SCROLLINERTIA = 0;
}
// Mouse Scroll Amount, issue #2949. "auto" or Integer.
if (process.env.SCROLLAMOUNT !== 'auto') {
Meteor.settings.public.SCROLLAMOUNT = process.env.SCROLLAMOUNT;
} else {
Meteor.settings.public.SCROLLAMOUNT = 'auto';
}
});

View file

@ -1,68 +1,76 @@
import { MongoInternals } from 'meteor/mongo';
Meteor.methods({
getStatistics() {
const os = require('os');
const pjson = require('/package.json');
const statistics = {};
let wekanVersion = pjson.version;
wekanVersion = wekanVersion.replace('v', '');
statistics.version = wekanVersion;
statistics.os = {
type: os.type(),
platform: os.platform(),
arch: os.arch(),
release: os.release(),
uptime: os.uptime(),
loadavg: os.loadavg(),
totalmem: os.totalmem(),
freemem: os.freemem(),
cpus: os.cpus(),
};
let nodeVersion = process.version;
nodeVersion = nodeVersion.replace('v', '');
statistics.process = {
nodeVersion,
pid: process.pid,
uptime: process.uptime(),
};
// Remove beginning of Meteor release text METEOR@
let meteorVersion = Meteor.release;
meteorVersion = meteorVersion.replace('METEOR@', '');
statistics.meteor = {
meteorVersion,
};
// Thanks to RocketChat for MongoDB version detection !
// https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js
let mongoVersion;
let mongoStorageEngine;
let mongoOplogEnabled;
try {
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
oplogEnabled = Boolean(
mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
);
const { version, storageEngine } = Promise.await(
mongo.db.command({ serverStatus: 1 }),
);
mongoVersion = version;
mongoStorageEngine = storageEngine.name;
mongoOplogEnabled = oplogEnabled;
} catch (e) {
try {
const { version } = Promise.await(mongo.db.command({ buildinfo: 1 }));
mongoVersion = version;
mongoStorageEngine = 'unknown';
} catch (e) {
mongoVersion = 'unknown';
mongoStorageEngine = 'unknown';
if (Meteor.isServer) {
Meteor.methods({
getStatistics() {
if (Meteor.user() && Meteor.user().isAdmin) {
const os = require('os');
const pjson = require('/package.json');
const statistics = {};
let wekanVersion = pjson.version;
wekanVersion = wekanVersion.replace('v', '');
statistics.version = wekanVersion;
statistics.os = {
type: os.type(),
platform: os.platform(),
arch: os.arch(),
release: os.release(),
uptime: os.uptime(),
loadavg: os.loadavg(),
totalmem: os.totalmem(),
freemem: os.freemem(),
cpus: os.cpus(),
};
let nodeVersion = process.version;
nodeVersion = nodeVersion.replace('v', '');
statistics.process = {
nodeVersion,
pid: process.pid,
uptime: process.uptime(),
};
// Remove beginning of Meteor release text METEOR@
let meteorVersion = Meteor.release;
meteorVersion = meteorVersion.replace('METEOR@', '');
statistics.meteor = {
meteorVersion,
};
// Thanks to RocketChat for MongoDB version detection !
// https://github.com/RocketChat/Rocket.Chat/blob/develop/app/utils/server/functions/getMongoInfo.js
let mongoVersion;
let mongoStorageEngine;
let mongoOplogEnabled;
try {
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
oplogEnabled = Boolean(
mongo._oplogHandle && mongo._oplogHandle.onOplogEntry,
);
const { version, storageEngine } = Promise.await(
mongo.db.command({ serverStatus: 1 }),
);
mongoVersion = version;
mongoStorageEngine = storageEngine.name;
mongoOplogEnabled = oplogEnabled;
} catch (e) {
try {
const { version } = Promise.await(
mongo.db.command({ buildinfo: 1 }),
);
mongoVersion = version;
mongoStorageEngine = 'unknown';
} catch (e) {
mongoVersion = 'unknown';
mongoStorageEngine = 'unknown';
}
}
statistics.mongo = {
mongoVersion,
mongoStorageEngine,
mongoOplogEnabled,
};
return statistics;
} else {
return false;
}
}
statistics.mongo = {
mongoVersion,
mongoStorageEngine,
mongoOplogEnabled,
};
return statistics;
},
});
},
});
}