feat: Update categories page UI (#13052)

This commit is contained in:
Sivin Varghese
2026-01-07 10:57:46 +05:30
committed by GitHub
parent b7be546934
commit dd426d69eb
6 changed files with 141 additions and 103 deletions
@@ -1,11 +1,10 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useToggle } from '@vueuse/core';
import CardLayout from 'dashboard/components-next/CardLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
const props = defineProps({
id: {
@@ -38,27 +37,6 @@ const emit = defineEmits(['click', 'action']);
const { t } = useI18n();
const [showActionsDropdown, toggleDropdown] = useToggle();
const categoryMenuItems = [
{
label: 'Edit',
action: 'edit',
value: 'edit',
icon: 'i-lucide-pencil',
},
{
label: 'Delete',
action: 'delete',
value: 'delete',
icon: 'i-lucide-trash',
},
];
const categoryTitleWithIcon = computed(() => {
return `${props.icon} ${props.title}`;
});
const description = computed(() => {
return props.description ? props.description : 'No description added';
});
@@ -71,59 +49,68 @@ const handleClick = slug => {
emit('click', slug);
};
const handleAction = ({ action, value }) => {
emit('action', { action, value, id: props.id });
toggleDropdown(false);
const handleAction = ({ action }) => {
emit('action', { action, id: props.id });
};
</script>
<template>
<CardLayout>
<div class="flex w-full gap-2">
<div class="flex justify-between w-full gap-2">
<div class="flex items-center justify-start w-full min-w-0 gap-2">
<span
class="text-base truncate cursor-pointer hover:underline underline-offset-2 hover:text-n-blue-text text-n-slate-12"
@click="handleClick(slug)"
>
{{ categoryTitleWithIcon }}
</span>
<span
class="inline-flex items-center justify-center h-6 px-2 py-1 text-xs text-center border rounded-lg bg-n-slate-1 whitespace-nowrap shrink-0 text-n-slate-11 border-n-slate-4"
>
{{
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_CARD.ARTICLES_COUNT', {
count: articlesCount,
})
}}
</span>
<div class="flex items-start justify-between gap-3 group/categoryCard">
<div
class="size-10 rounded-[0.625rem] mt-1 outline outline-1 outline-n-weak flex items-center justify-center flex-shrink-0"
>
<span v-if="icon" class="text-sm font-420">
{{ icon }}
</span>
<Icon v-else icon="i-lucide-shapes" class="size-4 shrink-0" />
</div>
<div class="flex flex-col w-full gap-2">
<div class="flex justify-between w-full gap-2 h-6">
<div class="flex items-center justify-start min-w-0 gap-2">
<span
class="text-sm font-medium truncate group-hover/categoryCard:text-n-blue-11 cursor-pointer text-n-slate-12"
@click="handleClick(slug)"
>
{{ title }}
</span>
<div class="w-px h-3 bg-n-weak rounded-lg shrink-0" />
<span class="text-sm text-n-slate-11 font-420 shrink-0">
{{
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_CARD.ARTICLES_COUNT', {
count: articlesCount,
})
}}
</span>
</div>
<div class="flex items-center gap-1 shrink-0">
<Button
icon="i-lucide-pen-line"
slate
xs
ghost
class="[&>span]:size-3.5"
@click="handleAction({ action: 'edit' })"
/>
<div class="w-px h-3 bg-n-weak rounded-lg" />
<Button
icon="i-lucide-trash"
slate
xs
ghost
class="[&>span]:size-3.5"
@click="handleAction({ action: 'delete' })"
/>
</div>
</div>
<div
v-on-clickaway="() => toggleDropdown(false)"
class="relative group"
<span
class="text-sm line-clamp-3"
:class="hasDescription ? 'text-n-slate-11' : 'text-n-slate-9'"
>
<Button
icon="i-lucide-ellipsis-vertical"
color="slate"
size="xs"
variant="ghost"
class="rounded-md group-hover:bg-n-alpha-2"
@click="toggleDropdown()"
/>
<DropdownMenu
v-if="showActionsDropdown"
:menu-items="categoryMenuItems"
class="mt-1 ltr:right-0 rtl:left-0 xl:ltr:left-0 xl:rtl:right-0 top-full z-60"
@action="handleAction"
/>
</div>
{{ description }}
</span>
</div>
</div>
<span
class="text-sm line-clamp-3"
:class="hasDescription ? 'text-n-slate-11' : 'text-n-slate-9'"
>
{{ description }}
</span>
</CardLayout>
</template>
@@ -1,5 +1,5 @@
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, useTemplateRef } from 'vue';
import { OnClickOutside } from '@vueuse/components';
import { useRoute } from 'vue-router';
import { useMapGetter } from 'dashboard/composables/store.js';
@@ -45,6 +45,7 @@ const emit = defineEmits(['update:currentPage', 'create']);
const route = useRoute();
const createPortalDialogRef = ref(null);
const createButtonRef = useTemplateRef('createButtonRef');
const showPortalSwitcher = ref(false);
@@ -119,6 +120,16 @@ const onClickCreateButton = () => {
</span>
</div>
</div>
<div ref="createButtonRef" class="relative z-20">
<Button
v-if="createButtonLabel"
:label="createButtonLabel"
icon="i-lucide-plus"
size="sm"
@click="onClickCreateButton"
/>
<slot name="modal" :button-ref="createButtonRef" />
</div>
<Button
v-if="createButtonLabel"
:label="createButtonLabel"
@@ -1,5 +1,7 @@
<script setup>
import { ref, computed } from 'vue';
import { vOnClickOutside } from '@vueuse/components';
import { useStoreGetters } from 'dashboard/composables/store.js';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useStore, useMapGetter } from 'dashboard/composables/store';
@@ -12,6 +14,7 @@ import CategoryHeaderControls from 'dashboard/components-next/HelpCenter/Pages/C
import CategoryEmptyState from 'dashboard/components-next/HelpCenter/EmptyState/Category/CategoryEmptyState.vue';
import EditCategoryDialog from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/EditCategoryDialog.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import CategoryDialog from 'dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryDialog.vue';
const props = defineProps({
categories: {
@@ -37,6 +40,36 @@ const { t } = useI18n();
const editCategoryDialog = ref(null);
const selectedCategory = ref(null);
const isCreateCategoryDialogOpen = ref(false);
const getters = useStoreGetters();
const currentPortalSlug = computed(() => 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 currentPortalName = computed(() => currentPortal.value?.name);
const activeLocale = computed(() => {
return props.allowedLocales.find(
locale => locale.code === route.params.locale
);
});
const activeLocaleName = computed(() => activeLocale.value?.name ?? '');
const activeLocaleCode = computed(() => activeLocale.value?.code ?? '');
const toggleCreateCategoryDialog = () => {
isCreateCategoryDialogOpen.value = !isCreateCategoryDialogOpen.value;
};
const closeCreateCategoryDialog = () => {
isCreateCategoryDialogOpen.value = false;
};
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
const isLoading = computed(() => props.isFetching || isSwitchingPortal.value);
@@ -101,7 +134,28 @@ const handleAction = ({ action, id, category: categoryData }) => {
</script>
<template>
<HelpCenterLayout :show-pagination-footer="false">
<HelpCenterLayout
:header-title="t('HELP_CENTER.CATEGORY_PAGE.HEADER')"
:create-button-label="
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.NEW_CATEGORY')
"
:show-pagination-footer="false"
@create="toggleCreateCategoryDialog"
>
<template #modal="{ buttonRef }">
<CategoryDialog
v-if="isCreateCategoryDialogOpen"
v-on-click-outside="[
closeCreateCategoryDialog,
{ ignore: [buttonRef] },
]"
mode="create"
:portal-name="currentPortalName"
:active-locale-name="activeLocaleName"
:active-locale-code="activeLocaleCode"
@close="closeCreateCategoryDialog"
/>
</template>
<template #header-actions>
<CategoryHeaderControls
:categories="categories"
@@ -33,7 +33,6 @@ const getters = useStoreGetters();
const { t } = useI18n();
const isLocaleMenuOpen = ref(false);
const isCreateCategoryDialogOpen = ref(false);
const isEditCategoryDialogOpen = ref(false);
const currentPortalSlug = computed(() => {
@@ -127,7 +126,19 @@ const handleBreadcrumbClick = () => {
<template>
<div class="flex items-center justify-between w-full">
<div v-if="!hasSelectedCategory" class="flex items-center gap-4">
<div
v-if="!hasSelectedCategory"
class="flex items-center gap-4 w-full justify-between"
>
<span
class="min-w-0 text-sm font-medium truncate text-n-slate-11 ltr:ml-1 rtl:mr-1"
>
{{
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.CATEGORIES_COUNT', {
n: categoriesCount,
})
}}
</span>
<div class="relative group">
<OnClickOutside @trigger="isLocaleMenuOpen = false">
<Button
@@ -142,44 +153,18 @@ const handleBreadcrumbClick = () => {
v-if="isLocaleMenuOpen"
:menu-items="localeMenuItems"
show-search
class="left-0 w-40 mt-2 overflow-y-auto xl:right-0 top-full max-h-60"
class="ltr:right-0 rtl:left-0 w-40 mt-2 overflow-y-auto top-full max-h-60"
@action="handleLocaleAction"
/>
</OnClickOutside>
</div>
<div class="w-px h-3.5 rounded my-auto bg-n-weak" />
<span class="min-w-0 text-sm font-medium truncate text-n-slate-12">
{{
t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.CATEGORIES_COUNT', {
n: categoriesCount,
})
}}
</span>
</div>
<Breadcrumb
v-else
:items="breadcrumbItems"
@click="handleBreadcrumbClick"
/>
<div v-if="!hasSelectedCategory" class="relative">
<OnClickOutside @trigger="isCreateCategoryDialogOpen = false">
<Button
:label="t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.NEW_CATEGORY')"
icon="i-lucide-plus"
size="sm"
@click="isCreateCategoryDialogOpen = !isCreateCategoryDialogOpen"
/>
<CategoryDialog
v-if="isCreateCategoryDialogOpen"
mode="create"
:portal-name="currentPortalName"
:active-locale-name="activeLocaleName"
:active-locale-code="activeLocaleCode"
@close="isCreateCategoryDialogOpen = false"
/>
</OnClickOutside>
</div>
<div v-else class="relative">
<div v-if="hasSelectedCategory" class="relative">
<OnClickOutside @trigger="isEditCategoryDialogOpen = false">
<Button
:label="t('HELP_CENTER.CATEGORY_PAGE.CATEGORY_HEADER.EDIT_CATEGORY')"
@@ -14,13 +14,13 @@ const handleClick = slug => {
emit('click', slug);
};
const handleAction = ({ action, value, id }, category) => {
emit('action', { action, value, id, category });
const handleAction = ({ action, id }, category) => {
emit('action', { action, id, category });
};
</script>
<template>
<ul role="list" class="grid w-full h-full grid-cols-1 gap-4 md:grid-cols-2">
<ul role="list" class="flex w-full h-full flex-col gap-4">
<CategoryCard
v-for="category in categories"
:id="category.id"
@@ -561,6 +561,7 @@
}
},
"CATEGORY_PAGE": {
"HEADER": "Categories",
"CATEGORY_HEADER": {
"NEW_CATEGORY": "New category",
"EDIT_CATEGORY": "Edit category",