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.
This commit is contained in:
sdarbinyan
2026-07-17 22:25:14 +04:00
parent cb3a6ac98a
commit ff4fba379f
8 changed files with 28 additions and 4 deletions

View File

@@ -38,7 +38,7 @@
</app-form-field> </app-form-field>
<h3>{{ 'adminProducts.translations' | translate }}</h3> <h3>{{ 'adminProducts.translations' | translate }}</h3>
@for (locale of ['en','ru','hy']; track locale) { @for (locale of locales; track locale) {
<div class="grid two sub-block"> <div class="grid two sub-block">
<app-form-field [label]="(('adminCategories.title' | translate) + ' ' + locale)"> <app-form-field [label]="(('adminCategories.title' | translate) + ' ' + locale)">
<app-input [ngModel]="category.translations[locale]?.title || ''" (ngModelChange)="updateTranslation(locale, 'title', $event)" /> <app-input [ngModel]="category.translations[locale]?.title || ''" (ngModelChange)="updateTranslation(locale, 'title', $event)" />

View File

@@ -21,6 +21,7 @@ export class AdminCategoryFormComponent {
@Input() parentOptions: AdminCategory[] = []; @Input() parentOptions: AdminCategory[] = [];
@Input() breadcrumb: string[] = []; @Input() breadcrumb: string[] = [];
@Input() slugTaken = false; @Input() slugTaken = false;
@Input() locales: string[] = ['en'];
@Input() mode: 'create' | 'edit' = 'create'; @Input() mode: 'create' | 'edit' = 'create';
@Output() categoryChange = new EventEmitter<Partial<AdminCategory>>(); @Output() categoryChange = new EventEmitter<Partial<AdminCategory>>();

View File

@@ -4,6 +4,7 @@ import { AdminCategory, AdminCategoryEditorMode, AdminCategoryListFilters } from
import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory'; import { AdminCategoriesFormFactory } from '../services/admin-categories-form.factory';
import { AdminCategoriesLocalGateway } from '../services/admin-categories-local.gateway'; import { AdminCategoriesLocalGateway } from '../services/admin-categories-local.gateway';
import { LocalStorageService } from '../../../../core/storage/local-storage.service'; 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 DRAFT_KEY_PREFIX = 'admin-category-draft:';
const NEW_CATEGORY_DRAFT_KEY = `${DRAFT_KEY_PREFIX}new`; const NEW_CATEGORY_DRAFT_KEY = `${DRAFT_KEY_PREFIX}new`;
@@ -13,6 +14,15 @@ export class AdminCategoriesFacade {
private readonly gateway = inject(AdminCategoriesLocalGateway); private readonly gateway = inject(AdminCategoriesLocalGateway);
private readonly formFactory = inject(AdminCategoriesFormFactory); private readonly formFactory = inject(AdminCategoriesFormFactory);
private readonly localStorage = inject(LocalStorageService); 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<AdminCategoryListFilters>({ search: '', visibility: 'all', includeDeleted: false }); readonly filters = signal<AdminCategoryListFilters>({ search: '', visibility: 'all', includeDeleted: false });
readonly categories = signal<AdminCategory[]>([]); readonly categories = signal<AdminCategory[]>([]);

View File

@@ -9,7 +9,7 @@ import { LanguageService } from '../../../../services/language.service';
selector: 'app-admin-category-editor-page', selector: 'app-admin-category-editor-page',
standalone: true, standalone: true,
imports: [AdminCategoryFormComponent, TranslatePipe], imports: [AdminCategoryFormComponent, TranslatePipe],
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [slugTaken]="facade.slugTaken()" [mode]="facade.editorMode()" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`, template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-category-form [category]="draft" [parentOptions]="parentOptions()" [breadcrumb]="facade.breadcrumbFor(draft.parentId)" [slugTaken]="facade.slugTaken()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" (categoryChange)="facade.updateDraft($event)" (saveDraft)="save(false)" (publish)="save(true)" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], 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 changeDetection: ChangeDetectionStrategy.OnPush
}) })
@@ -32,6 +32,7 @@ export class AdminCategoryEditorPageComponent {
if (this.facade.categories().length === 0) { if (this.facade.categories().length === 0) {
this.facade.loadList(); this.facade.loadList();
} }
this.facade.ensureLocalesLoaded();
if (!id) { if (!id) {
this.facade.startCreate(); this.facade.startCreate();
} else { } else {

View File

@@ -71,7 +71,7 @@
</div> </div>
<h3>{{ 'adminProducts.translations' | translate }}</h3> <h3>{{ 'adminProducts.translations' | translate }}</h3>
@for (locale of ['en','ru','hy']; track locale) { @for (locale of locales; track locale) {
<div class="grid two sub-block"> <div class="grid two sub-block">
<app-form-field [label]="(('adminProducts.name' | translate) + ' ' + locale)"> <app-form-field [label]="(('adminProducts.name' | translate) + ' ' + locale)">
<app-input [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" /> <app-input [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" />

View File

@@ -20,6 +20,7 @@ export class AdminProductFormComponent {
@Input({ required: true }) product!: AdminProduct; @Input({ required: true }) product!: AdminProduct;
@Input() categories: AdminProductCategoryOption[] = []; @Input() categories: AdminProductCategoryOption[] = [];
@Input() allProducts: AdminProduct[] = []; @Input() allProducts: AdminProduct[] = [];
@Input() locales: string[] = ['en'];
@Input() mode: 'create' | 'edit' | 'duplicate' = 'create'; @Input() mode: 'create' | 'edit' | 'duplicate' = 'create';
protected mediaPickerOpen = false; protected mediaPickerOpen = false;

View File

@@ -3,11 +3,21 @@ import { take } from 'rxjs/operators';
import { AdminProduct, AdminProductCategoryOption, AdminProductEditorMode, AdminProductListFilters } from '../models/admin-product.model'; import { AdminProduct, AdminProductCategoryOption, AdminProductEditorMode, AdminProductListFilters } from '../models/admin-product.model';
import { AdminProductsFormFactory } from '../services/admin-products-form.factory'; import { AdminProductsFormFactory } from '../services/admin-products-form.factory';
import { AdminProductsLocalGateway } from '../services/admin-products-local.gateway'; import { AdminProductsLocalGateway } from '../services/admin-products-local.gateway';
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AdminProductsFacade { export class AdminProductsFacade {
private readonly gateway = inject(AdminProductsLocalGateway); private readonly gateway = inject(AdminProductsLocalGateway);
private readonly formFactory = inject(AdminProductsFormFactory); 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<AdminProductListFilters>({ readonly filters = signal<AdminProductListFilters>({
search: '', search: '',

View File

@@ -9,7 +9,7 @@ import { LanguageService } from '../../../../services/language.service';
selector: 'app-admin-product-editor-page', selector: 'app-admin-product-editor-page',
standalone: true, standalone: true,
imports: [AdminProductFormComponent, TranslatePipe], imports: [AdminProductFormComponent, TranslatePipe],
template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-product-form [product]="draft" [categories]="facade.categories()" [allProducts]="facade.products()" [mode]="facade.editorMode()" (productChange)="facade.updateDraft($event)" (save)="save()" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`, template: `@if (facade.draft(); as draft) {<main class="editor-page"><header><h1>{{ title() | translate }}</h1></header><app-admin-product-form [product]="draft" [categories]="facade.categories()" [allProducts]="facade.products()" [locales]="facade.supportedLocales()" [mode]="facade.editorMode()" (productChange)="facade.updateDraft($event)" (save)="save()" /></main>} @else {<main class="editor-page"><p>{{ 'common.loading' | translate }}</p></main>}`,
styles: [`.editor-page { max-width: 1120px; margin: 0 auto; padding: 24px; display: grid; gap: 16px; } .editor-page h1, .editor-page p { margin: 0; }`], 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 changeDetection: ChangeDetectionStrategy.OnPush
}) })
@@ -22,6 +22,7 @@ export class AdminProductEditorPageComponent {
constructor() { constructor() {
this.facade.loadCategories(); this.facade.loadCategories();
this.facade.ensureLocalesLoaded();
const id = this.route.snapshot.paramMap.get('id'); 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'; const mode = this.route.snapshot.routeConfig?.path?.includes('duplicate') ? 'duplicate' : this.route.snapshot.routeConfig?.path?.includes('edit') ? 'edit' : 'create';
if (mode === 'create') { if (mode === 'create') {