Commit 5ec6487d authored by Tom Quirk's avatar Tom Quirk

Display success message when namespace is linked in Jira

Use localStorage to set and retrieve alert data,
used for success messages in Jira Connect app.

Here, we refactor SET_ERROR_MESSAGE to SET_ALERT.
This allows us to control the "global" gl-alert component
via the same mutation.

We also extend the gl-alert to include an optional
`linkUrl`, which is injected via gl-sprintf if it
exists.
parent db33a339
<script>
import { GlAlert, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { mapState } from 'vuex';
import { GlAlert, GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
import { mapState, mapMutations } from 'vuex';
import { getLocation } from '~/jira_connect/api';
import { __ } from '~/locale';
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';
export default {
......@@ -14,6 +15,8 @@ export default {
GlButton,
GlModal,
GroupsList,
GlLink,
GlSprintf,
},
directives: {
GlModalDirective,
......@@ -30,7 +33,7 @@ export default {
};
},
computed: {
...mapState(['errorMessage']),
...mapState(['alert']),
usersPathWithReturnTo() {
if (this.location) {
return `${this.usersPath}?return_to=${this.location}`;
......@@ -38,6 +41,18 @@ export default {
return this.usersPath;
},
alertLinkUrl() {
return this.alert?.linkUrl;
},
alertTitle() {
return this.alert?.title;
},
alertMessage() {
return this.alert?.message;
},
alertVariant() {
return this.alert?.variant;
},
},
modal: {
cancelProps: {
......@@ -45,20 +60,42 @@ export default {
},
},
created() {
this.setInitialAlert();
this.setLocation();
},
methods: {
...mapMutations({
setAlert: SET_ALERT,
}),
async setLocation() {
this.location = await getLocation();
},
setInitialAlert() {
const { linkUrl, title, message, variant } = retrieveAlert() || {};
this.setAlert({ linkUrl, title, message, variant });
},
},
};
</script>
<template>
<div>
<gl-alert v-if="errorMessage" class="gl-mb-7" variant="danger" :dismissible="false">
{{ errorMessage }}
<gl-alert
v-if="alertMessage"
class="gl-mb-7"
:variant="alertVariant"
:title="alertTitle"
@dismiss="setAlert"
>
<gl-sprintf v-if="alertLinkUrl" :message="alertMessage">
<template #link="{ content }">
<gl-link :href="alertLinkUrl" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template v-else>
{{ alertMessage }}
</template>
</gl-alert>
<h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
......
<script>
import { GlAvatar, GlButton, GlIcon } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { addSubscription } from '~/jira_connect/api';
import { s__ } from '~/locale';
import { persistAlert } from '../utils';
export default {
components: {
......@@ -31,6 +33,15 @@ export default {
addSubscription(this.subscriptionsPath, this.group.full_path)
.then(() => {
persistAlert({
title: s__('Integrations|Namespace successfully linked'),
message: s__(
'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();
})
.catch((error) => {
......
export const defaultPerPage = 10;
export const ALERT_LOCALSTORAGE_KEY = 'gitlab_alert';
......@@ -6,7 +6,7 @@ import Translate from '~/vue_shared/translate';
import JiraConnectApp from './components/app.vue';
import createStore from './store';
import { SET_ERROR_MESSAGE } from './store/mutation_types';
import { SET_ALERT } from './store/mutation_types';
const store = createStore();
......@@ -17,7 +17,7 @@ const reqComplete = () => {
const reqFailed = (res, fallbackErrorMessage) => {
const { error = fallbackErrorMessage } = res || {};
store.commit(SET_ERROR_MESSAGE, error);
store.commit(SET_ALERT, { message: error, variant: 'danger' });
};
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 {
[SET_ERROR_MESSAGE](state, errorMessage) {
state.errorMessage = errorMessage;
[SET_ALERT](state, { title, message, variant, linkUrl } = {}) {
state.alert = { title, message, variant, linkUrl };
},
};
export default () => ({
errorMessage: undefined,
alert: undefined,
});
import { ALERT_LOCALSTORAGE_KEY } from './constants';
/**
* Persist alert data to localStorage.
*/
export const persistAlert = ({ title, message, linkUrl, variant } = {}) => {
const payload = JSON.stringify({ title, message, linkUrl, variant });
localStorage.setItem(ALERT_LOCALSTORAGE_KEY, payload);
};
/**
* Return alert data from localStorage.
*/
export const retrieveAlert = () => {
const initialAlertJSON = window.localStorage.getItem(ALERT_LOCALSTORAGE_KEY);
// immediately clean up
window.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
......@@ -16360,6 +16360,9 @@ msgstr ""
msgid "Integrations|Linked namespaces"
msgstr ""
msgid "Integrations|Namespace successfully linked"
msgstr ""
msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance."
msgstr ""
......@@ -34510,6 +34513,9 @@ msgstr ""
msgid "You need to upload a GitLab project export archive (ending in .gz)."
msgstr ""
msgid "You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "You successfully declined the invitation"
msgstr ""
......
import { GlAlert, GlButton, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlButton, GlModal, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JiraConnectApp from '~/jira_connect/components/app.vue';
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');
......@@ -13,18 +15,19 @@ describe('JiraConnectApp', () => {
let store;
const findAlert = () => wrapper.findComponent(GlAlert);
const findAlertLink = () => findAlert().find(GlLink);
const findGlButton = () => wrapper.findComponent(GlButton);
const findGlModal = () => wrapper.findComponent(GlModal);
const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading');
const findHeaderText = () => findHeader().text();
const createComponent = (options = {}) => {
const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore();
wrapper = extendedWrapper(
shallowMount(JiraConnectApp, {
mountFn(JiraConnectApp, {
store,
...options,
provide,
}),
);
};
......@@ -68,25 +71,72 @@ describe('JiraConnectApp', () => {
});
});
it.each`
errorMessage | errorShouldRender
${'Test error'} | ${true}
${''} | ${false}
${undefined} | ${false}
`(
'renders correct alert when errorMessage is `$errorMessage`',
async ({ errorMessage, errorShouldRender }) => {
describe('alert', () => {
it.each`
message | variant | alertShouldRender
${'Test error'} | ${'danger'} | ${true}
${'Test notice'} | ${'info'} | ${true}
${''} | ${undefined} | ${false}
${undefined} | ${undefined} | ${false}
`(
'renders correct alert when message is `$message` and variant is `$variant`',
async ({ message, alertShouldRender, variant }) => {
createComponent();
store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick();
const alert = findAlert();
expect(alert.exists()).toBe(alertShouldRender);
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_ERROR_MESSAGE, errorMessage);
store.commit(SET_ALERT, { message: 'test message' });
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(errorShouldRender);
if (errorShouldRender) {
expect(findAlert().isVisible()).toBe(errorShouldRender);
expect(findAlert().html()).toContain(errorMessage);
}
},
);
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', async () => {
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';
import * as JiraConnectApi from '~/jira_connect/api';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
import { persistAlert } from '~/jira_connect/utils';
import { mockGroup1 } from '../mock_data';
jest.mock('~/jira_connect/utils');
describe('GroupsListItem', () => {
let wrapper;
const mockSubscriptionPath = 'subscriptionPath';
......@@ -85,7 +88,16 @@ describe('GroupsListItem', () => {
expect(findLinkButton().props('loading')).toBe(true);
await waitForPromises();
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', () => {
......
......@@ -8,11 +8,21 @@ describe('JiraConnect store mutations', () => {
localState = state();
});
describe('SET_ERROR_MESSAGE', () => {
it('sets error message', () => {
mutations.SET_ERROR_MESSAGE(localState, 'test error');
describe('SET_ALERT', () => {
it('sets alert state', () => {
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