Compare commits

...

25 Commits

Author SHA1 Message Date
Kerwin Bryant
d7dcd0c6ce
Merge ef6aedad3bc57215e85c374b0725b63fd618af9c into b907b9fb1a1a792b9bc25112fa3eb3f8d2fb4397 2025-09-30 23:13:12 -07:00
silverwind
b907b9fb1a
Enable a few more tsconfig options (#35553)
Enable a few more useful options in tsconfig. `noImplicitReturns` had
two cases which I've fixed. Also, partially sort the file.
2025-09-30 21:43:41 -07:00
Lunny Xiao
ef6aedad3b
revert unnecessary change 2025-09-17 18:07:05 -07:00
Lunny Xiao
7f9debb359 Merge branch 'feature/webhook-payload-optimization' of github.com:kerwin612/gitea into kerwin612-feature/webhook-payload-optimization 2025-09-17 17:59:45 -07:00
Kerwin Bryant
46c08243db
Update models/webhook/webhook.go
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Signed-off-by: Kerwin Bryant <kerwin612@qq.com>
2025-08-28 14:05:56 +08:00
kerwin612
94225ed539 clean code 2025-08-24 21:42:34 +08:00
kerwin612
98d4da1bea clean code 2025-08-24 21:16:10 +08:00
kerwin612
f4ea1a1293 clean code 2025-08-24 20:58:50 +08:00
kerwin612
10d0450a1d clean code 2025-08-24 20:39:48 +08:00
kerwin612
a3581be4f8 fix 2025-08-24 18:22:14 +08:00
Kerwin Bryant
80f8d2ce8a
Merge branch 'main' into feature/webhook-payload-optimization 2025-08-24 16:40:25 +08:00
kerwin612
eec07ee0c5 clean code 2025-08-15 15:46:58 +08:00
kerwin612
6d932c8a67 clean code 2025-08-15 14:56:22 +08:00
Kerwin Bryant
6b635dc2ec
Merge branch 'main' into feature/webhook-payload-optimization 2025-08-15 14:30:40 +08:00
kerwin612
655bfcd505 clean code 2025-08-11 09:37:57 +08:00
Kerwin Bryant
9767573a4f
Merge branch 'main' into feature/webhook-payload-optimization 2025-08-09 18:02:56 +08:00
kerwin612
b718c54e7c refactor: replace webhook payload optimization with JSON-based configuration 2025-08-04 16:47:23 +08:00
Kerwin Bryant
11cdba1b13
Merge branch 'main' into feature/webhook-payload-optimization 2025-08-04 10:20:50 +08:00
kerwin612
6e07c6f338 add webhook payload optimization API support 2025-07-30 20:48:10 +08:00
Kerwin Bryant
42a880fa89
Merge branch 'main' into feature/webhook-payload-optimization 2025-07-30 13:54:55 +08:00
kerwin612
544e445024 refactor: swap webhook payload optimization logic (-1/0 values) 2025-07-30 12:16:15 +08:00
kerwin612
9682610d9a fix 2025-07-24 21:46:10 +08:00
Kerwin Bryant
39718b2ee3
Merge branch 'main' into feature/webhook-payload-optimization
Signed-off-by: Kerwin Bryant <kerwin612@qq.com>
2025-07-24 19:48:59 +08:00
kerwin612
5472d66d16 refactor webhook payload optimization: switch from bool to limit number 2025-07-23 16:36:59 +08:00
kerwin612
1915361b42 Add webhook payload size optimization options 2025-07-19 22:11:11 +08:00
20 changed files with 896 additions and 19 deletions

View File

@ -394,6 +394,7 @@ func prepareMigrationTasks() []*migration {
// Gitea 1.24.0 ends at database version 321
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
newMigration(323, "Add webhook payload optimization JSON field", v1_25.AddWebhookPayloadOptimizationColumns),
}
return preparedMigrations
}

View File

@ -0,0 +1,22 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_25
import (
"xorm.io/xorm"
)
func AddWebhookPayloadOptimizationColumns(x *xorm.Engine) error {
type Webhook struct {
MetaSettings string `xorm:"meta_settings TEXT"`
}
_, err := x.SyncWithOptions(
xorm.SyncOptions{
IgnoreConstrains: true,
IgnoreIndices: true,
},
new(Webhook),
)
return err
}

View File

