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:
@@ -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">
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? []);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
Reference in New Issue
Block a user