feat(categories): implement professional category management experience
Categories dashboard (real total/visible/hidden/empty/root/subcategory/missing-image/missing-SEO/last-modified/completion% stats, recommended next action); tree view is now a real expand/collapse hierarchy (state persisted via LocalStorageService) with keyboard navigation (arrow keys, jump-to-parent), search that force-expands and keeps only matching branches + their ancestors/descendants instead of silently dropping deep matches; added Table/Cards views alongside the tree with density and saved column visibility; real bulk actions (show/hide/delete/assign parent/assign image via the Media Library picker/duplicate via the existing createCategory call/CSV export) plus the pre-existing single-item actions; reusable CategoryHealthWidget (image/SEO/description/valid-parent/visibility/products-assigned + completion%); editor reorganized into General/Media/SEO/Visibility/Navigation/Attributes/Advanced tabs, media now uses the shared ImageFieldComponent, SEO explains fields in plain language with a live preview, Navigation tab shows real breadcrumb/children/visibility-based nav status, Attributes uses the shared KeyValueEditorComponent. Filled in the adminCategories i18n namespace (previously only 2 of ~90 referenced keys existed) across en/ru/hy. Also fixed the pre-existing bug where a filtered/searched category list could silently drop a matched descendant whose ancestor's title didn't match, by loading the full catalog once and filtering client-side.
This commit is contained in:
@@ -1221,6 +1221,84 @@ export const en: Translations = {
|
||||
adminCategories: {
|
||||
chooseImage: 'Choose image',
|
||||
replaceImage: 'Replace image',
|
||||
search: 'Search categories…',
|
||||
showDeleted: 'Show deleted',
|
||||
create: 'Create category',
|
||||
edit: 'Edit category',
|
||||
title: 'Title',
|
||||
slug: 'URL slug',
|
||||
slugTaken: 'This slug is already used by another category.',
|
||||
parent: 'Parent category',
|
||||
noParent: 'No parent (top level)',
|
||||
icon: 'Icon',
|
||||
image: 'Image',
|
||||
imageHint: 'Shown in category listings and navigation.',
|
||||
description: 'Description',
|
||||
items: 'products',
|
||||
status: { draft: 'Draft', published: 'Published' },
|
||||
restore: 'Restore',
|
||||
saveDraft: 'Save draft',
|
||||
publish: 'Publish',
|
||||
confirmDelete: 'Delete this category? This cannot be undone.',
|
||||
confirmLeaveUnsaved: 'You have unsaved changes. Leave without saving?',
|
||||
deleteBlocked: 'This category can\'t be deleted while it has subcategories or assigned products.',
|
||||
emptyTitle: 'No categories yet',
|
||||
emptyDescription: 'Create your first category to start organizing products.',
|
||||
emptyGuide: 'Categories help customers browse and find products. Start with a few broad top-level categories (e.g. Clothing, Electronics), then add subcategories as your catalog grows — avoid nesting more than 2-3 levels deep.',
|
||||
viewTree: 'Tree',
|
||||
viewCards: 'Cards',
|
||||
expandAll: 'Expand all',
|
||||
collapseAll: 'Collapse all',
|
||||
expandNode: 'Expand',
|
||||
collapseNode: 'Collapse',
|
||||
jumpToParent: 'Jump to parent',
|
||||
deletedBadge: 'Deleted',
|
||||
column_slug: 'Slug',
|
||||
column_items: 'Products',
|
||||
column_status: 'Status',
|
||||
column_visibility: 'Visibility',
|
||||
column_updated: 'Last updated',
|
||||
bulkAssignParent: 'Assign parent',
|
||||
bulkAssignImage: 'Assign image',
|
||||
groupGeneral: 'General',
|
||||
groupMedia: 'Media',
|
||||
groupSeo: 'SEO',
|
||||
groupVisibility: 'Visibility',
|
||||
groupNavigation: 'Navigation',
|
||||
groupAttributes: 'Attributes',
|
||||
groupAdvanced: 'Advanced',
|
||||
visibilityExplain: 'A hidden category and its products stay out of the storefront navigation and search until you make it visible again.',
|
||||
navBreadcrumb: 'Breadcrumb',
|
||||
navRoot: 'This is a top-level category.',
|
||||
navChildren: 'Subcategories',
|
||||
navNoChildren: 'No subcategories yet.',
|
||||
navAppearsIn: 'Where this appears',
|
||||
navAppearsVisible: 'Visible in catalog navigation and search.',
|
||||
navAppearsHidden: 'Hidden — not shown in catalog navigation while hidden.',
|
||||
itemsCountLabel: 'Assigned products',
|
||||
createdAtLabel: 'Created',
|
||||
healthImage: 'Image',
|
||||
healthSeo: 'SEO',
|
||||
healthDescription: 'Description',
|
||||
healthParent: 'Parent',
|
||||
healthVisibility: 'Visibility',
|
||||
healthProducts: 'Products assigned',
|
||||
totalCategories: 'Total categories',
|
||||
visibleCount: 'Visible',
|
||||
hiddenCount: 'Hidden',
|
||||
emptyCount: 'Empty',
|
||||
rootCount: 'Root categories',
|
||||
subCount: 'Subcategories',
|
||||
missingImagesCount: 'Missing images',
|
||||
missingSeoCount: 'Missing SEO',
|
||||
lastModified: 'Last modified',
|
||||
lastModifiedNever: 'Never',
|
||||
completionLabel: 'Completion',
|
||||
recommendedNext: 'Recommended next step',
|
||||
recommendAddImage: 'A category has no image — add one so it looks trustworthy in listings.',
|
||||
recommendAddSeo: 'A category is missing a search title or description — add one so it can be found.',
|
||||
recommendFillEmpty: 'A category has no products assigned — add products or consider hiding it.',
|
||||
recommendNone: 'Nice work — your category structure is in good shape.',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'No products found',
|
||||
|
||||
@@ -1216,6 +1216,84 @@ export const hy: Translations = {
|
||||
adminCategories: {
|
||||
chooseImage: 'Ընտրել պատկեր',
|
||||
replaceImage: 'Փոխարինել պատկերը',
|
||||
search: 'Փնտրել կատեգորիաներ…',
|
||||
showDeleted: 'Ցույց տալ ջնջվածները',
|
||||
create: 'Ստեղծել կատեգորիա',
|
||||
edit: 'Խմբագրել կատեգորիան',
|
||||
title: 'Անուն',
|
||||
slug: 'URL հասցե',
|
||||
slugTaken: 'Այս հասցեն արդեն օգտագործվում է այլ կատեգորիայի կողմից։',
|
||||
parent: 'Ծնող կատեգորիա',
|
||||
noParent: 'Առանց ծնողի (վերին մակարդակ)',
|
||||
icon: 'Պատկերակ',
|
||||
image: 'Պատկեր',
|
||||
imageHint: 'Ցուցադրվում է կատեգորիաների ցանկերում և նավիգացիայում։',
|
||||
description: 'Նկարագրություն',
|
||||
items: 'ապրանք',
|
||||
status: { draft: 'Սևագիր', published: 'Հրապարակված' },
|
||||
restore: 'Վերականգնել',
|
||||
saveDraft: 'Պահպանել սևագիրը',
|
||||
publish: 'Հրապարակել',
|
||||
confirmDelete: 'Ջնջե՞լ այս կատեգորիան։ Հնարավոր չէ հետարկել։',
|
||||
confirmLeaveUnsaved: 'Դուք ունեք չպահպանված փոփոխություններ։ Դո՞ւրս գալ առանց պահպանելու։',
|
||||
deleteBlocked: 'Այս կատեգորիան հնարավոր չէ ջնջել, քանի դեռ ունի ենթակատեգորիաներ կամ նշանակված ապրանքներ։',
|
||||
emptyTitle: 'Կատեգորիաներ դեռ չկան',
|
||||
emptyDescription: 'Ստեղծեք առաջին կատեգորիան՝ ապրանքները կազմակերպելու համար։',
|
||||
emptyGuide: 'Կատեգորիաները օգնում են հաճախորդներին գտնել ապրանքներ։ Սկսեք մի քանի լայն վերին մակարդակի կատեգորիաներից (օր․՝ Հագուստ, Էլեկտրոնիկա), ապա ավելացրեք ենթակատեգորիաներ՝ կատալոգի աճին զուգահեռ․ խուսափեք 2-3 մակարդակից ավելի խորությունից։',
|
||||
viewTree: 'Ծառ',
|
||||
viewCards: 'Քարտեր',
|
||||
expandAll: 'Ընդարձակել բոլորը',
|
||||
collapseAll: 'Ծալել բոլորը',
|
||||
expandNode: 'Ընդարձակել',
|
||||
collapseNode: 'Ծալել',
|
||||
jumpToParent: 'Անցնել ծնողին',
|
||||
deletedBadge: 'Ջնջված',
|
||||
column_slug: 'Հասցե',
|
||||
column_items: 'Ապրանքներ',
|
||||
column_status: 'Կարգավիճակ',
|
||||
column_visibility: 'Տեսանելիություն',
|
||||
column_updated: 'Վերջին փոփոխություն',
|
||||
bulkAssignParent: 'Նշանակել ծնող',
|
||||
bulkAssignImage: 'Նշանակել պատկեր',
|
||||
groupGeneral: 'Հիմնական',
|
||||
groupMedia: 'Մեդիա',
|
||||
groupSeo: 'SEO',
|
||||
groupVisibility: 'Տեսանելիություն',
|
||||
groupNavigation: 'Նավիգացիա',
|
||||
groupAttributes: 'Հատկանիշներ',
|
||||
groupAdvanced: 'Լրացուցիչ',
|
||||
visibilityExplain: 'Թաքցված կատեգորիան և դրա ապրանքները չեն երևում խանութի նավիգացիայում և որոնման մեջ, մինչև այն նորից տեսանելի դարձնեք։',
|
||||
navBreadcrumb: 'Ուղենիշ',
|
||||
navRoot: 'Սա վերին մակարդակի կատեգորիա է։',
|
||||
navChildren: 'Ենթակատեգորիաներ',
|
||||
navNoChildren: 'Ենթակատեգորիաներ դեռ չկան։',
|
||||
navAppearsIn: 'Որտեղ է երևում',
|
||||
navAppearsVisible: 'Տեսանելի է կատալոգի նավիգացիայում և որոնման մեջ։',
|
||||
navAppearsHidden: 'Թաքցված է․ չի երևում կատալոգի նավիգացիայում, քանի դեռ թաքցված է։',
|
||||
itemsCountLabel: 'Նշանակված ապրանքներ',
|
||||
createdAtLabel: 'Ստեղծված է',
|
||||
healthImage: 'Պատկեր',
|
||||
healthSeo: 'SEO',
|
||||
healthDescription: 'Նկարագրություն',
|
||||
healthParent: 'Ծնող',
|
||||
healthVisibility: 'Տեսանելիություն',
|
||||
healthProducts: 'Նշանակված ապրանքներ',
|
||||
totalCategories: 'Ընդհանուր կատեգորիաներ',
|
||||
visibleCount: 'Տեսանելի',
|
||||
hiddenCount: 'Թաքցված',
|
||||
emptyCount: 'Դատարկ',
|
||||
rootCount: 'Արմատային կատեգորիաներ',
|
||||
subCount: 'Ենթակատեգորիաներ',
|
||||
missingImagesCount: 'Առանց պատկերի',
|
||||
missingSeoCount: 'Առանց SEO-ի',
|
||||
lastModified: 'Վերջին փոփոխություն',
|
||||
lastModifiedNever: 'Երբեք',
|
||||
completionLabel: 'Ավարտվածություն',
|
||||
recommendedNext: 'Հաջորդ առաջարկվող քայլը',
|
||||
recommendAddImage: 'Կատեգորիան պատկեր չունի․ ավելացրեք մեկը՝ վստահելիության համար։',
|
||||
recommendAddSeo: 'Կատեգորիան չունի որոնման վերնագիր կամ նկարագրություն․ ավելացրեք դրանք։',
|
||||
recommendFillEmpty: 'Կատեգորիան ապրանքներ չունի․ ավելացրեք ապրանքներ կամ թաքցրեք այն։',
|
||||
recommendNone: 'Հիանալի է․ կատեգորիաների կառուցվածքը լավ վիճակում է։',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'Ապրանքներ չեն գտնվել',
|
||||
|
||||
@@ -1216,6 +1216,84 @@ export const ru: Translations = {
|
||||
adminCategories: {
|
||||
chooseImage: 'Выбрать изображение',
|
||||
replaceImage: 'Заменить изображение',
|
||||
search: 'Поиск категорий…',
|
||||
showDeleted: 'Показать удалённые',
|
||||
create: 'Создать категорию',
|
||||
edit: 'Редактировать категорию',
|
||||
title: 'Название',
|
||||
slug: 'URL-адрес',
|
||||
slugTaken: 'Этот адрес уже используется другой категорией.',
|
||||
parent: 'Родительская категория',
|
||||
noParent: 'Без родителя (верхний уровень)',
|
||||
icon: 'Иконка',
|
||||
image: 'Изображение',
|
||||
imageHint: 'Показывается в списках категорий и навигации.',
|
||||
description: 'Описание',
|
||||
items: 'товаров',
|
||||
status: { draft: 'Черновик', published: 'Опубликовано' },
|
||||
restore: 'Восстановить',
|
||||
saveDraft: 'Сохранить черновик',
|
||||
publish: 'Опубликовать',
|
||||
confirmDelete: 'Удалить эту категорию? Это действие нельзя отменить.',
|
||||
confirmLeaveUnsaved: 'У вас есть несохранённые изменения. Выйти без сохранения?',
|
||||
deleteBlocked: 'Эту категорию нельзя удалить, пока у неё есть подкатегории или назначенные товары.',
|
||||
emptyTitle: 'Категорий пока нет',
|
||||
emptyDescription: 'Создайте первую категорию, чтобы начать организовывать товары.',
|
||||
emptyGuide: 'Категории помогают покупателям находить товары. Начните с нескольких верхнеуровневых категорий (например, Одежда, Электроника), затем добавляйте подкатегории по мере роста каталога — избегайте вложенности глубже 2-3 уровней.',
|
||||
viewTree: 'Дерево',
|
||||
viewCards: 'Карточки',
|
||||
expandAll: 'Развернуть всё',
|
||||
collapseAll: 'Свернуть всё',
|
||||
expandNode: 'Развернуть',
|
||||
collapseNode: 'Свернуть',
|
||||
jumpToParent: 'К родителю',
|
||||
deletedBadge: 'Удалена',
|
||||
column_slug: 'Адрес',
|
||||
column_items: 'Товары',
|
||||
column_status: 'Статус',
|
||||
column_visibility: 'Видимость',
|
||||
column_updated: 'Последнее изменение',
|
||||
bulkAssignParent: 'Назначить родителя',
|
||||
bulkAssignImage: 'Назначить изображение',
|
||||
groupGeneral: 'Основное',
|
||||
groupMedia: 'Медиа',
|
||||
groupSeo: 'SEO',
|
||||
groupVisibility: 'Видимость',
|
||||
groupNavigation: 'Навигация',
|
||||
groupAttributes: 'Атрибуты',
|
||||
groupAdvanced: 'Дополнительно',
|
||||
visibilityExplain: 'Скрытая категория и её товары не отображаются в навигации и поиске магазина, пока вы снова не сделаете её видимой.',
|
||||
navBreadcrumb: 'Хлебные крошки',
|
||||
navRoot: 'Это категория верхнего уровня.',
|
||||
navChildren: 'Подкатегории',
|
||||
navNoChildren: 'Подкатегорий пока нет.',
|
||||
navAppearsIn: 'Где это отображается',
|
||||
navAppearsVisible: 'Видна в навигации каталога и поиске.',
|
||||
navAppearsHidden: 'Скрыта — не отображается в навигации каталога, пока скрыта.',
|
||||
itemsCountLabel: 'Назначенные товары',
|
||||
createdAtLabel: 'Создана',
|
||||
healthImage: 'Изображение',
|
||||
healthSeo: 'SEO',
|
||||
healthDescription: 'Описание',
|
||||
healthParent: 'Родитель',
|
||||
healthVisibility: 'Видимость',
|
||||
healthProducts: 'Назначены товары',
|
||||
totalCategories: 'Всего категорий',
|
||||
visibleCount: 'Видимые',
|
||||
hiddenCount: 'Скрытые',
|
||||
emptyCount: 'Пустые',
|
||||
rootCount: 'Корневые категории',
|
||||
subCount: 'Подкатегории',
|
||||
missingImagesCount: 'Без изображений',
|
||||
missingSeoCount: 'Без SEO',
|
||||
lastModified: 'Последнее изменение',
|
||||
lastModifiedNever: 'Никогда',
|
||||
completionLabel: 'Готовность',
|
||||
recommendedNext: 'Рекомендуемый следующий шаг',
|
||||
recommendAddImage: 'У категории нет изображения — добавьте его для доверия в листингах.',
|
||||
recommendAddSeo: 'У категории нет заголовка или описания для поиска — добавьте их.',
|
||||
recommendFillEmpty: 'В категории нет товаров — добавьте товары или скройте категорию.',
|
||||
recommendNone: 'Отлично — структура категорий в хорошем состоянии.',
|
||||
},
|
||||
adminProducts: {
|
||||
emptyTitle: 'Товары не найдены',
|
||||
|
||||
@@ -1228,6 +1228,84 @@ export interface Translations {
|
||||
adminCategories: {
|
||||
chooseImage: string;
|
||||
replaceImage: string;
|
||||
search: string;
|
||||
showDeleted: string;
|
||||
create: string;
|
||||
edit: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
slugTaken: string;
|
||||
parent: string;
|
||||
noParent: string;
|
||||
icon: string;
|
||||
image: string;
|
||||
imageHint: string;
|
||||
description: string;
|
||||
items: string;
|
||||
status: { draft: string; published: string };
|
||||
restore: string;
|
||||
saveDraft: string;
|
||||
publish: string;
|
||||
confirmDelete: string;
|
||||
confirmLeaveUnsaved: string;
|
||||
deleteBlocked: string;
|
||||
emptyTitle: string;
|
||||
emptyDescription: string;
|
||||
emptyGuide: string;
|
||||
viewTree: string;
|
||||
viewCards: string;
|
||||
expandAll: string;
|
||||
collapseAll: string;
|
||||
expandNode: string;
|
||||
collapseNode: string;
|
||||
jumpToParent: string;
|
||||
deletedBadge: string;
|
||||
column_slug: string;
|
||||
column_items: string;
|
||||
column_status: string;
|
||||
column_visibility: string;
|
||||
column_updated: string;
|
||||
bulkAssignParent: string;
|
||||
bulkAssignImage: string;
|
||||
groupGeneral: string;
|
||||
groupMedia: string;
|
||||
groupSeo: string;
|
||||
groupVisibility: string;
|
||||
groupNavigation: string;
|
||||
groupAttributes: string;
|
||||
groupAdvanced: string;
|
||||
visibilityExplain: string;
|
||||
navBreadcrumb: string;
|
||||
navRoot: string;
|
||||
navChildren: string;
|
||||
navNoChildren: string;
|
||||
navAppearsIn: string;
|
||||
navAppearsVisible: string;
|
||||
navAppearsHidden: string;
|
||||
itemsCountLabel: string;
|
||||
createdAtLabel: string;
|
||||
healthImage: string;
|
||||
healthSeo: string;
|
||||
healthDescription: string;
|
||||
healthParent: string;
|
||||
healthVisibility: string;
|
||||
healthProducts: string;
|
||||
totalCategories: string;
|
||||
visibleCount: string;
|
||||
hiddenCount: string;
|
||||
emptyCount: string;
|
||||
rootCount: string;
|
||||
subCount: string;
|
||||
missingImagesCount: string;
|
||||
missingSeoCount: string;
|
||||
lastModified: string;
|
||||
lastModifiedNever: string;
|
||||
completionLabel: string;
|
||||
recommendedNext: string;
|
||||
recommendAddImage: string;
|
||||
recommendAddSeo: string;
|
||||
recommendFillEmpty: string;
|
||||
recommendNone: string;
|
||||
};
|
||||
adminProducts: {
|
||||
emptyTitle: string;
|
||||
|
||||
Reference in New Issue
Block a user