Commit c7153c19 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents bf10e225 96a64f64
import axios from 'axios';
const getJwt = async () => {
return AP.context.getToken();
};
export const addSubscription = async (addPath, namespace) => {
const jwt = await getJwt();
return axios.post(addPath, {
jwt,
namespace_path: namespace,
});
};
export const removeSubscription = async (removePath) => {
const jwt = await getJwt();
return axios.delete(removePath, {
params: {
jwt,
},
});
};
import Vue from 'vue'; import Vue from 'vue';
import $ from 'jquery'; import $ from 'jquery';
import App from './components/app.vue'; import App from './components/app.vue';
import { addSubscription, removeSubscription } from '~/jira_connect/api';
const store = { const store = {
state: { state: {
...@@ -35,38 +36,25 @@ const initJiraFormHandlers = () => { ...@@ -35,38 +36,25 @@ const initJiraFormHandlers = () => {
}); });
$('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) { $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) {
const actionUrl = $(this).attr('action'); const addPath = $(this).attr('action');
const namespace = $('#namespace-input').val();
e.preventDefault(); e.preventDefault();
AP.context.getToken((token) => { addSubscription(addPath, namespace)
// eslint-disable-next-line no-jquery/no-ajax .then(reqComplete)
$.post(actionUrl, { .catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
jwt: token,
namespace_path: $('#namespace-input').val(),
format: 'json',
})
.done(reqComplete)
.fail((err) => reqFailed(err, 'Failed to add namespace. Please try again.'));
});
}); });
$('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) { $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) {
const href = $(this).attr('href'); const removePath = $(this).attr('href');
e.preventDefault(); e.preventDefault();
AP.context.getToken((token) => { removeSubscription(removePath)
// eslint-disable-next-line no-jquery/no-ajax .then(reqComplete)
$.ajax({ .catch((err) =>
url: href, reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
method: 'DELETE', );
data: {
jwt: token,
format: 'json',
},
})
.done(reqComplete)
.fail((err) => reqFailed(err, 'Failed to remove namespace. Please try again.'));
});
}); });
}; };
......
...@@ -196,6 +196,10 @@ module Issuable ...@@ -196,6 +196,10 @@ module Issuable
is_a?(Issue) is_a?(Issue)
end end
def supports_assignee?
false
end
def severity def severity
return IssuableSeverity::DEFAULT unless supports_severity? return IssuableSeverity::DEFAULT unless supports_severity?
......
...@@ -9,7 +9,9 @@ module IssueAvailableFeatures ...@@ -9,7 +9,9 @@ module IssueAvailableFeatures
class_methods do class_methods do
# EE only features are listed on EE::IssueAvailableFeatures # EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types def available_features_for_issue_types
{}.with_indifferent_access {
assignee: %w(issue incident)
}.with_indifferent_access
end end
end end
......
...@@ -434,6 +434,10 @@ class Issue < ApplicationRecord ...@@ -434,6 +434,10 @@ class Issue < ApplicationRecord
moved_to || duplicated_to moved_to || duplicated_to
end end
def supports_assignee?
issue_type_supports?(:assignee)
end
private private
def ensure_metrics def ensure_metrics
......
...@@ -1774,6 +1774,10 @@ class MergeRequest < ApplicationRecord ...@@ -1774,6 +1774,10 @@ class MergeRequest < ApplicationRecord
false false
end end
def supports_assignee?
true
end
private private
def with_rebase_lock def with_rebase_lock
......
...@@ -104,6 +104,16 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity ...@@ -104,6 +104,16 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).api_unapprove_path presenter(merge_request).api_unapprove_path
end end
expose :blob_path do
expose :head_path, if: -> (mr, _) { mr.source_branch_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.source_branch_sha)
end
expose :base_path, if: -> (mr, _) { mr.diff_base_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.diff_base_sha)
end
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request
......
...@@ -115,16 +115,6 @@ class MergeRequestWidgetEntity < Grape::Entity ...@@ -115,16 +115,6 @@ class MergeRequestWidgetEntity < Grape::Entity
end end
end end
expose :blob_path do
expose :head_path, if: -> (mr, _) { mr.source_branch_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.source_branch_sha)
end
expose :base_path, if: -> (mr, _) { mr.diff_base_sha } do |merge_request|
project_blob_path(merge_request.project, merge_request.diff_base_sha)
end
end
expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do
expose :head_path do |merge_request| expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:codequality) head_pipeline_downloadable_path_for_report_type(:codequality)
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
GlTooltipDirective, GlTooltipDirective,
GlIcon, GlIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import DevopsAdoptionTableCellFlag from './devops_adoption_table_cell_flag.vue'; import DevopsAdoptionTableCellFlag from './devops_adoption_table_cell_flag.vue';
import DevopsAdoptionDeleteModal from './devops_adoption_delete_modal.vue'; import DevopsAdoptionDeleteModal from './devops_adoption_delete_modal.vue';
import { import {
...@@ -14,17 +15,38 @@ import { ...@@ -14,17 +15,38 @@ import {
DEVOPS_ADOPTION_STRINGS, DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_SEGMENT_MODAL_ID, DEVOPS_ADOPTION_SEGMENT_MODAL_ID,
DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID, DEVOPS_ADOPTION_SEGMENT_DELETE_MODAL_ID,
DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_BY_STORAGE_KEY,
DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_DESC_STORAGE_KEY,
} from '../constants'; } from '../constants';
const NAME_HEADER = 'name';
const formatter = (value, key, item) => {
if (key === NAME_HEADER) {
return value;
}
if (item.latestSnapshot && item.latestSnapshot[key] === false) {
return 1;
} else if (item.latestSnapshot && item.latestSnapshot[key]) {
return 2;
}
return 0;
};
const fieldOptions = { const fieldOptions = {
thClass: 'gl-bg-white! gl-text-gray-400', thClass: 'gl-bg-white! gl-text-gray-400',
thAttr: { 'data-testid': DEVOPS_ADOPTION_TABLE_TEST_IDS.TABLE_HEADERS }, thAttr: { 'data-testid': DEVOPS_ADOPTION_TABLE_TEST_IDS.TABLE_HEADERS },
formatter,
sortable: true,
sortByFormatted: true,
}; };
const { table: i18n } = DEVOPS_ADOPTION_STRINGS; const { table: i18n } = DEVOPS_ADOPTION_STRINGS;
const headers = [ const headers = [
'name', NAME_HEADER,
'issueOpened', 'issueOpened',
'mergeRequestOpened', 'mergeRequestOpened',
'mergeRequestApproved', 'mergeRequestApproved',
...@@ -41,6 +63,7 @@ export default { ...@@ -41,6 +63,7 @@ export default {
DevopsAdoptionTableCellFlag, DevopsAdoptionTableCellFlag,
GlButton, GlButton,
GlPopover, GlPopover,
LocalStorageSync,
DevopsAdoptionDeleteModal, DevopsAdoptionDeleteModal,
GlIcon, GlIcon,
}, },
...@@ -57,9 +80,12 @@ export default { ...@@ -57,9 +80,12 @@ export default {
key: 'actions', key: 'actions',
tdClass: 'actions-cell', tdClass: 'actions-cell',
...fieldOptions, ...fieldOptions,
sortable: false,
}, },
], ],
testids: DEVOPS_ADOPTION_TABLE_TEST_IDS, testids: DEVOPS_ADOPTION_TABLE_TEST_IDS,
sortByStorageKey: DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_BY_STORAGE_KEY,
sortDescStorageKey: DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_DESC_STORAGE_KEY,
props: { props: {
segments: { segments: {
type: Array, type: Array,
...@@ -71,6 +97,12 @@ export default { ...@@ -71,6 +97,12 @@ export default {
default: null, default: null,
}, },
}, },
data() {
return {
sortBy: NAME_HEADER,
sortDesc: false,
};
},
methods: { methods: {
popoverContainerId(name) { popoverContainerId(name) {
return `popover_container_id_for_${name}`; return `popover_container_id_for_${name}`;
...@@ -89,9 +121,23 @@ export default { ...@@ -89,9 +121,23 @@ export default {
</script> </script>
<template> <template>
<div> <div>
<local-storage-sync
v-model="sortBy"
:storage-key="$options.sortByStorageKey"
:data-testid="$options.testids.LOCAL_STORAGE_SORT_BY"
as-json
/>
<local-storage-sync
v-model="sortDesc"
:storage-key="$options.sortDescStorageKey"
:data-testid="$options.testids.LOCAL_STORAGE_SORT_DESC"
as-json
/>
<gl-table <gl-table
:fields="$options.tableHeaderFields" :fields="$options.tableHeaderFields"
:items="segments" :items="segments"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100" thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
stacked="sm" stacked="sm"
> >
......
...@@ -116,4 +116,12 @@ export const DEVOPS_ADOPTION_TABLE_TEST_IDS = { ...@@ -116,4 +116,12 @@ export const DEVOPS_ADOPTION_TABLE_TEST_IDS = {
DEPLOYS: 'deploysCol', DEPLOYS: 'deploysCol',
ACTIONS: 'actionsCol', ACTIONS: 'actionsCol',
SCANNING: 'scanningCol', SCANNING: 'scanningCol',
LOCAL_STORAGE_SORT_BY: 'localStorageSortBy',
LOCAL_STORAGE_SORT_DESC: 'localStorageSortDesc',
}; };
export const DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_BY_STORAGE_KEY =
'devops_adoption_segments_table_sort_by';
export const DEVOPS_ADOPTION_SEGMENTS_TABLE_SORT_DESC_STORAGE_KEY =
'devops_adoption_segments_table_sort_desc';
...@@ -6,7 +6,7 @@ module EE ...@@ -6,7 +6,7 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
def supports_epic? def supports_epic?
is_a?(Issue) && issue_type_supports?(:epics) && project.group.present? false
end end
def supports_health_status? def supports_health_status?
......
...@@ -251,6 +251,11 @@ module EE ...@@ -251,6 +251,11 @@ module EE
super || promoted_to_epic super || promoted_to_epic
end end
override :supports_epic?
def supports_epic?
issue_type_supports?(:epics) && project.group.present?
end
private private
def blocking_issues_ids def blocking_issues_ids
......
---
title: Add sorting by column in DevOps Adoption table
merge_request: 50743
author:
type: added
---
title: Block /assign quick action for test cases
merge_request: 50396
author:
type: other
...@@ -14,7 +14,8 @@ module EE ...@@ -14,7 +14,8 @@ module EE
params '@user1 @user2' params '@user1 @user2'
types Issue, MergeRequest types Issue, MergeRequest
condition do condition do
quick_action_target.allows_multiple_assignees? && quick_action_target.supports_assignee? &&
quick_action_target.allows_multiple_assignees? &&
quick_action_target.persisted? && quick_action_target.persisted? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end end
......
import { GlTable, GlButton, GlIcon } from '@gitlab/ui'; import { GlTable, GlButton, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import DevopsAdoptionTable from 'ee/admin/dev_ops_report/components/devops_adoption_table.vue'; import DevopsAdoptionTable from 'ee/admin/dev_ops_report/components/devops_adoption_table.vue';
import DevopsAdoptionTableCellFlag from 'ee/admin/dev_ops_report/components/devops_adoption_table_cell_flag.vue'; import DevopsAdoptionTableCellFlag from 'ee/admin/dev_ops_report/components/devops_adoption_table_cell_flag.vue';
import { DEVOPS_ADOPTION_TABLE_TEST_IDS as TEST_IDS } from 'ee/admin/dev_ops_report/constants'; import { DEVOPS_ADOPTION_TABLE_TEST_IDS as TEST_IDS } from 'ee/admin/dev_ops_report/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { devopsAdoptionSegmentsData, devopsAdoptionTableHeaders } from '../mock_data'; import { devopsAdoptionSegmentsData, devopsAdoptionTableHeaders } from '../mock_data';
describe('DevopsAdoptionTable', () => { describe('DevopsAdoptionTable', () => {
...@@ -21,6 +23,7 @@ describe('DevopsAdoptionTable', () => { ...@@ -21,6 +23,7 @@ describe('DevopsAdoptionTable', () => {
}; };
beforeEach(() => { beforeEach(() => {
localStorage.clear();
createComponent(); createComponent();
}); });
...@@ -39,6 +42,9 @@ describe('DevopsAdoptionTable', () => { ...@@ -39,6 +42,9 @@ describe('DevopsAdoptionTable', () => {
const findColSubComponent = (colTestId, childComponent) => const findColSubComponent = (colTestId, childComponent) =>
findCol(colTestId).find(childComponent); findCol(colTestId).find(childComponent);
const findSortByLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(0);
const findSortDescLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(1);
describe('table headings', () => { describe('table headings', () => {
let headers; let headers;
...@@ -60,7 +66,7 @@ describe('DevopsAdoptionTable', () => { ...@@ -60,7 +66,7 @@ describe('DevopsAdoptionTable', () => {
}); });
it(`displays the correct table heading text for "${label}"`, () => { it(`displays the correct table heading text for "${label}"`, () => {
expect(headerWrapper.text()).toBe(label); expect(headerWrapper.text()).toContain(label);
}); });
describe(`helper information for "${label}"`, () => { describe(`helper information for "${label}"`, () => {
...@@ -142,4 +148,42 @@ describe('DevopsAdoptionTable', () => { ...@@ -142,4 +148,42 @@ describe('DevopsAdoptionTable', () => {
expect(button.props('category')).toBe('tertiary'); expect(button.props('category')).toBe('tertiary');
}); });
}); });
describe('sorting', () => {
let headers;
beforeEach(() => {
headers = findTable().findAll(`[data-testid="${TEST_IDS.TABLE_HEADERS}"]`);
});
it('sorts the segments by name', async () => {
expect(findCol(TEST_IDS.SEGMENT).text()).toBe('Segment 1');
headers.at(0).trigger('click');
await nextTick();
expect(findCol(TEST_IDS.SEGMENT).text()).toBe('Segment 2');
});
it('should update local storage when the sort column changes', async () => {
expect(findSortByLocalStorageSync().props('value')).toBe('name');
headers.at(1).trigger('click');
await nextTick();
expect(findSortByLocalStorageSync().props('value')).toBe('issueOpened');
});
it('should update local storage when the sort direction changes', async () => {
expect(findSortDescLocalStorageSync().props('value')).toBe(false);
headers.at(0).trigger('click');
await nextTick();
expect(findSortDescLocalStorageSync().props('value')).toBe(true);
});
});
}); });
...@@ -44,6 +44,16 @@ RSpec.describe QuickActions::InterpretService do ...@@ -44,6 +44,16 @@ RSpec.describe QuickActions::InterpretService do
expect(updates[:assignee_ids]).to match_array([user.id, user3.id]) expect(updates[:assignee_ids]).to match_array([user.id, user3.id])
end end
context 'with test_case issue type' do
it 'does not mark to update assignee' do
test_case = create(:quality_test_case, project: project)
_, updates = service.execute("/assign @#{user3.username}", test_case)
expect(updates[:assignee_ids]).to eq(nil)
end
end
context 'assign command with multiple assignees' do context 'assign command with multiple assignees' do
it 'fetches assignee and populates assignee_ids if content contains /assign' do it 'fetches assignee and populates assignee_ids if content contains /assign' do
issue.update!(assignee_ids: [user.id]) issue.update!(assignee_ids: [user.id])
...@@ -207,6 +217,16 @@ RSpec.describe QuickActions::InterpretService do ...@@ -207,6 +217,16 @@ RSpec.describe QuickActions::InterpretService do
expect(updates[:assignee_ids]).to match_array([current_user.id]) expect(updates[:assignee_ids]).to match_array([current_user.id])
end end
context 'with test_case issue type' do
it 'does not mark to update assignee' do
test_case = create(:quality_test_case, project: project)
_, updates = service.execute("/reassign @#{current_user.username}", test_case)
expect(updates[:assignee_ids]).to eq(nil)
end
end
end end
end end
......
...@@ -26,7 +26,7 @@ module Gitlab ...@@ -26,7 +26,7 @@ module Gitlab
end end
types Issue, MergeRequest types Issue, MergeRequest
condition do condition do
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) quick_action_target.supports_assignee? && current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end end
parse_params do |assignee_param| parse_params do |assignee_param|
extract_users(assignee_param) extract_users(assignee_param)
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { addSubscription, removeSubscription } from '~/jira_connect/api';
describe('JiraConnect API', () => {
let mock;
let response;
const mockAddPath = 'addPath';
const mockRemovePath = 'removePath';
const mockNamespace = 'namespace';
const mockJwt = 'jwt';
const mockResponse = { success: true };
const tokenSpy = jest.fn().mockReturnValue(mockJwt);
window.AP = {
context: {
getToken: tokenSpy,
},
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
response = null;
});
describe('addSubscription', () => {
const makeRequest = () => addSubscription(mockAddPath, mockNamespace);
it('returns success response', async () => {
jest.spyOn(axios, 'post');
mock
.onPost(mockAddPath, {
jwt: mockJwt,
namespace_path: mockNamespace,
})
.replyOnce(httpStatus.OK, mockResponse);
response = await makeRequest();
expect(tokenSpy).toHaveBeenCalled();
expect(axios.post).toHaveBeenCalledWith(mockAddPath, {
jwt: mockJwt,
namespace_path: mockNamespace,
});
expect(response.data).toEqual(mockResponse);
});
});
describe('removeSubscription', () => {
const makeRequest = () => removeSubscription(mockRemovePath);
it('returns success response', async () => {
jest.spyOn(axios, 'delete');
mock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse);
response = await makeRequest();
expect(tokenSpy).toHaveBeenCalled();
expect(axios.delete).toHaveBeenCalledWith(mockRemovePath, {
params: {
jwt: mockJwt,
},
});
expect(response.data).toEqual(mockResponse);
});
});
});
...@@ -8,6 +8,7 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do ...@@ -8,6 +8,7 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
let_it_be(:project, refind: true) { create :project, :repository } let_it_be(:project, refind: true) { create :project, :repository }
let_it_be(:resource, refind: true) { create(:merge_request, source_project: project, target_project: project) } let_it_be(:resource, refind: true) { create(:merge_request, source_project: project, target_project: project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:request) { double('request', current_user: user, project: project) } let(:request) { double('request', current_user: user, project: project) }
...@@ -25,6 +26,17 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do ...@@ -25,6 +26,17 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
expect(subject[:merge_status]).to eq 'checking' expect(subject[:merge_status]).to eq 'checking'
end end
it 'has blob path data' do
allow(resource).to receive_messages(
base_pipeline: pipeline,
head_pipeline: pipeline
)
expect(subject).to include(:blob_path)
expect(subject[:blob_path]).to include(:base_path)
expect(subject[:blob_path]).to include(:head_path)
end
describe 'diverged_commits_count' do describe 'diverged_commits_count' do
context 'when MR open and its diverging' do context 'when MR open and its diverging' do
it 'returns diverged commits count' do it 'returns diverged commits count' do
......
...@@ -76,17 +76,6 @@ RSpec.describe MergeRequestWidgetEntity do ...@@ -76,17 +76,6 @@ RSpec.describe MergeRequestWidgetEntity do
.to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}.diff") .to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}.diff")
end end
it 'has blob path data' do
allow(resource).to receive_messages(
base_pipeline: pipeline,
head_pipeline: pipeline
)
expect(subject).to include(:blob_path)
expect(subject[:blob_path]).to include(:base_path)
expect(subject[:blob_path]).to include(:head_path)
end
describe 'codequality report artifacts', :request_store do describe 'codequality report artifacts', :request_store do
let(:merge_base_pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) } let(:merge_base_pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) }
......
...@@ -777,6 +777,11 @@ RSpec.describe QuickActions::InterpretService do ...@@ -777,6 +777,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue } let(:issuable) { issue }
end end
it_behaves_like 'assign command' do
let(:content) { "/assign @#{developer.username}" }
let(:issuable) { create(:incident, project: project) }
end
it_behaves_like 'assign command' do it_behaves_like 'assign command' do
let(:content) { "/assign @#{developer.username}" } let(:content) { "/assign @#{developer.username}" }
let(:issuable) { merge_request } let(:issuable) { merge_request }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment