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:
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,5 +28,6 @@
|
|||||||
@case ('preview') { <app-project-editor-preview-section /> }
|
@case ('preview') { <app-project-editor-preview-section /> }
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
<app-project-editor-save-bar />
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ProjectEditorNavigationSectionComponent } from '../sections/navigation-
|
|||||||
import { ProjectEditorPreviewSectionComponent } from '../sections/preview-section.component';
|
import { ProjectEditorPreviewSectionComponent } from '../sections/preview-section.component';
|
||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
import { StaticPagesEditorComponent } from '../../content-management/components/static-pages-editor.component';
|
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';
|
import { ProjectEditorSectionId } from '../models/project-editor.model';
|
||||||
|
|
||||||
const KNOWN_SECTIONS: ProjectEditorSectionId[] = [
|
const KNOWN_SECTIONS: ProjectEditorSectionId[] = [
|
||||||
@@ -41,6 +42,7 @@ const KNOWN_SECTIONS: ProjectEditorSectionId[] = [
|
|||||||
ProjectEditorLanguagesSectionComponent,
|
ProjectEditorLanguagesSectionComponent,
|
||||||
ProjectEditorNavigationSectionComponent,
|
ProjectEditorNavigationSectionComponent,
|
||||||
ProjectEditorPreviewSectionComponent,
|
ProjectEditorPreviewSectionComponent,
|
||||||
|
ProjectEditorSaveBarComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './project-editor-page.component.html',
|
templateUrl: './project-editor-page.component.html',
|
||||||
styleUrls: ['./project-editor-page.component.scss'],
|
styleUrls: ['./project-editor-page.component.scss'],
|
||||||
|
|||||||
@@ -512,6 +512,11 @@ export const en: Translations = {
|
|||||||
validationMissingWidget: 'A homepage section has a widget with no type.',
|
validationMissingWidget: 'A homepage section has a widget with no type.',
|
||||||
validationDuplicateNavLinks: 'Two or more header navigation links are duplicates.',
|
validationDuplicateNavLinks: 'Two or more header navigation links are duplicates.',
|
||||||
validationInvalidColors: 'One or more theme colors are not valid hex colors.',
|
validationInvalidColors: 'One or more theme colors are not valid hex colors.',
|
||||||
|
statusDraft: 'Draft',
|
||||||
|
statusPublished: 'Published',
|
||||||
|
unsavedChanges: 'Unsaved changes',
|
||||||
|
save: 'Save',
|
||||||
|
publish: 'Publish',
|
||||||
},
|
},
|
||||||
staticPages: {
|
staticPages: {
|
||||||
notFound: 'Page not found',
|
notFound: 'Page not found',
|
||||||
|
|||||||
@@ -512,6 +512,11 @@ export const hy: Translations = {
|
|||||||
validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։',
|
validationMissingWidget: 'Գլխավոր էջի սեկցիաներից մեկն ունի վիջեթ առանց տիպի։',
|
||||||
validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։',
|
validationDuplicateNavLinks: 'Վերնագրի նավիգացիայում կան կրկնվող հղումներ։',
|
||||||
validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։',
|
validationInvalidColors: 'Թեմայի գույներից մեկը կամ մի քանիսը վավեր hex գույն չեն։',
|
||||||
|
statusDraft: 'Սևագիր',
|
||||||
|
statusPublished: 'Հրապարակված',
|
||||||
|
unsavedChanges: 'Չպահված փոփոխություններ',
|
||||||
|
save: 'Պահպանել',
|
||||||
|
publish: 'Հրապարակել',
|
||||||
},
|
},
|
||||||
staticPages: {
|
staticPages: {
|
||||||
notFound: 'Էջը չի գտնվել',
|
notFound: 'Էջը չի գտնվել',
|
||||||
|
|||||||
@@ -512,6 +512,11 @@ export const ru: Translations = {
|
|||||||
validationMissingWidget: 'В секции главной страницы есть виджет без типа.',
|
validationMissingWidget: 'В секции главной страницы есть виджет без типа.',
|
||||||
validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.',
|
validationDuplicateNavLinks: 'Две или более ссылки в навигации шапки дублируются.',
|
||||||
validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.',
|
validationInvalidColors: 'Один или несколько цветов темы указаны некорректно.',
|
||||||
|
statusDraft: 'Черновик',
|
||||||
|
statusPublished: 'Опубликовано',
|
||||||
|
unsavedChanges: 'Есть несохранённые изменения',
|
||||||
|
save: 'Сохранить',
|
||||||
|
publish: 'Опубликовать',
|
||||||
},
|
},
|
||||||
staticPages: {
|
staticPages: {
|
||||||
notFound: 'Страница не найдена',
|
notFound: 'Страница не найдена',
|
||||||
|
|||||||
@@ -510,6 +510,11 @@ export interface Translations {
|
|||||||
validationMissingWidget: string;
|
validationMissingWidget: string;
|
||||||
validationDuplicateNavLinks: string;
|
validationDuplicateNavLinks: string;
|
||||||
validationInvalidColors: string;
|
validationInvalidColors: string;
|
||||||
|
statusDraft: string;
|
||||||
|
statusPublished: string;
|
||||||
|
unsavedChanges: string;
|
||||||
|
save: string;
|
||||||
|
publish: string;
|
||||||
};
|
};
|
||||||
staticPages: {
|
staticPages: {
|
||||||
notFound: string;
|
notFound: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user