feat(project-editor): per-field inline validation for languages, homepage, widgets, navigation, static-pages

Extends the fieldError() wiring pattern (already used in theme/general/branding)
to the remaining sections that have matching ProjectValidator fieldKeys:
- languages: localization.supportedLocales (no-languages, missing-translations)
- homepage: pages (empty-homepage, missing-widget, duplicate-routes)
- widgets: pages (invalid-widget-config)
- navigation: navigation.header (duplicate-nav-links)
- static-pages: staticPages (duplicate-slugs, invalid-css)

Header/footer/features sections have no matching validator issues today,
so nothing to wire there yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-17 16:18:22 +04:00
parent d8456ecb9f
commit 4543a6b6b6
10 changed files with 43 additions and 0 deletions

View File

@@ -3,6 +3,9 @@
<h2>{{ 'builder.staticPages' | translate }}</h2>
<app-button variant="primary" (click)="createPage()">{{ 'builder.createPage' | translate }}</app-button>
</div>
@if (fieldError('staticPages'); as msg) {
<p class="editor-error">{{ msg }}</p>
}
<div class="editor-grid three static-pages-toolbar">
<app-form-field [label]="'staticPages.searchPlaceholder' | translate">

View File

@@ -51,6 +51,10 @@ export class StaticPagesEditorComponent {
private readonly translate = inject(TranslateService);
readonly bootstrap = this.projectEditor.bootstrap;
readonly fieldError = (key: string): string | null => {
const messageKey = this.projectEditor.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
};
readonly allPages = computed(() => this.contentFacade.pages(this.bootstrap()));
readonly validation = computed(() => this.contentFacade.validatePages(this.bootstrap()));
readonly locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? ['en']);

View File

@@ -1,5 +1,8 @@
@if (homePage()) {
<app-section-card [title]="'builder.homepage' | translate">
@if (fieldError('pages'); as msg) {
<p class="editor-error">{{ msg }}</p>
}
<div cdkDropList class="sortable-list" (cdkDropListDropped)="drop($event)">
@for (section of sections(); track section.id) {
<div class="sortable-item" cdkDrag>

View File

@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service';
import { InputComponent } from '../../../shared/ui/input/input.component';
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
@@ -23,7 +24,12 @@ export interface LayoutStrategyOption {
})
export class ProjectEditorHomepageSectionComponent {
private readonly facade = inject(ProjectEditorFacade);
private readonly translate = inject(TranslateService);
readonly homePage = this.facade.homepagePage;
readonly fieldError = (key: string): string | null => {
const messageKey = this.facade.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
};
readonly sections = computed(() => [...(this.homePage()?.sections ?? [])].sort((a, b) => a.order - b.order));
readonly layoutStrategyOptions: LayoutStrategyOption[] = [

View File

@@ -1,6 +1,9 @@
<app-section-card [title]="'builder.languagesTab' | translate">
<app-locale-tabs [locales]="supportedLocales()" [defaultLocale]="tenantDefaultLocale()" (activeLocale)="onActiveLocaleChange($event)" />
<small class="field-desc">{{ 'builder.languagesTabDesc' | translate }}</small>
@if (fieldError('localization.supportedLocales'); as msg) {
<p class="editor-error">{{ msg }}</p>
}
<div class="editor-actions">
<app-input

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service';
import { ButtonComponent } from '../../../shared/ui/button/button.component';
import { InputComponent } from '../../../shared/ui/input/input.component';
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
@@ -17,7 +18,12 @@ import { LocaleTabsComponent } from '../../../shared/ui/locale-tabs/locale-tabs.
})
export class ProjectEditorLanguagesSectionComponent {
private readonly facade = inject(ProjectEditorFacade);
private readonly translate = inject(TranslateService);
readonly bootstrap = this.facade.bootstrap;
readonly fieldError = (key: string): string | null => {
const messageKey = this.facade.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
};
readonly locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? []);
readonly defaultLocale = computed(() => this.bootstrap()?.localization.defaultLocale ?? '');
readonly supportedLocales = computed(() => this.bootstrap()?.tenant.supportedLocales ?? []);

View File

@@ -1,5 +1,8 @@
<app-section-card [title]="'builder.navigationHeader' | translate">
<app-locale-tabs [locales]="supportedLocales()" [defaultLocale]="defaultLocale()" (activeLocale)="onActiveLocaleChange($event)" />
@if (fieldError('navigation.header'); as msg) {
<p class="editor-error">{{ msg }}</p>
}
<div class="editor-actions">
<app-button variant="primary" (click)="addLink('header')">{{ 'builder.addLink' | translate }}</app-button>

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service';
import { NavigationItemConfig } from '../../../shared/models/config';
import { ButtonComponent } from '../../../shared/ui/button/button.component';
import { InputComponent } from '../../../shared/ui/input/input.component';
@@ -22,7 +23,12 @@ import { ContentManagementFacade } from '../../content-management/facade/content
export class ProjectEditorNavigationSectionComponent {
private readonly facade = inject(ProjectEditorFacade);
private readonly contentFacade = inject(ContentManagementFacade);
private readonly translate = inject(TranslateService);
readonly bootstrap = this.facade.bootstrap;
readonly fieldError = (key: string): string | null => {
const messageKey = this.facade.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
};
readonly pageOptions = computed<SelectOption[]>(() =>
this.contentFacade.pages(this.bootstrap()).map(page => ({ value: page.id, label: page.title || page.id })),

View File

@@ -1,5 +1,8 @@
@if (widgets().length > 0) {
<app-section-card [title]="'builder.widgets' | translate">
@if (fieldError('pages'); as msg) {
<p class="editor-error">{{ msg }}</p>
}
<div class="stack-list">
@for (entry of widgets(); track entry.widget.id) {
<article class="sub-card">

View File

@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { TranslateService } from '../../../i18n/translate.service';
import { InputComponent } from '../../../shared/ui/input/input.component';
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
@@ -16,7 +17,12 @@ import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
})
export class ProjectEditorWidgetsSectionComponent {
private readonly facade = inject(ProjectEditorFacade);
private readonly translate = inject(TranslateService);
readonly widgets = this.facade.homepageWidgets;
readonly fieldError = (key: string): string | null => {
const messageKey = this.facade.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
};
updateWidget(widgetId: string, updater: (props: Record<string, unknown>) => Record<string, unknown>): void {
this.facade.updateBootstrap(current => ({