feat(admin-products): adopt Design System primitives in Product Form
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Replace single-line text/number inputs with app-form-field/app-input across
name, slug, sku, brand, priority, pricing, quantity, availability, per-locale
translation name/shortDescription, SEO metaTitle/keywords, and badges fields.
Save button now app-button. Textareas, selects, and checkboxes stay native -
no Textarea/Select/Checkbox primitive exists yet. Added app-form-field.full
grid-column rule alongside the existing label.full one (custom element,
different selector).

Production build green, arch:check passes.
This commit is contained in:
sdarbinyan
2026-07-15 07:39:56 +04:00
parent dbb22d1392
commit 53d6a8e169
3 changed files with 52 additions and 20 deletions

View File

@@ -1,11 +1,21 @@
<section class="form-card">
<div class="grid two">
<label><span>{{ 'adminProducts.name' | translate }}</span><input type="text" [ngModel]="product.name" (ngModelChange)="updateField('name', $event)" /></label>
<label><span>{{ 'adminProducts.slug' | translate }}</span><input type="text" [ngModel]="product.slug" (ngModelChange)="updateField('slug', $event)" /></label>
<label><span>{{ 'backoffice.sku' | translate }}</span><input type="text" [ngModel]="product.sku" (ngModelChange)="updateField('sku', $event)" /></label>
<label><span>{{ 'adminProducts.brand' | translate }}</span><input type="text" [ngModel]="product.brand" (ngModelChange)="updateField('brand', $event)" /></label>
<app-form-field [label]="'adminProducts.name' | translate">
<app-input [ngModel]="product.name" (ngModelChange)="updateField('name', $event)" />
</app-form-field>
<app-form-field [label]="'adminProducts.slug' | translate">
<app-input [ngModel]="product.slug" (ngModelChange)="updateField('slug', $event)" />
</app-form-field>
<app-form-field [label]="'backoffice.sku' | translate">
<app-input [ngModel]="product.sku" (ngModelChange)="updateField('sku', $event)" />
</app-form-field>
<app-form-field [label]="'adminProducts.brand' | translate">
<app-input [ngModel]="product.brand" (ngModelChange)="updateField('brand', $event)" />
</app-form-field>
<label><span>{{ 'adminProducts.category' | translate }}</span><select [ngModel]="product.categoryId" (ngModelChange)="updateField('categoryId', $event)">@for (category of categories; track category.id) {<option [ngValue]="category.id">{{ category.title }}</option>}</select></label>
<label><span>{{ 'adminProducts.priority' | translate }}</span><input type="number" [ngModel]="product.priority" (ngModelChange)="updateField('priority', +$event)" /></label>
<app-form-field [label]="'adminProducts.priority' | translate">
<app-input type="number" [ngModel]="product.priority" (ngModelChange)="updateField('priority', +$event)" />
</app-form-field>
<label class="check"><input type="checkbox" [checked]="product.visible" (change)="updateField('visible', $any($event.target).checked)" /><span>{{ 'adminProducts.visible' | translate }}</span></label>
</div>
@@ -18,16 +28,26 @@
<h3>{{ 'adminProducts.pricing' | translate }}</h3>
<div class="grid three">
<label><span>{{ 'backoffice.price' | translate }}</span><input type="number" [ngModel]="product.price" (ngModelChange)="updateField('price', +$event)" /></label>
<label><span>{{ 'adminProducts.discount' | translate }}</span><input type="number" [ngModel]="product.discount" (ngModelChange)="updateField('discount', +$event)" /></label>
<label><span>{{ 'adminProducts.currency' | translate }}</span><input type="text" [ngModel]="product.currency" (ngModelChange)="updateField('currency', $event)" /></label>
<app-form-field [label]="'backoffice.price' | translate">
<app-input type="number" [ngModel]="product.price" (ngModelChange)="updateField('price', +$event)" />
</app-form-field>
<app-form-field [label]="'adminProducts.discount' | translate">
<app-input type="number" [ngModel]="product.discount" (ngModelChange)="updateField('discount', +$event)" />
</app-form-field>
<app-form-field [label]="'adminProducts.currency' | translate">
<app-input [ngModel]="product.currency" (ngModelChange)="updateField('currency', $event)" />
</app-form-field>
</div>
<h3>{{ 'adminProducts.inventory' | translate }}</h3>
<div class="grid three">
<label><span>{{ 'adminProducts.quantity' | translate }}</span><input type="number" [ngModel]="product.quantity" (ngModelChange)="updateField('quantity', +$event)" /></label>
<app-form-field [label]="'adminProducts.quantity' | translate">
<app-input type="number" [ngModel]="product.quantity" (ngModelChange)="updateField('quantity', +$event)" />
</app-form-field>
<label><span>{{ 'adminProducts.stockStatus' | translate }}</span><select [ngModel]="product.stockStatus" (ngModelChange)="updateField('stockStatus', $event)"><option value="in_stock">{{ 'adminProducts.inStock' | translate }}</option><option value="low_stock">{{ 'adminProducts.lowStock' | translate }}</option><option value="out_of_stock">{{ 'adminProducts.outOfStock' | translate }}</option></select></label>
<label><span>{{ 'adminProducts.availability' | translate }}</span><input type="text" [ngModel]="product.availability" (ngModelChange)="updateField('availability', $event)" /></label>
<app-form-field [label]="'adminProducts.availability' | translate">
<app-input [ngModel]="product.availability" (ngModelChange)="updateField('availability', $event)" />
</app-form-field>
</div>
<h3>{{ 'adminProducts.content' | translate }}</h3>
@@ -41,17 +61,25 @@
<h3>{{ 'adminProducts.translations' | translate }}</h3>
@for (locale of ['en','ru','hy']; track locale) {
<div class="grid two sub-block">
<label><span>{{ 'adminProducts.name' | translate }} {{ locale }}</span><input type="text" [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" /></label>
<label><span>{{ 'adminProducts.shortDescription' | translate }} {{ locale }}</span><input type="text" [ngModel]="product.translations[locale]?.shortDescription || ''" (ngModelChange)="updateTranslation(locale, 'shortDescription', $event)" /></label>
<app-form-field [label]="(('adminProducts.name' | translate) + ' ' + locale)">
<app-input [ngModel]="product.translations[locale]?.name || ''" (ngModelChange)="updateTranslation(locale, 'name', $event)" />
</app-form-field>
<app-form-field [label]="(('adminProducts.shortDescription' | translate) + ' ' + locale)">
<app-input [ngModel]="product.translations[locale]?.shortDescription || ''" (ngModelChange)="updateTranslation(locale, 'shortDescription', $event)" />
</app-form-field>
<label class="full"><span>{{ 'adminProducts.htmlDescription' | translate }} {{ locale }}</span><textarea rows="4" [ngModel]="product.translations[locale]?.htmlDescription || ''" (ngModelChange)="updateTranslation(locale, 'htmlDescription', $event)"></textarea></label>
</div>
}
<h3>{{ 'adminProducts.seo' | translate }}</h3>
<div class="grid one">
<label><span>{{ 'adminProducts.metaTitle' | translate }}</span><input type="text" [ngModel]="product.seo.metaTitle" (ngModelChange)="productChange.emit({ seo: { ...product.seo, metaTitle: $event } })" /></label>
<app-form-field [label]="'adminProducts.metaTitle' | translate">
<app-input [ngModel]="product.seo.metaTitle" (ngModelChange)="productChange.emit({ seo: { ...product.seo, metaTitle: $event } })" />
</app-form-field>
<label><span>{{ 'adminProducts.metaDescription' | translate }}</span><textarea rows="3" [ngModel]="product.seo.metaDescription" (ngModelChange)="productChange.emit({ seo: { ...product.seo, metaDescription: $event } })"></textarea></label>
<label><span>{{ 'adminProducts.keywords' | translate }}</span><input type="text" [ngModel]="product.seo.keywords" (ngModelChange)="productChange.emit({ seo: { ...product.seo, keywords: $event } })" /></label>
<app-form-field [label]="'adminProducts.keywords' | translate">
<app-input [ngModel]="product.seo.keywords" (ngModelChange)="productChange.emit({ seo: { ...product.seo, keywords: $event } })" />
</app-form-field>
</div>
<h3>{{ 'adminProducts.marketplace' | translate }}</h3>
@@ -60,7 +88,9 @@
<label class="check"><input type="checkbox" [checked]="product.recommended" (change)="updateField('recommended', $any($event.target).checked)" /><span>{{ 'adminProducts.recommended' | translate }}</span></label>
<label class="check"><input type="checkbox" [checked]="product.isNew" (change)="updateField('isNew', $any($event.target).checked)" /><span>{{ 'adminProducts.new' | translate }}</span></label>
<label class="check"><input type="checkbox" [checked]="product.bestseller" (change)="updateField('bestseller', $any($event.target).checked)" /><span>{{ 'adminProducts.bestseller' | translate }}</span></label>
<label class="full"><span>{{ 'adminProducts.badges' | translate }}</span><input type="text" [ngModel]="product.badges.join(', ')" (ngModelChange)="updateList('badges', $event)" /></label>
<app-form-field class="full" [label]="'adminProducts.badges' | translate">
<app-input [ngModel]="product.badges.join(', ')" (ngModelChange)="updateList('badges', $event)" />
</app-form-field>
</div>
<h3>{{ 'adminProducts.customer' | translate }}</h3>
@@ -77,5 +107,5 @@
</article>
</div>
<div class="actions"><button type="button" (click)="save.emit()">{{ 'adminProducts.save' | translate }}</button></div>
<div class="actions"><app-button variant="primary" (click)="save.emit()">{{ 'adminProducts.save' | translate }}</app-button></div>
</section>

View File

@@ -4,13 +4,12 @@
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
label { display: grid; gap: 6px; font-weight: 600; }
label.full { grid-column: 1 / -1; }
label.full, app-form-field.full { grid-column: 1 / -1; }
label.check { display: flex; align-items: center; gap: 8px; }
input, textarea, select { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; font: inherit; }
textarea, select { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; font: inherit; }
input[type='checkbox'] { width: auto; }
.sub-block { border-top: 1px dashed #d9e2e1; padding-top: 12px; }
.readonly-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.readonly-grid article { border: 1px solid #ececec; border-radius: 12px; padding: 12px; }
.actions { display: flex; justify-content: flex-end; }
button { min-height: 42px; border-radius: 10px; border: 1px solid #497671; background: #497671; color: #fff; padding: 0 14px; font-weight: 700; cursor: pointer; }
@media (max-width: 900px) { .grid.two, .grid.three, .readonly-grid { grid-template-columns: 1fr; } }

View File

@@ -2,11 +2,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { FormsModule } from '@angular/forms';
import { AdminProduct, AdminProductCategoryOption } from '../models/admin-product.model';
import { TranslatePipe } from '../../../../i18n/translate.pipe';
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
import { InputComponent } from '../../../../shared/ui/input/input.component';
import { FormFieldComponent } from '../../../../shared/ui/form-field/form-field.component';
@Component({
selector: 'app-admin-product-form',
standalone: true,
imports: [FormsModule, TranslatePipe],
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, FormFieldComponent],
templateUrl: './admin-product-form.component.html',
styleUrls: ['./admin-product-form.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush