feat(admin-products): adopt Design System primitives in Product Form
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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:
@@ -1,11 +1,21 @@
|
|||||||
<section class="form-card">
|
<section class="form-card">
|
||||||
<div class="grid two">
|
<div class="grid two">
|
||||||
<label><span>{{ 'adminProducts.name' | translate }}</span><input type="text" [ngModel]="product.name" (ngModelChange)="updateField('name', $event)" /></label>
|
<app-form-field [label]="'adminProducts.name' | translate">
|
||||||
<label><span>{{ 'adminProducts.slug' | translate }}</span><input type="text" [ngModel]="product.slug" (ngModelChange)="updateField('slug', $event)" /></label>
|
<app-input [ngModel]="product.name" (ngModelChange)="updateField('name', $event)" />
|
||||||
<label><span>{{ 'backoffice.sku' | translate }}</span><input type="text" [ngModel]="product.sku" (ngModelChange)="updateField('sku', $event)" /></label>
|
</app-form-field>
|
||||||
<label><span>{{ 'adminProducts.brand' | translate }}</span><input type="text" [ngModel]="product.brand" (ngModelChange)="updateField('brand', $event)" /></label>
|
<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.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>
|
<label class="check"><input type="checkbox" [checked]="product.visible" (change)="updateField('visible', $any($event.target).checked)" /><span>{{ 'adminProducts.visible' | translate }}</span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -18,16 +28,26 @@
|
|||||||
|
|
||||||
<h3>{{ 'adminProducts.pricing' | translate }}</h3>
|
<h3>{{ 'adminProducts.pricing' | translate }}</h3>
|
||||||
<div class="grid three">
|
<div class="grid three">
|
||||||
<label><span>{{ 'backoffice.price' | translate }}</span><input type="number" [ngModel]="product.price" (ngModelChange)="updateField('price', +$event)" /></label>
|
<app-form-field [label]="'backoffice.price' | translate">
|
||||||
<label><span>{{ 'adminProducts.discount' | translate }}</span><input type="number" [ngModel]="product.discount" (ngModelChange)="updateField('discount', +$event)" /></label>
|
<app-input type="number" [ngModel]="product.price" (ngModelChange)="updateField('price', +$event)" />
|
||||||
<label><span>{{ 'adminProducts.currency' | translate }}</span><input type="text" [ngModel]="product.currency" (ngModelChange)="updateField('currency', $event)" /></label>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h3>{{ 'adminProducts.inventory' | translate }}</h3>
|
<h3>{{ 'adminProducts.inventory' | translate }}</h3>
|
||||||
<div class="grid three">
|
<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.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>
|
</div>
|
||||||
|
|
||||||
<h3>{{ 'adminProducts.content' | translate }}</h3>
|
<h3>{{ 'adminProducts.content' | translate }}</h3>
|
||||||
@@ -41,17 +61,25 @@
|
|||||||
<h3>{{ 'adminProducts.translations' | translate }}</h3>
|
<h3>{{ 'adminProducts.translations' | translate }}</h3>
|
||||||
@for (locale of ['en','ru','hy']; track locale) {
|
@for (locale of ['en','ru','hy']; track locale) {
|
||||||
<div class="grid two sub-block">
|
<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>
|
<app-form-field [label]="(('adminProducts.name' | translate) + ' ' + locale)">
|
||||||
<label><span>{{ 'adminProducts.shortDescription' | translate }} {{ locale }}</span><input type="text" [ngModel]="product.translations[locale]?.shortDescription || ''" (ngModelChange)="updateTranslation(locale, 'shortDescription', $event)" /></label>
|
<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>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<h3>{{ 'adminProducts.seo' | translate }}</h3>
|
<h3>{{ 'adminProducts.seo' | translate }}</h3>
|
||||||
<div class="grid one">
|
<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.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>
|
</div>
|
||||||
|
|
||||||
<h3>{{ 'adminProducts.marketplace' | translate }}</h3>
|
<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.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.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="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>
|
</div>
|
||||||
|
|
||||||
<h3>{{ 'adminProducts.customer' | translate }}</h3>
|
<h3>{{ 'adminProducts.customer' | translate }}</h3>
|
||||||
@@ -77,5 +107,5 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
label { display: grid; gap: 6px; font-weight: 600; }
|
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; }
|
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; }
|
input[type='checkbox'] { width: auto; }
|
||||||
.sub-block { border-top: 1px dashed #d9e2e1; padding-top: 12px; }
|
.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 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||||
.readonly-grid article { border: 1px solid #ececec; border-radius: 12px; padding: 12px; }
|
.readonly-grid article { border: 1px solid #ececec; border-radius: 12px; padding: 12px; }
|
||||||
.actions { display: flex; justify-content: flex-end; }
|
.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; } }
|
@media (max-width: 900px) { .grid.two, .grid.three, .readonly-grid { grid-template-columns: 1fr; } }
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
|
|||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { AdminProduct, AdminProductCategoryOption } from '../models/admin-product.model';
|
import { AdminProduct, AdminProductCategoryOption } from '../models/admin-product.model';
|
||||||
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
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({
|
@Component({
|
||||||
selector: 'app-admin-product-form',
|
selector: 'app-admin-product-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, TranslatePipe],
|
imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, FormFieldComponent],
|
||||||
templateUrl: './admin-product-form.component.html',
|
templateUrl: './admin-product-form.component.html',
|
||||||
styleUrls: ['./admin-product-form.component.scss'],
|
styleUrls: ['./admin-product-form.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
Reference in New Issue
Block a user