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>
|
<h2>{{ 'builder.staticPages' | translate }}</h2>
|
||||||
<app-button variant="primary" (click)="createPage()">{{ 'builder.createPage' | translate }}</app-button>
|
<app-button variant="primary" (click)="createPage()">{{ 'builder.createPage' | translate }}</app-button>
|
||||||
</div>
|
</div>
|
||||||
|
@if (fieldError('staticPages'); as msg) {
|
||||||
|
<p class="editor-error">{{ msg }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="editor-grid three static-pages-toolbar">
|
<div class="editor-grid three static-pages-toolbar">
|
||||||
<app-form-field [label]="'staticPages.searchPlaceholder' | translate">
|
<app-form-field [label]="'staticPages.searchPlaceholder' | translate">
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export class StaticPagesEditorComponent {
|
|||||||
private readonly translate = inject(TranslateService);
|
private readonly translate = inject(TranslateService);
|
||||||
|
|
||||||
readonly bootstrap = this.projectEditor.bootstrap;
|
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 allPages = computed(() => this.contentFacade.pages(this.bootstrap()));
|
||||||
readonly validation = computed(() => this.contentFacade.validatePages(this.bootstrap()));
|
readonly validation = computed(() => this.contentFacade.validatePages(this.bootstrap()));
|
||||||
readonly locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? ['en']);
|
readonly locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? ['en']);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
@if (homePage()) {
|
@if (homePage()) {
|
||||||
<app-section-card [title]="'builder.homepage' | translate">
|
<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)">
|
<div cdkDropList class="sortable-list" (cdkDropListDropped)="drop($event)">
|
||||||
@for (section of sections(); track section.id) {
|
@for (section of sections(); track section.id) {
|
||||||
<div class="sortable-item" cdkDrag>
|
<div class="sortable-item" cdkDrag>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/c
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
|
import { TranslateService } from '../../../i18n/translate.service';
|
||||||
import { InputComponent } from '../../../shared/ui/input/input.component';
|
import { InputComponent } from '../../../shared/ui/input/input.component';
|
||||||
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
||||||
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
||||||
@@ -23,7 +24,12 @@ export interface LayoutStrategyOption {
|
|||||||
})
|
})
|
||||||
export class ProjectEditorHomepageSectionComponent {
|
export class ProjectEditorHomepageSectionComponent {
|
||||||
private readonly facade = inject(ProjectEditorFacade);
|
private readonly facade = inject(ProjectEditorFacade);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
readonly homePage = this.facade.homepagePage;
|
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 sections = computed(() => [...(this.homePage()?.sections ?? [])].sort((a, b) => a.order - b.order));
|
||||||
|
|
||||||
readonly layoutStrategyOptions: LayoutStrategyOption[] = [
|
readonly layoutStrategyOptions: LayoutStrategyOption[] = [
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<app-section-card [title]="'builder.languagesTab' | translate">
|
<app-section-card [title]="'builder.languagesTab' | translate">
|
||||||
<app-locale-tabs [locales]="supportedLocales()" [defaultLocale]="tenantDefaultLocale()" (activeLocale)="onActiveLocaleChange($event)" />
|
<app-locale-tabs [locales]="supportedLocales()" [defaultLocale]="tenantDefaultLocale()" (activeLocale)="onActiveLocaleChange($event)" />
|
||||||
<small class="field-desc">{{ 'builder.languagesTabDesc' | translate }}</small>
|
<small class="field-desc">{{ 'builder.languagesTabDesc' | translate }}</small>
|
||||||
|
@if (fieldError('localization.supportedLocales'); as msg) {
|
||||||
|
<p class="editor-error">{{ msg }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
<app-input
|
<app-input
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
|
import { TranslateService } from '../../../i18n/translate.service';
|
||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
import { InputComponent } from '../../../shared/ui/input/input.component';
|
import { InputComponent } from '../../../shared/ui/input/input.component';
|
||||||
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.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 {
|
export class ProjectEditorLanguagesSectionComponent {
|
||||||
private readonly facade = inject(ProjectEditorFacade);
|
private readonly facade = inject(ProjectEditorFacade);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
readonly bootstrap = this.facade.bootstrap;
|
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 locales = computed(() => this.bootstrap()?.localization.supportedLocales ?? []);
|
||||||
readonly defaultLocale = computed(() => this.bootstrap()?.localization.defaultLocale ?? '');
|
readonly defaultLocale = computed(() => this.bootstrap()?.localization.defaultLocale ?? '');
|
||||||
readonly supportedLocales = computed(() => this.bootstrap()?.tenant.supportedLocales ?? []);
|
readonly supportedLocales = computed(() => this.bootstrap()?.tenant.supportedLocales ?? []);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<app-section-card [title]="'builder.navigationHeader' | translate">
|
<app-section-card [title]="'builder.navigationHeader' | translate">
|
||||||
<app-locale-tabs [locales]="supportedLocales()" [defaultLocale]="defaultLocale()" (activeLocale)="onActiveLocaleChange($event)" />
|
<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">
|
<div class="editor-actions">
|
||||||
<app-button variant="primary" (click)="addLink('header')">{{ 'builder.addLink' | translate }}</app-button>
|
<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 { FormsModule } from '@angular/forms';
|
||||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
|
import { TranslateService } from '../../../i18n/translate.service';
|
||||||
import { NavigationItemConfig } from '../../../shared/models/config';
|
import { NavigationItemConfig } from '../../../shared/models/config';
|
||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
import { InputComponent } from '../../../shared/ui/input/input.component';
|
import { InputComponent } from '../../../shared/ui/input/input.component';
|
||||||
@@ -22,7 +23,12 @@ import { ContentManagementFacade } from '../../content-management/facade/content
|
|||||||
export class ProjectEditorNavigationSectionComponent {
|
export class ProjectEditorNavigationSectionComponent {
|
||||||
private readonly facade = inject(ProjectEditorFacade);
|
private readonly facade = inject(ProjectEditorFacade);
|
||||||
private readonly contentFacade = inject(ContentManagementFacade);
|
private readonly contentFacade = inject(ContentManagementFacade);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
readonly bootstrap = this.facade.bootstrap;
|
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[]>(() =>
|
readonly pageOptions = computed<SelectOption[]>(() =>
|
||||||
this.contentFacade.pages(this.bootstrap()).map(page => ({ value: page.id, label: page.title || page.id })),
|
this.contentFacade.pages(this.bootstrap()).map(page => ({ value: page.id, label: page.title || page.id })),
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
@if (widgets().length > 0) {
|
@if (widgets().length > 0) {
|
||||||
<app-section-card [title]="'builder.widgets' | translate">
|
<app-section-card [title]="'builder.widgets' | translate">
|
||||||
|
@if (fieldError('pages'); as msg) {
|
||||||
|
<p class="editor-error">{{ msg }}</p>
|
||||||
|
}
|
||||||
<div class="stack-list">
|
<div class="stack-list">
|
||||||
@for (entry of widgets(); track entry.widget.id) {
|
@for (entry of widgets(); track entry.widget.id) {
|
||||||
<article class="sub-card">
|
<article class="sub-card">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
|
import { TranslateService } from '../../../i18n/translate.service';
|
||||||
import { InputComponent } from '../../../shared/ui/input/input.component';
|
import { InputComponent } from '../../../shared/ui/input/input.component';
|
||||||
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
||||||
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
||||||
@@ -16,7 +17,12 @@ import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
|||||||
})
|
})
|
||||||
export class ProjectEditorWidgetsSectionComponent {
|
export class ProjectEditorWidgetsSectionComponent {
|
||||||
private readonly facade = inject(ProjectEditorFacade);
|
private readonly facade = inject(ProjectEditorFacade);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
readonly widgets = this.facade.homepageWidgets;
|
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 {
|
updateWidget(widgetId: string, updater: (props: Record<string, unknown>) => Record<string, unknown>): void {
|
||||||
this.facade.updateBootstrap(current => ({
|
this.facade.updateBootstrap(current => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user