@ -22,6 +22,35 @@ import (
"xorm.io/builder"
)
// MetaSettings represents the metadata settings for webhook
type MetaSettings struct {
PayloadConfig PayloadConfig `json:"payload_config"` // Payload configuration
}
// PayloadConfig represents the configuration for webhook payload
type PayloadConfig struct {
Files PayloadConfigItem `json:"files"` // Files configuration
Commits PayloadConfigItem `json:"commits"` // Commits configuration
}
// PayloadConfigItem represents a single payload configuration item
type PayloadConfigItem struct {
Enable bool `json:"enable"` // Whether to enable this configuration
Limit int `json:"limit"` // 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
}
// DefaultMetaSettings returns the default webhook meta settings
func DefaultMetaSettings() MetaSettings {
return MetaSettings{
PayloadConfig: DefaultPayloadConfig(),
}
}
// DefaultPayloadConfig returns the default payload configuration
func DefaultPayloadConfig() PayloadConfig {
return PayloadConfig{}
}
// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
type ErrWebhookNotExist struct {
ID int64
@ -139,6 +168,9 @@ type Webhook struct {
// HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
HeaderAuthorizationEncrypted string `xorm:"TEXT"`
// Webhook metadata settings (JSON format)
MetaSettings string `xorm:"meta_settings TEXT"` // JSON: webhook metadata configuration
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
@ -346,3 +378,83 @@ func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
}
return DeleteWebhookByID(ctx, id)
}
// GetMetaSettings returns the webhook meta settings
func (w *Webhook) GetMetaSettings() MetaSettings {
if w.MetaSettings == "" {
return DefaultMetaSettings()
}
var settings MetaSettings
if err := json.Unmarshal([]byte(w.MetaSettings), &settings); err != nil {
log.Error("Failed to unmarshal webhook meta settings: %v", err)
return DefaultMetaSettings()
}
return settings
}
// GetPayloadConfig returns the payload configuration
func (w *Webhook) GetPayloadConfig() PayloadConfig {
return w.GetMetaSettings().PayloadConfig
}
// SetMetaSettings sets the webhook meta settings
func (w *Webhook) SetMetaSettings(settings MetaSettings) error {
data, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("failed to marshal webhook meta settings: %w", err)
}
w.MetaSettings = string(data)
return nil
}
// SetPayloadConfig sets the payload configuration
func (w *Webhook) SetPayloadConfig(config PayloadConfig) error {
settings := w.GetMetaSettings()
settings.PayloadConfig = config
return w.SetMetaSettings(settings)
}
// IsPayloadConfigEnabled returns whether payload configuration is enabled
func (w *Webhook) IsPayloadConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Files.Enable || config.Commits.Enable
}
// GetPayloadConfigLimit returns the payload configuration limit
func (w *Webhook) GetPayloadConfigLimit() int {
config := w.GetPayloadConfig()
if config.Files.Enable {
return config.Files.Limit
}
if config.Commits.Enable {
return config.Commits.Limit
}
return 0
}
// IsFilesConfigEnabled returns whether files configuration is enabled
func (w *Webhook) IsFilesConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Files.Enable
}
// GetFilesConfigLimit returns the files configuration limit
func (w *Webhook) GetFilesConfigLimit() int {
config := w.GetPayloadConfig()
return config.Files.Limit
}
// IsCommitsConfigEnabled returns whether commits configuration is enabled
func (w *Webhook) IsCommitsConfigEnabled() bool {
config := w.GetPayloadConfig()
return config.Commits.Enable
}
// GetCommitsConfigLimit returns the commits configuration limit
func (w *Webhook) GetCommitsConfigLimit() int {
config := w.GetPayloadConfig()
return config.Commits.Limit
}

View File

@ -330,3 +330,63 @@ func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *test
assert.NoError(t, CleanupHookTaskTable(t.Context(), OlderThan, 168*time.Hour, 0))
unittest.AssertExistsAndLoadBean(t, hookTask)
}
func TestWebhookPayloadOptimization(t *testing.T) {
webhook := &Webhook{}
// Test default configuration
config := webhook.GetPayloadConfig()
assert.False(t, config.Files.Enable)
assert.Equal(t, 0, config.Files.Limit)
assert.False(t, config.Commits.Enable)
assert.Equal(t, 0, config.Commits.Limit)
// Test setting configuration via meta settings
metaSettings := MetaSettings{
PayloadConfig: PayloadConfig{
Files: PayloadConfigItem{
Enable: true,
Limit: 5,
},
Commits: PayloadConfigItem{
Enable: true,
Limit: -3,
},
},
}
webhook.SetMetaSettings(metaSettings)
// Test getting configuration
config = webhook.GetPayloadConfig()
assert.True(t, config.Files.Enable)
assert.Equal(t, 5, config.Files.Limit)
assert.True(t, config.Commits.Enable)
assert.Equal(t, -3, config.Commits.Limit)
// Test individual methods
assert.True(t, webhook.IsFilesConfigEnabled())
assert.Equal(t, 5, webhook.GetFilesConfigLimit())
assert.True(t, webhook.IsCommitsConfigEnabled())
assert.Equal(t, -3, webhook.GetCommitsConfigLimit())
assert.True(t, webhook.IsPayloadConfigEnabled())
// Test backward compatibility with direct payload config setting
newConfig := PayloadConfig{
Files: PayloadConfigItem{
Enable: false,
Limit: 10,
},
Commits: PayloadConfigItem{
Enable: false,
Limit: 20,
},
}
webhook.SetPayloadConfig(newConfig)
// Verify the config is properly set through meta settings
config = webhook.GetPayloadConfig()
assert.False(t, config.Files.Enable)
assert.Equal(t, 10, config.Files.Limit)
assert.False(t, config.Commits.Enable)
assert.Equal(t, 20, config.Commits.Limit)
}

View File

@ -33,6 +33,8 @@ type Hook struct {
AuthorizationHeader string `json:"authorization_header"`
// Whether the webhook is active and will be triggered
Active bool `json:"active"`
// MetaSettings webhook metadata settings including payload optimization
MetaSettings map[string]any `json:"meta_settings"`
// swagger:strfmt date-time
// The date and time when the webhook was last updated
Updated time.Time `json:"updated_at"`
@ -63,6 +65,8 @@ type CreateHookOption struct {
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
// Authorization header to include in webhook requests
AuthorizationHeader string `json:"authorization_header"`
// Webhook metadata settings including payload optimization
MetaSettings map[string]any `json:"meta_settings"` // {"payload_config": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
// default: false
// Whether the webhook should be active upon creation
Active bool `json:"active"`
@ -78,6 +82,8 @@ type EditHookOption struct {
BranchFilter string `json:"branch_filter" binding:"GlobPattern"`
// Authorization header to include in webhook requests
AuthorizationHeader string `json:"authorization_header"`
// Webhook metadata settings including payload optimization
MetaSettings *map[string]any `json:"meta_settings"` // {"payload_config": {"files": {"enable": bool, "limit": int}, "commits": {"enable": bool, "limit": int}}}
// Whether the webhook is active and will be triggered
Active *bool `json:"active"`
}

View File

@ -2434,6 +2434,13 @@ settings.event_package = Package
settings.event_package_desc = Package created or deleted in a repository.
settings.branch_filter = Branch filter
settings.branch_filter_desc = Branch whitelist for push, branch creation and branch deletion events, specified as glob pattern. If empty or <code>*</code>, events for all branches are reported. See <a href="%[1]s">%[2]s</a> documentation for syntax. Examples: <code>master</code>, <code>{master,release*}</code>.
settings.payload_optimization = Payload Size Optimization
settings.payload_optimization_files = Files
settings.payload_optimization_commits = Commits
settings.payload_optimization_enable = Enable optimization
settings.payload_optimization_enable_desc = Enable payload size optimization for this item
settings.payload_optimization_limit = Limit
settings.payload_optimization_limit_desc = 0: trim all (none kept), >0: keep N items (forward order), <0: keep N items (reverse order)
settings.authorization_header = Authorization Header
settings.authorization_header_desc = Will be included as authorization header for requests when present. Examples: %s.
settings.active = Active
@ -3297,7 +3304,7 @@ auths.tip.github = Register a new OAuth application on %s
auths.tip.gitlab_new = Register a new application on %s
auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at %s
auths.tip.openid_connect = Use the OpenID Connect Discovery URL "https://{server}/.well-known/openid-configuration" to specify the endpoints
auths.tip.twitter = Go to %s, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled
auths.tip.twitter = Go to %s, create an application and ensure that the "Allow this application to be used to Sign in with Twitter" option is enabled
auths.tip.discord = Register a new application on %s
auths.tip.gitea = Register a new OAuth2 application. Guide can be found at %s
auths.tip.yandex = Create a new application at %s. Select following permissions from the "Yandex.Passport API" section: "Access to email address", "Access to user avatar" and "Access to username, first name and surname, gender"

View File

@ -21,6 +21,33 @@ import (
webhook_service "code.gitea.io/gitea/services/webhook"
)
// getPayloadConfigEnable extracts the "enable" boolean value from a payload config map
func getPayloadConfigEnable(m map[string]any) bool {
if val, ok := m["enable"]; ok {
if boolVal, ok := val.(bool); ok {
return boolVal
}
}
return false
}
// getPayloadConfigLimit extracts the "limit" integer value from a payload config map
func getPayloadConfigLimit(m map[string]any) int {
if val, ok := m["limit"]; ok {
switch v := val.(type) {
case int:
return v
case float64:
return int(v)
case string:
if intVal, err := strconv.Atoi(v); err == nil {
return intVal
}
}
}
return 0
}
// ListOwnerHooks lists the webhooks of the provided owner
func ListOwnerHooks(ctx *context.APIContext, owner *user_model.User) {
opts := &webhook.ListWebhookOptions{
@ -227,6 +254,44 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
IsActive: form.Active,
Type: form.Type,
}
// Set webhook meta settings
if form.MetaSettings != nil {
metaSettings := webhook.MetaSettings{}
// Parse payload config
if payloadOptMap, ok := form.MetaSettings["payload_config"].(map[string]any); ok {
payloadOptConfig := webhook.PayloadConfig{}
// Parse files config
if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
payloadOptConfig.Files = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(filesConfig),
Limit: getPayloadConfigLimit(filesConfig),
}
} else {
payloadOptConfig.Files = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}
// Parse commits config
if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
payloadOptConfig.Commits = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(commitsConfig),
Limit: getPayloadConfigLimit(commitsConfig),
}
} else {
payloadOptConfig.Commits = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}
metaSettings.PayloadConfig = payloadOptConfig
}
if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.APIErrorInternal(err)
return nil, false
}
}
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
if err != nil {
ctx.APIErrorInternal(err)
@ -391,6 +456,43 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.IsActive = *form.Active
}
// Update webhook meta settings
if form.MetaSettings != nil {
metaSettings := webhook.MetaSettings{}
// Parse payload config
if payloadOptMap, ok := (*form.MetaSettings)["payload_config"].(map[string]any); ok {
payloadOptConfig := webhook.PayloadConfig{}
// Parse files config
if filesConfig, ok := payloadOptMap["files"].(map[string]any); ok {
payloadOptConfig.Files = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(filesConfig),
Limit: getPayloadConfigLimit(filesConfig),
}
} else {
payloadOptConfig.Files = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}
// Parse commits config
if commitsConfig, ok := payloadOptMap["commits"].(map[string]any); ok {
payloadOptConfig.Commits = webhook.PayloadConfigItem{
Enable: getPayloadConfigEnable(commitsConfig),
Limit: getPayloadConfigLimit(commitsConfig),
}
} else {
payloadOptConfig.Commits = webhook.PayloadConfigItem{Enable: false, Limit: 0}
}
metaSettings.PayloadConfig = payloadOptConfig
}
if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.APIErrorInternal(err)
return false
}
}
if err := webhook.UpdateWebhook(ctx, w); err != nil {
ctx.APIErrorInternal(err)
return false

View File

@ -121,7 +121,15 @@ func checkHookType(ctx *context.Context) string {
// WebhooksNew render creating webhook page
func WebhooksNew(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
// Create a new webhook with default meta settings
newWebhook := &webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
// Initialize meta settings with default values
if err := newWebhook.SetMetaSettings(webhook.DefaultMetaSettings()); err != nil {
ctx.ServerError("SetMetaSettings", err)
return
}
ctx.Data["Webhook"] = newWebhook
orCtx, err := getOwnerRepoCtx(ctx)
if err != nil {
@ -207,7 +215,14 @@ func createWebhook(ctx *context.Context, params webhookParams) {
ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook")
ctx.Data["PageIsSettingsHooks"] = true
ctx.Data["PageIsSettingsHooksNew"] = true
ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
// Create a webhook with default meta settings for template rendering
newWebhook := &webhook.Webhook{HookEvent: &webhook_module.HookEvent{}}
if err := newWebhook.SetMetaSettings(webhook.DefaultMetaSettings()); err != nil {
ctx.ServerError("SetMetaSettings", err)
return
}
ctx.Data["Webhook"] = newWebhook
ctx.Data["HookType"] = params.Type
orCtx, err := getOwnerRepoCtx(ctx)
@ -244,6 +259,25 @@ func createWebhook(ctx *context.Context, params webhookParams) {
OwnerID: orCtx.OwnerID,
IsSystemWebhook: orCtx.IsSystemWebhook,
}
// Set webhook meta settings with payload config
metaSettings := webhook.MetaSettings{
PayloadConfig: webhook.PayloadConfig{
Files: webhook.PayloadConfigItem{
Enable: params.WebhookForm.PayloadOptimizationFilesEnable,
Limit: params.WebhookForm.PayloadOptimizationFilesLimit,
},
Commits: webhook.PayloadConfigItem{
Enable: params.WebhookForm.PayloadOptimizationCommitsEnable,
Limit: params.WebhookForm.PayloadOptimizationCommitsLimit,
},
},
}
if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.ServerError("SetMetaSettings", err)
return
}
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)
@ -295,6 +329,24 @@ func editWebhook(ctx *context.Context, params webhookParams) {
w.HTTPMethod = params.HTTPMethod
w.Meta = string(meta)
// Set webhook meta settings with payload config
metaSettings := webhook.MetaSettings{
PayloadConfig: webhook.PayloadConfig{
Files: webhook.PayloadConfigItem{
Enable: params.WebhookForm.PayloadOptimizationFilesEnable,
Limit: params.WebhookForm.PayloadOptimizationFilesLimit,
},
Commits: webhook.PayloadConfigItem{
Enable: params.WebhookForm.PayloadOptimizationCommitsEnable,
Limit: params.WebhookForm.PayloadOptimizationCommitsLimit,
},
},
}
if err := w.SetMetaSettings(metaSettings); err != nil {
ctx.ServerError("SetMetaSettings", err)
return
}
err = w.SetHeaderAuthorization(params.WebhookForm.AuthorizationHeader)
if err != nil {
ctx.ServerError("SetHeaderAuthorization", err)

View File

@ -239,6 +239,11 @@ type WebhookForm struct {
BranchFilter string `binding:"GlobPattern"`
AuthorizationHeader string
Secret string
// Payload config settings
PayloadOptimizationFilesEnable bool `form:"payload_optimization_files_enable"`
PayloadOptimizationFilesLimit int `form:"payload_optimization_files_limit"`
PayloadOptimizationCommitsEnable bool `form:"payload_optimization_commits_enable"`
PayloadOptimizationCommitsLimit int `form:"payload_optimization_commits_limit"`
}
// PushOnly if the hook will be triggered when push

View File

@ -409,6 +409,21 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
return nil, err
}
// Convert meta settings to map
metaSettings := w.GetMetaSettings()
metaSettingsMap := map[string]any{
"payload_config": map[string]any{
"files": map[string]any{
"enable": metaSettings.PayloadConfig.Files.Enable,
"limit": metaSettings.PayloadConfig.Files.Limit,
},
"commits": map[string]any{
"enable": metaSettings.PayloadConfig.Commits.Enable,
"limit": metaSettings.PayloadConfig.Commits.Limit,
},
},
}
return &api.Hook{
ID: w.ID,
Type: w.Type,
@ -417,6 +432,7 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) {
Config: config,
Events: w.EventsArray(),
AuthorizationHeader: authorizationHeader,
MetaSettings: metaSettingsMap,
Updated: w.UpdatedUnix.AsTime(),
Created: w.CreatedUnix.AsTime(),
BranchFilter: w.BranchFilter,

View File

@ -7,6 +7,7 @@ import (
"context"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/models/db"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization"
@ -15,10 +16,12 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
webhook_model "code.gitea.io/gitea/models/webhook"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/repository"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
@ -641,6 +644,138 @@ func (m *webhookNotifier) IssueChangeMilestone(ctx context.Context, doer *user_m
}
}
// applyWebhookPayloadOptimizations applies payload optimizations based on webhook configurations
func (m *webhookNotifier) applyWebhookPayloadOptimizations(ctx context.Context, repo *repo_model.Repository, apiCommits []*api.PayloadCommit, apiHeadCommit *api.PayloadCommit) ([]*api.PayloadCommit, *api.PayloadCommit) {
// Get all webhooks for this repository
webhooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
RepoID: repo.ID,
IsActive: optional.Some(true),
})
if err != nil {
log.Error("Failed to get webhooks for repository %d: %v", repo.ID, err)
// Continue with default behavior if we can't get webhooks
return apiCommits, apiHeadCommit
}
// Check if any webhook has payload optimization options enabled
var filesLimit, commitsLimit int
hasFilesLimit := false
hasCommitsLimit := false
optimizationEnabled := false
for _, webhook := range webhooks {
if webhook.HasEvent(webhook_module.HookEventPush) {
config := webhook.GetPayloadConfig()
// Check files optimization
if config.Files.Enable {
optimizationEnabled = true
if !hasFilesLimit || config.Files.Limit < filesLimit {
filesLimit = config.Files.Limit
hasFilesLimit = true
}
}
// Check commits optimization
if config.Commits.Enable {
optimizationEnabled = true
if !hasCommitsLimit || config.Commits.Limit < commitsLimit {
commitsLimit = config.Commits.Limit
hasCommitsLimit = true
}
}
}
}
// Apply payload optimizations based on webhook configurations
// 0: trim all (none kept), >0: trim to N items (forward order), <0: trim to N items (reverse order)
if optimizationEnabled {
// Apply files optimization to all commits
if hasFilesLimit {
for _, commit := range apiCommits {
if commit.Added != nil {
if filesLimit == 0 {
commit.Added = nil
} else if filesLimit > 0 && len(commit.Added) > filesLimit {
commit.Added = commit.Added[:filesLimit]
} else if filesLimit < 0 && len(commit.Added) > -filesLimit {
// Reverse order: keep the last N items
commit.Added = commit.Added[len(commit.Added)+filesLimit:]
}
}
if commit.Removed != nil {
if filesLimit == 0 {
commit.Removed = nil
} else if filesLimit > 0 && len(commit.Removed) > filesLimit {
commit.Removed = commit.Removed[:filesLimit]
} else if filesLimit < 0 && len(commit.Removed) > -filesLimit {
// Reverse order: keep the last N items
commit.Removed = commit.Removed[len(commit.Removed)+filesLimit:]
}
}
if commit.Modified != nil {
if filesLimit == 0 {
commit.Modified = nil
} else if filesLimit > 0 && len(commit.Modified) > filesLimit {
commit.Modified = commit.Modified[:filesLimit]
} else if filesLimit < 0 && len(commit.Modified) > -filesLimit {
// Reverse order: keep the last N items
commit.Modified = commit.Modified[len(commit.Modified)+filesLimit:]
}
}
}
// Apply files optimization to head commit
if apiHeadCommit != nil {
if apiHeadCommit.Added != nil {
if filesLimit == 0 {
apiHeadCommit.Added = nil
} else if filesLimit > 0 && len(apiHeadCommit.Added) > filesLimit {
apiHeadCommit.Added = apiHeadCommit.Added[:filesLimit]
} else if filesLimit < 0 && len(apiHeadCommit.Added) > -filesLimit {
// Reverse order: keep the last N items
apiHeadCommit.Added = apiHeadCommit.Added[len(apiHeadCommit.Added)+filesLimit:]
}
}
if apiHeadCommit.Removed != nil {
if filesLimit == 0 {
apiHeadCommit.Removed = nil
} else if filesLimit > 0 && len(apiHeadCommit.Removed) > filesLimit {
apiHeadCommit.Removed = apiHeadCommit.Removed[:filesLimit]
} else if filesLimit < 0 && len(apiHeadCommit.Removed) > -filesLimit {
// Reverse order: keep the last N items
apiHeadCommit.Removed = apiHeadCommit.Removed[len(apiHeadCommit.Removed)+filesLimit:]
}
}
if apiHeadCommit.Modified != nil {
if filesLimit == 0 {
apiHeadCommit.Modified = nil
} else if filesLimit > 0 && len(apiHeadCommit.Modified) > filesLimit {
apiHeadCommit.Modified = apiHeadCommit.Modified[:filesLimit]
} else if filesLimit < 0 && len(apiHeadCommit.Modified) > -filesLimit {
// Reverse order: keep the last N items
apiHeadCommit.Modified = apiHeadCommit.Modified[len(apiHeadCommit.Modified)+filesLimit:]
}
}
}
}
// Apply commits optimization
if hasCommitsLimit {
if commitsLimit == 0 {
apiCommits = nil
} else if commitsLimit > 0 && len(apiCommits) > commitsLimit {
apiCommits = apiCommits[:commitsLimit]
} else if commitsLimit < 0 && len(apiCommits) > -commitsLimit {
// Reverse order: keep the last N commits
apiCommits = apiCommits[len(apiCommits)+commitsLimit:]
}
}
}
return apiCommits, apiHeadCommit
}
func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.User, repo *repo_model.Repository, opts *repository.PushUpdateOptions, commits *repository.PushCommits) {
apiPusher := convert.ToUser(ctx, pusher, nil)
apiCommits, apiHeadCommit, err := commits.ToAPIPayloadCommits(ctx, repo)
@ -649,6 +784,9 @@ func (m *webhookNotifier) PushCommits(ctx context.Context, pusher *user_model.Us
return
}
// Apply payload optimizations
apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
Ref: opts.RefFullName.String(),
Before: opts.OldCommitID,
@ -888,6 +1026,9 @@ func (m *webhookNotifier) SyncPushCommits(ctx context.Context, pusher *user_mode
return
}
// Apply payload optimizations
apiCommits, apiHeadCommit = m.applyWebhookPayloadOptimizations(ctx, repo, apiCommits, apiHeadCommit)
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventPush, &api.PushPayload{
Ref: opts.RefFullName.String(),
Before: opts.OldCommitID,

View File

@ -90,3 +90,171 @@ func TestWebhookUserMail(t *testing.T) {
assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(t.Context(), user, nil).Email)
assert.Equal(t, user.Email, convert.ToUser(t.Context(), user, user).Email)
}
func TestWebhookPayloadOptimization(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
// Create test webhook
webhook := &webhook_model.Webhook{
RepoID: repo.ID,
URL: "http://example.com/webhook",
HTTPMethod: "POST",
ContentType: webhook_model.ContentTypeJSON,
Secret: "secret",
IsActive: true,
Type: webhook_module.GITEA,
HookEvent: &webhook_module.HookEvent{
PushOnly: true,
},
}
// Test case 1: No optimization enabled
webhook.SetMetaSettings(webhook_model.MetaSettings{
PayloadConfig: webhook_model.PayloadConfig{
Files: webhook_model.PayloadConfigItem{Enable: false, Limit: 0},
Commits: webhook_model.PayloadConfigItem{Enable: false, Limit: 0},
},
})
err := webhook.UpdateEvent()
assert.NoError(t, err)
err = webhook_model.CreateWebhook(t.Context(), webhook)
assert.NoError(t, err)
apiCommits := []*api.PayloadCommit{
{
ID: "abc123",
Message: "Test commit",
Added: []string{"file1.txt", "file2.txt"},
Removed: []string{"oldfile.txt"},
Modified: []string{"modified.txt"},
},
{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
},
}
apiHeadCommit := &api.PayloadCommit{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
}
// Should not modify anything when optimization is disabled
optimizedCommits, optimizedHeadCommit := (&webhookNotifier{}).applyWebhookPayloadOptimizations(t.Context(), repo, apiCommits, apiHeadCommit)
if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 2 {
assert.Equal(t, []string{"file1.txt", "file2.txt"}, optimizedCommits[0].Added)
assert.Equal(t, []string{"oldfile.txt"}, optimizedCommits[0].Removed)
assert.Equal(t, []string{"modified.txt"}, optimizedCommits[0].Modified)
assert.Equal(t, []string{"file3.txt"}, optimizedCommits[1].Added)
assert.Equal(t, []string{}, optimizedCommits[1].Removed)
assert.Equal(t, []string{"file1.txt"}, optimizedCommits[1].Modified)
}
if assert.NotNil(t, optimizedHeadCommit) {
assert.Equal(t, []string{"file3.txt"}, optimizedHeadCommit.Added)
assert.Equal(t, []string{}, optimizedHeadCommit.Removed)
assert.Equal(t, []string{"file1.txt"}, optimizedHeadCommit.Modified)
}
// Test case 2: Files optimization enabled, limit = 0 (trim all)
webhook.SetMetaSettings(webhook_model.MetaSettings{
PayloadConfig: webhook_model.PayloadConfig{
Files: webhook_model.PayloadConfigItem{Enable: true, Limit: 0},
Commits: webhook_model.PayloadConfigItem{Enable: false, Limit: 0},
},
})
err = webhook_model.UpdateWebhook(t.Context(), webhook)
assert.NoError(t, err)
apiCommits = []*api.PayloadCommit{
{
ID: "abc123",
Message: "Test commit",
Added: []string{"file1.txt", "file2.txt"},
Removed: []string{"oldfile.txt"},
Modified: []string{"modified.txt"},
},
{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
},
}
apiHeadCommit = &api.PayloadCommit{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
}
optimizedCommits, optimizedHeadCommit = (&webhookNotifier{}).applyWebhookPayloadOptimizations(t.Context(), repo, apiCommits, apiHeadCommit)
if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 2 {
assert.Nil(t, optimizedCommits[0].Added)
assert.Nil(t, optimizedCommits[0].Removed)
assert.Nil(t, optimizedCommits[0].Modified)
assert.Nil(t, optimizedCommits[1].Added)
assert.Nil(t, optimizedCommits[1].Removed)
assert.Nil(t, optimizedCommits[1].Modified)
}
if assert.NotNil(t, optimizedHeadCommit) {
assert.Nil(t, optimizedHeadCommit.Added)
assert.Nil(t, optimizedHeadCommit.Removed)
assert.Nil(t, optimizedHeadCommit.Modified)
}
// Test case 3: Commits optimization enabled, limit = 1 (keep first)
webhook.SetMetaSettings(webhook_model.MetaSettings{
PayloadConfig: webhook_model.PayloadConfig{
Files: webhook_model.PayloadConfigItem{Enable: false, Limit: 0},
Commits: webhook_model.PayloadConfigItem{Enable: true, Limit: 1},
},
})
err = webhook_model.UpdateWebhook(t.Context(), webhook)
assert.NoError(t, err)
apiCommits = []*api.PayloadCommit{
{
ID: "abc123",
Message: "Test commit",
Added: []string{"file1.txt", "file2.txt"},
Removed: []string{"oldfile.txt"},
Modified: []string{"modified.txt"},
},
{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
},
}
apiHeadCommit = &api.PayloadCommit{
ID: "def456",
Message: "Another commit",
Added: []string{"file3.txt"},
Removed: []string{},
Modified: []string{"file1.txt"},
}
optimizedCommits, optimizedHeadCommit = (&webhookNotifier{}).applyWebhookPayloadOptimizations(t.Context(), repo, apiCommits, apiHeadCommit)
if assert.NotNil(t, optimizedCommits) && len(optimizedCommits) == 1 {
assert.Equal(t, []string{"file1.txt", "file2.txt"}, optimizedCommits[0].Added)
assert.Equal(t, []string{"oldfile.txt"}, optimizedCommits[0].Removed)
assert.Equal(t, []string{"modified.txt"}, optimizedCommits[0].Modified)
}
if assert.NotNil(t, optimizedHeadCommit) {
assert.Equal(t, []string{"file3.txt"}, optimizedHeadCommit.Added)
assert.Equal(t, []string{}, optimizedHeadCommit.Removed)
assert.Equal(t, []string{"file1.txt"}, optimizedHeadCommit.Modified)
}
}

View File

@ -47,6 +47,41 @@
<span class="help">{{ctx.Locale.Tr "repo.settings.branch_filter_desc" "https://pkg.go.dev/github.com/gobwas/glob#Compile" "github.com/gobwas/glob"}}</span>
</div>
<!-- Payload size optimization options -->
<div class="field">
<h4>{{ctx.Locale.Tr "repo.settings.payload_optimization"}}</h4>
<div class="field">
<h5>{{ctx.Locale.Tr "repo.settings.payload_optimization_files"}}</h5>
<div class="field">
<div class="ui checkbox">
<input name="payload_optimization_files_enable" type="checkbox" {{if .Webhook.IsFilesConfigEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.payload_optimization_enable"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.payload_optimization_enable_desc"}}</span>
</div>
</div>
<div class="field {{if not .Webhook.IsFilesConfigEnabled}}tw-hidden{{end}}">
<label>{{ctx.Locale.Tr "repo.settings.payload_optimization_limit"}}</label>
<input name="payload_optimization_files_limit" type="number" value="{{.Webhook.GetFilesConfigLimit}}" placeholder="0" {{if not .Webhook.IsFilesConfigEnabled}}disabled{{end}}>
<span class="help">{{ctx.Locale.Tr "repo.settings.payload_optimization_limit_desc"}}</span>
</div>
</div>
<div class="field">
<h5>{{ctx.Locale.Tr "repo.settings.payload_optimization_commits"}}</h5>
<div class="field">
<div class="ui checkbox">
<input name="payload_optimization_commits_enable" type="checkbox" {{if .Webhook.IsCommitsConfigEnabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.payload_optimization_enable"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.payload_optimization_enable_desc"}}</span>
</div>
</div>
<div class="field {{if not .Webhook.IsCommitsConfigEnabled}}tw-hidden{{end}}">
<label>{{ctx.Locale.Tr "repo.settings.payload_optimization_limit"}}</label>
<input name="payload_optimization_commits_limit" type="number" value="{{.Webhook.GetCommitsConfigLimit}}" placeholder="0" {{if not .Webhook.IsCommitsConfigEnabled}}disabled{{end}}>
<span class="help">{{ctx.Locale.Tr "repo.settings.payload_optimization_limit_desc"}}</span>
</div>
</div>
</div>
<div class="field">
<h4>{{ctx.Locale.Tr "repo.settings.event_desc"}}</h4>
<div class="grouped event type fields">

View File

@ -22939,6 +22939,12 @@
},
"x-go-name": "Events"
},
"meta_settings": {
"description": "Webhook metadata settings including payload optimization",
"type": "object",
"additionalProperties": {},
"x-go-name": "MetaSettings"
},
"type": {
"type": "string",
"enum": [
@ -24172,6 +24178,12 @@
"type": "string"
},
"x-go-name": "Events"
},
"meta_settings": {
"description": "Webhook metadata settings including payload optimization",
"type": "object",
"additionalProperties": {},
"x-go-name": "MetaSettings"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
@ -25576,6 +25588,12 @@
"format": "int64",
"x-go-name": "ID"
},
"meta_settings": {
"description": "MetaSettings webhook metadata settings including payload optimization",
"type": "object",
"additionalProperties": {},
"x-go-name": "MetaSettings"
},
"type": {
"description": "The type of the webhook (e.g., gitea, slack, discord)",
"type": "string",

View File

@ -1716,3 +1716,98 @@ jobs:
assert.Equal(t, "user2/repo1", webhookData.payloads[i].Repo.FullName)
}
}
func Test_WebhookPayloadOptimizationAPI(t *testing.T) {
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAll)
// Test creating webhook with payload config options via API
createHookOption := map[string]any{
"type": "gitea",
"config": map[string]string{
"url": "http://example.com/webhook",
"content_type": "json",
},
"events": []string{"push"},
"meta_settings": map[string]any{
"payload_config": map[string]any{
"files": map[string]any{
"enable": true,
"limit": 2,
},
"commits": map[string]any{
"enable": true,
"limit": 1,
},
},
},
"active": true,
}
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/hooks", createHookOption).AddTokenAuth(token)
resp := session.MakeRequest(t, req, http.StatusCreated)
var hook api.Hook
DecodeJSON(t, resp, &hook)
// Verify the webhook was created with correct payload config settings
assert.NotNil(t, hook.MetaSettings)
payloadOptConfig := hook.MetaSettings["payload_config"].(map[string]any)
filesConfig := payloadOptConfig["files"].(map[string]any)
commitsConfig := payloadOptConfig["commits"].(map[string]any)
assert.Equal(t, true, filesConfig["enable"])
assert.InEpsilon(t, 2.0, filesConfig["limit"], 0.01)
assert.Equal(t, true, commitsConfig["enable"])
assert.InEpsilon(t, 1.0, commitsConfig["limit"], 0.01)
// Test updating webhook with different payload config options
editHookOption := map[string]any{
"meta_settings": map[string]any{
"payload_config": map[string]any{
"files": map[string]any{
"enable": false,
"limit": 0,
},
"commits": map[string]any{
"enable": false,
"limit": 0,
},
},
},
}
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/user2/repo1/hooks/%d", hook.ID), editHookOption).AddTokenAuth(token)
resp = session.MakeRequest(t, req, http.StatusOK)
var updatedHook api.Hook
DecodeJSON(t, resp, &updatedHook)
// Verify the webhook was updated with correct payload config settings
assert.NotNil(t, updatedHook.MetaSettings)
payloadOptConfig = updatedHook.MetaSettings["payload_config"].(map[string]any)
filesConfig = payloadOptConfig["files"].(map[string]any)
commitsConfig = payloadOptConfig["commits"].(map[string]any)
assert.Equal(t, false, filesConfig["enable"])
assert.EqualValues(t, 0, filesConfig["limit"])
assert.Equal(t, false, commitsConfig["enable"])
assert.EqualValues(t, 0, commitsConfig["limit"])
// Test getting webhook to verify the settings are persisted
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/hooks/%d", hook.ID)).AddTokenAuth(token)
resp = session.MakeRequest(t, req, http.StatusOK)
var retrievedHook api.Hook
DecodeJSON(t, resp, &retrievedHook)
// Verify the webhook settings are correctly retrieved
assert.NotNil(t, retrievedHook.MetaSettings)
payloadOptConfig = retrievedHook.MetaSettings["payload_config"].(map[string]any)
filesConfig = payloadOptConfig["files"].(map[string]any)
commitsConfig = payloadOptConfig["commits"].(map[string]any)
assert.Equal(t, false, filesConfig["enable"])
assert.EqualValues(t, 0, filesConfig["limit"])
assert.Equal(t, false, commitsConfig["enable"])
assert.EqualValues(t, 0, commitsConfig["limit"])
})
}

