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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 982 additions and 413 deletions

View file

@ -53,3 +53,4 @@ exclude_patterns:
- 'app/javascript/dashboard/i18n/index.js' - 'app/javascript/dashboard/i18n/index.js'
- 'app/javascript/widget/i18n/index.js' - 'app/javascript/widget/i18n/index.js'
- 'app/javascript/survey/i18n/index.js' - 'app/javascript/survey/i18n/index.js'
- 'app/javascript/shared/constants/locales.js'

View file

@ -1,9 +1,18 @@
import ApiClient from '../ApiClient'; /* global axios */
class ArticlesAPI extends ApiClient { import PortalsAPI from './portals';
class ArticlesAPI extends PortalsAPI {
constructor() { constructor() {
super('articles', { accountScoped: true }); 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(); export default new ArticlesAPI();

View file

@ -1,16 +1,9 @@
/* global axios */
import ApiClient from '../ApiClient'; import ApiClient from '../ApiClient';
class PortalsAPI extends ApiClient { class PortalsAPI extends ApiClient {
constructor() { constructor() {
super('portals', { accountScoped: true }); super('portals', { accountScoped: true });
} }
getArticles({ pageNumber, portalSlug, locale }) {
return axios.get(
`${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`
);
}
} }
export default PortalsAPI; export default PortalsAPI;

View file

@ -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'
);
});
});
});

View file

@ -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');
});
});

View file

