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' }] : [];
}
}

View File

@@ -504,6 +504,14 @@ export const en: Translations = {
importSource: 'Import Source', importSource: 'Import Source',
importError: 'Import failed. Invalid bootstrap JSON.', importError: 'Import failed. Invalid bootstrap JSON.',
featureFlags: 'Feature Flags', featureFlags: 'Feature Flags',
validationMissingLogo: 'Branding is missing a logo.',
validationNoLanguages: 'No languages are configured.',
validationInvalidUrl: 'The marketplace URL is invalid.',
validationDuplicateSlugs: 'Two or more static pages share the same slug.',
validationEmptyHomepage: 'The homepage has no sections.',
validationMissingWidget: 'A homepage section has a widget with no type.',
validationDuplicateNavLinks: 'Two or more header navigation links are duplicates.',
validationInvalidColors: 'One or more theme colors are not valid hex colors.',
}, },
staticPages: { staticPages: {
notFound: 'Page not found', notFound: 'Page not found',

View File

@@ -504,6 +504,14 @@ export const hy: Translations = {
importSource: 'Ներմուծման աղբյուր', importSource: 'Ներմուծման աղբյուր',
importError: 'Ներմուծումը ձախողվեց։ Սխալ bootstrap JSON։', importError: 'Ներմուծումը ձախողվեց։ Սխալ bootstrap JSON։',
featureFlags: 'Feature flags', featureFlags: 'Feature flags',
validationMissingLogo: 'Բրենդինգում բացակայում է լոգոն։',
validationNoLanguages: 'Կարգավորված լեզուներ չկան։',
validationInvalidUrl: 'Մարքեթփլեյսի URL-ը սխալ է։',
validationDuplicateSlugs: 'Երկու կամ ավելի ստատիկ էջեր ունեն նույն slug-ը։',
validationEmptyHomepage: 'Գլխավոր էջում սեկցիաներ չկան։',
validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։',
validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։',
validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։',
}, },
staticPages: { staticPages: {
notFound: 'Էջը չի գտնվել', notFound: 'Էջը չի գտնվել',

View File

@@ -504,6 +504,14 @@ export const ru: Translations = {
importSource: 'Источник импорта', importSource: 'Источник импорта',
importError: 'Импорт не удался. Некорректный JSON bootstrap.', importError: 'Импорт не удался. Некорректный JSON bootstrap.',
featureFlags: 'Фичи-флаги', featureFlags: 'Фичи-флаги',
validationMissingLogo: 'В брендинге отсутствует логотип.',
validationNoLanguages: 'Не настроены языки.',
validationInvalidUrl: 'Некорректный URL маркетплейса.',
validationDuplicateSlugs: 'Две или более статические страницы имеют одинаковый slug.',
validationEmptyHomepage: 'На главной странице нет секций.',
validationMissingWidget: 'В секции главной страницы есть виджет без типа.',
validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.',
validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.',
}, },
staticPages: { staticPages: {
notFound: 'Страница не найдена', notFound: 'Страница не найдена',

View File

@@ -502,6 +502,14 @@ export interface Translations {
importSource: string; importSource: string;
importError: string; importError: string;
featureFlags: string; featureFlags: string;
validationMissingLogo: string;
validationNoLanguages: string;
validationInvalidUrl: string;
validationDuplicateSlugs: string;
validationEmptyHomepage: string;
validationMissingWidget: string;
validationDuplicateNavLinks: string;
validationInvalidColors: string;
}; };
staticPages: { staticPages: {
notFound: string; notFound: string;