Skip to content

Commit 6bc0d05

Browse files
authored
Merge pull request #12 from umbraco/feature/zapier
Feature/zapier
2 parents 0580218 + 779a19b commit 6bc0d05

22 files changed

+821
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.mb0 {
2+
margin-bottom: 0px !important;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<div ng-controller="Umbraco.Cms.Integrations.Automation.Zapier.ZapConfigController as vm">
2+
<umb-box>
3+
<umb-box-header title="Content Properties"></umb-box-header>
4+
<umb-box-content>
5+
<div>
6+
<p>
7+
<a href="https://zapier.com/">Zapier</a> is an online platform that helps you automate workflows by connecting your apps and services you use.
8+
This allows you to automate tasks without having to build this integration yourself.
9+
When an event happens in one app, Zapier can tell another app to perform (or do) a particular action - no code necessary.
10+
</p>
11+
<p>
12+
The heart of any automation boils down to this simple command: <b>WHEN</b> <span>this happens</span> <b>THEN</b> <span>do that</span>.
13+
</p>
14+
<p>
15+
A Zap is an automated workflow that tells your apps to follow this simple command: "When this happens, do that."
16+
Every Zap has a trigger and one or more actions. A trigger is an event that starts a Zap, and an action is what your Zap does for you.
17+
</p>
18+
<p>
19+
Zap triggers use webhooks to execute the actions. Webhooks are automated messages sent from apps when something happens.
20+
</p>
21+
<p>
22+
You can initiate your automation when a content item of a particular document type is published in Umbraco.
23+
</p>
24+
<p>
25+
Using the filters below, map content items with Zap triggered webhooks. This will:
26+
</p>
27+
<ul>
28+
<li>Enable Zap invocations when content of the specified document type is published.</li>
29+
<li>Trigger a sample request using the webhook URL to review when creating the Zap trigger.</li>
30+
</ul>
31+
</div>
32+
<div class="mt3">
33+
<input id="inWebHookUrl" type="text" ng-model="vm.webHookUrl" class="w-20 mb0" placeholder="WebHook URL" no-dirty-check />
34+
<select id="selContentTypes" ng-model="vm.selectedContentType" class="mb0" no-dirty-check>
35+
<option value="">Please select a content type</option>
36+
<option ng-repeat="item in vm.contentTypes" value="{{ item.name }}">{{ item.name }}</option>
37+
</select>
38+
<umb-button action="vm.onAdd()"
39+
type="button"
40+
button-style="primary"
41+
label="Add">
42+
</umb-button>
43+
</div>
44+
</umb-box-content>
45+
</umb-box>
46+
<umb-box>
47+
<umb-box-header title="Registered Webhooks"></umb-box-header>
48+
<umb-content>
49+
<div class="mt2">
50+
51+
<umb-load-indicator ng-show="vm.loading"></umb-load-indicator>
52+
53+
<div class="umb-table" ng-if="vm.contentConfigs">
54+
<!-- Listviews head section -->
55+
<div class="umb-table-head">
56+
<div class="umb-table-row">
57+
<div class="umb-table-cell"></div>
58+
<div class="umb-table-cell umb-table__name">
59+
<a class="umb-table-head__link" href="#" prevent-default>
60+
<span>Content Type Name</span>
61+
</a>
62+
</div>
63+
<div class="umb-table-cell">
64+
<a class="umb-table-head__link" href="#" prevent-default>
65+
<span>WebHook URL</span>
66+
</a>
67+
</div>
68+
<div class="umb-table-cell"></div>
69+
</div>
70+
</div>
71+
72+
<!-- Listview body section -->
73+
<div class="umb-table-body">
74+
<div class="umb-table-row"
75+
ng-repeat="row in vm.contentConfigs track by $index">
76+
<div class="umb-table-cell"></div>
77+
<div class="umb-table-cell umb-table__name">
78+
<span ng-bind="row.contentTypeName"></span>
79+
</div>
80+
<div class="umb-table-cell">
81+
<span ng-bind="row.webHookUrl"></span>
82+
</div>
83+
<div class="umb-table-cell">
84+
<umb-button action="vm.onTrigger(row.contentTypeName, row.webHookUrl)"
85+
label="Trigger Webhook"
86+
type="button"
87+
button-style="info">
88+
</umb-button>
89+
<umb-button action="row.showDeletePrompt = true"
90+
label="Delete"
91+
type="button"
92+
button-style="danger">
93+
</umb-button>
94+
<umb-confirm-action ng-if="row.showDeletePrompt"
95+
direction="right"
96+
on-confirm="vm.onDelete(row.id)"
97+
on-cancel="row.showDeletePrompt = false">
98+
</umb-confirm-action>
99+
</div>
100+
</div>
101+
</div>
102+
</div>
103+
</div>
104+
</umb-content>
105+
</umb-box>
106+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
function zapierValidationService() {
2+
3+
const resources = {
4+
WebHookEmpty: "WebHook Url is required.",
5+
WebHookUrlInvalid: "WebHook Url format is invalid.",
6+
ContentTypeEmpty: "Content type is required."
7+
};
8+
9+
return {
10+
validateConfiguration: (webHookUrl, contentTypeAlias) => {
11+
12+
if (webHookUrl === undefined || webHookUrl.length === 0) return resources.WebHookEmpty;
13+
14+
let url;
15+
16+
try {
17+
url = new URL(webHookUrl);
18+
}
19+
catch (_) {
20+
return resources.WebHookUrlInvalid;
21+
}
22+
23+
if (!(url.protocol === "http:" || url.protocol === "https:")) return resources.WebHookUrlInvalid;
24+
25+
if (contentTypeAlias === undefined || contentTypeAlias.length === 0) return resources.ContentTypeEmpty;
26+
27+
return "";
28+
}
29+
}
30+
}
31+
32+
angular.module("umbraco.services")
33+
.service("umbracoCmsIntegrationsAutomationZapierValidationService", zapierValidationService);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
function zapierController($scope, notificationsService, umbracoCmsIntegrationsAutomationZapierResource, umbracoCmsIntegrationsAutomationZapierValidationService) {
2+
3+
var vm = this;
4+
5+
vm.loading = false;
6+
vm.contentTypes = [];
7+
vm.contentConfigs = [];
8+
9+
getContentTypes();
10+
11+
getContentConfigs();
12+
13+
vm.onAdd = function () {
14+
const validationResult =
15+
umbracoCmsIntegrationsAutomationZapierValidationService.validateConfiguration(vm.webHookUrl,
16+
vm.selectedContentType);
17+
18+
if (validationResult.length > 0) {
19+
notificationsService.warning("Zapier Content Config", validationResult);
20+
return;
21+
}
22+
23+
umbracoCmsIntegrationsAutomationZapierResource.addConfig(vm.webHookUrl, vm.selectedContentType).then(function (response) {
24+
25+
if (response.length > 0) {
26+
notificationsService.warning("Zapier Content Config", response);
27+
return;
28+
}
29+
30+
getContentTypes();
31+
32+
getContentConfigs();
33+
34+
reset();
35+
});
36+
}
37+
38+
vm.onTrigger = function (contentTypeName, webHookUrl) {
39+
40+
vm.loading = true;
41+
42+
umbracoCmsIntegrationsAutomationZapierResource.triggerWebHook(webHookUrl, contentTypeName).then(function(response) {
43+
44+
vm.loading = false;
45+
46+
if (response.length > 0)
47+
notificationsService.warning("WebHook Trigger", response);
48+
else
49+
notificationsService.success("WebHook Trigger", "WebHook triggered successfully. Please check your Zap trigger for the newly submitted request.");
50+
});
51+
}
52+
53+
vm.onDelete = function(id) {
54+
umbracoCmsIntegrationsAutomationZapierResource.deleteConfig(id).then(function () {
55+
getContentTypes();
56+
57+
getContentConfigs();
58+
});
59+
}
60+
61+
function getContentTypes() {
62+
umbracoCmsIntegrationsAutomationZapierResource.getContentTypes().then(function (response) {
63+
vm.contentTypes = response;
64+
});
65+
}
66+
67+
function getContentConfigs() {
68+
umbracoCmsIntegrationsAutomationZapierResource.getAllConfigs().then(function (response) {
69+
vm.contentConfigs = response;
70+
});
71+
}
72+
73+
function reset() {
74+
vm.webHookUrl = "";
75+
vm.selectedContentType = "";
76+
}
77+
78+
79+
}
80+
81+
angular.module("umbraco")
82+
.controller("Umbraco.Cms.Integrations.Automation.Zapier.ZapConfigController", zapierController);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
angular.module('umbraco.resources').factory('umbracoCmsIntegrationsAutomationZapierResource',
2+
function ($http, umbRequestHelper) {
3+
4+
const apiEndpoint = "backoffice/UmbracoCmsIntegrationsAutomationZapier/ZapConfig";
5+
6+
return {
7+
getContentTypes: function () {
8+
return umbRequestHelper.resourcePromise(
9+
$http.get(`${apiEndpoint}/GetContentTypes`),
10+
"Failed to get resource");
11+
},
12+
addConfig: function (webHookUrl, contentTypeName) {
13+
return umbRequestHelper.resourcePromise(
14+
$http.post(`${apiEndpoint}/Add`, { contentTypeName: contentTypeName, webHookUrl: webHookUrl }), "Failed to get resource");
15+
},
16+
getAllConfigs: function () {
17+
return umbRequestHelper.resourcePromise(
18+
$http.get(`${apiEndpoint}/GetAll`), "Failed to get resource");
19+
},
20+
triggerWebHook: function(webHookUrl, contentTypeName) {
21+
return umbRequestHelper.resourcePromise(
22+
$http.post(`${apiEndpoint}/TriggerAsync`, { contentTypeName: contentTypeName, webHookUrl: webHookUrl }), "Failed to get resource");
23+
},
24+
deleteConfig: function(id) {
25+
return umbRequestHelper.resourcePromise(
26+
$http.delete(`${apiEndpoint}/Delete?id=${id}`), "Failed to get resource");
27+
}
28+
};
29+
}
30+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<language>
3+
<area alias="dashboardTabs">
4+
<key alias="zapierDashboard">Zapier Integrations</key>
5+
</area>
6+
</language>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"javascript": [
3+
"~/App_Plugins/UmbracoCms.Integrations/Automation/Zapier/js/zapier.controller.js",
4+
"~/App_Plugins/UmbracoCms.Integrations/Automation/Zapier/js/zapier.resource.js",
5+
"~/App_Plugins/UmbracoCms.Integrations/Automation/Zapier/js/zapier-validation.service.js"
6+
],
7+
"css": [
8+
"~/App_Plugins/UmbracoCms.Integrations/Automation/Zapier/css/zapier.css"
9+
]
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Threading.Tasks;
5+
using Umbraco.Cms.Integrations.Automation.Zapier.Services;
6+
using Umbraco.Core;
7+
using Umbraco.Core.Composing;
8+
using Umbraco.Core.Events;
9+
using Umbraco.Core.Logging;
10+
using Umbraco.Core.Models;
11+
using Umbraco.Core.Services;
12+
using Umbraco.Core.Services.Implement;
13+
14+
namespace Umbraco.Cms.Integrations.Automation.Zapier.Components
15+
{
16+
17+
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
18+
public class NewContentPublishedComposer : ComponentComposer<NewContentPublishedComponent>
19+
{ }
20+
21+
public class NewContentPublishedComponent : IComponent
22+
{
23+
private readonly ZapConfigService _zapConfigService;
24+
25+
private readonly ZapierService _zapierService;
26+
27+
private readonly ILogger _logger;
28+
29+
public NewContentPublishedComponent(ZapConfigService zapConfigService, ZapierService zapierService, ILogger logger)
30+
{
31+
_zapConfigService = zapConfigService;
32+
33+
_zapierService = zapierService;
34+
35+
_logger = logger;
36+
}
37+
38+
public void Initialize()
39+
{
40+
ContentService.Published += ContentServiceOnPublished;
41+
}
42+
43+
public void Terminate()
44+
{
45+
ContentService.Published -= ContentServiceOnPublished;
46+
}
47+
48+
private void ContentServiceOnPublished(IContentService sender, ContentPublishedEventArgs e)
49+
{
50+
foreach (var node in e.PublishedEntities)
51+
{
52+
var zapContentConfig = _zapConfigService.GetByName(node.ContentType.Name);
53+
if (zapContentConfig == null) continue;
54+
55+
var content = new Dictionary<string, string>
56+
{
57+
{ Constants.Content.Name, node.Name },
58+
{ Constants.Content.PublishDate, DateTime.UtcNow.ToString() }
59+
};
60+
61+
var t = Task.Run(async () => await _zapierService.TriggerAsync(zapContentConfig.WebHookUrl, content));
62+
63+
var result = t.Result;
64+
65+
if(!string.IsNullOrEmpty(result))
66+
_logger.Error<NewContentPublishedComponent>(result);
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)