View File

@ -1,7 +1,7 @@
{
"include": [
"${configDir}/.*",
"${configDir}/*",
"${configDir}/.*",
"${configDir}/tests/e2e/**/*",
"${configDir}/tests/e2e/**/.*",
"${configDir}/tools/**/*",
@ -17,27 +17,31 @@
"allowImportingTsExtensions": true,
"allowJs": true,
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"erasableSyntaxOnly": true,
"esModuleInterop": true,
"exactOptionalPropertyTypes": false,
"isolatedModules": true,
"libReplacement": false,
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"verbatimModuleSyntax": true,
"stripInternal": true,
"sourceMap": true,
"strict": false,
"strictBindCallApply": true,
"strictBuiltinIteratorReturn": true,
"strictFunctionTypes": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"exactOptionalPropertyTypes": false,
"sourceMap": true,
"stripInternal": true,
"verbatimModuleSyntax": true,
"types": [
"vitest/globals",
"./web_src/js/globals.d.ts",

View File

@ -0,0 +1,31 @@
/**
* Webhook settings functionality
*/
import {toggleElemClass} from '../utils/dom.ts';
function setupOptimizationToggle(enableFieldName: string, limitFieldName: string): void {
const enableCheckbox = document.querySelector<HTMLInputElement>(`input[name="${enableFieldName}"]`);
if (!enableCheckbox) return;
enableCheckbox.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement;
const limitField = document.querySelector<HTMLInputElement>(`input[name="${limitFieldName}"]`);
if (limitField) {
limitField.disabled = !target.checked;
// Use toggleElemClass to show/hide the limit field container
const limitFieldContainer = limitField.closest('.field');
if (limitFieldContainer) {
toggleElemClass(limitFieldContainer, 'tw-hidden', !target.checked);
}
}
});
}
export function initRepoSettingsWebhook(): void {
if (!document.querySelector('.page-content.repository.settings.webhook')) return;
// Setup payload optimization toggles
setupOptimizationToggle('payload_optimization_files_enable', 'payload_optimization_files_limit');
setupOptimizationToggle('payload_optimization_commits_enable', 'payload_optimization_commits_limit');
}

View File

@ -2,6 +2,7 @@ import {createMonaco} from './codeeditor.ts';
import {onInputDebounce, queryElems, toggleElem} from '../utils/dom.ts';
import {POST} from '../modules/fetch.ts';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {initRepoSettingsWebhook} from './repo-settings-webhook.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {globMatch} from '../utils/glob.ts';
@ -152,4 +153,5 @@ export function initRepoSettings() {
initRepoSettingsSearchTeamBox();
initRepoSettingsGitHook();
initRepoSettingsBranchesDrag();
initRepoSettingsWebhook();
}

View File

@ -10,7 +10,7 @@ export function initTableSort() {
}
function tableSort(normSort: string, revSort: string, isDefault: string) {
if (!normSort) return false;
if (!normSort) return;
if (!revSort) revSort = '';
const url = new URL(window.location.href);

View File

@ -43,7 +43,7 @@ type ToastOpts = {
type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast {
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
const body = useHtmlBody ? message : htmlEscape(message);
const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : '';
@ -56,7 +56,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
showElem(toastDupNumEl);
toastDupNumEl.textContent = String(Number(toastDupNumEl.textContent) + 1);
animateOnce(toastDupNumEl, 'pulse-1p5-200');
return;
return null;
}
}
@ -83,15 +83,15 @@ function showToast(message: string, level: Intent, {gravity, position, duration,
return toast;
}
export function showInfoToast(message: string, opts?: ToastOpts): Toast {
export function showInfoToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'info', opts);
}
export function showWarningToast(message: string, opts?: ToastOpts): Toast {
export function showWarningToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'warning', opts);
}
export function showErrorToast(message: string, opts?: ToastOpts): Toast {
export function showErrorToast(message: string, opts?: ToastOpts): Toast | null {
return showToast(message, 'error', opts);
}