feat: Add store to manage categories (#5127)

This commit is contained in:
Fayaz Ahmed 2022-08-04 15:11:29 +05:30 committed by GitHub
parent d55a8f7987
commit 7c5ee55d3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 613 additions and 10 deletions

View file

@ -0,0 +1,27 @@
/* global axios */
import PortalsAPI from './portals';
class CategoriesAPI extends PortalsAPI {
constructor() {
super('categories', { accountScoped: true });
}
get({ portalSlug }) {
return axios.get(`${this.url}/${portalSlug}/categories`);
}
create({ portalSlug, categoryObj }) {
return axios.post(`${this.url}/${portalSlug}/categories`, categoryObj);
}
update({ portalSlug, categoryObj }) {
return axios.patch(`${this.url}/${portalSlug}/categories`, categoryObj);
}
delete({ portalSlug, categoryId }) {
return axios.delete(`${this.url}/${portalSlug}/categories/${categoryId}`);
}
}
export default new CategoriesAPI();

View file

@ -13,4 +13,4 @@ class PortalsAPI extends ApiClient {
}
}
export default new PortalsAPI();
export default PortalsAPI;

View file

@ -0,0 +1,12 @@
import categoriesAPI from '../../helpCenter/categories';
import ApiClient from '../../ApiClient';
describe('#BulkActionsAPI', () => {
it('creates correct instance', () => {
expect(categoriesAPI).toBeInstanceOf(ApiClient);
expect(categoriesAPI).toHaveProperty('get');
expect(categoriesAPI).toHaveProperty('create');
expect(categoriesAPI).toHaveProperty('update');
expect(categoriesAPI).toHaveProperty('delete');
});
});

View file

