feat: Page Editor UX Phase 2 - unsaved changes panel, property search, reset property, empty states
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- ProjectEditorFacade.resetField(key): field-level revert-to-original, reusing getByPath + a new immutable setByPath (write-side counterpart, scalar/object dot-paths only). - app-form-field gains showReset/resetLabel/(resetClicked) - a reusable per-field reset affordance, wired on branding logo/title and theme primary/background color. - Save bar: unsaved-changes count is now clickable, expanding a field-level diff list (reuses facade.changeSummary(), already built for the Preview tab) with jump-to-section links. - BuilderPropertySearchComponent: filters EditorSchemaService.all() by translated label/hint, jumps to the owning section - no new registry, reuses the existing schema. - navigation-section: app-empty-state (existing component) added for empty header/footer link lists. - Reset Section and Draft/Published badge were already implemented; not touched. - New copy added as translation keys (en/ru/hy).
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="builder-property-search">
|
||||||
|
<label class="builder-property-search__label" for="builder-property-search-input">
|
||||||
|
<app-icon name="search" [size]="14" aria-hidden="true" />
|
||||||
|
<span class="sr-only">{{ 'builder.propertySearchLabel' | translate }}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="builder-property-search-input"
|
||||||
|
type="search"
|
||||||
|
class="builder-property-search__input"
|
||||||
|
[placeholder]="'builder.propertySearchPlaceholder' | translate"
|
||||||
|
[attr.aria-label]="'builder.propertySearchLabel' | translate"
|
||||||
|
[ngModel]="query()"
|
||||||
|
(ngModelChange)="query.set($event)"
|
||||||
|
(focus)="onFocus()"
|
||||||
|
(blur)="onBlur()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
@if (open() && query().trim()) {
|
||||||
|
<ul class="builder-property-search__results" [attr.aria-label]="'builder.propertySearchResultsLabel' | translate">
|
||||||
|
@for (result of results(); track result.key) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="['/edit', result.sectionId] | langRoute" (click)="selectResult()">
|
||||||
|
<span class="builder-property-search__result-label">{{ result.label }}</span>
|
||||||
|
<span class="builder-property-search__result-section">{{ result.sectionLabel }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
} @empty {
|
||||||
|
<li class="builder-property-search__empty">{{ 'builder.propertySearchNoResults' | translate }}</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
.builder-property-search {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-property-search__label {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--space-sm, 0.5rem);
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-property-search__input {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 var(--space-sm, 0.5rem) 0 32px;
|
||||||
|
border: 1px solid var(--border-color, #e4e4e7);
|
||||||
|
border-radius: var(--radius-md, 6px);
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
color: var(--text-primary, #1f322d);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color, #2f6e5d);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color, #2f6e5d) 25%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-property-search__results {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-xs, 0.25rem);
|
||||||
|
list-style: none;
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
border: 1px solid var(--border-color, #e4e4e7);
|
||||||
|
border-radius: var(--radius-md, 6px);
|
||||||
|
box-shadow: var(--shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm, 0.5rem);
|
||||||
|
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
color: var(--text-primary, #1f322d);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background: var(--bg-secondary, #f4f4f5);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-property-search__result-section {
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
font-size: var(--font-size-xs, 0.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.builder-property-search__empty {
|
||||||
|
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { EditorSchemaService } from '../../schema/editor-schema.service';
|
||||||
|
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||||
|
import { TranslateService } from '../../../../i18n/translate.service';
|
||||||
|
import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
|
||||||
|
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
|
|
||||||
|
interface PropertySearchResult {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
hint: string;
|
||||||
|
sectionId: string;
|
||||||
|
sectionLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_LABEL_KEYS: Record<string, string> = {
|
||||||
|
general: 'builder.general',
|
||||||
|
branding: 'builder.branding',
|
||||||
|
theme: 'builder.theme',
|
||||||
|
header: 'builder.header',
|
||||||
|
footer: 'builder.footer',
|
||||||
|
homepage: 'builder.homepage',
|
||||||
|
widgets: 'builder.widgets',
|
||||||
|
'static-pages': 'builder.staticPages',
|
||||||
|
features: 'builder.marketplaceFeatures',
|
||||||
|
languages: 'builder.languagesTab',
|
||||||
|
navigation: 'builder.navigationTab',
|
||||||
|
preview: 'builder.preview',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the field-schema registry (EditorSchemaService.all(), the single source of
|
||||||
|
* truth already used for validation/readiness) by translated label/hint text, so a
|
||||||
|
* non-technical admin can find "where do I change X" without knowing which section
|
||||||
|
* it lives in. Selecting a result jumps to that field's section.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'app-builder-property-search',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule, TranslatePipe, RouterLink, LangRoutePipe, IconComponent],
|
||||||
|
templateUrl: './builder-property-search.component.html',
|
||||||
|
styleUrl: './builder-property-search.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class BuilderPropertySearchComponent {
|
||||||
|
private readonly schema = inject(EditorSchemaService);
|
||||||
|
private readonly translate = inject(TranslateService);
|
||||||
|
|
||||||
|
readonly query = signal('');
|
||||||
|
readonly open = signal(false);
|
||||||
|
|
||||||
|
private readonly allResults = computed<PropertySearchResult[]>(() =>
|
||||||
|
this.schema.all().map(field => ({
|
||||||
|
key: field.key,
|
||||||
|
label: this.translate.t(field.labelKey),
|
||||||
|
hint: field.hintKey ? this.translate.t(field.hintKey) : '',
|
||||||
|
sectionId: field.section,
|
||||||
|
sectionLabel: this.translate.t(SECTION_LABEL_KEYS[field.section] ?? field.section),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly results = computed<PropertySearchResult[]>(() => {
|
||||||
|
const term = this.query().trim().toLowerCase();
|
||||||
|
if (!term) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return this.allResults()
|
||||||
|
.filter(result =>
|
||||||
|
result.label.toLowerCase().includes(term) ||
|
||||||
|
result.hint.toLowerCase().includes(term) ||
|
||||||
|
result.sectionLabel.toLowerCase().includes(term)
|
||||||
|
)
|
||||||
|
.slice(0, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
onFocus(): void {
|
||||||
|
this.open.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur(): void {
|
||||||
|
// Deferred so a click on a result (which fires before blur settles) still registers.
|
||||||
|
setTimeout(() => this.open.set(false), 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RouterLink on the result anchor handles navigation; this just resets the search UI after the click. */
|
||||||
|
selectResult(): void {
|
||||||
|
this.query.set('');
|
||||||
|
this.open.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@
|
|||||||
<div class="project-editor-save-bar-status" role="status" aria-live="polite">
|
<div class="project-editor-save-bar-status" role="status" aria-live="polite">
|
||||||
<span>{{ (status() === 'published' ? 'builder.statusPublished' : 'builder.statusDraft') | translate }}</span>
|
<span>{{ (status() === 'published' ? 'builder.statusPublished' : 'builder.statusDraft') | translate }}</span>
|
||||||
@if (dirty()) {
|
@if (dirty()) {
|
||||||
<span class="project-editor-save-bar-dirty">{{ 'builder.unsavedChanges' | translate }}</span>
|
<button type="button" class="project-editor-save-bar-dirty project-editor-save-bar-dirty--toggle" [attr.aria-expanded]="changesOpen()" (click)="toggleChanges()">
|
||||||
|
{{ 'builder.unsavedChanges' | translate }} ({{ changeSummary().length }})
|
||||||
|
</button>
|
||||||
} @else if (lastSavedAt()) {
|
} @else if (lastSavedAt()) {
|
||||||
<span class="project-editor-save-bar-saved-at">{{ 'builder.lastSaved' | translate }}: {{ formatSavedAt(lastSavedAt()!) }}</span>
|
<span class="project-editor-save-bar-saved-at">{{ 'builder.lastSaved' | translate }}: {{ formatSavedAt(lastSavedAt()!) }}</span>
|
||||||
}
|
}
|
||||||
@@ -20,6 +22,18 @@
|
|||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (changesOpen() && changeSummary().length > 0) {
|
||||||
|
<ul class="project-editor-save-bar-changes" [attr.aria-label]="'builder.unsavedChangesPanelTitle' | translate">
|
||||||
|
@for (row of changeSummary(); track row.fieldKey) {
|
||||||
|
<li>
|
||||||
|
<a [routerLink]="row.section ? (['/edit', row.section] | langRoute) : null" (click)="toggleChanges()">
|
||||||
|
<span class="project-editor-save-bar-changes__field">{{ row.labelKey | translate }}</span>
|
||||||
|
<span class="project-editor-save-bar-changes__diff">{{ row.before || '—' }} → {{ row.after || '—' }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
<div class="project-editor-save-bar-actions">
|
<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]="!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="ghost" size="sm" [disabled]="!canRedo()" (click)="redo()">{{ 'builder.redo' | translate }}</app-button>
|
||||||
|
|||||||
@@ -16,6 +16,62 @@
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-editor-save-bar-dirty--toggle {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color, #2f6e5d);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-editor-save-bar-changes {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-xs, 0.25rem);
|
||||||
|
list-style: none;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg-primary, #fff);
|
||||||
|
border: 1px solid var(--border-color, #ddd);
|
||||||
|
border-bottom: none;
|
||||||
|
box-shadow: var(--shadow-md, 0 -4px 12px rgba(0, 0, 0, 0.08));
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm, 0.5rem);
|
||||||
|
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.5rem);
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
color: var(--text-primary, #1f322d);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
background: var(--bg-secondary, #f4f4f5);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-editor-save-bar-changes__diff {
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
font-size: var(--font-size-xs, 0.75rem);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.project-editor-save-bar-issues {
|
.project-editor-save-bar-issues {
|
||||||
margin: 0.25rem 0 0;
|
margin: 0.25rem 0 0;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||||
import { TranslateService } from '../../../../i18n/translate.service';
|
import { TranslateService } from '../../../../i18n/translate.service';
|
||||||
|
import { LangRoutePipe } from '../../../../pipes/lang-route.pipe';
|
||||||
import { ProjectEditorFacade } from '../../facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../../facade/project-editor.facade';
|
||||||
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
||||||
import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component';
|
import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/confirm-dialog.component';
|
||||||
@@ -8,7 +10,7 @@ import { ConfirmDialogComponent } from '../../../../shared/ui/confirm-dialog/con
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-editor-save-bar',
|
selector: 'app-project-editor-save-bar',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [TranslatePipe, ButtonComponent, ConfirmDialogComponent],
|
imports: [TranslatePipe, ButtonComponent, ConfirmDialogComponent, RouterLink, LangRoutePipe],
|
||||||
templateUrl: './project-editor-save-bar.component.html',
|
templateUrl: './project-editor-save-bar.component.html',
|
||||||
styleUrls: ['./project-editor-save-bar.component.scss'],
|
styleUrls: ['./project-editor-save-bar.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
@@ -22,6 +24,12 @@ export class ProjectEditorSaveBarComponent {
|
|||||||
readonly hasBlockingIssues = this.facade.hasBlockingIssues;
|
readonly hasBlockingIssues = this.facade.hasBlockingIssues;
|
||||||
readonly canUndo = this.facade.canUndo;
|
readonly canUndo = this.facade.canUndo;
|
||||||
readonly canRedo = this.facade.canRedo;
|
readonly canRedo = this.facade.canRedo;
|
||||||
|
readonly changeSummary = this.facade.changeSummary;
|
||||||
|
readonly changesOpen = signal(false);
|
||||||
|
|
||||||
|
toggleChanges(): void {
|
||||||
|
this.changesOpen.update(open => !open);
|
||||||
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
this.facade.undo();
|
this.facade.undo();
|
||||||
|
|||||||
@@ -453,6 +453,34 @@ export class ProjectEditorFacade {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverts a single schema-registered field to its originally loaded/published value.
|
||||||
|
* Field-level counterpart to resetSection - same "revert to original" semantics, scoped to
|
||||||
|
* one dot-path instead of a whole bootstrap key. No confirmation needed: it's a single-value
|
||||||
|
* change that flows through updateBootstrap like any edit, so undo covers it.
|
||||||
|
*/
|
||||||
|
resetField(key: string): void {
|
||||||
|
const original = this.state().originalBootstrap;
|
||||||
|
if (!original) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = JSON.parse(JSON.stringify(this.schema.getByPath(original, key)));
|
||||||
|
this.updateBootstrap(current => this.setByPath(current, key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Immutable dot-path setter, the write-side counterpart to EditorSchemaService.getByPath. Only used for resetField's scalar/object schema paths - none of the registered field keys index into array elements. */
|
||||||
|
private setByPath<T extends object>(source: T, key: string, value: unknown): T {
|
||||||
|
const [head, ...rest] = key.split('.');
|
||||||
|
if (rest.length === 0) {
|
||||||
|
return { ...source, [head]: value };
|
||||||
|
}
|
||||||
|
const child = (source as Record<string, unknown>)[head];
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
[head]: this.setByPath(typeof child === 'object' && child !== null ? child : {}, rest.join('.'), value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Discards the entire draft, reverting to the originally loaded/published bootstrap. Caller is responsible for confirmation UX. */
|
/** Discards the entire draft, reverting to the originally loaded/published bootstrap. Caller is responsible for confirmation UX. */
|
||||||
resetDraft(): void {
|
resetDraft(): void {
|
||||||
const original = this.state().originalBootstrap;
|
const original = this.state().originalBootstrap;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<app-icon name="home" [size]="16" />
|
<app-icon name="home" [size]="16" />
|
||||||
{{ 'builder.backToDashboard' | translate }}
|
{{ 'builder.backToDashboard' | translate }}
|
||||||
</a>
|
</a>
|
||||||
|
<app-builder-property-search class="project-editor-layout__search" />
|
||||||
<app-project-editor-nav (linkClicked)="closeMobileDrawer()" />
|
<app-project-editor-nav (linkClicked)="closeMobileDrawer()" />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
|||||||
@@ -309,6 +309,11 @@
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project-editor-layout__search {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--space-md, 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
.project-editor-live-preview__hint {
|
.project-editor-live-preview__hint {
|
||||||
margin: 0 0 var(--space-sm, 0.5rem);
|
margin: 0 0 var(--space-sm, 0.5rem);
|
||||||
font-size: var(--font-size-xs, 0.75rem);
|
font-size: var(--font-size-xs, 0.75rem);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { ProjectEditorSaveBarComponent } from '../components/save-bar/project-ed
|
|||||||
import { EDITOR_SECTION_BOOTSTRAP_KEYS, ProjectEditorSectionId } from '../models/project-editor.model';
|
import { EDITOR_SECTION_BOOTSTRAP_KEYS, ProjectEditorSectionId } from '../models/project-editor.model';
|
||||||
import { BUILDER_GROUPS, SECTION_TO_GROUP } from '../builder/builder-groups.model';
|
import { BUILDER_GROUPS, SECTION_TO_GROUP } from '../builder/builder-groups.model';
|
||||||
import { BuilderLivePreviewComponent } from '../components/live-preview/builder-live-preview.component';
|
import { BuilderLivePreviewComponent } from '../components/live-preview/builder-live-preview.component';
|
||||||
|
import { BuilderPropertySearchComponent } from '../components/property-search/builder-property-search.component';
|
||||||
|
|
||||||
/** Sections that currently expose highlight-mapped fields (see HighlightSourceDirective usages) - showing the panel elsewhere would just be an inert, unhighlightable diagram. */
|
/** Sections that currently expose highlight-mapped fields (see HighlightSourceDirective usages) - showing the panel elsewhere would just be an inert, unhighlightable diagram. */
|
||||||
const SECTIONS_WITH_LIVE_PREVIEW: ProjectEditorSectionId[] = ['branding', 'theme', 'homepage'];
|
const SECTIONS_WITH_LIVE_PREVIEW: ProjectEditorSectionId[] = ['branding', 'theme', 'homepage'];
|
||||||
@@ -73,6 +74,7 @@ const SECTION_LABEL_KEYS: Record<ProjectEditorSectionId, string> = {
|
|||||||
ButtonComponent,
|
ButtonComponent,
|
||||||
ConfirmDialogComponent,
|
ConfirmDialogComponent,
|
||||||
BuilderLivePreviewComponent,
|
BuilderLivePreviewComponent,
|
||||||
|
BuilderPropertySearchComponent,
|
||||||
],
|
],
|
||||||
templateUrl: './project-editor-page.component.html',
|
templateUrl: './project-editor-page.component.html',
|
||||||
styleUrls: ['./project-editor-page.component.scss'],
|
styleUrls: ['./project-editor-page.component.scss'],
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
[error]="fieldError('branding.logoUrl')"
|
[error]="fieldError('branding.logoUrl')"
|
||||||
[usedBy]="[('builder.usedByHeader' | translate), ('builder.usedByFooter' | translate)]"
|
[usedBy]="[('builder.usedByHeader' | translate), ('builder.usedByFooter' | translate)]"
|
||||||
[usedByLabel]="'builder.usedByLabel' | translate"
|
[usedByLabel]="'builder.usedByLabel' | translate"
|
||||||
|
[showReset]="isFieldModified('branding.logoUrl')"
|
||||||
|
[resetLabel]="'builder.resetField' | translate"
|
||||||
|
(resetClicked)="resetField('branding.logoUrl')"
|
||||||
>
|
>
|
||||||
<app-image-field [value]="bootstrap.branding.logoUrl" (valueChange)="updateField('logoUrl', $event)" />
|
<app-image-field [value]="bootstrap.branding.logoUrl" (valueChange)="updateField('logoUrl', $event)" />
|
||||||
</app-form-field>
|
</app-form-field>
|
||||||
@@ -28,6 +31,9 @@
|
|||||||
[hint]="'builder.marketplaceTitleDesc' | translate"
|
[hint]="'builder.marketplaceTitleDesc' | translate"
|
||||||
[usedBy]="[('builder.usedByHeader' | translate), ('builder.usedBySeo' | translate)]"
|
[usedBy]="[('builder.usedByHeader' | translate), ('builder.usedBySeo' | translate)]"
|
||||||
[usedByLabel]="'builder.usedByLabel' | translate"
|
[usedByLabel]="'builder.usedByLabel' | translate"
|
||||||
|
[showReset]="isFieldModified('seo.default.title')"
|
||||||
|
[resetLabel]="'builder.resetField' | translate"
|
||||||
|
(resetClicked)="resetField('seo.default.title')"
|
||||||
>
|
>
|
||||||
<app-input [ngModel]="bootstrap.seo.default.title" (ngModelChange)="updateMarketplaceTitle($event)" />
|
<app-input [ngModel]="bootstrap.seo.default.title" (ngModelChange)="updateMarketplaceTitle($event)" />
|
||||||
</app-form-field>
|
</app-form-field>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ export class ProjectEditorBrandingSectionComponent {
|
|||||||
const messageKey = this.facade.fieldError(key);
|
const messageKey = this.facade.fieldError(key);
|
||||||
return messageKey ? this.translate.t(messageKey) : null;
|
return messageKey ? this.translate.t(messageKey) : null;
|
||||||
};
|
};
|
||||||
|
readonly isFieldModified = (key: string): boolean => this.facade.modifiedFields().has(key);
|
||||||
|
resetField(key: string): void {
|
||||||
|
this.facade.resetField(key);
|
||||||
|
}
|
||||||
|
|
||||||
updateField<K extends BrandingImageField | 'brandName'>(key: K, value: string): void {
|
updateField<K extends BrandingImageField | 'brandName'>(key: K, value: string): void {
|
||||||
this.facade.updateBootstrap(current => ({
|
this.facade.updateBootstrap(current => ({
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stack-list">
|
<div class="stack-list">
|
||||||
|
@if (headerLinks().length === 0) {
|
||||||
|
<app-empty-state [title]="'builder.navigationHeaderEmptyTitle' | translate" [description]="'builder.navigationHeaderEmptyDesc' | translate" />
|
||||||
|
}
|
||||||
@for (item of headerLinks(); track item.id) {
|
@for (item of headerLinks(); track item.id) {
|
||||||
<article class="sub-card">
|
<article class="sub-card">
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
@@ -45,6 +48,9 @@
|
|||||||
|
|
||||||
@if (footerLinks(); as footer) {
|
@if (footerLinks(); as footer) {
|
||||||
<div class="stack-list">
|
<div class="stack-list">
|
||||||
|
@if (footer.length === 0) {
|
||||||
|
<app-empty-state [title]="'builder.navigationFooterEmptyTitle' | translate" [description]="'builder.navigationFooterEmptyDesc' | translate" />
|
||||||
|
}
|
||||||
@for (item of footer; track item.id) {
|
@for (item of footer; track item.id) {
|
||||||
<article class="sub-card">
|
<article class="sub-card">
|
||||||
<div class="editor-actions">
|
<div class="editor-actions">
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { SectionCardComponent } from '../../../shared/ui/section-card/section-ca
|
|||||||
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
import { ToggleComponent } from '../../../shared/ui/toggle/toggle.component';
|
||||||
import { LocaleTabsComponent } from '../../../shared/ui/locale-tabs/locale-tabs.component';
|
import { LocaleTabsComponent } from '../../../shared/ui/locale-tabs/locale-tabs.component';
|
||||||
import { SelectComponent, SelectOption } from '../../../shared/ui/select/select.component';
|
import { SelectComponent, SelectOption } from '../../../shared/ui/select/select.component';
|
||||||
|
import { EmptyStateComponent } from '../../../shared/ui/empty-state/empty-state.component';
|
||||||
import { ContentManagementFacade } from '../../content-management/facade/content-management.facade';
|
import { ContentManagementFacade } from '../../content-management/facade/content-management.facade';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-editor-navigation-section',
|
selector: 'app-project-editor-navigation-section',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, SectionCardComponent, ToggleComponent, LocaleTabsComponent, SelectComponent],
|
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, SectionCardComponent, ToggleComponent, LocaleTabsComponent, SelectComponent, EmptyStateComponent],
|
||||||
templateUrl: './navigation-section.component.html',
|
templateUrl: './navigation-section.component.html',
|
||||||
styleUrls: ['./section.shared.scss'],
|
styleUrls: ['./section.shared.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
[error]="fieldError('theme.palette.primary')"
|
[error]="fieldError('theme.palette.primary')"
|
||||||
[usedBy]="[('builder.usedByButtons' | translate), ('builder.usedByLinks' | translate), ('builder.usedByBadges' | translate)]"
|
[usedBy]="[('builder.usedByButtons' | translate), ('builder.usedByLinks' | translate), ('builder.usedByBadges' | translate)]"
|
||||||
[usedByLabel]="'builder.usedByLabel' | translate"
|
[usedByLabel]="'builder.usedByLabel' | translate"
|
||||||
|
[showReset]="isFieldModified('theme.palette.primary')"
|
||||||
|
[resetLabel]="'builder.resetField' | translate"
|
||||||
|
(resetClicked)="resetField('theme.palette.primary')"
|
||||||
><app-color-picker [ariaLabel]="'builder.primaryColor' | translate" [ngModel]="bootstrap.theme.palette.primary" (ngModelChange)="updatePalette('primary', $event)" /></app-form-field>
|
><app-color-picker [ariaLabel]="'builder.primaryColor' | translate" [ngModel]="bootstrap.theme.palette.primary" (ngModelChange)="updatePalette('primary', $event)" /></app-form-field>
|
||||||
<app-form-field [label]="'builder.secondaryColor' | translate" [hint]="'builder.secondaryColorDesc' | translate" [error]="fieldError('theme.palette.secondary')"><app-color-picker [ariaLabel]="'builder.secondaryColor' | translate" [ngModel]="bootstrap.theme.palette.secondary" (ngModelChange)="updatePalette('secondary', $event)" /></app-form-field>
|
<app-form-field [label]="'builder.secondaryColor' | translate" [hint]="'builder.secondaryColorDesc' | translate" [error]="fieldError('theme.palette.secondary')"><app-color-picker [ariaLabel]="'builder.secondaryColor' | translate" [ngModel]="bootstrap.theme.palette.secondary" (ngModelChange)="updatePalette('secondary', $event)" /></app-form-field>
|
||||||
<app-form-field
|
<app-form-field
|
||||||
@@ -17,6 +20,9 @@
|
|||||||
[error]="fieldError('theme.palette.backgroundPrimary')"
|
[error]="fieldError('theme.palette.backgroundPrimary')"
|
||||||
[usedBy]="[('builder.usedByPageBackground' | translate)]"
|
[usedBy]="[('builder.usedByPageBackground' | translate)]"
|
||||||
[usedByLabel]="'builder.usedByLabel' | translate"
|
[usedByLabel]="'builder.usedByLabel' | translate"
|
||||||
|
[showReset]="isFieldModified('theme.palette.backgroundPrimary')"
|
||||||
|
[resetLabel]="'builder.resetField' | translate"
|
||||||
|
(resetClicked)="resetField('theme.palette.backgroundPrimary')"
|
||||||
><app-color-picker [ariaLabel]="'builder.backgroundColor' | translate" [ngModel]="bootstrap.theme.palette.backgroundPrimary" (ngModelChange)="updatePalette('backgroundPrimary', $event)" /></app-form-field>
|
><app-color-picker [ariaLabel]="'builder.backgroundColor' | translate" [ngModel]="bootstrap.theme.palette.backgroundPrimary" (ngModelChange)="updatePalette('backgroundPrimary', $event)" /></app-form-field>
|
||||||
<app-form-field [label]="'builder.surfaceColor' | translate" [hint]="'builder.surfaceColorDesc' | translate" [error]="fieldError('theme.palette.backgroundSecondary')"><app-color-picker [ariaLabel]="'builder.surfaceColor' | translate" [ngModel]="bootstrap.theme.palette.backgroundSecondary" (ngModelChange)="updatePalette('backgroundSecondary', $event)" /></app-form-field>
|
<app-form-field [label]="'builder.surfaceColor' | translate" [hint]="'builder.surfaceColorDesc' | translate" [error]="fieldError('theme.palette.backgroundSecondary')"><app-color-picker [ariaLabel]="'builder.surfaceColor' | translate" [ngModel]="bootstrap.theme.palette.backgroundSecondary" (ngModelChange)="updatePalette('backgroundSecondary', $event)" /></app-form-field>
|
||||||
<app-form-field [label]="'builder.textColor' | translate" [hint]="'builder.textColorDesc' | translate" [error]="fieldError('theme.palette.textPrimary')"><app-color-picker [ariaLabel]="'builder.textColor' | translate" [ngModel]="bootstrap.theme.palette.textPrimary" (ngModelChange)="updatePalette('textPrimary', $event)" /></app-form-field>
|
<app-form-field [label]="'builder.textColor' | translate" [hint]="'builder.textColorDesc' | translate" [error]="fieldError('theme.palette.textPrimary')"><app-color-picker [ariaLabel]="'builder.textColor' | translate" [ngModel]="bootstrap.theme.palette.textPrimary" (ngModelChange)="updatePalette('textPrimary', $event)" /></app-form-field>
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export class ProjectEditorThemeSectionComponent {
|
|||||||
const messageKey = this.facade.fieldError(key);
|
const messageKey = this.facade.fieldError(key);
|
||||||
return messageKey ? this.translate.t(messageKey) : null;
|
return messageKey ? this.translate.t(messageKey) : null;
|
||||||
};
|
};
|
||||||
|
readonly isFieldModified = (key: string): boolean => this.facade.modifiedFields().has(key);
|
||||||
|
resetField(key: string): void {
|
||||||
|
this.facade.resetField(key);
|
||||||
|
}
|
||||||
|
|
||||||
readonly themeModeOptions: ThemeModeOption[] = [
|
readonly themeModeOptions: ThemeModeOption[] = [
|
||||||
{ value: 'light', labelKey: 'builder.themeModeLight', descKey: 'builder.themeModeLightDesc' },
|
{ value: 'light', labelKey: 'builder.themeModeLight', descKey: 'builder.themeModeLightDesc' },
|
||||||
|
|||||||
@@ -880,6 +880,16 @@ export const en: Translations = {
|
|||||||
usedByLinks: 'Links',
|
usedByLinks: 'Links',
|
||||||
usedByBadges: 'Badges',
|
usedByBadges: 'Badges',
|
||||||
usedByPageBackground: 'Page background',
|
usedByPageBackground: 'Page background',
|
||||||
|
resetField: 'Reset',
|
||||||
|
propertySearchLabel: 'Search settings',
|
||||||
|
propertySearchPlaceholder: 'Search settings…',
|
||||||
|
propertySearchResultsLabel: 'Search results',
|
||||||
|
propertySearchNoResults: 'No matching settings.',
|
||||||
|
unsavedChangesPanelTitle: 'Unsaved changes',
|
||||||
|
navigationHeaderEmptyTitle: 'No header links yet',
|
||||||
|
navigationHeaderEmptyDesc: 'Add a link so customers can navigate from the header.',
|
||||||
|
navigationFooterEmptyTitle: 'No footer links yet',
|
||||||
|
navigationFooterEmptyDesc: 'Add a link so customers can navigate from the footer.',
|
||||||
addBlockHint: 'Add a block to your homepage:',
|
addBlockHint: 'Add a block to your homepage:',
|
||||||
dragToReorder: 'Drag to reorder',
|
dragToReorder: 'Drag to reorder',
|
||||||
blockRemoveConfirm: 'Remove this block from the homepage?',
|
blockRemoveConfirm: 'Remove this block from the homepage?',
|
||||||
|
|||||||
@@ -880,6 +880,16 @@ export const hy: Translations = {
|
|||||||
usedByLinks: 'Հղումներ',
|
usedByLinks: 'Հղումներ',
|
||||||
usedByBadges: 'Կրծքանշաններ',
|
usedByBadges: 'Կրծքանշաններ',
|
||||||
usedByPageBackground: 'Էջի ֆոն',
|
usedByPageBackground: 'Էջի ֆոն',
|
||||||
|
resetField: 'Վերականգնել',
|
||||||
|
propertySearchLabel: 'Որոնել կարգավորումներ',
|
||||||
|
propertySearchPlaceholder: 'Որոնել կարգավորումներ…',
|
||||||
|
propertySearchResultsLabel: 'Որոնման արդյունքներ',
|
||||||
|
propertySearchNoResults: 'Համընկնումներ չեն գտնվել։',
|
||||||
|
unsavedChangesPanelTitle: 'Չպահպանված փոփոխություններ',
|
||||||
|
navigationHeaderEmptyTitle: 'Վերնագրում հղումներ դեռ չկան',
|
||||||
|
navigationHeaderEmptyDesc: 'Ավելացրեք հղում, որպեսզի գնորդները կարողանան նավարկել վերնագրից։',
|
||||||
|
navigationFooterEmptyTitle: 'Ստորագրում հղումներ դեռ չկան',
|
||||||
|
navigationFooterEmptyDesc: 'Ավելացրեք հղում, որպեսզի գնորդները կարողանան նավարկել ստորագրից։',
|
||||||
addBlockHint: 'Ավելացնել բլոկ գլխավոր էջում.',
|
addBlockHint: 'Ավելացնել բլոկ գլխավոր էջում.',
|
||||||
dragToReorder: 'Քաշեք՝ կարգը փոխելու համար',
|
dragToReorder: 'Քաշեք՝ կարգը փոխելու համար',
|
||||||
blockRemoveConfirm: 'Հեռացնե՞լ այս բլոկը գլխավոր էջից։',
|
blockRemoveConfirm: 'Հեռացնե՞լ այս բլոկը գլխավոր էջից։',
|
||||||
|
|||||||
@@ -880,6 +880,16 @@ export const ru: Translations = {
|
|||||||
usedByLinks: 'Ссылки',
|
usedByLinks: 'Ссылки',
|
||||||
usedByBadges: 'Значки',
|
usedByBadges: 'Значки',
|
||||||
usedByPageBackground: 'Фон страницы',
|
usedByPageBackground: 'Фон страницы',
|
||||||
|
resetField: 'Сбросить',
|
||||||
|
propertySearchLabel: 'Поиск настроек',
|
||||||
|
propertySearchPlaceholder: 'Поиск настроек…',
|
||||||
|
propertySearchResultsLabel: 'Результаты поиска',
|
||||||
|
propertySearchNoResults: 'Совпадений не найдено.',
|
||||||
|
unsavedChangesPanelTitle: 'Несохранённые изменения',
|
||||||
|
navigationHeaderEmptyTitle: 'В шапке пока нет ссылок',
|
||||||
|
navigationHeaderEmptyDesc: 'Добавьте ссылку, чтобы покупатели могли перемещаться из шапки сайта.',
|
||||||
|
navigationFooterEmptyTitle: 'В подвале пока нет ссылок',
|
||||||
|
navigationFooterEmptyDesc: 'Добавьте ссылку, чтобы покупатели могли перемещаться из подвала сайта.',
|
||||||
addBlockHint: 'Добавить блок на главную страницу:',
|
addBlockHint: 'Добавить блок на главную страницу:',
|
||||||
dragToReorder: 'Перетащите, чтобы изменить порядок',
|
dragToReorder: 'Перетащите, чтобы изменить порядок',
|
||||||
blockRemoveConfirm: 'Удалить этот блок с главной страницы?',
|
blockRemoveConfirm: 'Удалить этот блок с главной страницы?',
|
||||||
|
|||||||
@@ -881,6 +881,16 @@ export interface Translations {
|
|||||||
usedByLinks: string;
|
usedByLinks: string;
|
||||||
usedByBadges: string;
|
usedByBadges: string;
|
||||||
usedByPageBackground: string;
|
usedByPageBackground: string;
|
||||||
|
resetField: string;
|
||||||
|
propertySearchLabel: string;
|
||||||
|
propertySearchPlaceholder: string;
|
||||||
|
propertySearchResultsLabel: string;
|
||||||
|
propertySearchNoResults: string;
|
||||||
|
unsavedChangesPanelTitle: string;
|
||||||
|
navigationHeaderEmptyTitle: string;
|
||||||
|
navigationHeaderEmptyDesc: string;
|
||||||
|
navigationFooterEmptyTitle: string;
|
||||||
|
navigationFooterEmptyDesc: string;
|
||||||
blockRemoveConfirm: string;
|
blockRemoveConfirm: string;
|
||||||
blockHero: string;
|
blockHero: string;
|
||||||
blockHeroDesc: string;
|
blockHeroDesc: string;
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
<div class="app-form-field">
|
<div class="app-form-field">
|
||||||
@if (label()) {
|
@if (label()) {
|
||||||
|
<div class="app-form-field__label-row">
|
||||||
<label class="app-form-field__label" [for]="fieldId">
|
<label class="app-form-field__label" [for]="fieldId">
|
||||||
{{ label() }}
|
{{ label() }}
|
||||||
@if (required()) {
|
@if (required()) {
|
||||||
<span class="app-form-field__required" aria-hidden="true">*</span>
|
<span class="app-form-field__required" aria-hidden="true">*</span>
|
||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
|
@if (showReset()) {
|
||||||
|
<button type="button" class="app-form-field__reset" [attr.aria-label]="resetLabel()" (click)="resetClicked.emit()">
|
||||||
|
<app-icon name="refresh" [size]="12" />
|
||||||
|
{{ resetLabel() }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="app-form-field__control">
|
<div class="app-form-field__control">
|
||||||
|
|||||||
@@ -4,12 +4,40 @@
|
|||||||
gap: var(--space-xs, 0.25rem);
|
gap: var(--space-xs, 0.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-form-field__label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-sm, 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
.app-form-field__label {
|
.app-form-field__label {
|
||||||
font-size: var(--font-size-base, 0.875rem);
|
font-size: var(--font-size-base, 0.875rem);
|
||||||
font-weight: var(--font-weight-semibold, 600);
|
font-weight: var(--font-weight-semibold, 600);
|
||||||
color: var(--text-primary, #1f322d);
|
color: var(--text-primary, #1f322d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-form-field__reset {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: var(--text-secondary, #6b7280);
|
||||||
|
font-size: var(--font-size-xs, 0.75rem);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary-color, #2f6e5d);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--primary-color, #2f6e5d);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.app-form-field__required {
|
.app-form-field__required {
|
||||||
color: var(--error-color, #c0392b);
|
color: var(--error-color, #c0392b);
|
||||||
margin-left: 0.125rem;
|
margin-left: 0.125rem;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
|
||||||
|
import { IconComponent } from '../icon/icon.component';
|
||||||
|
|
||||||
let nextFormFieldId = 0;
|
let nextFormFieldId = 0;
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ export abstract class FormFieldContext {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-form-field',
|
selector: 'app-form-field',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
imports: [IconComponent],
|
||||||
templateUrl: './form-field.component.html',
|
templateUrl: './form-field.component.html',
|
||||||
styleUrl: './form-field.component.scss',
|
styleUrl: './form-field.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@@ -27,6 +29,10 @@ export class FormFieldComponent implements FormFieldContext {
|
|||||||
/** Optional "Where is this used?" list, e.g. ['Buttons', 'Links', 'Badges']. Already-translated strings. */
|
/** Optional "Where is this used?" list, e.g. ['Buttons', 'Links', 'Badges']. Already-translated strings. */
|
||||||
readonly usedBy = input<readonly string[] | null>(null);
|
readonly usedBy = input<readonly string[] | null>(null);
|
||||||
readonly usedByLabel = input<string | null>(null);
|
readonly usedByLabel = input<string | null>(null);
|
||||||
|
/** Shows a small "reset to original" button next to the label when the field has been changed. */
|
||||||
|
readonly showReset = input(false);
|
||||||
|
readonly resetLabel = input<string | null>(null);
|
||||||
|
readonly resetClicked = output<void>();
|
||||||
|
|
||||||
readonly fieldId = `app-form-field-${++nextFormFieldId}`;
|
readonly fieldId = `app-form-field-${++nextFormFieldId}`;
|
||||||
protected readonly hintId = `${this.fieldId}-hint`;
|
protected readonly hintId = `${this.fieldId}-hint`;
|
||||||
|
|||||||
Reference in New Issue
Block a user