feat(v4): Help center portal redesign improvements (#10349)
This commit is contained in:
@@ -30,7 +30,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
author: {
|
author: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
default: null,
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -157,7 +157,6 @@ const handleClick = id => {
|
|||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
v-if="author"
|
|
||||||
:author="author"
|
:author="author"
|
||||||
:name="authorName"
|
:name="authorName"
|
||||||
:src="authorThumbnailSrc"
|
:src="authorThumbnailSrc"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
import { OnClickOutside } from '@vueuse/components';
|
import { OnClickOutside } from '@vueuse/components';
|
||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['saveArticle', 'setAuthor', 'setCategory']);
|
const emit = defineEmits(['saveArticle', 'setAuthor', 'setCategory']);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const openAgentsList = ref(false);
|
const openAgentsList = ref(false);
|
||||||
const openCategoryList = ref(false);
|
const openCategoryList = ref(false);
|
||||||
@@ -36,13 +38,15 @@ const currentUser = computed(() =>
|
|||||||
agents.value.find(agent => agent.id === currentUserId.value)
|
agents.value.find(agent => agent.id === currentUserId.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const categorySlugFromRoute = computed(() => route.params.categorySlug);
|
||||||
|
|
||||||
const author = computed(() => {
|
const author = computed(() => {
|
||||||
if (isNewArticle.value) {
|
if (isNewArticle.value) {
|
||||||
return selectedAuthorId.value
|
return selectedAuthorId.value
|
||||||
? agents.value.find(agent => agent.id === selectedAuthorId.value)
|
? agents.value.find(agent => agent.id === selectedAuthorId.value)
|
||||||
: currentUser.value;
|
: currentUser.value;
|
||||||
}
|
}
|
||||||
return props.article?.author || currentUser.value;
|
return props.article?.author || null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const authorName = computed(
|
const authorName = computed(
|
||||||
@@ -51,24 +55,52 @@ const authorName = computed(
|
|||||||
const authorThumbnailSrc = computed(() => author.value?.thumbnail);
|
const authorThumbnailSrc = computed(() => author.value?.thumbnail);
|
||||||
|
|
||||||
const agentList = computed(() => {
|
const agentList = computed(() => {
|
||||||
return [...agents.value]
|
return (
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
agents.value
|
||||||
.map(agent => ({
|
?.map(({ name, id, thumbnail }) => ({
|
||||||
label: agent.name,
|
label: name,
|
||||||
value: agent.id,
|
value: id,
|
||||||
thumbnail: { name: agent.name, src: agent.thumbnail },
|
thumbnail: { name, src: thumbnail },
|
||||||
isSelected: agent.id === props.article?.author?.id,
|
isSelected:
|
||||||
action: 'assignAuthor',
|
id === props.article?.author?.id ||
|
||||||
}))
|
id === (selectedAuthorId.value || currentUserId.value),
|
||||||
.sort((a, b) => b.isSelected - a.isSelected);
|
action: 'assignAuthor',
|
||||||
|
}))
|
||||||
|
// Sort the list by isSelected first, then by name(label)
|
||||||
|
.toSorted((a, b) => {
|
||||||
|
if (a.isSelected !== b.isSelected) {
|
||||||
|
return Number(b.isSelected) - Number(a.isSelected);
|
||||||
|
}
|
||||||
|
return a.label.localeCompare(b.label);
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasAgentList = computed(() => {
|
const hasAgentList = computed(() => {
|
||||||
return agents.value?.length > 0;
|
return agents.value?.length > 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const findCategoryFromSlug = slug => {
|
||||||
|
return categories.value?.find(category => category.slug === slug);
|
||||||
|
};
|
||||||
|
|
||||||
|
const assignCategoryFromSlug = slug => {
|
||||||
|
const categoryFromSlug = findCategoryFromSlug(slug);
|
||||||
|
if (categoryFromSlug) {
|
||||||
|
selectedCategoryId.value = categoryFromSlug.id;
|
||||||
|
return categoryFromSlug;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const selectedCategory = computed(() => {
|
const selectedCategory = computed(() => {
|
||||||
if (isNewArticle.value) {
|
if (isNewArticle.value) {
|
||||||
|
if (categorySlugFromRoute.value) {
|
||||||
|
const categoryFromSlug = assignCategoryFromSlug(
|
||||||
|
categorySlugFromRoute.value
|
||||||
|
);
|
||||||
|
if (categoryFromSlug) return categoryFromSlug;
|
||||||
|
}
|
||||||
return selectedCategoryId.value
|
return selectedCategoryId.value
|
||||||
? categories.value.find(
|
? categories.value.find(
|
||||||
category => category.id === selectedCategoryId.value
|
category => category.id === selectedCategoryId.value
|
||||||
@@ -81,15 +113,20 @@ const selectedCategory = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const categoryList = computed(() => {
|
const categoryList = computed(() => {
|
||||||
return categories.value
|
return (
|
||||||
.map(category => ({
|
categories.value
|
||||||
label: category.name,
|
.map(({ name, id, icon }) => ({
|
||||||
value: category.id,
|
label: name,
|
||||||
emoji: category.icon,
|
value: id,
|
||||||
isSelected: category.id === props.article?.category?.id,
|
emoji: icon,
|
||||||
action: 'assignCategory',
|
isSelected: isNewArticle.value
|
||||||
}))
|
? id === (selectedCategoryId.value || selectedCategory.value?.id)
|
||||||
.sort((a, b) => b.isSelected - a.isSelected);
|
: id === props.article?.category?.id,
|
||||||
|
action: 'assignCategory',
|
||||||
|
}))
|
||||||
|
// Sort categories by isSelected
|
||||||
|
.toSorted((a, b) => Number(b.isSelected) - Number(a.isSelected))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasCategoryMenuItems = computed(() => {
|
const hasCategoryMenuItems = computed(() => {
|
||||||
@@ -124,6 +161,19 @@ const handleArticleAction = ({ action, value }) => {
|
|||||||
const updateMeta = meta => {
|
const updateMeta = meta => {
|
||||||
emit('saveArticle', { meta });
|
emit('saveArticle', { meta });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (categorySlugFromRoute.value && isNewArticle.value) {
|
||||||
|
// Assign category from slug if there is one
|
||||||
|
const categoryFromSlug = findCategoryFromSlug(categorySlugFromRoute.value);
|
||||||
|
if (categoryFromSlug) {
|
||||||
|
handleArticleAction({
|
||||||
|
action: 'assignCategory',
|
||||||
|
value: categoryFromSlug?.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -139,7 +189,6 @@ const updateMeta = meta => {
|
|||||||
>
|
>
|
||||||
<template #leftPrefix>
|
<template #leftPrefix>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
v-if="author"
|
|
||||||
:author="author"
|
:author="author"
|
||||||
:name="authorName"
|
:name="authorName"
|
||||||
:size="20"
|
:size="20"
|
||||||
|
|||||||
@@ -114,15 +114,23 @@ const getEmptyStateSubtitle = computed(() => getEmptyStateText('SUBTITLE'));
|
|||||||
|
|
||||||
const handleTabChange = tab =>
|
const handleTabChange = tab =>
|
||||||
updateRoute({ tab: tab.value === ARTICLE_TABS.ALL ? '' : tab.value });
|
updateRoute({ tab: tab.value === ARTICLE_TABS.ALL ? '' : tab.value });
|
||||||
|
|
||||||
const handleCategoryAction = value =>
|
const handleCategoryAction = value =>
|
||||||
updateRoute({ categorySlug: value === CATEGORY_ALL ? '' : value });
|
updateRoute({ categorySlug: value === CATEGORY_ALL ? '' : value });
|
||||||
|
|
||||||
const handleLocaleAction = value => {
|
const handleLocaleAction = value => {
|
||||||
updateRoute({ locale: value, categorySlug: '' });
|
updateRoute({ locale: value, categorySlug: '' });
|
||||||
emit('fetchPortal', value);
|
emit('fetchPortal', value);
|
||||||
};
|
};
|
||||||
const handlePageChange = page => emit('pageChange', page);
|
const handlePageChange = page => emit('pageChange', page);
|
||||||
const navigateToNewArticlePage = () =>
|
|
||||||
router.push({ name: 'portals_articles_new' });
|
const navigateToNewArticlePage = () => {
|
||||||
|
const { categorySlug, locale } = route.params;
|
||||||
|
router.push({
|
||||||
|
name: 'portals_articles_new',
|
||||||
|
params: { categorySlug, locale },
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
|
|
||||||
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
|
||||||
import LocaleList from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleList.vue';
|
import LocaleList from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleList.vue';
|
||||||
import AddLocaleDialog from 'dashboard/components-next/HelpCenter/Pages/LocalePage/AddLocaleDialog.vue';
|
import AddLocaleDialog from 'dashboard/components-next/HelpCenter/Pages/LocalePage/AddLocaleDialog.vue';
|
||||||
|
|
||||||
@@ -18,6 +21,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const addLocaleDialogRef = ref(null);
|
const addLocaleDialogRef = ref(null);
|
||||||
|
|
||||||
|
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
|
||||||
|
|
||||||
const openAddLocaleDialog = () => {
|
const openAddLocaleDialog = () => {
|
||||||
addLocaleDialogRef.value.dialogRef.open();
|
addLocaleDialogRef.value.dialogRef.open();
|
||||||
};
|
};
|
||||||
@@ -43,7 +48,13 @@ const localeCount = computed(() => props.locales?.length);
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<LocaleList :locales="locales" :portal="portal" />
|
<div
|
||||||
|
v-if="isSwitchingPortal"
|
||||||
|
class="flex items-center justify-center py-10 text-n-slate-11"
|
||||||
|
>
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<LocaleList v-else :locales="locales" :portal="portal" />
|
||||||
</template>
|
</template>
|
||||||
<AddLocaleDialog ref="addLocaleDialogRef" :portal="portal" />
|
<AddLocaleDialog ref="addLocaleDialogRef" :portal="portal" />
|
||||||
</HelpCenterLayout>
|
</HelpCenterLayout>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { computed } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||||
|
import { buildPortalURL } from 'dashboard/helper/portalHelper';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components-next/thumbnail/Thumbnail.vue';
|
||||||
@@ -25,6 +26,10 @@ const portals = useMapGetter('portals/allPortals');
|
|||||||
|
|
||||||
const currentPortalSlug = computed(() => route.params.portalSlug);
|
const currentPortalSlug = computed(() => route.params.portalSlug);
|
||||||
|
|
||||||
|
const portalLink = computed(() => {
|
||||||
|
return buildPortalURL(currentPortalSlug.value);
|
||||||
|
});
|
||||||
|
|
||||||
const isPortalActive = portal => {
|
const isPortalActive = portal => {
|
||||||
return portal.slug === currentPortalSlug.value;
|
return portal.slug === currentPortalSlug.value;
|
||||||
};
|
};
|
||||||
@@ -71,6 +76,10 @@ const openCreatePortalDialog = () => {
|
|||||||
emit('close');
|
emit('close');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClickPreviewPortal = () => {
|
||||||
|
window.open(portalLink.value, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
const redirectToPortalHomePage = () => {
|
const redirectToPortalHomePage = () => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'portals_index',
|
name: 'portals_index',
|
||||||
@@ -89,12 +98,22 @@ const redirectToPortalHomePage = () => {
|
|||||||
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
|
class="flex items-center justify-between gap-4 px-6 pb-3 border-b border-n-alpha-2"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h2
|
<div class="flex items-center gap-2">
|
||||||
class="text-base font-medium cursor-pointer text-slate-900 dark:text-slate-50 w-fit hover:underline"
|
<h2
|
||||||
@click="redirectToPortalHomePage"
|
class="text-base font-medium cursor-pointer text-slate-900 dark:text-slate-50 w-fit hover:underline"
|
||||||
>
|
@click="redirectToPortalHomePage"
|
||||||
{{ t('HELP_CENTER.PORTAL_SWITCHER.PORTALS') }}
|
>
|
||||||
</h2>
|
{{ t('HELP_CENTER.PORTAL_SWITCHER.PORTALS') }}
|
||||||
|
</h2>
|
||||||
|
<Button
|
||||||
|
icon="arrow-up-right-lucide"
|
||||||
|
variant="ghost"
|
||||||
|
icon-lib="lucide"
|
||||||
|
size="sm"
|
||||||
|
class="!w-6 !h-6 hover:bg-n-slate-2 text-n-slate-11 !p-0.5 rounded-md"
|
||||||
|
@click="onClickPreviewPortal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||||
{{ t('HELP_CENTER.PORTAL_SWITCHER.CREATE_PORTAL') }}
|
{{ t('HELP_CENTER.PORTAL_SWITCHER.CREATE_PORTAL') }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import { removeEmoji } from 'shared/helpers/emoji';
|
import { removeEmoji } from 'shared/helpers/emoji';
|
||||||
|
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||||
@@ -30,6 +31,9 @@ const props = defineProps({
|
|||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const hasImageLoaded = ref(false);
|
const hasImageLoaded = ref(false);
|
||||||
const imgError = ref(false);
|
const imgError = ref(false);
|
||||||
|
|
||||||
@@ -108,6 +112,7 @@ const onImgLoad = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
v-tooltip.top-start="t('THUMBNAIL.AUTHOR.NOT_AVAILABLE')"
|
||||||
class="flex items-center justify-center w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700/50"
|
class="flex items-center justify-center w-4 h-4 rounded-full bg-slate-100 dark:bg-slate-700/50"
|
||||||
>
|
>
|
||||||
<FluentIcon
|
<FluentIcon
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
"CONFIRM": "Confirm"
|
"CONFIRM": "Confirm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"THUMBNAIL": {
|
||||||
|
"AUTHOR": {
|
||||||
|
"NOT_AVAILABLE": "Author is not available"
|
||||||
|
}
|
||||||
|
},
|
||||||
"BREADCRUMB": {
|
"BREADCRUMB": {
|
||||||
"ARIA_LABEL": "Breadcrumb"
|
"ARIA_LABEL": "Breadcrumb"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -532,20 +532,20 @@
|
|||||||
"BUTTON_LABEL": "New article"
|
"BUTTON_LABEL": "New article"
|
||||||
},
|
},
|
||||||
"MINE": {
|
"MINE": {
|
||||||
"TITLE": "There are no articles in mine",
|
"TITLE": "You haven't written any articles here",
|
||||||
"SUBTITLE": "Mine articles will appear here"
|
"SUBTITLE": "All articles written by you show up here for quick access."
|
||||||
},
|
},
|
||||||
"DRAFT": {
|
"DRAFT": {
|
||||||
"TITLE": "There are no articles in draft",
|
"TITLE": "There are no articles in drafts",
|
||||||
"SUBTITLE": "Draft articles will appear here"
|
"SUBTITLE": "Draft articles will appear here"
|
||||||
},
|
},
|
||||||
"PUBLISHED": {
|
"PUBLISHED": {
|
||||||
"TITLE": "There are no articles in published",
|
"TITLE": "There are no published articles",
|
||||||
"SUBTITLE": "Published articles will appear here"
|
"SUBTITLE": "Published articles will appear here"
|
||||||
},
|
},
|
||||||
"ARCHIVED": {
|
"ARCHIVED": {
|
||||||
"TITLE": "There are no articles in archived",
|
"TITLE": "There are no articles in the archive",
|
||||||
"SUBTITLE": "Archived articles will appear here"
|
"SUBTITLE": "Archived articles don't show up on the portal, you can use it to mark deprecated or outdated pages"
|
||||||
},
|
},
|
||||||
"CATEGORY": {
|
"CATEGORY": {
|
||||||
"TITLE": "There are no articles in this category",
|
"TITLE": "There are no articles in this category",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const portalRoutes = [
|
|||||||
component: PortalsArticlesIndexPage,
|
component: PortalsArticlesIndexPage,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: getPortalRoute(':portalSlug/:locale/articles/new'),
|
path: getPortalRoute(':portalSlug/:locale/:categorySlug?/articles/new'),
|
||||||
name: 'portals_articles_new',
|
name: 'portals_articles_new',
|
||||||
meta: {
|
meta: {
|
||||||
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
permissions: ['administrator', 'agent', 'knowledge_base_manage'],
|
||||||
|
|||||||
@@ -300,5 +300,6 @@
|
|||||||
"m2 22l1-1h3l9-9M3 21v-3l9-9",
|
"m2 22l1-1h3l9-9M3 21v-3l9-9",
|
||||||
"m15 6l3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3z"
|
"m15 6l3.4-3.4a2.1 2.1 0 1 1 3 3L18 9l.4.4a2.1 2.1 0 1 1-3 3l-3.8-3.8a2.1 2.1 0 1 1 3-3z"
|
||||||
],
|
],
|
||||||
"building-lucide-outline": "M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Zm0-10H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2M10 6h4m-4 4h4m-4 4h4m-4 4h4"
|
"building-lucide-outline": "M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Zm0-10H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2M10 6h4m-4 4h4m-4 4h4m-4 4h4",
|
||||||
|
"arrow-up-right-lucide-outline": "M7 7h10v10M7 17L17 7"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user