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>
This commit is contained in:
parent
52a55827c3
commit
083bc27b1a
@ -7,12 +7,12 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = Current.account.webhooks.new(webhook_params)
|
||||
@webhook = Current.account.webhooks.new(webhook_create_params)
|
||||
@webhook.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@webhook.update!(webhook_params)
|
||||
@webhook.update!(webhook_update_params)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@ -22,10 +22,14 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
def webhook_create_params
|
||||
params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
|
||||
end
|
||||
|
||||
def webhook_update_params
|
||||
params.require(:webhook).permit(:name, subscriptions: [])
|
||||
end
|
||||
|
||||
def fetch_webhook
|
||||
@webhook = Current.account.webhooks.find(params[:id])
|
||||
end
|
||||
|
||||
@ -48,6 +48,13 @@
|
||||
"LABEL": "Webhook Name",
|
||||
"PLACEHOLDER": "Enter the name of the webhook"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Inbox",
|
||||
"TITLE": "Select the inbox",
|
||||
"PLACEHOLDER": "All Inboxes",
|
||||
"NO_RESULTS": "No inboxes found",
|
||||
"INPUT_PLACEHOLDER": "Search inbox"
|
||||
},
|
||||
"END_POINT": {
|
||||
"LABEL": "Webhook URL",
|
||||
"PLACEHOLDER": "Example: {webhookExampleURL}",
|
||||
|
||||
@ -53,6 +53,7 @@ export default {
|
||||
:value="value"
|
||||
:is-submitting="uiFlags.updatingItem"
|
||||
:submit-label="$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.EDIT_SUBMIT')"
|
||||
is-editing
|
||||
@submit="onSubmit"
|
||||
@cancel="onClose"
|
||||
/>
|
||||
|
||||
@ -4,6 +4,8 @@ import { required, url, minLength } from '@vuelidate/validators';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const { EXAMPLE_WEBHOOK_URL } = wootConstants;
|
||||
|
||||
@ -21,6 +23,7 @@ const SUPPORTED_WEBHOOK_EVENTS = [
|
||||
export default {
|
||||
components: {
|
||||
NextButton,
|
||||
MultiselectDropdown,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
@ -35,10 +38,17 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['submit', 'cancel'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
return {
|
||||
v$: useVuelidate(),
|
||||
inboxes: useMapGetter('inboxes/getInboxes'),
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
url: {
|
||||
@ -53,12 +63,27 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
url: this.value.url || '',
|
||||
assignedInbox: this.value.inbox || null,
|
||||
name: this.value.name || '',
|
||||
subscriptions: this.value.subscriptions || [],
|
||||
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inboxesList() {
|
||||
if (this.assignedInbox?.id) {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
name: this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER'
|
||||
),
|
||||
},
|
||||
...this.inboxes,
|
||||
];
|
||||
}
|
||||
return this.inboxes;
|
||||
},
|
||||
webhookURLInputPlaceholder() {
|
||||
return this.$t(
|
||||
'INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.PLACEHOLDER',
|
||||
@ -75,10 +100,14 @@ export default {
|
||||
onSubmit() {
|
||||
this.$emit('submit', {
|
||||
url: this.url,
|
||||
inbox_id: this.assignedInbox?.id || null,
|
||||
name: this.name,
|
||||
subscriptions: this.subscriptions,
|
||||
});
|
||||
},
|
||||
onClickAssignInbox(inbox) {
|
||||
this.assignedInbox = inbox;
|
||||
},
|
||||
getI18nKey,
|
||||
},
|
||||
};
|
||||
@ -93,6 +122,7 @@ export default {
|
||||
v-model="url"
|
||||
type="text"
|
||||
name="url"
|
||||
:disabled="isEditing"
|
||||
:placeholder="webhookURLInputPlaceholder"
|
||||
@input="v$.url.$touch"
|
||||
/>
|
||||
@ -100,6 +130,29 @@ export default {
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.LABEL') }}
|
||||
<div class="multiselect-wrap--small">
|
||||
<MultiselectDropdown
|
||||
:options="inboxesList"
|
||||
:selected-item="assignedInbox"
|
||||
:multiselector-title="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.NO_RESULTS')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.INBOX.INPUT_PLACEHOLDER')
|
||||
"
|
||||
:disabled="isEditing"
|
||||
@select="onClickAssignInbox"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
@ -139,7 +192,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<NextButton
|
||||
faded
|
||||
|
||||
@ -3,6 +3,7 @@ import { computed } from 'vue';
|
||||
import { getI18nKey } from 'dashboard/routes/dashboard/settings/helper/settingsHelper';
|
||||
import ShowMore from 'dashboard/components/widgets/ShowMore.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import InboxName from 'components/widgets/InboxName.vue';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
@ -37,6 +38,7 @@ const subscribedEvents = computed(() => {
|
||||
<template>
|
||||
<tr>
|
||||
<td class="py-4 ltr:pr-4 rtl:pl-4">
|
||||
<InboxName v-if="webhook.inbox" class="!mx-0" :inbox="webhook.inbox" />
|
||||
<div
|
||||
class="flex gap-2 font-medium break-words text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
|
||||
@ -36,6 +36,10 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: 'Search',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
@ -66,6 +70,8 @@ const hasValue = computed(() => {
|
||||
showSearchDropdown ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'
|
||||
"
|
||||
class="w-full !px-2"
|
||||
type="button"
|
||||
:disabled="disabled"
|
||||
@click="
|
||||
() => toggleDropdown() // ensure that the event is not passed to the button
|
||||
"
|
||||
@ -93,6 +99,10 @@ const hasValue = computed(() => {
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdown }"
|
||||
class="dropdown-pane"
|
||||
@click="
|
||||
// NOTE: Without this, the dropdown does not behave as expected when used inside a <label> tag.
|
||||
event => event.preventDefault()
|
||||
"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4
|
||||
|
||||
@ -88,6 +88,7 @@ class WebhookListener < BaseListener
|
||||
def deliver_account_webhooks(payload, account)
|
||||
account.webhooks.account_type.each do |webhook|
|
||||
next unless webhook.subscriptions.include?(payload[:event])
|
||||
next if payload[:inbox].present? && webhook.inbox_id.present? && webhook.inbox_id != payload[:inbox][:id]
|
||||
|
||||
WebhookJob.perform_later(webhook.url, payload)
|
||||
end
|
||||
|
||||
@ -7,5 +7,6 @@ if webhook.inbox
|
||||
json.inbox do
|
||||
json.id webhook.inbox.id
|
||||
json.name webhook.inbox.name
|
||||
json.channel_type webhook.inbox.channel_type
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Webhooks API', type: :request do
|
||||
let(:url) { 'https://hello.com' }
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com', name: 'My Webhook') }
|
||||
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: url, name: 'My Webhook') }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
|
||||
@ -113,13 +114,24 @@ RSpec.describe 'Webhooks API', type: :request do
|
||||
context 'when it is an authenticated admin user' do
|
||||
it 'updates webhook' do
|
||||
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
|
||||
params: { url: 'https://hello.com', name: 'Another Webhook' },
|
||||
params: { name: 'Another Webhook' },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
|
||||
expect(response.parsed_body['payload']['webhook']['name']).to eql 'Another Webhook'
|
||||
end
|
||||
|
||||
it 'ignores trying to update inbox_id and url' do
|
||||
new_inbox = create(:inbox, account: account)
|
||||
|
||||
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
|
||||
params: { url: 'https://other.url.com', inbox_id: new_inbox.id },
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']['webhook']['url']).to eql url
|
||||
expect(response.parsed_body['payload']['webhook']['inbox']['id']).to eql inbox.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@ -15,6 +15,30 @@ describe WebhookListener do
|
||||
let!(:conversation_created_event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
let!(:contact_event) { Events::Base.new(event_name, Time.zone.now, contact: contact) }
|
||||
|
||||
describe 'filter events by inbox' do
|
||||
let(:event_name) { :'message.created' }
|
||||
|
||||
context 'when webhook has an inbox and it matches the event inbox' do
|
||||
it 'triggers the webhook event' do
|
||||
webhook = create(:webhook, account: account, inbox: inbox)
|
||||
expect(WebhookJob).to receive(:perform_later)
|
||||
.with(webhook.url, message.webhook_data.merge(event: 'message_created')).once
|
||||
|
||||
listener.message_created(message_created_event)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when webhook has an inbox and it does not match the event inbox' do
|
||||
it 'does not trigger webhook' do
|
||||
another_inbox = create(:inbox, account: account)
|
||||
create(:webhook, account: account, inbox: another_inbox)
|
||||
expect(WebhookJob).to receive(:perform_later).exactly(0).times
|
||||
|
||||
listener.message_created(message_created_event)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#message_created' do
|
||||
let(:event_name) { :'message.created' }
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user