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:
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<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')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="onCreate">
|
||||
<div class="medium-12 columns">
|
||||
<div class="row article-info">
|
||||
<div class="columns medium-6">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.PORTAL') }}</span>
|
||||
<p class="value">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="columns medium-6">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.LOCALE') }}</span>
|
||||
<p class="value">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<woot-input
|
||||
v-model.trim="name"
|
||||
:class="{ error: $v.name.$error }"
|
||||
class="medium-12 columns"
|
||||
:error="nameError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.NAME.HELP_TEXT')"
|
||||
@input="onNameChange"
|
||||
/>
|
||||
<woot-input
|
||||
v-model.trim="slug"
|
||||
:class="{ error: $v.slug.$error }"
|
||||
class="medium-12 columns"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.ADD.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.ADD.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.ADD.SLUG.HELP_TEXT')"
|
||||
@input="$v.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.ADD.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</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 @click="addCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.ADD.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
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 {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
portalName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
slug: {
|
||||
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');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
slugError() {
|
||||
if (this.$v.slug.$error) {
|
||||
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNameChange() {
|
||||
this.slug = convertToCategorySlug(this.name);
|
||||
},
|
||||
onCreate() {
|
||||
this.$emit('create');
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.article-info {
|
||||
width: 100%;
|
||||
margin: 0 0 var(--space-normal);
|
||||
.value {
|
||||
color: var(--s-600);
|
||||
}
|
||||
}
|
||||
|
||||
.input-container::v-deep {
|
||||
margin: 0 0 var(--space-normal);
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import AddCategoryComponent from '../AddCategory.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Components/Help Center',
|
||||
component: AddCategoryComponent,
|
||||
argTypes: {
|
||||
show: {
|
||||
defaultValue: true,
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AddCategoryComponent },
|
||||
template:
|
||||
'<add-category-component v-bind="$props" @create="onCreate" @cancel="onClose" />',
|
||||
});
|
||||
|
||||
export const AddCategory = Template.bind({});
|
||||
AddCategory.args = {
|
||||
portalName: 'Chatwoot help center',
|
||||
locale: 'En-US',
|
||||
onCreate: action('create'),
|
||||
onClose: action('cancel'),
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user