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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user