Commit 2946e918 authored by Kushal Pandya's avatar Kushal Pandya

Move tabs within Requirements Vue app

- Moves Requirements tabs into Vue app.
- Makes Requirements count more reliable by refetching it from backend
on mutation.
parent 7da7c18c
<script>
import { GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { FilterState } from '../constants';
export default {
FilterState,
components: {
GlLink,
GlBadge,
GlButton,
},
props: {
filterBy: {
type: String,
required: true,
},
requirementsCount: {
type: Object,
required: true,
},
showCreateForm: {
type: Boolean,
required: true,
},
canCreateRequirement: {
type: Boolean,
required: false,
},
},
computed: {
isOpenTab() {
return this.filterBy === FilterState.opened;
},
isArchivedTab() {
return this.filterBy === FilterState.archived;
},
isAllTab() {
return this.filterBy === FilterState.all;
},
},
};
</script>
<template>
<div class="top-area">
<ul class="nav-links mobile-separator requirements-state-filters js-requirements-state-filters">
<li :class="{ active: isOpenTab }">
<gl-link
id="state-opened"
data-state="opened"
:title="__('Filter by requirements that are currently opened.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.opened })"
>
{{ __('Open') }}
<gl-badge class="badge-pill">{{ requirementsCount.OPENED }}</gl-badge>
</gl-link>
</li>
<li :class="{ active: isArchivedTab }">
<gl-link
id="state-archived"
data-state="archived"
:title="__('Filter by requirements that are currently archived.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.archived })"
>
{{ __('Archived') }}
<gl-badge class="badge-pill">{{ requirementsCount.ARCHIVED }}</gl-badge>
</gl-link>
</li>
<li :class="{ active: isAllTab }">
<gl-link
id="state-all"
data-state="all"
:title="__('Show all requirements.')"
@click="$emit('clickTab', { filterBy: $options.FilterState.all })"
>
{{ __('All') }}
<gl-badge class="badge-pill">{{ requirementsCount.ALL }}</gl-badge>
</gl-link>
</li>
</ul>
<div v-if="isOpenTab && canCreateRequirement" class="nav-controls">
<gl-button
category="primary"
variant="success"
class="js-new-requirement qa-new-requirement-button"
:disabled="showCreateForm"
@click="$emit('clickNewRequirement')"
>{{ __('New requirement') }}</gl-button
>
</div>
</div>
</template>
...@@ -7,10 +7,6 @@ query projectRequirements( ...@@ -7,10 +7,6 @@ query projectRequirements(
$nextPageCursor: String = "" $nextPageCursor: String = ""
) { ) {
project(fullPath: $projectPath) { project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
requirements( requirements(
first: $firstPageSize first: $firstPageSize
last: $lastPageSize last: $lastPageSize
......
query projectRequirements($projectPath: ID!) {
project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
}
}
...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; ...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import RequirementsRoot from './components/requirements_root.vue'; import RequirementsRoot from './components/requirements_root.vue';
...@@ -58,8 +59,8 @@ export default () => { ...@@ -58,8 +59,8 @@ export default () => {
const ALL = parseInt(all, 10); const ALL = parseInt(all, 10);
return { return {
filterBy: stateFilterBy, initialFilterBy: stateFilterBy,
requirementsCount: { initialRequirementsCount: {
OPENED, OPENED,
ARCHIVED, ARCHIVED,
ALL, ALL,
...@@ -77,13 +78,13 @@ export default () => { ...@@ -77,13 +78,13 @@ export default () => {
return createElement('requirements-root', { return createElement('requirements-root', {
props: { props: {
projectPath: this.projectPath, projectPath: this.projectPath,
filterBy: this.filterBy, initialFilterBy: this.initialFilterBy,
requirementsCount: this.requirementsCount, initialRequirementsCount: this.initialRequirementsCount,
page: parseInt(this.page, 10) || 1, page: parseInt(this.page, 10) || 1,
prev: this.prev, prev: this.prev,
next: this.next, next: this.next,
emptyStatePath: this.emptyStatePath, emptyStatePath: this.emptyStatePath,
canCreateRequirement: this.canCreateRequirement, canCreateRequirement: parseBoolean(this.canCreateRequirement),
requirementsWebUrl: this.requirementsWebUrl, requirementsWebUrl: this.requirementsWebUrl,
}, },
}); });
......
- page_title _('Requirements') - page_title _('Requirements')
- type = :requirements
- page_context_word = type.to_s.humanize(capitalize: false)
- @content_class = 'requirements-container' - @content_class = 'requirements-container'
-# We'd prefer to have following declarations be part of -# We'd prefer to have following declarations be part of
-# helpers in some way but given that they're very frontend-centeric, -# helpers in some way but given that they're very frontend-centeric,
-# keeping them in HAML view makes more sense. -# keeping them in HAML view makes more sense.
- page_size = 20 - page_size = 20
- ignore_page_params = ['next', 'prev', 'page']
- requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state) - requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state)
- total_requirements = requirements_count['opened'] + requirements_count['archived'] - total_requirements = requirements_count['opened'] + requirements_count['archived']
- is_open_tab = params[:state].nil? || params[:state] == 'opened' - is_open_tab = params[:state].nil? || params[:state] == 'opened'
...@@ -19,28 +16,6 @@ ...@@ -19,28 +16,6 @@
- else - else
- current_tab_count = total_requirements > page_size ? page_size : total_requirements - current_tab_count = total_requirements > page_size ? page_size : total_requirements
.top-area
%ul.nav-links.mobile-separator.requirements-state-filters.js-requirements-state-filters
%li{ class: active_when(is_open_tab) }>
= link_to page_filter_path(state: 'opened', without: ignore_page_params), id: 'state-opened', title: (_("Filter by %{issuable_type} that are currently opened.") % { issuable_type: page_context_word }), data: { state: 'opened' } do
= _('Open')
%span.badge.badge-pill.js-opened-count= requirements_count['opened']
%li{ class: active_when(params[:state] == 'archived') }>
= link_to page_filter_path(state: 'archived', without: ignore_page_params), id: 'state-archived', title: (_("Filter by %{issuable_type} that are currently archived.") % { issuable_type: page_context_word }), data: { state: 'archived' } do
= _('Archived')
%span.badge.badge-pill.js-archived-count= requirements_count['archived']
%li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', without: ignore_page_params), id: 'state-all', title: (_("Show all %{issuable_type}.") % { issuable_type: page_context_word }), data: { state: 'all' } do
= _('All')
%span.badge.badge-pill.js-all-count= total_requirements
.nav-controls
- if is_open_tab && can?(current_user, :create_requirement, @project)
%button.btn.btn-success.js-new-requirement.qa-new-requirement-button{ type: 'button' }
= _('New requirement')
#js-requirements-app{ data: { filter_by: params[:state], #js-requirements-app{ data: { filter_by: params[:state],
page: params[:page], page: params[:page],
prev: params[:prev], prev: params[:prev],
...@@ -50,7 +25,7 @@ ...@@ -50,7 +25,7 @@
archived: requirements_count['archived'], archived: requirements_count['archived'],
all: total_requirements, all: total_requirements,
requirements_web_url: project_requirements_management_requirements_path(@project), requirements_web_url: project_requirements_management_requirements_path(@project),
can_create_requirement: can?(current_user, :create_requirement, @project), can_create_requirement: "#{can?(current_user, :create_requirement, @project)}",
empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } } empty_state_path: image_path('illustrations/empty-state/empty-requirements-lg.svg') } }
- if current_tab_count == 0 - if current_tab_count == 0
-# Show regular spinner only when there will be no -# Show regular spinner only when there will be no
......
...@@ -184,9 +184,7 @@ describe 'Requirements list', :js do ...@@ -184,9 +184,7 @@ describe 'Requirements list', :js do
end end
it 'does not show button "New requirement"' do it 'does not show button "New requirement"' do
page.within('.nav-controls') do expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
expect(page).not_to have_selector('button.js-new-requirement')
end
end end
it 'shows list of all archived requirements' do it 'shows list of all archived requirements' do
...@@ -229,9 +227,7 @@ describe 'Requirements list', :js do ...@@ -229,9 +227,7 @@ describe 'Requirements list', :js do
end end
it 'does not show button "New requirement"' do it 'does not show button "New requirement"' do
page.within('.nav-controls') do expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
expect(page).not_to have_selector('button.js-new-requirement')
end
end end
it 'shows list of all requirements' do it 'shows list of all requirements' do
...@@ -251,9 +247,7 @@ describe 'Requirements list', :js do ...@@ -251,9 +247,7 @@ describe 'Requirements list', :js do
end end
it 'open tab does not show button "New requirement"' do it 'open tab does not show button "New requirement"' do
page.within('.nav-controls') do expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
expect(page).not_to have_selector('button.js-new-requirement')
end
end end
end end
end end
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlBadge, GlButton } from '@gitlab/ui';
import RequirementsTabs from 'ee/requirements/components/requirements_tabs.vue';
import { FilterState } from 'ee/requirements/constants';
import { mockRequirementsCount } from '../mock_data';
const createComponent = ({
filterBy = FilterState.opened,
requirementsCount = mockRequirementsCount,
showCreateForm = false,
canCreateRequirement = true,
} = {}) =>
shallowMount(RequirementsTabs, {
propsData: {
filterBy,
requirementsCount,
showCreateForm,
canCreateRequirement,
},
});
describe('RequirementsTabs', () => {
let wrapper;
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders "Open" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(0);
expect(tabEl.attributes('id')).toBe('state-opened');
expect(tabEl.attributes('data-state')).toBe('opened');
expect(tabEl.attributes('title')).toBe('Filter by requirements that are currently opened.');
expect(tabEl.text()).toContain('Open');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.OPENED}`);
});
it('renders "Archived" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(1);
expect(tabEl.attributes('id')).toBe('state-archived');
expect(tabEl.attributes('data-state')).toBe('archived');
expect(tabEl.attributes('title')).toBe('Filter by requirements that are currently archived.');
expect(tabEl.text()).toContain('Archived');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.ARCHIVED}`);
});
it('renders "All" tab', () => {
const tabEl = wrapper.findAll(GlLink).at(2);
expect(tabEl.attributes('id')).toBe('state-all');
expect(tabEl.attributes('data-state')).toBe('all');
expect(tabEl.attributes('title')).toBe('Show all requirements.');
expect(tabEl.text()).toContain('All');
expect(tabEl.find(GlBadge).text()).toBe(`${mockRequirementsCount.ALL}`);
});
it('renders class `active` on currently selected tab', () => {
const tabEl = wrapper.findAll('li').at(0);
expect(tabEl.classes()).toContain('active');
});
it('renders "New requirement" button when current tab is "Open" tab', () => {
wrapper.setProps({
filterBy: FilterState.opened,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(true);
expect(buttonEl.text()).toBe('New requirement');
});
});
it('does not render "New requirement" button when current tab is not "Open" tab', () => {
wrapper.setProps({
filterBy: FilterState.closed,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(false);
});
});
it('does not render "New requirement" button when `canCreateRequirement` prop is false', () => {
wrapper.setProps({
filterBy: FilterState.opened,
canCreateRequirement: false,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.exists()).toBe(false);
});
});
it('disables "New requirement" button when `showCreateForm` is true', () => {
wrapper.setProps({
showCreateForm: true,
});
return wrapper.vm.$nextTick(() => {
const buttonEl = wrapper.find(GlButton);
expect(buttonEl.props('disabled')).toBe(true);
});
});
});
});
...@@ -9308,9 +9308,6 @@ msgstr "" ...@@ -9308,9 +9308,6 @@ msgstr ""
msgid "Filter" msgid "Filter"
msgstr "" msgstr ""
msgid "Filter by %{issuable_type} that are currently archived."
msgstr ""
msgid "Filter by %{issuable_type} that are currently closed." msgid "Filter by %{issuable_type} that are currently closed."
msgstr "" msgstr ""
...@@ -9326,6 +9323,12 @@ msgstr "" ...@@ -9326,6 +9323,12 @@ msgstr ""
msgid "Filter by name" msgid "Filter by name"
msgstr "" msgstr ""
msgid "Filter by requirements that are currently archived."
msgstr ""
msgid "Filter by requirements that are currently opened."
msgstr ""
msgid "Filter by status" msgid "Filter by status"
msgstr "" msgstr ""
...@@ -19152,15 +19155,15 @@ msgstr "" ...@@ -19152,15 +19155,15 @@ msgstr ""
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account." msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
msgstr "" msgstr ""
msgid "Show all %{issuable_type}."
msgstr ""
msgid "Show all activity" msgid "Show all activity"
msgstr "" msgstr ""
msgid "Show all members" msgid "Show all members"
msgstr "" msgstr ""
msgid "Show all requirements."
msgstr ""
msgid "Show archived projects" msgid "Show archived projects"
msgstr "" msgstr ""
...@@ -19535,6 +19538,9 @@ msgstr "" ...@@ -19535,6 +19538,9 @@ msgstr ""
msgid "Something went wrong while fetching related merge requests." msgid "Something went wrong while fetching related merge requests."
msgstr "" msgstr ""
msgid "Something went wrong while fetching requirements count."
msgstr ""
msgid "Something went wrong while fetching requirements list." msgid "Something went wrong while fetching requirements list."
msgstr "" msgstr ""
......
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