Commit e97ccf3f authored by Zack Cuddy's avatar Zack Cuddy Committed by Kushal Pandya

Update store, add components

This sets up vuex actions/mutations

Filter bar component

This adds the vue compoenent to filter

Also had a Typo in template
parent 67d0d86f
......@@ -2,6 +2,7 @@
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import GeoDesignsFilterBar from './geo_designs_filter_bar.vue';
import GeoDesigns from './geo_designs.vue';
import GeoDesignsEmptyState from './geo_designs_empty_state.vue';
import GeoDesignsDisabled from './geo_designs_disabled.vue';
......@@ -10,6 +11,7 @@ export default {
name: 'GeoDesignsApp',
components: {
GlLoadingIcon,
GeoDesignsFilterBar,
GeoDesigns,
GeoDesignsEmptyState,
GeoDesignsDisabled,
......@@ -56,6 +58,7 @@ export default {
<template>
<article class="geo-designs-container">
<section v-if="designsEnabled">
<geo-designs-filter-bar />
<gl-loading-icon v-if="isLoading" size="xl" />
<template v-else>
<geo-designs v-if="hasDesigns" />
......
......@@ -36,11 +36,11 @@ export default {
v-for="design in designs"
:key="design.id"
:name="design.name"
:project-id="design.project_id"
:project-id="design.projectId"
:sync-status="design.state"
:last-synced="design.last_synced_at"
:last-verified="design.last_verified_at"
:last-checked="design.last_checked_at"
:last-synced="design.lastSyncedAt"
:last-verified="design.lastVerifiedAt"
:last-checked="design.lastCheckedAt"
/>
<gl-pagination
v-if="hasDesigns"
......
<script>
import { mapActions, mapState } from 'vuex';
import { debounce } from 'underscore';
import { GlTabs, GlTab, GlFormInput } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { DEFAULT_SEARCH_DELAY } from '../store/constants';
export default {
name: 'GeoDesignsFilterBar',
components: {
GlTabs,
GlTab,
GlFormInput,
Icon,
},
computed: {
...mapState(['currentFilterIndex', 'filterOptions', 'searchFilter']),
search: {
get() {
return this.searchFilter;
},
set: debounce(function debounceSearch(newVal) {
this.setSearch(newVal);
this.fetchDesigns();
}, DEFAULT_SEARCH_DELAY),
},
},
methods: {
...mapActions(['setFilter', 'setSearch', 'fetchDesigns']),
filterChange(filterIndex) {
this.setFilter(filterIndex);
this.fetchDesigns();
},
},
};
</script>
<template>
<gl-tabs :value="currentFilterIndex" @input="filterChange">
<gl-tab
v-for="(filter, index) in filterOptions"
:key="index"
:title="filter"
title-item-class="text-capitalize"
/>
<template v-slot:tabs-end>
<div class="d-flex align-items-center ml-auto">
<gl-form-input v-model="search" type="text" :placeholder="__(`Filter by name...`)" />
</div>
</template>
</gl-tabs>
</template>
import Api from 'ee/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import {
parseIntPagination,
normalizeHeaders,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import { FILTER_STATES } from './constants';
// Fetch Designs
export const requestDesigns = ({ commit }) => commit(types.REQUEST_DESIGNS);
......@@ -16,16 +21,23 @@ export const receiveDesignsError = ({ commit }) => {
export const fetchDesigns = ({ state, dispatch }) => {
dispatch('requestDesigns');
const { currentPage: page } = state;
const query = { page };
const statusFilterName = state.filterOptions[state.currentFilterIndex]
? state.filterOptions[state.currentFilterIndex]
: state.filterOptions[0];
const query = {
page: state.currentPage,
search: state.searchFilter ? state.searchFilter : null,
sync_status: statusFilterName === FILTER_STATES.ALL ? null : statusFilterName,
};
Api.getGeoDesigns(query)
.then(res => {
const normalizedHeaders = normalizeHeaders(res.headers);
const paginationInformation = parseIntPagination(normalizedHeaders);
const camelCaseData = convertObjectPropsToCamelCase(res.data, { deep: true });
dispatch('receiveDesignsSuccess', {
data: res.data,
data: camelCaseData,
perPage: paginationInformation.perPage,
total: paginationInformation.total,
});
......@@ -35,7 +47,15 @@ export const fetchDesigns = ({ state, dispatch }) => {
});
};
// Pagination
// Filtering/Pagination
export const setFilter = ({ commit }, filterIndex) => {
commit(types.SET_FILTER, filterIndex);
};
export const setSearch = ({ commit }, search) => {
commit(types.SET_SEARCH, search);
};
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
......@@ -20,3 +20,5 @@ export const STATUS_ICON_CLASS = {
[FILTER_STATES.FAILED]: 'text-danger',
[DEFAULT_STATUS]: 'text-muted',
};
export const DEFAULT_SEARCH_DELAY = 500;
export const SET_FILTER = 'SET_FILTER';
export const SET_SEARCH = 'SET_SEARCH';
export const SET_PAGE = 'SET_PAGE';
export const REQUEST_DESIGNS = 'REQUEST_DESIGNS';
......
import * as types from './mutation_types';
export default {
[types.SET_FILTER](state, filterIndex) {
state.currentPage = 1;
state.currentFilterIndex = filterIndex;
},
[types.SET_SEARCH](state, search) {
state.currentPage = 1;
state.searchFilter = search;
},
[types.SET_PAGE](state, page) {
state.currentPage = page;
},
......
import { FILTER_STATES } from './constants';
const createState = () => ({
isLoading: false,
......@@ -5,5 +7,9 @@ const createState = () => ({
totalDesigns: 0,
pageSize: 0,
currentPage: 1,
searchFilter: '',
currentFilterIndex: 0,
filterOptions: Object.values(FILTER_STATES),
});
export default createState;
......@@ -6,6 +6,7 @@ import store from 'ee/geo_designs/store';
import GeoDesignsDisabled from 'ee/geo_designs/components/geo_designs_disabled.vue';
import GeoDesigns from 'ee/geo_designs/components/geo_designs.vue';
import GeoDesignsEmptyState from 'ee/geo_designs/components/geo_designs_empty_state.vue';
import GeoDesignsFilterBar from 'ee/geo_designs/components/geo_designs_filter_bar.vue';
import {
MOCK_GEO_SVG_PATH,
MOCK_ISSUES_SVG_PATH,
......@@ -60,6 +61,7 @@ describe('GeoDesignsApp', () => {
const findGlLoadingIcon = () => findGeoDesignsContainer().find(GlLoadingIcon);
const findGeoDesigns = () => findGeoDesignsContainer().find(GeoDesigns);
const findGeoDesignsEmptyState = () => findGeoDesignsContainer().find(GeoDesignsEmptyState);
const findGeoDesignsFilterBar = () => findGeoDesignsContainer().find(GeoDesignsFilterBar);
describe('template', () => {
beforeEach(() => {
......@@ -99,6 +101,10 @@ describe('GeoDesignsApp', () => {
expect(findGeoDesignsEnabledContainer().exists()).toBe(true);
});
it('renders the filter bar', () => {
expect(findGeoDesignsFilterBar().exists()).toBe(true);
});
describe('when isLoading = true', () => {
beforeEach(() => {
wrapper.vm.$store.state.isLoading = true;
......
......@@ -14,9 +14,9 @@ describe('GeoDesignsApp', () => {
const propsData = {
name: mockDesign.name,
projectId: mockDesign.project_id,
projectId: mockDesign.projectId,
syncStatus: mockDesign.state,
lastSynced: mockDesign.last_synced_at,
lastSynced: mockDesign.lastSyncedAt,
lastVerified: null,
lastChecked: null,
};
......
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
import { GlTabs, GlTab, GlFormInput } from '@gitlab/ui';
import GeoDesignsFilterBar from 'ee/geo_designs/components/geo_designs_filter_bar.vue';
import store from 'ee/geo_designs/store';
import { DEFAULT_SEARCH_DELAY } from 'ee/geo_designs/store/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoDesignsFilterBar', () => {
let wrapper;
const actionSpies = {
setSearch: jest.fn(),
setFilter: jest.fn(),
fetchDesigns: jest.fn(),
};
const createComponent = () => {
wrapper = mount(localVue.extend(GeoDesignsFilterBar), {
localVue,
store,
methods: {
...actionSpies,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlTabsContainer = () => wrapper.find(GlTabs);
const findGlTab = () => findGlTabsContainer().findAll(GlTab);
const findGlFormInput = () => findGlTabsContainer().find(GlFormInput);
describe('template', () => {
beforeEach(() => {
createComponent();
});
describe('GlTab', () => {
it('renders', () => {
expect(findGlTabsContainer().exists()).toBe(true);
});
it('calls setFilter when input event is fired', () => {
findGlTabsContainer().vm.$emit('input');
expect(actionSpies.setFilter).toHaveBeenCalled();
});
});
it('renders an instance of GlTab for each FilterOption', () => {
expect(findGlTab().length).toBe(wrapper.vm.$store.state.filterOptions.length);
});
it('renders GlFormInput', () => {
expect(findGlFormInput().exists()).toBe(true);
});
});
describe('when search changes', () => {
beforeEach(() => {
createComponent();
actionSpies.fetchDesigns.mockClear(); // Will get called on init
wrapper.vm.search = 'test search';
});
it(`should wait ${DEFAULT_SEARCH_DELAY}ms before calling setSearch`, () => {
expect(actionSpies.setSearch).not.toHaveBeenCalledWith('test search');
jest.runAllTimers(); // Debounce
expect(actionSpies.setSearch).toHaveBeenCalledWith('test search');
});
it(`should wait ${DEFAULT_SEARCH_DELAY}ms before calling fetchDesigns`, () => {
expect(actionSpies.fetchDesigns).not.toHaveBeenCalled();
jest.runAllTimers(); // Debounce
expect(actionSpies.fetchDesigns).toHaveBeenCalled();
});
});
describe('filterChange', () => {
const testValue = 2;
beforeEach(() => {
createComponent();
wrapper.vm.filterChange(testValue);
});
it('should call setFilter with the filterIndex', () => {
expect(actionSpies.setFilter).toHaveBeenCalledWith(testValue);
});
it('should call fetchDesigns', () => {
expect(actionSpies.fetchDesigns).toHaveBeenCalled();
});
});
});
......@@ -78,7 +78,7 @@ describe('GeoDesigns', () => {
const designs = [...wrapper.vm.$store.state.designs];
for (let i = 0; i < designWrappers.length; i += 1) {
expect(designWrappers.at(i).props().projectId).toBe(designs[i].project_id);
expect(designWrappers.at(i).props().projectId).toBe(designs[i].projectId);
}
});
});
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export const MOCK_GEO_SVG_PATH = 'illustrations/gitlab_geo.svg';
export const MOCK_ISSUES_SVG_PATH = 'illustrations/issues.svg';
......@@ -32,7 +34,7 @@ export const MOCK_BASIC_FETCH_RESPONSE = {
};
export const MOCK_BASIC_FETCH_DATA_MAP = {
data: MOCK_BASIC_FETCH_RESPONSE.data,
data: convertObjectPropsToCamelCase(MOCK_BASIC_FETCH_RESPONSE.data, { deep: true }),
perPage: MOCK_BASIC_FETCH_RESPONSE.headers['x-per-page'],
total: MOCK_BASIC_FETCH_RESPONSE.headers['x-total'],
};
......@@ -11,8 +11,15 @@ jest.mock('~/flash');
describe('GeoDesigns Store Actions', () => {
let state;
let mock;
beforeEach(() => {
state = createState();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('requestDesigns', () => {
......@@ -49,15 +56,6 @@ describe('GeoDesigns Store Actions', () => {
});
describe('fetchDesigns', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock
......@@ -98,9 +96,116 @@ describe('GeoDesigns Store Actions', () => {
});
});
describe('queryParams', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock
.onGet()
.replyOnce(200, MOCK_BASIC_FETCH_RESPONSE.data, MOCK_BASIC_FETCH_RESPONSE.headers);
});
describe('no params set', () => {
it('should call fetchDesigns with default queryParams', () => {
state.isLoading = true;
function fetchDesignsCall() {
const callHistory = mock.history.get[0];
expect(callHistory.params.page).toEqual(1);
expect(callHistory.params.search).toBeNull();
expect(callHistory.params.sync_status).toBeNull();
}
testAction(
actions.fetchDesigns,
{},
state,
[],
[
{ type: 'requestDesigns' },
{ type: 'receiveDesignsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
fetchDesignsCall,
);
});
});
describe('with params set', () => {
it('should call fetchDesigns with queryParams', () => {
state.isLoading = true;
state.currentPage = 3;
state.searchFilter = 'test search';
state.currentFilterIndex = 2;
function fetchDesignsCall() {
const callHistory = mock.history.get[0];
expect(callHistory.params.page).toEqual(state.currentPage);
expect(callHistory.params.search).toEqual(state.searchFilter);
expect(callHistory.params.sync_status).toEqual(
state.filterOptions[state.currentFilterIndex],
);
}
testAction(
actions.fetchDesigns,
{},
state,
[],
[
{ type: 'requestDesigns' },
{ type: 'receiveDesignsSuccess', payload: MOCK_BASIC_FETCH_DATA_MAP },
],
fetchDesignsCall,
);
});
});
});
describe('setFilter', () => {
it('should commit mutation SET_FILTER', done => {
const testValue = 1;
testAction(
actions.setFilter,
testValue,
state,
[{ type: types.SET_FILTER, payload: testValue }],
[],
done,
);
});
});
describe('setSearch', () => {
it('should commit mutation SET_SEARCH', done => {
const testValue = 'Test Search';
testAction(
actions.setSearch,
testValue,
state,
[{ type: types.SET_SEARCH, payload: testValue }],
[],
done,
);
});
});
describe('setPage', () => {
it('should commit mutation SET_PAGE', done => {
testAction(actions.setPage, 2, state, [{ type: types.SET_PAGE, payload: 2 }], [], done);
state.currentPage = 1;
const testValue = 2;
testAction(
actions.setPage,
testValue,
state,
[{ type: types.SET_PAGE, payload: testValue }],
[],
done,
);
});
});
});
......@@ -9,10 +9,49 @@ describe('GeoDesigns Store Mutations', () => {
state = createState();
});
describe('SET_FILTER', () => {
const testValue = 2;
beforeEach(() => {
state.currentFilterIndex = 1;
state.currentPage = 2;
mutations[types.SET_FILTER](state, testValue);
});
it('sets the currentFilterIndex state key', () => {
expect(state.currentFilterIndex).toEqual(testValue);
});
it('resets the page to 1', () => {
expect(state.currentPage).toEqual(1);
});
});
describe('SET_SEARCH', () => {
const testValue = 'test search';
beforeEach(() => {
state.currentPage = 2;
mutations[types.SET_SEARCH](state, testValue);
});
it('sets the searchFilter state key', () => {
expect(state.searchFilter).toEqual(testValue);
});
it('resets the page to 1', () => {
expect(state.currentPage).toEqual(1);
});
});
describe('SET_PAGE', () => {
it('sets the page to the correct page', () => {
mutations[types.SET_PAGE](state, 2);
expect(state.currentPage).toEqual(2);
it('sets the currentPage state key', () => {
const testValue = 2;
mutations[types.SET_PAGE](state, testValue);
expect(state.currentPage).toEqual(testValue);
});
});
......
......@@ -7795,6 +7795,9 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
msgid "Filter by name..."
msgstr ""
msgid "Filter by two-factor authentication"
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