feat(builder): redesign marketplace builder experience and navigation

Renamed the experience consistently to Marketplace Builder everywhere
visible (page title/subtitle, breadcrumbs, sidebar, dashboard quick
action/shortcut copy) - internal ProjectEditor class/selector names
kept as-is to avoid regressions. builder.title/subtitle no longer say
'bootstrap-config editing surface' to end users.

New business-oriented IA (builder-groups.model.ts): 12 existing
ProjectEditorSectionIds regrouped into 8 groups (Marketplace, Branding
and Design, Homepage, Content, Languages, Marketplace Features,
Navigation and Search, Preview) - every existing section mapped to
exactly one group, none dropped, no group points at a page that
doesn't exist.

New Builder landing page at /edit (was a redirect straight into the
General form): readiness percent, recommended next step, one overview
card per group (purpose + real complete/in-progress/not-started/unknown
status, icon+text never color-alone), a Marketplace Readiness
checklist, quick links. All derived from real facade/schema signals
(required-field fill state, modifiedSections, staticPages, catalog
counts via BackofficeDataService) or explicitly marked unknown
(the "preview reviewed" check has no tracking - shown as unknown,
never guessed).

Section pages (/edit/:section) now share one consistent header:
breadcrumb (Builder > Group > Section), draft/published + modified
badges, and a contextual help panel (what is this / where visible /
what happens if you skip it) sourced per group. Sidebar nav rewritten
as grouped, icon-led sections with per-section and per-group status
indicators; layout converted from a top pill-tab bar to a responsive
sidebar (desktop 280px, tablet 72px icon rail, mobile drawer with
Escape-to-close), matching the Sprint 1 admin shell pattern. Section
form components themselves untouched.

Routes: 'edit' is now its own landing route instead of redirecting to
'edit/general'; /builder and /project-editor redirect to 'edit'.
Sprint 1/2 destinations that pointed at edit/general now point at the
new edit landing page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-18 09:43:03 +04:00
parent 419cb9a32c
commit 821a1c6ba8
19 changed files with 1699 additions and 152 deletions

View File

@@ -11,9 +11,24 @@ import { PlatformRuntimeService } from '../../../core/runtime/platform-runtime.s
import { ProjectValidator, ProjectValidationIssue } from '../services/project-validator.service';
import { ProjectEditorSectionId } from '../models/project-editor.model';
import { EditorSchemaService } from '../schema/editor-schema.service';
import { FieldSchema } from '../schema/field-schema.model';
import { History, commit as commitHistory, emptyHistory, redo as redoHistory, undo as undoHistory } from '../schema/history.util';
import { ProjectEditorDraftStorageService } from '../services/project-editor-draft-storage.service';
import { EDITOR_SECTION_BOOTSTRAP_KEYS } from '../models/project-editor.model';
import { EDITOR_SECTION_BOOTSTRAP_KEYS, BuilderSectionStatus } from '../models/project-editor.model';
import { BUILDER_GROUPS, BuilderGroupId } from '../builder/builder-groups.model';
function isFilled(value: unknown): boolean {
if (value === null || value === undefined) {
return false;
}
if (typeof value === 'string') {
return value.trim().length > 0;
}
if (Array.isArray(value)) {
return value.length > 0;
}
return true;
}
const HISTORY_DEBOUNCE_MS = 300;
@@ -134,6 +149,81 @@ export class ProjectEditorFacade {
}
return sections;
});
/** Required-field completeness per section (Sprint 3 Builder IA) - real schema data only, never guessed. */
readonly sectionStatus = computed<Map<ProjectEditorSectionId, BuilderSectionStatus>>(() => {
const current = this.bootstrap();
const map = new Map<ProjectEditorSectionId, BuilderSectionStatus>();
if (!current) {
return map;
}
const modified = this.modifiedSections();
const bySection = new Map<ProjectEditorSectionId, FieldSchema[]>();
for (const field of this.schema.all()) {
const list = bySection.get(field.section) ?? [];
list.push(field);
bySection.set(field.section, list);
}
for (const [section, fields] of bySection) {
const required = fields.filter(f => f.required);
if (required.length === 0) {
map.set(section, modified.has(section) ? 'in-progress' : 'unknown');
continue;
}
const filled = required.filter(f => isFilled(this.schema.getByPath(current, f.key))).length;
map.set(section, filled === required.length ? 'complete' : filled === 0 ? 'not-started' : 'in-progress');
}
return map;
});
/** Rolls sectionStatus up to the Builder's business-oriented groups. */
readonly groupStatus = computed<Map<BuilderGroupId, BuilderSectionStatus>>(() => {
const sections = this.sectionStatus();
const result = new Map<BuilderGroupId, BuilderSectionStatus>();
for (const group of BUILDER_GROUPS) {
const statuses = group.sections.map(id => sections.get(id) ?? 'unknown');
if (statuses.some(s => s === 'in-progress') || (statuses.some(s => s === 'not-started') && statuses.some(s => s === 'complete'))) {
result.set(group.id, 'in-progress');
} else if (statuses.every(s => s === 'not-started')) {
result.set(group.id, 'not-started');
} else {
const known = statuses.filter(s => s !== 'unknown');
result.set(group.id, known.length > 0 && known.every(s => s === 'complete') ? 'complete' : 'unknown');
}
}
return result;
});
/** % of schema-required fields filled across the whole config - the Builder overview's readiness number. Real data only. */
readonly readinessPercent = computed<number | null>(() => {
const current = this.bootstrap();
if (!current) {
return null;
}
const required = this.schema.all().filter(f => f.required);
if (required.length === 0) {
return null;
}
const filled = required.filter(f => isFilled(this.schema.getByPath(current, f.key))).length;
return Math.round((filled / required.length) * 100);
});
/** First section with a required field still missing, in group display order - the overview's "recommended next step". */
readonly nextRecommendedSection = computed<ProjectEditorSectionId | null>(() => {
if (!this.bootstrap()) {
return null;
}
const statuses = this.sectionStatus();
for (const group of BUILDER_GROUPS) {
for (const section of group.sections) {
const status = statuses.get(section);
if (status === 'not-started' || status === 'in-progress') {
return section;
}
}
}
return null;
});
/** Per-field before/after diff vs the originally loaded/published config, for the pre-publish preview. */
readonly changeSummary = computed<ChangeSummaryRow[]>(() => {
const current = this.bootstrap();