From ff4fba379fcdaf4b9dc236a57738bbd10f0e01d1 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Fri, 17 Jul 2026 22:25:14 +0400 Subject: [PATCH] fix(admin): read tenant supportedLocales instead of hardcoding en/ru/hy translation tabs Bug: admin-product-form.component.html and admin-category-form.component.html both `@for (locale of ['en','ru','hy']; ...)` over a fixed literal array instead of the tenant's actual configured locales. A tenant with fewer, more, or differently-ordered supported locales got translation tabs for languages it doesn't support and none for ones it does - same class of bug as the already-fixed general-section/LocaleSyncService gap in project-editor (docs/EDITOR.md), just never wired here at all. Fix: AdminProductsFacade and AdminCategoriesFacade each gained a `supportedLocales` computed (reads ProjectEditorFacade.bootstrap() .localization.supportedLocales, falling back to ['en'] before bootstrap loads) and an `ensureLocalesLoaded()` that calls ProjectEditorFacade.loadBootstrap() if it hasn't loaded yet - same lazy-load pattern AdminDashboardFacade.ensureLoaded() already uses for the same dependency. Both editor page components call ensureLocalesLoaded() in their constructor and pass `[locales]="facade.supportedLocales()"` down to the form components, which now expose a `locales: string[]` @Input() and iterate that instead of the hardcoded array. Verified live via window.ng.getComponent() on /ru/backoffice/{categories,products}/create?devBypassAdmin=true: both facade.supportedLocales() and the form's bound `locales` input now read the real tenant order ['ru','en','hy'] (default locale first, as configured) instead of the previous hardcoded ['en','ru','hy'] - confirmed by the rendered translation-tab order changing accordingly in both admin/products and admin/categories editors. tsc --noEmit clean. --- .../components/admin-category-form.component.html | 2 +- .../components/admin-category-form.component.ts | 1 + .../admin/categories/facade/admin-categories.facade.ts | 10 ++++++++++ .../pages/admin-category-editor-page.component.ts | 3 ++- .../components/admin-product-form.component.html | 2 +- .../components/admin-product-form.component.ts | 1 + .../admin/products/facade/admin-products.facade.ts | 10 ++++++++++ .../pages/admin-product-editor-page.component.ts | 3 ++- 8 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/app/features/admin/categories/components/admin-category-form.component.html b/src/app/features/admin/categories/components/admin-category-form.component.html index 1d5efab..dd7ef48 100644 --- a/src/app/features/admin/categories/components/admin-category-form.component.html +++ b/src/app/features/admin/categories/components/admin-category-form.component.html @@ -38,7 +38,7 @@

{{ 'adminProducts.translations' | translate }}

