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(
$nextPageCursor: String = ""
) {
project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
requirements(
first: $firstPageSize
last: $lastPageSize
......
query projectRequirements($projectPath: ID!) {
project(fullPath: $projectPath) {
requirementStatesCount {
opened
archived
}
}
}
......@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import RequirementsRoot from './components/requirements_root.vue';
......@@ -58,8 +59,8 @@ export default () => {
const ALL = parseInt(all, 10);
return {
filterBy: stateFilterBy,
requirementsCount: {
initialFilterBy: stateFilterBy,
initialRequirementsCount: {
OPENED,
ARCHIVED,
ALL,
......@@ -77,13 +78,13 @@ export default () => {
return createElement('requirements-root', {
props: {
projectPath: this.projectPath,
filterBy: this.filterBy,
requirementsCount: this.requirementsCount,
initialFilterBy: this.initialFilterBy,
initialRequirementsCount: this.initialRequirementsCount,
page: parseInt(this.page, 10) || 1,
prev: this.prev,
next: this.next,
emptyStatePath: this.emptyStatePath,
canCreateRequirement: this.canCreateRequirement,
canCreateRequirement: parseBoolean(this.canCreateRequirement),
requirementsWebUrl: this.requirementsWebUrl,
},
});
......
- page_title _('Requirements')
- type = :requirements
- page_context_word = type.to_s.humanize(capitalize: false)
- @content_class = 'requirements-container'
-# We'd prefer to have following declarations be part of
-# helpers in some way but given that they're very frontend-centeric,
-# keeping them in HAML view makes more sense.
- page_size = 20
- ignore_page_params = ['next', 'prev', 'page']
- requirements_count = Hash.new(0).merge(@project.requirements.counts_by_state)
- total_requirements = requirements_count['opened'] + requirements_count['archived']
- is_open_tab = params[:state].nil? || params[:state] == 'opened'
......@@ -19,28 +16,6 @@
- else
- 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],
page: params[:page],
prev: params[:prev],
......@@ -50,7 +25,7 @@
archived: requirements_count['archived'],
all: total_requirements,
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') } }
- if current_tab_count == 0
-# Show regular spinner only when there will be no
......
......@@ -184,9 +184,7 @@ describe 'Requirements list', :js do
end
it 'does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
end
it 'shows list of all archived requirements' do
......@@ -229,9 +227,7 @@ describe 'Requirements list', :js do
end
it 'does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
end
it 'shows list of all requirements' do
......@@ -251,9 +247,7 @@ describe 'Requirements list', :js do
end
it 'open tab does not show button "New requirement"' do
page.within('.nav-controls') do
expect(page).not_to have_selector('button.js-new-requirement')
end
expect(page).not_to have_selector('.nav-controls button.js-new-requirement')
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 ""
msgid "Filter"
msgstr ""
msgid "Filter by %{issuable_type} that are currently archived."
msgstr ""
msgid "Filter by %{issuable_type} that are currently closed."
msgstr ""
......@@ -9326,6 +9323,12 @@ msgstr ""
msgid "Filter by name"
msgstr ""
msgid "Filter by requirements that are currently archived."
msgstr ""
msgid "Filter by requirements that are currently opened."
msgstr ""
msgid "Filter by status"
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."
msgstr ""
msgid "Show all %{issuable_type}."
msgstr ""
msgid "Show all activity"
msgstr ""
msgid "Show all members"
msgstr ""
msgid "Show all requirements."
msgstr ""
msgid "Show archived projects"
msgstr ""
......@@ -19535,6 +19538,9 @@ msgstr ""
msgid "Something went wrong while fetching related merge requests."
msgstr ""
msgid "Something went wrong while fetching requirements count."
msgstr ""
msgid "Something went wrong while fetching requirements list."
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