Added webhook data level and events filtering

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-12-16 22:15:57 +01:00
parent 1881331ac0
commit 8bf457c592
9 changed files with 371 additions and 35 deletions
+32
View File
@@ -40,3 +40,35 @@ var Events = []string{
EVENT_CAMPAIGN_RECIPIENT_REPORTED,
EVENT_CAMPAIGN_RECIPIENT_CANCELLED,
}
// webhook event bit flags for storing selected events as int
// includes all events that call HandleWebhook (campaign.go + proxy.go)
const (
WEBHOOK_EVENT_BIT_CAMPAIGN_CLOSED = 1 << 0 // 1
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = 1 << 1 // 2
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = 1 << 2 // 4
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_READ = 1 << 3 // 8
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA = 1 << 4 // 16
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED = 1 << 5 // 32
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED = 1 << 6 // 64
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_PAGE_VISITED = 1 << 7 // 128
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED = 1 << 8 // 256
WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED = 1 << 9 // 512
)
// WEBHOOK_EVENT_ALL_BITS represents all events selected
const WEBHOOK_EVENT_ALL_BITS = 1023 // 2^10 - 1
// map event names to their bit positions
var WebhookEventToBit = map[string]int{
EVENT_CAMPAIGN_CLOSED: WEBHOOK_EVENT_BIT_CAMPAIGN_CLOSED,
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_SENT,
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED,
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_MESSAGE_READ,
EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA,
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED: WEBHOOK_EVENT_BIT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED,
}
+15 -1
View File
@@ -44,7 +44,21 @@ type Campaign struct {
IsAnonymous bool `gorm:"not null;default:false"`
IsTest bool `gorm:"not null;default:false"`
Obfuscate bool `gorm:"not null;default:false"`
WebhookIncludeData bool `gorm:"not null;default:false"`
WebhookIncludeData string `gorm:"not null;default:'full'"`
// WebhookEvents is a binary format storing selected events as bits (10 events)
// 0 = all events (default, backward compatible)
// non-zero = only selected events trigger webhooks
// bit 0 (1): campaign_closed
// bit 1 (2): campaign_recipient_message_sent
// bit 2 (4): campaign_recipient_message_failed
// bit 3 (8): campaign_recipient_message_read
// bit 4 (16): campaign_recipient_submitted_data
// bit 5 (32): campaign_recipient_evasion_page_visited
// bit 6 (64): campaign_recipient_before_page_visited
// bit 7 (128): campaign_recipient_page_visited
// bit 8 (256): campaign_recipient_after_page_visited
// bit 9 (512): campaign_recipient_deny_page_visited
WebhookEvents int `gorm:"not null;default:0"`
// has one
CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"`
+83 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"github.com/oapi-codegen/nullable"
"github.com/phishingclub/phishingclub/data"
"github.com/phishingclub/phishingclub/errs"
"github.com/phishingclub/phishingclub/utils"
"github.com/phishingclub/phishingclub/validate"
@@ -42,7 +43,8 @@ type Campaign struct {
IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"`
IsTest nullable.Nullable[bool] `json:"isTest"`
Obfuscate nullable.Nullable[bool] `json:"obfuscate"`
WebhookIncludeData nullable.Nullable[bool] `json:"webhookIncludeData"`
WebhookIncludeData nullable.Nullable[string] `json:"webhookIncludeData"`
WebhookEvents nullable.Nullable[int] `json:"webhookEvents"`
TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"`
Template *CampaignTemplate `json:"template"`
CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"`
@@ -63,6 +65,53 @@ type Campaign struct {
}
// Validate checks if the campaign has a valid state
const (
WebhookDataLevelNone = "none"
WebhookDataLevelBasic = "basic"
WebhookDataLevelFull = "full"
)
// WebhookEventsToBinary converts array of event names to bitwise int
func WebhookEventsToBinary(events []string) int {
if len(events) == 0 {
return 0 // 0 means all events
}
binary := 0
for _, event := range events {
if bit, ok := data.WebhookEventToBit[event]; ok {
binary |= bit
}
}
return binary
}
// WebhookEventsFromBinary converts bitwise int to array of event names
func WebhookEventsFromBinary(binary int) []string {
if binary == 0 {
return []string{} // empty array means all events
}
events := []string{}
for eventName, bit := range data.WebhookEventToBit {
if binary&bit != 0 {
events = append(events, eventName)
}
}
return events
}
// IsWebhookEventEnabled checks if an event should trigger webhook
func IsWebhookEventEnabled(webhookEvents int, eventName string) bool {
// 0 means all events enabled (backward compatible)
if webhookEvents == 0 {
return true
}
// check if the event bit is set
if bit, ok := data.WebhookEventToBit[eventName]; ok {
return webhookEvents&bit != 0
}
return false
}
func (c *Campaign) Validate() error {
if err := validate.NullableFieldRequired("name", c.Name); err != nil {
return err
@@ -216,6 +265,32 @@ func (c *Campaign) Validate() error {
c.NotableEventID.SetNull()
}
// validate webhookIncludeData enum
if c.WebhookIncludeData.IsSpecified() {
if level, err := c.WebhookIncludeData.Get(); err == nil {
if level != WebhookDataLevelNone &&
level != WebhookDataLevelBasic &&
level != WebhookDataLevelFull {
return validate.WrapErrorWithField(
errors.New("must be 'none', 'basic', or 'full'"),
"webhookIncludeData",
)
}
}
}
// validate webhookEvents is non-negative
if c.WebhookEvents.IsSpecified() && !c.WebhookEvents.IsNull() {
if events, err := c.WebhookEvents.Get(); err == nil {
if events < 0 {
return validate.WrapErrorWithField(
errors.New("must be non-negative"),
"webhookEvents",
)
}
}
}
return nil
}
@@ -379,11 +454,17 @@ func (c *Campaign) ToDBMap() map[string]any {
}
}
if c.WebhookIncludeData.IsSpecified() {
m["webhook_include_data"] = false
m["webhook_include_data"] = WebhookDataLevelFull
if v, err := c.WebhookIncludeData.Get(); err == nil {
m["webhook_include_data"] = v
}
}
if c.WebhookEvents.IsSpecified() {
m["webhook_events"] = 0
if v, err := c.WebhookEvents.Get(); err == nil {
m["webhook_events"] = v
}
}
if c.TemplateID.IsSpecified() {
m["campaign_template_id"] = nil
if v, err := c.TemplateID.Get(); err == nil {
+2
View File
@@ -1488,6 +1488,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
isTest := nullable.NewNullableWithValue(row.IsTest)
obfuscate := nullable.NewNullableWithValue(row.Obfuscate)
webhookIncludeData := nullable.NewNullableWithValue(row.WebhookIncludeData)
webhookEvents := nullable.NewNullableWithValue(row.WebhookEvents)
var templateID nullable.Nullable[uuid.UUID]
if row.CampaignTemplateID != nil {
templateID = nullable.NewNullableWithValue(*row.CampaignTemplateID)
@@ -1617,6 +1618,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
IsTest: isTest,
Obfuscate: obfuscate,
WebhookIncludeData: webhookIncludeData,
WebhookEvents: webhookEvents,
TemplateID: templateID,
Template: template,
RecipientGroups: recipientGroups,
+48 -11
View File
@@ -77,6 +77,14 @@ func (c *Campaign) Create(
if len(campaign.RecipientGroupIDs) == 0 {
return nil, validate.WrapErrorWithField(errors.New("no groups provided"), "Recipient Groups")
}
// set default webhookIncludeData for backward compatibility
if !campaign.WebhookIncludeData.IsSpecified() {
campaign.WebhookIncludeData.Set(model.WebhookDataLevelFull)
}
// set default webhookEvents (0 means all events) for backward compatibility
if !campaign.WebhookEvents.IsSpecified() {
campaign.WebhookEvents.Set(0)
}
// if the schedule type is scheduled, set the start time to start of day and end to the end of the last day
if campaign.ConstraintWeekDays.IsSpecified() && !campaign.ConstraintWeekDays.IsNull() {
if err := campaign.ValidateSendTimesSet(); err != nil {
@@ -1338,6 +1346,12 @@ func (c *Campaign) UpdateByID(
if v, err := incoming.WebhookIncludeData.Get(); err == nil {
current.WebhookIncludeData.Set(v)
}
if v, err := incoming.WebhookEvents.Get(); err == nil {
current.WebhookEvents.Set(v)
}
if v, err := incoming.TemplateID.Get(); err == nil {
current.TemplateID.Set(v)
}
if v, err := incoming.SortField.Get(); err == nil {
current.SortField.Set(v)
}
@@ -3149,27 +3163,50 @@ func (c *Campaign) HandleWebhook(
return errs.Wrap(err)
}
// check if campaign has webhookIncludeData enabled
// check campaign webhook data level
campaign, err := c.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{})
if err != nil {
return errs.Wrap(err)
}
// only include captured data if webhookIncludeData is enabled
var dataToSend map[string]interface{}
if campaign.WebhookIncludeData.MustGet() {
dataToSend = capturedData
// check if this event should trigger webhook notification
webhookEvents := 0
if events, err := campaign.WebhookEvents.Get(); err == nil {
webhookEvents = events
}
// 0 means all events (backward compatibility)
// otherwise check if the event bit is set
if !model.IsWebhookEventEnabled(webhookEvents, eventName) {
// event not in selected events, skip webhook
return nil
}
// determine what data to send based on webhookIncludeData level
dataLevel := model.WebhookDataLevelFull
if level, err := campaign.WebhookIncludeData.Get(); err == nil {
dataLevel = level
}
now := time.Now()
webhookReq := WebhookRequest{
Time: &now,
CampaignName: campaignName,
Event: eventName,
Data: dataToSend,
Time: &now,
Event: eventName,
}
if email != nil {
webhookReq.Email = email.String()
// apply data level filters
switch dataLevel {
case model.WebhookDataLevelNone:
// only time and event - no campaign name, no email, no data
case model.WebhookDataLevelBasic:
// include campaign name but no email or captured data
webhookReq.CampaignName = campaignName
case model.WebhookDataLevelFull:
// include everything
webhookReq.CampaignName = campaignName
webhookReq.Data = capturedData
if email != nil {
webhookReq.Email = email.String()
}
}
// the webhook is handles as a different go routine
// so we don't block the campaign handling thread
+25 -2
View File
@@ -386,10 +386,33 @@ func (w *Webhook) Send(
return data, nil
}
// WebhookRequest represents the payload sent to webhook endpoints.
// webhooks are sent based on the campaign's webhookEvents setting (stored as bitwise int):
// - 0: all events trigger webhooks (default, backward compatible)
// - non-zero: only events with their bit set trigger webhooks
//
// webhook events (10 total - events that call HandleWebhook):
// from campaign.go (4 events):
// - campaign_closed: when a campaign finishes
// - campaign_recipient_message_sent: when an email is successfully sent
// - campaign_recipient_message_failed: when an email fails to send
// - campaign_recipient_message_read: when tracking pixel is loaded
// from proxy.go (6 events):
// - campaign_recipient_submitted_data: when user submits data on phishing page
// - campaign_recipient_evasion_page_visited: when evasion page is visited
// - campaign_recipient_before_page_visited: when before page is visited
// - campaign_recipient_page_visited: when landing page is visited
// - campaign_recipient_after_page_visited: when after page is visited
// - campaign_recipient_deny_page_visited: when deny page is visited
//
// the fields included depend on the campaign's webhookIncludeData setting:
// - "none": only Time and Event are sent (maximum privacy)
// - "basic": Time, Event, and CampaignName are sent (no PII)
// - "full": all fields including Email and Data are sent (complete information)
type WebhookRequest struct {
Time *time.Time `json:"time"`
CampaignName string `json:"campaignName"`
Email string `json:"email"`
CampaignName string `json:"campaignName,omitempty"`
Email string `json:"email,omitempty"`
Event string `json:"event"`
Data map[string]interface{} `json:"data,omitempty"`
}
+8 -2
View File
@@ -512,7 +512,8 @@ export class API {
* @param {string} campaign.denyPageID uuid
* @param {string} campaign.evasionPageID uuid
* @param {string} campaign.webhookID uuid
* @param {boolean} [campaign.webhookIncludeData]
* @param {string} [campaign.webhookIncludeData]
* @param {number} [campaign.webhookEvents]
* @param {Array} [campaign.constraintWeekDays]
* @param {string} [campaign.constraintStartTime]
* @param {string} [campaign.constraintEndTime]
@@ -541,6 +542,7 @@ export class API {
evasionPageID,
webhookID,
webhookIncludeData,
webhookEvents,
constraintWeekDays,
constraintStartTime,
constraintEndTime,
@@ -568,6 +570,7 @@ export class API {
evasionPageID,
webhookID,
webhookIncludeData,
webhookEvents,
constraintWeekDays,
constraintStartTime,
constraintEndTime,
@@ -597,7 +600,8 @@ export class API {
* @param {string} campaign.denyPageID uuid
* @param {string} campaign.evasionPageID uuid
* @param {string} campaign.webhookID uuid
* @param {boolean} [campaign.webhookIncludeData]
* @param {string} [campaign.webhookIncludeData]
* @param {number} [campaign.webhookEvents]
* @param {Array} [campaign.constraintWeekDays]
* @param {string} [campaign.constraintStartTime]
* @param {string} [campaign.constraintEndTime]
@@ -626,6 +630,7 @@ export class API {
evasionPageID,
webhookID,
webhookIncludeData,
webhookEvents,
constraintWeekDays,
constraintStartTime,
constraintEndTime,
@@ -652,6 +657,7 @@ export class API {
evasionPageID,
webhookID,
webhookIncludeData,
webhookEvents,
constraintWeekDays,
constraintStartTime,
constraintEndTime,
@@ -413,18 +413,19 @@
{/if}
<!-- Selected items -->
{#if value.length > 0}
<div class="flex flex-row flex-wrap mt-4 gap-2">
<div class="flex flex-row flex-wrap mt-4 gap-2 max-h-48 overflow-y-auto">
{#each value as option}
<button
type="button"
on:click|preventDefault={removeSelectedOption}
on:keypress|preventDefault={removeSelectedOption}
data-value={option}
class="flex flex-row items-center bg-gray-100 dark:bg-gray-800/60 hover:bg-gray-200 dark:hover:bg-gray-700/80 px-2 py-1 rounded-md text-gray-900 dark:text-gray-300 text-sm transition-colors duration-200"
class="flex flex-row items-center bg-gray-100 dark:bg-gray-800/60 hover:bg-gray-200 dark:hover:bg-gray-700/80 px-2 py-1 rounded-md text-gray-900 dark:text-gray-300 text-xs transition-colors duration-200 flex-shrink-0"
aria-label="Remove {option}"
style="max-width: 280px;"
>
{option}
<img class="w-3 ml-2 pointer-events-none" src="/delete2.svg" alt="" />
<span class="truncate block" style="max-width: 250px;">{option}</span>
<img class="w-3 ml-1 pointer-events-none flex-shrink-0" src="/delete2.svg" alt="" />
</button>
{/each}
</div>
+153 -13
View File
@@ -130,6 +130,92 @@
}
];
// webhook data level descriptions:
// - none: only send event type and timestamp - no campaign info, emails, or data
// - basic: send campaign name and event, but exclude emails and submission data
// - full: include all information: emails, captured credentials, and submission data
const webhookDataLevelOptions = [
{
label: 'Minimum',
value: 'none'
},
{
label: 'Basic',
value: 'basic'
},
{
label: 'Full',
value: 'full'
}
];
// webhook event options - 0 means all events will trigger webhook (default)
const webhookEventOptions = [
'campaign_recipient_message_sent',
'campaign_recipient_message_failed',
'campaign_recipient_message_read',
'campaign_recipient_submitted_data',
'campaign_recipient_page_visited',
'campaign_recipient_before_page_visited',
'campaign_recipient_after_page_visited',
'campaign_recipient_evasion_page_visited',
'campaign_recipient_deny_page_visited',
'campaign_closed'
];
// human-readable display names for webhook events
const webhookEventDisplayNames = {
campaign_closed: 'Campaign Closed',
campaign_recipient_message_sent: 'Message Sent',
campaign_recipient_message_failed: 'Message Failed',
campaign_recipient_message_read: 'Message Read',
campaign_recipient_submitted_data: 'Submitted Data',
campaign_recipient_evasion_page_visited: 'Evasion Page Visited',
campaign_recipient_before_page_visited: 'Before Page Visited',
campaign_recipient_page_visited: 'Page Visited',
campaign_recipient_after_page_visited: 'After Page Visited',
campaign_recipient_deny_page_visited: 'Deny Page Visited'
};
// create display options array with nice names
const webhookEventDisplayOptions = webhookEventOptions.map((event) => ({
value: event,
label: webhookEventDisplayNames[event] || event
}));
// map event names to bit positions (must match backend data.WebhookEventToBit)
const webhookEventToBit = {
campaign_closed: 1 << 0, // 1
campaign_recipient_message_sent: 1 << 1, // 2
campaign_recipient_message_failed: 1 << 2, // 4
campaign_recipient_message_read: 1 << 3, // 8
campaign_recipient_submitted_data: 1 << 4, // 16
campaign_recipient_evasion_page_visited: 1 << 5, // 32
campaign_recipient_before_page_visited: 1 << 6, // 64
campaign_recipient_page_visited: 1 << 7, // 128
campaign_recipient_after_page_visited: 1 << 8, // 256
campaign_recipient_deny_page_visited: 1 << 9 // 512
};
const WEBHOOK_EVENT_ALL_BITS = 1023; // 2^10 - 1, all 10 events selected
// convert array of event names to bitwise int
const webhookEventsToBinary = (events) => {
if (!events?.length) return 0; // 0 means all events
return events.reduce((binary, event) => binary | (webhookEventToBit[event] || 0), 0);
};
// convert bitwise int to array of event names (internal values, not display names)
const webhookEventsFromBinary = (binary) => {
if (binary === 0) return [...webhookEventOptions]; // 0 means all events, return all
return webhookEventOptions.filter((event) => binary & webhookEventToBit[event]);
};
// helper to get display name for an event
const getEventDisplayName = (eventValue) => {
return webhookEventDisplayNames[eventValue] || eventValue;
};
let deleteValues = {
id: null,
name: null
@@ -292,7 +378,8 @@
obfuscate: false,
selectedCount: 0,
webhookValue: null,
webhookIncludeData: false,
webhookIncludeData: 'full',
webhookEvents: [...webhookEventOptions], // all events selected by default
jitterMin: 0,
jitterMax: 0
};
@@ -305,7 +392,6 @@
let isValidatingName = false;
let weekDaysAvailable = [];
let isDeleteAlertVisible = false;
let webhookIncludeDataCheckbox = null;
$: {
modalText = getModalText('campaign', modalMode);
@@ -675,6 +761,7 @@
constraintEndTime: contraintEndTimeUTC,
webhookID: webhookMap.byValueOrNull(formValues.webhookValue),
webhookIncludeData: formValues.webhookIncludeData,
webhookEvents: webhookEventsToBinary(formValues.webhookEvents),
jitterMin: formValues.jitterMin !== 0 ? formValues.jitterMin : null,
jitterMax: formValues.jitterMax !== 0 ? formValues.jitterMax : null
});
@@ -741,6 +828,7 @@
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
webhookID: webhookMap.byValueOrNull(formValues.webhookValue),
webhookIncludeData: formValues.webhookIncludeData,
webhookEvents: webhookEventsToBinary(formValues.webhookEvents),
jitterMin: formValues.jitterMin !== 0 ? formValues.jitterMin : null,
jitterMax: formValues.jitterMax !== 0 ? formValues.jitterMax : null
});
@@ -868,7 +956,8 @@
obfuscate: false,
selectedCount: 0,
webhookValue: null,
webhookIncludeData: false,
webhookIncludeData: 'full',
webhookEvents: [...webhookEventOptions], // all events selected by default
jitterMin: 0,
jitterMax: 0
};
@@ -976,7 +1065,8 @@
obfuscate: campaign.obfuscate || false,
template: templateMap.byKey(campaign.templateID),
webhookValue: webhookMap.byKey(campaign.webhookID),
webhookIncludeData: campaign.webhookIncludeData || false
webhookIncludeData: campaign.webhookIncludeData || 'full',
webhookEvents: webhookEventsFromBinary(campaign.webhookEvents || 0)
};
if (copyMode) {
@@ -1756,13 +1846,57 @@
</div>
{#if formValues.webhookValue}
<div class="mb-6">
<CheckboxField
bind:bindTo={webhookIncludeDataCheckbox}
<SelectSquare
bind:value={formValues.webhookIncludeData}
toolTipText="When enabled, captured data (credentials, cookies, etc.) will be included in the webhook payload"
>
Include captured data
</CheckboxField>
options={webhookDataLevelOptions}
label="Webhook Data Level"
toolTipText="Control what campaign and recipient information is sent to the webhook endpoint"
/>
</div>
<div class="mb-6 max-w-xl">
<label class="flex flex-col">
<div class="flex items-center py-2">
<p class="font-semibold text-slate-600 dark:text-gray-400">Webhook Events</p>
<div class="ml-2 text-sm text-gray-600 dark:text-gray-400">
<span class="text-xs"
>({formValues.webhookEvents.length === webhookEventOptions.length
? 'All'
: formValues.webhookEvents.length} selected)</span
>
</div>
</div>
<div class="flex flex-row flex-wrap gap-2 max-h-48 overflow-y-auto">
{#each webhookEventDisplayOptions as eventOption}
{@const isSelected = formValues.webhookEvents.includes(eventOption.value)}
<button
type="button"
on:click={() => {
if (isSelected) {
// prevent unselecting the last item
if (formValues.webhookEvents.length > 1) {
formValues.webhookEvents = formValues.webhookEvents.filter(
(e) => e !== eventOption.value
);
}
} else {
formValues.webhookEvents = [
...formValues.webhookEvents,
eventOption.value
];
}
}}
class="flex flex-row items-center px-3 py-2 rounded-md text-xs transition-colors duration-200 flex-shrink-0 {isSelected
? 'bg-green-50 dark:bg-green-900/30 text-green-600 dark:text-green-400 border-2 border-green-400 dark:border-green-500 hover:bg-green-100 dark:hover:bg-green-900/40'
: 'bg-white dark:bg-gray-900/60 text-gray-700 dark:text-gray-300 border-2 border-gray-200 dark:border-gray-700/60 hover:border-blue-300 dark:hover:border-highlight-blue/80 hover:bg-blue-50 dark:hover:bg-highlight-blue/20'}"
style="max-width: 280px;"
>
<span class="truncate block" style="max-width: 250px;">
{eventOption.label}
</span>
</button>
{/each}
</div>
</label>
</div>
{/if}
@@ -2171,10 +2305,16 @@
<span class="text-grayblue-dark font-medium">Webhook:</span>
<span class="text-pc-darkblue dark:text-white">{formValues.webhookValue}</span
>
<span class="text-grayblue-dark font-medium">Include Data:</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.webhookIncludeData ? 'Yes' : 'No'}</span
<span class="text-grayblue-dark font-medium">Data Level:</span>
<span class="text-pc-darkblue dark:text-white capitalize"
>{formValues.webhookIncludeData}</span
>
<span class="text-grayblue-dark font-medium">Webhook Events:</span>
<span class="text-pc-darkblue dark:text-white">
{formValues.webhookEvents.length === webhookEventOptions.length
? 'All Events'
: formValues.webhookEvents.length + ' selected'}
</span>
{/if}
{#if formValues.denyPageValue}