Commit 72f51af5 authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch '328453-move-alers-in-pipeline-editor-app-to-its-own-component' into 'master'

Move alerts in pipeline_editor_app to its own component

See merge request gitlab-org/gitlab!59796
parents 6690d9bb bc250d7c
<script>
import { GlAlert } from '@gitlab/ui';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
} from '../../constants';
import CodeSnippetAlert from '../code_snippet_alert/code_snippet_alert.vue';
import {
CODE_SNIPPET_SOURCE_URL_PARAM,
CODE_SNIPPET_SOURCES,
} from '../code_snippet_alert/constants';
export default {
components: {
GlAlert,
CodeSnippetAlert,
},
errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
[DEFAULT_SUCCESS]: __('Your action succeeded.'),
},
props: {
failureType: {
type: String,
required: false,
default: null,
},
failureReasons: {
type: Array,
required: false,
default: () => [],
},
showFailure: {
type: Boolean,
required: false,
default: false,
},
showSuccess: {
type: Boolean,
required: false,
default: false,
},
successType: {
type: String,
required: false,
default: null,
},
},
data() {
return {
codeSnippetCopiedFrom: '',
};
},
computed: {
failure() {
switch (this.failureType) {
case LOAD_FAILURE_UNKNOWN:
return {
text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
success() {
switch (this.successType) {
case COMMIT_SUCCESS:
return {
text: this.$options.successTexts[COMMIT_SUCCESS],
variant: 'info',
};
default:
return {
text: this.$options.successTexts[DEFAULT_SUCCESS],
variant: 'info',
};
}
},
},
created() {
this.parseCodeSnippetSourceParam();
},
methods: {
dismissCodeSnippetAlert() {
this.codeSnippetCopiedFrom = '';
},
dismissFailure() {
this.$emit('hide-failure');
},
dismissSuccess() {
this.$emit('hide-success');
},
parseCodeSnippetSourceParam() {
const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
window.history.replaceState(
{},
document.title,
removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
);
}
},
},
};
</script>
<template>
<div>
<code-snippet-alert
v-if="codeSnippetCopiedFrom"
:source="codeSnippetCopiedFrom"
class="gl-mb-5"
@dismiss="dismissCodeSnippetAlert"
/>
<gl-alert
v-if="showSuccess"
:variant="success.variant"
class="gl-mb-5"
@dismiss="dismissSuccess"
>
{{ success.text }}
</gl-alert>
<gl-alert
v-if="showFailure"
:variant="failure.variant"
class="gl-mb-5"
@dismiss="dismissFailure"
>
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
</div>
</template>
......@@ -14,6 +14,7 @@ export const COMMIT_FAILURE = 'COMMIT_FAILURE';
export const COMMIT_SUCCESS = 'COMMIT_SUCCESS';
export const DEFAULT_FAILURE = 'DEFAULT_FAILURE';
export const DEFAULT_SUCCESS = 'DEFAULT_SUCCESS';
export const LOAD_FAILURE_UNKNOWN = 'LOAD_FAILURE_UNKNOWN';
export const CREATE_TAB = 'CREATE_TAB';
......
<script>
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
import CodeSnippetAlert from './components/code_snippet_alert/code_snippet_alert.vue';
import {
CODE_SNIPPET_SOURCE_URL_PARAM,
CODE_SNIPPET_SOURCES,
} from './components/code_snippet_alert/constants';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
......@@ -32,11 +25,10 @@ import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
components: {
ConfirmUnsavedChangesDialog,
GlAlert,
GlLoadingIcon,
PipelineEditorEmptyState,
PipelineEditorHome,
CodeSnippetAlert,
PipelineEditorMessages,
},
inject: {
ciConfigPath: {
......@@ -51,15 +43,14 @@ export default {
ciConfigData: {},
failureType: null,
failureReasons: [],
showStartScreen: false,
initialCiFileContent: '',
isNewCiConfigFile: false,
lastCommittedContent: '',
currentCiFileContent: '',
showFailureAlert: false,
showSuccessAlert: false,
successType: null,
codeSnippetCopiedFrom: '',
showStartScreen: false,
showSuccess: false,
showFailure: false,
};
},
......@@ -152,50 +143,12 @@ export default {
isEmpty() {
return this.currentCiFileContent === '';
},
failure() {
switch (this.failureType) {
case LOAD_FAILURE_UNKNOWN:
return {
text: this.$options.errorTexts[LOAD_FAILURE_UNKNOWN],
variant: 'danger',
};
case COMMIT_FAILURE:
return {
text: this.$options.errorTexts[COMMIT_FAILURE],
variant: 'danger',
};
default:
return {
text: this.$options.errorTexts[DEFAULT_FAILURE],
variant: 'danger',
};
}
},
success() {
switch (this.successType) {
case COMMIT_SUCCESS:
return {
text: this.$options.successTexts[COMMIT_SUCCESS],
variant: 'info',
};
default:
return null;
}
},
},
i18n: {
tabEdit: s__('Pipelines|Write pipeline configuration'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
},
errorTexts: {
[COMMIT_FAILURE]: s__('Pipelines|The GitLab CI configuration could not be updated.'),
[DEFAULT_FAILURE]: __('Something went wrong on our end.'),
[LOAD_FAILURE_UNKNOWN]: s__('Pipelines|The CI configuration was not loaded, please try again.'),
},
successTexts: {
[COMMIT_SUCCESS]: __('Your changes have been successfully committed.'),
},
watch: {
isEmpty(flag) {
if (flag) {
......@@ -203,9 +156,6 @@ export default {
}
},
},
created() {
this.parseCodeSnippetSourceParam();
},
methods: {
handleBlobContentError(error = {}) {
const { networkError } = error;
......@@ -223,12 +173,11 @@ export default {
this.reportFailure(LOAD_FAILURE_UNKNOWN);
}
},
dismissFailure() {
this.showFailureAlert = false;
hideFailure() {
this.showFailure = false;
},
dismissSuccess() {
this.showSuccessAlert = false;
hideSuccess() {
this.showSuccess = false;
},
async refetchContent() {
this.$apollo.queries.initialCiFileContent.skip = false;
......@@ -238,13 +187,13 @@ export default {
this.setAppStatus(EDITOR_APP_STATUS_ERROR);
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showFailureAlert = true;
this.showFailure = true;
this.failureType = type;
this.failureReasons = reasons;
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
this.showSuccessAlert = true;
this.showSuccess = true;
this.successType = type;
},
resetContent() {
......@@ -277,20 +226,6 @@ export default {
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
parseCodeSnippetSourceParam() {
const [codeSnippetCopiedFrom] = getParameterValues(CODE_SNIPPET_SOURCE_URL_PARAM);
if (codeSnippetCopiedFrom && CODE_SNIPPET_SOURCES.includes(codeSnippetCopiedFrom)) {
this.codeSnippetCopiedFrom = codeSnippetCopiedFrom;
window.history.replaceState(
{},
document.title,
removeParams([CODE_SNIPPET_SOURCE_URL_PARAM]),
);
}
},
dismissCodeSnippetAlert() {
this.codeSnippetCopiedFrom = '';
},
},
};
</script>
......@@ -303,31 +238,15 @@ export default {
@createEmptyConfigFile="setNewEmptyCiConfigFile"
/>
<div v-else>
<code-snippet-alert
v-if="codeSnippetCopiedFrom"
:source="codeSnippetCopiedFrom"
class="gl-mb-5"
@dismiss="dismissCodeSnippetAlert"
<pipeline-editor-messages
:failure-type="failureType"
:failure-reasons="failureReasons"
:show-failure="showFailure"
:show-success="showSuccess"
:success-type="successType"
@hide-success="hideSuccess"
@hide-failure="hideFailure"
/>
<gl-alert
v-if="showSuccessAlert"
:variant="success.variant"
class="gl-mb-5"
@dismiss="dismissSuccess"
>
{{ success.text }}
</gl-alert>
<gl-alert
v-if="showFailureAlert"
:variant="failure.variant"
class="gl-mb-5"
@dismiss="dismissFailure"
>
{{ failure.text }}
<ul v-if="failureReasons.length" class="gl-mb-0">
<li v-for="reason in failureReasons" :key="reason">{{ reason }}</li>
</ul>
</gl-alert>
<pipeline-editor-home
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
......
......@@ -36882,6 +36882,9 @@ msgstr ""
msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO."
msgstr ""
msgid "Your action succeeded."
msgstr ""
msgid "Your applications (%{size})"
msgstr ""
......
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import {
COMMIT_FAILURE,
COMMIT_SUCCESS,
DEFAULT_FAILURE,
DEFAULT_SUCCESS,
LOAD_FAILURE_UNKNOWN,
} from '~/pipeline_editor/constants';
describe('Pipeline Editor messages', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(PipelineEditorMessages, {
propsData: props,
});
};
const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert);
const findAlert = () => wrapper.findComponent(GlAlert);
describe('success alert', () => {
it('shows a message for successful commit type', () => {
createComponent({ successType: COMMIT_SUCCESS, showSuccess: true });
expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
});
it('does not show alert when there is a successType but visibility is off', () => {
createComponent({ successType: COMMIT_SUCCESS, showSuccess: false });
expect(findAlert().exists()).toBe(false);
});
it('shows a success alert with default copy if `showSuccess` is true and the `successType` is not valid,', () => {
createComponent({ successType: 'random', showSuccess: true });
expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[DEFAULT_SUCCESS]);
});
it('emit `hide-success` event when clicking on the dismiss button', async () => {
const expectedEvent = 'hide-success';
createComponent({ successType: COMMIT_SUCCESS, showSuccess: true });
expect(wrapper.emitted(expectedEvent)).not.toBeDefined();
await findAlert().vm.$emit('dismiss');
expect(wrapper.emitted(expectedEvent)).toBeDefined();
});
});
describe('failure alert', () => {
it.each`
failureType | message | expectedFailureType
${COMMIT_FAILURE} | ${'failed commit'} | ${COMMIT_FAILURE}
${LOAD_FAILURE_UNKNOWN} | ${'loading failure'} | ${LOAD_FAILURE_UNKNOWN}
${'random'} | ${'error without a specified type'} | ${DEFAULT_FAILURE}
`('shows a message for $message', ({ failureType, expectedFailureType }) => {
createComponent({ failureType, showFailure: true });
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[expectedFailureType]);
});
it('show failure reasons when there are some', () => {
const failureReasons = ['There was a problem', 'ouppps'];
createComponent({ failureType: COMMIT_FAILURE, failureReasons, showFailure: true });
expect(wrapper.html()).toContain(failureReasons[0]);
expect(wrapper.html()).toContain(failureReasons[1]);
});
it('does not show a message for error with a disabled visibility', () => {
createComponent({ failureType: 'random', showFailure: false });
expect(findAlert().exists()).toBe(false);
});
it('emit `hide-failure` event when clicking on the dismiss button', async () => {
const expectedEvent = 'hide-failure';
createComponent({ failureType: COMMIT_FAILURE, showFailure: true });
expect(wrapper.emitted(expectedEvent)).not.toBeDefined();
await findAlert().vm.$emit('dismiss');
expect(wrapper.emitted(expectedEvent)).toBeDefined();
});
});
describe('code snippet alert', () => {
const setCodeSnippetUrlParam = (value) => {
global.jsdom.reconfigure({
url: `${TEST_HOST}/?code_snippet_copied_from=${value}`,
});
};
it('does not show by default', () => {
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(false);
});
it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => {
jest.spyOn(window.history, 'replaceState');
setCodeSnippetUrlParam(source);
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(true);
expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`);
});
it('does not show if URL param is invalid', () => {
setCodeSnippetUrlParam('foo_bar');
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(false);
});
it('disappears on dismiss', async () => {
setCodeSnippetUrlParam('api_fuzzing');
createComponent();
const alert = findCodeSnippetAlert();
expect(alert.exists()).toBe(true);
await alert.vm.$emit('dismiss');
expect(alert.exists()).toBe(false);
});
});
});
......@@ -2,17 +2,15 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
import CodeSnippetAlert from '~/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue';
import { CODE_SNIPPET_SOURCES } from '~/pipeline_editor/components/code_snippet_alert/constants';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import PipelineEditorEmptyState from '~/pipeline_editor/components/ui/pipeline_editor_empty_state.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_editor_messages.vue';
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
......@@ -56,6 +54,7 @@ describe('Pipeline editor app component', () => {
CommitForm,
PipelineEditorHome,
PipelineEditorTabs,
PipelineEditorMessages,
EditorLite: MockEditorLite,
PipelineEditorEmptyState,
},
......@@ -113,7 +112,6 @@ describe('Pipeline editor app component', () => {
const findEmptyState = () => wrapper.findComponent(PipelineEditorEmptyState);
const findEmptyStateButton = () =>
wrapper.findComponent(PipelineEditorEmptyState).findComponent(GlButton);
const findCodeSnippetAlert = () => wrapper.findComponent(CodeSnippetAlert);
beforeEach(() => {
mockBlobContentData = jest.fn();
......@@ -133,48 +131,6 @@ describe('Pipeline editor app component', () => {
});
});
describe('code snippet alert', () => {
const setCodeSnippetUrlParam = (value) => {
global.jsdom.reconfigure({
url: `${TEST_HOST}/?code_snippet_copied_from=${value}`,
});
};
it('does not show by default', () => {
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(false);
});
it.each(CODE_SNIPPET_SOURCES)('shows if URL param is %s, and cleans up URL', (source) => {
jest.spyOn(window.history, 'replaceState');
setCodeSnippetUrlParam(source);
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(true);
expect(window.history.replaceState).toHaveBeenCalledWith({}, document.title, `${TEST_HOST}/`);
});
it('does not show if URL param is invalid', () => {
setCodeSnippetUrlParam('foo_bar');
createComponent();
expect(findCodeSnippetAlert().exists()).toBe(false);
});
it('disappears on dismiss', async () => {
setCodeSnippetUrlParam('api_fuzzing');
createComponent();
const alert = findCodeSnippetAlert();
expect(alert.exists()).toBe(true);
await alert.vm.$emit('dismiss');
expect(alert.exists()).toBe(false);
});
});
describe('when queries are called', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml);
......@@ -235,11 +191,14 @@ describe('Pipeline editor app component', () => {
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
mockBlobContentData.mockRejectedValueOnce(new Error('My error!'));
await createComponentWithApollo();
expect(findEmptyState().exists()).toBe(false);
expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
expect(findAlert().text()).toBe(loadUnknownFailureText);
expect(findEditorHome().exists()).toBe(true);
});
});
......@@ -273,6 +232,7 @@ describe('Pipeline editor app component', () => {
describe('when the user commits', () => {
const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
const updateSuccessMessage = 'Your changes have been successfully committed.';
describe('and the commit mutation succeeds', () => {
beforeEach(() => {
......@@ -283,7 +243,7 @@ describe('Pipeline editor app component', () => {
});
it('shows a confirmation message', () => {
expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
expect(findAlert().text()).toBe(updateSuccessMessage);
});
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
......
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