feat(webhooks): add name to webhook (#12641)

## Description

When working with webhooks, it's easy to lose track of which URL is
which. Adding a `name` (optional) column to the webhook model is a
straight-forward solution to make it significantly easier to identify
webhooks.

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

Model and controller specs, and also running in production over several
months without any issues.

| Before | After |
| --- | --- |
| <img width="949" height="990" alt="image copy 3"
src="https://github.com/user-attachments/assets/6b33c072-7d16-4a9c-a129-f9c0751299f5"
/> | <img width="806" height="941" alt="image"
src="https://github.com/user-attachments/assets/77f3cb3a-2eb0-41ac-95bf-d02915589690"
/> |
| <img width="1231" height="650" alt="image copy 2"
src="https://github.com/user-attachments/assets/583374af-96e0-4436-b026-4ce79b7f9321"
/> | <img width="1252" height="650" alt="image copy"
src="https://github.com/user-attachments/assets/aa81fb31-fd18-4e21-a40e-d8ab0dc76b4e"
/> |


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
This commit is contained in:
Gabriel Jablonski
2025-11-13 04:58:15 -03:00
committed by GitHub
parent 4f09c2203c
commit bdcb1934c0
17 changed files with 97 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

@@ -46,6 +46,10 @@
"CONVERSATION_TYPING_OFF": "Conversation Typing Off" "CONVERSATION_TYPING_OFF": "Conversation Typing Off"
} }
}, },
"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

@@ -55,6 +55,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,
}; };
@@ -68,11 +69,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,
}); });
}, },
@@ -97,6 +102,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,16 @@ 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-n-slate-12"> <div class="flex gap-2 font-medium break-words text-n-slate-12">
{{ webhook.url }} <template v-if="webhook.name">
{{ webhook.name }}
<span class="text-n-slate-11">
{{ webhook.url }}
</span>
</template>
<template v-else>
{{ webhook.url }}
</template>
</div> </div>
<div class="block mt-1 text-sm text-n-slate-11"> <div class="block mt-1 text-sm text-n-slate-11">
<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.1]
def change
add_column :webhooks, :name, :string, null: true
end
end

View File

@@ -1230,6 +1230,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) 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

@@ -4,6 +4,9 @@ properties:
type: string type: string
description: The url where the events should be sent description: The url where the events should be sent
example: https://example.com/webhook example: https://example.com/webhook
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

@@ -9127,6 +9127,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": {
@@ -10706,6 +10710,10 @@
"description": "The url where the events should be sent", "description": "The url where the events should be sent",
"example": "https://example.com/webhook" "example": "https://example.com/webhook"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -7634,6 +7634,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": {
@@ -9213,6 +9217,10 @@
"description": "The url where the events should be sent", "description": "The url where the events should be sent",
"example": "https://example.com/webhook" "example": "https://example.com/webhook"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -1934,6 +1934,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": {
@@ -3513,6 +3517,10 @@
"description": "The url where the events should be sent", "description": "The url where the events should be sent",
"example": "https://example.com/webhook" "example": "https://example.com/webhook"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -1349,6 +1349,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": {
@@ -2928,6 +2932,10 @@
"description": "The url where the events should be sent", "description": "The url where the events should be sent",
"example": "https://example.com/webhook" "example": "https://example.com/webhook"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {

View File

@@ -2110,6 +2110,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": {
@@ -3689,6 +3693,10 @@
"description": "The url where the events should be sent", "description": "The url where the events should be sent",
"example": "https://example.com/webhook" "example": "https://example.com/webhook"
}, },
"name": {
"type": "string",
"description": "The name of the webhook"
},
"subscriptions": { "subscriptions": {
"type": "array", "type": "array",
"items": { "items": {