feat(project-editor): session undo/redo and modified-field tracking
Milestone 4 of the Configuration Engine sprint. - Add schema/history.util: pure undo/redo reducer (commit/undo/redo, depth cap) with full spec coverage. - Facade: debounced snapshot history (~300ms coalesce so a typing burst = one undo step); undo()/redo() route through the draft-save path so autosave never desyncs; canUndo/canRedo; history cleared on load/publish/resetDraft. modifiedFields (schema-diff vs original) + modifiedSections computeds. - save-bar: Undo/Redo buttons. Page: Ctrl/Cmd+Z / Shift+Z / Y shortcuts (skipped while a text field is focused so native text undo is preserved); beforeunload guard already present. - nav: amber modified-field dot per section (when no blocking badge). - i18n: builder.undo / builder.redo 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:
@@ -5,6 +5,6 @@
|
||||
routerLinkActive="active"
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
class="editor-nav-link"
|
||||
>{{ section.label | translate }}@if (issueCount(section.id); as count) {<span class="editor-nav-badge" [attr.aria-label]="count + ' ' + ('builder.title' | translate)">{{ count }}</span>}</a>
|
||||
>{{ section.label | translate }}@if (issueCount(section.id); as count) {<span class="editor-nav-badge" [attr.aria-label]="count + ' ' + ('builder.title' | translate)">{{ count }}</span>} @else if (isModified(section.id)) {<span class="editor-nav-dot" [attr.aria-label]="'builder.unsavedChanges' | translate"></span>}</a>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@@ -58,6 +58,20 @@
|
||||
color: var(--error-color, #ef4444);
|
||||
}
|
||||
|
||||
.editor-nav-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-left: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--warning-color, #f59e0b);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.editor-nav-link.active .editor-nav-dot {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.editor-nav-link {
|
||||
transition: none;
|
||||
|
||||
@@ -16,12 +16,18 @@ import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||
export class ProjectEditorNavComponent {
|
||||
private readonly facade = inject(ProjectEditorFacade);
|
||||
private readonly issuesBySection = this.facade.issuesBySection;
|
||||
private readonly modifiedSections = this.facade.modifiedSections;
|
||||
|
||||
/** Count of blocking issues in a section, for the nav badge. */
|
||||
issueCount(sectionId: ProjectEditorSectionId): number {
|
||||
return this.issuesBySection().get(sectionId) ?? 0;
|
||||
}
|
||||
|
||||
/** Whether a section has unsaved modified fields, for the nav dot. */
|
||||
isModified(sectionId: ProjectEditorSectionId): boolean {
|
||||
return this.modifiedSections().has(sectionId);
|
||||
}
|
||||
|
||||
readonly sections: Array<{ id: ProjectEditorSectionId; label: string }> = [
|
||||
{ id: 'general', label: 'builder.general' },
|
||||
{ id: 'branding', label: 'builder.branding' },
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
}
|
||||
</div>
|
||||
<div class="project-editor-save-bar-actions">
|
||||
<app-button variant="ghost" size="sm" [disabled]="!canUndo()" (click)="undo()">{{ 'builder.undo' | translate }}</app-button>
|
||||
<app-button variant="ghost" size="sm" [disabled]="!canRedo()" (click)="redo()">{{ 'builder.redo' | translate }}</app-button>
|
||||
<app-button variant="danger" size="sm" (click)="resetDraft()">{{ 'builder.resetDraft' | translate }}</app-button>
|
||||
<app-button variant="secondary" size="sm" (click)="save()">{{ 'builder.save' | translate }}</app-button>
|
||||
<app-button variant="primary" size="sm" [disabled]="hasBlockingIssues()" (click)="publish()">{{ 'builder.publish' | translate }}</app-button>
|
||||
|
||||
@@ -19,6 +19,16 @@ export class ProjectEditorSaveBarComponent {
|
||||
readonly status = this.facade.status;
|
||||
readonly issues = this.facade.validationIssues;
|
||||
readonly hasBlockingIssues = this.facade.hasBlockingIssues;
|
||||
readonly canUndo = this.facade.canUndo;
|
||||
readonly canRedo = this.facade.canRedo;
|
||||
|
||||
undo(): void {
|
||||
this.facade.undo();
|
||||
}
|
||||
|
||||
redo(): void {
|
||||
this.facade.redo();
|
||||
}
|
||||
readonly lastSavedAt = this.facade.lastSavedAt;
|
||||
readonly draftRestored = this.facade.draftRestored;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user