From 7fda37859da995f8f67327cab0f0b256266bbe55 Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Thu, 30 Oct 2025 19:50:58 -0400 Subject: [PATCH 01/18] Create vue component for webhooks form triggers Extracts trigger UI into its own Vue component in the process of migrating webhooks form from haml to Vue. Changelog: added --- .../components/webhook_form_trigger_item.vue | 85 +++++++++++++++++++ .../webhook_form_trigger_item_spec.js | 71 ++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue create mode 100644 spec/frontend/webhooks/components/webhook_form_trigger_item_spec.js diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue new file mode 100644 index 00000000000000..c91f5f680ed9d3 --- /dev/null +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue @@ -0,0 +1,85 @@ + + + diff --git a/spec/frontend/webhooks/components/webhook_form_trigger_item_spec.js b/spec/frontend/webhooks/components/webhook_form_trigger_item_spec.js new file mode 100644 index 00000000000000..2708f1149e9d05 --- /dev/null +++ b/spec/frontend/webhooks/components/webhook_form_trigger_item_spec.js @@ -0,0 +1,71 @@ +import { nextTick } from 'vue'; +import { GlFormCheckbox } from '@gitlab/ui'; + +import WebhookFormTriggerItem from '~/webhooks/components/webhook_form_trigger_item.vue'; + +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('WebhookFormTriggerItem', () => { + let wrapper; + + const label = 'Enable me'; + const helpText = 'Helpful text'; + const helpLinkPath = 'docs/help'; + const helpLinkText = 'Learn more'; + + const createComponent = (mountFn = shallowMountExtended, { props } = {}) => { + wrapper = mountFn(WebhookFormTriggerItem, { + propsData: { + triggerName: 'custom-trigger', + inputName: 'custom_trigger', + label, + ...props, + }, + }); + }; + + const findTriggerCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findHelpText = () => wrapper.find('.help-text'); + + describe('by default', () => { + beforeEach(() => { + createComponent(); + }); + it('displays the correct trigger text', () => { + expect(wrapper.text()).toBe(label); + }); + + it('excludes help subtext if none is passed', () => { + expect(findHelpText().exists()).toBe(false); + }); + }); + + it('includes help subtext when passed', () => { + createComponent(mountExtended, { props: { helpText } }); + + expect(findHelpText().text()).toBe(helpText); + }); + + it('includes link to docs when both link text and link path are passed', () => { + createComponent(mountExtended, { props: { helpText, helpLinkText, helpLinkPath } }); + + expect(findHelpText().text()).toContain(helpLinkText); + }); + + it('does not link to docs when missing either link text or path', () => { + createComponent(mountExtended, { props: { helpText, helpLinkText } }); + + expect(findHelpText().text()).not.toContain(helpLinkText); + }); + + it('emits input events when checkbox is checked and unchecked', async () => { + createComponent(); + + const checkbox = findTriggerCheckbox(); + await checkbox.vm.$emit('input', true); + await nextTick(); + await checkbox.vm.$emit('input', false); + + expect(wrapper.emitted('input')).toEqual([[true], [false]]); + }); +}); -- GitLab From e7297aad3ad70226e18f338067b80a8bbf48bd5b Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Fri, 31 Oct 2025 19:03:24 -0400 Subject: [PATCH 02/18] Create EE version of checkbox for vulnerability events --- .../vulnerability_events_trigger_item.vue | 32 +++++++++++++++++++ .../vulnerability_events_trigger_item_spec.js | 21 ++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue create mode 100644 ee/spec/frontend/webhooks/components/vulnerability_events_trigger_item_spec.js diff --git a/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue b/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue new file mode 100644 index 00000000000000..066426427bc2d5 --- /dev/null +++ b/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue @@ -0,0 +1,32 @@ + + + diff --git a/ee/spec/frontend/webhooks/components/vulnerability_events_trigger_item_spec.js b/ee/spec/frontend/webhooks/components/vulnerability_events_trigger_item_spec.js new file mode 100644 index 00000000000000..3041326b4b8989 --- /dev/null +++ b/ee/spec/frontend/webhooks/components/vulnerability_events_trigger_item_spec.js @@ -0,0 +1,21 @@ +import VulnerabilityEventsTriggerItem from 'ee/webhooks/components/vulnerability_events_trigger_item.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('VulnerabilityEventsTriggerItem', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMountExtended(VulnerabilityEventsTriggerItem, { + propsData: { + initialVulnerabilityTrigger: true, + }, + }); + }; + + it('renders Vulnerability Events hook', () => { + createComponent(); + + expect(wrapper.attributes('triggername')).toBe('vulnerability-events'); + expect(wrapper.attributes('value')).toBe('true'); + }); +}); -- GitLab From 929cd57d3f8aff26aa525f152ebf959529515a8b Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Tue, 4 Nov 2025 14:20:54 -0500 Subject: [PATCH 03/18] Create list of all webhook triggers in Vue - creates webhook_form_trigger_list - refactors push_events to be passed through webhook form app. --- .../javascripts/pages/projects/hooks/index.js | 2 -- .../webhooks/components/push_events.vue | 23 ++++++++++++++++++- .../components/webhook_form_trigger_item.vue | 4 ++-- app/assets/javascripts/webhooks/webhook.js | 23 ------------------- .../javascripts/pages/groups/hooks/index.js | 2 -- .../webhooks/components/push_events_spec.js | 6 ++--- 6 files changed, 27 insertions(+), 33 deletions(-) delete mode 100644 app/assets/javascripts/webhooks/webhook.js diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js index f25547f9982ee7..dc8710744b87c8 100644 --- a/app/assets/javascripts/pages/projects/hooks/index.js +++ b/app/assets/javascripts/pages/projects/hooks/index.js @@ -1,8 +1,6 @@ import initSearchSettings from '~/search_settings'; import initWebhookForm, { initHookTestDropdowns } from '~/webhooks'; -import { initPushEventsEditForm } from '~/webhooks/webhook'; initSearchSettings(); initWebhookForm(); -initPushEventsEditForm(); initHookTestDropdowns(); diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue index f73a14eb736586..7cf53a673f8d62 100644 --- a/app/assets/javascripts/webhooks/components/push_events.vue +++ b/app/assets/javascripts/webhooks/components/push_events.vue @@ -16,7 +16,28 @@ export default { GlFormInput, GlSprintf, }, - inject: ['pushEvents', 'strategy', 'isNewHook', 'pushEventsBranchFilter'], + props: { + pushEvents: { + type: Boolean, + required: true, + default: false, + }, + strategy: { + type: String, + required: true, + default: null, + }, + isNewHook: { + type: Boolean, + required: true, + default: false, + }, + pushEventsBranchFilter: { + type: String, + required: false, + default: null, + }, + }, data() { return { pushEventsData: !this.isNewHook && this.pushEvents, diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue index c91f5f680ed9d3..d088eb14567571 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue @@ -9,7 +9,7 @@ export default { GlLink, }, props: { - checked: { + value: { type: Boolean, required: false, default: false, @@ -68,7 +68,7 @@ export default { diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue new file mode 100644 index 00000000000000..57c5055dc31d6f --- /dev/null +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue @@ -0,0 +1,215 @@ + + + diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js index e6263861f7c3fb..1e5e25ce3da08d 100644 --- a/app/assets/javascripts/webhooks/index.js +++ b/app/assets/javascripts/webhooks/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import WebhookFormApp from './components/webhook_form_app.vue'; import TestDropdown from './components/test_dropdown.vue'; @@ -16,6 +17,9 @@ export default () => { urlVariables, secretToken: initialSecretToken, customHeaders, + hasGroup, + triggers: initialTriggers, + isNewHook, } = el.dataset; return new Vue({ @@ -30,6 +34,9 @@ export default () => { initialUrl, initialUrlVariables: JSON.parse(urlVariables), initialCustomHeaders: JSON.parse(customHeaders), + initialTriggers: convertObjectPropsToCamelCase(JSON.parse(initialTriggers)), + hasGroup: parseBoolean(hasGroup), + isNewHook: parseBoolean(isNewHook), }, }); }, diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb index cf110c96b1e8cc..f68a1bf42a0e93 100644 --- a/app/helpers/hooks_helper.rb +++ b/app/helpers/hooks_helper.rb @@ -8,7 +8,9 @@ def webhook_form_data(hook) secret_token: hook.masked_token, # always use masked_token to avoid exposing secret_token to frontend url: hook.url, url_variables: Gitlab::Json.dump(hook.url_variables.keys.map { { key: _1 } }), - custom_headers: Gitlab::Json.dump(hook.custom_headers.keys.map { { key: _1, value: WebHook::SECRET_MASK } }) + custom_headers: Gitlab::Json.dump(hook.custom_headers.keys.map { { key: _1, value: WebHook::SECRET_MASK } }), + is_new_hook: hook.new_record?.to_s, + triggers: Gitlab::Json.dump(all_triggers(hook)) } end @@ -56,6 +58,41 @@ def hook_log_path(hook, hook_log) admin_hook_hook_log_path(hook, hook_log) end end + + private + + def all_triggers(hook) + triggers = { + push_events: hook.push_events, + push_events_branch_filter: hook.push_events_branch_filter, + branch_filter_strategy: hook.branch_filter_strategy, + tag_push_events: hook.tag_push_events, + note_events: hook.note_events, + confidential_note_events: hook.confidential_note_events, + issues_events: hook.issues_events, + confidential_issues_events: hook.confidential_issues_events, + merge_requests_events: hook.merge_requests_events, + job_events: hook.job_events, + pipeline_events: hook.pipeline_events, + wiki_page_events: hook.wiki_page_events, + deployment_events: hook.deployment_events, + feature_flag_events: hook.feature_flag_events, + releases_events: hook.releases_events, + milestone_events: hook.milestone_events, + emoji_events: hook.emoji_events, + resource_access_token_events: hook.resource_access_token_events + } + + if hook.is_a?(GroupHook) + triggers.merge!( + member_events: hook.member_events, + project_events: hook.project_events, + subgroup_events: hook.subgroup_events + ) + end + + triggers + end end HooksHelper.prepend_mod_with('HooksHelper') diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 3be7461410d8bb..132d44a983ac1c 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,78 +1,7 @@ = form_errors(hook) +- has_group = defined?(@group) && @group.present? -.js-vue-webhook-form{ data: webhook_form_data(hook) } -.form-group - = form.label :url, s_('Webhooks|Trigger'), class: 'label-bold' - %ul.list-unstyled - %li.gl-pb-3 - .js-vue-push-events{ data: { push_events: hook.push_events.to_s, strategy: hook.branch_filter_strategy, is_new_hook: hook.new_record?.to_s, push_events_branch_filter: hook.push_events_branch_filter } } - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :tag_push_events, - integration_webhook_event_human_name(:tag_push_events), - help_text: s_('Webhooks|A new tag is pushed to the repository.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :note_events, - integration_webhook_event_human_name(:note_events), - help_text: s_('Webhooks|A comment is made or edited on an issue or merge request.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :confidential_note_events, - integration_webhook_event_human_name(:confidential_note_events), - help_text: s_('Webhooks|A comment is made or edited on a confidential issue.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :issues_events, - integration_webhook_event_human_name(:issues_events), - help_text: s_('Webhooks|An issue is created, updated, closed, or reopened.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :confidential_issues_events, - integration_webhook_event_human_name(:confidential_issues_events), - help_text: s_('Webhooks|A confidential issue is created, updated, closed, or reopened.') - - if @group - = render_if_exists 'groups/hooks/member_events', form: form - = render_if_exists 'groups/hooks/project_events', form: form - = render_if_exists 'groups/hooks/subgroup_events', form: form - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :merge_requests_events, - integration_webhook_event_human_name(:merge_requests_events), - help_text: s_('Webhooks|A merge request is created, updated, or merged.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :job_events, - integration_webhook_event_human_name(:job_events), - help_text: s_("Webhooks|A job's status changes.") - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :pipeline_events, - integration_webhook_event_human_name(:pipeline_events), - help_text: s_("Webhooks|A pipeline's status changes.") - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :wiki_page_events, - integration_webhook_event_human_name(:wiki_page_events), - help_text: s_('Webhooks|A wiki page is created or updated.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :deployment_events, - integration_webhook_event_human_name(:deployment_events), - help_text: s_('Webhooks|A deployment starts, finishes, fails, or is canceled.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :feature_flag_events, - integration_webhook_event_human_name(:feature_flag_events), - help_text: s_('Webhooks|A feature flag is turned on or off.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :releases_events, - integration_webhook_event_human_name(:releases_events), - help_text: s_('Webhooks|A release is created, updated, or deleted.') - %li.gl-pb-3 - = form.gitlab_ui_checkbox_component :milestone_events, - integration_webhook_event_human_name(:milestone_events), - help_text: s_('Webhooks|A milestone is created, closed, reopened, or deleted.') - %li.gl-pb-3 - - emoji_help_link = link_to s_('Which emoji events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'emoji-events') - = form.gitlab_ui_checkbox_component :emoji_events, - integration_webhook_event_human_name(:emoji_events), - help_text: s_('Webhooks|An emoji is awarded or revoked. %{help_link}?').html_safe % { help_link: emoji_help_link } - %li.gl-pb-3 - - access_token_help_link = link_to s_('Which project or group access token events trigger webhooks'), help_page_path('user/project/integrations/webhook_events.md', anchor: 'project-and-group-access-token-events') - = form.gitlab_ui_checkbox_component :resource_access_token_events, - integration_webhook_event_human_name(:resource_access_token_events), - help_text: s_('Webhooks|An access token expires in the next 7 days. %{help_link}?').html_safe % { help_link: access_token_help_link } - = render_if_exists 'shared/web_hooks/vulnerability_events', form: form +.js-vue-webhook-form{ data: webhook_form_data(hook).merge('has_group': has_group.to_s)} .form-group = form.label :custom_webhook_template, s_('Webhooks|Custom webhook template (optional)'), class: 'label-bold' diff --git a/ee/app/helpers/ee/hooks_helper.rb b/ee/app/helpers/ee/hooks_helper.rb index 02a1e2908f3297..e2fb41c7fdcc9e 100644 --- a/ee/app/helpers/ee/hooks_helper.rb +++ b/ee/app/helpers/ee/hooks_helper.rb @@ -38,5 +38,14 @@ def hook_log_path(hook, hook_log) super end end + + override :webhook_form_data + def webhook_form_data(hook) + data = super + triggers = ::Gitlab::Json.parse(data[:triggers]) + triggers[:vulnerability_events] = hook.vulnerability_events + data[:triggers] = ::Gitlab::Json.dump(triggers) + data + end end end diff --git a/ee/spec/helpers/ee/hooks_helper_spec.rb b/ee/spec/helpers/ee/hooks_helper_spec.rb index b6f56649c810d9..650b5f7b37b683 100644 --- a/ee/spec/helpers/ee/hooks_helper_spec.rb +++ b/ee/spec/helpers/ee/hooks_helper_spec.rb @@ -24,4 +24,12 @@ end end end + + describe '#webhook_form_data' do + it 'includes vulnerability_events in triggers' do + result = helper.webhook_form_data(group_hook) + expected_triggers = ::Gitlab::Json.parse(result[:triggers]) + expect(expected_triggers).to include('vulnerability_events' => group_hook.vulnerability_events) + end + end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 562645d06a3d43..791e1124b1c5ed 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -75220,79 +75220,154 @@ msgstr "" msgid "Webhooks help" msgstr "" -msgid "Webhooks|+ Add URL masking" +msgid "WebhooksTrigger|A comment is made or edited on a confidential issue." msgstr "" -msgid "Webhooks|+ Mask another portion of URL" +msgid "WebhooksTrigger|A comment is made or edited on an issue or merge request." msgstr "" -msgid "Webhooks|A URL is required." +msgid "WebhooksTrigger|A confidential issue is created, updated, closed, or reopened." msgstr "" -msgid "Webhooks|A comment is made or edited on a confidential issue." +msgid "WebhooksTrigger|A deployment starts, finishes, fails, or is canceled." msgstr "" -msgid "Webhooks|A comment is made or edited on an issue or merge request." +msgid "WebhooksTrigger|A feature flag is turned on or off." msgstr "" -msgid "Webhooks|A confidential issue is created, updated, closed, or reopened." +msgid "WebhooksTrigger|A group member is created, updated, or removed." msgstr "" -msgid "Webhooks|A deployment starts, finishes, fails, or is canceled." +msgid "WebhooksTrigger|A job's status changes." msgstr "" -msgid "Webhooks|A feature flag is turned on or off." +msgid "WebhooksTrigger|A merge request is created, updated, or merged." msgstr "" -msgid "Webhooks|A group member is created, updated, or removed." +msgid "WebhooksTrigger|A milestone is created, closed, reopened, or deleted." msgstr "" -msgid "Webhooks|A job's status changes." +msgid "WebhooksTrigger|A new tag is pushed to the repository." msgstr "" -msgid "Webhooks|A merge request is created, updated, or merged." +msgid "WebhooksTrigger|A pipeline's status changes." msgstr "" -msgid "Webhooks|A milestone is created, closed, reopened, or deleted." +msgid "WebhooksTrigger|A project is created or removed." msgstr "" -msgid "Webhooks|A new tag is pushed to the repository." +msgid "WebhooksTrigger|A release is created, updated, or deleted." msgstr "" -msgid "Webhooks|A pipeline's status changes." +msgid "WebhooksTrigger|A subgroup is created or removed." msgstr "" -msgid "Webhooks|A project is created or removed." +msgid "WebhooksTrigger|A vulnerability is created or updated." msgstr "" -msgid "Webhooks|A release is created, updated, or deleted." +msgid "WebhooksTrigger|A wiki page is created or updated." msgstr "" -msgid "Webhooks|A subgroup is created or removed." +msgid "WebhooksTrigger|An access token expires in the next 7 days." msgstr "" -msgid "Webhooks|A vulnerability is created or updated." +msgid "WebhooksTrigger|An emoji is awarded or revoked." msgstr "" -msgid "Webhooks|A webhook in this group was automatically disabled after it was retried multiple times." +msgid "WebhooksTrigger|An issue is created, updated, closed, or reopened." msgstr "" -msgid "Webhooks|A webhook in this project was automatically disabled after it was retried multiple times." +msgid "WebhooksTrigger|Comments" msgstr "" -msgid "Webhooks|A wiki page is created or updated." +msgid "WebhooksTrigger|Confidential comments" msgstr "" -msgid "Webhooks|Add custom header" +msgid "WebhooksTrigger|Confidential issues events" +msgstr "" + +msgid "WebhooksTrigger|Deployment events" +msgstr "" + +msgid "WebhooksTrigger|Emoji events" +msgstr "" + +msgid "WebhooksTrigger|Feature flag events" +msgstr "" + +msgid "WebhooksTrigger|Issues events" +msgstr "" + +msgid "WebhooksTrigger|Job events" +msgstr "" + +msgid "WebhooksTrigger|Member events" +msgstr "" + +msgid "WebhooksTrigger|Merge request events" +msgstr "" + +msgid "WebhooksTrigger|Milestone events" +msgstr "" + +msgid "WebhooksTrigger|Pipeline events" +msgstr "" + +msgid "WebhooksTrigger|Project events" +msgstr "" + +msgid "WebhooksTrigger|Releases events" +msgstr "" + +msgid "WebhooksTrigger|Resource access token events" +msgstr "" + +msgid "WebhooksTrigger|Subgroup events" +msgstr "" + +msgid "WebhooksTrigger|Tag push events" +msgstr "" + +msgid "WebhooksTrigger|Vulnerability events" +msgstr "" + +msgid "WebhooksTrigger|Which emoji events trigger webhooks?" msgstr "" -msgid "Webhooks|An access token expires in the next 7 days. %{help_link}?" +msgid "WebhooksTrigger|Which project or group access token events trigger webhooks?" msgstr "" -msgid "Webhooks|An emoji is awarded or revoked. %{help_link}?" +msgid "WebhooksTrigger|Wiki page events" msgstr "" -msgid "Webhooks|An issue is created, updated, closed, or reopened." +msgid "Webhooks|+ Add URL masking" +msgstr "" + +msgid "Webhooks|+ Mask another portion of URL" +msgstr "" + +msgid "Webhooks|A URL is required." +msgstr "" + +msgid "Webhooks|A group member is created, updated, or removed." +msgstr "" + +msgid "Webhooks|A project is created or removed." +msgstr "" + +msgid "Webhooks|A subgroup is created or removed." +msgstr "" + +msgid "Webhooks|A vulnerability is created or updated." +msgstr "" + +msgid "Webhooks|A webhook in this group was automatically disabled after it was retried multiple times." +msgstr "" + +msgid "Webhooks|A webhook in this project was automatically disabled after it was retried multiple times." +msgstr "" + +msgid "Webhooks|Add custom header" msgstr "" msgid "Webhooks|Are you sure you want to delete this group hook?" @@ -75677,12 +75752,6 @@ msgstr "" msgid "Which API requests are affected?" msgstr "" -msgid "Which emoji events trigger webhooks" -msgstr "" - -msgid "Which project or group access token events trigger webhooks" -msgstr "" - msgid "Who can approve?" msgstr "" diff --git a/spec/helpers/hooks_helper_spec.rb b/spec/helpers/hooks_helper_spec.rb index de96fe3152678a..e83387db68c0ca 100644 --- a/spec/helpers/hooks_helper_spec.rb +++ b/spec/helpers/hooks_helper_spec.rb @@ -8,6 +8,35 @@ let(:service_hook) { build_stubbed(:service_hook, integration: build_stubbed(:drone_ci_integration)) } let(:system_hook) { build_stubbed(:system_hook) } + let(:expected_triggers) do + triggers = { + push_events: project_hook.push_events, + push_events_branch_filter: project_hook.push_events_branch_filter, + branch_filter_strategy: project_hook.branch_filter_strategy, + tag_push_events: project_hook.tag_push_events, + note_events: project_hook.note_events, + confidential_note_events: project_hook.confidential_note_events, + issues_events: project_hook.issues_events, + confidential_issues_events: project_hook.confidential_issues_events, + merge_requests_events: project_hook.merge_requests_events, + job_events: project_hook.job_events, + pipeline_events: project_hook.pipeline_events, + wiki_page_events: project_hook.wiki_page_events, + deployment_events: project_hook.deployment_events, + feature_flag_events: project_hook.feature_flag_events, + releases_events: project_hook.releases_events, + milestone_events: project_hook.milestone_events, + emoji_events: project_hook.emoji_events, + resource_access_token_events: project_hook.resource_access_token_events + } + + if project_hook.respond_to?(:vulnerability_events) + triggers[:vulnerability_events] = project_hook.vulnerability_events + end + + triggers + end + describe '#webhook_form_data' do subject { helper.webhook_form_data(project_hook) } @@ -19,7 +48,9 @@ secret_token: nil, url: project_hook.url, url_variables: "[]", - custom_headers: "[]" + custom_headers: "[]", + is_new_hook: "false", + triggers: Gitlab::Json.dump(expected_triggers) ) end end @@ -34,7 +65,9 @@ secret_token: WebHook::SECRET_MASK, url: project_hook.url, url_variables: Gitlab::Json.dump([{ key: 'abc' }, { key: 'def' }]), - custom_headers: "[]" + custom_headers: "[]", + is_new_hook: "false", + triggers: Gitlab::Json.dump(expected_triggers) ) end end @@ -49,7 +82,9 @@ secret_token: WebHook::SECRET_MASK, url: project_hook.url, url_variables: "[]", - custom_headers: Gitlab::Json.dump([{ key: 'test', value: WebHook::SECRET_MASK }]) + custom_headers: Gitlab::Json.dump([{ key: 'test', value: WebHook::SECRET_MASK }]), + is_new_hook: "false", + triggers: Gitlab::Json.dump(expected_triggers) ) end end -- GitLab From 8b45e7b1342623863c79d8271d738da682aac21a Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Tue, 4 Nov 2025 18:03:56 -0500 Subject: [PATCH 05/18] Fix haml file --- app/views/shared/web_hooks/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 132d44a983ac1c..990f05bb41d92a 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,7 +1,7 @@ = form_errors(hook) - has_group = defined?(@group) && @group.present? -.js-vue-webhook-form{ data: webhook_form_data(hook).merge('has_group': has_group.to_s)} +.js-vue-webhook-form{ data: webhook_form_data(hook).merge('has_group': has_group.to_s) } .form-group = form.label :custom_webhook_template, s_('Webhooks|Custom webhook template (optional)'), class: 'label-bold' -- GitLab From 8a6197bc336c96998a5bc12e248672c63d8cc9cb Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Wed, 5 Nov 2025 16:42:18 -0500 Subject: [PATCH 06/18] Refactor trigger list Use a config object, start basic trigger list spec file. --- .../components/webhook_form_trigger_list.vue | 200 +++--------------- app/assets/javascripts/webhooks/constants.js | 142 +++++++++++++ .../vulnerability_events_trigger_item.vue | 2 +- locale/gitlab.pot | 6 +- .../webhook_form_trigger_list_spec.js | 44 ++++ 5 files changed, 216 insertions(+), 178 deletions(-) create mode 100644 spec/frontend/webhooks/components/webhook_form_trigger_list_spec.js diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue index 57c5055dc31d6f..da7ed24ecb962e 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue @@ -1,6 +1,7 @@ diff --git a/app/assets/javascripts/webhooks/constants.js b/app/assets/javascripts/webhooks/constants.js index 94b176e5ea00c5..e0182e07fcd30f 100644 --- a/app/assets/javascripts/webhooks/constants.js +++ b/app/assets/javascripts/webhooks/constants.js @@ -19,3 +19,145 @@ export const descriptionText = { export const MASK_ITEM_VALUE_HIDDEN = '************'; export const CUSTOM_HEADER_KEY_PATTERN = /^[A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*$/; + +export const TRIGGER_CONFIG = [ + { + key: 'tagPushEvents', + inputName: 'hook[tag_push_events]', + triggerName: 'tag-push-events', + label: s__('WebhooksTrigger|Tag push events'), + helpText: s__('WebhooksTrigger|A new tag is pushed to the repository.'), + }, + { + key: 'noteEvents', + inputName: 'hook[note_events]', + triggerName: 'note-events', + label: s__('WebhooksTrigger|Comments'), + helpText: s__('WebhooksTrigger|A comment is made or edited on an issue or merge request.'), + }, + { + key: 'confidentialNoteEvents', + inputName: 'hook[confidential_note_events]', + triggerName: 'confidential-note-events', + label: s__('WebhooksTrigger|Confidential comments'), + helpText: s__('WebhooksTrigger|A comment is made or edited on a confidential issue.'), + }, + { + key: 'issuesEvents', + inputName: 'hook[issues_events]', + triggerName: 'issues-events', + label: s__('WebhooksTrigger|Issues events'), + helpText: s__('WebhooksTrigger|An issue is created, updated, closed, or reopened.'), + }, + { + key: 'confidentialIssuesEvents', + inputName: 'hook[confidential_issues_events]', + triggerName: 'confidential-issues-events', + label: s__('WebhooksTrigger|Confidential issues events'), + helpText: s__('WebhooksTrigger|A confidential issue is created, updated, closed, or reopened.'), + }, + { + key: 'memberEvents', + inputName: 'hook[member_events]', + triggerName: 'member-events', + label: s__('WebhooksTrigger|Member events'), + helpText: s__('WebhooksTrigger|A group member is created, updated, or removed.'), + isGroupOnly: true, + }, + { + key: 'projectEvents', + inputName: 'hook[project_events]', + triggerName: 'project-events', + label: s__('WebhooksTrigger|Project events'), + helpText: s__('WebhooksTrigger|A project is created or removed.'), + isGroupOnly: true, + }, + { + key: 'subgroupEvents', + inputName: 'hook[subgroup_events]', + triggerName: 'subgroup-events', + label: s__('WebhooksTrigger|Subgroup events'), + helpText: s__('WebhooksTrigger|A subgroup is created or removed.'), + isGroupOnly: true, + }, + { + key: 'mergeRequestsEvents', + inputName: 'hook[merge_requests_events]', + triggerName: 'merge-requests-events', + label: s__('WebhooksTrigger|Merge request events'), + helpText: s__('WebhooksTrigger|A merge request is created, updated, or merged.'), + }, + { + key: 'jobEvents', + inputName: 'hook[job_events]', + triggerName: 'job-events', + label: s__('WebhooksTrigger|Job events'), + helpText: s__("WebhooksTrigger|A job's status changes."), + }, + { + key: 'pipelineEvents', + inputName: 'hook[pipeline_events]', + triggerName: 'pipeline-events', + label: s__('WebhooksTrigger|Pipeline events'), + helpText: s__("WebhooksTrigger|A pipeline's status changes."), + }, + { + key: 'wikiPageEvents', + inputName: 'hook[wiki_page_events]', + triggerName: 'wiki-page-events', + label: s__('WebhooksTrigger|Wiki page events'), + helpText: s__('WebhooksTrigger|A wiki page is created or updated.'), + }, + { + key: 'deploymentEvents', + inputName: 'hook[deployment_events]', + triggerName: 'deployment-events', + label: s__('WebhooksTrigger|Deployment events'), + helpText: s__('WebhooksTrigger|A deployment starts, finishes, fails, or is canceled.'), + }, + { + key: 'featureFlagEvents', + inputName: 'hook[feature_flag_events]', + triggerName: 'feature-flag-events', + label: s__('WebhooksTrigger|Feature flag events'), + helpText: s__('WebhooksTrigger|A feature flag is turned on or off.'), + }, + { + key: 'releasesEvents', + inputName: 'hook[releases_events]', + triggerName: 'releases-events', + label: s__('WebhooksTrigger|Releases events'), + helpText: s__('WebhooksTrigger|A release is created, updated, or deleted.'), + }, + { + key: 'milestoneEvents', + inputName: 'hook[milestone_events]', + triggerName: 'milestone-events', + label: s__('WebhooksTrigger|Milestone events'), + helpText: s__('WebhooksTrigger|A milestone is created, closed, reopened, or deleted.'), + }, + { + key: 'emojiEvents', + inputName: 'hook[emoji_events]', + triggerName: 'emoji-events', + label: s__('WebhooksTrigger|Emoji events'), + helpText: s__('WebhooksTrigger|An emoji is awarded or revoked.'), + helpLink: { + text: s__('WebhooksTrigger|Which emoji events trigger webhooks?'), + path: 'user/project/integrations/webhook_events.md', + anchor: 'emoji-events', + }, + }, + { + key: 'resourceAccessTokenEvents', + inputName: 'hook[resource_access_token_events]', + triggerName: 'resource-access-token-events', + label: s__('WebhooksTrigger|Resource access token events'), + helpText: s__('WebhooksTrigger|An access token expires in the next 7 days.'), + helpLink: { + text: s__('WebhooksTrigger|Which project or group access token events trigger webhooks?'), + path: 'user/project/integrations/webhook_events.md', + anchor: 'project-and-group-access-token-events', + }, + }, +]; diff --git a/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue b/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue index 066426427bc2d5..81336e5349c004 100644 --- a/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue +++ b/ee/app/assets/javascripts/webhooks/components/vulnerability_events_trigger_item.vue @@ -27,6 +27,6 @@ export default { input-name="hook[vulnerability_events]" trigger-name="vulnerability-events" :label="s__('WebhooksTrigger|Vulnerability events')" - :help-text="s__(`WebhooksTrigger|A vulnerability is created or updated.`)" + :help-text="s__('WebhooksTrigger|A vulnerability is created or updated.')" /> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 791e1124b1c5ed..d2188d5b44a740 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -75328,6 +75328,9 @@ msgstr "" msgid "WebhooksTrigger|Tag push events" msgstr "" +msgid "WebhooksTrigger|Trigger" +msgstr "" + msgid "WebhooksTrigger|Vulnerability events" msgstr "" @@ -75538,9 +75541,6 @@ msgstr "" msgid "Webhooks|The webhook has %{help_link_start}failed%{help_link_end} %{failure_count} times consecutively and is disabled for %{retry_time}. To re-enable the webhook earlier, see %{strong_start}Recent events%{strong_end} for more information about the error, then test your settings." msgstr "" -msgid "Webhooks|Trigger" -msgstr "" - msgid "Webhooks|URL is triggered when a member promotion request is queued, approved, or denied" msgstr "" diff --git a/spec/frontend/webhooks/components/webhook_form_trigger_list_spec.js b/spec/frontend/webhooks/components/webhook_form_trigger_list_spec.js new file mode 100644 index 00000000000000..81bb4004bc8453 --- /dev/null +++ b/spec/frontend/webhooks/components/webhook_form_trigger_list_spec.js @@ -0,0 +1,44 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +import WebhookFormTriggerList from '~/webhooks/components/webhook_form_trigger_list.vue'; + +describe('WebhookFormTriggerList', () => { + let wrapper; + + const defaultInitialTriggers = { + tagPushEvents: false, + noteEvents: false, + confidentialNoteEvents: false, + issuesEvents: false, + confidentialIssuesEvents: false, + memberEvents: false, + projectEvents: false, + subgroupEvents: false, + mergeRequestsEvents: false, + jobEvents: false, + pipelineEvents: false, + wikiPageEvents: false, + deploymentEvents: false, + featureFlagEvents: false, + releasesEvents: false, + milestoneEvents: false, + emojiEvents: false, + resourceAccessTokenEvents: false, + vulnerabilityEvents: false, + }; + + const createComponent = ({ props } = {}) => { + wrapper = shallowMountExtended(WebhookFormTriggerList, { + propsData: { + initialTriggers: defaultInitialTriggers, + hasGroup: false, + ...props, + }, + }); + }; + + it('renders a list of Triggers', () => { + createComponent(); + expect(wrapper.vm).toBe({}); + }); +}); -- GitLab From d0fa523ba47f1900872046aa773920f8e6872cb2 Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Thu, 6 Nov 2025 19:18:34 -0500 Subject: [PATCH 07/18] Update test files, fix webhook form app - Finishes the tests for the webhook_form_trigger_list - Modifies tests for webhook_form_app. --- .../webhooks/components/push_events.vue | 5 +- .../webhooks/components/webhook_form_app.vue | 3 +- .../components/webhook_form_trigger_item.vue | 1 - .../components/webhook_form_trigger_list.vue | 10 +- .../components/webhook_form_app_spec.js | 66 ++++++++ .../webhook_form_trigger_item_spec.js | 27 ++- .../webhook_form_trigger_list_spec.js | 154 +++++++++++++++++- 7 files changed, 246 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue index 7cf53a673f8d62..a346c6d8fd73b1 100644 --- a/app/assets/javascripts/webhooks/components/push_events.vue +++ b/app/assets/javascripts/webhooks/components/push_events.vue @@ -9,6 +9,7 @@ import { } from '~/webhooks/constants'; export default { + name: 'PushEvents', components: { GlFormCheckbox, GlFormRadio, @@ -25,7 +26,7 @@ export default { strategy: { type: String, required: true, - default: null, + default: '', }, isNewHook: { type: Boolean, @@ -35,7 +36,7 @@ export default { pushEventsBranchFilter: { type: String, required: false, - default: null, + default: '', }, }, data() { diff --git a/app/assets/javascripts/webhooks/components/webhook_form_app.vue b/app/assets/javascripts/webhooks/components/webhook_form_app.vue index eb65b6af53311d..e693ccb5e37e8b 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_app.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_app.vue @@ -7,6 +7,7 @@ import WebhookFormTriggerList from './webhook_form_trigger_list.vue'; import PushEvents from './push_events.vue'; export default { + name: 'WebhookFormApp', components: { GlFormGroup, GlFormInput, @@ -55,7 +56,7 @@ export default { }, hasGroup: { type: Boolean, - required: true, + required: false, default: false, }, isNewHook: { diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue index d088eb14567571..8bb27bb564cd96 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_item.vue @@ -69,7 +69,6 @@ export default { { if (obj.isGroupOnly) { return this.hasGroup; @@ -43,7 +43,6 @@ export default { -- GitLab From ae03225a41fb6ec8d4004bf9d4158c58cb6fdeca Mon Sep 17 00:00:00 2001 From: Oiza Baiye Date: Wed, 26 Nov 2025 20:14:36 -0500 Subject: [PATCH 14/18] Import push_events.vue directly in trigger list The slot-based approach in webhook_form_app is not required. --- .../webhooks/components/webhook_form_app.vue | 18 +++++------------ .../components/webhook_form_trigger_list.vue | 14 ++++++++++++- .../components/webhook_form_app_spec.js | 12 ----------- .../webhook_form_trigger_list_spec.js | 20 +++++++++---------- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/app/assets/javascripts/webhooks/components/webhook_form_app.vue b/app/assets/javascripts/webhooks/components/webhook_form_app.vue index 540ea27bc57dcb..0ed206d444783e 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_app.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_app.vue @@ -4,7 +4,6 @@ import { GlFormGroup, GlFormInput, GlFormTextarea, GlSprintf } from '@gitlab/ui' import FormUrlApp from './form_url_app.vue'; import FormCustomHeaders from './form_custom_headers.vue'; import WebhookFormTriggerList from './webhook_form_trigger_list.vue'; -import PushEvents from './push_events.vue'; export default { name: 'WebhookFormApp', @@ -15,7 +14,6 @@ export default { GlSprintf, FormUrlApp, FormCustomHeaders, - PushEvents, WebhookFormTriggerList, }, props: { @@ -69,9 +67,6 @@ export default { name: this.initialName, description: this.initialDescription, secretToken: this.initialSecretToken, - pushEvents: this.initialTriggers.pushEvents, - pushEventsBranchFilter: this.initialTriggers.pushEventsBranchFilter, - branchFilterStrategy: this.initialTriggers.branchFilterStrategy, }; }, }; @@ -128,14 +123,11 @@ export default { /> - - - + diff --git a/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue index 1e20fb430b509f..5d407ae41c80af 100644 --- a/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue +++ b/app/assets/javascripts/webhooks/components/webhook_form_trigger_list.vue @@ -4,6 +4,7 @@ import VulnerabilityEventsTriggerItem from 'ee_component/webhooks/components/vul import GroupEventsTriggerItems from 'ee_component/webhooks/components/group_events_trigger_items.vue'; import { TRIGGER_CONFIG } from '../constants'; import WebhookFormTriggerItem from './webhook_form_trigger_item.vue'; +import PushEvents from './push_events.vue'; export default { name: 'WebhookFormTriggerList', @@ -12,6 +13,7 @@ export default { GroupEventsTriggerItems, WebhookFormTriggerItem, VulnerabilityEventsTriggerItem, + PushEvents, }, props: { initialTriggers: { @@ -23,6 +25,11 @@ export default { type: Boolean, required: true, }, + isNewHook: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -35,7 +42,12 @@ export default {