mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-25 15:34:55 +00:00
feat(v4): Update the help center portal design (#10296)
Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
@@ -15,7 +15,6 @@ const Suspended = () => import('./suspended/Index.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
...helpcenterRoutes.routes,
|
||||
{
|
||||
path: frontendURL('accounts/:accountId'),
|
||||
component: AppContainer,
|
||||
@@ -35,6 +34,7 @@ export default {
|
||||
...contactRoutes,
|
||||
...searchRoutes,
|
||||
...notificationRoutes,
|
||||
...helpcenterRoutes.routes,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
<script>
|
||||
import Modal from 'dashboard/components/Modal.vue';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: ['cancel', 'update:show'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedLocale: '',
|
||||
isUpdating: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
addedLocales() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.map(locale => locale.code);
|
||||
},
|
||||
locales() {
|
||||
const addedLocales = this.portal.config.allowed_locales.map(
|
||||
locale => locale.code
|
||||
);
|
||||
return Object.keys(allLocales)
|
||||
.map(key => {
|
||||
return {
|
||||
id: key,
|
||||
name: allLocales[key],
|
||||
code: key,
|
||||
};
|
||||
})
|
||||
.filter(locale => {
|
||||
return !addedLocales.includes(locale.code);
|
||||
});
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
selectedLocale: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onCreate() {
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
const updatedLocales = this.addedLocales;
|
||||
updatedLocales.push(this.selectedLocale);
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
await this.$store.dispatch('portals/update', {
|
||||
portalSlug: this.portal.slug,
|
||||
config: { allowed_locales: updatedLocales },
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD_LOCALE.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.onClose();
|
||||
useTrack(PORTALS_EVENTS.CREATE_LOCALE, {
|
||||
localeAdded: this.selectedLocale,
|
||||
totalLocales: updatedLocales.length,
|
||||
from: this.$route.name,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD_LOCALE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
this.isUpdating = false;
|
||||
}
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal v-model:show="localShow" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.PORTAL.ADD_LOCALE.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.PORTAL.ADD_LOCALE.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<label :class="{ error: v$.selectedLocale.$error }">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.LABEL') }}
|
||||
<select v-model="selectedLocale">
|
||||
<option
|
||||
v-for="locale in locales"
|
||||
:key="locale.name"
|
||||
:value="locale.id"
|
||||
>
|
||||
{{ locale.name }}-{{ locale.code }}
|
||||
</option>
|
||||
</select>
|
||||
<span v-if="v$.selectedLocale.$error" class="message">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.LOCALE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD_LOCALE.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-container::v-deep {
|
||||
margin: 0 0 var(--space-normal);
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, defineEmits } from 'vue';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
||||
import FullEditor from 'dashboard/components/widgets/WootWriter/FullEditor.vue';
|
||||
import { ARTICLE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||
|
||||
const { article } = defineProps({
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['saveArticle']);
|
||||
|
||||
const saveArticle = debounce(value => emit('saveArticle', value), 400, false);
|
||||
|
||||
const articleTitle = computed({
|
||||
get: () => article.title,
|
||||
set: title => {
|
||||
saveArticle({ title });
|
||||
},
|
||||
});
|
||||
|
||||
const articleContent = computed({
|
||||
get: () => article.content,
|
||||
set: content => {
|
||||
saveArticle({ content });
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="edit-article--container">
|
||||
<ResizableTextArea
|
||||
v-model="articleTitle"
|
||||
type="text"
|
||||
:rows="1"
|
||||
class="article-heading"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.TITLE_PLACEHOLDER')"
|
||||
/>
|
||||
<FullEditor
|
||||
v-model="articleContent"
|
||||
class="article-content"
|
||||
:placeholder="$t('HELP_CENTER.EDIT_ARTICLE.CONTENT_PLACEHOLDER')"
|
||||
:enabled-menu-options="ARTICLE_EDITOR_MENU_OPTIONS"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-article--container {
|
||||
@apply my-8 mx-auto py-0 max-w-[56rem] w-full;
|
||||
}
|
||||
|
||||
.article-heading {
|
||||
@apply text-[2.5rem] font-semibold leading-normal w-full text-slate-900 dark:text-slate-75 p-4 hover:bg-slate-25 dark:hover:bg-slate-800 hover:rounded-md resize-none min-h-[4rem] max-h-[40rem] h-auto mb-2 border-0 border-solid border-transparent dark:border-transparent;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
@apply py-0 px-4 h-fit;
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.ProseMirror-menubar-wrapper {
|
||||
.ProseMirror-woot-style {
|
||||
@apply min-h-[15rem] max-h-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,167 +0,0 @@
|
||||
<script>
|
||||
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
showDragIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
author: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
category: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
views: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: 'draft',
|
||||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
updatedAt: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
lastUpdatedAt() {
|
||||
return dynamicTime(this.updatedAt);
|
||||
},
|
||||
formattedViewCount() {
|
||||
return Number(this.views || 0).toLocaleString('en');
|
||||
},
|
||||
readableViewCount() {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
compactDisplay: 'short',
|
||||
}).format(this.views || 0);
|
||||
},
|
||||
articleAuthorName() {
|
||||
return this.author?.name || '-';
|
||||
},
|
||||
labelColor() {
|
||||
switch (this.status) {
|
||||
case 'archived':
|
||||
return 'secondary';
|
||||
case 'draft':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getCategoryRoute(categorySlug) {
|
||||
const { portalSlug, locale } = this.$route.params;
|
||||
return frontendURL(
|
||||
`accounts/${this.accountId}/portals/${portalSlug}/${locale}/categories/${categorySlug}`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-1 gap-4 px-6 py-3 my-0 -mx-4 bg-white border-b text-slate-700 dark:text-slate-100 last:border-b-0 dark:bg-slate-900 lg:grid-cols-12 border-slate-50 dark:border-slate-800"
|
||||
>
|
||||
<span class="flex items-start col-span-6 gap-2 text-left">
|
||||
<fluent-icon
|
||||
v-if="showDragIcon"
|
||||
size="20"
|
||||
class="flex-shrink-0 block w-4 h-4 mt-1 cursor-move text-slate-200 dark:text-slate-700 hover:text-slate-400 hover:dark:text-slate-200"
|
||||
icon="grab-handle"
|
||||
/>
|
||||
<div class="flex flex-col truncate">
|
||||
<router-link :to="articleUrl(id)">
|
||||
<h6
|
||||
:title="title"
|
||||
class="text-base ltr:text-left rtl:text-right text-slate-800 dark:text-slate-100 mb-0.5 leading-6 font-medium hover:underline overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ title }}
|
||||
</h6>
|
||||
</router-link>
|
||||
<div class="flex items-center gap-1">
|
||||
<Thumbnail
|
||||
v-if="author"
|
||||
:src="author.thumbnail"
|
||||
:username="author.name"
|
||||
size="14px"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
v-tooltip.right="
|
||||
$t('HELP_CENTER.TABLE.COLUMNS.AUTHOR_NOT_AVAILABLE')
|
||||
"
|
||||
class="flex items-center justify-center rounded w-3.5 h-3.5 bg-woot-100 dark:bg-woot-700"
|
||||
>
|
||||
<fluent-icon
|
||||
icon="person"
|
||||
type="filled"
|
||||
size="10"
|
||||
class="text-woot-300 dark:text-woot-300"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-sm font-normal text-slate-700 dark:text-slate-200">
|
||||
{{ articleAuthorName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span class="flex items-center col-span-2">
|
||||
<router-link
|
||||
class="text-sm hover:underline p-0.5 truncate hover:bg-slate-25 hover:rounded-md"
|
||||
:to="getCategoryRoute(category.slug)"
|
||||
>
|
||||
<span :title="category.name">
|
||||
{{ category.name }}
|
||||
</span>
|
||||
</router-link>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center text-xs lg:text-sm"
|
||||
:title="formattedViewCount"
|
||||
>
|
||||
{{ readableViewCount }}
|
||||
<span class="ml-1 lg:hidden">
|
||||
{{ ` ${$t('HELP_CENTER.TABLE.HEADERS.READ_COUNT')}` }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="flex items-center capitalize">
|
||||
<woot-label
|
||||
class="!mb-0"
|
||||
:title="status"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
:color-scheme="labelColor"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center justify-end col-span-2 text-xs first-letter:uppercase text-slate-700 dark:text-slate-100"
|
||||
>
|
||||
{{ lastUpdatedAt }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,13 @@
|
||||
<script>
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import SearchHeader from './Header.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import ArticleView from './ArticleView.vue';
|
||||
import ArticlesAPI from 'dashboard/api/helpCenter/articles';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
|
||||
export default {
|
||||
name: 'ArticleSearchPopover',
|
||||
@@ -16,7 +16,6 @@ export default {
|
||||
SearchResults,
|
||||
ArticleView,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
@@ -69,6 +68,9 @@ export default {
|
||||
article.slug
|
||||
);
|
||||
},
|
||||
localeName(code) {
|
||||
return allLocales[code];
|
||||
},
|
||||
activeArticle(id) {
|
||||
return this.searchResultsWithUrl.find(article => article.id === id);
|
||||
},
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
<script>
|
||||
import ArticleItem from './ArticleItem.vue';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter.vue';
|
||||
import Draggable from 'vuedraggable';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArticleItem,
|
||||
TableFooter,
|
||||
Draggable,
|
||||
},
|
||||
props: {
|
||||
articles: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
totalCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 25,
|
||||
},
|
||||
},
|
||||
emits: ['reorder', 'pageChange'],
|
||||
data() {
|
||||
return {
|
||||
localArticles: this.articles || [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dragEnabled() {
|
||||
// dragging allowed only on category page
|
||||
return this.articles.length > 1 && this.onCategoryPage;
|
||||
},
|
||||
onCategoryPage() {
|
||||
return this.$route.name === 'show_category';
|
||||
},
|
||||
showArticleFooter() {
|
||||
return this.currentPage === 1
|
||||
? this.totalCount > 25
|
||||
: this.articles.length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
articles() {
|
||||
this.localArticles = [...this.articles];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDragEnd() {
|
||||
// why reuse the same positons array, instead of creating a new one?
|
||||
// this ensures that the shuffling happens within the same group
|
||||
// itself and does not create any new positions and avoid conflict with existing articles
|
||||
// so if a user sorts on page number 2, and the positions are say [550, 560, 570, 580, 590]
|
||||
// the new sorted items will be in the same position range as well
|
||||
const sortedArticlePositions = this.localArticles
|
||||
.map(article => article.position)
|
||||
.sort((a, b) => {
|
||||
// Why sort like this? Glad you asked!
|
||||
// because JavaScript is the doom of my existence, and if a `compareFn` is not supplied,
|
||||
// all non-undefined array elements are sorted by converting them to strings
|
||||
// and comparing strings in UTF-16 code units order.
|
||||
//
|
||||
// so an array [20, 10000, 10, 30, 40] will be sorted as [10, 10000, 20, 30, 40]
|
||||
|
||||
return a - b;
|
||||
});
|
||||
|
||||
const orderedArticles = this.localArticles.map(article => article.id);
|
||||
|
||||
const reorderedGroup = orderedArticles.reduce((obj, key, index) => {
|
||||
obj[key] = sortedArticlePositions[index];
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
this.$emit('reorder', reorderedGroup);
|
||||
},
|
||||
onPageChange(page) {
|
||||
this.$emit('pageChange', page);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="sticky z-10 content-center hidden h-12 grid-cols-12 gap-4 px-6 py-0 bg-white border-b lg:grid border-slate-50 dark:border-slate-700 top-16 dark:bg-slate-900"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
>
|
||||
<div
|
||||
class="col-span-6 px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.TITLE') }}
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.CATEGORY') }}
|
||||
</div>
|
||||
<div
|
||||
class="hidden px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right lg:block"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.READ_COUNT') }}
|
||||
</div>
|
||||
<div
|
||||
class="px-0 py-2 text-sm font-semibold text-left capitalize text-slate-700 dark:text-slate-100 rtl:text-right"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.STATUS') }}
|
||||
</div>
|
||||
<div
|
||||
class="hidden col-span-2 px-0 py-2 text-sm font-semibold text-right capitalize text-slate-700 dark:text-slate-100 rtl:text-left md:block"
|
||||
>
|
||||
{{ $t('HELP_CENTER.TABLE.HEADERS.LAST_EDITED') }}
|
||||
</div>
|
||||
</div>
|
||||
<Draggable
|
||||
tag="div"
|
||||
class="px-4 pb-4 border-t-0"
|
||||
:disabled="!dragEnabled"
|
||||
:list="localArticles"
|
||||
ghost-class="article-ghost-class"
|
||||
item-key="id"
|
||||
@start="dragging = true"
|
||||
@end="onDragEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<ArticleItem
|
||||
:id="element.id"
|
||||
:key="element.id"
|
||||
:class="{ draggable: onCategoryPage }"
|
||||
:title="element.title"
|
||||
:author="element.author"
|
||||
:show-drag-icon="dragEnabled"
|
||||
:category="element.category"
|
||||
:views="element.views"
|
||||
:status="element.status"
|
||||
:updated-at="element.updated_at"
|
||||
/>
|
||||
</template>
|
||||
</Draggable>
|
||||
|
||||
<TableFooter
|
||||
v-if="showArticleFooter"
|
||||
:current-page="currentPage"
|
||||
:total-count="totalCount"
|
||||
:page-size="pageSize"
|
||||
class="bottom-0 border-t dark:bg-slate-900 border-slate-75 dark:border-slate-700/50"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/*
|
||||
The .article-ghost-class class is maintained as the vueDraggable doesn't allow multiple classes
|
||||
to be passed in the ghost-class prop.
|
||||
*/
|
||||
.article-ghost-class {
|
||||
@apply opacity-50 bg-slate-50 dark:bg-slate-800;
|
||||
}
|
||||
</style>
|
||||
@@ -1,237 +0,0 @@
|
||||
<script>
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
|
||||
|
||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||
export default {
|
||||
components: {
|
||||
FluentIcon,
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
MultiselectDropdownItems,
|
||||
},
|
||||
props: {
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
selectedValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
selectedLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
shouldShowSettings: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
allLocales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['openModal', 'open', 'close', 'newArticlePage', 'changeLocale'],
|
||||
data() {
|
||||
return {
|
||||
showSortByDropdown: false,
|
||||
showLocaleDropdown: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
shouldShowLocaleDropdown() {
|
||||
return this.allLocales.length > 1;
|
||||
},
|
||||
switchableLocales() {
|
||||
return this.allLocales.filter(
|
||||
locale => locale.name !== this.selectedLocale
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFilterModal() {
|
||||
this.$emit('openModal');
|
||||
},
|
||||
openDropdown() {
|
||||
this.$emit('open');
|
||||
this.showSortByDropdown = true;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.$emit('close');
|
||||
this.showSortByDropdown = false;
|
||||
},
|
||||
openLocaleDropdown() {
|
||||
this.showLocaleDropdown = true;
|
||||
},
|
||||
closeLocaleDropdown() {
|
||||
this.showLocaleDropdown = false;
|
||||
},
|
||||
onClickNewArticlePage() {
|
||||
this.$emit('newArticlePage');
|
||||
},
|
||||
onClickSelectItem(value) {
|
||||
const { name, code } = value;
|
||||
this.closeLocaleDropdown();
|
||||
if (!name || name === this.selectedLocale) {
|
||||
return;
|
||||
}
|
||||
this.$emit('changeLocale', code);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="sticky top-0 z-50 flex items-center justify-between w-full h-16 p-6 bg-white dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<div class="flex items-center mx-2 my-0">
|
||||
<h3 class="mb-0 text-xl font-medium text-slate-800 dark:text-slate-100">
|
||||
{{ headerTitle }}
|
||||
</h3>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300 mx-2 mt-0.5">{{
|
||||
`(${count})`
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="filter"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="openFilterModal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
icon="arrow-sort"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openDropdown"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.SORT') }}
|
||||
<span
|
||||
class="inline-flex items-center ml-1 rtl:ml-0 rtl:mr-1 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedValue }}
|
||||
<FluentIcon class="dropdown-arrow" icon="chevron-down" size="14" />
|
||||
</span>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showSortByDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="send-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.PUBLISHED') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="dual-screen-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.DRAFT') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
<WootDropdownItem>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="calendar-clock"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.DROPDOWN_OPTIONS.ARCHIVED') }}
|
||||
</woot-button>
|
||||
</WootDropdownItem>
|
||||
</WootDropdownMenu>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="shouldShowSettings"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.HEADER.SETTINGS_BUTTON')"
|
||||
icon="settings"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
/>
|
||||
<div class="relative">
|
||||
<woot-button
|
||||
v-if="shouldShowLocaleDropdown"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
variant="hollow"
|
||||
@click="openLocaleDropdown"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full min-w-0">
|
||||
<span
|
||||
class="inline-flex items-center ml-1 rtl:ml-0 rtl:mr-1 text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ selectedLocale }}
|
||||
<FluentIcon
|
||||
class="dropdown-arrow"
|
||||
icon="chevron-down"
|
||||
size="14"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</woot-button>
|
||||
<div
|
||||
v-if="showLocaleDropdown"
|
||||
v-on-clickaway="closeLocaleDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<MultiselectDropdownItems
|
||||
:options="switchableLocales"
|
||||
:has-thumbnail="false"
|
||||
:selected-items="[selectedLocale]"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.HEADER.LOCALE_SELECT.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="$t('HELP_CENTER.HEADER.LOCALE_SELECT.NO_RESULT')"
|
||||
@select="onClickSelectItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="add"
|
||||
color-scheme="primary"
|
||||
@click="onClickNewArticlePage"
|
||||
>
|
||||
{{ $t('HELP_CENTER.HEADER.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dropdown-pane--open {
|
||||
@apply absolute top-10 right-0 z-50 min-w-[8rem];
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
@apply ml-1 rtl:ml-0 rtl:mr-1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,252 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
const { ARTICLE_STATUS_TYPES } = wootConstants;
|
||||
|
||||
export default {
|
||||
props: {
|
||||
isSidebarOpen: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
backButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSaved: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableOpenSidebarButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['back', 'show', 'add', 'updateMeta', 'open', 'close'],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
alertMessage: '',
|
||||
ARTICLE_STATUS_TYPES,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statusText() {
|
||||
return this.isUpdating
|
||||
? this.$t('HELP_CENTER.EDIT_HEADER.SAVING')
|
||||
: this.$t('HELP_CENTER.EDIT_HEADER.SAVED');
|
||||
},
|
||||
articleSlug() {
|
||||
return this.$route.params.articleSlug;
|
||||
},
|
||||
currentPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
currentArticleStatus() {
|
||||
return this.$store.getters['articles/articleStatus'](this.articleSlug);
|
||||
},
|
||||
isPublishedArticle() {
|
||||
return this.currentArticleStatus === 'published';
|
||||
},
|
||||
isArchivedArticle() {
|
||||
return this.currentArticleStatus === 'archived';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickGoBack() {
|
||||
this.$emit('back');
|
||||
},
|
||||
showPreview() {
|
||||
this.$emit('show');
|
||||
},
|
||||
onClickAdd() {
|
||||
this.$emit('add');
|
||||
},
|
||||
async updateArticleStatus(status) {
|
||||
try {
|
||||
await this.$store.dispatch('articles/update', {
|
||||
portalSlug: this.currentPortalSlug,
|
||||
articleId: this.articleSlug,
|
||||
status: status,
|
||||
});
|
||||
this.$emit('updateMeta');
|
||||
this.statusUpdateSuccessMessage(status);
|
||||
this.closeActionsDropdown();
|
||||
if (status === this.ARTICLE_STATUS_TYPES.ARCHIVE) {
|
||||
useTrack(PORTALS_EVENTS.ARCHIVE_ARTICLE, { uiFrom: 'header' });
|
||||
} else if (status === this.ARTICLE_STATUS_TYPES.PUBLISH) {
|
||||
useTrack(PORTALS_EVENTS.PUBLISH_ARTICLE);
|
||||
}
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message || this.statusUpdateErrorMessage(status);
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
statusUpdateSuccessMessage(status) {
|
||||
if (status === this.ARTICLE_STATUS_TYPES.PUBLISH) {
|
||||
this.alertMessage = this.$t('HELP_CENTER.PUBLISH_ARTICLE.API.SUCCESS');
|
||||
} else if (status === this.ARTICLE_STATUS_TYPES.ARCHIVE) {
|
||||
this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.SUCCESS');
|
||||
}
|
||||
},
|
||||
statusUpdateErrorMessage(status) {
|
||||
if (status === this.ARTICLE_STATUS_TYPES.PUBLISH) {
|
||||
this.alertMessage = this.$t('HELP_CENTER.PUBLISH_ARTICLE.API.ERROR');
|
||||
} else if (status === this.ARTICLE_STATUS_TYPES.ARCHIVE) {
|
||||
this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR');
|
||||
}
|
||||
},
|
||||
openSidebar() {
|
||||
this.$emit('open');
|
||||
},
|
||||
closeSidebar() {
|
||||
this.$emit('close');
|
||||
},
|
||||
openActionsDropdown() {
|
||||
this.showActionsDropdown = !this.showActionsDropdown;
|
||||
},
|
||||
closeActionsDropdown() {
|
||||
this.showActionsDropdown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between w-full h-16">
|
||||
<div class="flex items-center">
|
||||
<woot-button
|
||||
icon="chevron-left"
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
class="back-button"
|
||||
@click="onClickGoBack"
|
||||
>
|
||||
{{ backButtonLabel }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="isUpdating || isSaved"
|
||||
class="items-center ml-4 mr-1 text-xs draft-status rtl:ml-2 rtl:mr-4 text-slate-400 dark:text-slate-300"
|
||||
>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
|
||||
<woot-button
|
||||
class-names="article--buttons relative"
|
||||
icon="globe"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="showPreview"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PREVIEW') }}
|
||||
</woot-button>
|
||||
<!-- Hidden since this is in V2
|
||||
<woot-button
|
||||
v-if="shouldShowAddLocaleButton"
|
||||
class-names="article--buttons relative"
|
||||
icon="add"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="onClickAdd"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.ADD_TRANSLATION') }}
|
||||
</woot-button> -->
|
||||
<woot-button
|
||||
v-if="!isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.OPEN_SIDEBAR')"
|
||||
icon="pane-open"
|
||||
class-names="article--buttons relative sidebar-button"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
:is-disabled="enableOpenSidebarButton"
|
||||
@click="openSidebar"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="isSidebarOpen"
|
||||
v-tooltip.top-end="$t('HELP_CENTER.EDIT_HEADER.CLOSE_SIDEBAR')"
|
||||
icon="pane-close"
|
||||
class-names="article--buttons relative"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="closeSidebar"
|
||||
/>
|
||||
<div class="relative article--buttons">
|
||||
<div class="button-group">
|
||||
<woot-button
|
||||
class-names="publish-button"
|
||||
size="small"
|
||||
icon="checkmark"
|
||||
color-scheme="primary"
|
||||
:is-disabled="!articleSlug || isPublishedArticle"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.PUBLISH)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.PUBLISH_BUTTON') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
size="small"
|
||||
icon="chevron-down"
|
||||
:is-disabled="!articleSlug || isArchivedArticle"
|
||||
@click="openActionsDropdown"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeActionsDropdown"
|
||||
class="dropdown-pane dropdown-pane--open"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="book-clock"
|
||||
@click="updateArticleStatus(ARTICLE_STATUS_TYPES.ARCHIVE)"
|
||||
>
|
||||
{{ $t('HELP_CENTER.EDIT_HEADER.MOVE_TO_ARCHIVE_BUTTON') }}
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.article--buttons {
|
||||
.dropdown-pane {
|
||||
@apply absolute right-0;
|
||||
}
|
||||
}
|
||||
|
||||
.draft-status {
|
||||
animation: fadeIn 1s;
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,364 +0,0 @@
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import UpgradePage from './UpgradePage.vue';
|
||||
import NextSidebar from 'next/sidebar/Sidebar.vue';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import Sidebar from 'dashboard/components/layout/Sidebar.vue';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import PortalPopover from '../components/PortalPopover.vue';
|
||||
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
|
||||
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
|
||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
import AddCategory from '../pages/categories/AddCategory.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
const CommandBar = defineAsyncComponent(
|
||||
() => import('dashboard/routes/dashboard/commands/commandbar.vue')
|
||||
);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NextSidebar,
|
||||
AccountSelector,
|
||||
AddCategory,
|
||||
CommandBar,
|
||||
HelpCenterSidebar,
|
||||
NotificationPanel,
|
||||
PortalPopover,
|
||||
Sidebar,
|
||||
UpgradePage,
|
||||
WootKeyShortcutModal,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOnDesktop: true,
|
||||
showShortcutModal: false,
|
||||
showNotificationPanel: false,
|
||||
showPortalPopover: false,
|
||||
showAddCategoryModal: false,
|
||||
lastActivePortalSlug: '',
|
||||
showAccountModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
portals: 'portals/allPortals',
|
||||
categories: 'categories/allCategories',
|
||||
meta: 'portals/getMeta',
|
||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||
}),
|
||||
|
||||
isHelpCenterEnabled() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.HELP_CENTER
|
||||
);
|
||||
},
|
||||
showNextSidebar() {
|
||||
return this.isFeatureEnabledonAccount(
|
||||
this.accountId,
|
||||
FEATURE_FLAGS.CHATWOOT_V4
|
||||
);
|
||||
},
|
||||
isSidebarOpen() {
|
||||
const { show_help_center_secondary_sidebar: showSecondarySidebar } =
|
||||
this.uiSettings;
|
||||
return showSecondarySidebar;
|
||||
},
|
||||
showHelpCenterSidebar() {
|
||||
if (!this.isHelpCenterEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.portals.length === 0 ? false : this.isSidebarOpen;
|
||||
},
|
||||
selectedPortal() {
|
||||
const slug = this.$route.params.portalSlug || this.lastActivePortalSlug;
|
||||
if (slug) return this.$store.getters['portals/portalBySlug'](slug);
|
||||
|
||||
return this.$store.getters['portals/allPortals'][0];
|
||||
},
|
||||
selectedLocaleInPortal() {
|
||||
return this.$route.params.locale || this.defaultPortalLocale;
|
||||
},
|
||||
selectedPortalName() {
|
||||
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.selectedPortal ? this.selectedPortal?.slug : '';
|
||||
},
|
||||
defaultPortalLocale() {
|
||||
return this.selectedPortal
|
||||
? this.selectedPortal?.meta?.default_locale
|
||||
: '';
|
||||
},
|
||||
accessibleMenuItems() {
|
||||
if (!this.selectedPortal) return [];
|
||||
|
||||
const {
|
||||
allArticlesCount,
|
||||
mineArticlesCount,
|
||||
draftArticlesCount,
|
||||
archivedArticlesCount,
|
||||
} = this.meta;
|
||||
|
||||
return [
|
||||
{
|
||||
icon: 'book',
|
||||
label: 'HELP_CENTER.ALL_ARTICLES',
|
||||
key: 'list_all_locale_articles',
|
||||
count: allArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles`
|
||||
),
|
||||
toolTip: 'All Articles',
|
||||
toStateName: 'list_all_locale_articles',
|
||||
},
|
||||
{
|
||||
icon: 'pen',
|
||||
label: 'HELP_CENTER.MY_ARTICLES',
|
||||
key: 'list_mine_articles',
|
||||
count: mineArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/mine`
|
||||
),
|
||||
toolTip: 'My articles',
|
||||
toStateName: 'list_mine_articles',
|
||||
},
|
||||
{
|
||||
icon: 'draft',
|
||||
label: 'HELP_CENTER.DRAFT',
|
||||
key: 'list_draft_articles',
|
||||
count: draftArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/draft`
|
||||
),
|
||||
toolTip: 'Draft',
|
||||
toStateName: 'list_draft_articles',
|
||||
},
|
||||
{
|
||||
icon: 'archive',
|
||||
label: 'HELP_CENTER.ARCHIVED',
|
||||
key: 'list_archived_articles',
|
||||
count: archivedArticlesCount,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${this.selectedLocaleInPortal}/articles/archived`
|
||||
),
|
||||
toolTip: 'Archived',
|
||||
toStateName: 'list_archived_articles',
|
||||
},
|
||||
{
|
||||
icon: 'settings',
|
||||
label: 'HELP_CENTER.SETTINGS',
|
||||
key: 'edit_portal_information',
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/edit`
|
||||
),
|
||||
toStateName: 'edit_portal_information',
|
||||
},
|
||||
];
|
||||
},
|
||||
additionalSecondaryMenuItems() {
|
||||
if (!this.selectedPortal) return [];
|
||||
return [
|
||||
{
|
||||
icon: 'folder',
|
||||
label: 'HELP_CENTER.CATEGORY',
|
||||
hasSubMenu: true,
|
||||
showNewButton: true,
|
||||
key: 'category',
|
||||
children: this.categories.map(category => ({
|
||||
id: category.id,
|
||||
label: category.icon
|
||||
? `${category.icon} ${category.name}`
|
||||
: category.name,
|
||||
count: category.meta.articles_count,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.selectedPortalSlug}/${category.locale}/categories/${category.slug}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
];
|
||||
},
|
||||
currentRoute() {
|
||||
return ' ';
|
||||
},
|
||||
headerTitle() {
|
||||
return this.selectedPortal ? this.selectedPortal.name : '';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$route.name'() {
|
||||
const routeName = this.$route?.name;
|
||||
const routeParams = this.$route?.params;
|
||||
const updateMetaInAllPortals = routeName === 'list_all_portals';
|
||||
const updateMetaInEditArticle =
|
||||
routeName === 'edit_article' && routeParams?.recentlyCreated;
|
||||
const updateMetaInLocaleArticles =
|
||||
routeName === 'list_all_locale_articles' &&
|
||||
routeParams?.recentlyDeleted;
|
||||
if (
|
||||
updateMetaInAllPortals ||
|
||||
updateMetaInEditArticle ||
|
||||
updateMetaInLocaleArticles
|
||||
) {
|
||||
this.fetchPortalAndItsCategories();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
emitter.on(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) this.lastActivePortalSlug = slug;
|
||||
|
||||
this.fetchPortalAndItsCategories();
|
||||
},
|
||||
unmounted() {
|
||||
emitter.off(BUS_EVENTS.TOGGLE_SIDEMENU, this.toggleSidebar);
|
||||
},
|
||||
updated() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug !== this.lastActivePortalSlug) {
|
||||
this.lastActivePortalSlug = slug;
|
||||
this.updateUISettings({
|
||||
last_active_portal_slug: slug,
|
||||
last_active_locale_code: this.selectedLocaleInPortal,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSidebar() {
|
||||
if (this.portals.length > 0) {
|
||||
this.updateUISettings({
|
||||
show_help_center_secondary_sidebar: !this.isSidebarOpen,
|
||||
});
|
||||
}
|
||||
},
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.selectedLocaleInPortal,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
toggleKeyShortcutModal() {
|
||||
this.showShortcutModal = true;
|
||||
},
|
||||
closeKeyShortcutModal() {
|
||||
this.showShortcutModal = false;
|
||||
},
|
||||
openNotificationPanel() {
|
||||
this.showNotificationPanel = true;
|
||||
},
|
||||
closeNotificationPanel() {
|
||||
this.showNotificationPanel = false;
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.showPortalPopover = !this.showPortalPopover;
|
||||
},
|
||||
closePortalPopover() {
|
||||
this.showPortalPopover = false;
|
||||
},
|
||||
onClickOpenAddCategoryModal() {
|
||||
this.showAddCategoryModal = true;
|
||||
},
|
||||
onClickCloseAddCategoryModal() {
|
||||
this.showAddCategoryModal = false;
|
||||
},
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<NextSidebar
|
||||
v-if="showNextSidebar"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
/>
|
||||
<Sidebar
|
||||
v-else
|
||||
:route="currentRoute"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-notification-panel="openNotificationPanel"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
/>
|
||||
<HelpCenterSidebar
|
||||
v-if="showHelpCenterSidebar"
|
||||
:header-title="headerTitle"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
:locale-slug="selectedLocaleInPortal"
|
||||
:sub-title="localeName(selectedLocaleInPortal)"
|
||||
:accessible-menu-items="accessibleMenuItems"
|
||||
:additional-secondary-menu-items="additionalSecondaryMenuItems"
|
||||
@open-popover="openPortalPopover"
|
||||
@open-modal="onClickOpenAddCategoryModal"
|
||||
/>
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex flex-1 h-full px-0 overflow-hidden bg-white dark:bg-slate-900"
|
||||
>
|
||||
<router-view @reload-locale="fetchPortalAndItsCategories" />
|
||||
<CommandBar />
|
||||
<AccountSelector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
/>
|
||||
<WootKeyShortcutModal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<NotificationPanel
|
||||
v-if="showNotificationPanel"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<PortalPopover
|
||||
v-if="showPortalPopover"
|
||||
:portals="portals"
|
||||
:active-portal-slug="selectedPortalSlug"
|
||||
:active-locale="selectedLocaleInPortal"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
@close-popover="closePortalPopover"
|
||||
/>
|
||||
<AddCategory
|
||||
v-if="showAddCategoryModal"
|
||||
v-model:show="showAddCategoryModal"
|
||||
:portal-name="selectedPortalName"
|
||||
:locale="selectedLocaleInPortal"
|
||||
:portal-slug="selectedPortalSlug"
|
||||
@cancel="onClickCloseAddCategoryModal"
|
||||
/>
|
||||
</section>
|
||||
<UpgradePage v-else />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,34 +0,0 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent md:max-w-2xl"
|
||||
>
|
||||
<h3
|
||||
v-if="$slots.title || title"
|
||||
class="text-lg text-black-900 dark:text-slate-200"
|
||||
>
|
||||
<slot name="title">{{ title }}</slot>
|
||||
</h3>
|
||||
<div
|
||||
class="mx-0 my-4 border-b border-solid border-slate-25 dark:border-slate-800"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<div>
|
||||
<slot name="footer-left" />
|
||||
</div>
|
||||
<div>
|
||||
<slot name="footer-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,379 +0,0 @@
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import LocaleItemTable from './PortalListItemTable.vue';
|
||||
import { PORTALS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail: thumbnail,
|
||||
LocaleItemTable,
|
||||
},
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
default: '',
|
||||
values: ['archived', 'draft', 'published'],
|
||||
},
|
||||
},
|
||||
emits: ['addLocale', 'openSite'],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showDeleteConfirmationPopup: false,
|
||||
alertMessage: '',
|
||||
selectedPortalForDelete: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
labelColor() {
|
||||
switch (this.status) {
|
||||
case 'Archived':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'success';
|
||||
}
|
||||
},
|
||||
deleteMessageValue() {
|
||||
return ` ${this.selectedPortalForDelete.name}?`;
|
||||
},
|
||||
locales() {
|
||||
return this.portal ? this.portal.config.allowed_locales : [];
|
||||
},
|
||||
allowedLocales() {
|
||||
return Object.keys(this.locales).map(key => {
|
||||
return this.locales[key].code;
|
||||
});
|
||||
},
|
||||
articleCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addLocale() {
|
||||
this.$emit('addLocale', this.portal.id);
|
||||
},
|
||||
openSite() {
|
||||
this.$emit('openSite', this.portal.slug);
|
||||
},
|
||||
openSettings() {
|
||||
this.fetchPortalAndItsCategories();
|
||||
this.navigateToPortalEdit();
|
||||
},
|
||||
onClickOpenDeleteModal(portal) {
|
||||
this.selectedPortalForDelete = portal;
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
async fetchPortalAndItsCategories() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
const {
|
||||
slug,
|
||||
config: { allowed_locales: allowedLocales },
|
||||
} = this.portal;
|
||||
const selectedPortalParam = {
|
||||
portalSlug: slug,
|
||||
locale: allowedLocales[0].code,
|
||||
};
|
||||
this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
this.$store.dispatch('categories/index', selectedPortalParam);
|
||||
},
|
||||
async onClickDeletePortal() {
|
||||
const { slug } = this.selectedPortalForDelete;
|
||||
try {
|
||||
await this.$store.dispatch('portals/delete', {
|
||||
portalSlug: slug,
|
||||
});
|
||||
this.selectedPortalForDelete = {};
|
||||
this.closeDeletePopup();
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS'
|
||||
);
|
||||
this.updateUISettings({
|
||||
last_active_portal_slug: undefined,
|
||||
last_active_locale_code: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR'
|
||||
);
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
changeDefaultLocale({ localeCode }) {
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: this.allowedLocales,
|
||||
defaultLocale: localeCode,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.CHANGE_DEFAULT_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
useTrack(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
|
||||
newLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
deletePortalLocale({ localeCode }) {
|
||||
const updatedLocales = this.allowedLocales.filter(
|
||||
code => code !== localeCode
|
||||
);
|
||||
const defaultLocale = this.portal.meta.default_locale;
|
||||
this.updatePortalLocales({
|
||||
allowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
successMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.SUCCESS_MESSAGE'
|
||||
),
|
||||
errorMessage: this.$t(
|
||||
'HELP_CENTER.PORTAL.DELETE_LOCALE.API.ERROR_MESSAGE'
|
||||
),
|
||||
});
|
||||
useTrack(PORTALS_EVENTS.DELETE_LOCALE, {
|
||||
deletedLocale: localeCode,
|
||||
from: this.$route.name,
|
||||
});
|
||||
},
|
||||
async updatePortalLocales({
|
||||
allowedLocales,
|
||||
defaultLocale,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
}) {
|
||||
try {
|
||||
await this.$store.dispatch('portals/update', {
|
||||
portalSlug: this.portal.slug,
|
||||
config: {
|
||||
default_locale: defaultLocale,
|
||||
allowed_locales: allowedLocales,
|
||||
},
|
||||
});
|
||||
this.alertMessage = successMessage;
|
||||
} catch (error) {
|
||||
this.alertMessage = error?.message || errorMessage;
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
navigateToPortalEdit() {
|
||||
this.$router.push({
|
||||
name: 'edit_portal_information',
|
||||
params: { portalSlug: this.portal.slug },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="relative flex p-4 mb-3 bg-white border border-solid rounded-md dark:bg-slate-900 border-slate-100 dark:border-slate-600"
|
||||
>
|
||||
<Thumbnail :username="portal.name" variant="square" />
|
||||
<div class="flex-grow ml-2 rtl:ml-0 rtl:mr-2">
|
||||
<header class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="mb-0 text-lg text-slate-800 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h2>
|
||||
<woot-label
|
||||
:title="status"
|
||||
:color-scheme="labelColor"
|
||||
size="small"
|
||||
variant="smooth"
|
||||
class="mx-2 my-0"
|
||||
/>
|
||||
</div>
|
||||
<p class="mb-0 text-sm text-slate-700 dark:text-slate-200">
|
||||
{{ articleCount }}
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.COUNT_LABEL'
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-row gap-1">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
@click="addLocale"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
@click="openSite"
|
||||
>
|
||||
{{
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.VISIT')
|
||||
}}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.SETTINGS'
|
||||
)
|
||||
"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
icon="settings"
|
||||
color-scheme="secondary"
|
||||
@click="openSettings"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.DELETE')
|
||||
"
|
||||
variant="hollow"
|
||||
color-scheme="alert"
|
||||
size="small"
|
||||
icon="delete"
|
||||
@click="onClickOpenDeleteModal(portal)"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="mb-2 text-base font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<div
|
||||
class="flex justify-between mr-[6.25rem] rtl:mr-0 rtl:ml-[6.25rem] max-w-[80vw]"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.NAME'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.DOMAIN'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.custom_domain }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SLUG'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.slug }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.TITLE'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.page_title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.THEME'
|
||||
)
|
||||
}}</label>
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class="w-4 h-4 mr-1 border border-solid rounded-md rtl:mr-0 rtl:ml-1 border-slate-25 dark:border-slate-800"
|
||||
:style="{ background: portal.color }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-start mb-4">
|
||||
<label>{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.PORTAL_CONFIG.ITEMS.SUB_TEXT'
|
||||
)
|
||||
}}</label>
|
||||
<span class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ portal.header_text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-12">
|
||||
<h2
|
||||
class="mb-2 text-base font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TITLE'
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<LocaleItemTable
|
||||
:locales="locales"
|
||||
:selected-locale-code="portal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<woot-delete-modal
|
||||
v-model:show="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="onClickDeletePortal"
|
||||
:title="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.TITLE')"
|
||||
:message="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.MESSAGE')"
|
||||
:message-value="deleteMessageValue"
|
||||
:confirm-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.YES')"
|
||||
:reject-text="$t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,147 +0,0 @@
|
||||
<script>
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
export default {
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
locales: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedLocaleCode: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['changeDefaultLocale', 'delete'],
|
||||
|
||||
methods: {
|
||||
changeDefaultLocale(localeCode) {
|
||||
this.$emit('changeDefaultLocale', { localeCode });
|
||||
},
|
||||
deleteLocale(localeCode) {
|
||||
this.$emit('delete', { localeCode });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table class="woot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.NAME'
|
||||
)
|
||||
}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.CODE'
|
||||
)
|
||||
}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.ARTICLE_COUNT'
|
||||
)
|
||||
}}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.CATEGORIES'
|
||||
)
|
||||
}}
|
||||
</th>
|
||||
<th scope="col" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td colspan="100%" class="horizontal-line" />
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr v-for="locale in locales" :key="locale.code">
|
||||
<td>
|
||||
<span>{{ localeName(locale.code) }}</span>
|
||||
<woot-label
|
||||
v-if="locale.code === selectedLocaleCode"
|
||||
:title="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.DEFAULT_LOCALE'
|
||||
)
|
||||
"
|
||||
color-scheme="warning"
|
||||
small
|
||||
variant="smooth"
|
||||
class="default-status"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ locale.code }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ locale.articles_count }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ locale.categories_count }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.SWAP'
|
||||
)
|
||||
"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
icon="arrow-swap"
|
||||
color-scheme="primary"
|
||||
:disabled="locale.code === selectedLocaleCode"
|
||||
@click="changeDefaultLocale(locale.code)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.AVAILABLE_LOCALES.TABLE.DELETE'
|
||||
)
|
||||
"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
icon="delete"
|
||||
color-scheme="alert"
|
||||
:disabled="locale.code === selectedLocaleCode"
|
||||
@click="deleteLocale(locale.code)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
thead tr th {
|
||||
@apply text-sm font-medium normal-case text-slate-600 dark:text-slate-200 pl-0 rtl:pl-2.5 rtl:pr-0 pt-0;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
@apply border-b-0;
|
||||
td {
|
||||
@apply text-sm pl-0 rtl:pl-2.5 rtl:pr-0;
|
||||
.default-status {
|
||||
@apply py-0 pr-0 pl-1;
|
||||
}
|
||||
span {
|
||||
@apply text-slate-700 dark:text-slate-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.horizontal-line {
|
||||
@apply border-b border-solid border-slate-75 dark:border-slate-700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script>
|
||||
import PortalSwitch from './PortalSwitch.vue';
|
||||
export default {
|
||||
components: {
|
||||
PortalSwitch,
|
||||
},
|
||||
props: {
|
||||
portals: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['closePopover', 'fetchPortal'],
|
||||
|
||||
methods: {
|
||||
closePortalPopover() {
|
||||
this.$emit('closePopover');
|
||||
},
|
||||
openPortalPage() {
|
||||
this.closePortalPopover();
|
||||
this.$router.push({
|
||||
name: 'list_all_portals',
|
||||
});
|
||||
},
|
||||
fetchPortalAndItsCategories() {
|
||||
this.$emit('fetchPortal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-on-clickaway="closePortalPopover"
|
||||
class="absolute overflow-y-scroll max-h-[96vh] p-4 bg-white dark:bg-slate-800 rounded-md shadow-lg max-w-[30rem] z-[1000]"
|
||||
>
|
||||
<header>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg text-slate-800 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.TITLE') }}
|
||||
</h2>
|
||||
<div>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="settings"
|
||||
size="small"
|
||||
@click="openPortalPage"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
size="small"
|
||||
@click="closePortalPopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-slate-600 dark:text-slate-300">
|
||||
{{ $t('HELP_CENTER.PORTAL.POPOVER.SUBTITLE') }}
|
||||
</p>
|
||||
</header>
|
||||
<div>
|
||||
<PortalSwitch
|
||||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:active-portal-slug="activePortalSlug"
|
||||
:active-locale="activeLocale"
|
||||
:active="portal.slug === activePortalSlug"
|
||||
@open-portal-page="closePortalPopover"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,240 +0,0 @@
|
||||
<script setup>
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
|
||||
import { defineOptions, reactive, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { hasValidAvatarUrl } from 'dashboard/helper/URLHelper';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { isDomain } from 'shared/helpers/Validators';
|
||||
import SettingsLayout from './Layout/SettingsLayout.vue';
|
||||
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
submitButtonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['submit', 'deleteLogo']);
|
||||
|
||||
defineOptions({
|
||||
name: 'PortalSettingsBasicForm',
|
||||
});
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const state = reactive({
|
||||
name: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
logoUrl: '',
|
||||
avatarBlobId: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
slug: {
|
||||
required,
|
||||
},
|
||||
domain: {
|
||||
isDomain,
|
||||
},
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const nameError = computed(() => {
|
||||
if (v$.value.name.$error) {
|
||||
return t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const slugError = computed(() => {
|
||||
if (v$.value.slug.$error) {
|
||||
return t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const domainError = computed(() => {
|
||||
if (v$.value.domain.$error) {
|
||||
return t('HELP_CENTER.PORTAL.ADD.DOMAIN.ERROR');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const domainHelpText = computed(() => {
|
||||
return buildPortalURL(state.slug);
|
||||
});
|
||||
|
||||
const domainExampleHelpText = computed(() => {
|
||||
return t('HELP_CENTER.PORTAL.ADD.DOMAIN.HELP_TEXT', {
|
||||
exampleURL: EXAMPLE_URL,
|
||||
});
|
||||
});
|
||||
|
||||
const showDeleteButton = computed(() => {
|
||||
return hasValidAvatarUrl(state.logoUrl);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const portal = props.portal || {};
|
||||
state.name = portal.name || '';
|
||||
state.slug = portal.slug || '';
|
||||
state.domain = portal.custom_domain || '';
|
||||
|
||||
if (portal.logo) {
|
||||
const {
|
||||
logo: { file_url: logoURL, blob_id: blobId },
|
||||
} = portal;
|
||||
state.logoUrl = logoURL;
|
||||
state.avatarBlobId = blobId;
|
||||
}
|
||||
});
|
||||
|
||||
function onNameChange() {
|
||||
state.slug = convertToCategorySlug(state.name);
|
||||
}
|
||||
|
||||
function onSubmitClick() {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const portal = {
|
||||
name: state.name,
|
||||
slug: state.slug,
|
||||
custom_domain: state.domain,
|
||||
blob_id: state.avatarBlobId || null,
|
||||
};
|
||||
emit('submit', portal);
|
||||
}
|
||||
async function deleteAvatar() {
|
||||
state.logoUrl = '';
|
||||
state.avatarBlobId = '';
|
||||
emit('deleteLogo');
|
||||
}
|
||||
|
||||
async function uploadLogoToStorage(file) {
|
||||
try {
|
||||
const { fileUrl, blobId } = await uploadFile(file);
|
||||
if (fileUrl) {
|
||||
state.logoUrl = fileUrl;
|
||||
state.avatarBlobId = blobId;
|
||||
}
|
||||
} catch (error) {
|
||||
useAlert(t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_UPLOAD_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
function onFileChange({ file }) {
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
uploadLogoToStorage(file);
|
||||
} else {
|
||||
const errorKey =
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR';
|
||||
useAlert(t(errorKey, { size: MAXIMUM_FILE_UPLOAD_SIZE }));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:title="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.TITLE')
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<div class="flex flex-row items-center">
|
||||
<woot-avatar-uploader
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.LOGO.LABEL')"
|
||||
:src="state.logoUrl"
|
||||
@on-avatar-select="onFileChange"
|
||||
/>
|
||||
<div v-if="showDeleteButton" class="avatar-delete-btn">
|
||||
<woot-button
|
||||
type="button"
|
||||
color-scheme="alert"
|
||||
variant="hollow"
|
||||
size="small"
|
||||
@click="deleteAvatar"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
class="mt-1 mb-0 text-xs not-italic text-slate-600 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.LOGO.HELP_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.name"
|
||||
:class="{ error: v$.name.$error }"
|
||||
:error="nameError"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.NAME.HELP_TEXT')"
|
||||
@blur="v$.name.$touch"
|
||||
@update:model-value="onNameChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.slug"
|
||||
:class="{ error: v$.slug.$error }"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.SLUG.PLACEHOLDER')"
|
||||
:help-text="domainHelpText"
|
||||
@blur="v$.slug.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.domain"
|
||||
:class="{ error: v$.domain.$error }"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.DOMAIN.PLACEHOLDER')"
|
||||
:help-text="domainExampleHelpText"
|
||||
:error="domainError"
|
||||
@blur="v$.domain.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer-right>
|
||||
<woot-button
|
||||
:is-loading="isSubmitting"
|
||||
:is-disabled="v$.$invalid"
|
||||
@click="onSubmitClick"
|
||||
>
|
||||
{{ submitButtonText }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
@@ -1,155 +0,0 @@
|
||||
<script setup>
|
||||
import { getRandomColor } from 'dashboard/helper/labelColor';
|
||||
import SettingsLayout from './Layout/SettingsLayout.vue';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { url } from '@vuelidate/validators';
|
||||
|
||||
import { defineOptions, reactive, computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const props = defineProps({
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
defineOptions({
|
||||
name: 'PortalSettingsCustomizationForm',
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { EXAMPLE_URL } = wootConstants;
|
||||
|
||||
const state = reactive({
|
||||
color: getRandomColor(),
|
||||
pageTitle: '',
|
||||
headerText: '',
|
||||
homePageLink: '',
|
||||
});
|
||||
|
||||
const rules = {
|
||||
homePageLink: { url },
|
||||
};
|
||||
|
||||
const homepageExampleHelpText = computed(() => {
|
||||
return t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.HELP_TEXT', {
|
||||
exampleURL: EXAMPLE_URL,
|
||||
});
|
||||
});
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
function updateDataFromStore() {
|
||||
const { portal } = props;
|
||||
if (portal) {
|
||||
state.color = portal.color || getRandomColor();
|
||||
state.pageTitle = portal.page_title || '';
|
||||
state.headerText = portal.header_text || '';
|
||||
state.homePageLink = portal.homepage_link || '';
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmitClick() {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const portal = {
|
||||
id: props.portal.id,
|
||||
slug: props.portal.slug,
|
||||
color: state.color,
|
||||
page_title: state.pageTitle,
|
||||
header_text: state.headerText,
|
||||
homepage_link: state.homePageLink,
|
||||
};
|
||||
|
||||
emit('submit', portal);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateDataFromStore();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsLayout
|
||||
:title="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.TITLE')
|
||||
"
|
||||
>
|
||||
<div class="flex-grow-0 flex-shrink-0">
|
||||
<div class="mb-4">
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.LABEL') }}
|
||||
</label>
|
||||
<woot-color-picker v-model="state.color" />
|
||||
<p
|
||||
class="mt-1 mb-0 text-xs not-italic text-slate-600 dark:text-slate-400"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.THEME_COLOR.HELP_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.pageTitle"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.PAGE_TITLE.HELP_TEXT')"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.headerText"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.PORTAL.ADD.HEADER_TEXT.HELP_TEXT')"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<woot-input
|
||||
v-model="state.homePageLink"
|
||||
:label="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.PLACEHOLDER')"
|
||||
:help-text="homepageExampleHelpText"
|
||||
:error="
|
||||
v$.homePageLink.$error
|
||||
? $t('HELP_CENTER.PORTAL.ADD.HOME_PAGE_LINK.ERROR')
|
||||
: ''
|
||||
"
|
||||
:class="{ error: v$.homePageLink.$error }"
|
||||
@blur="v$.homePageLink.$touch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer-right>
|
||||
<woot-button
|
||||
:is-loading="isSubmitting"
|
||||
:is-disabled="v$.$invalid"
|
||||
@click="onSubmitClick"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.UPDATE_PORTAL_BUTTON'
|
||||
)
|
||||
}}
|
||||
</woot-button>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.colorpicker--selected {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,175 +0,0 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import portalMixin from '../mixins/portalMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
props: {
|
||||
portal: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activePortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeLocale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['fetchPortal', 'openPortalPage'],
|
||||
data() {
|
||||
return {
|
||||
selectedLocale: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locales() {
|
||||
return this.portal?.config?.allowed_locales;
|
||||
},
|
||||
articlesCount() {
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.reduce((acc, locale) => {
|
||||
return acc + locale.articles_count;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.selectedLocale = this.locale || this.portal?.meta?.default_locale;
|
||||
},
|
||||
methods: {
|
||||
onClick(event, code, portal) {
|
||||
event.preventDefault();
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: portal.slug,
|
||||
locale: code,
|
||||
},
|
||||
});
|
||||
this.$emit('fetchPortal');
|
||||
this.$emit('openPortalPage');
|
||||
},
|
||||
isLocaleActive(code, slug) {
|
||||
const isPortalActive = this.portal.slug === slug;
|
||||
const isLocaleActive = this.activeLocale === code;
|
||||
return isPortalActive && isLocaleActive;
|
||||
},
|
||||
isLocaleDefault(code) {
|
||||
return this.portal?.meta?.default_locale === code;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="portal" :class="{ active }">
|
||||
<Thumbnail :username="portal.name" variant="square" />
|
||||
<div class="actions-container">
|
||||
<header class="flex items-center justify-between mb-2.5">
|
||||
<div>
|
||||
<h3 class="text-sm mb-0.5 text-slate-700 dark:text-slate-100">
|
||||
{{ portal.name }}
|
||||
</h3>
|
||||
<p class="mb-0 text-xs text-slate-600 dark:text-slate-200">
|
||||
{{ articlesCount }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }}
|
||||
</p>
|
||||
</div>
|
||||
<woot-label
|
||||
v-if="active"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="success"
|
||||
:title="$t('HELP_CENTER.PORTAL.ACTIVE_BADGE')"
|
||||
/>
|
||||
</header>
|
||||
<div class="portal-locales">
|
||||
<h5 class="text-base text-slate-700 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.PORTAL.CHOOSE_LOCALE_LABEL') }}
|
||||
</h5>
|
||||
<ul>
|
||||
<li v-for="locale in locales" :key="locale.code">
|
||||
<woot-button
|
||||
:variant="`locale-item ${
|
||||
isLocaleActive(locale.code, activePortalSlug)
|
||||
? 'smooth'
|
||||
: 'clear'
|
||||
}`"
|
||||
size="large"
|
||||
color-scheme="secondary"
|
||||
@click="event => onClick(event, locale.code, portal)"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="meta">
|
||||
<h6 class="text-sm text-left mb-0.5">
|
||||
<span class="text-slate-700 dark:text-slate-100">
|
||||
{{ localeName(locale.code) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="isLocaleDefault(locale.code)"
|
||||
class="text-sm text-slate-300 dark:text-slate-200"
|
||||
>
|
||||
{{ `(${$t('HELP_CENTER.PORTAL.DEFAULT')})` }}
|
||||
</span>
|
||||
</h6>
|
||||
|
||||
<span
|
||||
class="flex w-full text-sm leading-4 text-left text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ locale.articles_count }}
|
||||
{{ $t('HELP_CENTER.PORTAL.ARTICLES_LABEL') }} -
|
||||
{{ locale.code }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="isLocaleActive(locale.code, activePortalSlug)">
|
||||
<fluent-icon icon="checkmark" class="locale__radio" />
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.portal {
|
||||
@apply bg-white dark:bg-slate-800 rounded-md p-4 relative flex mb-4 border border-solid border-slate-100 dark:border-slate-600;
|
||||
|
||||
&.active {
|
||||
@apply bg-white dark:bg-slate-800 border border-solid border-woot-400 dark:border-woot-500;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
@apply ml-2.5 rtl:ml-0 rtl:mr-2.5 flex-grow;
|
||||
|
||||
.portal-locales {
|
||||
ul {
|
||||
@apply list-none p-0 m-0;
|
||||
}
|
||||
|
||||
.locale__radio {
|
||||
@apply w-8 text-green-600 dark:text-green-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.locale-item {
|
||||
@apply flex items-start py-1 px-4 rounded-md w-full mb-2;
|
||||
|
||||
p {
|
||||
@apply mb-0 text-left;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
<script>
|
||||
import SecondaryNavItem from 'dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue';
|
||||
import SidebarHeader from './SidebarHeader.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryNavItem,
|
||||
SidebarHeader,
|
||||
},
|
||||
props: {
|
||||
thumbnailSrc: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
localeSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
accessibleMenuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
additionalSecondaryMenuItems: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['openPopover', 'openModal'],
|
||||
computed: {
|
||||
hasCategory() {
|
||||
return (
|
||||
this.additionalSecondaryMenuItems[0] &&
|
||||
this.additionalSecondaryMenuItems[0].children.length > 0
|
||||
);
|
||||
},
|
||||
portalLink() {
|
||||
return `/hc/${this.portalSlug}/${this.localeSlug}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openPortalPopover() {
|
||||
this.$emit('openPopover');
|
||||
},
|
||||
onClickOpenAddCatogoryModal() {
|
||||
this.$emit('openModal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col h-full overflow-auto text-sm bg-white border-r w-60 dark:bg-slate-900 dark:border-slate-700 rtl:border-r-0 rtl:border-l border-slate-50"
|
||||
>
|
||||
<SidebarHeader
|
||||
:thumbnail-src="thumbnailSrc"
|
||||
:header-title="headerTitle"
|
||||
:sub-title="subTitle"
|
||||
:portal-link="portalLink"
|
||||
class="px-4"
|
||||
@open-popover="openPortalPopover"
|
||||
/>
|
||||
<transition-group name="menu-list" tag="ul" class="p-2 mb-0 ml-0 list-none">
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
:key="menuItem.toState"
|
||||
:menu-item="menuItem"
|
||||
/>
|
||||
<SecondaryNavItem
|
||||
v-for="menuItem in additionalSecondaryMenuItems"
|
||||
:key="menuItem.key"
|
||||
:menu-item="menuItem"
|
||||
@open="onClickOpenAddCatogoryModal"
|
||||
/>
|
||||
<p
|
||||
v-if="!hasCategory"
|
||||
key="empty-category-nessage"
|
||||
class="p-1.5 px-4 text-slate-300"
|
||||
>
|
||||
{{ $t('SIDEBAR.HELP_CENTER.CATEGORY_EMPTY_MESSAGE') }}
|
||||
</p>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
thumbnailSrc: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalLink: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['openPopover'],
|
||||
methods: {
|
||||
popoutHelpCenter() {
|
||||
window.open(this.portalLink, '_blank');
|
||||
},
|
||||
openPortalPopover() {
|
||||
this.$emit('openPopover');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between h-16 px-0 py-4 border-b mb-1/4 border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<Thumbnail
|
||||
size="32px"
|
||||
:src="thumbnailSrc"
|
||||
:username="headerTitle"
|
||||
variant="square"
|
||||
/>
|
||||
<div class="flex flex-col items-start ml-2 rtl:ml-0 rtl:mr-2">
|
||||
<h4
|
||||
class="h-4 mb-0 overflow-hidden text-sm leading-4 w-28 whitespace-nowrap text-ellipsis text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ headerTitle }}
|
||||
</h4>
|
||||
<span class="h-4 text-xs leading-4 text-slate-600 dark:text-slate-200">
|
||||
{{ subTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="small"
|
||||
icon="arrow-up-right"
|
||||
@click="popoutHelpCenter"
|
||||
/>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="arrow-swap"
|
||||
@click="openPortalPopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,226 +1,113 @@
|
||||
import HelpCenterLayout from './components/HelpCenterLayout.vue';
|
||||
import { getPortalRoute } from './helpers/routeHelper';
|
||||
|
||||
const ListAllPortals = () => import('./pages/portals/ListAllPortals.vue');
|
||||
const NewPortal = () => import('./pages/portals/NewPortal.vue');
|
||||
import HelpCenterPageRouteView from './pages/HelpCenterPageRouteView.vue';
|
||||
|
||||
const EditPortal = () => import('./pages/portals/EditPortal.vue');
|
||||
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic.vue');
|
||||
const EditPortalCustomization = () =>
|
||||
import('./pages/portals/EditPortalCustomization.vue');
|
||||
const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue');
|
||||
const ShowPortal = () => import('./pages/portals/ShowPortal.vue');
|
||||
const PortalDetails = () => import('./pages/portals/PortalDetails.vue');
|
||||
const PortalCustomization = () =>
|
||||
import('./pages/portals/PortalCustomization.vue');
|
||||
const PortalSettingsFinish = () =>
|
||||
import('./pages/portals/PortalSettingsFinish.vue');
|
||||
const PortalsIndex = () => import('./pages/PortalsIndexPage.vue');
|
||||
const PortalsNew = () => import('./pages/PortalsNewPage.vue');
|
||||
|
||||
const ListAllCategories = () =>
|
||||
import('./pages/categories/ListAllCategories.vue');
|
||||
const NewCategory = () => import('./pages/categories/NewCategory.vue');
|
||||
const EditCategory = () => import('./pages/categories/EditCategory.vue');
|
||||
const ListCategoryArticles = () =>
|
||||
import('./pages/articles/ListCategoryArticles.vue');
|
||||
const ListAllArticles = () => import('./pages/articles/ListAllArticles.vue');
|
||||
const DefaultPortalArticles = () =>
|
||||
import('./pages/articles/DefaultPortalArticles.vue');
|
||||
const NewArticle = () => import('./pages/articles/NewArticle.vue');
|
||||
const EditArticle = () => import('./pages/articles/EditArticle.vue');
|
||||
const PortalsArticlesIndexPage = () =>
|
||||
import('./pages/PortalsArticlesIndexPage.vue');
|
||||
const PortalsArticlesNewPage = () =>
|
||||
import('./pages/PortalsArticlesNewPage.vue');
|
||||
const PortalsArticlesEditPage = () =>
|
||||
import('./pages/PortalsArticlesEditPage.vue');
|
||||
|
||||
const PortalsCategoriesIndexPage = () =>
|
||||
import('./pages/PortalsCategoriesIndexPage.vue');
|
||||
|
||||
const PortalsLocalesIndexPage = () =>
|
||||
import('./pages/PortalsLocalesIndexPage.vue');
|
||||
|
||||
const PortalsSettingsIndexPage = () =>
|
||||
import('./pages/PortalsSettingsIndexPage.vue');
|
||||
|
||||
const portalRoutes = [
|
||||
{
|
||||
path: getPortalRoute(''),
|
||||
name: 'default_portal_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
component: DefaultPortalArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('all'),
|
||||
name: 'list_all_portals',
|
||||
path: getPortalRoute(':portalSlug/:locale/:categorySlug?/articles/:tab?'),
|
||||
name: 'portals_articles_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllPortals,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('new'),
|
||||
component: NewPortal,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'new_portal_information',
|
||||
component: PortalDetails,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/customization',
|
||||
name: 'portal_customization',
|
||||
component: PortalCustomization,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':portalSlug/finish',
|
||||
name: 'portal_finish',
|
||||
component: PortalSettingsFinish,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug'),
|
||||
name: 'portalSlug',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ShowPortal,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/edit'),
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditPortal,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'edit_portal_information',
|
||||
component: EditPortalBasic,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'customizations',
|
||||
name: 'edit_portal_customization',
|
||||
component: EditPortalCustomization,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'locales',
|
||||
name: 'edit_portal_locales',
|
||||
component: EditPortalLocales,
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
name: 'list_all_locale_categories',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const articleRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles'),
|
||||
name: 'list_all_locale_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
component: PortalsArticlesIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
||||
name: 'new_article',
|
||||
name: 'portals_articles_new',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: NewArticle,
|
||||
component: PortalsArticlesNewPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/mine'),
|
||||
name: 'list_mine_articles',
|
||||
path: getPortalRoute(
|
||||
':portalSlug/:locale/:categorySlug?/articles/:tab?/edit/:articleSlug'
|
||||
),
|
||||
name: 'portals_articles_edit',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/archived'),
|
||||
name: 'list_archived_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
component: PortalsArticlesEditPage,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/draft'),
|
||||
name: 'list_draft_articles',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
},
|
||||
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
|
||||
name: 'edit_article',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditArticle,
|
||||
},
|
||||
];
|
||||
|
||||
const categoryRoutes = [
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories'),
|
||||
name: 'all_locale_categories',
|
||||
name: 'portals_categories_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllCategories,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/new'),
|
||||
name: 'new_category_in_locale',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: NewCategory,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'show_category',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListAllArticles,
|
||||
component: PortalsCategoriesIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(
|
||||
':portalSlug/:locale/categories/:categorySlug/articles'
|
||||
),
|
||||
name: 'show_category_articles',
|
||||
name: 'portals_categories_articles_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: ListCategoryArticles,
|
||||
component: PortalsArticlesIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
|
||||
name: 'edit_category',
|
||||
path: getPortalRoute(
|
||||
':portalSlug/:locale/categories/:categorySlug/articles/:articleSlug'
|
||||
),
|
||||
name: 'portals_categories_articles_edit',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: EditCategory,
|
||||
component: PortalsArticlesEditPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/locales'),
|
||||
name: 'portals_locales_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: PortalsLocalesIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':portalSlug/settings'),
|
||||
name: 'portals_settings_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||
},
|
||||
component: PortalsSettingsIndexPage,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute('new'),
|
||||
name: 'portals_new',
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
component: PortalsNew,
|
||||
},
|
||||
{
|
||||
path: getPortalRoute(':navigationPath'),
|
||||
name: 'portals_index',
|
||||
meta: {
|
||||
permissions: ['administrator', 'knowledge_base_manage'],
|
||||
},
|
||||
component: PortalsIndex,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -228,8 +115,8 @@ export default {
|
||||
routes: [
|
||||
{
|
||||
path: getPortalRoute(),
|
||||
component: HelpCenterLayout,
|
||||
children: [...portalRoutes, ...articleRoutes, ...categoryRoutes],
|
||||
component: HelpCenterPageRouteView,
|
||||
children: [...portalRoutes],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
|
||||
export const getPortalRoute = (path = '') => {
|
||||
const slugToBeAdded = path ? `/${path}` : '';
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({ accountId: 'getCurrentAccountId' }),
|
||||
portalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
locale() {
|
||||
return this.$route.params.locale;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
articleUrl(id) {
|
||||
return frontendURL(
|
||||
`accounts/${this.accountId}/portals/${this.portalSlug}/${this.locale}/articles/${id}`
|
||||
);
|
||||
},
|
||||
localeName(code) {
|
||||
return allLocales[code];
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createStore } from 'vuex';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import portalMixin from '../portalMixin';
|
||||
import ListAllArticles from '../../pages/portals/ListAllPortals.vue';
|
||||
|
||||
// Create router instance
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/:portalSlug/:locale/articles', // Add leading "/"
|
||||
name: 'list_all_locale_articles',
|
||||
component: ListAllArticles,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('portalMixin', () => {
|
||||
let getters;
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
getters = {
|
||||
getCurrentAccountId: () => 1,
|
||||
};
|
||||
const Component = {
|
||||
render() {},
|
||||
title: 'TestComponent',
|
||||
mixins: [portalMixin],
|
||||
};
|
||||
store = createStore({ getters });
|
||||
wrapper = shallowMount(Component, {
|
||||
global: {
|
||||
plugins: [store, router],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns account id', () => {
|
||||
expect(wrapper.vm.accountId).toBe(1);
|
||||
});
|
||||
|
||||
it('returns article url', async () => {
|
||||
await router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: { portalSlug: 'fur-rent', locale: 'en' },
|
||||
});
|
||||
expect(wrapper.vm.articleUrl(1)).toBe(
|
||||
'/app/accounts/1/portals/fur-rent/en/articles/1'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns portal locale', async () => {
|
||||
await router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: { portalSlug: 'fur-rent', locale: 'es' },
|
||||
});
|
||||
expect(wrapper.vm.portalSlug).toBe('fur-rent');
|
||||
});
|
||||
|
||||
it('returns portal slug', async () => {
|
||||
await router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: { portalSlug: 'campaign', locale: 'es' },
|
||||
});
|
||||
expect(wrapper.vm.portalSlug).toBe('campaign');
|
||||
});
|
||||
|
||||
it('returns locale name', async () => {
|
||||
await router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: { portalSlug: 'fur-rent', locale: 'es' },
|
||||
});
|
||||
expect(wrapper.vm.localeName('es')).toBe('Spanish');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import UpgradePage from '../components/UpgradePage.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
const accountId = computed(() => store.getters.getCurrentAccountId);
|
||||
const portals = computed(() => store.getters['portals/allPortals']);
|
||||
const isFeatureEnabledonAccount = (id, flag) =>
|
||||
store.getters['accounts/isFeatureEnabledonAccount'](id, flag);
|
||||
|
||||
const isHelpCenterEnabled = computed(() =>
|
||||
isFeatureEnabledonAccount(accountId.value, FEATURE_FLAGS.HELP_CENTER)
|
||||
);
|
||||
|
||||
const selectedPortal = computed(() => {
|
||||
const slug =
|
||||
route.params.portalSlug || uiSettings.value.last_active_portal_slug;
|
||||
if (slug) return store.getters['portals/portalBySlug'](slug);
|
||||
return portals.value[0];
|
||||
});
|
||||
|
||||
const defaultPortalLocale = computed(() =>
|
||||
selectedPortal.value ? selectedPortal.value.meta?.default_locale : ''
|
||||
);
|
||||
const selectedLocaleInPortal = computed(
|
||||
() => route.params.locale || defaultPortalLocale.value
|
||||
);
|
||||
|
||||
const selectedPortalSlug = computed(() =>
|
||||
selectedPortal.value ? selectedPortal.value.slug : ''
|
||||
);
|
||||
|
||||
const fetchPortalAndItsCategories = async () => {
|
||||
await store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: selectedLocaleInPortal.value,
|
||||
};
|
||||
store.dispatch('portals/show', selectedPortalParam);
|
||||
store.dispatch('categories/index', selectedPortalParam);
|
||||
store.dispatch('agents/get');
|
||||
};
|
||||
|
||||
onMounted(() => fetchPortalAndItsCategories());
|
||||
|
||||
watch(
|
||||
() => route.params.portalSlug,
|
||||
newSlug => {
|
||||
if (newSlug && newSlug !== uiSettings.value.last_active_portal_slug) {
|
||||
updateUISettings({
|
||||
last_active_portal_slug: newSlug,
|
||||
last_active_locale_code: selectedLocaleInPortal.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-grow-0 w-full h-full min-h-0 app-wrapper">
|
||||
<section
|
||||
v-if="isHelpCenterEnabled"
|
||||
class="flex flex-1 h-full px-0 overflow-hidden bg-white dark:bg-slate-900"
|
||||
>
|
||||
<router-view />
|
||||
</section>
|
||||
<UpgradePage v-else />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { articleSlug, portalSlug } = route.params;
|
||||
|
||||
const articleById = useMapGetter('articles/articleById');
|
||||
|
||||
const article = computed(() => articleById.value(articleSlug));
|
||||
|
||||
const isUpdating = ref(false);
|
||||
const isSaved = ref(false);
|
||||
|
||||
const portalLink = computed(() => {
|
||||
const { slug: categorySlug, locale: categoryLocale } = article.value.category;
|
||||
const { slug: articleSlugValue } = article.value;
|
||||
return buildPortalArticleURL(
|
||||
portalSlug,
|
||||
categorySlug,
|
||||
categoryLocale,
|
||||
articleSlugValue
|
||||
);
|
||||
});
|
||||
|
||||
const saveArticle = async ({ ...values }) => {
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
await store.dispatch('articles/update', {
|
||||
portalSlug,
|
||||
articleId: articleSlug,
|
||||
...values,
|
||||
});
|
||||
isSaved.value = true;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('HELP_CENTER.EDIT_ARTICLE_PAGE.API.ERROR');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
isUpdating.value = false;
|
||||
isSaved.value = true;
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
const isCategoryArticles = computed(() => {
|
||||
return (
|
||||
route.name === 'portals_categories_articles_index' ||
|
||||
route.name === 'portals_categories_articles_edit' ||
|
||||
route.name === 'portals_categories_index'
|
||||
);
|
||||
});
|
||||
|
||||
const goBackToArticles = () => {
|
||||
const { tab, categorySlug, locale } = route.params;
|
||||
if (isCategoryArticles.value) {
|
||||
router.push({
|
||||
name: 'portals_categories_articles_index',
|
||||
params: { categorySlug, locale },
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { tab, categorySlug, locale },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fetchArticleDetails = () => {
|
||||
store.dispatch('articles/show', {
|
||||
id: articleSlug,
|
||||
portalSlug,
|
||||
});
|
||||
};
|
||||
|
||||
const previewArticle = () => {
|
||||
window.open(portalLink.value, '_blank');
|
||||
useTrack(PORTALS_EVENTS.PREVIEW_ARTICLE, {
|
||||
status: article.value?.status,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchArticleDetails();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArticleEditor
|
||||
:article="article"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
@save-article="saveArticle"
|
||||
@preview-article="previewArticle"
|
||||
@go-back="goBackToArticles"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
import { getArticleStatus } from 'dashboard/helper/portalHelper.js';
|
||||
import ArticlesPage from 'dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const pageNumber = ref(1);
|
||||
|
||||
const articles = useMapGetter('articles/allArticles');
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
const meta = useMapGetter('articles/getMeta');
|
||||
const portalMeta = useMapGetter('portals/getMeta');
|
||||
const currentUserId = useMapGetter('getCurrentUserID');
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const selectedPortalSlug = computed(() => route.params.portalSlug);
|
||||
const selectedCategorySlug = computed(() => route.params.categorySlug);
|
||||
const status = computed(() => getArticleStatus(route.params.tab));
|
||||
|
||||
const author = computed(() =>
|
||||
route.params.tab === 'mine' ? currentUserId.value : null
|
||||
);
|
||||
|
||||
const activeLocale = computed(() => route.params.locale);
|
||||
const portal = computed(() => getPortalBySlug.value(selectedPortalSlug.value));
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale.code,
|
||||
name: allLocales[locale.code],
|
||||
code: locale.code,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const defaultPortalLocale = computed(() => {
|
||||
return portal.value?.meta?.default_locale;
|
||||
});
|
||||
|
||||
const selectedLocaleInPortal = computed(() => {
|
||||
return route.params.locale || defaultPortalLocale.value;
|
||||
});
|
||||
|
||||
const isCategoryArticles = computed(() => {
|
||||
return (
|
||||
route.name === 'portals_categories_articles_index' ||
|
||||
route.name === 'portals_categories_articles_edit' ||
|
||||
route.name === 'portals_categories_index'
|
||||
);
|
||||
});
|
||||
|
||||
const fetchArticles = ({ pageNumber: pageNumberParam } = {}) => {
|
||||
store.dispatch('articles/index', {
|
||||
pageNumber: pageNumberParam || pageNumber.value,
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: activeLocale.value,
|
||||
status: status.value,
|
||||
authorId: author.value,
|
||||
categorySlug: selectedCategorySlug.value,
|
||||
});
|
||||
};
|
||||
|
||||
const onPageChange = pageNumberParam => {
|
||||
fetchArticles({ pageNumber: pageNumberParam });
|
||||
};
|
||||
|
||||
const fetchPortalAndItsCategories = async locale => {
|
||||
await store.dispatch('portals/index');
|
||||
const selectedPortalParam = {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: locale || selectedLocaleInPortal.value,
|
||||
};
|
||||
store.dispatch('portals/show', selectedPortalParam);
|
||||
store.dispatch('categories/index', selectedPortalParam);
|
||||
store.dispatch('agents/get');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchArticles();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.params,
|
||||
() => {
|
||||
pageNumber.value = 1;
|
||||
fetchArticles();
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<ArticlesPage
|
||||
v-if="portal"
|
||||
:articles="articles"
|
||||
:portal-name="portal.name"
|
||||
:categories="categories"
|
||||
:allowed-locales="allowedLocales"
|
||||
:meta="meta"
|
||||
:portal-meta="portalMeta"
|
||||
:is-category-articles="isCategoryArticles"
|
||||
@page-change="onPageChange"
|
||||
@fetch-portal="fetchPortalAndItsCategories"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import ArticleEditor from 'dashboard/components-next/HelpCenter/Pages/ArticleEditorPage/ArticleEditor.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { portalSlug } = route.params;
|
||||
|
||||
const selectedAuthorId = ref(null);
|
||||
const selectedCategoryId = ref(null);
|
||||
|
||||
const currentUserId = useMapGetter('getCurrentUserID');
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
|
||||
const categoryId = computed(() => categories.value[0]?.id || null);
|
||||
|
||||
const article = ref({});
|
||||
const isUpdating = ref(false);
|
||||
const isSaved = ref(false);
|
||||
|
||||
const setAuthorId = authorId => {
|
||||
selectedAuthorId.value = authorId;
|
||||
};
|
||||
|
||||
const setCategoryId = newCategoryId => {
|
||||
selectedCategoryId.value = newCategoryId;
|
||||
};
|
||||
|
||||
const createNewArticle = async ({ title, content }) => {
|
||||
if (title) article.value.title = title;
|
||||
if (content) article.value.content = content;
|
||||
|
||||
if (!article.value.title || !article.value.content) return;
|
||||
|
||||
isUpdating.value = true;
|
||||
try {
|
||||
const { locale } = route.params;
|
||||
const articleId = await store.dispatch('articles/create', {
|
||||
portalSlug,
|
||||
content: article.value.content,
|
||||
title: article.value.title,
|
||||
locale: locale,
|
||||
authorId: selectedAuthorId.value || currentUserId.value,
|
||||
categoryId: selectedCategoryId.value || categoryId.value,
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.CREATE_ARTICLE, { locale });
|
||||
|
||||
router.replace({
|
||||
name: 'portals_articles_edit',
|
||||
params: {
|
||||
articleSlug: articleId,
|
||||
portalSlug,
|
||||
locale,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.message || t('HELP_CENTER.EDIT_ARTICLE_PAGE.API.ERROR');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goBackToArticles = () => {
|
||||
const { tab, categorySlug, locale } = route.params;
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { tab, categorySlug, locale },
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ArticleEditor
|
||||
:article="article"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
@save-article="createNewArticle"
|
||||
@go-back="goBackToArticles"
|
||||
@set-author="setAuthorId"
|
||||
@set-category="setCategoryId"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import CategoriesPage from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue';
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const categories = useMapGetter('categories/allCategories');
|
||||
|
||||
const selectedPortalSlug = computed(() => route.params.portalSlug);
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const isFetching = useMapGetter('categories/isFetching');
|
||||
|
||||
const portal = computed(() => getPortalBySlug.value(selectedPortalSlug.value));
|
||||
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale.code,
|
||||
name: allLocales[locale.code],
|
||||
code: locale.code,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const fetchCategoriesByPortalSlugAndLocale = async localeCode => {
|
||||
await store.dispatch('categories/index', {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: localeCode,
|
||||
});
|
||||
};
|
||||
|
||||
const updateMeta = async localeCode => {
|
||||
return store.dispatch('portals/show', {
|
||||
portalSlug: selectedPortalSlug.value,
|
||||
locale: localeCode,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchCategories = async localeCode => {
|
||||
await fetchCategoriesByPortalSlugAndLocale(localeCode);
|
||||
await updateMeta(localeCode);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchCategoriesByPortalSlugAndLocale(route.params.locale);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CategoriesPage
|
||||
:categories="categories"
|
||||
:is-fetching="isFetching"
|
||||
:allowed-locales="allowedLocales"
|
||||
@fetch-categories="fetchCategories"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted } from 'vue';
|
||||
import { useStore } from 'vuex';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
const { uiSettings } = useUISettings();
|
||||
const route = useRoute();
|
||||
|
||||
const portals = computed(() => store.getters['portals/allPortals']);
|
||||
|
||||
const isPortalPresent = portalSlug => {
|
||||
return !!portals.value.find(portal => portal.slug === portalSlug);
|
||||
};
|
||||
|
||||
const routeToView = (name, params) => {
|
||||
router.replace({ name, params, replace: true });
|
||||
};
|
||||
|
||||
const generateRouterParams = () => {
|
||||
const {
|
||||
last_active_portal_slug: lastActivePortalSlug,
|
||||
last_active_locale_code: lastActiveLocaleCode,
|
||||
} = uiSettings.value || {};
|
||||
if (isPortalPresent(lastActivePortalSlug)) {
|
||||
return {
|
||||
portalSlug: lastActivePortalSlug,
|
||||
locale: lastActiveLocaleCode,
|
||||
};
|
||||
}
|
||||
|
||||
if (portals.value.length > 0) {
|
||||
const { slug: portalSlug, meta: { default_locale: locale } = {} } =
|
||||
portals.value[0];
|
||||
return { portalSlug, locale };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const routeToLastActivePortal = () => {
|
||||
const params = generateRouterParams();
|
||||
const { navigationPath } = route.params;
|
||||
const isAValidRoute = [
|
||||
'portals_articles_index',
|
||||
'portals_categories_index',
|
||||
'portals_locales_index',
|
||||
'portals_settings_index',
|
||||
].includes(navigationPath);
|
||||
|
||||
const navigateTo = isAValidRoute ? navigationPath : 'portals_articles_index';
|
||||
if (params) {
|
||||
return routeToView(navigateTo, params);
|
||||
}
|
||||
return routeToView('portals_new', {});
|
||||
};
|
||||
|
||||
const performRouting = async () => {
|
||||
await store.dispatch('portals/index');
|
||||
nextTick(() => routeToLastActivePortal());
|
||||
};
|
||||
|
||||
onMounted(() => performRouting());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full bg-n-background text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import LocalesPage from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const portal = computed(() => getPortalBySlug.value(route.params.portalSlug));
|
||||
|
||||
const allowedLocales = computed(() => {
|
||||
if (!portal.value) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allAllowedLocales } = portal.value.config;
|
||||
return allAllowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale?.code,
|
||||
name: allLocales[locale?.code],
|
||||
code: locale?.code,
|
||||
articlesCount: locale?.articles_count || 0,
|
||||
categoriesCount: locale?.categories_count || 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LocalesPage :locales="allowedLocales" :portal="portal" />
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
import PortalEmptyState from 'dashboard/components-next/HelpCenter/EmptyState/Portal/PortalEmptyState.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full bg-n-background">
|
||||
<PortalEmptyState />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import PortalSettings from 'dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalSettings.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
const portals = useMapGetter('portals/allPortals');
|
||||
const isFetching = useMapGetter('portals/isFetchingPortals');
|
||||
const getPortalBySlug = useMapGetter('portals/portalBySlug');
|
||||
|
||||
const getNextAvailablePortal = deletedPortalSlug =>
|
||||
portals.value?.find(portal => portal.slug !== deletedPortalSlug) ?? null;
|
||||
|
||||
const getDefaultLocale = slug => {
|
||||
return getPortalBySlug.value(slug)?.meta?.default_locale;
|
||||
};
|
||||
|
||||
const fetchPortalAndItsCategories = async (slug, locale) => {
|
||||
const selectedPortalParam = { portalSlug: slug, locale };
|
||||
await Promise.all([
|
||||
store.dispatch('portals/index'),
|
||||
store.dispatch('portals/show', selectedPortalParam),
|
||||
store.dispatch('categories/index', selectedPortalParam),
|
||||
store.dispatch('agents/get'),
|
||||
store.dispatch('inboxes/get'),
|
||||
]);
|
||||
};
|
||||
|
||||
const updateRouteAfterDeletion = async deletedPortalSlug => {
|
||||
const nextPortal = getNextAvailablePortal(deletedPortalSlug);
|
||||
if (nextPortal) {
|
||||
const {
|
||||
slug,
|
||||
meta: { default_locale: defaultLocale },
|
||||
} = nextPortal;
|
||||
await fetchPortalAndItsCategories(slug, defaultLocale);
|
||||
router.push({
|
||||
name: 'portals_articles_index',
|
||||
params: { portalSlug: slug, locale: defaultLocale },
|
||||
});
|
||||
} else {
|
||||
router.push({ name: 'portals_new' });
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPortalRoute = async (newSlug, defaultLocale) => {
|
||||
// This is to refresh the portal route and update the UI settings
|
||||
// If there is slug change, this will be called to refresh the route and UI settings
|
||||
await fetchPortalAndItsCategories(newSlug, defaultLocale);
|
||||
updateUISettings({
|
||||
last_active_portal_slug: newSlug,
|
||||
last_active_locale_code: defaultLocale,
|
||||
});
|
||||
await router.replace({
|
||||
name: 'portals_settings_index',
|
||||
params: { portalSlug: newSlug },
|
||||
});
|
||||
};
|
||||
|
||||
const updatePortalSettings = async portalObj => {
|
||||
const { portalSlug } = route.params;
|
||||
try {
|
||||
const defaultLocale = getDefaultLocale(portalSlug);
|
||||
await store.dispatch('portals/update', {
|
||||
...portalObj,
|
||||
portalSlug: portalSlug || portalObj?.slug,
|
||||
});
|
||||
|
||||
// If there is a slug change, this will refresh the route and update the UI settings
|
||||
if (portalObj?.slug && portalSlug !== portalObj.slug) {
|
||||
await refreshPortalRoute(portalObj.slug, defaultLocale);
|
||||
}
|
||||
useAlert(
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.API.UPDATE_PORTAL.SUCCESS_MESSAGE')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL_SETTINGS.API.UPDATE_PORTAL.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const deletePortal = async selectedPortalForDelete => {
|
||||
const { slug } = selectedPortalForDelete;
|
||||
try {
|
||||
await store.dispatch('portals/delete', { portalSlug: slug });
|
||||
await updateRouteAfterDeletion(slug);
|
||||
useAlert(
|
||||
t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.DELETE_PORTAL.API.DELETE_ERROR')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePortal = updatePortalSettings;
|
||||
const handleUpdatePortalConfiguration = updatePortalSettings;
|
||||
const handleDeletePortal = deletePortal;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PortalSettings
|
||||
:portals="portals"
|
||||
:is-fetching="isFetching"
|
||||
@update-portal="handleUpdatePortal"
|
||||
@update-portal-configuration="handleUpdatePortalConfiguration"
|
||||
@delete-portal="handleDeletePortal"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,277 +0,0 @@
|
||||
<script>
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { isEmptyObject } from 'dashboard/helper/commons.js';
|
||||
export default {
|
||||
components: {
|
||||
MultiselectDropdown,
|
||||
},
|
||||
props: {
|
||||
article: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'saveArticle',
|
||||
'archiveArticle',
|
||||
'deleteArticle',
|
||||
'updateMeta',
|
||||
'saveArticle',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
metaTitle: '',
|
||||
metaDescription: '',
|
||||
metaTags: [],
|
||||
metaOptions: [],
|
||||
tagInputValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
categories: 'categories/allCategories',
|
||||
agents: 'agents/getAgents',
|
||||
}),
|
||||
assignedAuthor() {
|
||||
return this.article?.author;
|
||||
},
|
||||
selectedCategory() {
|
||||
return this.article?.category;
|
||||
},
|
||||
allTags() {
|
||||
return this.metaTags.map(item => item.name);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
article: {
|
||||
handler() {
|
||||
if (!isEmptyObject(this.article.meta || {})) {
|
||||
const {
|
||||
meta: { title = '', description = '', tags = [] },
|
||||
} = this.article;
|
||||
this.metaTitle = title;
|
||||
this.metaDescription = description;
|
||||
this.metaTags = this.formattedTags({ tags });
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.saveArticle = debounce(
|
||||
() => {
|
||||
this.$emit('saveArticle', {
|
||||
meta: {
|
||||
title: this.metaTitle,
|
||||
description: this.metaDescription,
|
||||
tags: this.allTags,
|
||||
},
|
||||
});
|
||||
},
|
||||
1000,
|
||||
false
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
formattedTags({ tags }) {
|
||||
return tags.map(tag => ({
|
||||
name: tag,
|
||||
}));
|
||||
},
|
||||
addTagValue(tagValue) {
|
||||
const tags = tagValue
|
||||
.split(',')
|
||||
.map(tag => tag.trim())
|
||||
.filter(tag => tag && !this.allTags.includes(tag));
|
||||
|
||||
this.metaTags.push(...this.formattedTags({ tags: [...new Set(tags)] }));
|
||||
this.saveArticle();
|
||||
},
|
||||
removeTag() {
|
||||
this.saveArticle();
|
||||
},
|
||||
handleSearchChange(value) {
|
||||
this.tagInputValue = value;
|
||||
},
|
||||
onBlur() {
|
||||
if (this.tagInputValue) {
|
||||
this.addTagValue(this.tagInputValue);
|
||||
}
|
||||
},
|
||||
onClickSelectCategory({ id }) {
|
||||
this.$emit('saveArticle', { category_id: id });
|
||||
},
|
||||
onClickAssignAuthor({ id }) {
|
||||
this.$emit('saveArticle', { author_id: id });
|
||||
this.updateMeta();
|
||||
},
|
||||
onChangeMetaInput() {
|
||||
this.saveArticle();
|
||||
},
|
||||
onClickArchiveArticle() {
|
||||
this.$emit('archiveArticle');
|
||||
this.updateMeta();
|
||||
},
|
||||
onClickDeleteArticle() {
|
||||
this.$emit('deleteArticle');
|
||||
this.updateMeta();
|
||||
},
|
||||
updateMeta() {
|
||||
this.$emit('updateMeta');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="popover-animation">
|
||||
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
|
||||
<div
|
||||
class="min-w-[15rem] max-w-[22.5rem] p-6 overflow-y-auto border-l rtl:border-r rtl:border-l-0 border-solid border-slate-50 dark:border-slate-700"
|
||||
>
|
||||
<h3 class="text-base text-slate-800 dark:text-slate-100">
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.TITLE') }}
|
||||
</h3>
|
||||
<div class="mt-4 mb-6">
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.LABEL') }}
|
||||
<MultiselectDropdown
|
||||
:options="categories"
|
||||
:selected-item="selectedCategory"
|
||||
:has-thumbnail="false"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.CATEGORY.SEARCH_PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@select="onClickSelectCategory"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.LABEL') }}
|
||||
<MultiselectDropdown
|
||||
:options="agents"
|
||||
:selected-item="assignedAuthor"
|
||||
:multiselector-title="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.TITLE')
|
||||
"
|
||||
:multiselector-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.PLACEHOLDER')
|
||||
"
|
||||
:no-search-result="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.NO_RESULT')
|
||||
"
|
||||
:input-placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.AUTHOR.SEARCH_PLACEHOLDER')
|
||||
"
|
||||
@select="onClickAssignAuthor"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaTitle"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TITLE.PLACEHOLDER')
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="metaDescription"
|
||||
class="text-sm"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t(
|
||||
'HELP_CENTER.ARTICLE_SETTINGS.FORM.META_DESCRIPTION.PLACEHOLDER'
|
||||
)
|
||||
"
|
||||
@input="onChangeMetaInput"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.LABEL') }}
|
||||
<multiselect
|
||||
v-model="metaTags"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.ARTICLE_SETTINGS.FORM.META_TAGS.PLACEHOLDER')
|
||||
"
|
||||
class="min-w-[300px]"
|
||||
label="name"
|
||||
:options="metaOptions"
|
||||
track-by="name"
|
||||
multiple
|
||||
taggable
|
||||
:close-on-select="false"
|
||||
@search-change="handleSearchChange"
|
||||
@close="onBlur"
|
||||
@tag="addTagValue"
|
||||
@remove="removeTag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<woot-button
|
||||
icon="archive"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
@click="onClickArchiveArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.ARCHIVE') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
icon="delete"
|
||||
size="small"
|
||||
variant="clear"
|
||||
color-scheme="alert"
|
||||
@click="onClickDeleteArticle"
|
||||
>
|
||||
{{ $t('HELP_CENTER.ARTICLE_SETTINGS.BUTTONS.DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep {
|
||||
.multiselect {
|
||||
@apply mb-0;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.multiselect--active .multiselect__tags {
|
||||
padding-right: var(--space-small) !important;
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
@apply text-slate-300 dark:text-slate-200 pt-2 mb-0;
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ portals: 'portals/allPortals' }),
|
||||
},
|
||||
mounted() {
|
||||
this.performRouting();
|
||||
},
|
||||
methods: {
|
||||
isPortalPresent(portalSlug) {
|
||||
return !!this.portals.find(portal => portal.slug === portalSlug);
|
||||
},
|
||||
async performRouting() {
|
||||
await this.$store.dispatch('portals/index');
|
||||
this.$nextTick(() => this.routeToLastActivePortal());
|
||||
},
|
||||
routeToView(name, params) {
|
||||
this.$router.replace({ name, params, replace: true });
|
||||
},
|
||||
async routeToLastActivePortal() {
|
||||
// TODO: This method should be written as a navigation guard rather than
|
||||
// a method in the component.
|
||||
const {
|
||||
last_active_portal_slug: lastActivePortalSlug,
|
||||
last_active_locale_code: lastActiveLocaleCode,
|
||||
} = this.uiSettings || {};
|
||||
|
||||
if (this.isPortalPresent(lastActivePortalSlug)) {
|
||||
// Check if the last active portal from the user preferences is available in the current
|
||||
// list of portals. If it is, navigate there. The last active portal is saved in the user's
|
||||
// UI settings, regardless of the account. Consequently, it's possible that the saved portal
|
||||
// slug is not available in the current account.
|
||||
this.routeToView('list_all_locale_articles', {
|
||||
portalSlug: lastActivePortalSlug,
|
||||
locale: lastActiveLocaleCode,
|
||||
});
|
||||
} else if (this.portals.length > 0) {
|
||||
// If the last active portal is available, check for the exisiting list of portals and
|
||||
// navigate to the first available portal.
|
||||
const { slug: portalSlug, meta: { default_locale: locale } = {} } =
|
||||
this.portals[0];
|
||||
this.routeToView('list_all_locale_articles', { portalSlug, locale });
|
||||
} else {
|
||||
// If no portals are available, navigate to the portal list page to prompt creation.
|
||||
this.$router.replace({ name: 'list_all_portals', replace: true });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-center w-full text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('HELP_CENTER.LOADING') }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,216 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import EditArticleHeader from '../../components/Header/EditArticleHeader.vue';
|
||||
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||
import ArticleSettings from './ArticleSettings.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { buildPortalArticleURL } from 'dashboard/helper/portalHelper';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
const { ARTICLE_STATUS_TYPES } = wootConstants;
|
||||
export default {
|
||||
components: {
|
||||
EditArticleHeader,
|
||||
ArticleEditor,
|
||||
Spinner,
|
||||
ArticleSettings,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
data() {
|
||||
return {
|
||||
isUpdating: false,
|
||||
isSaved: false,
|
||||
showArticleSettings: true,
|
||||
alertMessage: '',
|
||||
showDeleteConfirmationPopup: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isFetching: 'articles/isFetching',
|
||||
}),
|
||||
article() {
|
||||
return this.$store.getters['articles/articleById'](this.articleId);
|
||||
},
|
||||
articleId() {
|
||||
return this.$route.params.articleSlug;
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
selectedLocale() {
|
||||
return this.$route.params.locale;
|
||||
},
|
||||
portalLink() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
return buildPortalArticleURL(
|
||||
slug,
|
||||
this.article.category.slug,
|
||||
this.article.category.locale,
|
||||
this.article.slug
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchArticleDetails();
|
||||
},
|
||||
methods: {
|
||||
onClickGoBack() {
|
||||
if (window.history.length > 2) {
|
||||
this.$router.go(-1);
|
||||
} else {
|
||||
this.$router.push({ name: 'list_all_locale_articles' });
|
||||
}
|
||||
},
|
||||
fetchArticleDetails() {
|
||||
this.$store.dispatch('articles/show', {
|
||||
id: this.articleId,
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
});
|
||||
},
|
||||
openDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
confirmDeletion() {
|
||||
this.closeDeletePopup();
|
||||
this.deleteArticle();
|
||||
useTrack(PORTALS_EVENTS.DELETE_ARTICLE, {
|
||||
status: this.article?.status,
|
||||
});
|
||||
},
|
||||
async saveArticle({ ...values }) {
|
||||
this.isUpdating = true;
|
||||
try {
|
||||
await this.$store.dispatch('articles/update', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
articleId: this.articleId,
|
||||
...values,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message || this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR');
|
||||
useAlert(this.alertMessage);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.isUpdating = false;
|
||||
this.isSaved = true;
|
||||
}, 1500);
|
||||
}
|
||||
},
|
||||
async deleteArticle() {
|
||||
try {
|
||||
await this.$store.dispatch('articles/delete', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
articleId: this.articleId,
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.DELETE_ARTICLE.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.locale,
|
||||
recentlyDeleted: true,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.DELETE_ARTICLE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
async archiveArticle() {
|
||||
try {
|
||||
await this.$store.dispatch('articles/update', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
articleId: this.articleId,
|
||||
status: ARTICLE_STATUS_TYPES.ARCHIVE,
|
||||
});
|
||||
this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.SUCCESS');
|
||||
useTrack(PORTALS_EVENTS.ARCHIVE_ARTICLE, { uiFrom: 'sidebar' });
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message || this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
updateMeta() {
|
||||
const selectedPortalParam = {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.selectedLocale,
|
||||
};
|
||||
return this.$store.dispatch('portals/show', selectedPortalParam);
|
||||
},
|
||||
openArticleSettings() {
|
||||
this.showArticleSettings = true;
|
||||
},
|
||||
closeArticleSettings() {
|
||||
this.showArticleSettings = false;
|
||||
},
|
||||
showArticleInPortal() {
|
||||
window.open(this.portalLink, '_blank');
|
||||
useTrack(PORTALS_EVENTS.PREVIEW_ARTICLE, {
|
||||
status: this.article?.status,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full overflow-auto article-container">
|
||||
<div
|
||||
class="flex-1 flex-shrink-0 px-6 overflow-auto"
|
||||
:class="{ 'flex-grow-1 flex-shrink-0': showArticleSettings }"
|
||||
>
|
||||
<EditArticleHeader
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
:is-updating="isUpdating"
|
||||
:is-saved="isSaved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@show="showArticleInPortal"
|
||||
@update-meta="updateMeta"
|
||||
/>
|
||||
<div v-if="isFetching" class="h-full p-4 text-base text-center">
|
||||
<Spinner size="" />
|
||||
<span>{{ $t('HELP_CENTER.EDIT_ARTICLE.LOADING') }}</span>
|
||||
</div>
|
||||
<ArticleEditor
|
||||
v-else
|
||||
:is-settings-sidebar-open="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
<ArticleSettings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
@delete-article="openDeletePopup"
|
||||
@archive-article="archiveArticle"
|
||||
@update-meta="updateMeta"
|
||||
/>
|
||||
<woot-delete-modal
|
||||
v-model:show="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.TITLE')"
|
||||
:message="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.MESSAGE')"
|
||||
:confirm-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.YES')"
|
||||
:reject-text="$t('HELP_CENTER.DELETE_ARTICLE.MODAL.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,195 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import allLocales from 'shared/constants/locales.js';
|
||||
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import ArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/ArticleHeader.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import ArticleTable from '../../components/ArticleTable.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ArticleHeader,
|
||||
ArticleTable,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
},
|
||||
emits: ['reloadLocale'],
|
||||
data() {
|
||||
return {
|
||||
pageNumber: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
articles: 'articles/allArticles',
|
||||
categories: 'categories/allCategories',
|
||||
meta: 'articles/getMeta',
|
||||
isFetching: 'articles/isFetching',
|
||||
currentUserId: 'getCurrentUserID',
|
||||
getPortalBySlug: 'portals/portalBySlug',
|
||||
}),
|
||||
selectedCategory() {
|
||||
return this.categories.find(
|
||||
category => category.slug === this.selectedCategorySlug
|
||||
);
|
||||
},
|
||||
shouldShowEmptyState() {
|
||||
return !this.isFetching && !this.articles.length;
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
selectedCategorySlug() {
|
||||
const { categorySlug } = this.$route.params;
|
||||
return categorySlug;
|
||||
},
|
||||
articleType() {
|
||||
return this.$route.path.split('/').pop();
|
||||
},
|
||||
headerTitle() {
|
||||
switch (this.articleType) {
|
||||
case 'mine':
|
||||
return this.$t('HELP_CENTER.HEADER.TITLES.MINE');
|
||||
case 'draft':
|
||||
return this.$t('HELP_CENTER.HEADER.TITLES.DRAFT');
|
||||
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');
|
||||
}
|
||||
},
|
||||
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;
|
||||
},
|
||||
headerTitleInCategoryView() {
|
||||
return this.categories && this.categories.length
|
||||
? this.selectedCategory.name
|
||||
: '';
|
||||
},
|
||||
activeLocale() {
|
||||
return this.$route.params.locale;
|
||||
},
|
||||
activeLocaleName() {
|
||||
return allLocales[this.activeLocale];
|
||||
},
|
||||
portal() {
|
||||
return this.getPortalBySlug(this.selectedPortalSlug);
|
||||
},
|
||||
allowedLocales() {
|
||||
if (!this.portal) {
|
||||
return [];
|
||||
}
|
||||
const { allowed_locales: allowedLocales } = this.portal.config;
|
||||
return allowedLocales.map(locale => {
|
||||
return {
|
||||
id: locale.code,
|
||||
name: allLocales[locale.code],
|
||||
code: locale.code,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.pageNumber = 1;
|
||||
this.fetchArticles();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchArticles();
|
||||
},
|
||||
|
||||
methods: {
|
||||
newArticlePage() {
|
||||
this.$router.push({ name: 'new_article' });
|
||||
},
|
||||
fetchArticles({ pageNumber } = {}) {
|
||||
this.$store.dispatch('articles/index', {
|
||||
pageNumber: pageNumber || this.pageNumber,
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
locale: this.activeLocale,
|
||||
status: this.status,
|
||||
authorId: this.author,
|
||||
categorySlug: this.selectedCategorySlug,
|
||||
});
|
||||
},
|
||||
onPageChange(pageNumber) {
|
||||
this.fetchArticles({ pageNumber });
|
||||
},
|
||||
onReorder(reorderedGroup) {
|
||||
this.$store.dispatch('articles/reorder', {
|
||||
reorderedGroup,
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
});
|
||||
},
|
||||
onChangeLocale(locale) {
|
||||
this.$router.push({
|
||||
name: 'list_all_locale_articles',
|
||||
params: {
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
locale,
|
||||
},
|
||||
});
|
||||
this.$emit('reloadLocale');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col w-full max-w-full px-0 py-0 overflow-auto bg-white dark:bg-slate-900"
|
||||
>
|
||||
<ArticleHeader
|
||||
:header-title="headerTitle"
|
||||
:count="meta.count"
|
||||
:selected-locale="activeLocaleName"
|
||||
:all-locales="allowedLocales"
|
||||
selected-value="Published"
|
||||
class="border-b border-slate-50 dark:border-slate-700"
|
||||
@new-article-page="newArticlePage"
|
||||
@change-locale="onChangeLocale"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center px-4 py-6 text-base text-slate-600 dark:text-slate-200"
|
||||
>
|
||||
<Spinner />
|
||||
<span class="text-slate-600 dark:text-slate-200">
|
||||
{{ $t('HELP_CENTER.TABLE.LOADING_MESSAGE') }}
|
||||
</span>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.TABLE.NO_ARTICLES')"
|
||||
/>
|
||||
<div v-else class="flex flex-1">
|
||||
<ArticleTable
|
||||
:articles="articles"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="Number(meta.count)"
|
||||
@page-change="onPageChange"
|
||||
@reorder="onReorder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>{{ 'Component to list articles in a category in a portal' }}</div>
|
||||
</template>
|
||||
@@ -1,118 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import EditArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/EditArticleHeader.vue';
|
||||
import ArticleEditor from '../../components/ArticleEditor.vue';
|
||||
import portalMixin from '../../mixins/portalMixin';
|
||||
import ArticleSettings from './ArticleSettings.vue';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
components: {
|
||||
EditArticleHeader,
|
||||
ArticleEditor,
|
||||
ArticleSettings,
|
||||
},
|
||||
mixins: [portalMixin],
|
||||
data() {
|
||||
return {
|
||||
articleTitle: '',
|
||||
articleContent: '',
|
||||
showOpenSidebarButton: false,
|
||||
showArticleSettings: true,
|
||||
article: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUserID: 'getCurrentUserID',
|
||||
categories: 'categories/allCategories',
|
||||
}),
|
||||
articleId() {
|
||||
return this.$route.params.articleSlug;
|
||||
},
|
||||
newArticle() {
|
||||
return { title: this.articleTitle, content: this.articleContent };
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.$route.params.portalSlug;
|
||||
},
|
||||
categoryId() {
|
||||
return this.categories.length ? this.categories[0].id : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickGoBack() {
|
||||
this.$router.push({ name: 'list_all_locale_articles' });
|
||||
},
|
||||
async createNewArticle({ ...values }) {
|
||||
const { title, content } = values;
|
||||
if (title) this.articleTitle = title;
|
||||
if (content) this.articleContent = content;
|
||||
if (this.articleTitle && this.articleContent) {
|
||||
try {
|
||||
const articleId = await this.$store.dispatch('articles/create', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
content: this.articleContent,
|
||||
title: this.articleTitle,
|
||||
author_id: this.currentUserID,
|
||||
// TODO: Change to un categorized later when API supports
|
||||
category_id: this.categoryId,
|
||||
});
|
||||
this.$router.push({
|
||||
name: 'edit_article',
|
||||
params: {
|
||||
articleSlug: articleId,
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
locale: this.locale,
|
||||
recentlyCreated: true,
|
||||
},
|
||||
});
|
||||
useTrack(PORTALS_EVENTS.CREATE_ARTICLE, {
|
||||
locale: this.locale,
|
||||
});
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.CREATE_ARTICLE.API.ERROR_MESSAGE');
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
}
|
||||
},
|
||||
openArticleSettings() {
|
||||
this.showArticleSettings = true;
|
||||
},
|
||||
closeArticleSettings() {
|
||||
this.showArticleSettings = false;
|
||||
},
|
||||
saveArticle() {
|
||||
this.alertMessage = this.$t('HELP_CENTER.CREATE_ARTICLE.ERROR_MESSAGE');
|
||||
useAlert(this.alertMessage);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-1 overflow-auto">
|
||||
<div
|
||||
class="flex-1 flex-shrink-0 px-6 overflow-y-auto"
|
||||
:class="{ 'flex-grow-1': showArticleSettings }"
|
||||
>
|
||||
<EditArticleHeader
|
||||
:back-button-label="$t('HELP_CENTER.HEADER.TITLES.ALL_ARTICLES')"
|
||||
draft-state="saved"
|
||||
:is-sidebar-open="showArticleSettings"
|
||||
@back="onClickGoBack"
|
||||
@open="openArticleSettings"
|
||||
@close="closeArticleSettings"
|
||||
@save-article="createNewArticle"
|
||||
/>
|
||||
<ArticleEditor :article="newArticle" @save-article="createNewArticle" />
|
||||
</div>
|
||||
<ArticleSettings
|
||||
v-if="showArticleSettings"
|
||||
:article="article"
|
||||
@save-article="saveArticle"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,199 +0,0 @@
|
||||
<script>
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
import NameEmojiInput from './NameEmojiInput.vue';
|
||||
|
||||
export default {
|
||||
components: { NameEmojiInput },
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portalName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
portalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['create', 'cancel', 'update:show'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
icon: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
slug: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
selectedPortalSlug() {
|
||||
return this.$route.params.portalSlug
|
||||
? this.$route.params.portalSlug
|
||||
: this.portalSlug;
|
||||
},
|
||||
slugError() {
|
||||
if (this.v$.slug.$error) {
|
||||
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onNameChange(name) {
|
||||
this.name = name;
|
||||
this.slug = convertToCategorySlug(this.name);
|
||||
},
|
||||
onCreate() {
|
||||
this.$emit('create');
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
onClickInsertEmoji(emoji) {
|
||||
this.icon = emoji;
|
||||
},
|
||||
|
||||
async addCategory() {
|
||||
const { name, slug, description, locale, icon } = this;
|
||||
const data = {
|
||||
name,
|
||||
icon,
|
||||
slug,
|
||||
description,
|
||||
locale,
|
||||
};
|
||||
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();
|
||||
useTrack(PORTALS_EVENTS.CREATE_CATEGORY, {
|
||||
hasDescription: Boolean(description),
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error?.message;
|
||||
this.alertMessage =
|
||||
errorMessage || this.$t('HELP_CENTER.CATEGORY.ADD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal v-model:show="localShow" :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="w-full" @submit.prevent="onCreate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mx-0 mt-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.ADD.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<NameEmojiInput
|
||||
: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')"
|
||||
:has-error="v$.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
@name-change="onNameChange"
|
||||
@icon-change="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="slug"
|
||||
:class="{ error: v$.slug.$error }"
|
||||
class="w-full"
|
||||
: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"
|
||||
@blur="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="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.input-container::v-deep {
|
||||
@apply mt-0 mb-4 mx-0;
|
||||
|
||||
input {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,113 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
emits: ['edit', 'delete'],
|
||||
|
||||
methods: {
|
||||
editCategory(category) {
|
||||
this.$emit('edit', category);
|
||||
},
|
||||
deleteCategory(category) {
|
||||
this.$emit('delete', category);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<table class="woot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.NAME') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.DESCRIPTION') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.LOCALE') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.ARTICLE_COUNT') }}
|
||||
</th>
|
||||
<th scope="col" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td colspan="100%" class="horizontal-line" />
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr v-for="category in categories" :key="category.id">
|
||||
<td>
|
||||
<span>{{ category.icon }} {{ category.name }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ category.description }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ category.locale }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>{{ category.meta.articles_count }}</span>
|
||||
</td>
|
||||
<td class="inline-flex gap-1">
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.ACTION_BUTTON.EDIT'
|
||||
)
|
||||
"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
icon="edit"
|
||||
color-scheme="secondary"
|
||||
@click="editCategory(category)"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.ACTION_BUTTON.DELETE'
|
||||
)
|
||||
"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
icon="delete"
|
||||
color-scheme="alert"
|
||||
@click="deleteCategory(category)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p
|
||||
v-if="categories.length === 0"
|
||||
class="flex justify-center mt-8 text-base text-slate-500 dark:text-slate-300"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TABLE.EMPTY_TEXT') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
table {
|
||||
thead tr th {
|
||||
@apply text-sm font-medium normal-case text-slate-800 dark:text-slate-100 pl-0 rtl:pl-2.5 rtl:pr-0 pt-0;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
@apply border-b-0;
|
||||
td {
|
||||
@apply text-sm pl-0 rtl:pl-2.5 rtl:pr-0 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
.horizontal-line {
|
||||
@apply border-b border-solid border-slate-75 dark:border-slate-700;
|
||||
}
|
||||
</style>
|
||||
@@ -1,219 +0,0 @@
|
||||
<script>
|
||||
import { required, minLength } from '@vuelidate/validators';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { convertToCategorySlug } from 'dashboard/helper/commons.js';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
import CategoryNameIconInput from './NameEmojiInput.vue';
|
||||
|
||||
export default {
|
||||
components: { CategoryNameIconInput },
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
portalName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
category: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
selectedPortalSlug: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update', 'cancel', 'update:show'],
|
||||
setup() {
|
||||
return { v$: useVuelidate() };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: this.category.id,
|
||||
name: '',
|
||||
icon: '',
|
||||
slug: '',
|
||||
description: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
name: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
slug: {
|
||||
required,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
localShow: {
|
||||
get() {
|
||||
return this.show;
|
||||
},
|
||||
set(value) {
|
||||
this.$emit('update:show', value);
|
||||
},
|
||||
},
|
||||
slugError() {
|
||||
if (this.v$.slug.$error) {
|
||||
return this.$t('HELP_CENTER.CATEGORY.ADD.SLUG.ERROR');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateDataFromStore();
|
||||
},
|
||||
methods: {
|
||||
updateDataFromStore() {
|
||||
const { category } = this;
|
||||
this.name = category.name;
|
||||
this.icon = category.icon;
|
||||
this.slug = category.slug;
|
||||
this.description = category.description;
|
||||
},
|
||||
changeName(name) {
|
||||
this.name = name;
|
||||
this.slug = convertToCategorySlug(this.name);
|
||||
},
|
||||
onClickInsertEmoji(emoji) {
|
||||
this.icon = emoji;
|
||||
},
|
||||
onUpdate() {
|
||||
this.$emit('update');
|
||||
},
|
||||
onClose() {
|
||||
this.$emit('cancel');
|
||||
},
|
||||
async editCategory() {
|
||||
const { id, name, slug, icon, description } = this;
|
||||
const data = {
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
slug,
|
||||
description,
|
||||
};
|
||||
this.v$.$touch();
|
||||
if (this.v$.$invalid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$store.dispatch('categories/update', {
|
||||
portalSlug: this.selectedPortalSlug,
|
||||
categoryId: id,
|
||||
categoryObj: data,
|
||||
});
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.CATEGORY.EDIT.API.SUCCESS_MESSAGE'
|
||||
);
|
||||
useTrack(PORTALS_EVENTS.EDIT_CATEGORY);
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const errorMessage = error?.message;
|
||||
this.alertMessage =
|
||||
errorMessage ||
|
||||
this.$t('HELP_CENTER.CATEGORY.EDIT.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<woot-modal v-model:show="localShow" :on-close="onClose">
|
||||
<woot-modal-header
|
||||
:header-title="$t('HELP_CENTER.CATEGORY.EDIT.TITLE')"
|
||||
:header-content="$t('HELP_CENTER.CATEGORY.EDIT.SUB_TITLE')"
|
||||
/>
|
||||
<form class="w-full" @submit.prevent="onUpdate">
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row w-full mx-0 mt-0 mb-4">
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.PORTAL') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ portalName }}</p>
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-[50%]">
|
||||
<label>
|
||||
<span>{{ $t('HELP_CENTER.CATEGORY.EDIT.LOCALE') }}</span>
|
||||
<p class="text-slate-600 dark:text-slate-400">{{ locale }}</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<CategoryNameIconInput
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.NAME.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.NAME.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.NAME.HELP_TEXT')"
|
||||
:has-error="v$.name.$error"
|
||||
:error-message="$t('HELP_CENTER.CATEGORY.ADD.NAME.ERROR')"
|
||||
:existing-name="category.name"
|
||||
:saved-icon="category.icon"
|
||||
@name-change="changeName"
|
||||
@icon-change="onClickInsertEmoji"
|
||||
/>
|
||||
<woot-input
|
||||
v-model="slug"
|
||||
:class="{ error: v$.slug.$error }"
|
||||
class="w-full"
|
||||
:error="slugError"
|
||||
:label="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.LABEL')"
|
||||
:placeholder="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.PLACEHOLDER')"
|
||||
:help-text="$t('HELP_CENTER.CATEGORY.EDIT.SLUG.HELP_TEXT')"
|
||||
@input="v$.slug.$touch"
|
||||
@blur="v$.slug.$touch"
|
||||
/>
|
||||
<label>
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.LABEL') }}
|
||||
<textarea
|
||||
v-model="description"
|
||||
rows="3"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('HELP_CENTER.CATEGORY.EDIT.DESCRIPTION.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div class="w-full">
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button @click="editCategory">
|
||||
{{ $t('HELP_CENTER.CATEGORY.EDIT.BUTTONS.CREATE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
@@ -1,159 +0,0 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import { defineOptions, ref, computed } from 'vue';
|
||||
|
||||
import CategoryListItem from './CategoryListItem.vue';
|
||||
import AddCategory from './AddCategory.vue';
|
||||
import EditCategory from './EditCategory.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ListAllCategories',
|
||||
});
|
||||
|
||||
const selectedCategory = ref({});
|
||||
const currentLocaleCode = ref('en');
|
||||
const showEditCategoryModal = ref(false);
|
||||
const showAddCategoryModal = ref(false);
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const currentPortalSlug = computed(() => {
|
||||
return route.params.portalSlug;
|
||||
});
|
||||
|
||||
const categoriesByLocaleCode = computed(() => {
|
||||
return getters['categories/categoriesByLocaleCode'].value(
|
||||
currentLocaleCode.value
|
||||
);
|
||||
});
|
||||
|
||||
const currentPortal = computed(() => {
|
||||
const slug = currentPortalSlug.value;
|
||||
if (slug) return getters['portals/portalBySlug'].value(slug);
|
||||
|
||||
return getters['portals/allPortals'].value[0];
|
||||
});
|
||||
const currentPortalName = computed(() => {
|
||||
return currentPortal.value ? currentPortal.value.name : '';
|
||||
});
|
||||
const allLocales = computed(() => {
|
||||
return currentPortal.value ? currentPortal.value.config.allowed_locales : [];
|
||||
});
|
||||
|
||||
const allowedLocaleCodes = computed(() => {
|
||||
return allLocales.value.map(locale => locale.code);
|
||||
});
|
||||
|
||||
function openAddCategoryModal() {
|
||||
showAddCategoryModal.value = true;
|
||||
}
|
||||
function openEditCategoryModal(category) {
|
||||
selectedCategory.value = category;
|
||||
showEditCategoryModal.value = true;
|
||||
}
|
||||
function closeAddCategoryModal() {
|
||||
showAddCategoryModal.value = false;
|
||||
}
|
||||
function closeEditCategoryModal() {
|
||||
showEditCategoryModal.value = false;
|
||||
}
|
||||
async function fetchCategoriesByPortalSlugAndLocale(localeCode) {
|
||||
await store.dispatch('categories/index', {
|
||||
portalSlug: currentPortalSlug.value,
|
||||
locale: localeCode,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteCategory(category) {
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await store.dispatch('categories/delete', {
|
||||
portalSlug: currentPortalSlug.value,
|
||||
categoryId: category.id,
|
||||
});
|
||||
alertMessage = t('HELP_CENTER.CATEGORY.DELETE.API.SUCCESS_MESSAGE');
|
||||
useTrack(PORTALS_EVENTS.DELETE_CATEGORY, {
|
||||
hasArticles: category?.meta?.articles_count !== 0,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error?.message;
|
||||
alertMessage =
|
||||
errorMessage || t('HELP_CENTER.CATEGORY.DELETE.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(alertMessage);
|
||||
}
|
||||
}
|
||||
function changeCurrentCategory(event) {
|
||||
const localeCode = event.target.value;
|
||||
currentLocaleCode.value = localeCode;
|
||||
fetchCategoriesByPortalSlugAndLocale(localeCode);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-6xl">
|
||||
<header class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center w-full gap-3">
|
||||
<label
|
||||
class="mb-0 text-base font-normal text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.TITLE') }}
|
||||
</label>
|
||||
<select
|
||||
:value="currentLocaleCode"
|
||||
class="w-[15%] h-8 mb-0 py-0.5"
|
||||
@change="changeCurrentCategory"
|
||||
>
|
||||
<option
|
||||
v-for="allowedLocaleCode in allowedLocaleCodes"
|
||||
:key="allowedLocaleCode"
|
||||
:value="allowedLocaleCode"
|
||||
>
|
||||
{{ allowedLocaleCode }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="items-center flex-none">
|
||||
<woot-button
|
||||
size="small"
|
||||
variant="smooth"
|
||||
color-scheme="primary"
|
||||
icon="add"
|
||||
@click="openAddCategoryModal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.EDIT.CATEGORIES.NEW_CATEGORY') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="category-list">
|
||||
<CategoryListItem
|
||||
:categories="categoriesByLocaleCode"
|
||||
@delete="deleteCategory"
|
||||
@edit="openEditCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
<EditCategory
|
||||
v-if="showEditCategoryModal"
|
||||
v-model:show="showEditCategoryModal"
|
||||
:portal-name="currentPortalName"
|
||||
:locale="selectedCategory.locale"
|
||||
:category="selectedCategory"
|
||||
:selected-portal-slug="currentPortalSlug"
|
||||
@cancel="closeEditCategoryModal"
|
||||
/>
|
||||
<AddCategory
|
||||
v-if="showAddCategoryModal"
|
||||
v-model:show="showAddCategoryModal"
|
||||
:portal-name="currentPortalName"
|
||||
:locale="currentLocaleCode"
|
||||
@cancel="closeAddCategoryModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,125 +0,0 @@
|
||||
<script>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const EmojiInput = defineAsyncComponent(
|
||||
() => import('shared/components/emoji/EmojiInput.vue')
|
||||
);
|
||||
|
||||
export default {
|
||||
components: { EmojiInput },
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
existingName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
savedIcon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['iconChange', 'nameChange'],
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
icon: '',
|
||||
showEmojiPicker: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
nameErrorMessage() {
|
||||
if (this.hasError) {
|
||||
return this.errorMessage;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateDataFromStore();
|
||||
},
|
||||
methods: {
|
||||
updateDataFromStore() {
|
||||
this.name = this.existingName;
|
||||
this.icon = this.savedIcon;
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
},
|
||||
onClickInsertEmoji(emoji = '') {
|
||||
this.icon = emoji;
|
||||
this.$emit('iconChange', emoji);
|
||||
this.showEmojiPicker = false;
|
||||
},
|
||||
onNameChange() {
|
||||
this.$emit('nameChange', this.name);
|
||||
},
|
||||
hideEmojiPicker() {
|
||||
if (this.showEmojiPicker) {
|
||||
this.showEmojiPicker = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex items-center">
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
class="absolute [&>span]:flex [&>span]:items-center [&>span]:justify-center z-10 top-[28px] h-[2.5rem] w-[2.45rem] !text-slate-400 dark:!text-slate-600 dark:!bg-slate-900 !p-0"
|
||||
color-scheme="secondary"
|
||||
@click="toggleEmojiPicker"
|
||||
>
|
||||
<span v-if="icon" v-dompurify-html="icon" class="text-lg" />
|
||||
<fluent-icon
|
||||
v-else
|
||||
size="18"
|
||||
icon="emoji-add"
|
||||
type="outline"
|
||||
class="text-slate-400 dark:text-slate-600"
|
||||
/>
|
||||
</woot-button>
|
||||
<woot-input
|
||||
v-model="name"
|
||||
:class="{ error: hasError }"
|
||||
class="!mt-0 !mb-4 !mx-0 [&>input]:!mb-0 ltr:[&>input]:!ml-12 rtl:[&>input]:!mr-12 relative w-[calc(100%-3rem)] [&>p]:w-max"
|
||||
:error="nameErrorMessage"
|
||||
:label="label"
|
||||
:placeholder="placeholder"
|
||||
:help-text="helpText"
|
||||
@update:model-value="onNameChange"
|
||||
/>
|
||||
<EmojiInput
|
||||
v-if="showEmojiPicker"
|
||||
v-on-clickaway="hideEmojiPicker"
|
||||
class="left-0 top-16"
|
||||
show-remove-button
|
||||
:on-click="onClickInsertEmoji"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.emoji-dialog::before {
|
||||
@apply hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +0,0 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>{{ 'Component to create a category' }}</div>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>{{ 'Component to show details of a category' }}</div>
|
||||
</template>
|
||||
@@ -1,107 +0,0 @@
|
||||
<script>
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SettingsHeader from 'dashboard/routes/dashboard/settings/SettingsHeader.vue';
|
||||
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsHeader,
|
||||
SettingIntroBanner,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
computed: {
|
||||
currentPortal() {
|
||||
const slug = this.$route.params.portalSlug;
|
||||
if (slug) return this.$store.getters['portals/portalBySlug'](slug);
|
||||
return this.$store.getters['portals/allPortals'][0];
|
||||
},
|
||||
tabs() {
|
||||
const tabs = [
|
||||
{
|
||||
key: 'edit_portal_information',
|
||||
name: this.$t('HELP_CENTER.PORTAL.EDIT.TABS.BASIC_SETTINGS.TITLE'),
|
||||
},
|
||||
{
|
||||
key: 'edit_portal_customization',
|
||||
name: this.$t(
|
||||
'HELP_CENTER.PORTAL.EDIT.TABS.CUSTOMIZATION_SETTINGS.TITLE'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: `list_all_locale_categories`,
|
||||
name: this.$t('HELP_CENTER.PORTAL.EDIT.TABS.CATEGORY_SETTINGS.TITLE'),
|
||||
},
|
||||
{
|
||||
key: 'edit_portal_locales',
|
||||
name: this.$t('HELP_CENTER.PORTAL.EDIT.TABS.LOCALE_SETTINGS.TITLE'),
|
||||
},
|
||||
];
|
||||
|
||||
return tabs;
|
||||
},
|
||||
activeTabIndex() {
|
||||
return this.tabs.map(tab => tab.key).indexOf(this.$route.name);
|
||||
},
|
||||
portalName() {
|
||||
return this.currentPortal ? this.currentPortal.name : '';
|
||||
},
|
||||
currentPortalLocale() {
|
||||
return this.currentPortal ? this.currentPortal?.meta?.default_locale : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTabChange(index) {
|
||||
const nextRoute = this.tabs.map(tab => tab.key)[index];
|
||||
const slug = this.$route.params.portalSlug;
|
||||
|
||||
this.$router.push({
|
||||
name: nextRoute,
|
||||
params: { portalSlug: slug, locale: this.currentPortalLocale },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:header-title="$t('HELP_CENTER.PORTAL.EDIT.HEADER_TEXT')"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div class="overflow-auto max-h-[96%]">
|
||||
<SettingIntroBanner :header-title="portalName">
|
||||
<woot-tabs
|
||||
:index="activeTabIndex"
|
||||
:border="false"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<woot-tabs-item
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="tab.key"
|
||||
:index="index"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</SettingIntroBanner>
|
||||
<div class="flex flex-wrap max-w-full px-8 py-4 my-auto">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
::v-deep .tabs {
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,85 +0,0 @@
|
||||
<script setup>
|
||||
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineOptions, computed, ref, onMounted } from 'vue';
|
||||
|
||||
defineOptions({ name: 'EditPortalBasic' });
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const uiFlags = getters['portals/uiFlagsIn'];
|
||||
|
||||
const lastPortalSlug = ref(null);
|
||||
|
||||
const currentPortalSlug = computed(() => {
|
||||
return route.params.portalSlug;
|
||||
});
|
||||
|
||||
const currentPortal = computed(() => {
|
||||
const slug = route.params.portalSlug;
|
||||
return getters['portals/portalBySlug'].value(slug);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
lastPortalSlug.value = currentPortalSlug.value;
|
||||
});
|
||||
|
||||
async function updatePortalSettings(portalObj) {
|
||||
let alertMessage = '';
|
||||
|
||||
try {
|
||||
const portalSlug = lastPortalSlug.value;
|
||||
await store.dispatch('portals/update', { ...portalObj, portalSlug });
|
||||
|
||||
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
|
||||
|
||||
if (lastPortalSlug.value !== portalObj.slug) {
|
||||
await store.dispatch('portals/index');
|
||||
router.replace({
|
||||
name: route.name,
|
||||
params: { portalSlug: portalObj.slug },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
alertMessage =
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
|
||||
} finally {
|
||||
useAlert(alertMessage);
|
||||
}
|
||||
}
|
||||
async function deleteLogo() {
|
||||
try {
|
||||
const portalSlug = lastPortalSlug.value;
|
||||
await store.dispatch('portals/deleteLogo', {
|
||||
portalSlug,
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
error?.message || t('HELP_CENTER.PORTAL.ADD.LOGO.IMAGE_DELETE_ERROR')
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<PortalSettingsBasicForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
:submit-button-text="
|
||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||
"
|
||||
@submit="updatePortalSettings"
|
||||
@delete-logo="deleteLogo"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script setup>
|
||||
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineOptions, computed } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'EditPortalCustomization',
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const uiFlags = getters['portals/uiFlagsIn'];
|
||||
|
||||
const currentPortal = computed(() => {
|
||||
const slug = route.params.portalSlug;
|
||||
return getters['portals/portalBySlug'].value(slug);
|
||||
});
|
||||
|
||||
async function updatePortalSettings(portalObj) {
|
||||
const portalSlug = route.params.portalSlug;
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await store.dispatch('portals/update', {
|
||||
...portalObj,
|
||||
portalSlug,
|
||||
});
|
||||
|
||||
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
|
||||
} catch (error) {
|
||||
alertMessage =
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
|
||||
} finally {
|
||||
useAlert(alertMessage);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<PortalSettingsCustomizationForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
:submit-button-text="
|
||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||
"
|
||||
@submit="updatePortalSettings"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup>
|
||||
import LocaleItemTable from 'dashboard/routes/dashboard/helpcenter/components/PortalListItemTable.vue';
|
||||
import AddLocale from 'dashboard/routes/dashboard/helpcenter/components/AddLocale.vue';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineOptions, ref, onBeforeMount, computed } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'EditPortalLocales',
|
||||
});
|
||||
|
||||
const isAddLocaleModalOpen = ref(false);
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
const currentPortalSlug = computed(() => {
|
||||
return route.params.portalSlug;
|
||||
});
|
||||
const currentPortal = computed(() => {
|
||||
const slug = currentPortalSlug.value;
|
||||
if (slug) return getters['portals/portalBySlug'].value(slug);
|
||||
|
||||
return getters['portals/allPortals'].value[0];
|
||||
});
|
||||
const locales = computed(() => {
|
||||
return currentPortal.value?.config.allowed_locales;
|
||||
});
|
||||
const allowedLocales = computed(() => {
|
||||
return Object.keys(locales.value).map(key => {
|
||||
return locales.value[key].code;
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchPortals() {
|
||||
await store.dispatch('portals/index');
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
fetchPortals();
|
||||
});
|
||||
|
||||
async function updatePortalLocales({
|
||||
newAllowedLocales,
|
||||
defaultLocale,
|
||||
messageKey,
|
||||
}) {
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await store.dispatch('portals/update', {
|
||||
portalSlug: currentPortalSlug.value,
|
||||
config: {
|
||||
default_locale: defaultLocale,
|
||||
allowed_locales: newAllowedLocales,
|
||||
},
|
||||
});
|
||||
alertMessage = t(`HELP_CENTER.PORTAL.${messageKey}.API.SUCCESS_MESSAGE`);
|
||||
} catch (error) {
|
||||
alertMessage =
|
||||
error?.message || t(`HELP_CENTER.PORTAL.${messageKey}.API.ERROR_MESSAGE`);
|
||||
} finally {
|
||||
useAlert(alertMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function changeDefaultLocale({ localeCode }) {
|
||||
updatePortalLocales({
|
||||
newAllowedLocales: allowedLocales.value,
|
||||
defaultLocale: localeCode,
|
||||
messageKey: 'CHANGE_DEFAULT_LOCALE',
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.SET_DEFAULT_LOCALE, {
|
||||
newLocale: localeCode,
|
||||
from: route.name,
|
||||
});
|
||||
}
|
||||
function deletePortalLocale({ localeCode }) {
|
||||
const updatedLocales = allowedLocales.value.filter(
|
||||
code => code !== localeCode
|
||||
);
|
||||
|
||||
const defaultLocale = currentPortal.value?.meta.default_locale;
|
||||
|
||||
updatePortalLocales({
|
||||
newAllowedLocales: updatedLocales,
|
||||
defaultLocale,
|
||||
messageKey: 'DELETE_LOCALE',
|
||||
});
|
||||
|
||||
useTrack(PORTALS_EVENTS.DELETE_LOCALE, {
|
||||
deletedLocale: localeCode,
|
||||
from: route.name,
|
||||
});
|
||||
}
|
||||
|
||||
function closeAddLocaleModal() {
|
||||
isAddLocaleModalOpen.value = false;
|
||||
}
|
||||
function addLocale() {
|
||||
isAddLocaleModalOpen.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full max-w-6xl space-y-4 bg-white dark:bg-slate-900">
|
||||
<div class="flex justify-end">
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
size="small"
|
||||
color-scheme="primary"
|
||||
class="header-action-buttons"
|
||||
@click="addLocale"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.PORTAL_SETTINGS.LIST_ITEM.HEADER.ADD') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<LocaleItemTable
|
||||
v-if="currentPortal"
|
||||
:locales="locales"
|
||||
:selected-locale-code="currentPortal.meta.default_locale"
|
||||
@change-default-locale="changeDefaultLocale"
|
||||
@delete="deletePortalLocale"
|
||||
/>
|
||||
<woot-modal
|
||||
v-model:show="isAddLocaleModalOpen"
|
||||
:on-close="closeAddLocaleModal"
|
||||
>
|
||||
<AddLocale
|
||||
:show="isAddLocaleModalOpen"
|
||||
:portal="currentPortal"
|
||||
@cancel="closeAddLocaleModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import PortalListItem from '../../components/PortalListItem.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import AddLocale from '../../components/AddLocale.vue';
|
||||
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PortalListItem,
|
||||
EmptyState,
|
||||
Spinner,
|
||||
AddLocale,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isAddLocaleModalOpen: false,
|
||||
selectedPortal: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
portals: 'portals/allPortals',
|
||||
isFetching: 'portals/isFetchingPortals',
|
||||
}),
|
||||
portalStatus() {
|
||||
return this.archived ? 'Archived' : 'Live';
|
||||
},
|
||||
shouldShowEmptyState() {
|
||||
return !this.isFetching && !this.portals.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openPortal(portalSlug) {
|
||||
window.open(buildPortalURL(portalSlug), '_blank');
|
||||
},
|
||||
addPortal() {
|
||||
this.$router.push({ name: 'new_portal_information' });
|
||||
},
|
||||
closeAddLocaleModal() {
|
||||
this.isAddLocaleModalOpen = false;
|
||||
this.selectedPortal = {};
|
||||
},
|
||||
addLocale(portalId) {
|
||||
this.isAddLocaleModalOpen = true;
|
||||
this.selectedPortal = this.portals.find(portal => portal.id === portalId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full max-w-full px-4 py-2">
|
||||
<div class="flex items-center justify-between h-12 mx-0 mt-0 mb-2">
|
||||
<div class="flex items-center">
|
||||
<woot-sidemenu-icon />
|
||||
<h1
|
||||
class="mx-2 my-0 text-2xl font-medium text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.HEADER') }}
|
||||
</h1>
|
||||
</div>
|
||||
<woot-button
|
||||
color-scheme="primary"
|
||||
icon="add"
|
||||
size="small"
|
||||
@click="addPortal"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.NEW_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div class="h-[90vh] overflow-y-scroll">
|
||||
<PortalListItem
|
||||
v-for="portal in portals"
|
||||
:key="portal.id"
|
||||
:portal="portal"
|
||||
:status="portalStatus"
|
||||
@add-locale="addLocale"
|
||||
@open-site="openPortal"
|
||||
/>
|
||||
<div
|
||||
v-if="isFetching"
|
||||
class="flex items-center justify-center p-40 text-base"
|
||||
>
|
||||
<Spinner />
|
||||
<span>{{ $t('HELP_CENTER.PORTAL.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
<EmptyState
|
||||
v-else-if="shouldShowEmptyState"
|
||||
:title="$t('HELP_CENTER.PORTAL.NO_PORTALS_MESSAGE')"
|
||||
/>
|
||||
</div>
|
||||
<woot-modal
|
||||
v-model:show="isAddLocaleModalOpen"
|
||||
:on-close="closeAddLocaleModal"
|
||||
>
|
||||
<AddLocale
|
||||
:show="isAddLocaleModalOpen"
|
||||
:portal="selectedPortal"
|
||||
@cancel="closeAddLocaleModal"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SettingsHeader from 'dashboard/routes/dashboard/settings/SettingsHeader.vue';
|
||||
export default {
|
||||
components: {
|
||||
SettingsHeader,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
items() {
|
||||
const routes = {
|
||||
BASIC: 'new_portal_information',
|
||||
CUSTOMIZATION: 'portal_customization',
|
||||
FINISH: 'portal_finish',
|
||||
};
|
||||
|
||||
const steps = ['BASIC', 'CUSTOMIZATION', 'FINISH'];
|
||||
|
||||
return steps.map(step => ({
|
||||
title: this.$t(`HELP_CENTER.PORTAL.ADD.CREATE_FLOW.${step}.TITLE`),
|
||||
route: routes[step],
|
||||
body: this.useInstallationName(
|
||||
this.$t(`HELP_CENTER.PORTAL.ADD.CREATE_FLOW.${step}.BODY`),
|
||||
this.globalConfig.installationName
|
||||
),
|
||||
}));
|
||||
},
|
||||
portalHeaderText() {
|
||||
if (this.$route.name === 'new_portal_information') {
|
||||
return this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.HEADER'
|
||||
);
|
||||
}
|
||||
if (this.$route.name === 'portal_customization') {
|
||||
return this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.CUSTOMIZATION_PAGE.HEADER'
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex-1">
|
||||
<SettingsHeader
|
||||
button-route="new"
|
||||
:header-title="portalHeaderText"
|
||||
show-back-button
|
||||
:back-button-label="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BACK_BUTTON')
|
||||
"
|
||||
:show-new-button="false"
|
||||
/>
|
||||
<div
|
||||
class="grid grid-cols-[20rem_1fr] w-full h-full overflow-auto rtl:pl-0 rtl:pr-4 bg-slate-50 dark:bg-slate-800 p-5"
|
||||
>
|
||||
<woot-wizard
|
||||
class="hidden md:block"
|
||||
:global-config="globalConfig"
|
||||
:items="items"
|
||||
/>
|
||||
<div
|
||||
class="w-full p-5 bg-white border border-transparent border-solid rounded-md shadow-sm dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,71 +0,0 @@
|
||||
<script setup>
|
||||
import PortalSettingsCustomizationForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsCustomizationForm.vue';
|
||||
import { PORTALS_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { defineOptions, onMounted, computed } from 'vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'PortalCustomization',
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const uiFlags = getters['portals/uiFlagsIn'];
|
||||
|
||||
const currentPortal = computed(() => {
|
||||
const slug = route.params.portalSlug;
|
||||
if (slug) return getters['portals/portalBySlug'].value(slug);
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
store.dispatch('portals/index');
|
||||
});
|
||||
|
||||
async function updatePortalSettings(portalObj) {
|
||||
const portalSlug = route.params.portalSlug;
|
||||
let alertMessage = '';
|
||||
try {
|
||||
await store.dispatch('portals/update', {
|
||||
portalSlug,
|
||||
...portalObj,
|
||||
});
|
||||
alertMessage = t('HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_UPDATE');
|
||||
|
||||
useTrack(PORTALS_EVENTS.ONBOARD_CUSTOMIZATION, {
|
||||
hasHomePageLink: Boolean(portalObj.homepage_link),
|
||||
hasPageTitle: Boolean(portalObj.page_title),
|
||||
hasHeaderText: Boolean(portalObj.headerText),
|
||||
});
|
||||
} catch (error) {
|
||||
alertMessage =
|
||||
error?.message ||
|
||||
t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_UPDATE');
|
||||
} finally {
|
||||
useAlert(alertMessage);
|
||||
router.push({ name: 'portal_finish' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-root-v-if -->
|
||||
<template>
|
||||
<PortalSettingsCustomizationForm
|
||||
v-if="currentPortal"
|
||||
:portal="currentPortal"
|
||||
:is-submitting="uiFlags.isUpdating"
|
||||
:submit-button-text="
|
||||
$t('HELP_CENTER.PORTAL.EDIT.EDIT_BASIC_INFO.BUTTON_TEXT')
|
||||
"
|
||||
@submit="updatePortalSettings"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,70 +0,0 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert, useTrack } from 'dashboard/composables';
|
||||
|
||||
import PortalSettingsBasicForm from 'dashboard/routes/dashboard/helpcenter/components/PortalSettingsBasicForm.vue';
|
||||
import { PORTALS_EVENTS } from '../../../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PortalSettingsBasicForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
slug: '',
|
||||
domain: '',
|
||||
alertMessage: '',
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'portals/uiFlagsIn',
|
||||
}),
|
||||
},
|
||||
|
||||
methods: {
|
||||
async createPortal(portal) {
|
||||
try {
|
||||
const { blob_id: blobId } = portal;
|
||||
await this.$store.dispatch('portals/create', {
|
||||
portal,
|
||||
blob_id: blobId,
|
||||
});
|
||||
|
||||
this.alertMessage = this.$t(
|
||||
'HELP_CENTER.PORTAL.ADD.API.SUCCESS_MESSAGE_FOR_BASIC'
|
||||
);
|
||||
this.$router.push({
|
||||
name: 'portal_customization',
|
||||
params: { portalSlug: portal.slug },
|
||||
});
|
||||
const analyticsPayload = {
|
||||
has_custom_domain: portal.domain !== '',
|
||||
};
|
||||
useTrack(PORTALS_EVENTS.ONBOARD_BASIC_INFORMATION, analyticsPayload);
|
||||
useTrack(PORTALS_EVENTS.CREATE_PORTAL, analyticsPayload);
|
||||
} catch (error) {
|
||||
this.alertMessage =
|
||||
error?.message ||
|
||||
this.$t('HELP_CENTER.PORTAL.ADD.API.ERROR_MESSAGE_FOR_BASIC');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PortalSettingsBasicForm
|
||||
:is-submitting="uiFlags.isCreating"
|
||||
:submit-button-text="
|
||||
$t(
|
||||
'HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.BASIC_SETTINGS_PAGE.CREATE_BASIC_SETTING_BUTTON'
|
||||
)
|
||||
"
|
||||
@submit="createPortal"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script setup>
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { defineOptions } from 'vue';
|
||||
defineOptions({
|
||||
name: 'PortalSettingsFinish',
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-grow-0 flex-shrink-0 w-full h-full max-w-full px-6 pt-3 pb-6 bg-white border border-transparent border-solid dark:bg-slate-900 dark:border-transparent"
|
||||
>
|
||||
<EmptyState
|
||||
:title="$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.TITLE')"
|
||||
:message="
|
||||
$t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.MESSAGE')
|
||||
"
|
||||
>
|
||||
<div class="w-full text-center">
|
||||
<router-link
|
||||
class="rounded button success nice"
|
||||
:to="{
|
||||
name: 'list_all_portals',
|
||||
}"
|
||||
>
|
||||
{{ $t('HELP_CENTER.PORTAL.ADD.CREATE_FLOW_PAGE.FINISH_PAGE.FINISH') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
<!-- Unused file deprecated -->
|
||||
<template>
|
||||
<div>{{ 'Component to view details of portal' }}</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user