feat: add name to webhook (#4)

* feat(migration): add name column to webhook table

* feat(webhooks): add name parameter to webhook params

* feat(webhooks): add example webhook name constant and input field to form

* fix(webhooks): add webhook name label and placeholder to multiple locales in the form

* feat(webhooks): display webhook name in the UI and include it in the API response

* Revert 'fix(webhooks): add webhook name label and placeholder to multiple locales in the form'

This reverts commit e547778a1c038c934e22ceb25935f541cb09e2cd.

* test(webhooks): add tests for creating and updating webhooks with name attribute

* chore(webhooks): add name property to webhook definitions in Swagger documentation

* chore(webhooks): remove unnecessary input touch event for webhook name field

* chore(webhooks): apply review changes requested

* chore(webhooks): revert auto lint changes in commit 18ec4cafeb72fd385b70f65f1873d7cfb65216a6
This commit is contained in:
Cayo P. R. Oliveira 2025-02-14 11:43:15 -03:00 committed by gabrieljablonski
parent 846b9fb9ce
commit f6bdc40c24
13 changed files with 67 additions and 5 deletions

View File

@ -23,7 +23,7 @@ class Api::V1::Accounts::WebhooksController < Api::V1::Accounts::BaseController
private private
def webhook_params def webhook_params
params.require(:webhook).permit(:inbox_id, :url, subscriptions: []) params.require(:webhook).permit(:inbox_id, :name, :url, subscriptions: [])
end end
def fetch_webhook def fetch_webhook

View File

@ -44,6 +44,10 @@
"CONTACT_UPDATED": "Contact updated" "CONTACT_UPDATED": "Contact updated"
} }
}, },
"NAME": {
"LABEL": "Webhook Name",
"PLACEHOLDER": "Enter the name of the webhook"
},
"END_POINT": { "END_POINT": {
"LABEL": "Webhook URL", "LABEL": "Webhook URL",
"PLACEHOLDER": "Example: {webhookExampleURL}", "PLACEHOLDER": "Example: {webhookExampleURL}",

View File

@ -53,6 +53,7 @@ export default {
data() { data() {
return { return {
url: this.value.url || '', url: this.value.url || '',
name: this.value.name || '',
subscriptions: this.value.subscriptions || [], subscriptions: this.value.subscriptions || [],
supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS, supportedWebhookEvents: SUPPORTED_WEBHOOK_EVENTS,
}; };
@ -66,11 +67,15 @@ export default {
} }
); );
}, },
webhookNameInputPlaceholder() {
return this.$t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.PLACEHOLDER');
},
}, },
methods: { methods: {
onSubmit() { onSubmit() {
this.$emit('submit', { this.$emit('submit', {
url: this.url, url: this.url,
name: this.name,
subscriptions: this.subscriptions, subscriptions: this.subscriptions,
}); });
}, },
@ -95,6 +100,15 @@ export default {
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }} {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.END_POINT.ERROR') }}
</span> </span>
</label> </label>
<label>
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.NAME.LABEL') }}
<input
v-model="name"
type="text"
name="name"
:placeholder="webhookNameInputPlaceholder"
/>
</label>
<label :class="{ error: v$.url.$error }" class="mb-2"> <label :class="{ error: v$.url.$error }" class="mb-2">
{{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }} {{ $t('INTEGRATION_SETTINGS.WEBHOOK.FORM.SUBSCRIPTIONS.LABEL') }}
</label> </label>

View File

@ -37,8 +37,18 @@ const subscribedEvents = computed(() => {
<template> <template>
<tr> <tr>
<td class="py-4 ltr:pr-4 rtl:pl-4"> <td class="py-4 ltr:pr-4 rtl:pl-4">
<div class="font-medium break-words text-slate-700 dark:text-slate-100"> <div
{{ webhook.url }} class="flex gap-2 font-medium break-words text-slate-700 dark:text-slate-100"
>
<template v-if="webhook.name">
{{ webhook.name }}
<span class="text-slate-500 dark:text-slate-400">
{{ webhook.url }}
</span>
</template>
<template v-else>
{{ webhook.url }}
</template>
</div> </div>
<div class="block mt-1 text-sm text-slate-500 dark:text-slate-400"> <div class="block mt-1 text-sm text-slate-500 dark:text-slate-400">
<span class="font-medium"> <span class="font-medium">

View File

@ -3,6 +3,7 @@
# Table name: webhooks # Table name: webhooks
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# name :string
# subscriptions :jsonb # subscriptions :jsonb
# url :string # url :string
# webhook_type :integer default("account_type") # webhook_type :integer default("account_type")

View File

@ -1,4 +1,5 @@
json.id webhook.id json.id webhook.id
json.name webhook.name
json.url webhook.url json.url webhook.url
json.account_id webhook.account_id json.account_id webhook.account_id
json.subscriptions webhook.subscriptions json.subscriptions webhook.subscriptions

View File

@ -0,0 +1,5 @@
class AddNameToWebhooks < ActiveRecord::Migration[7.0]
def change
add_column :webhooks, :name, :string, null: true
end
end

View File

@ -1055,6 +1055,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_04_02_233933) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "webhook_type", default: 0 t.integer "webhook_type", default: 0
t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "contact_created", "contact_updated", "message_created", "message_updated", "webwidget_triggered"] t.jsonb "subscriptions", default: ["conversation_status_changed", "conversation_updated", "conversation_created", "contact_created", "contact_updated", "message_created", "message_updated", "webwidget_triggered"]
t.string "name"
t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true t.index ["account_id", "url"], name: "index_webhooks_on_account_id_and_url", unique: true
end end

View File

@ -3,7 +3,7 @@ require 'rails_helper'
RSpec.describe 'Webhooks API', type: :request do RSpec.describe 'Webhooks API', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com') } let(:webhook) { create(:webhook, account: account, inbox: inbox, url: 'https://hello.com', name: 'My Webhook') }
let(:administrator) { create(:user, account: account, role: :administrator) } let(:administrator) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
@ -49,6 +49,16 @@ RSpec.describe 'Webhooks API', type: :request do
expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com' expect(response.parsed_body['payload']['webhook']['url']).to eql 'https://hello.com'
end end
it 'creates webhook with name' do
post "/api/v1/accounts/#{account.id}/webhooks",
params: { account_id: account.id, inbox_id: inbox.id, url: 'https://hello.com', name: 'My Webhook' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['webhook']['name']).to eql 'My Webhook'
end
it 'throws error when invalid url provided' do it 'throws error when invalid url provided' do
post "/api/v1/accounts/#{account.id}/webhooks", post "/api/v1/accounts/#{account.id}/webhooks",
params: { account_id: account.id, inbox_id: inbox.id, url: 'javascript:alert(1)' }, params: { account_id: account.id, inbox_id: inbox.id, url: 'javascript:alert(1)' },
@ -103,11 +113,12 @@ RSpec.describe 'Webhooks API', type: :request do
context 'when it is an authenticated admin user' do context 'when it is an authenticated admin user' do
it 'updates webhook' do it 'updates webhook' do
put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}", put "/api/v1/accounts/#{account.id}/webhooks/#{webhook.id}",
params: { url: 'https://hello.com' }, params: { url: 'https://hello.com', name: 'Another Webhook' },
headers: administrator.create_new_auth_token, headers: administrator.create_new_auth_token,
as: :json as: :json
expect(response).to have_http_status(:success) 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']['url']).to eql 'https://hello.com'
expect(response.parsed_body['payload']['webhook']['name']).to eql 'Another Webhook'
end end
end end
end end

View File

@ -3,6 +3,7 @@ FactoryBot.define do
account_id { 1 } account_id { 1 }
inbox_id { 1 } inbox_id { 1 }
url { 'https://api.chatwoot.com' } url { 'https://api.chatwoot.com' }
name { 'My Webhook' }
subscriptions do subscriptions do
%w[ %w[
conversation_status_changed conversation_status_changed

View File

@ -3,6 +3,9 @@ properties:
url: url:
type: string type: string
description: The url where the events should be sent description: The url where the events should be sent
name:
type: string
description: The name of the webhook
subscriptions: subscriptions:
type: array type: array
items: items:

View File

@ -6,6 +6,9 @@ properties:
url: url:
type: string type: string
description: The url to which the events will be send description: The url to which the events will be send
name:
type: string
description: The name of the webhook
subscriptions: subscriptions:
type: array type: array
items: items:

View File

@ -6132,6 +6132,10 @@
"type": "string", "type": "string",
"description": "The url to which the events will be send" "description": "The url to which the events will be send"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {
@ -6730,6 +6734,10 @@
"type": "string", "type": "string",
"description": "The url where the events should be sent" "description": "The url where the events should be sent"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {