feat(project-editor): centralize schema-driven validation
Milestone 2 of the Configuration Engine sprint.
- Add schema/validators/primitives: pure isValidHexColor/HttpUrl/Email,
validateJson, validateCss, extractStyleBlocks, normalizeRoute. One function
per concern, no duplicated validator logic.
- Refactor ProjectValidator to compose the primitives and tag every issue with
section + fieldKey + severity ('error' blocks publish, 'warning' advisory).
Preserves all existing codes/messages; adds duplicate-routes, invalid-css,
invalid-widget-config checks.
- Facade: issuesByField, issuesBySection, blockingIssues, hasBlockingIssues;
publish() now gates on severity==='error' instead of any issue.
- i18n: add validationInvalidJson/Css/DuplicateRoutes/InvalidWidgetConfig to
the Translations interface + en/ru/hy.
- Specs: primitives + ProjectValidator (25 passing total).
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:
@@ -8,7 +8,8 @@ import { ProjectEditorPreviewService } from '../services/project-editor-preview.
|
||||
import { LocaleSyncService } from '../services/locale-sync.service';
|
||||
import { ProjectEditorState } from '../models/project-editor.model';
|
||||
import { PlatformRuntimeService } from '../../../core/runtime/platform-runtime.service';
|
||||
import { ProjectValidator } from '../services/project-validator.service';
|
||||
import { ProjectValidator, ProjectValidationIssue } from '../services/project-validator.service';
|
||||
import { ProjectEditorSectionId } from '../models/project-editor.model';
|
||||
import { ProjectEditorDraftStorageService } from '../services/project-editor-draft-storage.service';
|
||||
import { EDITOR_SECTION_BOOTSTRAP_KEYS } from '../models/project-editor.model';
|
||||
|
||||
@@ -47,6 +48,36 @@ export class ProjectEditorFacade {
|
||||
const current = this.bootstrap();
|
||||
return current ? this.validator.validate(current) : [];
|
||||
});
|
||||
/** Issues that block publishing (`severity === 'error'`), in declaration order. */
|
||||
readonly blockingIssues = computed(() => this.validationIssues().filter(issue => issue.severity === 'error'));
|
||||
readonly hasBlockingIssues = computed(() => this.blockingIssues().length > 0);
|
||||
/** Issues grouped by the schema field key they map to, for inline field errors. */
|
||||
readonly issuesByField = computed<Map<string, ProjectValidationIssue[]>>(() => {
|
||||
const map = new Map<string, ProjectValidationIssue[]>();
|
||||
for (const issue of this.validationIssues()) {
|
||||
if (!issue.fieldKey) {
|
||||
continue;
|
||||
}
|
||||
const bucket = map.get(issue.fieldKey);
|
||||
if (bucket) {
|
||||
bucket.push(issue);
|
||||
} else {
|
||||
map.set(issue.fieldKey, [issue]);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
});
|
||||
/** Blocking-issue count per editor section, for nav badges. */
|
||||
readonly issuesBySection = computed<Map<ProjectEditorSectionId, number>>(() => {
|
||||
const map = new Map<ProjectEditorSectionId, number>();
|
||||
for (const issue of this.blockingIssues()) {
|
||||
if (!issue.section) {
|
||||
continue;
|
||||
}
|
||||
map.set(issue.section, (map.get(issue.section) ?? 0) + 1);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
readonly dirty = computed(() => {
|
||||
const current = this.bootstrap();
|
||||
if (!current) {
|
||||
@@ -173,7 +204,7 @@ export class ProjectEditorFacade {
|
||||
|
||||
publish(): boolean {
|
||||
const current = this.state().bootstrap;
|
||||
if (!current || this.validationIssues().length > 0) {
|
||||
if (!current || this.hasBlockingIssues()) {
|
||||
return false;
|
||||
}
|
||||
this.runtime.reloadFromBootstrap(current);
|
||||
|
||||
Reference in New Issue
Block a user