feat(project-editor): add sticky save/publish bar with validation summary

Mounts a new ProjectEditorSaveBarComponent in the editor page that shows
draft/published status, unsaved-changes indicator, and validation issues,
wiring the Task 8 facade save()/publish()/dirty/status/validationIssues
signals to an actual UI for the first time.
This commit is contained in:
sdarbinyan
2026-07-13 15:00:03 +04:00
parent 85038df24a
commit 1e7add2fc8
9 changed files with 95 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
<div class="project-editor-save-bar">
<div class="project-editor-save-bar-status">
<span>{{ (status() === 'published' ? 'builder.statusPublished' : 'builder.statusDraft') | translate }}</span>
@if (dirty()) {
<span class="project-editor-save-bar-dirty">{{ 'builder.unsavedChanges' | translate }}</span>
}
@if (issues().length > 0) {
<ul class="project-editor-save-bar-issues">
@for (issue of issues(); track issue.code) {
<li>{{ issue.message | translate }}</li>
}
</ul>
}
</div>
<div class="project-editor-save-bar-actions">
<button type="button" (click)="save()">{{ 'builder.save' | translate }}</button>
<button type="button" [disabled]="issues().length > 0" (click)="publish()">{{ 'builder.publish' | translate }}</button>
</div>
</div>

View File

@@ -0,0 +1,27 @@
.project-editor-save-bar {
position: sticky;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 1rem;
padding: 0.75rem 1rem;
background: var(--surface, #fff);
border-top: 1px solid var(--border, #ddd);
z-index: 5;
}
.project-editor-save-bar-dirty {
color: var(--warning, #b45309);
margin-left: 0.5rem;
}
.project-editor-save-bar-issues {
margin: 0.25rem 0 0;
padding-left: 1.25rem;
color: var(--danger, #b91c1c);
}
.project-editor-save-bar-actions button + button {
margin-left: 0.5rem;
}

View File

@@ -0,0 +1,26 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { ProjectEditorFacade } from '../../facade/project-editor.facade';
@Component({
selector: 'app-project-editor-save-bar',
standalone: true,
imports: [TranslatePipe],
templateUrl: './project-editor-save-bar.component.html',
styleUrls: ['./project-editor-save-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectEditorSaveBarComponent {
private readonly facade = inject(ProjectEditorFacade);
readonly dirty = this.facade.dirty;
readonly status = this.facade.status;
readonly issues = this.facade.validationIssues;
save(): void {
this.facade.save();
}
publish(): void {
this.facade.publish();
}
}

View File

@@ -28,5 +28,6 @@
@case ('preview') { <app-project-editor-preview-section /> }
}
</section>
<app-project-editor-save-bar />
}
</main>

View File

@@ -17,6 +17,7 @@ import { ProjectEditorNavigationSectionComponent } from '../sections/navigation-
import { ProjectEditorPreviewSectionComponent } from '../sections/preview-section.component';
import { TranslatePipe } from '../../../i18n/translate.pipe';
import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component';
import { ProjectEditorSaveBarComponent } from '../components/save-bar/project-editor-save-bar.component';
import { ProjectEditorSectionId } from '../models/project-editor.model';
const KNOWN_SECTIONS: ProjectEditorSectionId[] = [
@@ -41,6 +42,7 @@ const KNOWN_SECTIONS: ProjectEditorSectionId[] = [
ProjectEditorLanguagesSectionComponent,
ProjectEditorNavigationSectionComponent,
ProjectEditorPreviewSectionComponent,
ProjectEditorSaveBarComponent,
],
templateUrl: './project-editor-page.component.html',
styleUrls: ['./project-editor-page.component.scss'],

View File

@@ -512,6 +512,11 @@ export const en: Translations = {
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.',
statusDraft: 'Draft',
statusPublished: 'Published',
unsavedChanges: 'Unsaved changes',
save: 'Save',
publish: 'Publish',
},
staticPages: {
notFound: 'Page not found',

View File

@@ -512,6 +512,11 @@ export const hy: Translations = {
validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։',
validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։',
validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։',
statusDraft: 'Սևագիր',
statusPublished: 'Հրապարակված',
unsavedChanges: 'Չպահված փոփոխություններ',
save: 'Պահպանել',
publish: 'Հրապարակել',
},
staticPages: {
notFound: 'Էջը չի գտնվել',

View File

@@ -512,6 +512,11 @@ export const ru: Translations = {
validationMissingWidget: 'В секции главной страницы есть виджет без типа.',
validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.',
validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.',
statusDraft: 'Черновик',
statusPublished: 'Опубликовано',
unsavedChanges: 'Есть несохранённые изменения',
save: 'Сохранить',
publish: 'Опубликовать',
},
staticPages: {
notFound: 'Страница не найдена',

View File

@@ -510,6 +510,11 @@ export interface Translations {
validationMissingWidget: string;
validationDuplicateNavLinks: string;
validationInvalidColors: string;
statusDraft: string;
statusPublished: string;
unsavedChanges: string;
save: string;
publish: string;
};
staticPages: {
notFound: string;