Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
b2e2c43b3c
commit
d703818fb0
|
@ -0,0 +1,68 @@
|
||||||
|
<script>
|
||||||
|
import { GlTab } from '@gitlab/ui';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper of <gl-tab> to optionally lazily render this tab's content
|
||||||
|
* when its shown **without dismounting after its hidden**.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* API is the same as <gl-tab>, for example:
|
||||||
|
*
|
||||||
|
* <gl-tabs>
|
||||||
|
* <editor-tab title="Tab 1" :lazy="true">
|
||||||
|
* lazily mounted content (gets mounted if this is first tab)
|
||||||
|
* </editor-tab>
|
||||||
|
* <editor-tab title="Tab 2" :lazy="true">
|
||||||
|
* lazily mounted content
|
||||||
|
* </editor-tab>
|
||||||
|
* <editor-tab title="Tab 3">
|
||||||
|
* eagerly mounted content
|
||||||
|
* </editor-tab>
|
||||||
|
* </gl-tabs>
|
||||||
|
*
|
||||||
|
* Once the tab is selected it is permanently set as "not-lazy"
|
||||||
|
* so it's contents are not dismounted.
|
||||||
|
*
|
||||||
|
* lazy is "false" by default, as in <gl-tab>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlTab,
|
||||||
|
// Use a small renderless component to know when the tab content mounts because:
|
||||||
|
// - gl-tab always gets mounted, even if lazy is `true`. See:
|
||||||
|
// https://github.com/bootstrap-vue/bootstrap-vue/blob/dev/src/components/tabs/tab.js#L180
|
||||||
|
// - we cannot listen to events on <slot />
|
||||||
|
MountSpy: {
|
||||||
|
render: () => null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
lazy: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLazy: this.lazy,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onContentMounted() {
|
||||||
|
// When a child is first mounted make the entire tab
|
||||||
|
// permanently mounted by setting 'lazy' to false.
|
||||||
|
this.isLazy = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<gl-tab :lazy="isLazy" v-bind="$attrs" v-on="$listeners">
|
||||||
|
<slot v-for="slot in Object.keys($slots)" :slot="slot" :name="slot"></slot>
|
||||||
|
<mount-spy @hook:mounted="onContentMounted" />
|
||||||
|
</gl-tab>
|
||||||
|
</template>
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlAlert, GlLoadingIcon, GlTab, GlTabs } from '@gitlab/ui';
|
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
|
||||||
import { __, s__, sprintf } from '~/locale';
|
import { __, s__, sprintf } from '~/locale';
|
||||||
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
|
||||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
|
@ -7,6 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||||
import CiLint from './components/lint/ci_lint.vue';
|
import CiLint from './components/lint/ci_lint.vue';
|
||||||
import CommitForm from './components/commit/commit_form.vue';
|
import CommitForm from './components/commit/commit_form.vue';
|
||||||
|
import EditorTab from './components/ui/editor_tab.vue';
|
||||||
import TextEditor from './components/text_editor.vue';
|
import TextEditor from './components/text_editor.vue';
|
||||||
|
|
||||||
import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
|
import commitCiFileMutation from './graphql/mutations/commit_ci_file.mutation.graphql';
|
||||||
|
@ -28,9 +29,9 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
CommitForm,
|
CommitForm,
|
||||||
CiLint,
|
CiLint,
|
||||||
|
EditorTab,
|
||||||
GlAlert,
|
GlAlert,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
GlTab,
|
|
||||||
GlTabs,
|
GlTabs,
|
||||||
PipelineGraph,
|
PipelineGraph,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
|
@ -66,8 +67,6 @@ export default {
|
||||||
content: '',
|
content: '',
|
||||||
contentModel: '',
|
contentModel: '',
|
||||||
lastCommitSha: this.commitSha,
|
lastCommitSha: this.commitSha,
|
||||||
currentTabIndex: 0,
|
|
||||||
editorIsReady: false,
|
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
|
|
||||||
// Success and failure state
|
// Success and failure state
|
||||||
|
@ -128,9 +127,6 @@ export default {
|
||||||
isCiConfigDataLoading() {
|
isCiConfigDataLoading() {
|
||||||
return this.$apollo.queries.ciConfigData.loading;
|
return this.$apollo.queries.ciConfigData.loading;
|
||||||
},
|
},
|
||||||
isVisualizeTabActive() {
|
|
||||||
return this.currentTabIndex === 1;
|
|
||||||
},
|
|
||||||
defaultCommitMessage() {
|
defaultCommitMessage() {
|
||||||
return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
|
return sprintf(this.$options.i18n.defaultCommitMessage, { sourcePath: this.ciConfigPath });
|
||||||
},
|
},
|
||||||
|
@ -305,33 +301,30 @@ export default {
|
||||||
<div class="gl-mt-4">
|
<div class="gl-mt-4">
|
||||||
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
|
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
|
||||||
<div v-else class="file-editor gl-mb-3">
|
<div v-else class="file-editor gl-mb-3">
|
||||||
<gl-tabs v-model="currentTabIndex">
|
<gl-tabs>
|
||||||
<!-- editor should be mounted when its tab is visible, so the container has a size -->
|
<editor-tab :lazy="true" :title="$options.i18n.tabEdit">
|
||||||
<gl-tab :title="$options.i18n.tabEdit" :lazy="!editorIsReady">
|
|
||||||
<!-- editor should be mounted only once, when the tab is displayed -->
|
|
||||||
<text-editor
|
<text-editor
|
||||||
v-model="contentModel"
|
v-model="contentModel"
|
||||||
:ci-config-path="ciConfigPath"
|
:ci-config-path="ciConfigPath"
|
||||||
:commit-sha="lastCommitSha"
|
:commit-sha="lastCommitSha"
|
||||||
:project-path="projectPath"
|
:project-path="projectPath"
|
||||||
@editor-ready="editorIsReady = true"
|
|
||||||
/>
|
/>
|
||||||
</gl-tab>
|
</editor-tab>
|
||||||
|
<editor-tab
|
||||||
<gl-tab
|
|
||||||
v-if="glFeatures.ciConfigVisualizationTab"
|
v-if="glFeatures.ciConfigVisualizationTab"
|
||||||
|
:lazy="true"
|
||||||
:title="$options.i18n.tabGraph"
|
:title="$options.i18n.tabGraph"
|
||||||
:lazy="!isVisualizeTabActive"
|
:title-link-attributes="{ 'data-testid': 'visualization-tab-btn' }"
|
||||||
data-testid="visualization-tab"
|
data-testid="visualization-tab"
|
||||||
>
|
>
|
||||||
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
|
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
|
||||||
<pipeline-graph v-else :pipeline-data="ciConfigData" />
|
<pipeline-graph v-else :pipeline-data="ciConfigData" />
|
||||||
</gl-tab>
|
</editor-tab>
|
||||||
|
|
||||||
<gl-tab :title="$options.i18n.tabLint">
|
<editor-tab :title="$options.i18n.tabLint">
|
||||||
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
|
<gl-loading-icon v-if="isCiConfigDataLoading" size="lg" class="gl-m-3" />
|
||||||
<ci-lint v-else :ci-config="ciConfigData" />
|
<ci-lint v-else :ci-config="ciConfigData" />
|
||||||
</gl-tab>
|
</editor-tab>
|
||||||
</gl-tabs>
|
</gl-tabs>
|
||||||
</div>
|
</div>
|
||||||
<commit-form
|
<commit-form
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
- if viewer.valid?(project: @project, sha: @commit.sha, user: @current_user)
|
- if viewer.valid?(project: @project, sha: @commit.sha, user: @current_user)
|
||||||
= sprite_icon('check')
|
= sprite_icon('check')
|
||||||
This GitLab CI configuration is valid.
|
= s_('Pipelines|This GitLab CI configuration is valid.')
|
||||||
- else
|
- else
|
||||||
= sprite_icon('warning-solid')
|
= sprite_icon('warning-solid')
|
||||||
This GitLab CI configuration is invalid:
|
= s_('Pipelines|This GitLab CI configuration is invalid:')
|
||||||
= viewer.validation_message(project: @project, sha: @commit.sha, user: @current_user)
|
= viewer.validation_message(project: @project, sha: @commit.sha, user: @current_user)
|
||||||
|
|
||||||
= link_to 'Learn more', help_page_path('ci/yaml/README')
|
= link_to _('Learn more'), help_page_path('ci/yaml/README')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= loading_icon(css_class: "gl-vertical-align-text-bottom mr-1")
|
= loading_icon(css_class: "gl-vertical-align-text-bottom mr-1")
|
||||||
Validating GitLab CI configuration…
|
= s_('Pipelines|Validating GitLab CI configuration…')
|
||||||
|
|
||||||
= link_to 'Learn more', help_page_path('ci/yaml/README')
|
= link_to _('Learn more'), help_page_path('ci/yaml/README')
|
||||||
|
|
|
@ -20611,6 +20611,12 @@ msgstr ""
|
||||||
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
|
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|This GitLab CI configuration is invalid:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|This GitLab CI configuration is valid."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|This is a child pipeline within the parent pipeline"
|
msgid "Pipelines|This is a child pipeline within the parent pipeline"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -20626,6 +20632,9 @@ msgstr ""
|
||||||
msgid "Pipelines|Trigger user has insufficient permissions to project"
|
msgid "Pipelines|Trigger user has insufficient permissions to project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pipelines|Validating GitLab CI configuration…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Pipelines|Visualize"
|
msgid "Pipelines|Visualize"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import { GlTabs } from '@gitlab/ui';
|
||||||
|
|
||||||
|
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
|
||||||
|
|
||||||
|
const mockContent1 = 'MOCK CONTENT 1';
|
||||||
|
const mockContent2 = 'MOCK CONTENT 2';
|
||||||
|
|
||||||
|
describe('~/pipeline_editor/components/ui/editor_tab.vue', () => {
|
||||||
|
let wrapper;
|
||||||
|
let mockChildMounted = jest.fn();
|
||||||
|
|
||||||
|
const MockChild = {
|
||||||
|
props: ['content'],
|
||||||
|
template: '<div>{{content}}</div>',
|
||||||
|
mounted() {
|
||||||
|
mockChildMounted(this.content);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const MockTabbedContent = {
|
||||||
|
components: {
|
||||||
|
EditorTab,
|
||||||
|
GlTabs,
|
||||||
|
MockChild,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<gl-tabs>
|
||||||
|
<editor-tab :title-link-attributes="{ 'data-testid': 'tab1-btn' }" :lazy="true">
|
||||||
|
<mock-child content="${mockContent1}"/>
|
||||||
|
</editor-tab>
|
||||||
|
<editor-tab :title-link-attributes="{ 'data-testid': 'tab2-btn' }" :lazy="true">
|
||||||
|
<mock-child content="${mockContent2}"/>
|
||||||
|
</editor-tab>
|
||||||
|
</gl-tabs>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createWrapper = () => {
|
||||||
|
wrapper = mount(MockTabbedContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockChildMounted = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tabs are mounted lazily', async () => {
|
||||||
|
createWrapper();
|
||||||
|
|
||||||
|
expect(mockChildMounted).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('first tab is only mounted after nextTick', async () => {
|
||||||
|
createWrapper();
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(mockChildMounted).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockChildMounted).toHaveBeenCalledWith(mockContent1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('user interaction', () => {
|
||||||
|
const clickTab = async (testid) => {
|
||||||
|
wrapper.find(`[data-testid="${testid}"]`).trigger('click');
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
createWrapper();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mounts a tab once after selecting it', async () => {
|
||||||
|
await clickTab('tab2-btn');
|
||||||
|
|
||||||
|
expect(mockChildMounted).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockChildMounted).toHaveBeenNthCalledWith(1, mockContent1);
|
||||||
|
expect(mockChildMounted).toHaveBeenNthCalledWith(2, mockContent2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mounts each tab once after selecting each', async () => {
|
||||||
|
await clickTab('tab2-btn');
|
||||||
|
await clickTab('tab1-btn');
|
||||||
|
await clickTab('tab2-btn');
|
||||||
|
|
||||||
|
expect(mockChildMounted).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockChildMounted).toHaveBeenNthCalledWith(1, mockContent1);
|
||||||
|
expect(mockChildMounted).toHaveBeenNthCalledWith(2, mockContent2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,14 +1,6 @@
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
|
import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import {
|
import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui';
|
||||||
GlAlert,
|
|
||||||
GlButton,
|
|
||||||
GlFormInput,
|
|
||||||
GlFormTextarea,
|
|
||||||
GlLoadingIcon,
|
|
||||||
GlTabs,
|
|
||||||
GlTab,
|
|
||||||
} from '@gitlab/ui';
|
|
||||||
import waitForPromises from 'helpers/wait_for_promises';
|
import waitForPromises from 'helpers/wait_for_promises';
|
||||||
import VueApollo from 'vue-apollo';
|
import VueApollo from 'vue-apollo';
|
||||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||||
|
@ -28,6 +20,7 @@ import {
|
||||||
|
|
||||||
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
|
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
|
||||||
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
|
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
|
||||||
|
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
|
||||||
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
|
||||||
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
|
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
|
||||||
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
|
import TextEditor from '~/pipeline_editor/components/text_editor.vue';
|
||||||
|
@ -139,7 +132,7 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
|
|
||||||
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
|
||||||
const findAlert = () => wrapper.find(GlAlert);
|
const findAlert = () => wrapper.find(GlAlert);
|
||||||
const findTabAt = (i) => wrapper.findAll(GlTab).at(i);
|
const findTabAt = i => wrapper.findAll(EditorTab).at(i);
|
||||||
const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
|
const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
|
||||||
const findTextEditor = () => wrapper.find(TextEditor);
|
const findTextEditor = () => wrapper.find(TextEditor);
|
||||||
const findEditorLite = () => wrapper.find(MockEditorLite);
|
const findEditorLite = () => wrapper.find(MockEditorLite);
|
||||||
|
@ -172,22 +165,14 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
|
|
||||||
describe('tabs', () => {
|
describe('tabs', () => {
|
||||||
describe('editor tab', () => {
|
describe('editor tab', () => {
|
||||||
beforeEach(() => {
|
it('displays editor only after the tab is mounted', async () => {
|
||||||
createComponent();
|
createComponent({ mountFn: mount });
|
||||||
});
|
|
||||||
|
|
||||||
it('displays the tab and its content', async () => {
|
expect(findTabAt(0).find(TextEditor).exists()).toBe(false);
|
||||||
expect(findTabAt(0).find(TextEditor).exists()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays tab lazily, until editor is ready', async () => {
|
|
||||||
expect(findTabAt(0).attributes('lazy')).toBe('true');
|
|
||||||
|
|
||||||
findTextEditor().vm.$emit('editor-ready');
|
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
expect(findTabAt(0).attributes('lazy')).toBe(undefined);
|
expect(findTabAt(0).find(TextEditor).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -207,6 +192,21 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
|
||||||
expect(findLoadingIcon().exists()).toBe(true);
|
expect(findLoadingIcon().exists()).toBe(true);
|
||||||
expect(findPipelineGraph().exists()).toBe(false);
|
expect(findPipelineGraph().exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('displays the graph only after the tab is mounted and selected', async () => {
|
||||||
|
createComponent({ mountFn: mount });
|
||||||
|
|
||||||
|
expect(findTabAt(1).find(PipelineGraph).exists()).toBe(false);
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Select visualization tab
|
||||||
|
wrapper.find('[data-testid="visualization-tab-btn"]').trigger('click');
|
||||||
|
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findTabAt(1).find(PipelineGraph).exists()).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with feature flag off', () => {
|
describe('with feature flag off', () => {
|
||||||
|
|
Loading…
Reference in New Issue