Skip to content

Commit 760f130

Browse files
feat: webhook inbox (#5)
* chore: exclude inbox_id from webhook update parameters * feat: display inbox label in webhook settings * feat: add inbox selection to webhook form * feat: prevent updating inbox_id in webhook update * feat: integrate MultiselectDropdown for inbox selection in webhook form * feat: add inbox matching logic to webhook event delivery * feat: remove unused inbox input placeholder from webhook form * fix: MultiselectDropdown component submiting form * feat: refine webhook parameters for create and update actions * feat: disable URL input field when editing webhook * chore: remove unnecessary parentheses * chore: update webhook controller spec to ignore inbox_id and url updates * fix: clean up JSON formatting * fix: standardize inbox_id to inboxId in WebhookForm component * refactor: replace LabelItem with InboxName component in WebhookRow * chore: enhance MultiselectDropdown with button variant prop and update styling in WebhookForm * chore: simplify MultiselectDropdown wrapper in WebhookForm component * chore: update selectedInbox initialization to null and reorder form fields * refactor: simplify InboxName * chore: add dark variant styling for buttons in SCSS * test: add inbox filtering for webhook event triggers in WebhookListener * test: refactor webhook controller spec to use a variable for URL and improve update expectations * feat(webhook): all inboxes option * chore: remove dark variant styling for buttons in SCSS * fix: bad interaction multiselectdropdown inside label * chore: invert if * chore: rename to assignedInbox and drop inboxId * refactor(WebhookForm): restore div separating fields from buttons * test: improve description --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
1 parent 779c153 commit 760f130

File tree

10 files changed

+122
-8
lines changed

10 files changed

+122
-8
lines changed

app/controllers/api/v1/accounts/webhooks_controller.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ def index
77
end
88

99
def create
10-
@webhook = Current.account.webhooks.new(webhook_params)
10+
@webhook = Current.account.webhooks.new(webhook_create_params)
1111
@webhook.save!
1212
end
1313

1414
def update
15-
@webhook.update!(webhook_params)
15+
@webhook.update!(webhook_update_params)
1616
end
1717

1818
def destroy
@@ -22,10 +22,14 @@ def destroy
2222

2323
private
2424

25-
def webhook_params
25+
def webhook_create_params
2626
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
2727
end
2828

29+
def webhook_update_params
30+
params.require(:webhook).permit(:name, subscriptions: [])
31+
end
32+
2933
def fetch_webhook
3034
@webhook = Current.account.webhooks.find(params[:id])
3135
end

app/javascript/dashboard/i18n/locale/en/integrations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@
4848
"LABEL": "Webhook Name",
4949
"PLACEHOLDER": "Enter the name of the webhook"
5050
},
51+
"INBOX": {
52+
"LABEL": "Inbox",
53+
"TITLE": "Select the inbox",
54+
"PLACEHOLDER": "All Inboxes",
55+
"NO_RESULTS": "No inboxes found",
56+
"INPUT_PLACEHOLDER": "Search inbox"
57+
},
5158
"END_POINT": {
5259
"LABEL": "Webhook URL",
5360
"PLACEHOLDER": "Example: {webhookExampleURL}",

app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/EditWebHook.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default {
5353
:value="value"
5454
:is-submitting="uiFlags.updatingItem"
5555
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
56+
is-editing
5657
@submit="onSubmit"
5758
@cancel="onClose"
5859
/>

app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookForm.vue

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { required, url, minLength } from '@vuelidate/validators';
44
import wootConstants from 'dashboard/constants/globals';
55
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
66
import NextButton from 'dashboard/components-next/button/Button.vue';
7+
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
8+
import { useMapGetter } from 'dashboard/composables/store';
79
810
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
911
@@ -21,6 +23,7 @@ const SUPPORTED_WEBHOOK_EVENTS = [
2123
export default {
2224
components: {
2325
NextButton,
26+
MultiselectDropdown,
2427
},
2528
props: {
2629
value: {
@@ -35,10 +38,17 @@ export default {
3538
type: String,
3639
required: true,
3740
},
41+
isEditing: {
42+
type: Boolean,
43+
default: false,
44+
},
3845
},
3946
emits: ['submit', 'cancel'],
4047
setup() {
41-
return { v$: useVuelidate() };
48+
return {
49+
v$: useVuelidate(),
50+
inboxes: useMapGetter('inboxes/getInboxes'),
51+
};
4252
},
4353
validations: {
4454
url: {
@@ -53,12 +63,27 @@ export default {
5363
data() {
5464
return {
5565
url: this.value.url || '',
66+
assignedInbox: this.value.inbox || null,
5667
name: this.value.name || '',
5768
subscriptions: this.value.subscriptions || [],
5869
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
5970
};
6071
},
6172
computed: {
73+
inboxesList() {
74+
if (this.assignedInbox?.id) {
75+
return [
76+
{
77+
id: 0,
78+
name: this.$t(
79+
'INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER'
80+
),
81+
},
82+
...this.inboxes,
83+
];
84+
}
85+
return this.inboxes;
86+
},
6287
webhookURLInputPlaceholder() {
6388
return this.$t(
6489
'INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER',
@@ -75,10 +100,14 @@ export default {
75100
onSubmit() {
76101
this.$emit('submit', {
77102
url: this.url,
103+
inbox_id: this.assignedInbox?.id || null,
78104
name: this.name,
79105
subscriptions: this.subscriptions,
80106
});
81107
},
108+
onClickAssignInbox(inbox) {
109+
this.assignedInbox = inbox;
110+
},
82111
getI18nKey,
83112
},
84113
};
@@ -93,13 +122,37 @@ export default {
93122
v-model="url"
94123
type="text"
95124
name="url"
125+
:disabled="isEditing"
96126
:placeholder="webhookURLInputPlaceholder"
97127
@input="v$.url.$touch"
98128
/>
99129
<span v-if="v$.url.$error" class="message">
100130
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
101131
</span>
102132
</label>
133+
<label>
134+
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.LABEL') }}
135+
<div class="multiselect-wrap--small">
136+
<MultiselectDropdown
137+
:options="inboxesList"
138+
:selected-item="assignedInbox"
139+
:multiselector-title="
140+
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.TITLE')
141+
"
142+
:multiselector-placeholder="
143+
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER')
144+
"
145+
:no-search-result="
146+
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.NO_RESULTS')
147+
"
148+
:input-placeholder="
149+
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.INPUT_PLACEHOLDER')
150+
"
151+
:disabled="isEditing"
152+
@select="onClickAssignInbox"
153+
/>
154+
</div>
155+
</label>
103156
<label>
104157
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
105158
<input
@@ -139,7 +192,6 @@ export default {
139192
</div>
140193
</div>
141194
</div>
142-
143195
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
144196
<NextButton
145197
faded

app/javascript/dashboard/routes/dashboard/settings/integrations/Webhooks/WebhookRow.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed } from 'vue';
33
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
44
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
55
import { useI18n } from 'vue-i18n';
6+
import InboxName from 'components/widgets/InboxName.vue';
67
78
import Button from 'dashboard/components-next/button/Button.vue';
89
@@ -37,6 +38,7 @@ const subscribedEvents = computed(() => {
3738
<template>
3839
<tr>
3940
<td class="py-4 ltr:pr-4 rtl:pl-4">
41+
<InboxName v-if="webhook.inbox" class="!mx-0" :inbox="webhook.inbox" />
4042
<div
4143
class="flex gap-2 font-medium break-words text-slate-700 dark:text-slate-100"
4244
>

app/javascript/shared/components/ui/MultiselectDropdown.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const props = defineProps({
3636
type: String,
3737
default: 'Search',
3838
},
39+
disabled: {
40+
type: Boolean,
41+
default: false,
42+
},
3943
});
4044
4145
const emit = defineEmits(['select']);
@@ -66,6 +70,8 @@ const hasValue = computed(() => {
6670
showSearchDropdown ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
6771
"
6872
class="w-full !px-2"
73+
type="button"
74+
:disabled="disabled"
6975
@click="
7076
() => toggleDropdown() // ensure that the event is not passed to the button
7177
"
@@ -93,6 +99,10 @@ const hasValue = computed(() => {
9399
<div
94100
:class="{ 'dropdown-pane--open': showSearchDropdown }"
95101
class="dropdown-pane"
102+
@click="
103+
// NOTE: Without this, the dropdown does not behave as expected when used inside a <label> tag.
104+
event => event.preventDefault()
105+
"
96106
>
97107
<div class="flex items-center justify-between mb-1">
98108
<h4

app/listeners/webhook_listener.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ def inbox_updated(event)
8888
def deliver_account_webhooks(payload, account)
8989
account.webhooks.account_type.each do |webhook|
9090
next unless webhook.subscriptions.include?(payload[:event])
91+
next if payload[:inbox].present? && webhook.inbox_id.present? && webhook.inbox_id != payload[:inbox][:id]
9192

9293
WebhookJob.perform_later(webhook.url, payload)
9394
end

app/views/api/v1/accounts/webhooks/_webhook.json.jbuilder

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ if webhook.inbox
77
json.inbox do
88
json.id webhook.inbox.id
99
json.name webhook.inbox.name
10+
json.channel_type webhook.inbox.channel_type
1011
end
1112
end

spec/controllers/api/v1/accounts/webhook_controller_spec.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
require 'rails_helper'
22

33
RSpec.describe 'Webhooks API', type: :request do
4+
let(:url) { 'https://hello.com' }
45
let(:account) { create(:account) }
56
let(:inbox) { create(:inbox, account: account) }
6-
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com', name: 'My Webhook') }
7+
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: url, name: 'My Webhook') }
78
let(:administrator) { create(:user, account: account, role: :administrator) }
89
let(:agent) { create(:user, account: account, role: :agent) }
910

@@ -113,13 +114,24 @@
113114
context 'when it is an authenticated admin user' do
114115
it 'updates webhook' do
115116
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
116-
params: { url: 'https://hello.com', name: 'Another Webhook' },
117+
params: { name: 'Another Webhook' },
117118
headers: administrator.create_new_auth_token,
118119
as: :json
119120
expect(response).to have_http_status(:success)
120-
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
121121
expect(response.parsed_body['payload']['webhook']['name']).to eql 'Another Webhook'
122122
end
123+
124+
it 'ignores trying to update inbox_id and url' do
125+
new_inbox = create(:inbox, account: account)
126+
127+
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
128+
params: { url: 'https://other.url.com', inbox_id: new_inbox.id },
129+
headers: administrator.create_new_auth_token,
130+
as: :json
131+
expect(response).to have_http_status(:success)
132+
expect(response.parsed_body['payload']['webhook']['url']).to eql url
133+
expect(response.parsed_body['payload']['webhook']['inbox']['id']).to eql inbox.id
134+
end
123135
end
124136
end
125137

spec/listeners/webhook_listener_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,30 @@
1515
let!(:conversation_created_event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
1616
let!(:contact_event) { Events::Base.new(event_name, Time.zone.now, contact: contact) }
1717

18+
describe 'filter events by inbox' do
19+
let(:event_name) { :'message.created' }
20+
21+
context 'when webhook has an inbox and it matches the event inbox' do
22+
it 'triggers the webhook event' do
23+
webhook = create(:webhook, account: account, inbox: inbox)
24+
expect(WebhookJob).to receive(:perform_later)
25+
.with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
26+
27+
listener.message_created(message_created_event)
28+
end
29+
end
30+
31+
context 'when webhook has an inbox and it does not match the event inbox' do
32+
it 'does not trigger webhook' do
33+
another_inbox = create(:inbox, account: account)
34+
create(:webhook, account: account, inbox: another_inbox)
35+
expect(WebhookJob).to receive(:perform_later).exactly(0).times
36+
37+
listener.message_created(message_created_event)
38+
end
39+
end
40+
end
41+
1842
describe '#message_created' do
1943
let(:event_name) { :'message.created' }
2044

0 commit comments

Comments
 (0)