@ -1,6 +1,7 @@
import portalAPI from 'dashboard/api/helpCenter/portals';
import PortalAPI from 'dashboard/api/helpCenter/portals';
import articlesAPI from 'dashboard/api/helpCenter/articles';
import { throwErrorMessage } from 'dashboard/store/utils/api';
const portalAPIs = new PortalAPI();
import types from '../../mutation-types';
export const actions = {
index: async ({ commit }, { pageNumber, portalSlug, locale }) => {
@ -8,7 +9,7 @@ export const actions = {
commit(types.SET_UI_FLAG, { isFetching: true });
const {
data: { payload, meta },
} = await portalAPI.getArticles({
} = await portalAPIs.getArticles({
pageNumber,
portalSlug,
locale,
@ -44,7 +45,7 @@ export const actions = {
show: async ({ commit }, { id, portalSlug }) => {
commit(types.SET_UI_FLAG, { isFetching: true });
try {
const response = await portalAPI.getArticle({ id, portalSlug });
const response = await articlesAPI.getArticle({ id, portalSlug });
const {
data: { payload },
} = response;

View file

@ -0,0 +1,84 @@
import categoriesAPI from 'dashboard/api/helpCenter/categories.js';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import types from '../../mutation-types';
export const actions = {
index: async ({ commit }, { portalSlug }) => {
try {
commit(types.SET_UI_FLAG, { isFetching: true });
const {
data: { payload },
} = await categoriesAPI.get({ portalSlug });
const categoryIds = payload.map(category => category.id);
commit(types.ADD_MANY_CATEGORIES, payload);
commit(types.ADD_MANY_CATEGORIES_ID, categoryIds);
return categoryIds;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.SET_UI_FLAG, { isFetching: false });
}
},
create: async ({ commit }, portalSlug, categoryObj) => {
commit(types.SET_UI_FLAG, { isCreating: true });
try {
const { data } = await categoriesAPI.create({ portalSlug, categoryObj });
const { id: categoryId } = data;
commit(types.ADD_CATEGORY, data);
commit(types.ADD_CATEGORY_ID, categoryId);
return categoryId;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.SET_UI_FLAG, { isCreating: false });
}
},
update: async ({ commit }, portalSlug, categoryObj) => {
const categoryId = categoryObj.id;
commit(types.ADD_CATEGORY_FLAG, {
uiFlags: {
isUpdating: true,
},
categoryId,
});
try {
const { data } = await categoriesAPI.update({ portalSlug, categoryObj });
commit(types.UPDATE_CATEGORY, data);
return categoryId;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.ADD_CATEGORY_FLAG, {
uiFlags: {
isUpdating: false,
},
categoryId,
});
}
},
delete: async ({ commit }, portalSlug, categoryId) => {
commit(types.ADD_CATEGORY_FLAG, {
uiFlags: {
isDeleting: true,
},
categoryId,
});
try {
await categoriesAPI.delete({ portalSlug, categoryId });
commit(types.REMOVE_CATEGORY, categoryId);
commit(types.REMOVE_CATEGORY_ID, categoryId);
return categoryId;
} catch (error) {
return throwErrorMessage(error);
} finally {
commit(types.ADD_CATEGORY_FLAG, {
uiFlags: {
isDeleting: false,
},
categoryId,
});
}
},
};

View file

@ -0,0 +1,24 @@
export const getters = {
uiFlags: state => helpCenterId => {
const uiFlags = state.categories.uiFlags.byId[helpCenterId];
if (uiFlags) return uiFlags;
return { isFetching: false, isUpdating: false, isDeleting: false };
},
isFetching: state => state.uiFlags.isFetching,
categoryById: (...getterArguments) => categoryId => {
const [state] = getterArguments;
const category = state.categories.byId[categoryId];
if (!category) return undefined;
return category;
},
allCategories: (...getterArguments) => {
const [state, _getters] = getterArguments;
const categories = state.categories.allIds.map(id => {
return _getters.categoryById(id);
});
return categories;
},
getMeta: state => {
return state.meta;
},
};

View file

@ -0,0 +1,31 @@
import { getters } from './getters';
import { actions } from './actions';
import { mutations } from './mutations';
export const defaultHelpCenterFlags = {
isFetching: false,
isUpdating: false,
isDeleting: false,
};
const state = {
categoriess: {
byId: {},
byLocale: {},
allIds: [],
uiFlags: {
byId: {},
},
},
uiFlags: {
allFetched: false,
isFetching: false,
},
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};

View file

@ -0,0 +1,74 @@
import types from '../../mutation-types';
import Vue from 'vue';
export const mutations = {
[types.SET_UI_FLAG](_state, uiFlags) {
_state.uiFlags = {
..._state.uiFlags,
...uiFlags,
};
},
[types.ADD_CATEGORY]: ($state, category) => {
if (!category.id) return;
Vue.set($state.categories.byId, category.id, {
...category,
});
},
[types.CLEAR_CATEGORIES]: $state => {
Vue.set($state.categories, 'byId', {});
Vue.set($state.categories, 'allIds', []);
Vue.set($state.categories, 'uiFlags', {});
},
[types.ADD_MANY_CATEGORIES]($state, categories) {
const allCategories = { ...$state.categories.byId };
categories.forEach(category => {
allCategories[category.id] = category;
});
Vue.set($state.categories, 'byId', allCategories);
},
[types.ADD_MANY_CATEGORIES_ID]($state, categoryIds) {
$state.categories.allIds.push(...categoryIds);
},
[types.SET_CATEGORIES_META]: ($state, data) => {
const { categories_count: count, current_page: currentPage } = data;
Vue.set($state.meta, 'count', count);
Vue.set($state.meta, 'currentPage', currentPage);
},
[types.ADD_CATEGORY_ID]: ($state, categoryId) => {
$state.categories.allIds.push(categoryId);
},
[types.ADD_CATEGORY_FLAG]: ($state, { categoryId, uiFlags }) => {
const flags = $state.categories.uiFlags.byId[categoryId];
Vue.set($state.categories.uiFlags.byId, categoryId, {
...{
isFetching: false,
isUpdating: false,
isDeleting: false,
},
...flags,
...uiFlags,
});
},
[types.UPDATE_CATEGORY]($state, category) {
const categoryId = category.id;
if (!$state.categories.allIds.includes(categoryId)) return;
Vue.set($state.categories.byId, categoryId, {
...category,
});
},
[types.REMOVE_CATEGORY]($state, categoryId) {
const { [categoryId]: toBeRemoved, ...newById } = $state.categories.byId;
Vue.set($state.categories, 'byId', newById);
},
[types.REMOVE_CATEGORY_ID]($state, categoryId) {
$state.categories.allIds = $state.categories.allIds.filter(
id => id !== categoryId
);
},
};

View file

@ -0,0 +1,138 @@
import axios from 'axios';
import { actions } from '../actions';
import * as types from '../../../mutation-types';
import { categoriesPayload } from './fixtures';
const commit = jest.fn();
global.axios = axios;
jest.mock('axios');
describe('#actions', () => {
describe('#index', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: categoriesPayload });
await actions.index({ commit }, { portalSlug: 'room-rental' });
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isFetching: true }],
[types.default.ADD_MANY_CATEGORIES, categoriesPayload.payload],
[types.default.ADD_MANY_CATEGORIES_ID, [1, 2]],
[types.default.SET_UI_FLAG, { isFetching: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.get.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.index({ commit }, { portalSlug: 'room-rental' })
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isFetching: true }],
[types.default.SET_UI_FLAG, { isFetching: false }],
]);
});
});
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: categoriesPayload.payload[0] });
await actions.create({ commit }, categoriesPayload.payload[0]);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isCreating: true }],
[types.default.ADD_CATEGORY, categoriesPayload.payload[0]],
[types.default.ADD_CATEGORY_ID, 1],
[types.default.SET_UI_FLAG, { isCreating: false }],
]);
});
it('sends correct actions if API is error', async () => {
axios.post.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.create({ commit }, 'web-docs', categoriesPayload.payload[0])
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[types.default.SET_UI_FLAG, { isCreating: true }],
[types.default.SET_UI_FLAG, { isCreating: false }],
]);
});
});
describe('#update', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: categoriesPayload.payload[0] });
await actions.update(
{ commit },
'web-docs',
categoriesPayload.payload[0]
);
expect(commit.mock.calls).toEqual([
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isUpdating: true }, categoryId: 1 },
],
[types.default.UPDATE_CATEGORY, categoriesPayload.payload[0]],
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isUpdating: false }, categoryId: 1 },
],
]);
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.update({ commit }, 'web-docs', categoriesPayload.payload[0])
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isUpdating: true }, categoryId: 1 },
],
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isUpdating: false }, categoryId: 1 },
],
]);
});
});
describe('#delete', () => {
it('sends correct actions if API is success', async () => {
axios.delete.mockResolvedValue({ data: categoriesPayload.payload[0] });
await actions.delete(
{ commit },
'portal-slug',
categoriesPayload.payload[0].id
);
expect(commit.mock.calls).toEqual([
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isDeleting: true }, categoryId: 1 },
],
[types.default.REMOVE_CATEGORY, categoriesPayload.payload[0].id],
[types.default.REMOVE_CATEGORY_ID, categoriesPayload.payload[0].id],
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isDeleting: false }, categoryId: 1 },
],
]);
});
it('sends correct actions if API is error', async () => {
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
await expect(
actions.delete(
{ commit },
'portal-slug',
categoriesPayload.payload[0].id
)
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isDeleting: true }, categoryId: 1 },
],
[
types.default.ADD_CATEGORY_FLAG,
{ uiFlags: { isDeleting: false }, categoryId: 1 },
],
]);
});
});
});

