feat(project-editor): add ProjectValidator with MVP validation rules
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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' }] : [];
|
||||
}
|
||||
}
|
||||
@@ -504,6 +504,14 @@ export const en: Translations = {
|
||||
importSource: 'Import Source',
|
||||
importError: 'Import failed. Invalid bootstrap JSON.',
|
||||
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: {
|
||||
notFound: 'Page not found',
|
||||
|
||||
@@ -504,6 +504,14 @@ export const hy: Translations = {
|
||||
importSource: 'Ներմուծման աղբյուր',
|
||||
importError: 'Ներմուծումը ձախողվեց։ Սխալ bootstrap JSON։',
|
||||
featureFlags: 'Feature flags',
|
||||
validationMissingLogo: 'Բրենդինգում բացակայում է լոգոն։',
|
||||
validationNoLanguages: 'Կարգավորված լեզուներ չկան։',
|
||||
validationInvalidUrl: 'Մարքեթփլեյսի URL-ը սխալ է։',
|
||||
validationDuplicateSlugs: 'Երկու կամ ավելի ստատիկ էջեր ունեն նույն slug-ը։',
|
||||
validationEmptyHomepage: 'Գլխավոր էջում սեկցիաներ չկան։',
|
||||
validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։',
|
||||
validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։',
|
||||
validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։',
|
||||
},
|
||||
staticPages: {
|
||||
notFound: 'Էջը չի գտնվել',
|
||||
|
||||
@@ -504,6 +504,14 @@ export const ru: Translations = {
|
||||
importSource: 'Источник импорта',
|
||||
importError: 'Импорт не удался. Некорректный JSON bootstrap.',
|
||||
featureFlags: 'Фичи-флаги',
|
||||
validationMissingLogo: 'В брендинге отсутствует логотип.',
|
||||
validationNoLanguages: 'Не настроены языки.',
|
||||
validationInvalidUrl: 'Некорректный URL маркетплейса.',
|
||||
validationDuplicateSlugs: 'Две или более статические страницы имеют одинаковый slug.',
|
||||
validationEmptyHomepage: 'На главной странице нет секций.',
|
||||
validationMissingWidget: 'В секции главной страницы есть виджет без типа.',
|
||||
validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.',
|
||||
validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.',
|
||||
},
|
||||
staticPages: {
|
||||
notFound: 'Страница не найдена',
|
||||
|
||||
@@ -502,6 +502,14 @@ export interface Translations {
|
||||
importSource: string;
|
||||
importError: string;
|
||||
featureFlags: string;
|
||||
validationMissingLogo: string;
|
||||
validationNoLanguages: string;
|
||||
validationInvalidUrl: string;
|
||||
validationDuplicateSlugs: string;
|
||||
validationEmptyHomepage: string;
|
||||
validationMissingWidget: string;
|
||||
validationDuplicateNavLinks: string;
|
||||
validationInvalidColors: string;
|
||||
};
|
||||
staticPages: {
|
||||
notFound: string;
|
||||
|
||||
Reference in New Issue
Block a user