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