Skip to content
Snippets Groups Projects
Commit 37523900 authored by Mireya Andres's avatar Mireya Andres
Browse files

Add file tree popover and save display state in local storage

- Add popover to alert users on the new feature
- Save display state of pipeline editor file tree in local storage
parent 66f26b48
No related branches found
No related tags found
1 merge request!85874Add file tree popover and save display state in local storage
Showing
with 247 additions and 33 deletions
......@@ -3,11 +3,13 @@ import { GlButton } from '@gitlab/ui';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { EDITOR_APP_STATUS_EMPTY } from '../../constants';
import FileTreePopover from '../popovers/file_tree_popover.vue';
import BranchSwitcher from './branch_switcher.vue';
export default {
components: {
BranchSwitcher,
FileTreePopover,
GlButton,
},
mixins: [glFeatureFlagMixin()],
......@@ -56,11 +58,13 @@ export default {
<div class="gl-mb-4">
<gl-button
v-if="showFileTreeToggle"
id="file-tree-toggle"
icon="file-tree"
data-testid="file-tree-toggle"
:aria-label="__('File Tree')"
@click="onFileTreeBtnClick"
/>
<file-tree-popover v-if="showFileTreeToggle" />
<branch-switcher
:has-unsaved-changes="hasUnsavedChanges"
:should-load-new-branch="shouldLoadNewBranch"
......
......@@ -23,7 +23,7 @@ import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
import WalkthroughPopover from './walkthrough_popover.vue';
import WalkthroughPopover from './popovers/walkthrough_popover.vue';
export default {
i18n: {
......
<script>
import { GlPopover, GlOutsideDirective as Outside } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { FILE_TREE_POPOVER_DISMISSED_KEY } from '../../constants';
export default {
name: 'PipelineEditorFileTreePopover',
directives: { Outside },
i18n: {
description: s__(
'pipelineEditorWalkthrough|You can use the file tree to view your pipeline configuration files.',
),
learnMore: __('Learn more'),
},
components: {
GlPopover,
},
data() {
return {
showPopover: false,
};
},
mounted() {
this.showPopover = localStorage.getItem(FILE_TREE_POPOVER_DISMISSED_KEY) !== 'true';
},
methods: {
closePopover() {
this.showPopover = false;
},
dismissPermanently() {
this.closePopover();
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
},
},
};
</script>
<template>
<gl-popover
v-if="showPopover"
show
show-close-button
target="file-tree-toggle"
triggers="manual"
placement="right"
data-qa-selector="file_tree_popover"
@close-button-clicked="dismissPermanently"
>
<div v-outside="closePopover" class="gl-display-flex gl-flex-direction-column">
<p class="gl-font-base">{{ $options.i18n.description }}</p>
</div>
</gl-popover>
</template>
......@@ -49,6 +49,9 @@ export const BRANCH_PAGINATION_LIMIT = 20;
export const BRANCH_SEARCH_DEBOUNCE = '500';
export const SOURCE_EDITOR_DEBOUNCE = 500;
export const FILE_TREE_DISPLAY_KEY = 'pipeline_editor_file_tree_display';
export const FILE_TREE_POPOVER_DISMISSED_KEY = 'pipeline_editor_file_tree_popover_dismissed';
export const STARTER_TEMPLATE_NAME = 'Getting-Started';
export const pipelineEditorTrackingOptions = {
......
......@@ -382,7 +382,7 @@ export default {
</script>
<template>
<div class="gl-mt-4 gl-relative">
<div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
v-else-if="showStartScreen"
......
......@@ -8,7 +8,7 @@ import PipelineEditorFileNav from './components/file_nav/pipeline_editor_file_na
import PipelineEditorFileTree from './components/file_tree/container.vue';
import PipelineEditorHeader from './components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from './components/pipeline_editor_tabs.vue';
import { CREATE_TAB } from './constants';
import { CREATE_TAB, FILE_TREE_DISPLAY_KEY } from './constants';
export default {
commitSectionRef: 'commitSectionRef',
......@@ -77,6 +77,9 @@ export default {
return this.showFileTree && this.glFeatures.pipelineEditorFileTree;
},
},
mounted() {
this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false;
},
methods: {
closeBranchModal() {
this.showSwitchBranchModal = false;
......@@ -92,6 +95,7 @@ export default {
},
toggleFileTree() {
this.showFileTree = !this.showFileTree;
localStorage.setItem(FILE_TREE_DISPLAY_KEY, this.showFileTree);
},
switchBranch() {
this.showSwitchBranchModal = false;
......
......@@ -45683,6 +45683,9 @@ msgstr ""
msgid "pipelineEditorWalkthrough|Use the %{boldStart}commit changes%{boldEnd} button at the bottom of the page to run the pipeline."
msgstr ""
 
msgid "pipelineEditorWalkthrough|You can use the file tree to view your pipeline configuration files."
msgstr ""
msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
msgstr ""
 
......
......@@ -5,6 +5,10 @@ module Page
module Project
module PipelineEditor
class Show < QA::Page::Base
view 'app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue' do
element :pipeline_editor_app, required: true
end
view 'app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue' do
element :branch_selector_button, required: true
element :branch_menu_item_button
......@@ -46,6 +50,21 @@ class Show < QA::Page::Base
element :file_editor_container
end
view 'app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue' do
element :file_tree_popover
end
def initialize
dismiss_file_tree_popover if has_element?(:file_tree_popover)
super
end
def dismiss_file_tree_popover
# clicking outside the popover will dismiss it
click_element(:pipeline_editor_app)
end
def open_branch_selector_dropdown
click_element(:branch_selector_button)
end
......
......@@ -12,6 +12,8 @@
let(:other_branch) { 'test' }
before do
stub_feature_flags(pipeline_editor_file_tree: false)
sign_in(user)
project.add_developer(user)
......@@ -22,11 +24,7 @@
wait_for_requests
end
it 'user sees the Pipeline Editor page' do
expect(page).to have_content('Pipeline Editor')
end
describe 'Branch switcher' do
shared_examples 'default branch switcher behavior' do
def switch_to_branch(branch)
find('[data-testid="branch-selector"]').click
......@@ -68,6 +66,28 @@ def switch_to_branch(branch)
end
end
it 'user sees the Pipeline Editor page' do
expect(page).to have_content('Pipeline Editor')
end
describe 'Branch Switcher (pipeline_editor_file_tree disabled)' do
it_behaves_like 'default branch switcher behavior'
end
describe 'Branch Switcher (pipeline_editor_file_tree enabled)' do
before do
stub_feature_flags(pipeline_editor_file_tree: true)
visit project_ci_pipeline_editor_path(project)
wait_for_requests
# close button for the popover
find('[data-testid="close-button"]').click
end
it_behaves_like 'default branch switcher behavior'
end
describe 'Editor navigation' do
context 'when no change is made' do
it 'user can navigate away without a browser alert' do
......
......@@ -5,6 +5,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_VALID } from '~/pipeline_editor/constants';
......@@ -47,6 +48,7 @@ describe('Pipeline editor file nav', () => {
const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher);
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findPopoverContainer = () => wrapper.findComponent(FileTreePopover);
afterEach(() => {
wrapper.destroy();
......@@ -64,31 +66,47 @@ describe('Pipeline editor file nav', () => {
it('does not render the file tree button', () => {
expect(findFileTreeBtn().exists()).toBe(false);
});
it('does not render the file tree popover', () => {
expect(findPopoverContainer().exists()).toBe(false);
});
});
describe('with pipelineEditorFileTree feature flag ON', () => {
describe('when editor is in the empty state', () => {
it('does not render the file tree button', () => {
beforeEach(() => {
createComponent({
appStatus: EDITOR_APP_STATUS_EMPTY,
isNewCiConfigFile: false,
pipelineEditorFileTree: true,
});
});
it('does not render the file tree button', () => {
expect(findFileTreeBtn().exists()).toBe(false);
});
it('does not render the file tree popover', () => {
expect(findPopoverContainer().exists()).toBe(false);
});
});
describe('when user is about to create their config file for the first time', () => {
it('does not render the file tree button', () => {
beforeEach(() => {
createComponent({
appStatus: EDITOR_APP_STATUS_VALID,
isNewCiConfigFile: true,
pipelineEditorFileTree: true,
});
});
it('does not render the file tree button', () => {
expect(findFileTreeBtn().exists()).toBe(false);
});
it('does not render the file tree popover', () => {
expect(findPopoverContainer().exists()).toBe(false);
});
});
describe('when editor has a non-empty config file open', () => {
......@@ -105,6 +123,10 @@ describe('Pipeline editor file nav', () => {
expect(findFileTreeBtn().props('icon')).toBe('file-tree');
});
it('renders the file tree popover', () => {
expect(findPopoverContainer().exists()).toBe(true);
});
it('file tree button emits toggle-file-tree event', () => {
expect(wrapper.emitted('toggle-file-tree')).toBe(undefined);
......
......@@ -3,7 +3,7 @@ import { shallowMount, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
......
import { nextTick } from 'vue';
import { GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue';
import { FILE_TREE_POPOVER_DISMISSED_KEY } from '~/pipeline_editor/constants';
describe('FileTreePopover component', () => {
let wrapper;
const findPopover = () => wrapper.findComponent(GlPopover);
const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(FileTreePopover);
};
afterEach(() => {
localStorage.clear();
wrapper.destroy();
});
describe('default', () => {
beforeEach(async () => {
createComponent();
});
it('renders dismissable popover', async () => {
expect(findPopover().exists()).toBe(true);
findPopover().vm.$emit('close-button-clicked');
await nextTick();
expect(findPopover().exists()).toBe(false);
});
});
describe('when popover has already been dismissed before', () => {
it('does not render popover', async () => {
localStorage.setItem(FILE_TREE_POPOVER_DISMISSED_KEY, 'true');
createComponent();
expect(findPopover().exists()).toBe(false);
});
});
});
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import WalkthroughPopover from '~/pipeline_editor/components/walkthrough_popover.vue';
import WalkthroughPopover from '~/pipeline_editor/components/popovers/walkthrough_popover.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.config.ignoredElements = ['gl-emoji'];
......
......@@ -10,7 +10,13 @@ import PipelineEditorFileTree from '~/pipeline_editor/components/file_tree/conta
import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switcher.vue';
import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
import { MERGED_TAB, VISUALIZE_TAB, CREATE_TAB, LINT_TAB } from '~/pipeline_editor/constants';
import {
MERGED_TAB,
VISUALIZE_TAB,
CREATE_TAB,
LINT_TAB,
FILE_TREE_DISPLAY_KEY,
} from '~/pipeline_editor/constants';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import { mockLintResponse, mockCiYml } from './mock_data';
......@@ -55,6 +61,7 @@ describe('Pipeline editor home wrapper', () => {
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
afterEach(() => {
localStorage.clear();
wrapper.destroy();
});
......@@ -252,34 +259,69 @@ describe('Pipeline editor home wrapper', () => {
});
describe('with pipelineEditorFileTree feature flag ON', () => {
beforeEach(() => {
createComponent({
glFeatures: {
pipelineEditorFileTree: true,
},
stubs: {
GlButton,
PipelineEditorFileNav,
},
describe('button toggle', () => {
beforeEach(() => {
createComponent({
glFeatures: {
pipelineEditorFileTree: true,
},
stubs: {
GlButton,
PipelineEditorFileNav,
},
});
});
});
it('shows button toggle', () => {
expect(findFileTreeBtn().exists()).toBe(true);
});
it('shows button toggle', () => {
expect(findFileTreeBtn().exists()).toBe(true);
});
it('hides the file tree by default', () => {
expect(findPipelineEditorFileTree().exists()).toBe(false);
it('toggles the drawer on button click', async () => {
await toggleFileTree();
expect(findPipelineEditorFileTree().exists()).toBe(true);
await toggleFileTree();
expect(findPipelineEditorFileTree().exists()).toBe(false);
});
it('sets the display state in local storage', async () => {
await toggleFileTree();
expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('true');
await toggleFileTree();
expect(localStorage.getItem(FILE_TREE_DISPLAY_KEY)).toBe('false');
});
});
it('toggles the drawer on button click', async () => {
await toggleFileTree();
describe('when file tree display state is saved in local storage', () => {
beforeEach(() => {
localStorage.setItem(FILE_TREE_DISPLAY_KEY, 'true');
createComponent({
glFeatures: { pipelineEditorFileTree: true },
stubs: { PipelineEditorFileNav },
});
});
expect(findPipelineEditorFileTree().exists()).toBe(true);
it('shows the file tree by default', () => {
expect(findPipelineEditorFileTree().exists()).toBe(true);
});
});
await toggleFileTree();
describe('when file tree display state is not saved in local storage', () => {
beforeEach(() => {
createComponent({
glFeatures: { pipelineEditorFileTree: true },
stubs: { PipelineEditorFileNav },
});
});
expect(findPipelineEditorFileTree().exists()).toBe(false);
it('hides the file tree by default', () => {
expect(findPipelineEditorFileTree().exists()).toBe(false);
});
});
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment