feat: Category store integration (#5218)

* Add more actions

* Complete sidebar store integration

* Complete portal list store integration

* Fixed the specs

* Added missing specs

* Add comment

* Code cleanup

* Fixed all the spec issues

* Add portal and article API specs

* Add category name in article list

* Add more locales

* Code beautification

* Exclude locale from codeclimate ci

* feat: Category store integration

* chore: Minor fixes

* chore: API call fixes

* chore: Minor fixes

* chore: Minor fixes

* chore: Adds the ability for get articles based on categories

* chore: minor fixes

* chore: Minor fixes

* chore: fixes specs and minor improvements

* chore: Review fixes

* chore: Minor fixes

* chore: Review fixes

* chore: Review fixes

* chore: Spacing fixes

* Code cleanup

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sivin Varghese 2022-08-10 10:48:41 +05:30 committed by GitHub
parent 16ad263a3a
commit 9bc75225fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 196 additions and 80 deletions

View file

@ -7,10 +7,18 @@ class ArticlesAPI extends PortalsAPI {
super('articles', { accountScoped: true });
}
getArticles({ pageNumber, portalSlug, locale, status, author_id }) {
getArticles({
pageNumber,
portalSlug,
locale,
status,
author_id,
category_slug,
}) {
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
if (status !== undefined) baseUrl += `&status=${status}`;
if (author_id) baseUrl += `&author_id=${author_id}`;
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
return axios.get(baseUrl);
}
}

View file

@ -5,8 +5,12 @@
{{ $t(`SIDEBAR.${menuItem.label}`) }}
</span>
<div v-if="isHelpCenterSidebar" class="submenu-icons">
<fluent-icon icon="search" class="submenu-icon" size="16" />
<fluent-icon icon="add" class="submenu-icon" size="16" />
<div class="submenu-icon">
<fluent-icon icon="search" size="16" />
</div>
<div class="submenu-icon" @click="onClickOpen">
<fluent-icon icon="add" size="16" />
</div>
</div>
</div>
<router-link
@ -71,6 +75,9 @@
</a>
</li>
</router-link>
<p v-if="isHelpCenterSidebar && isCategoryEmpty" class="empty-text">
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
</p>
</ul>
</li>
</template>
@ -98,6 +105,10 @@ export default {
type: Boolean,
default: false,
},
isCategoryEmpty: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({ activeInbox: 'getSelectedInbox' }),
@ -134,11 +145,8 @@ export default {
this.menuItem.toStateName === 'settings_applications'
);
},
isAllArticles() {
return (
this.$store.state.route.name === 'list_all_locale_articles' &&
this.menuItem.toStateName === 'all_locale_articles'
);
isArticlesView() {
return this.$store.state.route.name === this.menuItem.toStateName;
},
computedClass() {
@ -158,7 +166,7 @@ export default {
return ' ';
}
if (this.isHelpCenterSidebar) {
if (this.isAllArticles) {
if (this.isArticlesView) {
return 'is-active';
}
return ' ';
@ -195,6 +203,9 @@ export default {
showItem(item) {
return this.isAdmin && item.newLink !== undefined;
},
onClickOpen() {
this.$emit('open');
},
},
};
</script>
@ -324,4 +335,10 @@ export default {
}
}
}
.empty-text {
color: var(--s-600);
font-size: var(--font-size-small);
margin: var(--space-smaller) 0;
}
</style>

View file

@ -70,4 +70,7 @@ export default {
color: var(--s-600);
font-style: normal;
}
.message {
margin-top: 0 !important;
}
</style>

View file

@ -174,6 +174,10 @@
"BUTTONS": {
"CREATE": "Create category",
"CANCEL": "Cancel"
},
"API": {
"SUCCESS_MESSAGE": "Category created successfully",
"ERROR_MESSAGE": "Unable to create category"
}
}
}

View file

@ -197,7 +197,8 @@
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
"ARCHIVED": "Archived",
"CATEGORY": "Category"
"CATEGORY": "Category",
"CATEGORY_EMPTY_MESSAGE": "No categories found"
},
"DOCS": "Read docs"
},

View file

@ -1,5 +1,5 @@
<template>
<modal :show.sync="show" :on-close="onClose">
<woot-modal :show.sync="show" :on-close="onClose">
<woot-modal-header
:header-title="$t('HELP_CENTER.CATEGORY.ADD.TITLE')"
:header-content="$t('HELP_CENTER.CATEGORY.ADD.SUB_TITLE')"
@ -40,7 +40,7 @@
:help-text="$t('HELP_CENTER.CATEGORY.ADD.SLUG.HELP_TEXT')"
@input="$v.slug.$touch"
/>
<label :class="{ error: $v.description.$error }">
<label>
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.LABEL') }}
<textarea
v-model="description"
@ -49,37 +49,30 @@
:placeholder="
$t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.PLACEHOLDER')
"
@blur="$v.description.$touch"
/>
<span v-if="$v.description.$error" class="message">
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.ERROR') }}
</span>
</label>
<div class="medium-12 columns">
<div class="modal-footer justify-content-end w-full">
<woot-button class="button clear" @click.prevent="onClose">
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CANCEL') }}
</woot-button>
<woot-button>
<woot-button @click="addCategory">
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CREATE') }}
</woot-button>
</div>
</div>
</div>
</form>
</modal>
</woot-modal>
</template>
<script>
import Modal from 'dashboard/components/Modal';
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { required, minLength } from 'vuelidate/lib/validators';
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
export default {
components: {
Modal,
},
mixins: [alertMixin],
props: {
show: {
@ -110,11 +103,14 @@ export default {
slug: {
required,
},
description: {
required,
},
},
computed: {
...mapGetters({
selectedPortal: 'portals/getSelectedPortal',
}),
selectedPortalSlug() {
return this.selectedPortal?.slug;
},
nameError() {
if (this.$v.name.$error) {
return this.$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
@ -134,17 +130,38 @@ export default {
},
onCreate() {
this.$emit('create');
this.reset();
this.$emit('cancel');
},
onClose() {
this.reset();
this.$emit('cancel');
},
reset() {
this.name = '';
this.slug = '';
this.description = '';
async addCategory() {
const { name, slug, description } = this;
const data = {
name,
slug,
description,
};
this.$v.$touch();
if (this.$v.$invalid) {
return;
}
try {
await this.$store.dispatch('categories/create', {
portalSlug: this.selectedPortalSlug,
categoryObj: data,
});
this.alertMessage = this.$t(
'HELP_CENTER.CATEGORY.ADD.API.SUCCESS_MESSAGE'
);
this.onClose();
} catch (error) {
const errorMessage = error?.message;
this.alertMessage =
errorMessage || this.$t('HELP_CENTER.CATEGORY.ADD.API.ERROR_MESSAGE');
} finally {
this.showAlert(this.alertMessage);
}
},
},
};

View file

@ -13,6 +13,7 @@
:accessible-menu-items="accessibleMenuItems"
:additional-secondary-menu-items="additionalSecondaryMenuItems"
@open-popover="openPortalPopover"
@open-modal="onClickOpenAddCatogoryModal"
/>
</div>
<section class="app-content columns">
@ -33,6 +34,12 @@
:active-portal="selectedPortal"
@close-popover="closePortalPopover"
/>
<add-category
v-if="showAddCategoryModal"
:portal-name="selectedPortalName"
:locale="selectedPortalLocale"
@cancel="onClickCloseAddCategoryModal"
/>
</section>
</div>
</template>
@ -45,8 +52,10 @@ import PortalPopover from '../components/PortalPopover.vue';
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel';
import portalMixin from '../mixins/portalMixin';
import AddCategory from '../components/AddCategory.vue';
export default {
components: {
Sidebar,
@ -55,6 +64,7 @@ export default {
WootKeyShortcutModal,
NotificationPanel,
PortalPopover,
AddCategory,
},
mixins: [portalMixin],
data() {
@ -62,6 +72,7 @@ export default {
showShortcutModal: false,
showNotificationPanel: false,
showPortalPopover: false,
showAddCategoryModal: false,
};
},
@ -70,9 +81,13 @@ export default {
accountId: 'getCurrentAccountId',
selectedPortal: 'portals/getSelectedPortal',
portals: 'portals/allPortals',
categories: 'categories/allCategories',
meta: 'portals/getMeta',
isFetching: 'portals/isFetchingPortals',
}),
selectedPortalName() {
return this.selectedPortal ? this.selectedPortal.name : '';
},
selectedPortalSlug() {
return this.portalSlug || this.selectedPortal?.slug;
},
@ -98,18 +113,18 @@ export default {
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles`
),
toolTip: 'All Articles',
toStateName: 'list_all_selectedPortalLocale_articles',
toStateName: 'list_all_locale_articles',
},
{
icon: 'pen',
label: 'HELP_CENTER.MY_ARTICLES',
key: 'mine_articles',
key: 'list_mine_articles',
count: mineArticlesCount,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/mine`
),
toolTip: 'My articles',
toStateName: 'mine_articles',
toStateName: 'list_mine_articles',
},
{
icon: 'draft',
@ -142,26 +157,15 @@ export default {
label: 'HELP_CENTER.CATEGORY',
hasSubMenu: true,
key: 'category',
children: [
{
id: 1,
label: 'Getting started',
count: 12,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/getting-started`
),
},
{
id: 2,
label: 'Channel',
count: 19,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/channel`
),
},
],
children: this.categories.map(category => ({
id: category.id,
label: category.name,
count: category.meta.articles_count,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${category.locale}/categories/${category.slug}`
),
})),
},
];
},
@ -173,11 +177,15 @@ export default {
},
},
mounted() {
this.fetchPortals();
this.fetchPortalsAndItsCategories();
},
methods: {
fetchPortals() {
this.$store.dispatch('portals/index');
fetchPortalsAndItsCategories() {
this.$store.dispatch('portals/index').then(() => {
this.$store.dispatch('categories/index', {
portalSlug: this.selectedPortalSlug,
});
});
},
toggleKeyShortcutModal() {
this.showShortcutModal = true;
@ -197,6 +205,18 @@ export default {
closePortalPopover() {
this.showPortalPopover = false;
},
openPortalPage() {
this.$router.push({
name: 'list_all_portals',
});
this.showPortalPopover = false;
},
onClickOpenAddCatogoryModal() {
this.showAddCategoryModal = true;
},
onClickCloseAddCategoryModal() {
this.showAddCategoryModal = false;
},
},
};
</script>

View file

@ -19,6 +19,8 @@
:key="menuItem.key"
:menu-item="menuItem"
:is-help-center-sidebar="true"
:is-category-empty="!hasCategory"
@open="onClickOpenAddCatogoryModal"
/>
</transition-group>
</div>
@ -60,6 +62,14 @@ export default {
data() {
return {};
},
computed: {
hasCategory() {
return (
this.additionalSecondaryMenuItems[0] &&
this.additionalSecondaryMenuItems[0].children.length > 0
);
},
},
methods: {
onSearch(value) {
this.$emit('input', value);
@ -67,6 +77,9 @@ export default {
openPortalPopover() {
this.$emit('open-popover');
},
onClickOpenAddCatogoryModal() {
this.$emit('open-modal');
},
},
};
</script>

View file

@ -1,4 +1,4 @@
import AddCategoryComponent from './AddCategory.vue';
import AddCategoryComponent from '../AddCategory.vue';
import { action } from '@storybook/addon-actions';
export default {

View file

@ -9,7 +9,7 @@ const ShowPortal = () => import('./pages/portals/ShowPortal');
const ListAllCategories = () => import('./pages/categories/ListAllCategories');
const NewCategory = () => import('./pages/categories/NewCategory');
const EditCategory = () => import('./pages/categories/EditCategory');
const ShowCategory = () => import('./pages/categories/ShowCategory');
// const ShowCategory = () => import('./pages/categories/ShowCategory');
const ListCategoryArticles = () =>
import('./pages/articles/ListCategoryArticles');
@ -103,7 +103,7 @@ const categoryRoutes = [
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'show_category',
roles: ['administrator', 'agent'],
component: ShowCategory,
component: ListAllArticles,
},
{
path: getPortalRoute(

View file

@ -2,7 +2,7 @@
<div class="container">
<article-header
:header-title="headerTitle"
:count="meta.count"
:count="articleCount"
selected-value="Published"
@newArticlePage="newArticlePage"
/>
@ -10,7 +10,7 @@
:articles="articles"
:article-count="articles.length"
:current-page="Number(meta.currentPage)"
:total-count="meta.count"
:total-count="articleCount"
@on-page-change="onPageChange"
/>
<div v-if="shouldShowLoader" class="articles--loader">
@ -45,17 +45,31 @@ export default {
computed: {
...mapGetters({
articles: 'articles/allArticles',
categories: 'categories/allCategories',
selectedPortal: 'portals/getSelectedPortal',
uiFlags: 'articles/uiFlags',
meta: 'articles/getMeta',
isFetching: 'articles/isFetching',
currentUserId: 'getCurrentUserID',
}),
selectedCategory() {
return this.categories.find(
category => category.slug === this.selectedCategorySlug
);
},
shouldShowEmptyState() {
return !this.isFetching && !this.articles.length;
},
shouldShowLoader() {
return this.isFetching && !this.articles.length;
},
selectedPortalSlug() {
return this.selectedPortal?.slug;
},
selectedCategorySlug() {
const { categorySlug } = this.$route.params;
return categorySlug;
},
articleType() {
return this.$route.path.split('/').pop();
},
@ -68,6 +82,9 @@ export default {
case 'archived':
return this.$t('HELP_CENTER.HEADER.TITLES.ARCHIVED');
default:
if (this.$route.name === 'show_category') {
return this.headerTitleInCategoryView;
}
return this.$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES');
}
},
@ -89,6 +106,14 @@ export default {
}
return null;
},
articleCount() {
return this.articles ? this.articles.length : 0;
},
headerTitleInCategoryView() {
return this.categories && this.categories.length
? this.selectedCategory.name
: '';
},
},
watch: {
$route() {
@ -111,6 +136,7 @@ export default {
locale: this.$route.params.locale,
status: this.status,
author_id: this.author,
category_slug: this.selectedCategorySlug,
});
},
onPageChange(page) {

View file

@ -5,7 +5,7 @@ import types from '../../mutation-types';
export const actions = {
index: async (
{ commit },
{ pageNumber, portalSlug, locale, status, author_id }
{ pageNumber, portalSlug, locale, status, author_id, category_slug }
) => {
try {
commit(types.SET_UI_FLAG, { isFetching: true });
@ -17,6 +17,7 @@ export const actions = {
locale,
status,
author_id,
category_slug,
});
const articleIds = payload.map(article => article.id);
commit(types.CLEAR_ARTICLES);

View file

@ -5,13 +5,16 @@ 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;
if (portalSlug) {
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;
}
return '';
} catch (error) {
return throwErrorMessage(error);
} finally {
@ -19,12 +22,14 @@ export const actions = {
}
},
create: async ({ commit }, portalSlug, categoryObj) => {
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);
const {
data: { payload },
} = await categoriesAPI.create({ portalSlug, categoryObj });
const { id: categoryId } = payload;
commit(types.ADD_CATEGORY, payload);
commit(types.ADD_CATEGORY_ID, categoryId);
return categoryId;
} catch (error) {

View file

@ -32,12 +32,13 @@ describe('#actions', () => {
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]);
axios.post.mockResolvedValue({ data: categoriesPayload });
await actions.create({ commit }, categoriesPayload);
const { id: categoryId } = categoriesPayload;
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.ADD_CATEGORY, categoriesPayload.payload],
[types.default.ADD_CATEGORY_ID, categoryId],
[types.default.SET_UI_FLAG, { isCreating: false }],
]);
});