feat(static-pages): CRUD completion + search/filter/bulk actions
Milestone 2 of the Static Pages Module sprint. - StaticPagesEditorComponent: duplicate page, confirm-before-delete/bulk- delete (matches the resetDraft confirm pattern), route/enabled/customTemplate/ media(hero/thumbnail/gallery) fields wired into the card, per-page publish/ unpublish action, status + duplicate-route/invalid-html/invalid-seo badges. - Search (id/slug/route/title across all locales), filter by status (draft/published) and by locale (hides pages missing a translation for the selected locale) - all local computed() filters, no new service. - Bulk selection (per-row + select-all-visible checkboxes) with bulk delete/ enable/disable/publish/unpublish, one updateBootstrap() call each. - Correctness note: introduced `allPages` (unfiltered) vs `pages` (filtered view) computeds. Every mutation (create/duplicate/delete/move/bulk) reads from allPages(), never the filtered pages() - reading from the filtered view would have silently deleted whatever an active search/filter hid on the next persist(). Documented inline on persist() as a guardrail for future edits. - Fixed a template compile error found by the build gate: Angular templates don't support inline arrow functions in binding expressions ((ngModelChange)="...map(v => v.trim())..." failed to parse) - moved the gallery CSV-parsing into a component method (updateGallery). - SEO robots field added to the page card (validated against a known-token set from M1). - i18n: staticPages.* extended (search/filter/bulk/route/enabled/status/ media/robots/disabled labels) across the interface + en/ru/hy. Gate: tsc --noEmit, npm test (57/57), arch:check, build all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -719,6 +719,33 @@ export const en: Translations = {
|
||||
backHome: 'Back to home',
|
||||
duplicateSlug: 'Duplicate slugs are not allowed.',
|
||||
emptyTitle: 'Each page must have at least one title.',
|
||||
duplicateRoute: 'Two or more pages share the same route.',
|
||||
invalidHtml: 'This page has malformed HTML content.',
|
||||
invalidSeo: 'This page has an invalid SEO field (canonical/OG image URL or robots value).',
|
||||
searchPlaceholder: 'Search pages…',
|
||||
filterAll: 'All statuses',
|
||||
filterAllLocales: 'All languages',
|
||||
confirmDeletePage: 'Delete this page? This cannot be undone.',
|
||||
confirmBulkDelete: 'Delete the selected pages? This cannot be undone.',
|
||||
duplicatePage: 'Duplicate',
|
||||
selectAll: 'Select all',
|
||||
selectedCount: 'selected',
|
||||
bulkDelete: 'Delete selected',
|
||||
bulkEnable: 'Enable selected',
|
||||
bulkDisable: 'Disable selected',
|
||||
bulkPublishAction: 'Publish selected',
|
||||
bulkUnpublish: 'Unpublish selected',
|
||||
routeLabel: 'Route',
|
||||
enabledLabel: 'Enabled',
|
||||
statusLabel: 'Status',
|
||||
publishPageAction: 'Publish page',
|
||||
unpublishPageAction: 'Unpublish page',
|
||||
heroImageLabel: 'Hero image',
|
||||
thumbnailLabel: 'Thumbnail',
|
||||
galleryLabel: 'Gallery',
|
||||
customTemplateLabel: 'Custom template',
|
||||
seoRobotsLabel: 'Robots',
|
||||
disabledBadge: 'Disabled',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Widget unavailable',
|
||||
|
||||
@@ -719,6 +719,33 @@ export const hy: Translations = {
|
||||
backHome: 'Վերադառնալ գլխավոր',
|
||||
duplicateSlug: 'Կրկնվող slug-երը թույլատրելի չեն։',
|
||||
emptyTitle: 'Յուրաքանչյուր էջ պետք է ունենա առնվազն մեկ վերնագիր։',
|
||||
duplicateRoute: 'Երկու կամ ավելի էջ ունեն նույն երթուղին։',
|
||||
invalidHtml: 'Այս էջը պարունակում է սխալ HTML բովանդակություն։',
|
||||
invalidSeo: 'Այս էջն ունի անվավեր SEO դաշտ (canonical/OG պատկեր կամ robots արժեք)։',
|
||||
searchPlaceholder: 'Փնտրել էջեր…',
|
||||
filterAll: 'Բոլոր կարգավիճակները',
|
||||
filterAllLocales: 'Բոլոր լեզուները',
|
||||
confirmDeletePage: 'Ջնջե՞լ այս էջը։ Հնարավոր չէ հետարկել։',
|
||||
confirmBulkDelete: 'Ջնջե՞լ ընտրված էջերը։ Հնարավոր չէ հետարկել։',
|
||||
duplicatePage: 'Կրկնօրինակել',
|
||||
selectAll: 'Ընտրել բոլորը',
|
||||
selectedCount: 'ընտրված է',
|
||||
bulkDelete: 'Ջնջել ընտրվածները',
|
||||
bulkEnable: 'Միացնել ընտրվածները',
|
||||
bulkDisable: 'Անջատել ընտրվածները',
|
||||
bulkPublishAction: 'Հրապարակել ընտրվածները',
|
||||
bulkUnpublish: 'Հանել հրապարակումից ընտրվածները',
|
||||
routeLabel: 'Երթուղի',
|
||||
enabledLabel: 'Միացված է',
|
||||
statusLabel: 'Կարգավիճակ',
|
||||
publishPageAction: 'Հրապարակել էջը',
|
||||
unpublishPageAction: 'Հանել էջը հրապարակումից',
|
||||
heroImageLabel: 'Գլխավոր պատկեր',
|
||||
thumbnailLabel: 'Մանրապատկեր',
|
||||
galleryLabel: 'Պատկերասրահ',
|
||||
customTemplateLabel: 'Հատուկ ձևանմուշ',
|
||||
seoRobotsLabel: 'Robots',
|
||||
disabledBadge: 'Անջատված',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Վիջեթը հասանելի չէ',
|
||||
|
||||
@@ -719,6 +719,33 @@ export const ru: Translations = {
|
||||
backHome: 'На главную',
|
||||
duplicateSlug: 'Дублирующиеся slug запрещены.',
|
||||
emptyTitle: 'У каждой страницы должен быть хотя бы один заголовок.',
|
||||
duplicateRoute: 'Два или более страниц используют один и тот же маршрут.',
|
||||
invalidHtml: 'На этой странице некорректный HTML-контент.',
|
||||
invalidSeo: 'У этой страницы некорректное SEO-поле (canonical/OG-изображение или значение robots).',
|
||||
searchPlaceholder: 'Поиск страниц…',
|
||||
filterAll: 'Все статусы',
|
||||
filterAllLocales: 'Все языки',
|
||||
confirmDeletePage: 'Удалить эту страницу? Это действие нельзя отменить.',
|
||||
confirmBulkDelete: 'Удалить выбранные страницы? Это действие нельзя отменить.',
|
||||
duplicatePage: 'Дублировать',
|
||||
selectAll: 'Выбрать все',
|
||||
selectedCount: 'выбрано',
|
||||
bulkDelete: 'Удалить выбранные',
|
||||
bulkEnable: 'Включить выбранные',
|
||||
bulkDisable: 'Отключить выбранные',
|
||||
bulkPublishAction: 'Опубликовать выбранные',
|
||||
bulkUnpublish: 'Снять с публикации выбранные',
|
||||
routeLabel: 'Маршрут',
|
||||
enabledLabel: 'Включена',
|
||||
statusLabel: 'Статус',
|
||||
publishPageAction: 'Опубликовать страницу',
|
||||
unpublishPageAction: 'Снять страницу с публикации',
|
||||
heroImageLabel: 'Главное изображение',
|
||||
thumbnailLabel: 'Миниатюра',
|
||||
galleryLabel: 'Галерея',
|
||||
customTemplateLabel: 'Пользовательский шаблон',
|
||||
seoRobotsLabel: 'Robots',
|
||||
disabledBadge: 'Отключена',
|
||||
},
|
||||
widgets: {
|
||||
unavailable: 'Виджет недоступен',
|
||||
|
||||
@@ -717,6 +717,33 @@ export interface Translations {
|
||||
backHome: string;
|
||||
duplicateSlug: string;
|
||||
emptyTitle: string;
|
||||
duplicateRoute: string;
|
||||
invalidHtml: string;
|
||||
invalidSeo: string;
|
||||
searchPlaceholder: string;
|
||||
filterAll: string;
|
||||
filterAllLocales: string;
|
||||
confirmDeletePage: string;
|
||||
confirmBulkDelete: string;
|
||||
duplicatePage: string;
|
||||
selectAll: string;
|
||||
selectedCount: string;
|
||||
bulkDelete: string;
|
||||
bulkEnable: string;
|
||||
bulkDisable: string;
|
||||
bulkPublishAction: string;
|
||||
bulkUnpublish: string;
|
||||
routeLabel: string;
|
||||
enabledLabel: string;
|
||||
statusLabel: string;
|
||||
publishPageAction: string;
|
||||
unpublishPageAction: string;
|
||||
heroImageLabel: string;
|
||||
thumbnailLabel: string;
|
||||
galleryLabel: string;
|
||||
customTemplateLabel: string;
|
||||
seoRobotsLabel: string;
|
||||
disabledBadge: string;
|
||||
};
|
||||
widgets: {
|
||||
unavailable: string;
|
||||
|
||||
Reference in New Issue
Block a user