Commit 4d89e6a2 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 3658a9b8 01eafdbc
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { GlAlert } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'JiraConnectApp', name: 'JiraConnectApp',
components: {
GlAlert,
},
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState(['errorMessage']), ...mapState(['errorMessage']),
...@@ -16,6 +20,12 @@ export default { ...@@ -16,6 +20,12 @@ export default {
<template> <template>
<div> <div>
<gl-alert v-if="errorMessage" class="gl-mb-6" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<h1>GitLab for Jira Configuration</h1>
<div v-if="showNewUi"> <div v-if="showNewUi">
<h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3> <h3 data-testid="new-jira-connect-ui-heading">{{ s__('Integrations|Linked namespaces') }}</h3>
</div> </div>
......
...@@ -23,11 +23,9 @@ const initJiraFormHandlers = () => { ...@@ -23,11 +23,9 @@ const initJiraFormHandlers = () => {
}; };
const reqFailed = (res, fallbackErrorMessage) => { const reqFailed = (res, fallbackErrorMessage) => {
const { responseJSON: { error = fallbackErrorMessage } = {} } = res || {}; const { error = fallbackErrorMessage } = res || {};
store.commit(SET_ERROR_MESSAGE, error); store.commit(SET_ERROR_MESSAGE, error);
// eslint-disable-next-line no-alert
alert(error);
}; };
if (typeof AP.getLocation === 'function') { if (typeof AP.getLocation === 'function') {
......
...@@ -44,13 +44,21 @@ export default { ...@@ -44,13 +44,21 @@ export default {
<template> <template>
<div data-testid="ci-lint-value"> <div data-testid="ci-lint-value">
<pre v-if="scripts.beforeScript.show" data-testid="ci-lint-before-script">{{ <pre
scripts.beforeScript.content v-if="scripts.beforeScript.show"
}}</pre> class="gl-white-space-pre-wrap"
<pre v-if="scripts.script.show" data-testid="ci-lint-script">{{ scripts.script.content }}</pre> data-testid="ci-lint-before-script"
<pre v-if="scripts.afterScript.show" data-testid="ci-lint-after-script">{{ >{{ scripts.beforeScript.content }}</pre
scripts.afterScript.content >
<pre v-if="scripts.script.show" class="gl-white-space-pre-wrap" data-testid="ci-lint-script">{{
scripts.script.content
}}</pre> }}</pre>
<pre
v-if="scripts.afterScript.show"
class="gl-white-space-pre-wrap"
data-testid="ci-lint-after-script"
>{{ scripts.afterScript.content }}</pre
>
<ul class="gl-list-style-none gl-pl-0 gl-mb-0"> <ul class="gl-list-style-none gl-pl-0 gl-mb-0">
<li v-if="tagList"> <li v-if="tagList">
......
...@@ -256,7 +256,7 @@ export default { ...@@ -256,7 +256,7 @@ export default {
return { return {
...filter, ...filter,
value: { value: {
data: stripQuotes(valueString), data: typeof valueString === 'string' ? stripQuotes(valueString) : valueString,
operator: filter.value.operator, operator: filter.value.operator,
}, },
}; };
......
@import 'mixins_and_variables_and_functions'; @import 'mixins_and_variables_and_functions';
// We should only import styles that we actually use. /**
// @import '@gitlab/ui/src/scss/gitlab_ui'; NOTE: We should only import styles that we actually use.
Ex:
@import '@gitlab/ui/src/scss/gitlab_ui';
*/
@import '@gitlab/ui/src/scss/bootstrap'; @import '@gitlab/ui/src/scss/bootstrap';
@import 'bootstrap-vue/src/index'; @import 'bootstrap-vue/src/index';
@import '@gitlab/ui/src/scss/utilities'; @import '@gitlab/ui/src/scss/utilities';
@import '@gitlab/ui/src/components/base/alert/alert';
$atlaskit-border-color: #dfe1e6; $atlaskit-border-color: #dfe1e6;
......
...@@ -9,10 +9,9 @@ ...@@ -9,10 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
.jira-connect-app .jira-connect-app
%h1
GitLab for Jira Configuration
- if current_user.blank? && @subscriptions.empty? - if current_user.blank? && @subscriptions.empty?
%h1
GitLab for Jira Configuration
%h2.heading-with-border Sign in to GitLab.com to get started. %h2.heading-with-border Sign in to GitLab.com to get started.
.gl-mt-5 .gl-mt-5
......
---
title: Improve error messages when adding namespaces in Jira Connect App
merge_request: 48651
author:
type: changed
...@@ -67,8 +67,9 @@ You can also filter epics in the Roadmap view by: ...@@ -67,8 +67,9 @@ You can also filter epics in the Roadmap view by:
- Author - Author
- Label - Label
- Milestone - Milestone
- Confidentiality of epics
![roadmap date range in weeks](img/roadmap_filters_v13_7.png) ![roadmap date range in weeks](img/roadmap_filters_v13_8.png)
Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics). Roadmaps can also be [visualized inside an epic](../epics/index.md#roadmap-in-epics).
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
GlFilteredSearchToken,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -132,10 +133,23 @@ export default { ...@@ -132,10 +133,23 @@ export default {
}); });
}, },
}, },
{
type: 'confidential',
icon: 'eye-slash',
title: __('Confidential'),
unique: true,
token: GlFilteredSearchToken,
operators: [{ value: '=', description: __('is'), default: 'true' }],
options: [
{ icon: 'eye-slash', value: true, title: __('Yes') },
{ icon: 'eye', value: false, title: __('No') },
],
},
]; ];
}, },
getFilteredSearchValue() { getFilteredSearchValue() {
const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, confidential, search } =
this.filterParams || {};
const filteredSearchValue = []; const filteredSearchValue = [];
if (authorUsername) { if (authorUsername) {
...@@ -161,6 +175,13 @@ export default { ...@@ -161,6 +175,13 @@ export default {
); );
} }
if (confidential !== undefined) {
filteredSearchValue.push({
type: 'confidential',
value: { data: confidential },
});
}
if (search) { if (search) {
filteredSearchValue.push(search); filteredSearchValue.push(search);
} }
...@@ -169,7 +190,8 @@ export default { ...@@ -169,7 +190,8 @@ export default {
}, },
updateUrl() { updateUrl() {
const queryParams = urlParamsToObject(window.location.search); const queryParams = urlParamsToObject(window.location.search);
const { authorUsername, labelName, milestoneTitle, search } = this.filterParams || {}; const { authorUsername, labelName, milestoneTitle, confidential, search } =
this.filterParams || {};
queryParams.state = this.epicsState; queryParams.state = this.epicsState;
queryParams.sort = this.sortedBy; queryParams.sort = this.sortedBy;
...@@ -191,6 +213,12 @@ export default { ...@@ -191,6 +213,12 @@ export default {
queryParams['label_name[]'] = labelName; queryParams['label_name[]'] = labelName;
} }
if (confidential !== undefined) {
queryParams.confidential = confidential;
} else {
delete queryParams.confidential;
}
if (search) { if (search) {
queryParams.search = search; queryParams.search = search;
} else { } else {
...@@ -229,6 +257,9 @@ export default { ...@@ -229,6 +257,9 @@ export default {
case 'label_name': case 'label_name':
labels.push(filter.value.data); labels.push(filter.value.data);
break; break;
case 'confidential':
filterParams.confidential = filter.value.data;
break;
default: default:
break; break;
} }
......
...@@ -9,6 +9,7 @@ fragment BaseEpic on Epic { ...@@ -9,6 +9,7 @@ fragment BaseEpic on Epic {
dueDate dueDate
hasChildren hasChildren
hasParent hasParent
confidential
descendantWeightSum { descendantWeightSum {
closedIssues closedIssues
openedIssues openedIssues
......
...@@ -9,6 +9,7 @@ query epicChildEpics( ...@@ -9,6 +9,7 @@ query epicChildEpics(
$dueDate: Time $dueDate: Time
$labelName: [String!] = [] $labelName: [String!] = []
$authorUsername: String = "" $authorUsername: String = ""
$confidential: Boolean
$search: String = "" $search: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
...@@ -25,6 +26,7 @@ query epicChildEpics( ...@@ -25,6 +26,7 @@ query epicChildEpics(
endDate: $dueDate endDate: $dueDate
labelName: $labelName labelName: $labelName
authorUsername: $authorUsername authorUsername: $authorUsername
confidential: $confidential
search: $search search: $search
) { ) {
edges { edges {
......
...@@ -9,6 +9,7 @@ query groupEpics( ...@@ -9,6 +9,7 @@ query groupEpics(
$labelName: [String!] = [] $labelName: [String!] = []
$authorUsername: String = "" $authorUsername: String = ""
$milestoneTitle: String = "" $milestoneTitle: String = ""
$confidential: Boolean
$search: String = "" $search: String = ""
) { ) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
...@@ -22,6 +23,7 @@ query groupEpics( ...@@ -22,6 +23,7 @@ query groupEpics(
labelName: $labelName labelName: $labelName
authorUsername: $authorUsername authorUsername: $authorUsername
milestoneTitle: $milestoneTitle milestoneTitle: $milestoneTitle
confidential: $confidential
search: $search search: $search
) { ) {
edges { edges {
......
...@@ -57,11 +57,17 @@ export default () => { ...@@ -57,11 +57,17 @@ export default () => {
supportedPresetTypes.indexOf(dataset.presetType) > -1 supportedPresetTypes.indexOf(dataset.presetType) > -1
? dataset.presetType ? dataset.presetType
: PRESET_TYPES.MONTHS; : PRESET_TYPES.MONTHS;
const filterParams = Object.assign( const rawFilterParams = urlParamsToObject(window.location.search.substring(1));
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search.substring(1)), { const filterParams = {
...convertObjectPropsToCamelCase(rawFilterParams, {
dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary dropKeys: ['scope', 'utf8', 'state', 'sort', 'layout'], // These keys are unsupported/unnecessary
}), }),
); // We shall put parsed value of `confidential` only
// when it is defined.
...(rawFilterParams.confidential && {
confidential: parseBoolean(rawFilterParams.confidential),
}),
};
const timeframe = getTimeframeForPreset( const timeframe = getTimeframeForPreset(
presetType, presetType,
window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH, window.innerWidth - el.offsetLeft - EPIC_DETAILS_CELL_WIDTH,
......
---
title: Add confidential filter token in Roadmap
merge_request: 51196
author:
type: added
import { GlSegmentedControl, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlSegmentedControl, GlDropdown, GlDropdownItem, GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
...@@ -89,6 +89,7 @@ describe('RoadmapFilters', () => { ...@@ -89,6 +89,7 @@ describe('RoadmapFilters', () => {
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0', milestoneTitle: '4.0',
confidential: true,
}); });
wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc'); wrapper.vm.$store.dispatch('setSortedBy', 'end_date_asc');
...@@ -97,7 +98,7 @@ describe('RoadmapFilters', () => { ...@@ -97,7 +98,7 @@ describe('RoadmapFilters', () => {
wrapper.vm.updateUrl(); wrapper.vm.updateUrl();
expect(global.window.location.href).toBe( expect(global.window.location.href).toBe(
`${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&author_username=root&milestone_title=4.0&label_name%5B%5D=Bug`, `${TEST_HOST}/?state=${EPICS_STATES.CLOSED}&sort=end_date_asc&author_username=root&milestone_title=4.0&label_name%5B%5D=Bug&confidential=true`,
); );
}); });
}); });
...@@ -143,10 +144,18 @@ describe('RoadmapFilters', () => { ...@@ -143,10 +144,18 @@ describe('RoadmapFilters', () => {
type: 'author_username', type: 'author_username',
value: { data: 'root' }, value: { data: 'root' },
}, },
{
type: 'milestone_title',
value: { data: '4.0' },
},
{ {
type: 'label_name', type: 'label_name',
value: { data: 'Bug' }, value: { data: 'Bug' },
}, },
{
type: 'confidential',
value: { data: true },
},
]; ];
let filteredSearchBar; let filteredSearchBar;
...@@ -192,6 +201,18 @@ describe('RoadmapFilters', () => { ...@@ -192,6 +201,18 @@ describe('RoadmapFilters', () => {
operators: [{ value: '=', description: 'is', default: 'true' }], operators: [{ value: '=', description: 'is', default: 'true' }],
fetchMilestones: expect.any(Function), fetchMilestones: expect.any(Function),
}, },
{
type: 'confidential',
icon: 'eye-slash',
title: 'Confidential',
unique: true,
token: GlFilteredSearchToken,
operators: [{ value: '=', description: 'is', default: 'true' }],
options: [
{ icon: 'eye-slash', value: true, title: 'Yes' },
{ icon: 'eye', value: false, title: 'No' },
],
},
]); ]);
}); });
...@@ -220,6 +241,8 @@ describe('RoadmapFilters', () => { ...@@ -220,6 +241,8 @@ describe('RoadmapFilters', () => {
wrapper.vm.$store.dispatch('setFilterParams', { wrapper.vm.$store.dispatch('setFilterParams', {
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0',
confidential: true,
}); });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -241,6 +264,8 @@ describe('RoadmapFilters', () => { ...@@ -241,6 +264,8 @@ describe('RoadmapFilters', () => {
expect(wrapper.vm.setFilterParams).toHaveBeenCalledWith({ expect(wrapper.vm.setFilterParams).toHaveBeenCalledWith({
authorUsername: 'root', authorUsername: 'root',
labelName: ['Bug'], labelName: ['Bug'],
milestoneTitle: '4.0',
confidential: true,
}); });
expect(wrapper.vm.fetchEpics).toHaveBeenCalled(); expect(wrapper.vm.fetchEpics).toHaveBeenCalled();
expect(wrapper.vm.updateUrl).toHaveBeenCalled(); expect(wrapper.vm.updateUrl).toHaveBeenCalled();
......
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { GlAlert } from '@gitlab/ui';
import JiraConnectApp from '~/jira_connect/components/app.vue'; import JiraConnectApp from '~/jira_connect/components/app.vue';
import createStore from '~/jira_connect/store';
import { SET_ERROR_MESSAGE } from '~/jira_connect/store/mutation_types';
Vue.use(Vuex);
describe('JiraConnectApp', () => { describe('JiraConnectApp', () => {
let wrapper; let wrapper;
let store;
const findAlert = () => wrapper.findComponent(GlAlert);
const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading'); const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading');
const findHeaderText = () => findHeader().text(); const findHeaderText = () => findHeader().text();
const createComponent = (options = {}) => { const createComponent = (options = {}) => {
store = createStore();
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(JiraConnectApp, { shallowMount(JiraConnectApp, {
store,
provide: { provide: {
glFeatures: { newJiraConnectUi: true }, glFeatures: { newJiraConnectUi: true },
}, },
...@@ -43,5 +55,26 @@ describe('JiraConnectApp', () => { ...@@ -43,5 +55,26 @@ describe('JiraConnectApp', () => {
expect(findHeader().exists()).toBe(false); expect(findHeader().exists()).toBe(false);
}); });
}); });
it.each`
errorMessage | errorShouldRender
${'Test error'} | ${true}
${''} | ${false}
${undefined} | ${false}
`(
'renders correct alert when errorMessage is `$errorMessage`',
async ({ errorMessage, errorShouldRender }) => {
createComponent();
store.commit(SET_ERROR_MESSAGE, errorMessage);
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(errorShouldRender);
if (errorShouldRender) {
expect(findAlert().isVisible()).toBe(errorShouldRender);
expect(findAlert().html()).toContain(errorMessage);
}
},
);
}); });
}); });
...@@ -25,6 +25,7 @@ import { ...@@ -25,6 +25,7 @@ import {
tokenValueLabel, tokenValueLabel,
tokenValueMilestone, tokenValueMilestone,
tokenValueMembership, tokenValueMembership,
tokenValueConfidential,
} from './mock_data'; } from './mock_data';
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({ jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...@@ -227,12 +228,13 @@ describe('FilteredSearchBarRoot', () => { ...@@ -227,12 +228,13 @@ describe('FilteredSearchBarRoot', () => {
}); });
describe('removeQuotesEnclosure', () => { describe('removeQuotesEnclosure', () => {
const mockFilters = [tokenValueAuthor, tokenValueLabel, 'foo']; const mockFilters = [tokenValueAuthor, tokenValueLabel, tokenValueConfidential, 'foo'];
it('returns filter array with unescaped strings for values which have spaces', () => { it('returns filter array with unescaped strings for values which have spaces', () => {
expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([ expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
tokenValueAuthor, tokenValueAuthor,
tokenValueLabel, tokenValueLabel,
tokenValueConfidential,
'foo', 'foo',
]); ]);
}); });
......
...@@ -155,6 +155,14 @@ export const tokenValueMembership = { ...@@ -155,6 +155,14 @@ export const tokenValueMembership = {
}, },
}; };
export const tokenValueConfidential = {
type: 'confidential',
value: {
operator: '=',
data: true,
},
};
export const tokenValuePlain = { export const tokenValuePlain = {
type: 'filtered-search-term', type: 'filtered-search-term',
value: { data: 'foo' }, value: { data: 'foo' },
......
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