@ -31,7 +31,9 @@
"NEW_BUTTON": "New Portal", "NEW_BUTTON": "New Portal",
"ACTIVE_BADGE": "active", "ACTIVE_BADGE": "active",
"CHOOSE_LOCALE_LABEL": "Choose a locale", "CHOOSE_LOCALE_LABEL": "Choose a locale",
"LOADING_MESSAGE": "Loading portals...",
"ARTICLES_LABEL": "articles", "ARTICLES_LABEL": "articles",
"NO_PORTALS_MESSAGE": "There are no available portals",
"ADD_NEW_LOCALE": "Add a new locale", "ADD_NEW_LOCALE": "Add a new locale",
"POPOVER": { "POPOVER": {
"TITLE": "Portals", "TITLE": "Portals",

View file

@ -15,7 +15,7 @@
</div> </div>
</div> </div>
</td> </td>
<td>{{ category }}</td> <td>{{ category.name }}</td>
<td>{{ readCount }}</td> <td>{{ readCount }}</td>
<td> <td>
<Label :title="status" :color-scheme="labelColor" /> <Label :title="status" :color-scheme="labelColor" />
@ -48,8 +48,8 @@ export default {
default: () => {}, default: () => {},
}, },
category: { category: {
type: String, type: Object,
default: '', default: () => {},
}, },
readCount: { readCount: {
type: Number, type: Number,

View file

@ -28,6 +28,7 @@
</tbody> </tbody>
</table> </table>
<table-footer <table-footer
v-if="articles.length"
:on-page-change="onPageChange" :on-page-change="onPageChange"
:current-page="Number(currentPage)" :current-page="Number(currentPage)"
:total-count="totalCount" :total-count="totalCount"

View file

@ -6,10 +6,10 @@
@open-key-shortcut-modal="toggleKeyShortcutModal" @open-key-shortcut-modal="toggleKeyShortcutModal"
@close-key-shortcut-modal="closeKeyShortcutModal" @close-key-shortcut-modal="closeKeyShortcutModal"
/> />
<div class="margin-right-small"> <div v-if="portals.length" class="margin-right-small">
<help-center-sidebar <help-center-sidebar
header-title="Help Center" :header-title="headerTitle"
sub-title="English" :sub-title="localeName(selectedPortalLocale)"
:accessible-menu-items="accessibleMenuItems" :accessible-menu-items="accessibleMenuItems"
:additional-secondary-menu-items="additionalSecondaryMenuItems" :additional-secondary-menu-items="additionalSecondaryMenuItems"
@open-popover="openPortalPopover" @open-popover="openPortalPopover"
@ -30,8 +30,8 @@
<portal-popover <portal-popover
v-if="showPortalPopover" v-if="showPortalPopover"
:portals="portals" :portals="portals"
:active-portal="selectedPortal"
@close-popover="closePortalPopover" @close-popover="closePortalPopover"
@open-portal-page="openPortalPage"
/> />
</section> </section>
</div> </div>
@ -41,12 +41,12 @@ import { mapGetters } from 'vuex';
import { frontendURL } from '../../../../helper/URLHelper'; import { frontendURL } from '../../../../helper/URLHelper';
import Sidebar from 'dashboard/components/layout/Sidebar'; import Sidebar from 'dashboard/components/layout/Sidebar';
import PortalPopover from 'dashboard/routes/dashboard/helpcenter/components/PortalPopover'; import PortalPopover from '../components/PortalPopover.vue';
import HelpCenterSidebar from 'dashboard/routes/dashboard/helpcenter/components/Sidebar/Sidebar'; import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue'; import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal'; 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.vue';
import portalMixin from '../mixins/portalMixin';
export default { export default {
components: { components: {
Sidebar, Sidebar,
@ -56,6 +56,7 @@ export default {
NotificationPanel, NotificationPanel,
PortalPopover, PortalPopover,
}, },
mixins: [portalMixin],
data() { data() {
return { return {
showShortcutModal: false, showShortcutModal: false,
@ -63,36 +64,49 @@ export default {
showPortalPopover: false, showPortalPopover: false,
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
selectedPortal: 'portals/getSelectedPortal',
portals: 'portals/allPortals',
meta: 'portals/getMeta',
isFetching: 'portals/isFetchingPortals',
}), }),
portalSlug() { selectedPortalSlug() {
return this.$route.params.portalSlug; return this.portalSlug || this.selectedPortal?.slug;
}, },
locale() { selectedPortalLocale() {
return this.$route.params.locale; return this.locale || this.selectedPortal?.meta?.default_locale;
}, },
accessibleMenuItems() { accessibleMenuItems() {
const {
meta: {
all_articles_count: allArticlesCount,
mine_articles_count: mineArticlesCount,
draft_articles_count: draftArticlesCount,
archived_articles_count: archivedArticlesCount,
} = {},
} = this.selectedPortal;
return [ return [
{ {
icon: 'book', icon: 'book',
label: 'HELP_CENTER.ALL_ARTICLES', label: 'HELP_CENTER.ALL_ARTICLES',
key: 'list_all_locale_articles', key: 'list_all_locale_articles',
count: 199, count: allArticlesCount,
toState: frontendURL( toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles` `accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles`
), ),
toolTip: 'All Articles', toolTip: 'All Articles',
toStateName: 'list_all_locale_articles', toStateName: 'list_all_selectedPortalLocale_articles',
}, },
{ {
icon: 'pen', icon: 'pen',
label: 'HELP_CENTER.MY_ARTICLES', label: 'HELP_CENTER.MY_ARTICLES',
key: 'mine_articles', key: 'mine_articles',
count: 112, count: mineArticlesCount,
toState: frontendURL( 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', toolTip: 'My articles',
toStateName: 'mine_articles', toStateName: 'mine_articles',
@ -101,9 +115,9 @@ export default {
icon: 'draft', icon: 'draft',
label: 'HELP_CENTER.DRAFT', label: 'HELP_CENTER.DRAFT',
key: 'list_draft_articles', key: 'list_draft_articles',
count: 32, count: draftArticlesCount,
toState: frontendURL( toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/draft` `accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/draft`
), ),
toolTip: 'Draft', toolTip: 'Draft',
toStateName: 'list_draft_articles', toStateName: 'list_draft_articles',
@ -112,9 +126,9 @@ export default {
icon: 'archive', icon: 'archive',
label: 'HELP_CENTER.ARCHIVED', label: 'HELP_CENTER.ARCHIVED',
key: 'list_archived_articles', key: 'list_archived_articles',
count: 10, count: archivedArticlesCount,
toState: frontendURL( toState: frontendURL(
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/archived` `accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedPortalLocale}/articles/archived`
), ),
toolTip: 'Archived', toolTip: 'Archived',
toStateName: 'list_archived_articles', toStateName: 'list_archived_articles',
@ -147,172 +161,6 @@ export default {
`accounts/${this.accountId}/portals/:portalSlug/:locale/categories/channel` `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() { currentRoute() {
return ' '; return ' ';
}, },
headerTitle() {
return this.selectedPortal.name;
},
},
mounted() {
this.fetchPortals();
}, },
methods: { methods: {
fetchPortals() {
this.$store.dispatch('portals/index');
},
toggleKeyShortcutModal() { toggleKeyShortcutModal() {
this.showShortcutModal = true; this.showShortcutModal = true;
}, },
@ -340,12 +197,6 @@ export default {
closePortalPopover() { closePortalPopover() {
this.showPortalPopover = false; this.showPortalPopover = false;
}, },
openPortalPage() {
this.$router.push({
name: 'list_all_portals',
});
this.showPortalPopover = false;
},
}, },
}; };
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div v-for="portal in portals" :key="portal.id" class="portal"> <div class="portal">
<thumbnail :username="portal.name" variant="square" /> <thumbnail :username="portal.name" variant="square" />
<div class="container"> <div class="container">
<header> <header>
@ -118,7 +118,6 @@
class="theme-color" class="theme-color"
:style="{ background: portal.color }" :style="{ background: portal.color }"
/> />
<span class="text-block-title">{{ portal.page_title }}</span>
</div> </div>
</div> </div>
<div class="configuration-item"> <div class="configuration-item">
@ -127,7 +126,7 @@
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT' 'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
) )
}}</label> }}</label>
<span class="text-block-title">{{ portal.page_title }}</span> <span class="text-block-title">{{ portal.header_text }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -141,8 +140,8 @@
}} }}
</h2> </h2>
<locale-item-table <locale-item-table
:portals="portal" :locales="locales"
:selected-locale-code="selectedLocaleCode" :selected-locale-code="portal.meta.default_locale"
@swap="swapLocale" @swap="swapLocale"
@delete="deleteLocale" @delete="deleteLocale"
/> />
@ -163,33 +162,28 @@ export default {
LocaleItemTable, LocaleItemTable,
}, },
props: { props: {
portals: { portal: {
type: Array, type: Object,
default: () => [], default: () => {},
}, },
status: { status: {
type: String, type: String,
default: '', default: '',
values: ['archived', 'draft', 'published'], values: ['archived', 'draft', 'published'],
}, },
selectedLocaleCode: {
type: String,
default: '',
},
}, },
computed: { computed: {
labelColor() { labelColor() {
switch (this.status) { switch (this.status) {
case 'archived': case 'Archived':
return 'secondary';
case 'draft':
return 'warning'; return 'warning';
default: default:
return 'success'; return 'success';
} }
}, },
defaultLocale(code) {
return code === this.selectedLocaleCode; locales() {
return this.portal ? this.portal.config.allowed_locales : [];
}, },
}, },
methods: { methods: {

View file

@ -37,9 +37,9 @@
<td colspan="100%" class="horizontal-line" /> <td colspan="100%" class="horizontal-line" />
</tr> </tr>
<tbody> <tbody>
<tr v-for="locale in portals.locales" :key="locale.code"> <tr v-for="locale in locales" :key="locale.code">
<td> <td>
<span>{{ locale.name }}</span> <span>{{ localeName(locale.code) }}</span>
<Label <Label
v-if="locale.code === selectedLocaleCode" v-if="locale.code === selectedLocaleCode"
:title=" :title="
@ -95,20 +95,23 @@
<script> <script>
import Label from 'dashboard/components/ui/Label'; import Label from 'dashboard/components/ui/Label';
import portalMixin from '../mixins/portalMixin';
export default { export default {
components: { components: {
Label, Label,
}, },
mixins: [portalMixin],
props: { props: {
portals: { locales: {
type: Object, type: Array,
default: () => {}, default: () => [],
}, },
selectedLocaleCode: { selectedLocaleCode: {
type: String, type: String,
default: '', default: '',
}, },
}, },
methods: { methods: {
swapLocale() { swapLocale() {
this.$emit('swap'); this.$emit('swap');

View file

@ -10,7 +10,6 @@
color-scheme="secondary" color-scheme="secondary"
icon="settings" icon="settings"
size="small" size="small"
@click="openPortalPage"
> >
{{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }} {{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }}
</woot-button> </woot-button>
@ -24,6 +23,8 @@
v-for="portal in portals" v-for="portal in portals"
:key="portal.id" :key="portal.id"
:portal="portal" :portal="portal"
:active="portal.id === activePortal.id"
@open-portal-page="openPortalPage"
/> />
</div> </div>
<footer> <footer>
@ -50,13 +51,26 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
activePortal: {
type: Object,
default: () => ({}),
},
}, },
methods: { methods: {
closePortalPopover() { closePortalPopover() {
this.$emit('close-popover'); this.$emit('close-popover');
}, },
openPortalPage() { openPortalPage({ slug, locale }) {
this.$emit('open-portal-page'); 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,
},
});
}, },
}, },
}; };

View file

@ -6,7 +6,7 @@
<div> <div>
<h2 class="portal-title">{{ portal.name }}</h2> <h2 class="portal-title">{{ portal.name }}</h2>
<p class="portal-count"> <p class="portal-count">
{{ portal.articles_count }} {{ articlesCount }}
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} {{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }}
</p> </p>
</div> </div>
@ -19,7 +19,7 @@
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }} {{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
</h2> </h2>
<ul> <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"> <label :for="`locale-${locale.code}`" class="locale-item">
<input <input
:id="`locale-${locale.code}`" :id="`locale-${locale.code}`"
@ -27,9 +27,10 @@
type="radio" type="radio"
name="locale" name="locale"
:value="locale.code" :value="locale.code"
@click="onClick(locale.code, portal)"
/> />
<div> <div>
<p>{{ locale.name }}</p> <p>{{ localeName(locale.code) }}</p>
<span> <span>
{{ locale.articles_count }} {{ locale.articles_count }}
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} - {{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} -
@ -49,10 +50,12 @@
<script> <script>
import thumbnail from 'dashboard/components/widgets/Thumbnail'; import thumbnail from 'dashboard/components/widgets/Thumbnail';
import portalMixin from '../mixins/portalMixin';
export default { export default {
components: { components: {
thumbnail, thumbnail,
}, },
mixins: [portalMixin],
props: { props: {
portal: { portal: {
type: Object, type: Object,
@ -65,11 +68,24 @@ export default {
}, },
data() { data() {
return { return {
selectedLocale: '', selectedLocale: null,
}; };
}, },
computed: {
locales() {
return this.portal?.config?.allowed_locales;
},
articlesCount() {
return this.portal?.meta?.all_articles_count;
},
},
mounted() { 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> </script>

View file

@ -1,5 +1,6 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { frontendURL } from 'dashboard/helper/URLHelper'; import { frontendURL } from 'dashboard/helper/URLHelper';
import allLocales from 'shared/constants/locales.js';
export default { export default {
computed: { computed: {
...mapGetters({ accountId: 'getCurrentAccountId' }), ...mapGetters({ accountId: 'getCurrentAccountId' }),
@ -16,5 +17,8 @@ export default {
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/${id}` `accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/${id}`
); );
}, },
localeName(code) {
return allLocales[code];
},
}, },
}; };

View file

@ -40,7 +40,7 @@ describe('portalMixin', () => {
expect(wrapper.vm.accountId).toBe(1); expect(wrapper.vm.accountId).toBe(1);
}); });
it('returns portal url', () => { it('returns article url', () => {
router.push({ router.push({
name: 'list_all_locale_articles', name: 'list_all_locale_articles',
params: { portalSlug: 'fur-rent', locale: 'en' }, params: { portalSlug: 'fur-rent', locale: 'en' },
@ -65,4 +65,12 @@ describe('portalMixin', () => {
}); });
expect(wrapper.vm.portalSlug).toBe('campaign'); 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');
});
}); });

View file

@ -13,12 +13,12 @@
:total-count="meta.count" :total-count="meta.count"
@on-page-change="onPageChange" @on-page-change="onPageChange"
/> />
<div v-if="isFetching" class="articles--loader"> <div v-if="shouldShowLoader" class="articles--loader">
<spinner /> <spinner />
<span>{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}</span> <span>{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}</span>
</div> </div>
<empty-state <empty-state
v-else-if="!isFetching && !articles.length" v-else-if="shouldShowEmptyState"
:title="$t('HELP_CENTER.TABLE.NO_ARTICLES')" :title="$t('HELP_CENTER.TABLE.NO_ARTICLES')"
/> />
</div> </div>
@ -48,10 +48,13 @@ export default {
uiFlags: 'articles/uiFlags', uiFlags: 'articles/uiFlags',
meta: 'articles/getMeta', meta: 'articles/getMeta',
isFetching: 'articles/isFetching', isFetching: 'articles/isFetching',
currentUserId: 'getCurrentUserID',
}), }),
shouldShowEmptyState() {
showEmptyState() { return !this.isFetching && !this.articles.length;
return this.articles.length === 0; },
shouldShowLoader() {
return this.isFetching && !this.articles.length;
}, },
articleType() { articleType() {
return this.$route.path.split('/').pop(); return this.$route.path.split('/').pop();
@ -68,19 +71,46 @@ export default {
return this.$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES'); 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() { mounted() {
this.fetchArticles({ pageNumber: this.pageNumber }); this.fetchArticles();
}, },
methods: { methods: {
newArticlePage() { newArticlePage() {
this.$router.push({ name: 'new_article' }); this.$router.push({ name: 'new_article' });
}, },
fetchArticles({ pageNumber }) { fetchArticles() {
this.$store.dispatch('articles/index', { this.$store.dispatch('articles/index', {
pageNumber, pageNumber: this.pageNumber,
portalSlug: this.$route.params.portalSlug, portalSlug: this.$route.params.portalSlug,
locale: this.$route.params.locale, locale: this.$route.params.locale,
status: this.status,
author_id: this.author,
}); });
}, },
onPageChange(page) { onPageChange(page) {

View file

@ -8,134 +8,46 @@
</div> </div>
<div class="portal-container"> <div class="portal-container">
<portal-list-item <portal-list-item
:portals="portals" v-for="portal in portals"
status="published" :key="portal.id"
selected-locale-code="en-US" :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>
</div> </div>
</template> </template>
<script> <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 { export default {
components: { components: {
PortalListItem, PortalListItem,
EmptyState,
Spinner,
}, },
data() { computed: {
return { ...mapGetters({
// Dummy data for testing will remove once the state is implemented. portals: 'portals/allPortals',
portals: [ meta: 'portals/getMeta',
{ isFetching: 'portals/isFetchingPortals',
name: 'Chatwoot Help Center', }),
id: 1, portalStatus() {
color: 'red', return this.archived ? 'Archived' : 'Live';
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,
}, },
{ shouldShowEmptyState() {
code: 'fr-FR', return !this.isFetching && !this.portals.length;
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,
},
],
},
],
};
}, },
methods: { methods: {
createPortal() { createPortal() {
@ -149,7 +61,13 @@ export default {
.container { .container {
padding: var(--space-small) var(--space-normal); padding: var(--space-small) var(--space-normal);
width: 100%; width: 100%;
.portals--loader {
align-items: center;
display: flex;
font-size: var(--font-size-default);
justify-content: center;
padding: var(--space-big);
}
.header-wrap { .header-wrap {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View file

@ -36,6 +36,8 @@ import teams from './modules/teams';
import userNotificationSettings from './modules/userNotificationSettings'; import userNotificationSettings from './modules/userNotificationSettings';
import webhooks from './modules/webhooks'; import webhooks from './modules/webhooks';
import articles from './modules/helpCenterArticles'; import articles from './modules/helpCenterArticles';
import portals from './modules/helpCenterPortals';
import categories from './modules/helpCenterCategories';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export default new Vuex.Store({
@ -75,5 +77,7 @@ export default new Vuex.Store({
userNotificationSettings, userNotificationSettings,
webhooks, webhooks,
articles, articles,
portals,
categories,
}, },
}); });

View file

@ -1,18 +1,22 @@
import PortalAPI from 'dashboard/api/helpCenter/portals';
import articlesAPI from 'dashboard/api/helpCenter/articles'; import articlesAPI from 'dashboard/api/helpCenter/articles';
import { throwErrorMessage } from 'dashboard/store/utils/api'; import { throwErrorMessage } from 'dashboard/store/utils/api';
const portalAPIs = new PortalAPI();
import types from '../../mutation-types'; import types from '../../mutation-types';
export const actions = { export const actions = {
index: async ({ commit }, { pageNumber, portalSlug, locale }) => { index: async (
{ commit },
{ pageNumber, portalSlug, locale, status, author_id }
) => {
try { try {
commit(types.SET_UI_FLAG, { isFetching: true }); commit(types.SET_UI_FLAG, { isFetching: true });
const { const {
data: { payload, meta }, data: { payload, meta },
} = await portalAPIs.getArticles({ } = await articlesAPI.getArticles({
pageNumber, pageNumber,
portalSlug, portalSlug,
locale, locale,
status,
author_id,
}); });
const articleIds = payload.map(article => article.id); const articleIds = payload.map(article => article.id);
commit(types.CLEAR_ARTICLES); commit(types.CLEAR_ARTICLES);

View file

@ -8,7 +8,7 @@ export const defaultHelpCenterFlags = {
isDeleting: false, isDeleting: false,
}; };
const state = { const state = {
categoriess: { categories: {
byId: {}, byId: {},
byLocale: {}, byLocale: {},
allIds: [], allIds: [],

View file

@ -3,13 +3,22 @@ import { throwErrorMessage } from 'dashboard/store/utils/api';
import { types } from './mutations'; import { types } from './mutations';
const portalAPIs = new PortalAPI(); const portalAPIs = new PortalAPI();
export const actions = { export const actions = {
index: async ({ commit }) => { index: async ({ commit, state, dispatch }) => {
try { try {
commit(types.SET_UI_FLAG, { isFetching: true }); commit(types.SET_UI_FLAG, { isFetching: true });
const { data } = await portalAPIs.get(); const {
const portalIds = data.map(portal => portal.id); data: { payload, meta },
commit(types.ADD_MANY_PORTALS_ENTRY, data); } = 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); 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) { } catch (error) {
throwErrorMessage(error); throwErrorMessage(error);
} finally { } finally {
@ -68,4 +77,8 @@ export const actions = {
}); });
} }
}, },
setPortalId: async ({ commit }, portalId) => {
commit(types.SET_SELECTED_PORTAL_ID, portalId);
},
}; };

View file

@ -16,11 +16,18 @@ export const getters = {
}, },
allPortals: (...getterArguments) => { allPortals: (...getterArguments) => {
const [state, _getters] = getterArguments; const [state, _getters] = getterArguments;
const portals = state.portals.allIds.map(id => { const portals = state.portals.allIds.map(id => {
return _getters.portalById(id); return _getters.portalById(id);
}); });
return portals; return portals;
}, },
count: state => state.portals.allIds.length || 0, 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);
},
}; };

View file

@ -9,6 +9,11 @@ export const defaultPortalFlags = {
}; };
const state = { const state = {
meta: {
count: 0,
currentPage: 1,
},
portals: { portals: {
byId: {}, byId: {},
allIds: [], allIds: [],
@ -20,6 +25,7 @@ const state = {
meta: { meta: {
byId: {}, byId: {},
}, },
selectedPortalId: null,
}, },
uiFlags: { uiFlags: {
allFetched: false, allFetched: false,

View file

@ -4,9 +4,12 @@ import { defaultPortalFlags } from './index';
export const types = { export const types = {
SET_UI_FLAG: 'setUIFlag', SET_UI_FLAG: 'setUIFlag',
ADD_PORTAL_ENTRY: 'addPortalEntry', ADD_PORTAL_ENTRY: 'addPortalEntry',
SET_PORTALS_META: 'setPortalsMeta',
ADD_MANY_PORTALS_ENTRY: 'addManyPortalsEntry', ADD_MANY_PORTALS_ENTRY: 'addManyPortalsEntry',
ADD_PORTAL_ID: 'addPortalId', ADD_PORTAL_ID: 'addPortalId',
CLEAR_PORTALS: 'clearPortals',
ADD_MANY_PORTALS_IDS: 'addManyPortalsIds', ADD_MANY_PORTALS_IDS: 'addManyPortalsIds',
SET_SELECTED_PORTAL_ID: 'setSelectedPortalId',
UPDATE_PORTAL_ENTRY: 'updatePortalEntry', UPDATE_PORTAL_ENTRY: 'updatePortalEntry',
REMOVE_PORTAL_ENTRY: 'removePortalEntry', REMOVE_PORTAL_ENTRY: 'removePortalEntry',
REMOVE_PORTAL_ID: 'removePortalId', REMOVE_PORTAL_ID: 'removePortalId',
@ -32,9 +35,23 @@ export const mutations = {
portals.forEach(portal => { portals.forEach(portal => {
allPortals[portal.id] = portal; allPortals[portal.id] = portal;
}); });
Vue.set($state.portals, 'byId', { Vue.set($state.portals, 'byId', allPortals);
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) { [types.ADD_PORTAL_ID]($state, portalId) {

View file

@ -4,6 +4,7 @@ import { types } from '../mutations';
import { apiResponse } from './fixtures'; import { apiResponse } from './fixtures';
const commit = jest.fn(); const commit = jest.fn();
const dispatch = jest.fn();
global.axios = axios; global.axios = axios;
jest.mock('axios'); jest.mock('axios');
@ -11,11 +12,20 @@ describe('#actions', () => {
describe('#index', () => { describe('#index', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
axios.get.mockResolvedValue({ data: apiResponse }); 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([ expect(commit.mock.calls).toEqual([
[types.SET_UI_FLAG, { isFetching: true }], [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.ADD_MANY_PORTALS_IDS, [1, 2]],
[types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }],
[types.SET_UI_FLAG, { isFetching: false }], [types.SET_UI_FLAG, { isFetching: false }],
]); ]);
}); });
@ -31,7 +41,7 @@ describe('#actions', () => {
describe('#create', () => { describe('#create', () => {
it('sends correct actions if API is success', async () => { 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( await actions.create(
{ commit }, { commit },
{ {
@ -42,7 +52,7 @@ describe('#actions', () => {
); );
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[types.SET_UI_FLAG, { isCreating: true }], [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.ADD_PORTAL_ID, 2],
[types.SET_UI_FLAG, { isCreating: false }], [types.SET_UI_FLAG, { isCreating: false }],
]); ]);
@ -59,14 +69,14 @@ describe('#actions', () => {
describe('#update', () => { describe('#update', () => {
it('sends correct actions if API is success', async () => { it('sends correct actions if API is success', async () => {
axios.patch.mockResolvedValue({ data: apiResponse[1] }); axios.patch.mockResolvedValue({ data: apiResponse.payload[1] });
await actions.update({ commit }, apiResponse[1]); await actions.update({ commit }, apiResponse.payload[1]);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.SET_HELP_PORTAL_UI_FLAG, types.SET_HELP_PORTAL_UI_FLAG,
{ uiFlags: { isUpdating: true }, portalId: 2 }, { uiFlags: { isUpdating: true }, portalId: 2 },
], ],
[types.UPDATE_PORTAL_ENTRY, apiResponse[1]], [types.UPDATE_PORTAL_ENTRY, apiResponse.payload[1]],
[ [
types.SET_HELP_PORTAL_UI_FLAG, types.SET_HELP_PORTAL_UI_FLAG,
{ uiFlags: { isUpdating: false }, portalId: 2 }, { uiFlags: { isUpdating: false }, portalId: 2 },
@ -75,9 +85,9 @@ describe('#actions', () => {
}); });
it('sends correct actions if API is error', async () => { it('sends correct actions if API is error', async () => {
axios.patch.mockRejectedValue({ message: 'Incorrect header' }); axios.patch.mockRejectedValue({ message: 'Incorrect header' });
await expect(actions.update({ commit }, apiResponse[1])).rejects.toThrow( await expect(
Error actions.update({ commit }, apiResponse.payload[1])
); ).rejects.toThrow(Error);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
[ [
types.SET_HELP_PORTAL_UI_FLAG, 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]]);
});
});
}); });

View file

@ -1,4 +1,8 @@
export default { export default {
meta: {
count: 0,
currentPage: 1,
},
portals: { portals: {
byId: { byId: {
1: { 1: {
@ -36,6 +40,7 @@ export default {
1: { isFetching: false, isUpdating: true, isDeleting: false }, 1: { isFetching: false, isUpdating: true, isDeleting: false },
}, },
}, },
selectedPortalId: 1,
}, },
uiFlags: { uiFlags: {
allFetched: false, allFetched: false,
@ -43,7 +48,8 @@ export default {
}, },
}; };
export const apiResponse = [ export const apiResponse = {
payload: [
{ {
id: 1, id: 1,
color: 'red', color: 'red',
@ -72,4 +78,9 @@ export const apiResponse = [
allowed_locales: ['en'], allowed_locales: ['en'],
}, },
}, },
]; ],
meta: {
current_page: 1,
portals_count: 1,
},
};

View file

@ -42,4 +42,9 @@ describe('#getters', () => {
const state = portal; const state = portal;
expect(getters.count(state)).toEqual(2); expect(getters.count(state)).toEqual(2);
}); });
it('getMeta', () => {
const state = portal;
expect(getters.getMeta(state)).toEqual({ count: 0, currentPage: 1 });
});
}); });

View file

@ -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);
});
});
}); });

View file

@ -0,0 +1,566 @@
const locales = {
af: 'Afrikaans',
af_NA: 'Afrikaans (Namibia)',
af_ZA: 'Afrikaans (South Africa)',
ak: 'Akan',
ak_GH: 'Akan (Ghana)',
sq: 'Albanian',
sq_AL: 'Albanian (Albania)',
sq_XK: 'Albanian (Kosovo)',
sq_MK: 'Albanian (Macedonia)',
am: 'Amharic',
am_ET: 'Amharic (Ethiopia)',
ar: 'Arabic',
ar_DZ: 'Arabic (Algeria)',
ar_BH: 'Arabic (Bahrain)',
ar_TD: 'Arabic (Chad)',
ar_KM: 'Arabic (Comoros)',
ar_DJ: 'Arabic (Djibouti)',
ar_EG: 'Arabic (Egypt)',
ar_ER: 'Arabic (Eritrea)',
ar_IQ: 'Arabic (Iraq)',
ar_IL: 'Arabic (Israel)',
ar_JO: 'Arabic (Jordan)',
ar_KW: 'Arabic (Kuwait)',
ar_LB: 'Arabic (Lebanon)',
ar_LY: 'Arabic (Libya)',
ar_MR: 'Arabic (Mauritania)',
ar_MA: 'Arabic (Morocco)',
ar_OM: 'Arabic (Oman)',
ar_PS: 'Arabic (Palestinian Territories)',
ar_QA: 'Arabic (Qatar)',
ar_SA: 'Arabic (Saudi Arabia)',
ar_SO: 'Arabic (Somalia)',
ar_SS: 'Arabic (South Sudan)',
ar_SD: 'Arabic (Sudan)',
ar_SY: 'Arabic (Syria)',
ar_TN: 'Arabic (Tunisia)',
ar_AE: 'Arabic (United Arab Emirates)',
ar_EH: 'Arabic (Western Sahara)',
ar_YE: 'Arabic (Yemen)',
hy: 'Armenian',
hy_AM: 'Armenian (Armenia)',
as: 'Assamese',
as_IN: 'Assamese (India)',
az: 'Azerbaijani',
az_AZ: 'Azerbaijani (Azerbaijan)',
az_Cyrl_AZ: 'Azerbaijani (Cyrillic, Azerbaijan)',
az_Cyrl: 'Azerbaijani (Cyrillic)',
az_Latn_AZ: 'Azerbaijani (Latin, Azerbaijan)',
az_Latn: 'Azerbaijani (Latin)',
bm: 'Bambara',
bm_Latn_ML: 'Bambara (Latin, Mali)',
bm_Latn: 'Bambara (Latin)',
eu: 'Basque',
eu_ES: 'Basque (Spain)',
be: 'Belarusian',
be_BY: 'Belarusian (Belarus)',
bn: 'Bengali',
bn_BD: 'Bengali (Bangladesh)',
bn_IN: 'Bengali (India)',
bs: 'Bosnian',
bs_BA: 'Bosnian (Bosnia & Herzegovina)',
bs_Cyrl_BA: 'Bosnian (Cyrillic, Bosnia & Herzegovina)',
bs_Cyrl: 'Bosnian (Cyrillic)',
bs_Latn_BA: 'Bosnian (Latin, Bosnia & Herzegovina)',
bs_Latn: 'Bosnian (Latin)',
br: 'Breton',
br_FR: 'Breton (France)',
bg: 'Bulgarian',
bg_BG: 'Bulgarian (Bulgaria)',
my: 'Burmese',
my_MM: 'Burmese (Myanmar (Burma))',
ca: 'Catalan',
ca_AD: 'Catalan (Andorra)',
ca_FR: 'Catalan (France)',
ca_IT: 'Catalan (Italy)',
ca_ES: 'Catalan (Spain)',
zh: 'Chinese',
zh_CN: 'Chinese (China)',
zh_HK: 'Chinese (Hong Kong SAR China)',
zh_MO: 'Chinese (Macau SAR China)',
zh_Hans_CN: 'Chinese (Simplified, China)',
zh_Hans_HK: 'Chinese (Simplified, Hong Kong SAR China)',
zh_Hans_MO: 'Chinese (Simplified, Macau SAR China)',
zh_Hans_SG: 'Chinese (Simplified, Singapore)',
zh_Hans: 'Chinese (Simplified)',
zh_SG: 'Chinese (Singapore)',
zh_TW: 'Chinese (Taiwan)',
zh_Hant_HK: 'Chinese (Traditional, Hong Kong SAR China)',
zh_Hant_MO: 'Chinese (Traditional, Macau SAR China)',
zh_Hant_TW: 'Chinese (Traditional, Taiwan)',
zh_Hant: 'Chinese (Traditional)',
kw: 'Cornish',
kw_GB: 'Cornish (United Kingdom)',
hr: 'Croatian',
hr_BA: 'Croatian (Bosnia & Herzegovina)',
hr_HR: 'Croatian (Croatia)',
cs: 'Czech',
cs_CZ: 'Czech (Czech Republic)',
da: 'Danish',
da_DK: 'Danish (Denmark)',
da_GL: 'Danish (Greenland)',
nl: 'Dutch',
nl_AW: 'Dutch (Aruba)',
nl_BE: 'Dutch (Belgium)',
nl_BQ: 'Dutch (Caribbean Netherlands)',
nl_CW: 'Dutch (Curaçao)',
nl_NL: 'Dutch (Netherlands)',
nl_SX: 'Dutch (Sint Maarten)',
nl_SR: 'Dutch (Suriname)',
dz: 'Dzongkha',
dz_BT: 'Dzongkha (Bhutan)',
en: 'English',
en_AS: 'English (American Samoa)',
en_AI: 'English (Anguilla)',
en_AG: 'English (Antigua & Barbuda)',
en_AU: 'English (Australia)',
en_BS: 'English (Bahamas)',
en_BB: 'English (Barbados)',
en_BE: 'English (Belgium)',
en_BZ: 'English (Belize)',
en_BM: 'English (Bermuda)',
en_BW: 'English (Botswana)',
en_IO: 'English (British Indian Ocean Territory)',
en_VG: 'English (British Virgin Islands)',
en_CM: 'English (Cameroon)',
en_CA: 'English (Canada)',
en_KY: 'English (Cayman Islands)',
en_CX: 'English (Christmas Island)',
en_CC: 'English (Cocos (Keeling) Islands)',
en_CK: 'English (Cook Islands)',
en_DG: 'English (Diego Garcia)',
en_DM: 'English (Dominica)',
en_ER: 'English (Eritrea)',
en_FK: 'English (Falkland Islands)',
en_FJ: 'English (Fiji)',
en_GM: 'English (Gambia)',
en_GH: 'English (Ghana)',
en_GI: 'English (Gibraltar)',
en_GD: 'English (Grenada)',
en_GU: 'English (Guam)',
en_GG: 'English (Guernsey)',
en_GY: 'English (Guyana)',
en_HK: 'English (Hong Kong SAR China)',
en_IN: 'English (India)',
en_IE: 'English (Ireland)',
en_IM: 'English (Isle of Man)',
en_JM: 'English (Jamaica)',
en_JE: 'English (Jersey)',
en_KE: 'English (Kenya)',
en_KI: 'English (Kiribati)',
en_LS: 'English (Lesotho)',
en_LR: 'English (Liberia)',
en_MO: 'English (Macau SAR China)',
en_MG: 'English (Madagascar)',
en_MW: 'English (Malawi)',
en_MY: 'English (Malaysia)',
en_MT: 'English (Malta)',
en_MH: 'English (Marshall Islands)',
en_MU: 'English (Mauritius)',
en_FM: 'English (Micronesia)',
en_MS: 'English (Montserrat)',
en_NA: 'English (Namibia)',
en_NR: 'English (Nauru)',
en_NZ: 'English (New Zealand)',
en_NG: 'English (Nigeria)',
en_NU: 'English (Niue)',
en_NF: 'English (Norfolk Island)',
en_MP: 'English (Northern Mariana Islands)',
en_PK: 'English (Pakistan)',
en_PW: 'English (Palau)',
en_PG: 'English (Papua New Guinea)',
en_PH: 'English (Philippines)',
en_PN: 'English (Pitcairn Islands)',
en_PR: 'English (Puerto Rico)',
en_RW: 'English (Rwanda)',
en_WS: 'English (Samoa)',
en_SC: 'English (Seychelles)',
en_SL: 'English (Sierra Leone)',
en_SG: 'English (Singapore)',
en_SX: 'English (Sint Maarten)',
en_SB: 'English (Solomon Islands)',
en_ZA: 'English (South Africa)',
en_SS: 'English (South Sudan)',
en_SH: 'English (St. Helena)',
en_KN: 'English (St. Kitts & Nevis)',
en_LC: 'English (St. Lucia)',
en_VC: 'English (St. Vincent & Grenadines)',
en_SD: 'English (Sudan)',
en_SZ: 'English (Swaziland)',
en_TZ: 'English (Tanzania)',
en_TK: 'English (Tokelau)',
en_TO: 'English (Tonga)',
en_TT: 'English (Trinidad & Tobago)',
en_TC: 'English (Turks & Caicos Islands)',
en_TV: 'English (Tuvalu)',
en_UM: 'English (U.S. Outlying Islands)',
en_VI: 'English (U.S. Virgin Islands)',
en_UG: 'English (Uganda)',
en_GB: 'English (United Kingdom)',
en_US: 'English (United States)',
en_VU: 'English (Vanuatu)',
en_ZM: 'English (Zambia)',
en_ZW: 'English (Zimbabwe)',
eo: 'Esperanto',
et: 'Estonian',
et_EE: 'Estonian (Estonia)',
ee: 'Ewe',
ee_GH: 'Ewe (Ghana)',
ee_TG: 'Ewe (Togo)',
fo: 'Faroese',
fo_FO: 'Faroese (Faroe Islands)',
fi: 'Finnish',
fi_FI: 'Finnish (Finland)',
fr: 'French',
fr_DZ: 'French (Algeria)',
fr_BE: 'French (Belgium)',
fr_BJ: 'French (Benin)',
fr_BF: 'French (Burkina Faso)',
fr_BI: 'French (Burundi)',
fr_CM: 'French (Cameroon)',
fr_CA: 'French (Canada)',
fr_CF: 'French (Central African Republic)',
fr_TD: 'French (Chad)',
fr_KM: 'French (Comoros)',
fr_CG: 'French (Congo - Brazzaville)',
fr_CD: 'French (Congo - Kinshasa)',
fr_CI: 'French (Côte dIvoire)',
fr_DJ: 'French (Djibouti)',
fr_GQ: 'French (Equatorial Guinea)',
fr_FR: 'French (France)',
fr_GF: 'French (French Guiana)',
fr_PF: 'French (French Polynesia)',
fr_GA: 'French (Gabon)',
fr_GP: 'French (Guadeloupe)',
fr_GN: 'French (Guinea)',
fr_HT: 'French (Haiti)',
fr_LU: 'French (Luxembourg)',
fr_MG: 'French (Madagascar)',
fr_ML: 'French (Mali)',
fr_MQ: 'French (Martinique)',
fr_MR: 'French (Mauritania)',
fr_MU: 'French (Mauritius)',
fr_YT: 'French (Mayotte)',
fr_MC: 'French (Monaco)',
fr_MA: 'French (Morocco)',
fr_NC: 'French (New Caledonia)',
fr_NE: 'French (Niger)',
fr_RE: 'French (Réunion)',
fr_RW: 'French (Rwanda)',
fr_SN: 'French (Senegal)',
fr_SC: 'French (Seychelles)',
fr_BL: 'French (St. Barthélemy)',
fr_MF: 'French (St. Martin)',
fr_PM: 'French (St. Pierre & Miquelon)',
fr_CH: 'French (Switzerland)',
fr_SY: 'French (Syria)',
fr_TG: 'French (Togo)',
fr_TN: 'French (Tunisia)',
fr_VU: 'French (Vanuatu)',
fr_WF: 'French (Wallis & Futuna)',
ff: 'Fulah',
ff_CM: 'Fulah (Cameroon)',
ff_GN: 'Fulah (Guinea)',
ff_MR: 'Fulah (Mauritania)',
ff_SN: 'Fulah (Senegal)',
gl: 'Galician',
gl_ES: 'Galician (Spain)',
lg: 'Ganda',
lg_UG: 'Ganda (Uganda)',
ka: 'Georgian',
ka_GE: 'Georgian (Georgia)',
de: 'German',
de_AT: 'German (Austria)',
de_BE: 'German (Belgium)',
de_DE: 'German (Germany)',
de_LI: 'German (Liechtenstein)',
de_LU: 'German (Luxembourg)',
de_CH: 'German (Switzerland)',
el: 'Greek',
el_CY: 'Greek (Cyprus)',
el_GR: 'Greek (Greece)',
gu: 'Gujarati',
gu_IN: 'Gujarati (India)',
ha: 'Hausa',
ha_GH: 'Hausa (Ghana)',
ha_Latn_GH: 'Hausa (Latin, Ghana)',
ha_Latn_NE: 'Hausa (Latin, Niger)',
ha_Latn_NG: 'Hausa (Latin, Nigeria)',
ha_Latn: 'Hausa (Latin)',
ha_NE: 'Hausa (Niger)',
ha_NG: 'Hausa (Nigeria)',
he: 'Hebrew',
he_IL: 'Hebrew (Israel)',
hi: 'Hindi',
hi_IN: 'Hindi (India)',
hu: 'Hungarian',
hu_HU: 'Hungarian (Hungary)',
is: 'Icelandic',
is_IS: 'Icelandic (Iceland)',
ig: 'Igbo',
ig_NG: 'Igbo (Nigeria)',
id: 'Indonesian',
id_ID: 'Indonesian (Indonesia)',
ga: 'Irish',
ga_IE: 'Irish (Ireland)',
it: 'Italian',
it_IT: 'Italian (Italy)',
it_SM: 'Italian (San Marino)',
it_CH: 'Italian (Switzerland)',
ja: 'Japanese',
ja_JP: 'Japanese (Japan)',
kl: 'Kalaallisut',
kl_GL: 'Kalaallisut (Greenland)',
kn: 'Kannada',
kn_IN: 'Kannada (India)',
ks: 'Kashmiri',
ks_Arab_IN: 'Kashmiri (Arabic, India)',
ks_Arab: 'Kashmiri (Arabic)',
ks_IN: 'Kashmiri (India)',
kk: 'Kazakh',
kk_Cyrl_KZ: 'Kazakh (Cyrillic, Kazakhstan)',
kk_Cyrl: 'Kazakh (Cyrillic)',
kk_KZ: 'Kazakh (Kazakhstan)',
km: 'Khmer',
km_KH: 'Khmer (Cambodia)',
ki: 'Kikuyu',
ki_KE: 'Kikuyu (Kenya)',
rw: 'Kinyarwanda',
rw_RW: 'Kinyarwanda (Rwanda)',
ko: 'Korean',
ko_KP: 'Korean (North Korea)',
ko_KR: 'Korean (South Korea)',
ky: 'Kyrgyz',
ky_Cyrl_KG: 'Kyrgyz (Cyrillic, Kyrgyzstan)',
ky_Cyrl: 'Kyrgyz (Cyrillic)',
ky_KG: 'Kyrgyz (Kyrgyzstan)',
lo: 'Lao',
lo_LA: 'Lao (Laos)',
lv: 'Latvian',
lv_LV: 'Latvian (Latvia)',
ln: 'Lingala',
ln_AO: 'Lingala (Angola)',
ln_CF: 'Lingala (Central African Republic)',
ln_CG: 'Lingala (Congo - Brazzaville)',
ln_CD: 'Lingala (Congo - Kinshasa)',
lt: 'Lithuanian',
lt_LT: 'Lithuanian (Lithuania)',
lu: 'Luba-Katanga',
lu_CD: 'Luba-Katanga (Congo - Kinshasa)',
lb: 'Luxembourgish',
lb_LU: 'Luxembourgish (Luxembourg)',
mk: 'Macedonian',
mk_MK: 'Macedonian (Macedonia)',
mg: 'Malagasy',
mg_MG: 'Malagasy (Madagascar)',
ms: 'Malay',
ms_BN: 'Malay (Brunei)',
ms_Latn_BN: 'Malay (Latin, Brunei)',
ms_Latn_MY: 'Malay (Latin, Malaysia)',
ms_Latn_SG: 'Malay (Latin, Singapore)',
ms_Latn: 'Malay (Latin)',
ms_MY: 'Malay (Malaysia)',
ms_SG: 'Malay (Singapore)',
ml: 'Malayalam',
ml_IN: 'Malayalam (India)',
mt: 'Maltese',
mt_MT: 'Maltese (Malta)',
gv: 'Manx',
gv_IM: 'Manx (Isle of Man)',
mr: 'Marathi',
mr_IN: 'Marathi (India)',
mn: 'Mongolian',
mn_Cyrl_MN: 'Mongolian (Cyrillic, Mongolia)',
mn_Cyrl: 'Mongolian (Cyrillic)',
mn_MN: 'Mongolian (Mongolia)',
ne: 'Nepali',
ne_IN: 'Nepali (India)',
ne_NP: 'Nepali (Nepal)',
nd: 'North Ndebele',
nd_ZW: 'North Ndebele (Zimbabwe)',
se: 'Northern Sami',
se_FI: 'Northern Sami (Finland)',
se_NO: 'Northern Sami (Norway)',
se_SE: 'Northern Sami (Sweden)',
no: 'Norwegian',
no_NO: 'Norwegian (Norway)',
nb: 'Norwegian Bokmål',
nb_NO: 'Norwegian Bokmål (Norway)',
nb_SJ: 'Norwegian Bokmål (Svalbard & Jan Mayen)',
nn: 'Norwegian Nynorsk',
nn_NO: 'Norwegian Nynorsk (Norway)',
or: 'Oriya',
or_IN: 'Oriya (India)',
om: 'Oromo',
om_ET: 'Oromo (Ethiopia)',
om_KE: 'Oromo (Kenya)',
os: 'Ossetic',
os_GE: 'Ossetic (Georgia)',
os_RU: 'Ossetic (Russia)',
ps: 'Pashto',
ps_AF: 'Pashto (Afghanistan)',
fa: 'Persian',
fa_AF: 'Persian (Afghanistan)',
fa_IR: 'Persian (Iran)',
pl: 'Polish',
pl_PL: 'Polish (Poland)',
pt: 'Portuguese',
pt_AO: 'Portuguese (Angola)',
pt_BR: 'Portuguese (Brazil)',
pt_CV: 'Portuguese (Cape Verde)',
pt_GW: 'Portuguese (Guinea-Bissau)',
pt_MO: 'Portuguese (Macau SAR China)',
pt_MZ: 'Portuguese (Mozambique)',
pt_PT: 'Portuguese (Portugal)',
pt_ST: 'Portuguese (São Tomé & Príncipe)',
pt_TL: 'Portuguese (Timor-Leste)',
pa: 'Punjabi',
pa_Arab_PK: 'Punjabi (Arabic, Pakistan)',
pa_Arab: 'Punjabi (Arabic)',
pa_Guru_IN: 'Punjabi (Gurmukhi, India)',
pa_Guru: 'Punjabi (Gurmukhi)',
pa_IN: 'Punjabi (India)',
pa_PK: 'Punjabi (Pakistan)',
qu: 'Quechua',
qu_BO: 'Quechua (Bolivia)',
qu_EC: 'Quechua (Ecuador)',
qu_PE: 'Quechua (Peru)',
ro: 'Romanian',
ro_MD: 'Romanian (Moldova)',
ro_RO: 'Romanian (Romania)',
rm: 'Romansh',
rm_CH: 'Romansh (Switzerland)',
rn: 'Rundi',
rn_BI: 'Rundi (Burundi)',
ru: 'Russian',
ru_BY: 'Russian (Belarus)',
ru_KZ: 'Russian (Kazakhstan)',
ru_KG: 'Russian (Kyrgyzstan)',
ru_MD: 'Russian (Moldova)',
ru_RU: 'Russian (Russia)',
ru_UA: 'Russian (Ukraine)',
sg: 'Sango',
sg_CF: 'Sango (Central African Republic)',
gd: 'Scottish Gaelic',
gd_GB: 'Scottish Gaelic (United Kingdom)',
sr: 'Serbian',
sr_BA: 'Serbian (Bosnia & Herzegovina)',
sr_Cyrl_BA: 'Serbian (Cyrillic, Bosnia & Herzegovina)',
sr_Cyrl_XK: 'Serbian (Cyrillic, Kosovo)',
sr_Cyrl_ME: 'Serbian (Cyrillic, Montenegro)',
sr_Cyrl_RS: 'Serbian (Cyrillic, Serbia)',
sr_Cyrl: 'Serbian (Cyrillic)',
sr_XK: 'Serbian (Kosovo)',
sr_Latn_BA: 'Serbian (Latin, Bosnia & Herzegovina)',
sr_Latn_XK: 'Serbian (Latin, Kosovo)',
sr_Latn_ME: 'Serbian (Latin, Montenegro)',
sr_Latn_RS: 'Serbian (Latin, Serbia)',
sr_Latn: 'Serbian (Latin)',
sr_ME: 'Serbian (Montenegro)',
sr_RS: 'Serbian (Serbia)',
sh: 'Serbo-Croatian',
sh_BA: 'Serbo-Croatian (Bosnia & Herzegovina)',
sn: 'Shona',
sn_ZW: 'Shona (Zimbabwe)',
ii: 'Sichuan Yi',
ii_CN: 'Sichuan Yi (China)',
si: 'Sinhala',
si_LK: 'Sinhala (Sri Lanka)',
sk: 'Slovak',
sk_SK: 'Slovak (Slovakia)',
sl: 'Slovenian',
sl_SI: 'Slovenian (Slovenia)',
so: 'Somali',
so_DJ: 'Somali (Djibouti)',
so_ET: 'Somali (Ethiopia)',
so_KE: 'Somali (Kenya)',
so_SO: 'Somali (Somalia)',
es: 'Spanish',
es_AR: 'Spanish (Argentina)',
es_BO: 'Spanish (Bolivia)',
es_IC: 'Spanish (Canary Islands)',
es_EA: 'Spanish (Ceuta & Melilla)',
es_CL: 'Spanish (Chile)',
es_CO: 'Spanish (Colombia)',
es_CR: 'Spanish (Costa Rica)',
es_CU: 'Spanish (Cuba)',
es_DO: 'Spanish (Dominican Republic)',
es_EC: 'Spanish (Ecuador)',
es_SV: 'Spanish (El Salvador)',
es_GQ: 'Spanish (Equatorial Guinea)',
es_GT: 'Spanish (Guatemala)',
es_HN: 'Spanish (Honduras)',
es_MX: 'Spanish (Mexico)',
es_NI: 'Spanish (Nicaragua)',
es_PA: 'Spanish (Panama)',
es_PY: 'Spanish (Paraguay)',
es_PE: 'Spanish (Peru)',
es_PH: 'Spanish (Philippines)',
es_PR: 'Spanish (Puerto Rico)',
es_ES: 'Spanish (Spain)',
es_US: 'Spanish (United States)',
es_UY: 'Spanish (Uruguay)',
es_VE: 'Spanish (Venezuela)',
sw: 'Swahili',
sw_KE: 'Swahili (Kenya)',
sw_TZ: 'Swahili (Tanzania)',
sw_UG: 'Swahili (Uganda)',
sv: 'Swedish',
sv_AX: 'Swedish (Åland Islands)',
sv_FI: 'Swedish (Finland)',
sv_SE: 'Swedish (Sweden)',
tl: 'Tagalog',
tl_PH: 'Tagalog (Philippines)',
ta: 'Tamil',
ta_IN: 'Tamil (India)',
ta_MY: 'Tamil (Malaysia)',
ta_SG: 'Tamil (Singapore)',
ta_LK: 'Tamil (Sri Lanka)',
te: 'Telugu',
te_IN: 'Telugu (India)',
th: 'Thai',
th_TH: 'Thai (Thailand)',
bo: 'Tibetan',
bo_CN: 'Tibetan (China)',
bo_IN: 'Tibetan (India)',
ti: 'Tigrinya',
ti_ER: 'Tigrinya (Eritrea)',
ti_ET: 'Tigrinya (Ethiopia)',
to: 'Tongan',
to_TO: 'Tongan (Tonga)',
tr: 'Turkish',
tr_CY: 'Turkish (Cyprus)',
tr_TR: 'Turkish (Turkey)',
uk: 'Ukrainian',
uk_UA: 'Ukrainian (Ukraine)',
ur: 'Urdu',
ur_IN: 'Urdu (India)',
ur_PK: 'Urdu (Pakistan)',
ug: 'Uyghur',
ug_Arab_CN: 'Uyghur (Arabic, China)',
ug_Arab: 'Uyghur (Arabic)',
ug_CN: 'Uyghur (China)',
uz: 'Uzbek',
uz_AF: 'Uzbek (Afghanistan)',
uz_Arab_AF: 'Uzbek (Arabic, Afghanistan)',
uz_Arab: 'Uzbek (Arabic)',
uz_Cyrl_UZ: 'Uzbek (Cyrillic, Uzbekistan)',
uz_Cyrl: 'Uzbek (Cyrillic)',
uz_Latn_UZ: 'Uzbek (Latin, Uzbekistan)',
uz_Latn: 'Uzbek (Latin)',
uz_UZ: 'Uzbek (Uzbekistan)',
vi: 'Vietnamese',
vi_VN: 'Vietnamese (Vietnam)',
cy: 'Welsh',
cy_GB: 'Welsh (United Kingdom)',
fy: 'Western Frisian',
fy_NL: 'Western Frisian (Netherlands)',
yi: 'Yiddish',
yo: 'Yoruba',
yo_BJ: 'Yoruba (Benin)',
yo_NG: 'Yoruba (Nigeria)',
zu: 'Zulu',
zu_ZA: 'Zulu (South Africa)',
};
export default locales;

View file

@ -1,5 +1,4 @@
json.id article.id json.id article.id
json.category_id article.category_id
json.title article.title json.title article.title
json.content article.content json.content article.content
json.description article.description json.description article.description
@ -7,6 +6,11 @@ json.status article.status
json.account_id article.account_id json.account_id article.account_id
json.updated_at article.updated_at.to_i json.updated_at article.updated_at.to_i
json.category do
json.id article.category_id
json.name article.category.name
end
if article.portal.present? if article.portal.present?
json.portal do json.portal do
json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal json.partial! 'api/v1/accounts/portals/portal.json.jbuilder', portal: article.portal