diff --git a/src/app/features/project-editor/facade/project-editor.facade.ts b/src/app/features/project-editor/facade/project-editor.facade.ts index 5e7cb8d..c07db96 100644 --- a/src/app/features/project-editor/facade/project-editor.facade.ts +++ b/src/app/features/project-editor/facade/project-editor.facade.ts @@ -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(() => { + 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({ diff --git a/src/app/features/project-editor/sections/preview-section.component.html b/src/app/features/project-editor/sections/preview-section.component.html index 2ffaa02..c4b4dd8 100644 --- a/src/app/features/project-editor/sections/preview-section.component.html +++ b/src/app/features/project-editor/sections/preview-section.component.html @@ -1,3 +1,38 @@ + + @if (issues().length > 0) { + + } @else { +

{{ 'builder.previewNoIssues' | translate }}

+ } + + @if (changeSummary().length > 0) { + + + + + + + + + + @for (row of changeSummary(); track row.fieldKey) { + + + + + + } + +
{{ 'builder.previewChangeField' | translate }}{{ 'builder.previewChangeBefore' | translate }}{{ 'builder.previewChangeAfter' | translate }}
{{ row.labelKey | translate }}{{ row.before || '—' }}{{ row.after || '—' }}
+ } @else { +

{{ 'builder.previewNoChanges' | translate }}

+ } +
+
{{ 'builder.livePreview' | translate }} diff --git a/src/app/features/project-editor/sections/preview-section.component.ts b/src/app/features/project-editor/sections/preview-section.component.ts index 19b3f04..42df348 100644 --- a/src/app/features/project-editor/sections/preview-section.component.ts +++ b/src/app/features/project-editor/sections/preview-section.component.ts @@ -18,6 +18,9 @@ export class ProjectEditorPreviewSectionComponent { readonly exportValue = signal(''); readonly importValue = signal(''); readonly importError = this.facade.importError; + readonly changeSummary = this.facade.changeSummary; + readonly issues = this.facade.validationIssues; + readonly hasBlockingIssues = this.facade.hasBlockingIssues; refreshExport(): void { this.exportValue.set(this.facade.exportBootstrap()); diff --git a/src/app/features/project-editor/sections/section.shared.scss b/src/app/features/project-editor/sections/section.shared.scss index 34fd465..a1a86ed 100644 --- a/src/app/features/project-editor/sections/section.shared.scss +++ b/src/app/features/project-editor/sections/section.shared.scss @@ -170,6 +170,57 @@ button.secondary { color: #991b1b; } +.preview-validation { + margin: 0 0 12px; + padding-left: 1.25rem; + color: var(--warning-color, #b45309); + + &.has-errors { + color: var(--error-color, #991b1b); + } + + li.is-warning { + color: var(--warning-color, #b45309); + } + + li.is-error { + color: var(--error-color, #991b1b); + } +} + +.preview-validation-ok { + margin: 0 0 12px; + color: var(--text-secondary, #5f6e6a); +} + +.preview-changes { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + + th, + td { + text-align: left; + padding: 8px 10px; + border-bottom: 1px solid var(--border-color, #d3dad9); + } + + th { + color: var(--text-secondary, #5f6e6a); + font-weight: 600; + } + + .preview-before { + color: var(--text-secondary, #5f6e6a); + text-decoration: line-through; + } + + .preview-after { + color: var(--text-primary, #1e3c38); + font-weight: 600; + } +} + @media (max-width: 900px) { .editor-grid.two, .editor-grid.three, diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index d051767..afbe40d 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -502,6 +502,12 @@ export const en: Translations = { searchSuggestions: 'Search Suggestions', searchHistory: 'Search History', livePreview: 'Live Preview', + previewChangesTitle: 'Changes since last publish', + previewNoIssues: 'No validation issues.', + previewNoChanges: 'No changes since the last publish.', + previewChangeField: 'Field', + previewChangeBefore: 'Before', + previewChangeAfter: 'After', languagesTab: 'Languages', addLanguage: 'Add Language', removeLanguage: 'Remove', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 0b104d5..7373c80 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -502,6 +502,12 @@ export const hy: Translations = { searchSuggestions: 'Որոնման առաջարկներ', searchHistory: 'Որոնման պատմություն', livePreview: 'Կենդանի preview', + previewChangesTitle: 'Փոփոխություններ վերջին հրապարակումից ի վեր', + previewNoIssues: 'Վավերացման խնդիրներ չկան։', + previewNoChanges: 'Փոփոխություններ վերջին հրապարակումից ի վեր չկան։', + previewChangeField: 'Դաշտ', + previewChangeBefore: 'Մինչ', + previewChangeAfter: 'Հետո', languagesTab: 'Լեզուներ', addLanguage: 'Ավելացնել լեզու', removeLanguage: 'Հեռացնել', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 99d8ef3..0e03086 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -502,6 +502,12 @@ export const ru: Translations = { searchSuggestions: 'Поисковые подсказки', searchHistory: 'История поиска', livePreview: 'Живое превью', + previewChangesTitle: 'Изменения с последней публикации', + previewNoIssues: 'Нет проблем валидации.', + previewNoChanges: 'Нет изменений с последней публикации.', + previewChangeField: 'Поле', + previewChangeBefore: 'До', + previewChangeAfter: 'После', languagesTab: 'Языки', addLanguage: 'Добавить язык', removeLanguage: 'Удалить', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index b8e6756..a0367be 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -500,6 +500,12 @@ export interface Translations { searchSuggestions: string; searchHistory: string; livePreview: string; + previewChangesTitle: string; + previewNoIssues: string; + previewNoChanges: string; + previewChangeField: string; + previewChangeBefore: string; + previewChangeAfter: string; languagesTab: string; addLanguage: string; removeLanguage: string;