feat: Portals store integration (#5185)

This commit is contained in:
Muhsin Keloth
2022-08-08 15:47:32 +05:30
committed by GitHub
parent 052422ed03
commit 20f3568583
30 changed files with 982 additions and 413 deletions
@@ -1,9 +1,18 @@
import ApiClient from '../ApiClient';
/* global axios */
class ArticlesAPI extends ApiClient {
import PortalsAPI from './portals';
class ArticlesAPI extends PortalsAPI {
constructor() {
super('articles', { accountScoped: true });
}
getArticles({ pageNumber, portalSlug, locale, status, author_id }) {
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
if (status !== undefined) baseUrl += `&status=${status}`;
if (author_id) baseUrl += `&author_id=${author_id}`;
return axios.get(baseUrl);
}
}
export default new ArticlesAPI();
@@ -1,16 +1,9 @@
/* global axios */
import ApiClient from '../ApiClient';
class PortalsAPI extends ApiClient {
constructor() {
super('portals', { accountScoped: true });
}
getArticles({ pageNumber, portalSlug, locale }) {
return axios.get(
`${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`
);
}
}
export default PortalsAPI;
@@ -0,0 +1,29 @@
import articlesAPI from '../helpCenter/articles';
import ApiClient from 'dashboard/api/helpCenter/portals';
import describeWithAPIMock from './apiSpecHelper';
describe('#PortalAPI', () => {
it('creates correct instance', () => {
expect(articlesAPI).toBeInstanceOf(ApiClient);
expect(articlesAPI).toHaveProperty('get');
expect(articlesAPI).toHaveProperty('show');
expect(articlesAPI).toHaveProperty('create');
expect(articlesAPI).toHaveProperty('update');
expect(articlesAPI).toHaveProperty('delete');
expect(articlesAPI).toHaveProperty('getArticles');
});
describeWithAPIMock('API calls', context => {
it('#getArticles', () => {
articlesAPI.getArticles({
pageNumber: 1,
portalSlug: 'room-rental',
locale: 'en-US',
status: 'published',
author_id: '1',
});
expect(context.axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
);
});
});
});
@@ -0,0 +1,13 @@
import PortalsAPI from '../helpCenter/portals';
import ApiClient from '../ApiClient';
const portalAPI = new PortalsAPI();
describe('#PortalAPI', () => {
it('creates correct instance', () => {
expect(portalAPI).toBeInstanceOf(ApiClient);
expect(portalAPI).toHaveProperty('get');
expect(portalAPI).toHaveProperty('show');
expect(portalAPI).toHaveProperty('create');
expect(portalAPI).toHaveProperty('update');
expect(portalAPI).toHaveProperty('delete');
});
});
@@ -31,7 +31,9 @@
"NEW_BUTTON": "New Portal",
"ACTIVE_BADGE": "active",
"CHOOSE_LOCALE_LABEL": "Choose a locale",
"LOADING_MESSAGE": "Loading portals...",
"ARTICLES_LABEL": "articles",
"NO_PORTALS_MESSAGE": "There are no available portals",
"ADD_NEW_LOCALE": "Add a new locale",
"POPOVER": {
"TITLE": "Portals",
@@ -15,7 +15,7 @@
</div>
</div>
</td>
<td>{{ category }}</td>
<td>{{ category.name }}</td>
<td>{{ readCount }}</td>
<td>
<Label :title="status" :color-scheme="labelColor" />
@@ -48,8 +48,8 @@ export default {
default: () => {},
},
category: {
type: String,
default: '',
type: Object,
default: () => {},
},
readCount: {
type: Number,
@@ -28,6 +28,7 @@
</tbody>
</table>
<table-footer
v-if="articles.length"
:on-page-change="onPageChange"
:current-page="Number(currentPage)"
:total-count="totalCount"
@@ -6,10 +6,10 @@
@open-key-shortcut-modal="toggleKeyShortcutModal"
@close-key-shortcut-modal="closeKeyShortcutModal"
/>
<div class="margin-right-small">
<div v-if="portals.length" class="margin-right-small">
<help-center-sidebar
header-title="Help Center"
sub-title="English"
:header-title="headerTitle"
:sub-title="localeName(selectedPortalLocale)"
:accessible-menu-items="accessibleMenuItems"
:additional-secondary-menu-items="additionalSecondaryMenuItems"
@open-popover="openPortalPopover"
@@ -30,8 +30,8 @@
<portal-popover
v-if="showPortalPopover"
:portals="portals"
:active-portal="selectedPortal"
@close-popover="closePortalPopover"
@open-portal-page="openPortalPage"
/>
</section>
</div>
@@ -41,12 +41,12 @@ import { mapGetters } from 'vuex';
import { frontendURL } from '../../../../helper/URLHelper';
import Sidebar from 'dashboard/components/layout/Sidebar';
import PortalPopover from 'dashboard/routes/dashboard/helpcenter/components/PortalPopover';
import HelpCenterSidebar from 'dashboard/routes/dashboard/helpcenter/components/Sidebar/Sidebar';
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 portalMixin from '../mixins/portalMixin';
export default {
components: {
Sidebar,
@@ -56,6 +56,7 @@ export default {
NotificationPanel,
PortalPopover,
},
mixins: [portalMixin],
data() {
return {
showShortcutModal: false,
@@ -63,36 +64,49 @@ export default {
showPortalPopover: false,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
selectedPortal: 'portals/getSelectedPortal',
portals: 'portals/allPortals',
meta: 'portals/getMeta',
isFetching: 'portals/isFetchingPortals',
}),
portalSlug() {
return this.$route.params.portalSlug;
selectedPortalSlug() {
return this.portalSlug || this.selectedPortal?.slug;
},
locale() {
return this.$route.params.locale;
selectedPortalLocale() {
return this.locale || this.selectedPortal?.meta?.default_locale;
},
accessibleMenuItems() {
const {
meta: {
all_articles_count: allArticlesCount,
mine_articles_count: mineArticlesCount,
draft_articles_count: draftArticlesCount,
archived_articles_count: archivedArticlesCount,
} = {},
} = this.selectedPortal;
return [
{
icon: 'book',
label: 'HELP_CENTER.ALL_ARTICLES',
key: 'list_all_locale_articles',
count: 199,
count: allArticlesCount,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles`
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles`
),
toolTip: 'All Articles',
toStateName: 'list_all_locale_articles',
toStateName: 'list_all_selectedPortalLocale_articles',
},
{
icon: 'pen',
label: 'HELP_CENTER.MY_ARTICLES',
key: 'mine_articles',
count: 112,
count: mineArticlesCount,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/mine`
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/mine`
),
toolTip: 'My articles',
toStateName: 'mine_articles',
@@ -101,9 +115,9 @@ export default {
icon: 'draft',
label: 'HELP_CENTER.DRAFT',
key: 'list_draft_articles',
count: 32,
count: draftArticlesCount,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/draft`
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/draft`
),
toolTip: 'Draft',
toStateName: 'list_draft_articles',
@@ -112,9 +126,9 @@ export default {
icon: 'archive',
label: 'HELP_CENTER.ARCHIVED',
key: 'list_archived_articles',
count: 10,
count: archivedArticlesCount,
toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/archived`
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/archived`
),
toolTip: 'Archived',
toStateName: 'list_archived_articles',
@@ -147,172 +161,6 @@ export default {
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/channel`
),
},
{
id: 3,
label: 'Feature',
count: 24,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/feature`
),
},
{
id: 4,
label: 'Advanced',
count: 8,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/advanced`
),
},
{
id: 5,
label: 'Mobile app',
count: 3,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/mobile-app`
),
},
{
id: 6,
label: 'Others',
count: 39,
truncateLabel: true,
toState: frontendURL(
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/others`
),
},
],
},
];
},
portals() {
return [
{
name: 'Chatwoot Help Center',
id: 1,
color: null,
custom_domain: 'doc',
articles_count: 123,
header_text: null,
homepage_link: null,
page_title: null,
slug: 'first_portal',
archived: false,
config: {
allowed_locales: [
{
code: 'en',
name: 'English',
articles_count: 123,
},
{
code: 'fr',
name: 'Français',
articles_count: 123,
},
{
code: 'de',
name: 'Deutsch',
articles_count: 32,
},
{
code: 'es',
name: 'Español',
articles_count: 12,
},
{
code: 'it',
name: 'Italiano',
articles_count: 8,
},
],
},
locales: [
{
name: 'English',
code: 'en',
articles_count: 12,
},
{
name: 'Español',
code: 'es',
articles_count: 42,
},
{
name: 'French',
code: 'fr',
articles_count: 29,
},
{
name: 'Italian',
code: 'it',
articles_count: 4,
},
{
name: 'German',
code: 'de',
articles_count: 66,
},
],
},
{
name: 'Chatwoot Docs',
id: 2,
color: null,
custom_domain: 'doc',
articles_count: 124,
header_text: null,
homepage_link: null,
page_title: null,
slug: 'second_portal',
archived: false,
config: {
allowed_locales: [
{
code: 'en',
name: 'English',
articles_count: 123,
},
{
code: 'fr',
name: 'Français',
articles_count: 123,
},
{
code: 'de',
name: 'Deutsch',
articles_count: 32,
},
{
code: 'es',
name: 'Español',
articles_count: 12,
},
{
code: 'it',
name: 'Italiano',
articles_count: 8,
},
],
},
locales: [
{
name: 'English',
code: 'en',
articles_count: 12,
},
{
name: 'Japanese',
code: 'jp',
articles_count: 4,
},
{
name: 'Mandarin',
code: 'CH',
articles_count: 6,
},
],
},
];
@@ -320,8 +168,17 @@ export default {
currentRoute() {
return ' ';
},
headerTitle() {
return this.selectedPortal.name;
},
},
mounted() {
this.fetchPortals();
},
methods: {
fetchPortals() {
this.$store.dispatch('portals/index');
},
toggleKeyShortcutModal() {
this.showShortcutModal = true;
},
@@ -340,12 +197,6 @@ export default {
closePortalPopover() {
this.showPortalPopover = false;
},
openPortalPage() {
this.$router.push({
name: 'list_all_portals',
});
this.showPortalPopover = false;
},
},
};
</script>
@@ -1,6 +1,6 @@
<template>
<div>
<div v-for="portal in portals" :key="portal.id" class="portal">
<div class="portal">
<thumbnail :username="portal.name" variant="square" />
<div class="container">
<header>
@@ -118,7 +118,6 @@
class="theme-color"
:style="{ background: portal.color }"
/>
<span class="text-block-title">{{ portal.page_title }}</span>
</div>
</div>
<div class="configuration-item">
@@ -127,7 +126,7 @@
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
)
}}</label>
<span class="text-block-title">{{ portal.page_title }}</span>
<span class="text-block-title">{{ portal.header_text }}</span>
</div>
</div>
</div>
@@ -141,8 +140,8 @@
}}
</h2>
<locale-item-table
:portals="portal"
:selected-locale-code="selectedLocaleCode"
:locales="locales"
:selected-locale-code="portal.meta.default_locale"
@swap="swapLocale"
@delete="deleteLocale"
/>
@@ -163,33 +162,28 @@ export default {
LocaleItemTable,
},
props: {
portals: {
type: Array,
default: () => [],
portal: {
type: Object,
default: () => {},
},
status: {
type: String,
default: '',
values: ['archived', 'draft', 'published'],
},
selectedLocaleCode: {
type: String,
default: '',
},
},
computed: {
labelColor() {
switch (this.status) {
case 'archived':
return 'secondary';
case 'draft':
case 'Archived':
return 'warning';
default:
return 'success';
}
},
defaultLocale(code) {
return code === this.selectedLocaleCode;
locales() {
return this.portal ? this.portal.config.allowed_locales : [];
},
},
methods: {
@@ -37,9 +37,9 @@
<td colspan="100%" class="horizontal-line" />
</tr>
<tbody>
<tr v-for="locale in portals.locales" :key="locale.code">
<tr v-for="locale in locales" :key="locale.code">
<td>
<span>{{ locale.name }}</span>
<span>{{ localeName(locale.code) }}</span>
<Label
v-if="locale.code === selectedLocaleCode"
:title="
@@ -95,20 +95,23 @@
<script>
import Label from 'dashboard/components/ui/Label';
import portalMixin from '../mixins/portalMixin';
export default {
components: {
Label,
},
mixins: [portalMixin],
props: {
portals: {
type: Object,
default: () => {},
locales: {
type: Array,
default: () => [],
},
selectedLocaleCode: {
type: String,
default: '',
},
},
methods: {
swapLocale() {
this.$emit('swap');
@@ -10,7 +10,6 @@
color-scheme="secondary"
icon="settings"
size="small"
@click="openPortalPage"
>
{{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }}
</woot-button>
@@ -24,6 +23,8 @@
v-for="portal in portals"
:key="portal.id"
:portal="portal"
:active="portal.id === activePortal.id"
@open-portal-page="openPortalPage"
/>
</div>
<footer>
@@ -50,13 +51,26 @@ export default {
type: Array,
default: () => [],
},
activePortal: {
type: Object,
default: () => ({}),
},
},
methods: {
closePortalPopover() {
this.$emit('close-popover');
},
openPortalPage() {
this.$emit('open-portal-page');
openPortalPage({ slug, locale }) {
this.$emit('close-popover');
const portal = this.portals.find(p => p.slug === slug);
this.$store.dispatch('portals/setPortalId', portal.id);
this.$router.push({
name: 'list_all_locale_articles',
params: {
portalSlug: slug,
locale: locale,
},
});
},
},
};
@@ -6,7 +6,7 @@
<div>
<h2 class="portal-title">{{ portal.name }}</h2>
<p class="portal-count">
{{ portal.articles_count }}
{{ articlesCount }}
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }}
</p>
</div>
@@ -19,7 +19,7 @@
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
</h2>
<ul>
<li v-for="locale in portal.locales" :key="locale.code">
<li v-for="locale in locales" :key="locale.code">
<label :for="`locale-${locale.code}`" class="locale-item">
<input
:id="`locale-${locale.code}`"
@@ -27,9 +27,10 @@
type="radio"
name="locale"
:value="locale.code"
@click="onClick(locale.code, portal)"
/>
<div>
<p>{{ locale.name }}</p>
<p>{{ localeName(locale.code) }}</p>
<span>
{{ locale.articles_count }}
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} -
@@ -49,10 +50,12 @@
<script>
import thumbnail from 'dashboard/components/widgets/Thumbnail';
import portalMixin from '../mixins/portalMixin';
export default {
components: {
thumbnail,
},
mixins: [portalMixin],
props: {
portal: {
type: Object,
@@ -65,11 +68,24 @@ export default {
},
data() {
return {
selectedLocale: '',
selectedLocale: null,
};
},
computed: {
locales() {
return this.portal?.config?.allowed_locales;
},
articlesCount() {
return this.portal?.meta?.all_articles_count;
},
},
mounted() {
this.selectedLocale = this.portal.locales[0].code;
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
},
methods: {
onClick(code, portal) {
this.$emit('open-portal-page', { slug: portal.slug, locale: code });
},
},
};
</script>
@@ -1,5 +1,6 @@
import { mapGetters } from 'vuex';
import { frontendURL } from 'dashboard/helper/URLHelper';
import allLocales from 'shared/constants/locales.js';
export default {
computed: {
...mapGetters({ accountId: 'getCurrentAccountId' }),
@@ -16,5 +17,8 @@ export default {
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/${id}`
);
},
localeName(code) {
return allLocales[code];
},
},
};
@@ -40,7 +40,7 @@ describe('portalMixin', () => {
expect(wrapper.vm.accountId).toBe(1);
});
it('returns portal url', () => {
it('returns article url', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'en' },
@@ -65,4 +65,12 @@ describe('portalMixin', () => {
});
expect(wrapper.vm.portalSlug).toBe('campaign');
});
it('returns locale name', () => {
router.push({
name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'es' },
});
expect(wrapper.vm.localeName('es')).toBe('Spanish');
});
});
@@ -13,12 +13,12 @@
:total-count="meta.count"
@on-page-change="onPageChange"
/>
<div v-if="isFetching" class="articles--loader">
<div v-if="shouldShowLoader" class="articles--loader">
<spinner />
<span>{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}</span>
</div>
<empty-state
v-else-if="!isFetching && !articles.length"
v-else-if="shouldShowEmptyState"
:title="$t('HELP_CENTER.TABLE.NO_ARTICLES')"
/>
</div>
@@ -48,10 +48,13 @@ export default {
uiFlags: 'articles/uiFlags',
meta: 'articles/getMeta',
isFetching: 'articles/isFetching',
currentUserId: 'getCurrentUserID',
}),
showEmptyState() {
return this.articles.length === 0;
shouldShowEmptyState() {
return !this.isFetching && !this.articles.length;
},
shouldShowLoader() {
return this.isFetching && !this.articles.length;
},
articleType() {
return this.$route.path.split('/').pop();
@@ -68,19 +71,46 @@ export default {
return this.$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES');
}
},
status() {
switch (this.articleType) {
case 'draft':
return 0;
case 'published':
return 1;
case 'archived':
return 2;
default:
return undefined;
}
},
author() {
if (this.articleType === 'mine') {
return this.currentUserId;
}
return null;
},
},
watch: {
$route() {
this.pageNumber = 1;
this.fetchArticles();
},
},
mounted() {
this.fetchArticles({ pageNumber: this.pageNumber });
this.fetchArticles();
},
methods: {
newArticlePage() {
this.$router.push({ name: 'new_article' });
},
fetchArticles({ pageNumber }) {
fetchArticles() {
this.$store.dispatch('articles/index', {
pageNumber,
pageNumber: this.pageNumber,
portalSlug: this.$route.params.portalSlug,
locale: this.$route.params.locale,
status: this.status,
author_id: this.author,
});
},
onPageChange(page) {
@@ -8,134 +8,46 @@
</div>
<div class="portal-container">
<portal-list-item
:portals="portals"
status="published"
selected-locale-code="en-US"
v-for="portal in portals"
:key="portal.id"
:portal="portal"
:status="portalStatus"
/>
<div v-if="isFetching" class="portals--loader">
<spinner />
<span>{{ $t('HELP_CENTER.PORTAL.LOADING_MESSAGE') }}</span>
</div>
<empty-state
v-else-if="shouldShowEmptyState"
:title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')"
/>
</div>
</div>
</template>
<script>
import PortalListItem from 'dashboard/routes/dashboard/helpcenter/components/PortalListItem';
import { mapGetters } from 'vuex';
import PortalListItem from '../../components/PortalListItem';
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState';
export default {
components: {
PortalListItem,
EmptyState,
Spinner,
},
data() {
return {
// Dummy data for testing will remove once the state is implemented.
portals: [
{
name: 'Chatwoot Help Center',
id: 1,
color: 'red',
custom_domain: 'help-center.chatwoot.com',
articles_count: 123,
header_text: 'Help center',
homepage_link: null,
page_title: 'English',
slug: 'help-center',
archived: false,
config: {
allowed_locales: [
{
code: 'en-US',
name: 'English',
articles_count: 123,
categories_count: 42,
},
{
code: 'fr-FR',
name: 'Français',
articles_count: 23,
categories_count: 11,
},
{
code: 'de-DE',
name: 'Deutsch',
articles_count: 32,
categories_count: 12,
},
{
code: 'es-ES',
name: 'Español',
articles_count: 12,
categories_count: 4,
},
],
},
locales: [
{
code: 'en-US',
name: 'English',
articles_count: 123,
categories_count: 42,
},
{
code: 'fr-FR',
name: 'Français',
articles_count: 23,
categories_count: 11,
},
{
code: 'de-DE',
name: 'Deutsch',
articles_count: 32,
categories_count: 12,
},
{
code: 'es-ES',
name: 'Español',
articles_count: 12,
categories_count: 4,
},
],
},
{
name: 'Chatwoot Docs',
id: 2,
color: 'green',
custom_domain: 'doc-chatwoot.com',
articles_count: 67,
header_text: 'Docs',
homepage_link: null,
page_title: 'Portal',
slug: 'second_portal',
archived: false,
config: {
allowed_locales: [
{
name: 'English',
code: 'en-EN',
articles_count: 12,
categories_count: 66,
},
{
name: 'Mandarin',
code: 'ch-CH',
articles_count: 6,
categories_count: 23,
},
],
},
locales: [
{
name: 'English',
code: 'en-EN',
articles_count: 12,
categories_count: 66,
},
{
name: 'Mandarin',
code: 'ch-CH',
articles_count: 6,
categories_count: 23,
},
],
},
],
};
computed: {
...mapGetters({
portals: 'portals/allPortals',
meta: 'portals/getMeta',
isFetching: 'portals/isFetchingPortals',
}),
portalStatus() {
return this.archived ? 'Archived' : 'Live';
},
shouldShowEmptyState() {
return !this.isFetching && !this.portals.length;
},
},
methods: {
createPortal() {
@@ -149,7 +61,13 @@ export default {
.container {
padding: var(--space-small) var(--space-normal);
width: 100%;
.portals--loader {
align-items: center;
display: flex;
font-size: var(--font-size-default);
justify-content: center;
padding: var(--space-big);
}
.header-wrap {
display: flex;
justify-content: space-between;
+4
View File
@@ -36,6 +36,8 @@ import teams from './modules/teams';
import userNotificationSettings from './modules/userNotificationSettings';
import webhooks from './modules/webhooks';
import articles from './modules/helpCenterArticles';
import portals from './modules/helpCenterPortals';
import categories from './modules/helpCenterCategories';
Vue.use(Vuex);
export default new Vuex.Store({
@@ -75,5 +77,7 @@ export default new Vuex.Store({
userNotificationSettings,
webhooks,
articles,
portals,
categories,
},
});
@@ -1,18 +1,22 @@
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 }) => {
index: async (
{ commit },
{ pageNumber, portalSlug, locale, status, author_id }
) => {
try {
commit(types.SET_UI_FLAG, { isFetching: true });
const {
data: { payload, meta },
} = await portalAPIs.getArticles({
} = await articlesAPI.getArticles({
pageNumber,
portalSlug,
locale,
status,
author_id,
});
const articleIds = payload.map(article => article.id);
commit(types.CLEAR_ARTICLES);
@@ -8,7 +8,7 @@ export const defaultHelpCenterFlags = {
isDeleting: false,
};
const state = {
categoriess: {
categories: {
byId: {},
byLocale: {},
allIds: [],
@@ -3,13 +3,22 @@ import { throwErrorMessage } from 'dashboard/store/utils/api';
import { types } from './mutations';
const portalAPIs = new PortalAPI();
export const actions = {
index: async ({ commit }) => {
index: async ({ commit, state, dispatch }) => {
try {
commit(types.SET_UI_FLAG, { isFetching: true });
const { data } = await portalAPIs.get();
const portalIds = data.map(portal => portal.id);
commit(types.ADD_MANY_PORTALS_ENTRY, data);
const {
data: { payload, meta },
} = await portalAPIs.get();
commit(types.CLEAR_PORTALS);
const portalIds = payload.map(portal => portal.id);
commit(types.ADD_MANY_PORTALS_ENTRY, payload);
commit(types.ADD_MANY_PORTALS_IDS, portalIds);
const { selectedPortalId } = state;
// Check if selected portal is still in the portals list
if (!portalIds.includes(selectedPortalId)) {
dispatch('setPortalId', portalIds[0]);
}
commit(types.SET_PORTALS_META, meta);
} catch (error) {
throwErrorMessage(error);
} finally {
@@ -68,4 +77,8 @@ export const actions = {
});
}
},
setPortalId: async ({ commit }, portalId) => {
commit(types.SET_SELECTED_PORTAL_ID, portalId);
},
};
@@ -16,11 +16,18 @@ export const getters = {
},
allPortals: (...getterArguments) => {
const [state, _getters] = getterArguments;
const portals = state.portals.allIds.map(id => {
return _getters.portalById(id);
});
return portals;
},
count: state => state.portals.allIds.length || 0,
getMeta: state => {
return state.meta;
},
getSelectedPortal: (...getterArguments) => {
const [state, _getters] = getterArguments;
const { selectedPortalId } = state.portals;
return _getters.portalById(selectedPortalId);
},
};
@@ -9,6 +9,11 @@ export const defaultPortalFlags = {
};
const state = {
meta: {
count: 0,
currentPage: 1,
},
portals: {
byId: {},
allIds: [],
@@ -20,6 +25,7 @@ const state = {
meta: {
byId: {},
},
selectedPortalId: null,
},
uiFlags: {
allFetched: false,
@@ -4,9 +4,12 @@ import { defaultPortalFlags } from './index';
export const types = {
SET_UI_FLAG: 'setUIFlag',
ADD_PORTAL_ENTRY: 'addPortalEntry',
SET_PORTALS_META: 'setPortalsMeta',
ADD_MANY_PORTALS_ENTRY: 'addManyPortalsEntry',
ADD_PORTAL_ID: 'addPortalId',
CLEAR_PORTALS: 'clearPortals',
ADD_MANY_PORTALS_IDS: 'addManyPortalsIds',
SET_SELECTED_PORTAL_ID: 'setSelectedPortalId',
UPDATE_PORTAL_ENTRY: 'updatePortalEntry',
REMOVE_PORTAL_ENTRY: 'removePortalEntry',
REMOVE_PORTAL_ID: 'removePortalId',
@@ -32,9 +35,23 @@ export const mutations = {
portals.forEach(portal => {
allPortals[portal.id] = portal;
});
Vue.set($state.portals, 'byId', {
allPortals,
});
Vue.set($state.portals, 'byId', allPortals);
},
[types.CLEAR_PORTALS]: $state => {
Vue.set($state.portals, 'byId', {});
Vue.set($state.portals, 'allIds', []);
Vue.set($state.portals, 'uiFlags', {});
},
[types.SET_PORTALS_META]: ($state, data) => {
const { portals_count: count, current_page: currentPage } = data;
Vue.set($state.meta, 'count', count);
Vue.set($state.meta, 'currentPage', currentPage);
},
[types.SET_SELECTED_PORTAL_ID]: ($state, portalId) => {
Vue.set($state.portals, 'selectedPortalId', portalId);
},
[types.ADD_PORTAL_ID]($state, portalId) {
@@ -4,6 +4,7 @@ import { types } from '../mutations';
import { apiResponse } from './fixtures';
const commit = jest.fn();
const dispatch = jest.fn();
global.axios = axios;
jest.mock('axios');
@@ -11,11 +12,20 @@ describe('#actions', () => {
describe('#index', () => {
it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: apiResponse });
await actions.index({ commit });
await actions.index({
commit,
dispatch,
state: {
selectedPortalId: 4,
},
});
expect(dispatch.mock.calls).toMatchObject([['setPortalId', 1]]);
expect(commit.mock.calls).toEqual([
[types.SET_UI_FLAG, { isFetching: true }],
[types.ADD_MANY_PORTALS_ENTRY, apiResponse],
[types.CLEAR_PORTALS],
[types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload],
[types.ADD_MANY_PORTALS_IDS, [1, 2]],
[types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }],
[types.SET_UI_FLAG, { isFetching: false }],
]);
});
@@ -31,7 +41,7 @@ describe('#actions', () => {
describe('#create', () => {
it('sends correct actions if API is success', async () => {
axios.post.mockResolvedValue({ data: apiResponse[1] });
axios.post.mockResolvedValue({ data: apiResponse.payload[1] });
await actions.create(
{ commit },
{
@@ -42,7 +52,7 @@ describe('#actions', () => {
);
expect(commit.mock.calls).toEqual([
[types.SET_UI_FLAG, { isCreating: true }],
[types.ADD_PORTAL_ENTRY, apiResponse[1]],
[types.ADD_PORTAL_ENTRY, apiResponse.payload[1]],
[types.ADD_PORTAL_ID, 2],
[types.SET_UI_FLAG, { isCreating: false }],
]);
@@ -59,14 +69,14 @@ describe('#actions', () => {
describe('#update', () => {
it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: apiResponse[1] });
await actions.update({ commit }, apiResponse[1]);
axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
await actions.update({ commit }, apiResponse.payload[1]);
expect(commit.mock.calls).toEqual([
[
types.SET_HELP_PORTAL_UI_FLAG,
{ uiFlags: { isUpdating: true }, portalId: 2 },
],
[types.UPDATE_PORTAL_ENTRY, apiResponse[1]],
[types.UPDATE_PORTAL_ENTRY, apiResponse.payload[1]],
[
types.SET_HELP_PORTAL_UI_FLAG,
{ uiFlags: { isUpdating: false }, portalId: 2 },
@@ -75,9 +85,9 @@ describe('#actions', () => {
});
it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.update({ commit }, apiResponse[1])).rejects.toThrow(
Error
);
await expect(
actions.update({ commit }, apiResponse.payload[1])
).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([
[
types.SET_HELP_PORTAL_UI_FLAG,
@@ -123,4 +133,11 @@ describe('#actions', () => {
]);
});
});
describe('#setPortalId', () => {
it('sends correct actions', async () => {
axios.delete.mockResolvedValue({});
await actions.setPortalId({ commit }, 1);
expect(commit.mock.calls).toEqual([[types.SET_SELECTED_PORTAL_ID, 1]]);
});
});
});
@@ -1,4 +1,8 @@
export default {
meta: {
count: 0,
currentPage: 1,
},
portals: {
byId: {
1: {
@@ -36,6 +40,7 @@ export default {
1: { isFetching: false, isUpdating: true, isDeleting: false },
},
},
selectedPortalId: 1,
},
uiFlags: {
allFetched: false,
@@ -43,33 +48,39 @@ export default {
},
};
export const apiResponse = [
{
id: 1,
color: 'red',
custom_domain: 'domain_for_help',
header_text: 'Domain Header',
homepage_link: 'help-center',
name: 'help name',
page_title: 'page title',
slug: 'domain',
archived: false,
config: {
allowed_locales: ['en'],
export const apiResponse = {
payload: [
{
id: 1,
color: 'red',
custom_domain: 'domain_for_help',
header_text: 'Domain Header',
homepage_link: 'help-center',
name: 'help name',
page_title: 'page title',
slug: 'domain',
archived: false,
config: {
allowed_locales: ['en'],
},
},
},
{
id: 2,
color: 'green',
custom_domain: 'campaign_for_help',
header_text: 'Campaign Header',
homepage_link: 'help-center',
name: 'help name',
page_title: 'campaign title',
slug: 'campaign',
archived: false,
config: {
allowed_locales: ['en'],
{
id: 2,
color: 'green',
custom_domain: 'campaign_for_help',
header_text: 'Campaign Header',
homepage_link: 'help-center',
name: 'help name',
page_title: 'campaign title',
slug: 'campaign',
archived: false,
config: {
allowed_locales: ['en'],
},
},
],
meta: {
current_page: 1,
portals_count: 1,
},
];
};
@@ -42,4 +42,9 @@ describe('#getters', () => {
const state = portal;
expect(getters.count(state)).toEqual(2);
});
it('getMeta', () => {
const state = portal;
expect(getters.getMeta(state)).toEqual({ count: 0, currentPage: 1 });
});
});
@@ -92,4 +92,33 @@ describe('#mutations', () => {
});
});
});
describe('#CLEAR_PORTALS', () => {
it('clears portals', () => {
mutations[types.CLEAR_PORTALS](state);
expect(state.portals.allIds).toEqual([]);
expect(state.portals.byId).toEqual({});
expect(state.portals.uiFlags).toEqual({});
});
});
describe('#SET_PORTALS_META', () => {
it('add meta to state', () => {
mutations[types.SET_PORTALS_META](state, {
portals_count: 10,
current_page: 1,
});
expect(state.meta).toEqual({
count: 10,
currentPage: 1,
});
});
});
describe('#SET_SELECTED_PORTAL_ID', () => {
it('set selected portal id', () => {
mutations[types.SET_SELECTED_PORTAL_ID](state, 4);
expect(state.portals.selectedPortalId).toEqual(4);
});
});
});