-
-
-
- {{ s__('Badges|This group has no badges') }}
- {{ s__('Badges|This project has no badges') }}
-
-
-
+
+
+
+
+
+ {{ badgeKindText(item) }}
+
+
+
+
+
+
+
+ {{ item.linkUrl }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
deleted file mode 100644
index 4c2b700c7ff..00000000000
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
- {{ badgeKindText }}
-
-
{{ badge.linkUrl }}
-
-
-
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 09f997d73aa..6fe772e717c 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -1,5 +1,5 @@
+
+
+
+
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
index b2c9290ae30..6c861e18a2f 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
@@ -1,14 +1,9 @@
-
-
-
-
{{ $options.i18n.emptyState }}
-
-
{{ $options.i18n.addBranchRule }}
-
+
+
+
+
+ {{ __('Branch Rules') }}
+
+
+
+ {{ branchRules.length }}
+
+
+ {{ $options.i18n.addBranchRule }}
+
+
+
+
+ {{ $options.i18n.emptyState }}
+
+
{{ $options.i18n.branchRuleModalDescription }}
{{ $options.i18n.branchRuleModalContent }}
-
+
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index a5ff478a826..f45a5b12db6 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -6,7 +6,7 @@ import { getAccessLevels } from '../../../utils';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
- detailsButtonLabel: s__('BranchRules|Details'),
+ detailsButtonLabel: s__('BranchRules|View details'),
allowForcePush: s__('BranchRules|Allowed to force push'),
codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
statusChecks: s__('BranchRules|%{total} status %{subject}'),
@@ -153,28 +153,36 @@ export default {
-
-
-
{{ name }}
-
-
{{
- $options.i18n.defaultLabel
- }}
-
-
{{
- $options.i18n.protectedLabel
- }}
-
-
-
-
- {{ $options.i18n.detailsButtonLabel }}
+
-
+
+
{{ name }}
+
+
{{
+ $options.i18n.defaultLabel
+ }}
+
+
{{
+ $options.i18n.protectedLabel
+ }}
+
+
+
+
+ {{ $options.i18n.detailsButtonLabel }}
+
+
diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb
index 3ba911dbcc5..59aafc32d94 100644
--- a/app/models/concerns/enums/sbom.rb
+++ b/app/models/concerns/enums/sbom.rb
@@ -26,7 +26,9 @@ module Enums
end
def self.purl_types
- PURL_TYPES
+ # return 0 by default if the purl_type is not found, to prevent
+ # consumers from producing invalid SQL caused by null entries
+ @_purl_types ||= PURL_TYPES.dup.tap { |h| h.default = 0 }
end
end
end
diff --git a/app/serializers/profile/event_entity.rb b/app/serializers/profile/event_entity.rb
index b769a80ef58..f3c1a927084 100644
--- a/app/serializers/profile/event_entity.rb
+++ b/app/serializers/profile/event_entity.rb
@@ -51,7 +51,7 @@ module Profile
expose(:id) { |event| event.target.id }
expose(:target_type, as: :type)
expose(:target_title, as: :title)
- expose(:issue_type, if: ->(event) { event.work_item? }) do |event|
+ expose(:issue_type, if: ->(event) { event.work_item? || event.issue? }) do |event|
event.target.issue_type
end
diff --git a/app/views/projects/branch_rules/_show.html.haml b/app/views/projects/branch_rules/_show.html.haml
index 605715e2899..ed7561122c0 100644
--- a/app/views/projects/branch_rules/_show.html.haml
+++ b/app/views/projects/branch_rules/_show.html.haml
@@ -12,5 +12,5 @@
= _('Define rules for who can push, merge, and the required approvals for each branch.')
= link_to(_('Leave feedback.'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/388149', target: '_blank', rel: 'noopener noreferrer')
- .settings-content.gl-pr-0
+ .settings-content
#js-branch-rules{ data: { project_path: @project.full_path, branch_rules_path: project_settings_repository_branch_rules_path(@project), show_code_owners: show_code_owners.to_s, show_status_checks: show_status_checks.to_s, show_approvers: show_approvers.to_s } }
diff --git a/lib/sbom/purl_type/converter.rb b/lib/sbom/purl_type/converter.rb
index e02d6932167..bfcfb414180 100644
--- a/lib/sbom/purl_type/converter.rb
+++ b/lib/sbom/purl_type/converter.rb
@@ -14,15 +14,24 @@ module Sbom
'composer' => 'composer',
'conan' => 'conan',
'go' => 'golang',
+ 'gobinary' => 'golang', # this package manager is generated by trivy
'nuget' => 'nuget',
'pip' => 'pypi',
'pipenv' => 'pypi',
- 'setuptools' => 'pypi'
+ 'setuptools' => 'pypi',
+ 'python-pkg' => 'pypi' # this package manager is generated by trivy
}.with_indifferent_access.freeze
def self.purl_type_for_pkg_manager(package_manager)
+ matches = package_manager.match(TRIVY_PACKAGE_MANAGER_REGEX)
+
+ package_manager = matches['trivy-package-manager-type'] if matches
+
PACKAGE_MANAGER_TO_PURL_TYPE_MAP[package_manager]
end
+
+ TRIVY_PACKAGE_MANAGER_REGEX = /\((?
.*?)\)/
+ private_constant :TRIVY_PACKAGE_MANAGER_REGEX
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d87aaee2558..0cc4b7364d3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7108,12 +7108,18 @@ msgstr ""
msgid "BackgroundMigrations|Started at"
msgstr ""
+msgid "Badge"
+msgstr ""
+
msgid "Badges"
msgstr ""
msgid "Badges|Add badge"
msgstr ""
+msgid "Badges|Add new badge"
+msgstr ""
+
msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
msgstr ""
@@ -7132,6 +7138,9 @@ msgstr ""
msgid "Badges|Deleting the badge failed, please try again."
msgstr ""
+msgid "Badges|Edit badge"
+msgstr ""
+
msgid "Badges|Enter a valid URL"
msgstr ""
@@ -7174,10 +7183,10 @@ msgstr ""
msgid "Badges|The badge was deleted."
msgstr ""
-msgid "Badges|This group has no badges"
+msgid "Badges|This group has no badges, start by creating a new one above."
msgstr ""
-msgid "Badges|This project has no badges"
+msgid "Badges|This project has no badges, start by creating a new one above."
msgstr ""
msgid "Badges|You are going to delete this badge. Deleted badges %{strongStart}cannot%{strongEnd} be restored."
@@ -8190,6 +8199,9 @@ msgstr ""
msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
msgstr ""
+msgid "Branch Rules"
+msgstr ""
+
msgid "Branch already exists"
msgstr ""
@@ -8295,9 +8307,6 @@ msgstr ""
msgid "BranchRules|Create wildcard: %{searchTerm}"
msgstr ""
-msgid "BranchRules|Details"
-msgstr ""
-
msgid "BranchRules|Does not allow force push"
msgstr ""
@@ -8367,6 +8376,9 @@ msgstr ""
msgid "BranchRules|Users"
msgstr ""
+msgid "BranchRules|View details"
+msgstr ""
+
msgid "BranchRules|default"
msgstr ""
@@ -13013,6 +13025,39 @@ msgstr ""
msgid "ContributionEvent|Approved merge request %{targetLink} in %{resourceParentLink}."
msgstr ""
+msgid "ContributionEvent|Closed Epic %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed incident %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed issue %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed key result %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed merge request %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed milestone %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed objective %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed requirement %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed resource."
+msgstr ""
+
+msgid "ContributionEvent|Closed task %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
+msgid "ContributionEvent|Closed test case %{targetLink} in %{resourceParentLink}."
+msgstr ""
+
msgid "ContributionEvent|Created project %{resourceParentLink}."
msgstr ""
diff --git a/package.json b/package.json
index b9120d6188a..9c80b30bb3d 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@gitlab/cluster-client": "^1.2.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
- "@gitlab/svgs": "3.58.0",
+ "@gitlab/svgs": "3.59.0",
"@gitlab/ui": "64.20.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230802205337",
diff --git a/qa/qa/page/component/badges.rb b/qa/qa/page/component/badges.rb
index f2c5f809d8d..b1dbd08dfa5 100644
--- a/qa/qa/page/component/badges.rb
+++ b/qa/qa/page/component/badges.rb
@@ -13,7 +13,7 @@ module QA
view 'app/assets/javascripts/badges/components/badge_list.vue' do
element :badge_list_content
- element :badge_list_row
+ element :badge_list
end
view 'app/assets/javascripts/badges/components/badge.vue' do
@@ -38,7 +38,7 @@ module QA
def has_badge?(badge_name)
within_element(:badge_list_content) do
- has_element?(:badge_list_row, badge_name: badge_name)
+ has_element?(:badge_list, badge_name: badge_name)
end
end
diff --git a/spec/features/groups/settings/group_badges_spec.rb b/spec/features/groups/settings/group_badges_spec.rb
index 4a4cb297fcf..1f16a288882 100644
--- a/spec/features/groups/settings/group_badges_spec.rb
+++ b/spec/features/groups/settings/group_badges_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[0]).to have_content badge_1.link_url
expect(rows[1]).to have_content badge_2.link_url
@@ -33,6 +33,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
context 'adding a badge', :js do
it 'user can preview a badge' do
+ click_button 'Add badge'
page.within '.badge-settings form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@@ -44,6 +45,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
end
it do
+ click_button 'Add badge'
page.within '.badge-settings' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@@ -51,7 +53,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
click_button 'Add badge'
wait_for_requests
- within '.card-body' do
+ within '.gl-card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -63,32 +65,35 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
+ end
- within 'form' do
- expect(find('#badge-link-url').value).to eq badge_2.link_url
- expect(find('#badge-image-url').value).to eq badge_2.image_url
- end
+ page.within '.gl-modal' do
+ expect(find('#badge-link-url').value).to eq badge_2.link_url
+ expect(find('#badge-image-url').value).to eq badge_2.image_url
end
end
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
- within 'form' do
- fill_in 'badge-link-url', with: badge_link_url
- fill_in 'badge-image-url', with: badge_image_url
+ end
- click_button 'Save changes'
- wait_for_requests
- end
+ page.within '.gl-modal' do
+ fill_in 'badge-link-url', with: badge_link_url
+ fill_in 'badge-image-url', with: badge_image_url
- rows = all('.card-body > div')
+ click_button 'Save changes'
+ wait_for_requests
+ end
+
+ page.within '.badge-settings' do
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -102,7 +107,7 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -112,14 +117,14 @@ RSpec.describe 'Group Badges', feature_category: :groups_and_projects do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 1
expect(rows[0]).to have_content badge_1.link_url
end
diff --git a/spec/features/projects/settings/project_badges_spec.rb b/spec/features/projects/settings/project_badges_spec.rb
index 1f170300155..a66bf5cd3a9 100644
--- a/spec/features/projects/settings/project_badges_spec.rb
+++ b/spec/features/projects/settings/project_badges_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[0]).to have_content group_badge.link_url
expect(rows[1]).to have_content project_badge.link_url
@@ -33,6 +33,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
context 'adding a badge', :js do
it 'user can preview a badge' do
+ click_button 'Add badge'
page.within '.badge-settings form' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@@ -44,6 +45,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
end
it do
+ click_button 'Add badge'
page.within '.badge-settings' do
fill_in 'badge-link-url', with: badge_link_url
fill_in 'badge-image-url', with: badge_image_url
@@ -51,7 +53,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
click_button 'Add badge'
wait_for_requests
- within '.card-body' do
+ within '.gl-card-body' do
expect(find('a')[:href]).to eq badge_link_url
expect(find('a img')[:src]).to eq badge_image_url
end
@@ -63,32 +65,35 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'form is shown when clicking edit button in list' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
+ end
- within 'form' do
- expect(find('#badge-link-url').value).to eq project_badge.link_url
- expect(find('#badge-image-url').value).to eq project_badge.image_url
- end
+ page.within '.gl-modal' do
+ expect(find('#badge-link-url').value).to eq project_badge.link_url
+ expect(find('#badge-image-url').value).to eq project_badge.image_url
end
end
it 'updates a badge when submitting the edit form' do
page.within '.badge-settings' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
rows[1].find('[aria-label="Edit"]').click
- within 'form' do
- fill_in 'badge-link-url', with: badge_link_url
- fill_in 'badge-image-url', with: badge_image_url
+ end
- click_button 'Save changes'
- wait_for_requests
- end
+ page.within '.gl-modal' do
+ fill_in 'badge-link-url', with: badge_link_url
+ fill_in 'badge-image-url', with: badge_image_url
- rows = all('.card-body > div')
+ click_button 'Save changes'
+ wait_for_requests
+ end
+
+ page.within '.badge-settings' do
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
expect(rows[1]).to have_content badge_link_url
end
@@ -102,7 +107,7 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'shows a modal when deleting a badge' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
@@ -112,14 +117,14 @@ RSpec.describe 'Project Badges', feature_category: :groups_and_projects do
it 'deletes a badge when confirming the modal' do
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 2
click_delete_button(rows[1])
find('.modal .btn-danger').click
wait_for_requests
- rows = all('.card-body > div')
+ rows = all('.gl-card-body tbody tr')
expect(rows.length).to eq 1
expect(rows[0]).to have_content group_badge.link_url
end
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index d7519f1f80d..9609a29b7ed 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -49,7 +49,7 @@ describe('BadgeForm component', () => {
it('stops editing when cancel button is clicked', async () => {
createComponent({ isEditing: true });
- const cancelButton = wrapper.find('.row-content-block button');
+ const cancelButton = wrapper.findAll('[data-testid="action-buttons"] button').at(1);
await cancelButton.trigger('click');
@@ -143,13 +143,13 @@ describe('BadgeForm component', () => {
describe('if isEditing is false', () => {
const props = { isEditing: false };
- it('renders one button', () => {
+ it('renders two buttons', () => {
createComponent(props);
expect(wrapper.find('.row-content-block').exists()).toBe(false);
- const buttons = wrapper.findAll('.form-group:last-of-type button');
+ const buttons = wrapper.findAll('[data-testid="action-buttons"] button');
- expect(buttons).toHaveLength(1);
+ expect(buttons).toHaveLength(2);
const buttonAddWrapper = buttons.at(0);
expect(buttonAddWrapper.isVisible()).toBe(true);
@@ -164,15 +164,15 @@ describe('BadgeForm component', () => {
it('renders two buttons', () => {
createComponent(props);
- const buttons = wrapper.findAll('.row-content-block button');
+ const buttons = wrapper.findAll('[data-testid="action-buttons"] button');
expect(buttons).toHaveLength(2);
- const saveButton = buttons.at(1);
+ const saveButton = buttons.at(0);
expect(saveButton.isVisible()).toBe(true);
expect(saveButton.text()).toBe('Save changes');
- const cancelButton = buttons.at(0);
+ const cancelButton = buttons.at(1);
expect(cancelButton.isVisible()).toBe(true);
expect(cancelButton.text()).toBe('Cancel');
});
diff --git a/spec/frontend/badges/components/badge_list_row_spec.js b/spec/frontend/badges/components/badge_list_row_spec.js
deleted file mode 100644
index cbbeb36ff33..00000000000
--- a/spec/frontend/badges/components/badge_list_row_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mount } from '@vue/test-utils';
-
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import BadgeListRow from '~/badges/components/badge_list_row.vue';
-import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
-
-import createState from '~/badges/store/state';
-import mutations from '~/badges/store/mutations';
-import actions from '~/badges/store/actions';
-
-import { createDummyBadge } from '../dummy_badge';
-
-Vue.use(Vuex);
-
-describe('BadgeListRow component', () => {
- let badge;
- let wrapper;
- let mockedActions;
-
- const createComponent = (kind) => {
- setHTMLFixture(``);
-
- mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
-
- const store = new Vuex.Store({
- state: {
- ...createState(),
- kind: PROJECT_BADGE,
- },
- mutations,
- actions: mockedActions,
- });
-
- badge = createDummyBadge();
- badge.kind = kind;
- wrapper = mount(BadgeListRow, {
- attachTo: document.body,
- store,
- propsData: { badge },
- });
- };
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('for a project badge', () => {
- beforeEach(() => {
- createComponent(PROJECT_BADGE);
- });
-
- it('renders the badge', () => {
- const badgeImage = wrapper.find('.project-badge');
-
- expect(badgeImage.exists()).toBe(true);
- expect(badgeImage.attributes('src')).toBe(badge.renderedImageUrl);
- });
-
- it('renders the badge name', () => {
- expect(wrapper.text()).toMatch(badge.name);
- });
-
- it('renders the badge link', () => {
- expect(wrapper.text()).toMatch(badge.linkUrl);
- });
-
- it('renders the badge kind', () => {
- expect(wrapper.text()).toMatch('Project Badge');
- });
-
- it('shows edit and delete buttons', () => {
- const buttons = wrapper.findAll('.table-button-footer button');
-
- expect(buttons).toHaveLength(2);
- const editButton = buttons.at(0);
-
- expect(editButton.isVisible()).toBe(true);
- expect(editButton.element).toHaveSpriteIcon('pencil');
-
- const deleteButton = buttons.at(1);
- expect(deleteButton.isVisible()).toBe(true);
- expect(deleteButton.element).toHaveSpriteIcon('remove');
- });
-
- it('calls editBadge when clicking then edit button', async () => {
- const editButton = wrapper.find('.table-button-footer button:first-of-type');
-
- await editButton.trigger('click');
-
- expect(mockedActions.editBadge).toHaveBeenCalled();
- });
-
- it('calls updateBadgeInModal and shows modal when clicking then delete button', async () => {
- const deleteButton = wrapper.find('.table-button-footer button:last-of-type');
-
- await deleteButton.trigger('click');
-
- expect(mockedActions.updateBadgeInModal).toHaveBeenCalled();
- });
- });
-
- describe('for a group badge', () => {
- beforeEach(() => {
- createComponent(GROUP_BADGE);
- });
-
- it('renders the badge kind', () => {
- expect(wrapper.text()).toMatch('Group Badge');
- });
-
- it('hides edit and delete buttons', () => {
- const buttons = wrapper.findAll('.table-button-footer button');
-
- expect(buttons).toHaveLength(0);
- });
- });
-});
diff --git a/spec/frontend/badges/components/badge_list_spec.js b/spec/frontend/badges/components/badge_list_spec.js
index 374b7b50af4..388e091d059 100644
--- a/spec/frontend/badges/components/badge_list_spec.js
+++ b/spec/frontend/badges/components/badge_list_spec.js
@@ -1,14 +1,12 @@
+import { GlTable, GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
-import { mount } from '@vue/test-utils';
-
-import BadgeList from '~/badges/components/badge_list.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { GROUP_BADGE, PROJECT_BADGE } from '~/badges/constants';
-
import createState from '~/badges/store/state';
import mutations from '~/badges/store/mutations';
import actions from '~/badges/store/actions';
-
+import BadgeList from '~/badges/components/badge_list.vue';
import { createDummyBadge } from '../dummy_badge';
Vue.use(Vuex);
@@ -21,9 +19,16 @@ const badges = Array.from({ length: numberOfDummyBadges }).map((_, idx) => ({
describe('BadgeList component', () => {
let wrapper;
+ let mockedActions;
+
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableRow = (pos) => findTable().find('tbody').findAll('tr').at(pos);
+ const findButtons = () => wrapper.findByTestId('badge-actions').findAllComponents(GlButton);
+ const findEditButton = () => wrapper.findByTestId('edit-badge-button');
+ const findDeleteButton = () => wrapper.findByTestId('delete-badge');
const createComponent = (customState) => {
- const mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
+ mockedActions = Object.fromEntries(Object.keys(actions).map((name) => [name, jest.fn()]));
const store = new Vuex.Store({
state: {
@@ -35,28 +40,23 @@ describe('BadgeList component', () => {
actions: mockedActions,
});
- wrapper = mount(BadgeList, { store });
+ wrapper = mountExtended(BadgeList, {
+ store,
+ stubs: {
+ GlTable,
+ GlButton,
+ },
+ });
};
describe('for project badges', () => {
- it('renders a header with the badge count', () => {
- createComponent({
- kind: PROJECT_BADGE,
- badges,
- });
-
- const header = wrapper.find('.card-header');
-
- expect(header.text()).toMatchInterpolatedText('Your badges 3');
- });
-
it('renders a row for each badge', () => {
createComponent({
kind: PROJECT_BADGE,
badges,
});
- const rows = wrapper.findAll('.gl-responsive-table-row');
+ const rows = findTable().find('tbody').findAll('tr');
expect(rows).toHaveLength(numberOfDummyBadges);
});
@@ -89,4 +89,60 @@ describe('BadgeList component', () => {
expect(wrapper.text()).toMatch('This group has no badges');
});
});
+
+ describe('BadgeList item', () => {
+ beforeEach(() => {
+ createComponent({
+ kind: PROJECT_BADGE,
+ badges,
+ });
+ });
+
+ it('renders the badge', () => {
+ const badgeImage = wrapper.find('.project-badge');
+
+ expect(badgeImage.exists()).toBe(true);
+ expect(badgeImage.attributes('src')).toBe(badges[0].renderedImageUrl);
+ });
+
+ it('renders the badge name', () => {
+ const badgeCell = findTableRow(0).findAll('td').at(0);
+
+ expect(badgeCell.text()).toMatch(badges[0].name);
+ });
+
+ it('renders the badge link', () => {
+ expect(wrapper.text()).toMatch(badges[0].linkUrl);
+ });
+
+ it('renders the badge kind', () => {
+ expect(wrapper.text()).toMatch('Project Badge');
+ });
+
+ it('shows edit and delete buttons', () => {
+ expect(findButtons()).toHaveLength(2);
+
+ const editButton = findEditButton();
+
+ expect(editButton.isVisible()).toBe(true);
+ expect(editButton.element).toHaveSpriteIcon('pencil');
+
+ const deleteButton = findDeleteButton();
+
+ expect(deleteButton.isVisible()).toBe(true);
+ expect(deleteButton.element).toHaveSpriteIcon('remove');
+ });
+
+ it('calls editBadge when clicking then edit button', () => {
+ findEditButton().trigger('click');
+
+ expect(mockedActions.editBadge).toHaveBeenCalled();
+ });
+
+ it('calls updateBadgeInModal and shows modal when clicking then delete button', () => {
+ findDeleteButton().trigger('click');
+
+ expect(mockedActions.updateBadgeInModal).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/badges/components/badge_settings_spec.js b/spec/frontend/badges/components/badge_settings_spec.js
index 7ad2c99869c..dcba895edf6 100644
--- a/spec/frontend/badges/components/badge_settings_spec.js
+++ b/spec/frontend/badges/components/badge_settings_spec.js
@@ -1,10 +1,10 @@
-import { GlModal } from '@gitlab/ui';
+import { GlCard, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
-import BadgeList from '~/badges/components/badge_list.vue';
-import BadgeListRow from '~/badges/components/badge_list_row.vue';
import BadgeSettings from '~/badges/components/badge_settings.vue';
+import BadgeList from '~/badges/components/badge_list.vue';
+import BadgeForm from '~/badges/components/badge_form.vue';
import store from '~/badges/store';
import { createDummyBadge } from '../dummy_badge';
@@ -22,8 +22,10 @@ describe('BadgeSettings component', () => {
wrapper = shallowMount(BadgeSettings, {
store,
stubs: {
+ GlCard,
+ GlTable,
'badge-list': BadgeList,
- 'badge-list-row': BadgeListRow,
+ 'badge-form': BadgeForm,
},
});
};
@@ -32,35 +34,35 @@ describe('BadgeSettings component', () => {
createComponent();
});
- it('displays modal if button for deleting a badge is clicked', async () => {
- const button = wrapper.find('[data-testid="delete-badge"]');
+ it('renders a header with the badge count', () => {
+ createComponent();
- button.vm.$emit('click');
- await nextTick();
+ const cardTitle = wrapper.find('.gl-new-card-title');
+ const cardCount = wrapper.find('.gl-new-card-count');
- const modal = wrapper.findComponent(GlModal);
- expect(modal.isVisible()).toBe(true);
+ expect(cardTitle.text()).toContain('Your badges');
+ expect(cardCount.text()).toContain('1');
});
- it('displays a form to add a badge', () => {
- expect(wrapper.find('[data-testid="add-new-badge"]').isVisible()).toBe(true);
+ it('displays a table', () => {
+ expect(wrapper.findComponent(GlTable).isVisible()).toBe(true);
});
- it('displays badge list', () => {
+ it('renders badge add form', () => {
+ expect(wrapper.findComponent(BadgeForm).exists()).toBe(true);
+ });
+
+ it('renders badge list', () => {
expect(wrapper.findComponent(BadgeList).isVisible()).toBe(true);
});
describe('when editing', () => {
beforeEach(() => {
- createComponent(true);
+ createComponent({ isEditing: true });
});
it('displays a form to edit a badge', () => {
expect(wrapper.find('[data-testid="edit-badge"]').isVisible()).toBe(true);
});
-
- it('displays no badge list', () => {
- expect(wrapper.findComponent(BadgeList).isVisible()).toBe(false);
- });
});
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_closed_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_closed_spec.js
new file mode 100644
index 00000000000..19c78730828
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_closed_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { TARGET_TYPE_WORK_ITEM } from '~/contribution_events/constants';
+import {
+ eventMilestoneClosed,
+ eventIssueClosed,
+ eventMergeRequestClosed,
+ eventTaskClosed,
+ eventIncidentClosed,
+} from '../../utils';
+
+describe('ContributionEventClosed', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData }) => {
+ wrapper = shallowMountExtended(ContributionEventClosed, {
+ propsData,
+ });
+ };
+
+ describe.each`
+ event | expectedMessage | iconName | iconClass
+ ${eventMilestoneClosed()} | ${'Closed milestone %{targetLink} in %{resourceParentLink}.'} | ${'status_closed'} | ${'gl-text-blue-500'}
+ ${eventIssueClosed()} | ${'Closed issue %{targetLink} in %{resourceParentLink}.'} | ${'issue-closed'} | ${'gl-text-blue-500'}
+ ${eventMergeRequestClosed()} | ${'Closed merge request %{targetLink} in %{resourceParentLink}.'} | ${'merge-request-close'} | ${'gl-text-red-500'}
+ ${{ target: { type: 'unsupported type' } }} | ${'Closed resource.'} | ${'status_closed'} | ${'gl-text-blue-500'}
+ `(
+ 'when event target type is $event.target.type',
+ ({ event, expectedMessage, iconName, iconClass }) => {
+ it('renders `ContributionEventBase` with correct props', () => {
+ createComponent({ propsData: { event } });
+
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ message: expectedMessage,
+ iconName,
+ iconClass,
+ });
+ });
+ },
+ );
+
+ describe(`when event target type is ${TARGET_TYPE_WORK_ITEM}`, () => {
+ describe.each`
+ event | expectedMessage
+ ${eventTaskClosed()} | ${'Closed task %{targetLink} in %{resourceParentLink}.'}
+ ${eventIncidentClosed()} | ${'Closed incident %{targetLink} in %{resourceParentLink}.'}
+ ${{ target: { type: TARGET_TYPE_WORK_ITEM, issue_type: 'unsupported type' } }} | ${'Closed resource.'}
+ `('when issue type is $event.target.issue_type', ({ event, expectedMessage }) => {
+ it('renders `ContributionEventBase` with correct props', () => {
+ createComponent({ propsData: { event } });
+
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ message: expectedMessage,
+ iconName: 'status_closed',
+ iconClass: 'gl-text-blue-500',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js
index 8487f6c6ec9..d8fecdb5e8b 100644
--- a/spec/frontend/contribution_events/components/contribution_events_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_events_spec.js
@@ -8,6 +8,7 @@ import ContributionEventPushed from '~/contribution_events/components/contributi
import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
import ContributionEventCreated from '~/contribution_events/components/contribution_event/contribution_event_created.vue';
+import ContributionEventClosed from '~/contribution_events/components/contribution_event/contribution_event_closed.vue';
import {
eventApproved,
eventExpired,
@@ -17,6 +18,7 @@ import {
eventPrivate,
eventMerged,
eventCreated,
+ eventClosed,
} from '../utils';
describe('ContributionEvents', () => {
@@ -34,6 +36,7 @@ describe('ContributionEvents', () => {
eventPrivate(),
eventMerged(),
eventCreated(),
+ eventClosed(),
],
},
});
@@ -49,6 +52,7 @@ describe('ContributionEvents', () => {
${ContributionEventPrivate} | ${eventPrivate()}
${ContributionEventMerged} | ${eventMerged()}
${ContributionEventCreated} | ${eventCreated()}
+ ${ContributionEventClosed} | ${eventClosed()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {
diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js
index ba75d39a90f..dd92f6c317a 100644
--- a/spec/frontend/contribution_events/utils.js
+++ b/spec/frontend/contribution_events/utils.js
@@ -7,6 +7,7 @@ import {
EVENT_TYPE_PUSHED,
EVENT_TYPE_PRIVATE,
EVENT_TYPE_MERGED,
+ EVENT_TYPE_CLOSED,
PUSH_EVENT_REF_TYPE_BRANCH,
PUSH_EVENT_REF_TYPE_TAG,
EVENT_TYPE_CREATED,
@@ -21,6 +22,15 @@ import {
} from '~/contribution_events/constants';
const findEventByAction = (action) => () => events.find((event) => event.action === action);
+const findEventByActionAndTargetType = (action, targetType) => () =>
+ events.find((event) => event.action === action && event.target?.type === targetType);
+const findEventByActionAndIssueType = (action, issueType) => () =>
+ events.find(
+ (event) =>
+ event.action === action &&
+ event.target?.type === TARGET_TYPE_WORK_ITEM &&
+ event.target.issue_type === issueType,
+ );
export const eventApproved = findEventByAction(EVENT_TYPE_APPROVED);
@@ -62,15 +72,10 @@ export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE })
export const eventCreated = findEventByAction(EVENT_TYPE_CREATED);
-export const findCreatedEvent = (targetType) => () =>
- events.find((event) => event.action === EVENT_TYPE_CREATED && event.target?.type === targetType);
-export const findWorkItemCreatedEvent = (issueType) => () =>
- events.find(
- (event) =>
- event.action === EVENT_TYPE_CREATED &&
- event.target?.type === TARGET_TYPE_WORK_ITEM &&
- event.target.issue_type === issueType,
- );
+export const findCreatedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
+export const findWorkItemCreatedEvent = (issueType) =>
+ findEventByActionAndIssueType(EVENT_TYPE_CREATED, issueType);
export const eventProjectCreated = findCreatedEvent(undefined);
export const eventMilestoneCreated = findCreatedEvent(TARGET_TYPE_MILESTONE);
@@ -80,3 +85,18 @@ export const eventWikiPageCreated = findCreatedEvent(TARGET_TYPE_WIKI);
export const eventDesignCreated = findCreatedEvent(TARGET_TYPE_DESIGN);
export const eventTaskCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_TASK);
export const eventIncidentCreated = findWorkItemCreatedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
+
+export const eventClosed = findEventByAction(EVENT_TYPE_CLOSED);
+
+export const findClosedEvent = (targetType) =>
+ findEventByActionAndTargetType(EVENT_TYPE_CREATED, targetType);
+export const findWorkItemClosedEvent = (issueType) =>
+ findEventByActionAndIssueType(EVENT_TYPE_CLOSED, issueType);
+
+export const eventMilestoneClosed = findClosedEvent(TARGET_TYPE_MILESTONE);
+export const eventIssueClosed = findClosedEvent(TARGET_TYPE_ISSUE);
+export const eventMergeRequestClosed = findClosedEvent(TARGET_TYPE_MERGE_REQUEST);
+export const eventWikiPageClosed = findClosedEvent(TARGET_TYPE_WIKI);
+export const eventDesignClosed = findClosedEvent(TARGET_TYPE_DESIGN);
+export const eventTaskClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_TASK);
+export const eventIncidentClosed = findWorkItemClosedEvent(WORK_ITEM_ISSUE_TYPE_INCIDENT);
diff --git a/spec/lib/sbom/purl_type/converter_spec.rb b/spec/lib/sbom/purl_type/converter_spec.rb
new file mode 100644
index 00000000000..2eb35c4d079
--- /dev/null
+++ b/spec/lib/sbom/purl_type/converter_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Sbom::PurlType::Converter, feature_category: :dependency_management do
+ describe '.purl_type_for_pkg_manager' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:actual_purl_type) { described_class.purl_type_for_pkg_manager(package_manager) }
+
+ where(:given_package_manager, :expected_purl_type) do
+ 'bundler' | 'gem'
+ 'yarn' | 'npm'
+ 'npm' | 'npm'
+ 'pnpm' | 'npm'
+ 'maven' | 'maven'
+ 'sbt' | 'maven'
+ 'gradle' | 'maven'
+ 'composer' | 'composer'
+ 'conan' | 'conan'
+ 'go' | 'golang'
+ 'nuget' | 'nuget'
+ 'pip' | 'pypi'
+ 'pipenv' | 'pypi'
+ 'setuptools' | 'pypi'
+ 'Python (python-pkg)' | 'pypi'
+ 'analyzer (gobinary)' | 'golang'
+ 'unknown-pkg-manager' | nil
+ 'Python (unknown)' | nil
+ end
+
+ with_them do
+ let(:package_manager) { given_package_manager }
+
+ it 'returns the expected purl_type' do
+ expect(actual_purl_type).to eql(expected_purl_type)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/enums/sbom_spec.rb b/spec/models/concerns/enums/sbom_spec.rb
new file mode 100644
index 00000000000..41670880630
--- /dev/null
+++ b/spec/models/concerns/enums/sbom_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Enums::Sbom, feature_category: :dependency_management do
+ describe '.purl_types' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject(:actual_purl_type) { described_class.purl_types[package_manager] }
+
+ where(:given_package_manager, :expected_purl_type) do
+ :composer | 1
+ 'composer' | 1
+ :conan | 2
+ 'conan' | 2
+ :gem | 3
+ :golang | 4
+ :maven | 5
+ :npm | 6
+ :nuget | 7
+ :pypi | 8
+ :apk | 9
+ :rpm | 10
+ :deb | 11
+ :cbl_mariner | 12
+ 'unknown-pkg-manager' | 0
+ 'Python (unknown)' | 0
+ end
+
+ with_them do
+ let(:package_manager) { given_package_manager }
+
+ it 'returns the expected purl_type' do
+ expect(actual_purl_type).to eql(expected_purl_type)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/profile/event_entity_spec.rb b/spec/serializers/profile/event_entity_spec.rb
index dbd748d3b11..b1246e7e47d 100644
--- a/spec/serializers/profile/event_entity_spec.rb
+++ b/spec/serializers/profile/event_entity_spec.rb
@@ -140,6 +140,17 @@ RSpec.describe Profile::EventEntity, feature_category: :user_profile do
expect(subject[:target][:issue_type]).to eq('incident')
end
end
+
+ context 'when target is an issue' do
+ let(:issue) { build_stubbed(:issue, author: target_user, project: project) }
+ let(:event) do
+ build(:event, :created, author: target_user, project: project, target: issue)
+ end
+
+ it 'exposes `issue_type`' do
+ expect(subject[:target][:issue_type]).to eq('issue')
+ end
+ end
end
context 'with resource parent' do
diff --git a/spec/support/shared_contexts/user_contribution_events_shared_context.rb b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
index 74f25e5f4d1..54407f74bde 100644
--- a/spec/support/shared_contexts/user_contribution_events_shared_context.rb
+++ b/spec/support/shared_contexts/user_contribution_events_shared_context.rb
@@ -34,11 +34,18 @@ RSpec.shared_context 'with user contribution events' do
# closed
let_it_be(:closed_issue_event) { create(:event, :closed, author: user, project: project, target: issue) }
let_it_be(:closed_milestone_event) { create(:event, :closed, author: user, project: project, target: milestone) }
- let_it_be(:closed_incident_event) { create(:event, :closed, author: user, project: project, target: incident) }
let_it_be(:closed_merge_request_event) do
create(:event, :closed, author: user, project: project, target: merge_request)
end
+ let_it_be(:closed_task_event) do
+ create(:event, :closed, :for_work_item, author: user, project: project, target: task)
+ end
+
+ let_it_be(:closed_incident_event) do
+ create(:event, :closed, :for_work_item, author: user, project: project, target: incident)
+ end
+
# commented
let_it_be(:commented_event) do
create(:event, :commented, author: user, project: project, target: note_on_issue)
diff --git a/yarn.lock b/yarn.lock
index fc60e294131..e4d728799fe 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1127,10 +1127,10 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
-"@gitlab/svgs@3.58.0":
- version "3.58.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.58.0.tgz#cae8483c81e260af6d1d55a25235099683ed76b7"
- integrity sha512-4aCsp0sVn+XBYJAiO/7IdwVxfINBJ0bRvvuvM1R91KYjs2XFw/rtg1HPQ+9MxZHcD5x/cIdDL6dWwr3XzfFWjw==
+"@gitlab/svgs@3.59.0":
+ version "3.59.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.59.0.tgz#21090154aa7987e059264e13182c4c60e6d0d4b3"
+ integrity sha512-5+FZ0Clwtf2X6oHEEVCwbhqhmnxT8Ds1CGFxHzzWsvQ5Hkdt658BVAicsbvQSU+TuEIhnKOK3BfooyleMUwLlQ==
"@gitlab/ui@64.20.1":
version "64.20.1"