View file

@ -0,0 +1,77 @@
export const categoriesPayload = {
payload: [
{
id: 1,
name: 'FAQs',
slug: 'faq',
locale: 'en',
description: 'This category is for FAQs',
position: 0,
account_id: 1,
meta: {
articles_count: 1,
},
},
{
id: 2,
name: 'Product updates',
slug: 'product-updates',
locale: 'en',
description: 'This category is for product updates',
position: 0,
account_id: 1,
meta: {
articles_count: 0,
},
},
],
meta: {
current_page: 1,
categories_count: 2,
},
};
export const categoriesState = {
meta: {
count: 123,
currentPage: 1,
},
categories: {
byId: {
1: {
id: 1,
name: 'FAQs',
slug: 'faq',
locale: 'en',
description: 'This category is for FAQs',
position: 0,
account_id: 1,
meta: {
articles_count: 1,
},
},
2: {
id: 2,
name: 'Product updates',
slug: 'product-updates',
locale: 'en',
description: 'This category is for product updates',
position: 0,
account_id: 1,
meta: {
articles_count: 0,
},
},
},
allIds: [1, 2],
uiFlags: {
byId: {
1: { isFetching: false, isUpdating: true, isDeleting: false },
},
},
},
uiFlags: {
allFetched: false,
isFetching: true,
},
};

View file

@ -0,0 +1,25 @@
import { getters } from '../getters';
import { categoriesState } from './fixtures';
describe('#getters', () => {
let state = {};
beforeEach(() => {
state = categoriesState;
});
it('uiFlags', () => {
expect(getters.uiFlags(state)(1)).toEqual({
isFetching: false,
isUpdating: true,
isDeleting: false,
});
});
it('categoryById', () => {
expect(getters.categoryById(state)(1)).toEqual(
categoriesState.categories.byId[1]
);
});
it('isFetchingCategories', () => {
expect(getters.isFetching(state)).toEqual(true);
});
});

View file

