feat(v4): Help center portal redesign improvements (#10349)

This commit is contained in:
Sivin Varghese
2024-10-29 09:34:43 +05:30
committed by GitHub
parent 035a037313
commit f73798a1aa
10 changed files with 139 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,11 @@
"CONFIRM": "Confirm" "CONFIRM": "Confirm"
} }
}, },
"THUMBNAIL": {
"AUTHOR": {
"NOT_AVAILABLE": "Author is not available"
}
},
"BREADCRUMB": { "BREADCRUMB": {
"ARIA_LABEL": "Breadcrumb" "ARIA_LABEL": "Breadcrumb"
} }

View File

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

View File

@@ -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'],

View File

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