- @for (locale of ['en','ru','hy']; track locale) { + @for (locale of locales; track locale) {
diff --git a/src/app/features/admin/categories/components/admin-category-form.component.ts b/src/app/features/admin/categories/components/admin-category-form.component.ts index 254969d..9a33f42 100644 --- a/src/app/features/admin/categories/components/admin-category-form.component.ts +++ b/src/app/features/admin/categories/components/admin-category-form.component.ts @@ -21,6 +21,7 @@ export class AdminCategoryFormComponent { @Input() parentOptions: AdminCategory[] = []; @Input() breadcrumb: string[] = []; @Input() slugTaken = false; + @Input() locales: string[] = ['en']; @Input() mode: 'create' | 'edit' = 'create'; @Output() categoryChange = new EventEmitter>(); diff --git a/src/app/features/admin/categories/facade/admin-categories.facade.ts b/src/app/features/admin/categories/facade/admin-categories.facade.ts index 689bcea..b9a96ba 100644 --- a/src/app/features/admin/categories/facade/admin-categories.facade.ts +++ b/src/app/features/admin/categories/facade/admin-categories.facade.ts @@ -4,6 +4,7 @@ import { AdminCategory, AdminCategoryEditorMode, AdminCategoryListFilters } from import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory'; import { AdminCategoriesLocalGateway } from '../services/admin-categories-local.gateway'; import { LocalStorageService } from '../../../../core/storage/local-storage.service'; +import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade'; const DRAFT_KEY_PREFIX = 'admin-category-draft:'; const NEW_CATEGORY_DRAFT_KEY = `${DRAFT_KEY_PREFIX}new`; @@ -13,6 +14,15 @@ export class AdminCategoriesFacade { private readonly gateway = inject(AdminCategoriesLocalGateway); private readonly formFactory = inject(AdminCategoriesFormFactory); private readonly localStorage = inject(LocalStorageService); + private readonly projectEditor = inject(ProjectEditorFacade); + + readonly supportedLocales = computed(() => this.projectEditor.bootstrap()?.localization.supportedLocales ?? ['en']); + + ensureLocalesLoaded(): void { + if (!this.projectEditor.bootstrap()) { + this.projectEditor.loadBootstrap(); + } + } readonly filters = signal({ search: '', visibility: 'all', includeDeleted: false }); readonly categories = signal([]); diff --git a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts index 118efa0..778266c 100644 --- a/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts +++ b/src/app/features/admin/categories/pages/admin-category-editor-page.component.ts @@ -9,7 +9,7 @@ import { LanguageService } from '../../../../services/language.service'; selector: 'app-admin-category-editor-page', standalone: true, imports: [AdminCategoryFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -32,6 +32,7 @@ export class AdminCategoryEditorPageComponent { if (this.facade.categories().length === 0) { this.facade.loadList(); } + this.facade.ensureLocalesLoaded(); if (!id) { this.facade.startCreate(); } else { diff --git a/src/app/features/admin/products/components/admin-product-form.component.html b/src/app/features/admin/products/components/admin-product-form.component.html index 8a107ba..ac70505 100644 --- a/src/app/features/admin/products/components/admin-product-form.component.html +++ b/src/app/features/admin/products/components/admin-product-form.component.html @@ -71,7 +71,7 @@

{{ 'adminProducts.translations' | translate }}

- @for (locale of ['en','ru','hy']; track locale) { + @for (locale of locales; track locale) {
diff --git a/src/app/features/admin/products/components/admin-product-form.component.ts b/src/app/features/admin/products/components/admin-product-form.component.ts index 91e1bdb..c583490 100644 --- a/src/app/features/admin/products/components/admin-product-form.component.ts +++ b/src/app/features/admin/products/components/admin-product-form.component.ts @@ -20,6 +20,7 @@ export class AdminProductFormComponent { @Input({ required: true }) product!: AdminProduct; @Input() categories: AdminProductCategoryOption[] = []; @Input() allProducts: AdminProduct[] = []; + @Input() locales: string[] = ['en']; @Input() mode: 'create' | 'edit' | 'duplicate' = 'create'; protected mediaPickerOpen = false; diff --git a/src/app/features/admin/products/facade/admin-products.facade.ts b/src/app/features/admin/products/facade/admin-products.facade.ts index 848c689..5746f14 100644 --- a/src/app/features/admin/products/facade/admin-products.facade.ts +++ b/src/app/features/admin/products/facade/admin-products.facade.ts @@ -3,11 +3,21 @@ import { take } from 'rxjs/operators'; import { AdminProduct, AdminProductCategoryOption, AdminProductEditorMode, AdminProductListFilters } from '../models/admin-product.model'; import { AdminProductsFormFactory } from '../services/admin-products-form.factory'; import { AdminProductsLocalGateway } from '../services/admin-products-local.gateway'; +import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade'; @Injectable({ providedIn: 'root' }) export class AdminProductsFacade { private readonly gateway = inject(AdminProductsLocalGateway); private readonly formFactory = inject(AdminProductsFormFactory); + private readonly projectEditor = inject(ProjectEditorFacade); + + readonly supportedLocales = computed(() => this.projectEditor.bootstrap()?.localization.supportedLocales ?? ['en']); + + ensureLocalesLoaded(): void { + if (!this.projectEditor.bootstrap()) { + this.projectEditor.loadBootstrap(); + } + } readonly filters = signal({ search: '', diff --git a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts index 46bd843..08e2983 100644 --- a/src/app/features/admin/products/pages/admin-product-editor-page.component.ts +++ b/src/app/features/admin/products/pages/admin-product-editor-page.component.ts @@ -9,7 +9,7 @@ import { LanguageService } from '../../../../services/language.service'; selector: 'app-admin-product-editor-page', standalone: true, imports: [AdminProductFormComponent, TranslatePipe], - template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, + template: `@if (facade.draft(); as draft) {

{{ title() | translate }}

} @else {

{{ 'common.loading' | translate }}

}`, styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], changeDetection: ChangeDetectionStrategy.OnPush }) @@ -22,6 +22,7 @@ export class AdminProductEditorPageComponent { constructor() { this.facade.loadCategories(); + this.facade.ensureLocalesLoaded(); const id = this.route.snapshot.paramMap.get('id'); const mode = this.route.snapshot.routeConfig?.path?.includes('duplicate') ? 'duplicate' : this.route.snapshot.routeConfig?.path?.includes('edit') ? 'edit' : 'create'; if (mode === 'create') {