feat(project-editor): live inline validation and publish gating
Milestone 3 of the Configuration Engine sprint. - Facade fieldError(key) accessor over issuesByField for inline field errors. - Bind [error] on schema-backed fields: theme palette colours, general name/domain, branding logo (translated via each section). - project-editor-nav: per-section blocking-issue count badge (issuesBySection). - save-bar: Publish now disabled on hasBlockingIssues() (errors only, so new warnings no longer block); issue list tags warning vs error severity. Editor verified rendering at /ru/edit/theme with the new nav + save bar; validation logic covered by the 25 unit tests. Gate: tsc --noEmit, npm test (25/25), arch:check, build all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,6 @@
|
||||
routerLinkActive="active"
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
class="editor-nav-link"
|
||||
>{{ section.label | translate }}</a>
|
||||
>{{ section.label | translate }}@if (issueCount(section.id); as count) {<span class="editor-nav-badge" [attr.aria-label]="count + ' ' + ('builder.title' | translate)">{{ count }}</span>}</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@@ -36,6 +36,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.editor-nav-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 6px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--error-color, #ef4444);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.editor-nav-link.active .editor-nav-badge {
|
||||
background: #fff;
|
||||
color: var(--error-color, #ef4444);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.editor-nav-link {
|
||||
transition: none;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||
import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
|
||||
import { ProjectEditorSectionId } from '../models/project-editor.model';
|
||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-editor-nav',
|
||||
@@ -13,6 +14,14 @@ import { ProjectEditorSectionId } from '../models/project-editor.model';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ProjectEditorNavComponent {
|
||||
private readonly facade = inject(ProjectEditorFacade);
|
||||
private readonly issuesBySection = this.facade.issuesBySection;
|
||||
|
||||
/** Count of blocking issues in a section, for the nav badge. */
|
||||
issueCount(sectionId: ProjectEditorSectionId): number {
|
||||
return this.issuesBySection().get(sectionId) ?? 0;
|
||||
}
|
||||
|
||||
readonly sections: Array<{ id: ProjectEditorSectionId; label: string }> = [
|
||||
{ id: 'general', label: 'builder.general' },
|
||||
{ id: 'branding', label: 'builder.branding' },
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@if (issues().length > 0) {
|
||||
<ul class="project-editor-save-bar-issues">
|
||||
@for (issue of issues(); track issue.code) {
|
||||
<li>{{ issue.message | translate }}</li>
|
||||
<li [class.is-warning]="issue.severity === 'warning'" [class.is-error]="issue.severity === 'error'">{{ issue.message | translate }}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@@ -23,6 +23,6 @@
|
||||
<div class="project-editor-save-bar-actions">
|
||||
<app-button variant="danger" size="sm" (click)="resetDraft()">{{ 'builder.resetDraft' | translate }}</app-button>
|
||||
<app-button variant="secondary" size="sm" (click)="save()">{{ 'builder.save' | translate }}</app-button>
|
||||
<app-button variant="primary" size="sm" [disabled]="issues().length > 0" (click)="publish()">{{ 'builder.publish' | translate }}</app-button>
|
||||
<app-button variant="primary" size="sm" [disabled]="hasBlockingIssues()" (click)="publish()">{{ 'builder.publish' | translate }}</app-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.25rem;
|
||||
color: var(--danger, #b91c1c);
|
||||
|
||||
li.is-warning {
|
||||
color: var(--warning-color, #b45309);
|
||||
}
|
||||
|
||||
li.is-error {
|
||||
color: var(--error-color, #b91c1c);
|
||||
}
|
||||
}
|
||||
|
||||
.project-editor-save-bar-actions {
|
||||
|
||||
@@ -18,6 +18,7 @@ export class ProjectEditorSaveBarComponent {
|
||||
readonly dirty = this.facade.dirty;
|
||||
readonly status = this.facade.status;
|
||||
readonly issues = this.facade.validationIssues;
|
||||
readonly hasBlockingIssues = this.facade.hasBlockingIssues;
|
||||
readonly lastSavedAt = this.facade.lastSavedAt;
|
||||
readonly draftRestored = this.facade.draftRestored;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user