diff options
-rw-r--r-- | CHANGELOG.md | 8 | ||||
-rw-r--r-- | client/components/boards/boardHeader.jade | 20 | ||||
-rw-r--r-- | client/components/boards/boardHeader.js | 27 | ||||
-rw-r--r-- | client/components/boards/boardHeader.styl | 3 | ||||
-rw-r--r-- | i18n/en.i18n.json | 2 | ||||
-rw-r--r-- | models/activities.js | 6 | ||||
-rw-r--r-- | models/integrations.js | 146 | ||||
-rw-r--r-- | server/notifications/outgoing.js | 23 |
8 files changed, 199 insertions, 36 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index b63c0a13..46a07757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Upcoming Wekan release -This release fixes the following bugs: +This release adds the following new features: + +* [Add more than one Outgoing Webhook](https://github.com/wekan/wekan/pull/1199). + +and fixes the following bugs: * [Fix errors caused by checklist items activities](https://github.com/wekan/wekan/pull/1200). -Thanks to GitHub users GhassenRjab and nztqa for contributions. +Thanks to GitHub users andresmanelli, GhassenRjab and nztqa for contributions. # v0.34 2017-08-30 Wekan release diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index 3d98322d..9539fc79 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -227,11 +227,21 @@ template(name="archiveBoardPopup") button.js-confirm.negate.full(type="submit") {{_ 'archive'}} template(name="outgoingWebhooksPopup") - form + each integrations + form.integration-form + if title + h4 {{title}} + else + h4 {{_ 'no-name'}} + label + | URL + input.js-outgoing-webhooks-url(type="text" name="url" value=url) + input(type="hidden" value=_id name="id") + input.primary.wide(type="submit" value="{{_ 'save'}}") + form.integration-form + h4 + | {{_ 'new-integration'}} label | URL - if integration.enabled - input.js-outgoing-webhooks-url(type="text" value=integration.url autofocus) - else - input.js-outgoing-webhooks-url(type="text" autofocus) + input.js-outgoing-webhooks-url(type="text" name="url" autofocus) input.primary.wide(type="submit" value="{{_ 'save'}}") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index 2ee21905..b7807ca9 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -241,39 +241,44 @@ BlazeComponent.extendComponent({ }).register('boardChangeWatchPopup'); BlazeComponent.extendComponent({ - integration() { + integrations() { const boardId = Session.get('currentBoard'); - return Integrations.findOne({ boardId: `${boardId}` }); + return Integrations.find({ boardId: `${boardId}` }).fetch(); + }, + + integration(id) { + const boardId = Session.get('currentBoard'); + return Integrations.findOne({ _id: id, boardId: `${boardId}` }); }, events() { return [{ 'submit'(evt) { evt.preventDefault(); - const url = this.find('.js-outgoing-webhooks-url').value.trim(); + const url = evt.target.url.value; const boardId = Session.get('currentBoard'); - const integration = this.integration(); - if (integration) { + let id = null; + let integration = null; + if (evt.target.id) { + id = evt.target.id.value; + integration = this.integration(id); if (url) { Integrations.update(integration._id, { $set: { - enabled: true, url: `${url}`, }, }); } else { - Integrations.update(integration._id, { - $set: { - enabled: false, - }, - }); + Integrations.remove(integration._id); } } else if (url) { Integrations.insert({ + userId: Meteor.userId(), enabled: true, type: 'outgoing-webhooks', url: `${url}`, boardId: `${boardId}`, + activities: ['all'], }); } Popup.close(); diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl new file mode 100644 index 00000000..0abdb5bd --- /dev/null +++ b/client/components/boards/boardHeader.styl @@ -0,0 +1,3 @@ +.integration-form + padding: 5px + border-bottom: 1px solid #ccc diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index b2ea9354..ebf4722b 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -368,6 +368,8 @@ "error-notAuthorized": "You are not authorized to view this page.", "outgoing-webhooks": "Outgoing Webhooks", "outgoingWebhooksPopup-title": "Outgoing Webhooks", + "new-integration": "New integration", + "no-name": "(Unknown)", "Wekan_version": "Wekan version", "Node_version": "Node version", "OS_Arch": "OS Arch", diff --git a/models/activities.js b/models/activities.js index 208dd629..c732a940 100644 --- a/models/activities.js +++ b/models/activities.js @@ -140,9 +140,9 @@ if (Meteor.isServer) { Notifications.notify(user, title, description, params); }); - const integration = Integrations.findOne({ boardId: board._id, type: 'outgoing-webhooks', enabled: true }); - if (integration) { - Meteor.call('outgoingWebhooks', integration, description, params); + const integrations = Integrations.find({ boardId: board._id, type: 'outgoing-webhooks', enabled: true, activities: { '$in': [description, 'all'] } }).fetch(); + if (integrations.length > 0) { + Meteor.call('outgoingWebhooks', integrations, description, params); } }); } diff --git a/models/integrations.js b/models/integrations.js index b9bf248f..826873ce 100644 --- a/models/integrations.js +++ b/models/integrations.js @@ -11,6 +11,11 @@ Integrations.attachSchema(new SimpleSchema({ }, type: { type: String, + defaultValue: 'outgoing-webhooks', + }, + activities: { + type: [String], + defaultValue: ['all'], }, url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex) type: String, @@ -35,11 +40,6 @@ Integrations.attachSchema(new SimpleSchema({ }, userId: { type: String, - autoValue() { // eslint-disable-line consistent-return - if (this.isInsert || this.isUpdate) { - return this.userId; - } - }, }, })); @@ -50,5 +50,141 @@ Integrations.allow({ update(userId, doc) { return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); }, + remove(userId, doc) { + return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); + }, fetch: ['boardId'], }); + +//INTEGRATIONS REST API +if (Meteor.isServer) { + // Get all integrations in board + JsonRoutes.add('GET', '/api/boards/:boardId/integrations', function(req, res, next) { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + const data = Integrations.find({ boardId: paramBoardId }, { fields: { token: 0 } }).map(function(doc) { + return doc; + }); + + JsonRoutes.sendResult(res, {code: 200, data}); + }); + + // Get a single integration in board + JsonRoutes.add('GET', '/api/boards/:boardId/integrations/:intId', function(req, res, next) { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({ _id: paramIntId, boardId: paramBoardId }, { fields: { token: 0 } }), + }); + }); + + // Create a new integration + JsonRoutes.add('POST', '/api/boards/:boardId/integrations', function(req, res, next) { + const paramBoardId = req.params.boardId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + const id = Integrations.insert({ + userId: req.userId, + boardId: paramBoardId, + url: req.body.url, + }); + + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: id, + }, + }); + }); + + // Edit integration data + JsonRoutes.add('PUT', '/api/boards/:boardId/integrations/:intId', function (req, res, next) { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + if (req.body.hasOwnProperty('enabled')) { + const newEnabled = req.body.enabled; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {enabled: newEnabled}}); + } + if (req.body.hasOwnProperty('title')) { + const newTitle = req.body.title; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {title: newTitle}}); + } + if (req.body.hasOwnProperty('url')) { + const newUrl = req.body.url; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {url: newUrl}}); + } + if (req.body.hasOwnProperty('token')) { + const newToken = req.body.token; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {token: newToken}}); + } + if (req.body.hasOwnProperty('activities')) { + const newActivities = req.body.activities; + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$set: {activities: newActivities}}); + } + + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramIntId, + }, + }); + }); + + // Delete subscribed activities + JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + const newActivities = req.body.activities; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$pullAll: {activities: newActivities}}); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), + }); + }); + + // Add subscribed activities + JsonRoutes.add('POST', '/api/boards/:boardId/integrations/:intId/activities', function (req, res, next) { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + const newActivities = req.body.activities; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.update({_id: paramIntId, boardId: paramBoardId}, + {$addToSet: {activities: { $each: newActivities}}}); + + JsonRoutes.sendResult(res, { + code: 200, + data: Integrations.findOne({_id: paramIntId, boardId: paramBoardId}, { fields: {_id: 1, activities: 1}}), + }); + }); + + // Delete integration + JsonRoutes.add('DELETE', '/api/boards/:boardId/integrations/:intId', function (req, res, next) { + const paramBoardId = req.params.boardId; + const paramIntId = req.params.intId; + Authentication.checkBoardAccess(req.userId, paramBoardId); + + Integrations.direct.remove({_id: paramIntId, boardId: paramBoardId}); + JsonRoutes.sendResult(res, { + code: 200, + data: { + _id: paramIntId, + }, + }); + }); +} diff --git a/server/notifications/outgoing.js b/server/notifications/outgoing.js index c227366e..07a0a903 100644 --- a/server/notifications/outgoing.js +++ b/server/notifications/outgoing.js @@ -9,8 +9,8 @@ const postCatchError = Meteor.wrapAsync((url, options, resolve) => { }); Meteor.methods({ - outgoingWebhooks(integration, description, params) { - check(integration, Object); + outgoingWebhooks(integrations, description, params) { + check(integrations, Array); check(description, String); check(params, Object); @@ -19,7 +19,8 @@ Meteor.methods({ if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`; }); - const user = Users.findOne(integration.userId); + 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; @@ -31,7 +32,7 @@ Meteor.methods({ ['cardId', 'listId', 'oldListId', 'boardId'].forEach((key) => { if (params[key]) value[key] = params[key]; }); - value.$description = description; + value.description = description; const options = { headers: { @@ -41,12 +42,14 @@ Meteor.methods({ data: value, }; - const response = postCatchError(integration.url, options); + integrations.forEach((integration) => { + const response = postCatchError(integration.url, options); - if (response && response.statusCode && response.statusCode === 200) { - return true; // eslint-disable-line consistent-return - } else { - throw new Meteor.Error('error-invalid-webhook-response'); - } + if (response && response.statusCode && response.statusCode === 200) { + return true; // eslint-disable-line consistent-return + } else { + throw new Meteor.Error('error-invalid-webhook-response'); + } + }); }, }); |