feat(project-editor): add ProjectValidator with MVP validation rules

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-13 10:51:05 +04:00
parent 646a68e9fd
commit 265f2fcbac
5 changed files with 102 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
import { Injectable } from '@angular/core';
import { BootstrapConfig } from '../../../shared/models/config';
export interface ProjectValidationIssue {
code: string;
message: string;
}
const HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
const HTTP_URL = /^https?:\/\/\S+$/;
@Injectable({ providedIn: 'root' })
export class ProjectValidator {
validate(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
return [
...this.brandingIssues(bootstrap),
...this.languageIssues(bootstrap),
...this.urlIssues(bootstrap),
...this.duplicateSlugIssues(bootstrap),
...this.homepageIssues(bootstrap),
...this.navigationIssues(bootstrap),
...this.colorIssues(bootstrap),
];
}
private brandingIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
return bootstrap.branding.logoUrl ? [] : [{ code: 'missing-logo', message: 'builder.validationMissingLogo' }];
}
private languageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
return bootstrap.localization.supportedLocales.length > 0 ? [] : [{ code: 'no-languages', message: 'builder.validationNoLanguages' }];
}
private urlIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
const url = bootstrap.tenant.websiteBaseUrl;
return !url || HTTP_URL.test(url) ? [] : [{ code: 'invalid-url', message: 'builder.validationInvalidUrl' }];
}
private duplicateSlugIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
const staticPages = bootstrap.staticPages;
if (!staticPages || Array.isArray(staticPages)) {
return [];
}
const slugs = Object.values(staticPages).map(page => page.slug);
const hasDuplicates = slugs.some((slug, index) => slugs.indexOf(slug) !== index);
return hasDuplicates ? [{ code: 'duplicate-slugs', message: 'builder.validationDuplicateSlugs' }] : [];
}
private homepageIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
const homePage = bootstrap.pages.find(page => page.key === 'home' || page.route.path === '/');
if (!homePage || homePage.sections.length === 0) {
return [{ code: 'empty-homepage', message: 'builder.validationEmptyHomepage' }];
}
const hasMissingWidgetType = homePage.sections.some(section => section.widgets.some(widget => !widget.type?.trim()));
return hasMissingWidgetType ? [{ code: 'missing-widget', message: 'builder.validationMissingWidget' }] : [];
}
private navigationIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
const keyOf = (item: { label?: string | Record<string, string>; route?: string }): string =>
`${typeof item.label === 'string' ? item.label : JSON.stringify(item.label ?? {})}|${item.route ?? ''}`;
const keys = bootstrap.navigation.header.map(keyOf);
const hasDuplicates = keys.some((key, index) => keys.indexOf(key) !== index);
return hasDuplicates ? [{ code: 'duplicate-nav-links', message: 'builder.validationDuplicateNavLinks' }] : [];
}
private colorIssues(bootstrap: BootstrapConfig): ProjectValidationIssue[] {
const invalid = Object.values(bootstrap.theme.palette).some(value => !HEX_COLOR.test(value));
return invalid ? [{ code: 'invalid-colors', message: 'builder.validationInvalidColors' }] : [];
}
}