feat(project-editor): pre-publish change + validation preview

Milestone 5 of the Configuration Engine sprint.

- Facade: changeSummary computed - per modified field, before/after values
  (schema label + stringified diff vs originalBootstrap), reusing
  modifiedFields from M4.
- preview-section: new "changes since last publish" card ahead of the
  existing export/import/live-preview card - validation issue list
  (warning/error styled) plus a before/after change table. Reuses the
  existing, non-destructive ProjectEditorPreviewService.preview() call.
- i18n: previewChangesTitle/NoIssues/NoChanges/ChangeField/Before/After in
  interface + en/ru/hy.

Gate: tsc --noEmit, npm test (33/33), arch:check, build all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-17 09:00:47 +04:00
parent 1db0d4dfea
commit 96b12a1fbe
8 changed files with 153 additions and 0 deletions

View File

@@ -14,6 +14,15 @@ import { EditorSchemaService } from '../schema/editor-schema.service';
import { History, commit as commitHistory, emptyHistory, redo as redoHistory, undo as undoHistory } from '../schema/history.util';
const HISTORY_DEBOUNCE_MS = 300;
export interface ChangeSummaryRow {
readonly fieldKey: string;
/** i18n key for the field's label. */
readonly labelKey: string;
readonly section?: ProjectEditorSectionId;
readonly before: string;
readonly after: string;
}
import { ProjectEditorDraftStorageService } from '../services/project-editor-draft-storage.service';
import { EDITOR_SECTION_BOOTSTRAP_KEYS } from '../models/project-editor.model';
@@ -123,6 +132,37 @@ export class ProjectEditorFacade {
}
return sections;
});
/** Per-field before/after diff vs the originally loaded/published config, for the pre-publish preview. */
readonly changeSummary = computed<ChangeSummaryRow[]>(() => {
const current = this.bootstrap();
const original = this.state().originalBootstrap;
if (!current || !original) {
return [];
}
const rows: ChangeSummaryRow[] = [];
for (const key of this.modifiedFields()) {
const field = this.schema.getField(key);
rows.push({
fieldKey: key,
labelKey: field?.labelKey ?? key,
section: field?.section,
before: this.stringifyValue(this.schema.getByPath(original, key)),
after: this.stringifyValue(this.schema.getByPath(current, key)),
});
}
return rows;
});
private stringifyValue(value: unknown): string {
if (value === null || value === undefined) {
return '';
}
if (typeof value === 'object') {
const json = JSON.stringify(value);
return json.length > 80 ? `${json.slice(0, 77)}` : json;
}
return String(value);
}
loadBootstrap(): void {
this.configService.loadBootstrap(true).pipe(take(1)).subscribe({