Commit 42860513 authored by Phil Hughes's avatar Phil Hughes

Merge branch '235909-make-it-clearer-what-to-do-after-adding-a-namespace' into 'master'

Display success message after successfully adding a namespace

See merge request gitlab-org/gitlab!53332
parents f62d6148 ef82e4c0
<script> <script>
import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { GlAlert, GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
import { mapState } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import { getLocation } from '~/jira_connect/api'; import { getLocation } from '~/jira_connect/api';
import { __ } from '~/locale'; import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SET_ALERT } from '../store/mutation_types';
import { retrieveAlert } from '../utils';
import GroupsList from './groups_list.vue'; import GroupsList from './groups_list.vue';
export default { export default {
...@@ -14,6 +15,8 @@ export default { ...@@ -14,6 +15,8 @@ export default {
GlButton, GlButton,
GlModal, GlModal,
GroupsList, GroupsList,
GlLink,
GlSprintf,
}, },
directives: { directives: {
GlModalDirective, GlModalDirective,
...@@ -30,7 +33,7 @@ export default { ...@@ -30,7 +33,7 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['errorMessage']), ...mapState(['alert']),
usersPathWithReturnTo() { usersPathWithReturnTo() {
if (this.location) { if (this.location) {
return `${this.usersPath}?return_to=${this.location}`; return `${this.usersPath}?return_to=${this.location}`;
...@@ -38,6 +41,9 @@ export default { ...@@ -38,6 +41,9 @@ export default {
return this.usersPath; return this.usersPath;
}, },
shouldShowAlert() {
return Boolean(this.alert?.message);
},
}, },
modal: { modal: {
cancelProps: { cancelProps: {
...@@ -45,20 +51,42 @@ export default { ...@@ -45,20 +51,42 @@ export default {
}, },
}, },
created() { created() {
this.setInitialAlert();
this.setLocation(); this.setLocation();
}, },
methods: { methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
async setLocation() { async setLocation() {
this.location = await getLocation(); this.location = await getLocation();
}, },
setInitialAlert() {
const { linkUrl, title, message, variant } = retrieveAlert() || {};
this.setAlert({ linkUrl, title, message, variant });
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-alert v-if="errorMessage" class="gl-mb-7" variant="danger" :dismissible="false"> <gl-alert
{{ errorMessage }} v-if="shouldShowAlert"
class="gl-mb-7"
:variant="alert.variant"
:title="alert.title"
@dismiss="setAlert"
>
<gl-sprintf v-if="alert.linkUrl" :message="alert.message">
<template #link="{ content }">
<gl-link :href="alert.linkUrl" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else>
{{ alert.message }}
</template>
</gl-alert> </gl-alert>
<h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
......
<script> <script>
import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui'; import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { addSubscription } from '~/jira_connect/api'; import { addSubscription } from '~/jira_connect/api';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { persistAlert } from '../utils';
export default { export default {
components: { components: {
...@@ -31,6 +33,15 @@ export default { ...@@ -31,6 +33,15 @@ export default {
addSubscription(this.subscriptionsPath, this.group.full_path) addSubscription(this.subscriptionsPath, this.group.full_path)
.then(() => { .then(() => {
persistAlert({
title: s__('Integrations|Namespace successfully linked'),
message: s__(
'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
),
linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
variant: 'success',
});
AP.navigator.reload(); AP.navigator.reload();
}) })
.catch((error) => { .catch((error) => {
......
export const defaultPerPage = 10; export const defaultPerPage = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
...@@ -6,7 +6,7 @@ import Translate from '~/vue_shared/translate'; ...@@ -6,7 +6,7 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue'; import JiraConnectApp from './components/app.vue';
import createStore from './store'; import createStore from './store';
import { SET_ERROR_MESSAGE } from './store/mutation_types'; import { SET_ALERT } from './store/mutation_types';
const store = createStore(); const store = createStore();
...@@ -17,7 +17,7 @@ const reqComplete = () => { ...@@ -17,7 +17,7 @@ const reqComplete = () => {
const reqFailed = (res, fallbackErrorMessage) => { const reqFailed = (res, fallbackErrorMessage) => {
const { error = fallbackErrorMessage } = res || {}; const { error = fallbackErrorMessage } = res || {};
store.commit(SET_ERROR_MESSAGE, error); store.commit(SET_ALERT, { message: error, variant: 'danger' });
}; };
const updateSignInLinks = async () => { const updateSignInLinks = async () => {
......
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; export const SET_ALERT = 'SET_ALERT';
import { SET_ERROR_MESSAGE } from './mutation_types'; import { SET_ALERT } from './mutation_types';
export default { export default {
[SET_ERROR_MESSAGE](state, errorMessage) { [SET_ALERT](state, { title, message, variant, linkUrl } = {}) {
state.errorMessage = errorMessage; state.alert = { title, message, variant, linkUrl };
}, },
}; };
export default () => ({ export default () => ({
errorMessage: undefined, alert: undefined,
}); });
import AccessorUtilities from '~/lib/utils/accessor';
import { ALERT_LOCALSTORAGE_KEY } from './constants';
/**
* Persist alert data to localStorage.
*/
export const persistAlert = ({ title, message, linkUrl, variant } = {}) => {
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return;
}
const payload = JSON.stringify({ title, message, linkUrl, variant });
localStorage.setItem(ALERT_LOCALSTORAGE_KEY, payload);
};
/**
* Return alert data from localStorage.
*/
export const retrieveAlert = () => {
if (!AccessorUtilities.isLocalStorageAccessSafe()) {
return null;
}
const initialAlertJSON = localStorage.getItem(ALERT_LOCALSTORAGE_KEY);
// immediately clean up
localStorage.removeItem(ALERT_LOCALSTORAGE_KEY);
if (!initialAlertJSON) {
return null;
}
return JSON.parse(initialAlertJSON);
};
---
title: Display success message after successfully adding a namespace in Jira Connect
merge_request: 53332
author:
type: added
...@@ -16380,6 +16380,9 @@ msgstr "" ...@@ -16380,6 +16380,9 @@ msgstr ""
msgid "Integrations|Linked namespaces" msgid "Integrations|Linked namespaces"
msgstr "" msgstr ""
msgid "Integrations|Namespace successfully linked"
msgstr ""
msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance." msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance."
msgstr "" msgstr ""
...@@ -16452,6 +16455,9 @@ msgstr "" ...@@ -16452,6 +16455,9 @@ msgstr ""
msgid "Integrations|You must have owner or maintainer permissions to link namespaces." msgid "Integrations|You must have owner or maintainer permissions to link namespaces."
msgstr "" msgstr ""
msgid "Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "Interactive mode" msgid "Interactive mode"
msgstr "" msgstr ""
......
import { GlAlert, GlButton, GlModal } from '@gitlab/ui'; import { GlAlert, GlButton, GlModal, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/components/app.vue'; import JiraConnectApp from '~/jira_connect/components/app.vue';
import createStore from '~/jira_connect/store'; import createStore from '~/jira_connect/store';
import { SET_ERROR_MESSAGE } from '~/jira_connect/store/mutation_types'; import { SET_ALERT } from '~/jira_connect/store/mutation_types';
import { persistAlert } from '~/jira_connect/utils';
import { __ } from '~/locale';
jest.mock('~/jira_connect/api'); jest.mock('~/jira_connect/api');
...@@ -13,18 +15,19 @@ describe('JiraConnectApp', () => { ...@@ -13,18 +15,19 @@ describe('JiraConnectApp', () => {
let store; let store;
const findAlert = () => wrapper.findComponent(GlAlert); const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertLink = () => findAlert().find(GlLink);
const findGlButton = () => wrapper.findComponent(GlButton); const findGlButton = () => wrapper.findComponent(GlButton);
const findGlModal = () => wrapper.findComponent(GlModal); const findGlModal = () => wrapper.findComponent(GlModal);
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 = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore(); store = createStore();
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(JiraConnectApp, { mountFn(JiraConnectApp, {
store, store,
...options, provide,
}), }),
); );
}; };
...@@ -68,25 +71,72 @@ describe('JiraConnectApp', () => { ...@@ -68,25 +71,72 @@ describe('JiraConnectApp', () => {
}); });
}); });
describe('alert', () => {
it.each` it.each`
errorMessage | errorShouldRender message | variant | alertShouldRender
${'Test error'} | ${true} ${'Test error'} | ${'danger'} | ${true}
${''} | ${false} ${'Test notice'} | ${'info'} | ${true}
${undefined} | ${false} ${''} | ${undefined} | ${false}
${undefined} | ${undefined} | ${false}
`( `(
'renders correct alert when errorMessage is `$errorMessage`', 'renders correct alert when message is `$message` and variant is `$variant`',
async ({ errorMessage, errorShouldRender }) => { async ({ message, alertShouldRender, variant }) => {
createComponent(); createComponent();
store.commit(SET_ERROR_MESSAGE, errorMessage); store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(errorShouldRender); const alert = findAlert();
if (errorShouldRender) {
expect(findAlert().isVisible()).toBe(errorShouldRender); expect(alert.exists()).toBe(alertShouldRender);
expect(findAlert().html()).toContain(errorMessage); if (alertShouldRender) {
expect(alert.isVisible()).toBe(alertShouldRender);
expect(alert.html()).toContain(message);
expect(alert.props('variant')).toBe(variant);
expect(findAlertLink().exists()).toBe(false);
} }
}, },
); );
it('hides alert on @dismiss event', async () => {
createComponent();
store.commit(SET_ALERT, { message: 'test message' });
await wrapper.vm.$nextTick();
findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
});
it('renders link when `linkUrl` is set', async () => {
createComponent({ mountFn: mount });
store.commit(SET_ALERT, {
message: __('test message %{linkStart}test link%{linkEnd}'),
linkUrl: 'https://gitlab.com',
});
await wrapper.vm.$nextTick();
const alertLink = findAlertLink();
expect(alertLink.exists()).toBe(true);
expect(alertLink.text()).toContain('test link');
expect(alertLink.attributes('href')).toBe('https://gitlab.com');
});
describe('when alert is set in localStoage', () => {
it('renders alert on mount', () => {
persistAlert({ message: 'error message' });
createComponent();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.html()).toContain('error message');
});
});
});
}); });
}); });
...@@ -5,8 +5,11 @@ import waitForPromises from 'helpers/wait_for_promises'; ...@@ -5,8 +5,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/api'; import * as JiraConnectApi from '~/jira_connect/api';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue'; import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
import { persistAlert } from '~/jira_connect/utils';
import { mockGroup1 } from '../mock_data'; import { mockGroup1 } from '../mock_data';
jest.mock('~/jira_connect/utils');
describe('GroupsListItem', () => { describe('GroupsListItem', () => {
let wrapper; let wrapper;
const mockSubscriptionPath = 'subscriptionPath'; const mockSubscriptionPath = 'subscriptionPath';
...@@ -85,7 +88,16 @@ describe('GroupsListItem', () => { ...@@ -85,7 +88,16 @@ describe('GroupsListItem', () => {
expect(findLinkButton().props('loading')).toBe(true); expect(findLinkButton().props('loading')).toBe(true);
await waitForPromises();
expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path); expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
expect(persistAlert).toHaveBeenCalledWith({
linkUrl: '/help/integration/jira_development_panel.html#usage',
message:
'You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
title: 'Namespace successfully linked',
variant: 'success',
});
}); });
describe('when request is successful', () => { describe('when request is successful', () => {
......
...@@ -8,11 +8,21 @@ describe('JiraConnect store mutations', () => { ...@@ -8,11 +8,21 @@ describe('JiraConnect store mutations', () => {
localState = state(); localState = state();
}); });
describe('SET_ERROR_MESSAGE', () => { describe('SET_ALERT', () => {
it('sets error message', () => { it('sets alert state', () => {
mutations.SET_ERROR_MESSAGE(localState, 'test error'); mutations.SET_ALERT(localState, {
message: 'test error',
variant: 'danger',
title: 'test title',
linkUrl: 'linkUrl',
});
expect(localState.errorMessage).toBe('test error'); expect(localState.alert).toMatchObject({
message: 'test error',
variant: 'danger',
title: 'test title',
linkUrl: 'linkUrl',
});
}); });
}); });
}); });
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { ALERT_LOCALSTORAGE_KEY } from '~/jira_connect/constants';
import { persistAlert, retrieveAlert } from '~/jira_connect/utils';
useLocalStorageSpy();
describe('JiraConnect utils', () => {
describe('alert utils', () => {
it.each`
arg | expectedRetrievedValue
${{ title: 'error' }} | ${{ title: 'error' }}
${{ title: 'error', randomKey: 'test' }} | ${{ title: 'error' }}
${{ title: 'error', message: 'error message', linkUrl: 'link', variant: 'danger' }} | ${{ title: 'error', message: 'error message', linkUrl: 'link', variant: 'danger' }}
${undefined} | ${{}}
`(
'persists and retrieves alert data from localStorage when arg is $arg',
({ arg, expectedRetrievedValue }) => {
persistAlert(arg);
expect(localStorage.setItem).toHaveBeenCalledWith(
ALERT_LOCALSTORAGE_KEY,
JSON.stringify(expectedRetrievedValue),
);
const retrievedValue = retrieveAlert();
expect(localStorage.getItem).toHaveBeenCalledWith(ALERT_LOCALSTORAGE_KEY);
expect(retrievedValue).toEqual(expectedRetrievedValue);
},
);
});
});
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