feat(v4): Update the help center portal design (#10296)

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sivin Varghese
2024-10-24 10:39:36 +05:30
committed by GitHub
parent 6d3ecfe3c1
commit a3855a8d1d
144 changed files with 6376 additions and 6604 deletions

View File

@@ -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,
],
},
{

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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],
},
],
};

View File

@@ -1,4 +1,4 @@
import { frontendURL } from '../../../../helper/URLHelper';
import { frontendURL } from 'dashboard/helper/URLHelper';
export const getPortalRoute = (path = '') => {
const slugToBeAdded = path ? `/${path}` : '';

View File

@@ -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];
},
},
};

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
<!-- Unused file deprecated -->
<template>
<div>{{ 'Component to list articles in a category in a portal' }}</div>
</template>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
<!-- Unused file deprecated -->
<template>
<div>{{ 'Component to create a category' }}</div>
</template>

View File

@@ -1,4 +0,0 @@
<!-- Unused file deprecated -->
<template>
<div>{{ 'Component to show details of a category' }}</div>
</template>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +0,0 @@
<!-- Unused file deprecated -->
<template>
<div>{{ 'Component to view details of portal' }}</div>
</template>