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: {
type: Object,
required: true,
default: null,
},
category: {
type: Object,
@@ -157,7 +157,6 @@ const handleClick = id => {
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<Thumbnail
v-if="author"
:author="author"
:name="authorName"
:src="authorThumbnailSrc"

View File

@@ -1,6 +1,7 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { OnClickOutside } from '@vueuse/components';
import { useMapGetter } from 'dashboard/composables/store';
@@ -19,6 +20,7 @@ const props = defineProps({
const emit = defineEmits(['saveArticle', 'setAuthor', 'setCategory']);
const { t } = useI18n();
const route = useRoute();
const openAgentsList = ref(false);
const openCategoryList = ref(false);
@@ -36,13 +38,15 @@ const currentUser = computed(() =>
agents.value.find(agent => agent.id === currentUserId.value)
);
const categorySlugFromRoute = computed(() => route.params.categorySlug);
const author = computed(() => {
if (isNewArticle.value) {
return selectedAuthorId.value
? agents.value.find(agent => agent.id === selectedAuthorId.value)
: currentUser.value;
}
return props.article?.author || currentUser.value;
return props.article?.author || null;
});
const authorName = computed(
@@ -51,24 +55,52 @@ const authorName = computed(
const authorThumbnailSrc = computed(() => author.value?.thumbnail);
const agentList = computed(() => {
return [...agents.value]
.sort((a, b) => a.name.localeCompare(b.name))
.map(agent => ({
label: agent.name,
value: agent.id,
thumbnail: { name: agent.name, src: agent.thumbnail },
isSelected: agent.id === props.article?.author?.id,
action: 'assignAuthor',
}))
.sort((a, b) => b.isSelected - a.isSelected);
return (
agents.value
?.map(({ name, id, thumbnail }) => ({
label: name,
value: id,
thumbnail: { name, src: thumbnail },
isSelected:
id === props.article?.author?.id ||
id === (selectedAuthorId.value || currentUserId.value),
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(() => {
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(() => {
if (isNewArticle.value) {
if (categorySlugFromRoute.value) {
const categoryFromSlug = assignCategoryFromSlug(
categorySlugFromRoute.value
);
if (categoryFromSlug) return categoryFromSlug;
}
return selectedCategoryId.value
? categories.value.find(
category => category.id === selectedCategoryId.value
@@ -81,15 +113,20 @@ const selectedCategory = computed(() => {
});
const categoryList = computed(() => {
return categories.value
.map(category => ({
label: category.name,
value: category.id,
emoji: category.icon,
isSelected: category.id === props.article?.category?.id,
action: 'assignCategory',
}))
.sort((a, b) => b.isSelected - a.isSelected);
return (
categories.value
.map(({ name, id, icon }) => ({
label: name,
value: id,
emoji: icon,
isSelected: isNewArticle.value
? id === (selectedCategoryId.value || selectedCategory.value?.id)
: id === props.article?.category?.id,
action: 'assignCategory',
}))
// Sort categories by isSelected
.toSorted((a, b) => Number(b.isSelected) - Number(a.isSelected))
);
});
const hasCategoryMenuItems = computed(() => {
@@ -124,6 +161,19 @@ const handleArticleAction = ({ action, value }) => {
const updateMeta = 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>
<template>
@@ -139,7 +189,6 @@ const updateMeta = meta => {
>
<template #leftPrefix>
<Thumbnail
v-if="author"
:author="author"
:name="authorName"
:size="20"

View File

@@ -114,15 +114,23 @@ const getEmptyStateSubtitle = computed(() => getEmptyStateText('SUBTITLE'));
const handleTabChange = tab =>
updateRoute({ tab: tab.value === ARTICLE_TABS.ALL ? '' : tab.value });
const handleCategoryAction = value =>
updateRoute({ categorySlug: value === CATEGORY_ALL ? '' : value });
const handleLocaleAction = value => {
updateRoute({ locale: value, categorySlug: '' });
emit('fetchPortal', value);
};
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>
<template>

View File

@@ -1,7 +1,10 @@
<script setup>
import { computed, ref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store.js';
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.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 AddLocaleDialog from 'dashboard/components-next/HelpCenter/Pages/LocalePage/AddLocaleDialog.vue';
@@ -18,6 +21,8 @@ const props = defineProps({
const addLocaleDialogRef = ref(null);
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
const openAddLocaleDialog = () => {
addLocaleDialogRef.value.dialogRef.open();
};
@@ -43,7 +48,13 @@ const localeCount = computed(() => props.locales?.length);
</div>
</template>
<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>
<AddLocaleDialog ref="addLocaleDialogRef" :portal="portal" />
</HelpCenterLayout>

View File

@@ -3,6 +3,7 @@ import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
import { buildPortalURL } from 'dashboard/helper/portalHelper';
import Button from 'dashboard/components-next/button/Button.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 portalLink = computed(() => {
return buildPortalURL(currentPortalSlug.value);
});
const isPortalActive = portal => {
return portal.slug === currentPortalSlug.value;
};
@@ -71,6 +76,10 @@ const openCreatePortalDialog = () => {
emit('close');
};
const onClickPreviewPortal = () => {
window.open(portalLink.value, '_blank');
};
const redirectToPortalHomePage = () => {
router.push({
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"
>
<div class="flex flex-col gap-1">
<h2
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>
<div class="flex items-center gap-2">
<h2
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>
<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">
{{ t('HELP_CENTER.PORTAL_SWITCHER.CREATE_PORTAL') }}
</p>

View File

@@ -1,5 +1,6 @@
<script setup>
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
@@ -30,6 +31,9 @@ const props = defineProps({
default: '',
},
});
const { t } = useI18n();
const hasImageLoaded = ref(false);
const imgError = ref(false);
@@ -108,6 +112,7 @@ const onImgLoad = () => {
</div>
<div
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"
>
<FluentIcon

View File

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

View File

@@ -532,20 +532,20 @@
"BUTTON_LABEL": "New article"
},
"MINE": {
"TITLE": "There are no articles in mine",
"SUBTITLE": "Mine articles will appear here"
"TITLE": "You haven't written any articles here",
"SUBTITLE": "All articles written by you show up here for quick access."
},
"DRAFT": {
"TITLE": "There are no articles in draft",
"TITLE": "There are no articles in drafts",
"SUBTITLE": "Draft articles will appear here"
},
"PUBLISHED": {
"TITLE": "There are no articles in published",
"TITLE": "There are no published articles",
"SUBTITLE": "Published articles will appear here"
},
"ARCHIVED": {
"TITLE": "There are no articles in archived",
"SUBTITLE": "Archived articles will appear here"
"TITLE": "There are no articles in the archive",
"SUBTITLE": "Archived articles don't show up on the portal, you can use it to mark deprecated or outdated pages"
},
"CATEGORY": {
"TITLE": "There are no articles in this category",

View File

@@ -31,7 +31,7 @@ const portalRoutes = [
component: PortalsArticlesIndexPage,
},
{
path: getPortalRoute(':portalSlug/:locale/articles/new'),
path: getPortalRoute(':portalSlug/:locale/:categorySlug?/articles/new'),
name: 'portals_articles_new',
meta: {
permissions: ['administrator', 'agent', 'knowledge_base_manage'],

View File

@@ -300,5 +300,6 @@
"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"
],
"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"
}