Commit fd46e26d authored by Nathan Friend's avatar Nathan Friend

Merge branch '213219_01-update-replicable-ui' into 'master'

Update Geo Replicable Filter Nav

See merge request gitlab-org/gitlab!29979
parents f7b46b76 93b589b2
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
<template> <template>
<article class="geo-replicable-container"> <article class="geo-replicable-container">
<geo-replicable-filter-bar /> <geo-replicable-filter-bar class="mb-3" />
<gl-loading-icon v-if="isLoading" size="xl" /> <gl-loading-icon v-if="isLoading" size="xl" />
<template v-else> <template v-else>
<geo-replicable v-if="hasReplicableItems" /> <geo-replicable v-if="hasReplicableItems" />
......
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { GlTabs, GlTab, GlFormInput, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlSearchBoxByType, GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import { DEFAULT_SEARCH_DELAY, ACTION_TYPES } from '../store/constants'; import { DEFAULT_SEARCH_DELAY, ACTION_TYPES } from '../store/constants';
export default { export default {
name: 'GeoReplicableFilterBar', name: 'GeoReplicableFilterBar',
components: { components: {
GlTabs, GlSearchBoxByType,
GlTab,
GlFormInput,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
Icon, GlButton,
}, },
computed: { computed: {
...mapState(['currentFilterIndex', 'filterOptions', 'searchFilter', 'replicableType']), ...mapState(['currentFilterIndex', 'filterOptions', 'searchFilter', 'replicableType']),
...@@ -43,29 +40,33 @@ export default { ...@@ -43,29 +40,33 @@ export default {
</script> </script>
<template> <template>
<gl-tabs :value="currentFilterIndex" @input="filterChange"> <nav
<gl-tab class="row d-flex flex-column flex-sm-row align-items-center bg-secondary border-bottom border-secondary-100 p-3"
>
<gl-dropdown :text="__('Filter by status')" class="col px-1 my-1 my-sm-0 w-100">
<gl-dropdown-item
v-for="(filter, index) in filterOptions" v-for="(filter, index) in filterOptions"
:key="index" :key="index"
:title="filter" :class="{ 'bg-secondary-100': index === currentFilterIndex }"
title-item-class="text-capitalize" @click="filterChange(index)"
/> >
<template #tabs-end> <span
<div class="d-flex align-items-center ml-auto"> >{{ filter.label }} <span v-if="filter.label === 'All'">{{ replicableType }}</span></span
<gl-form-input v-model="search" type="text" :placeholder="__(`Filter by name...`)" /> >
<gl-dropdown class="ml-2">
<template #button-content>
<span>
<icon name="cloud-gear" />
{{ __('Batch operations') }}
<icon name="chevron-down" />
</span>
</template>
<gl-dropdown-item @click="initiateAllReplicableSyncs($options.actionTypes.RESYNC)">
{{ resyncText }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
<gl-search-box-by-type
v-model="search"
class="col px-1 my-1 my-sm-0 bg-white w-100"
type="text"
:placeholder="__(`Filter by name`)"
/>
<div class="col col-sm-6 d-flex justify-content-end my-1 my-sm-0 w-100">
<gl-button
class="text-secondary-700"
@click="initiateAllReplicableSyncs($options.actionTypes.RESYNC)"
>{{ __('Resync all') }}</gl-button
>
</div> </div>
</template> </nav>
</gl-tabs>
</template> </template>
...@@ -26,13 +26,14 @@ export const receiveReplicableItemsError = ({ state, commit }) => { ...@@ -26,13 +26,14 @@ export const receiveReplicableItemsError = ({ state, commit }) => {
export const fetchReplicableItems = ({ state, dispatch }) => { export const fetchReplicableItems = ({ state, dispatch }) => {
dispatch('requestReplicableItems'); dispatch('requestReplicableItems');
const statusFilterName = state.filterOptions[state.currentFilterIndex] const { filterOptions, currentFilterIndex, currentPage, searchFilter } = state;
? state.filterOptions[state.currentFilterIndex]
: state.filterOptions[0]; const statusFilter = currentFilterIndex ? filterOptions[currentFilterIndex] : filterOptions[0];
const query = { const query = {
page: state.currentPage, page: currentPage,
search: state.searchFilter ? state.searchFilter : null, search: searchFilter || null,
sync_status: statusFilterName === FILTER_STATES.ALL ? null : statusFilterName, sync_status: statusFilter.value === FILTER_STATES.ALL.value ? null : statusFilter.value,
}; };
Api.getGeoReplicableItems(state.replicableType, query) Api.getGeoReplicableItems(state.replicableType, query)
......
import { __ } from '~/locale';
export const FILTER_STATES = { export const FILTER_STATES = {
ALL: 'all', ALL: {
SYNCED: 'synced', label: __('All'),
PENDING: 'pending', value: '',
FAILED: 'failed', },
PENDING: {
label: __('In progress'),
value: 'pending',
},
FAILED: {
label: __('Failed'),
value: 'failed',
},
SYNCED: {
label: __('Synced'),
value: 'synced',
},
}; };
export const DEFAULT_STATUS = 'never'; export const DEFAULT_STATUS = 'never';
export const STATUS_ICON_NAMES = { export const STATUS_ICON_NAMES = {
[FILTER_STATES.SYNCED]: 'status_closed', [FILTER_STATES.SYNCED.value]: 'status_closed',
[FILTER_STATES.PENDING]: 'status_scheduled', [FILTER_STATES.PENDING.value]: 'status_scheduled',
[FILTER_STATES.FAILED]: 'status_failed', [FILTER_STATES.FAILED.value]: 'status_failed',
[DEFAULT_STATUS]: 'status_notfound', [DEFAULT_STATUS]: 'status_notfound',
}; };
export const STATUS_ICON_CLASS = { export const STATUS_ICON_CLASS = {
[FILTER_STATES.SYNCED]: 'text-success', [FILTER_STATES.SYNCED.value]: 'text-success',
[FILTER_STATES.PENDING]: 'text-warning', [FILTER_STATES.PENDING.value]: 'text-warning',
[FILTER_STATES.FAILED]: 'text-danger', [FILTER_STATES.FAILED.value]: 'text-danger',
[DEFAULT_STATUS]: 'text-muted', [DEFAULT_STATUS]: 'text-muted',
}; };
......
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import { GlTabs, GlTab, GlFormInput, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlButton } from '@gitlab/ui';
import GeoReplicableFilterBar from 'ee/geo_replicable/components/geo_replicable_filter_bar.vue'; import GeoReplicableFilterBar from 'ee/geo_replicable/components/geo_replicable_filter_bar.vue';
import store from 'ee/geo_replicable/store'; import store from 'ee/geo_replicable/store';
import { DEFAULT_SEARCH_DELAY } from 'ee/geo_replicable/store/constants'; import { DEFAULT_SEARCH_DELAY } from 'ee/geo_replicable/store/constants';
...@@ -32,48 +32,53 @@ describe('GeoReplicableFilterBar', () => { ...@@ -32,48 +32,53 @@ describe('GeoReplicableFilterBar', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGlTabsContainer = () => wrapper.find(GlTabs); const findNavContainer = () => wrapper.find('nav');
const findGlTab = () => findGlTabsContainer().findAll(GlTab); const findGlDropdown = () => findNavContainer().find(GlDropdown);
const findGlFormInput = () => findGlTabsContainer().find(GlFormInput); const findGlDropdownItems = () => findNavContainer().findAll(GlDropdownItem);
const findGlDropdown = () => findGlTabsContainer().find(GlDropdown); const findDropdownItemsText = () => findGlDropdownItems().wrappers.map(w => w.text());
const findGlDropdownItem = () => findGlTabsContainer().find(GlDropdownItem); const findGlSearchBox = () => findNavContainer().find(GlSearchBoxByType);
const findGlButton = () => findNavContainer().find(GlButton);
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
describe('GlTab', () => { it('renders nav container always', () => {
it('renders', () => { expect(findNavContainer().exists()).toBeTruthy();
expect(findGlTabsContainer().exists()).toBe(true);
}); });
it('calls setFilter when input event is fired', () => { it('renders dropdown always', () => {
findGlTabsContainer().vm.$emit('input'); expect(findGlDropdown().exists()).toBeTruthy();
expect(actionSpies.setFilter).toHaveBeenCalled();
});
}); });
it('renders an instance of GlTab for each FilterOption', () => { describe('Filter options', () => {
expect(findGlTab().length).toBe(wrapper.vm.$store.state.filterOptions.length); it('renders a dropdown item for each filterOption', () => {
expect(findDropdownItemsText()).toStrictEqual(wrapper.vm.filterOptions.map(n => n.label));
}); });
it('renders GlFormInput', () => { it('clicking a dropdown item calls setFilter with its index', () => {
expect(findGlFormInput().exists()).toBe(true); const index = 1;
findGlDropdownItems()
.at(index)
.find('button')
.trigger('click');
expect(actionSpies.setFilter).toHaveBeenCalledWith(index);
});
}); });
it('renders GlDropdown', () => { it('renders a search box always', () => {
expect(findGlDropdown().exists()).toBe(true); expect(findGlSearchBox().exists()).toBeTruthy();
}); });
describe('GlDropDownItem', () => { describe('Re-sync all button', () => {
it('renders', () => { it('renders always', () => {
expect(findGlDropdownItem().exists()).toBe(true); expect(findGlButton().exists()).toBeTruthy();
}); });
it('calls initiateAllReplicableSyncs when clicked', () => { it('calls initiateAllReplicableSyncs when clicked', () => {
const innerButton = findGlDropdownItem().find('button'); findGlButton().trigger('click');
innerButton.trigger('click');
expect(actionSpies.initiateAllReplicableSyncs).toHaveBeenCalled(); expect(actionSpies.initiateAllReplicableSyncs).toHaveBeenCalled();
}); });
}); });
......
...@@ -17,7 +17,7 @@ describe('GeoReplicableStatus', () => { ...@@ -17,7 +17,7 @@ describe('GeoReplicableStatus', () => {
let wrapper; let wrapper;
const propsData = { const propsData = {
status: FILTER_STATES.SYNCED, status: FILTER_STATES.SYNCED.value,
}; };
const createComponent = () => { const createComponent = () => {
...@@ -47,9 +47,9 @@ describe('GeoReplicableStatus', () => { ...@@ -47,9 +47,9 @@ describe('GeoReplicableStatus', () => {
describe.each` describe.each`
status | iconName | iconClass status | iconName | iconClass
${FILTER_STATES.SYNCED} | ${STATUS_ICON_NAMES[FILTER_STATES.SYNCED]} | ${STATUS_ICON_CLASS[FILTER_STATES.SYNCED]} ${FILTER_STATES.SYNCED.value} | ${STATUS_ICON_NAMES[FILTER_STATES.SYNCED.value]} | ${STATUS_ICON_CLASS[FILTER_STATES.SYNCED.value]}
${FILTER_STATES.PENDING} | ${STATUS_ICON_NAMES[FILTER_STATES.PENDING]} | ${STATUS_ICON_CLASS[FILTER_STATES.PENDING]} ${FILTER_STATES.PENDING.value} | ${STATUS_ICON_NAMES[FILTER_STATES.PENDING.value]} | ${STATUS_ICON_CLASS[FILTER_STATES.PENDING.value]}
${FILTER_STATES.FAILED} | ${STATUS_ICON_NAMES[FILTER_STATES.FAILED]} | ${STATUS_ICON_CLASS[FILTER_STATES.FAILED]} ${FILTER_STATES.FAILED.value} | ${STATUS_ICON_NAMES[FILTER_STATES.FAILED.value]} | ${STATUS_ICON_CLASS[FILTER_STATES.FAILED.value]}
${DEFAULT_STATUS} | ${STATUS_ICON_NAMES[DEFAULT_STATUS]} | ${STATUS_ICON_CLASS[DEFAULT_STATUS]} ${DEFAULT_STATUS} | ${STATUS_ICON_NAMES[DEFAULT_STATUS]} | ${STATUS_ICON_CLASS[DEFAULT_STATUS]}
`(`iconProperties`, ({ status, iconName, iconClass }) => { `(`iconProperties`, ({ status, iconName, iconClass }) => {
beforeEach(() => { beforeEach(() => {
......
...@@ -118,7 +118,7 @@ describe('GeoReplicable Store Actions', () => { ...@@ -118,7 +118,7 @@ describe('GeoReplicable Store Actions', () => {
expect(Api.getGeoReplicableItems).toHaveBeenCalledWith(MOCK_REPLICABLE_TYPE, { expect(Api.getGeoReplicableItems).toHaveBeenCalledWith(MOCK_REPLICABLE_TYPE, {
page: 3, page: 3,
search: 'test search', search: 'test search',
sync_status: state.filterOptions[2], sync_status: state.filterOptions[2].value,
}); });
}, },
); );
......
...@@ -2994,9 +2994,6 @@ msgstr "" ...@@ -2994,9 +2994,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo." msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "" msgstr ""
msgid "Batch operations"
msgstr ""
msgid "BatchComments|Delete all pending comments" msgid "BatchComments|Delete all pending comments"
msgstr "" msgstr ""
...@@ -9215,7 +9212,10 @@ msgstr "" ...@@ -9215,7 +9212,10 @@ msgstr ""
msgid "Filter by milestone name" msgid "Filter by milestone name"
msgstr "" msgstr ""
msgid "Filter by name..." msgid "Filter by name"
msgstr ""
msgid "Filter by status"
msgstr "" msgstr ""
msgid "Filter by two-factor authentication" msgid "Filter by two-factor authentication"
...@@ -11208,6 +11208,9 @@ msgstr "" ...@@ -11208,6 +11208,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you." msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr "" msgstr ""
msgid "In progress"
msgstr ""
msgid "In the next step, you'll be able to select the projects you want to import." msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "" msgstr ""
...@@ -17592,6 +17595,9 @@ msgstr "" ...@@ -17592,6 +17595,9 @@ msgstr ""
msgid "Resync" msgid "Resync"
msgstr "" msgstr ""
msgid "Resync all"
msgstr ""
msgid "Resync all %{replicableType}" msgid "Resync all %{replicableType}"
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