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 $ from 'jquery';
import App from './components/app.vue';
import { addSubscription, removeSubscription } from '~/jira_connect/api';
const store = {
state: {
......@@ -35,38 +36,25 @@ const initJiraFormHandlers = () => {
});
$('#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();
AP.context.getToken((token) => {
// eslint-disable-next-line no-jquery/no-ajax
$.post(actionUrl, {
jwt: token,
namespace_path: $('#namespace-input').val(),
format: 'json',
})
.done(reqComplete)
.fail((err) => reqFailed(err, 'Failed to add namespace. Please try again.'));
});
addSubscription(addPath, namespace)
.then(reqComplete)
.catch((err) => reqFailed(err.response.data, 'Failed to add namespace. Please try again.'));
});
$('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) {
const href = $(this).attr('href');
const removePath = $(this).attr('href');
e.preventDefault();
AP.context.getToken((token) => {
// eslint-disable-next-line no-jquery/no-ajax
$.ajax({
url: href,
method: 'DELETE',
data: {
jwt: token,
format: 'json',
},
})
.done(reqComplete)
.fail((err) => reqFailed(err, 'Failed to remove namespace. Please try again.'));
});
removeSubscription(removePath)
.then(reqComplete)
.catch((err) =>
reqFailed(err.response.data, 'Failed to remove namespace. Please try again.'),
);
});
};
......
......@@ -196,6 +196,10 @@ module Issuable
is_a?(Issue)
end
def supports_assignee?
false
end
def severity
return IssuableSeverity::DEFAULT unless supports_severity?
......
......@@ -9,7 +9,9 @@ module IssueAvailableFeatures
class_methods do
# EE only features are listed on EE::IssueAvailableFeatures
def available_features_for_issue_types
{}.with_indifferent_access
{
assignee: %w(issue incident)
}.with_indifferent_access
end
end
......
......@@ -434,6 +434,10 @@ class Issue < ApplicationRecord
moved_to || duplicated_to
end
def supports_assignee?
issue_type_supports?(:assignee)
end
private
def ensure_metrics
......
......@@ -1774,6 +1774,10 @@ class MergeRequest < ApplicationRecord
false
end
def supports_assignee?
true
end
private
def with_rebase_lock
......
......@@ -104,6 +104,16 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).api_unapprove_path
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
delegate :current_user, to: :request
......
......@@ -115,16 +115,6 @@ class MergeRequestWidgetEntity < Grape::Entity
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 :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:codequality)
......
......@@ -7,6 +7,7 @@ import {
GlTooltipDirective,
GlIcon,
} from '@gitlab/ui';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import DevopsAdoptionTableCellFlag from './devops_adoption_table_cell_flag.vue';
import DevopsAdoptionDeleteModal from './devops_adoption_delete_modal.vue';
import {
......@@ -14,17 +15,38 @@ import {
DEVOPS_ADOPTION_STRINGS,
DEVOPS_ADOPTION_SEGMENT_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';
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 = {
thClass: 'gl-bg-white! gl-text-gray-400',
thAttr: { 'data-testid': DEVOPS_ADOPTION_TABLE_TEST_IDS.TABLE_HEADERS },
formatter,
sortable: true,
sortByFormatted: true,
};
const { table: i18n } = DEVOPS_ADOPTION_STRINGS;
const headers = [
'name',
NAME_HEADER,
'issueOpened',
'mergeRequestOpened',
'mergeRequestApproved',
......@@ -41,6 +63,7 @@ export default {
DevopsAdoptionTableCellFlag,
GlButton,
GlPopover,
LocalStorageSync,
DevopsAdoptionDeleteModal,
GlIcon,
},
......@@ -57,9 +80,12 @@ export default {
key: 'actions',
tdClass: 'actions-cell',
...fieldOptions,
sortable: false,
},
],
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: {
segments: {
type: Array,
......@@ -71,6 +97,12 @@ export default {
default: null,
},
},
data() {
return {
sortBy: NAME_HEADER,
sortDesc: false,
};
},
methods: {
popoverContainerId(name) {
return `popover_container_id_for_${name}`;
......@@ -89,9 +121,23 @@ export default {
</script>
<template>
<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
:fields="$options.tableHeaderFields"
: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"
stacked="sm"
>
......
......@@ -116,4 +116,12 @@ export const DEVOPS_ADOPTION_TABLE_TEST_IDS = {
DEPLOYS: 'deploysCol',
ACTIONS: 'actionsCol',
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
extend ::Gitlab::Utils::Override
def supports_epic?
is_a?(Issue) && issue_type_supports?(:epics) && project.group.present?
false
end
def supports_health_status?
......
......@@ -251,6 +251,11 @@ module EE
super || promoted_to_epic
end
override :supports_epic?
def supports_epic?
issue_type_supports?(:epics) && project.group.present?
end
private
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
params '@user1 @user2'
types Issue, MergeRequest
condition do
quick_action_target.allows_multiple_assignees? &&
quick_action_target.supports_assignee? &&
quick_action_target.allows_multiple_assignees? &&
quick_action_target.persisted? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end
......
import { GlTable, GlButton, GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
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 { 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';
describe('DevopsAdoptionTable', () => {
......@@ -21,6 +23,7 @@ describe('DevopsAdoptionTable', () => {
};
beforeEach(() => {
localStorage.clear();
createComponent();
});
......@@ -39,6 +42,9 @@ describe('DevopsAdoptionTable', () => {
const findColSubComponent = (colTestId, childComponent) =>
findCol(colTestId).find(childComponent);
const findSortByLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(0);
const findSortDescLocalStorageSync = () => wrapper.findAll(LocalStorageSync).at(1);
describe('table headings', () => {
let headers;
......@@ -60,7 +66,7 @@ describe('DevopsAdoptionTable', () => {
});
it(`displays the correct table heading text for "${label}"`, () => {
expect(headerWrapper.text()).toBe(label);
expect(headerWrapper.text()).toContain(label);
});
describe(`helper information for "${label}"`, () => {
......@@ -142,4 +148,42 @@ describe('DevopsAdoptionTable', () => {
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
expect(updates[:assignee_ids]).to match_array([user.id, user3.id])
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
it 'fetches assignee and populates assignee_ids if content contains /assign' do
issue.update!(assignee_ids: [user.id])
......@@ -207,6 +217,16 @@ RSpec.describe QuickActions::InterpretService do
expect(updates[:assignee_ids]).to match_array([current_user.id])
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
......
......@@ -26,7 +26,7 @@ module Gitlab
end
types Issue, MergeRequest
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
parse_params do |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
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(:user) { create(:user) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:request) { double('request', current_user: user, project: project) }
......@@ -25,6 +26,17 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
expect(subject[:merge_status]).to eq 'checking'
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
context 'when MR open and its diverging' do
it 'returns diverged commits count' do
......
......@@ -76,17 +76,6 @@ RSpec.describe MergeRequestWidgetEntity do
.to eq("/#{resource.project.full_path}/-/merge_requests/#{resource.iid}.diff")
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
let(:merge_base_pipeline) { create(:ci_pipeline, :with_codequality_report, project: project) }
......
......@@ -777,6 +777,11 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { issue }
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
let(:content) { "/assign @#{developer.username}" }
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