Commit 05beab14 authored by Sarah Groff Hennigh-Palermo's avatar Sarah Groff Hennigh-Palermo

Merge branch 'branch-switcher-ui' into 'master'

Add branch switcher UI to the pipeline editor [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!57562
parents c455cd48 6c8ae41d
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
import getAvailableBranches from '~/pipeline_editor/graphql/queries/available_branches.graphql';
import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.graphql';
export default {
i18n: {
title: s__('Branches'),
fetchError: s__('Unable to fetch branch list for this project.'),
},
components: {
GlDropdown,
GlDropdownItem,
GlDropdownSectionHeader,
GlIcon,
},
inject: ['projectFullPath'],
apollo: {
branches: {
query: getAvailableBranches,
variables() {
return {
projectFullPath: this.projectFullPath,
};
},
update(data) {
return data.project?.repository?.branches || [];
},
error() {
this.$emit('showError', {
type: DEFAULT_FAILURE,
reasons: [this.$options.i18n.fetchError],
});
},
},
currentBranch: {
query: getCurrentBranch,
},
},
computed: {
hasBranchList() {
return this.branches?.length > 0;
},
},
};
</script>
<template>
<gl-dropdown v-if="hasBranchList" class="gl-ml-2" :text="currentBranch" icon="branch">
<gl-dropdown-section-header>
{{ this.$options.i18n.title }}
</gl-dropdown-section-header>
<gl-dropdown-item
v-for="branch in branches"
:key="branch.name"
:is-checked="currentBranch === branch.name"
:is-check-item="true"
>
<gl-icon name="check" class="gl-visibility-hidden" />
{{ branch.name }}
</gl-dropdown-item>
</gl-dropdown>
</template>
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BranchSwitcher from './branch_switcher.vue';
export default {
components: {
BranchSwitcher,
},
mixins: [glFeatureFlagsMixin()],
computed: {
showBranchSwitcher() {
return this.glFeatures.pipelineEditorBranchSwitcher;
},
},
};
</script>
<template>
<div class="gl-mb-5">
<branch-switcher v-if="showBranchSwitcher" v-on="$listeners" />
</div>
</template>
query getAvailableBranches($projectFullPath: ID!) {
project(fullPath: $projectFullPath) @client {
repository {
branches {
name
}
}
}
}
...@@ -11,6 +11,23 @@ export const resolvers = { ...@@ -11,6 +11,23 @@ export const resolvers = {
}), }),
}; };
}, },
/* eslint-disable @gitlab/require-i18n-strings */
project() {
return {
__typename: 'Project',
repository: {
__typename: 'Repository',
branches: [
{ __typename: 'Branch', name: 'master' },
{ __typename: 'Branch', name: 'main' },
{ __typename: 'Branch', name: 'develop' },
{ __typename: 'Branch', name: 'production' },
{ __typename: 'Branch', name: 'test' },
],
},
};
},
/* eslint-enable @gitlab/require-i18n-strings */
}, },
Mutation: { Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => { lintCI: (_, { endpoint, content, dry_run }) => {
......
<script> <script>
import CommitSection from './components/commit/commit_section.vue'; import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue'; import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue'; import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants'; import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
...@@ -7,6 +8,7 @@ import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants'; ...@@ -7,6 +8,7 @@ import { TABS_WITH_COMMIT_FORM, CREATE_TAB } from './constants';
export default { export default {
components: { components: {
CommitSection, CommitSection,
PipelineEditorFileNav,
PipelineEditorHeader, PipelineEditorHeader,
PipelineEditorTabs, PipelineEditorTabs,
}, },
...@@ -44,6 +46,7 @@ export default { ...@@ -44,6 +46,7 @@ export default {
<template> <template>
<div> <div>
<pipeline-editor-file-nav v-on="$listeners" />
<pipeline-editor-header <pipeline-editor-header
:ci-config-data="ciConfigData" :ci-config-data="ciConfigData"
:is-new-ci-config-file="isNewCiConfigFile" :is-new-ci-config-file="isNewCiConfigFile"
......
...@@ -7,6 +7,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml) push_frontend_feature_flag(:ci_config_merged_tab, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_status_for_pipeline_editor, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml)
push_frontend_feature_flag(:pipeline_editor_branch_switcher, @project, default_enabled: :yaml)
end end
feature_category :pipeline_authoring feature_category :pipeline_authoring
......
---
name: pipeline_editor_branch_switcher
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57562
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326189
milestone: '13.11'
type: development
group: group::pipeline authoring
default_enabled: false
...@@ -32968,6 +32968,9 @@ msgstr "" ...@@ -32968,6 +32968,9 @@ msgstr ""
msgid "Unable to create link to vulnerability" msgid "Unable to create link to vulnerability"
msgstr "" msgstr ""
msgid "Unable to fetch branch list for this project."
msgstr ""
msgid "Unable to fetch unscanned projects" msgid "Unable to fetch unscanned projects"
msgstr "" msgstr ""
......
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import { DEFAULT_FAILURE } from '~/pipeline_editor/constants';
import { mockDefaultBranch, mockProjectBranches, mockProjectFullPath } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Pipeline editor branch switcher', () => {
let wrapper;
let mockApollo;
let mockAvailableBranchQuery;
const createComponentWithApollo = () => {
const resolvers = {
Query: {
project: mockAvailableBranchQuery,
},
};
mockApollo = createMockApollo([], resolvers);
wrapper = shallowMount(BranchSwitcher, {
localVue,
apolloProvider: mockApollo,
provide: {
projectFullPath: mockProjectFullPath,
},
data() {
return {
currentBranch: mockDefaultBranch,
};
},
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
beforeEach(() => {
mockAvailableBranchQuery = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
describe('while querying', () => {
beforeEach(() => {
createComponentWithApollo();
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
});
describe('after querying', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches);
createComponentWithApollo();
await waitForPromises();
});
it('query is called with correct variables', async () => {
expect(mockAvailableBranchQuery).toHaveBeenCalledTimes(1);
expect(mockAvailableBranchQuery).toHaveBeenCalledWith(
expect.anything(),
{
fullPath: mockProjectFullPath,
},
expect.anything(),
expect.anything(),
);
});
it('renders list of branches', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownItems()).toHaveLength(mockProjectBranches.repository.branches.length);
});
it('renders current branch at the top of the list with a check mark', () => {
const firstDropdownItem = findDropdownItems().at(0);
const icon = firstDropdownItem.findComponent(GlIcon);
expect(firstDropdownItem.text()).toBe(mockDefaultBranch);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('check');
});
it('does not render check mark for other branches', () => {
const secondDropdownItem = findDropdownItems().at(1);
const icon = secondDropdownItem.findComponent(GlIcon);
expect(icon.classes()).toContain('gl-visibility-hidden');
});
});
describe('on fetch error', () => {
beforeEach(async () => {
mockAvailableBranchQuery.mockResolvedValue(new Error());
createComponentWithApollo();
await waitForPromises();
});
it('does not render dropdown', () => {
expect(findDropdown().exists()).toBe(false);
});
it('shows an error message', () => {
expect(wrapper.emitted('showError')).toBeDefined();
expect(wrapper.emitted('showError')[0]).toEqual([
{
reasons: [wrapper.vm.$options.i18n.fetchError],
type: DEFAULT_FAILURE,
},
]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
describe('Pipeline editor file nav', () => {
let wrapper;
const mockProvide = {
glFeatures: {
pipelineEditorBranchSwitcher: true,
},
};
const createComponent = ({ provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorFileNav, {
provide: {
...mockProvide,
...provide,
},
});
};
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders the branch switcher', () => {
expect(findBranchSwitcher().exists()).toBe(true);
});
});
describe('with branch switcher feature flag OFF', () => {
it('does not render the branch switcher', () => {
createComponent({
provide: {
glFeatures: { pipelineEditorBranchSwitcher: false },
},
});
expect(findBranchSwitcher().exists()).toBe(false);
});
});
});
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
mockDefaultBranch, mockDefaultBranch,
mockLintResponse, mockLintResponse,
mockProjectFullPath, mockProjectFullPath,
mockProjectBranches,
} from '../mock_data'; } from '../mock_data';
jest.mock('~/api', () => { jest.mock('~/api', () => {
...@@ -46,6 +47,23 @@ describe('~/pipeline_editor/graphql/resolvers', () => { ...@@ -46,6 +47,23 @@ describe('~/pipeline_editor/graphql/resolvers', () => {
await expect(result.rawData).resolves.toBe(mockCiYml); await expect(result.rawData).resolves.toBe(mockCiYml);
}); });
}); });
describe('project', () => {
it('resolves project data with type names', async () => {
const result = await resolvers.Query.project();
// eslint-disable-next-line no-underscore-dangle
expect(result.__typename).toBe('Project');
});
it('resolves project with available list of branches', async () => {
const result = await resolvers.Query.project();
expect(result.repository.branches).toHaveLength(
mockProjectBranches.repository.branches.length,
);
});
});
}); });
describe('Mutation', () => { describe('Mutation', () => {
......
...@@ -138,6 +138,20 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => { ...@@ -138,6 +138,20 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
}; };
}; };
export const mockProjectBranches = {
__typename: 'Project',
repository: {
__typename: 'Repository',
branches: [
{ __typename: 'Branch', name: 'master' },
{ __typename: 'Branch', name: 'main' },
{ __typename: 'Branch', name: 'develop' },
{ __typename: 'Branch', name: 'production' },
{ __typename: 'Branch', name: 'test' },
],
},
};
export const mockProjectPipeline = { export const mockProjectPipeline = {
pipeline: { pipeline: {
commitPath: '/-/commit/aabbccdd', commitPath: '/-/commit/aabbccdd',
......
...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue'; import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue'; import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue'; import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants'; import { MERGED_TAB, VISUALIZE_TAB } from '~/pipeline_editor/constants';
...@@ -27,6 +28,7 @@ describe('Pipeline editor home wrapper', () => { ...@@ -27,6 +28,7 @@ describe('Pipeline editor home wrapper', () => {
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
const findCommitSection = () => wrapper.findComponent(CommitSection); const findCommitSection = () => wrapper.findComponent(CommitSection);
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -38,6 +40,10 @@ describe('Pipeline editor home wrapper', () => { ...@@ -38,6 +40,10 @@ describe('Pipeline editor home wrapper', () => {
createComponent(); createComponent();
}); });
it('shows the file nav', () => {
expect(findFileNav().exists()).toBe(true);
});
it('shows the pipeline editor header', () => { it('shows the pipeline editor header', () => {
expect(findPipelineEditorHeader().exists()).toBe(true); expect(findPipelineEditorHeader().exists()).toBe(true);
}); });
......
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