@ -0,0 +1,101 @@
import { mutations } from '../mutations';
import types from '../../../mutation-types';
import { categoriesState, categoriesPayload } from './fixtures';
describe('#mutations', () => {
let state = {};
beforeEach(() => {
state = categoriesState;
});
describe('#SET_UI_FLAG', () => {
it('It returns default flags if empty object passed', () => {
mutations[types.SET_UI_FLAG](state, {});
expect(state.uiFlags).toEqual({
allFetched: false,
isFetching: true,
});
});
it('Update flags when flag passed as parameters', () => {
mutations[types.SET_UI_FLAG](state, { isFetching: true });
expect(state.uiFlags).toEqual({
allFetched: false,
isFetching: true,
});
});
});
describe('#ADD_CATEGORY', () => {
it('add valid category to state', () => {
mutations[types.ADD_CATEGORY](state, categoriesPayload.payload[0]);
expect(state.categories.byId[1]).toEqual(categoriesPayload.payload[0]);
});
it('does not add category with empty data passed', () => {
mutations[types.ADD_CATEGORY](state, {});
expect(state).toEqual(categoriesState);
});
});
describe('#CATEGORIES_META', () => {
it('add meta to state', () => {
mutations[types.SET_CATEGORIES_META](state, {
categories_count: 3,
current_page: 1,
});
expect(state.meta).toEqual({
count: 3,
currentPage: 1,
});
});
});
describe('#ADD_CATEGORY_ID', () => {
it('add valid category id to state', () => {
mutations[types.ADD_CATEGORY_ID](state, 3);
expect(state.categories.allIds).toEqual([1, 2, 3]);
});
it('Does not invalid category with empty data passed', () => {
mutations[types.ADD_CATEGORY_ID](state, {});
expect(state).toEqual(categoriesState);
});
});
describe('#UPDATE_CATEGORY', () => {
it('does not updates if empty object is passed', () => {
mutations[types.UPDATE_CATEGORY](state, {});
expect(state).toEqual(categoriesState);
});
it('does not updates if object id is not present ', () => {
mutations[types.UPDATE_CATEGORY](state, { id: 5 });
expect(state).toEqual(categoriesState);
});
it(' updates if object with id already present in the state', () => {
mutations[types.UPDATE_CATEGORY](state, {
id: 2,
title: 'This category is for product updates',
});
expect(state.categories.byId[2].title).toEqual(
'This category is for product updates'
);
});
});
describe('#REMOVE_CATEGORY', () => {
it('does not remove object entry if no id is passed', () => {
mutations[types.REMOVE_CATEGORY](state, undefined);
expect(state).toEqual({ ...categoriesState });
});
it('removes category if valid category id passed', () => {
mutations[types.REMOVE_CATEGORY](state, 2);
expect(state.categories.byId[2]).toEqual(undefined);
});
});
// describe('#CLEAR_CATEGORIES', () => {
// it('clears categories', () => {
// mutations[types.CLEAR_CATEGORIES](state);
// expect(state.categories.allIds).toEqual([]);
// expect(state.categories.byId).toEqual({});
// expect(state.categories.uiFlags).toEqual({});
// });
// });
});

View file

@ -1,12 +1,12 @@
import PortalsAPI from 'dashboard/api/helpCenter/portals.js';
import PortalAPI from 'dashboard/api/helpCenter/portals.js';
import { throwErrorMessage } from 'dashboard/store/utils/api';
import { types } from './mutations';
const portalAPIs = new PortalAPI();
export const actions = {
index: async ({ commit }) => {
try {
commit(types.SET_UI_FLAG, { isFetching: true });
const { data } = await PortalsAPI.get();
const { data } = await portalAPIs.get();
const portalIds = data.map(portal => portal.id);
commit(types.ADD_MANY_PORTALS_ENTRY, data);
commit(types.ADD_MANY_PORTALS_IDS, portalIds);
@ -20,7 +20,7 @@ export const actions = {
create: async ({ commit }, params) => {
commit(types.SET_UI_FLAG, { isCreating: true });
try {
const { data } = await PortalsAPI.create(params);
const { data } = await portalAPIs.create(params);
const { id: portalId } = data;
commit(types.ADD_PORTAL_ENTRY, data);
commit(types.ADD_PORTAL_ID, portalId);
@ -38,7 +38,7 @@ export const actions = {
portalId,
});
try {
const { data } = await PortalsAPI.update(params);
const { data } = await portalAPIs.update(params);
commit(types.UPDATE_PORTAL_ENTRY, data);
} catch (error) {
throwErrorMessage(error);
@ -56,7 +56,7 @@ export const actions = {
portalId,
});
try {
await PortalsAPI.delete(portalId);
await portalAPIs.delete(portalId);
commit(types.REMOVE_PORTAL_ENTRY, portalId);
commit(types.REMOVE_PORTAL_ID, portalId);
} catch (error) {

View file

@ -233,4 +233,13 @@ export default {
REMOVE_ARTICLE: 'REMOVE_ARTICLE',
REMOVE_ARTICLE_ID: 'REMOVE_ARTICLE_ID',
SET_UI_FLAG: 'SET_UI_FLAG',
// Help Center -- Categories
ADD_CATEGORY: 'ADD_CATEGORY',
ADD_CATEGORY_ID: 'ADD_CATEGORY_ID',
ADD_MANY_CATEGORIES: 'ADD_MANY_CATEGORIES',
ADD_MANY_CATEGORIES_ID: 'ADD_MANY_CATEGORIES_ID',
ADD_CATEGORY_FLAG: 'ADD_CATEGORY_FLAG',
UPDATE_CATEGORY: 'UPDATE_CATEGORY',
REMOVE_CATEGORY: 'REMOVE_CATEGORY',
REMOVE_CATEGORY_ID: 'REMOVE_CATEGORY_ID',
};