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

Replace raw button/table/input elements with app-button, app-table, app-badge,
app-input, app-pagination. Removed now-redundant button/table/th/td CSS from
the stylesheet (would have double-styled the primitives' projected content
under Angular's emulated encapsulation). Native selects and checkboxes kept
as-is - no Select/Checkbox primitive exists yet.

Production build green, arch:check passes. In-browser verification hit a
dev-server routing hiccup unrelated to these changes (a temporary unguarded
preview route 404'd despite compiling correctly); relied on the identical,
already-verified primitive usage pattern from the Static Pages editor and
Media Manager instead.
This commit is contained in:
sdarbinyan
2026-07-15 07:33:25 +04:00
parent 9ab807342e
commit dbb22d1392
3 changed files with 53 additions and 50 deletions

View File

@@ -1,7 +1,7 @@
<section class="admin-products-card"> <section class="admin-products-card">
<div class="toolbar"> <div class="toolbar">
<div class="filters"> <div class="filters">
<input type="search" [ngModel]="filters.search" (ngModelChange)="filtersChange.emit({ search: $event })" [placeholder]="'adminProducts.search' | translate" /> <app-input type="search" [ngModel]="filters.search" (ngModelChange)="filtersChange.emit({ search: $event })" [placeholder]="'adminProducts.search' | translate" />
<select [ngModel]="filters.categoryId" (ngModelChange)="filtersChange.emit({ categoryId: $event || null })"> <select [ngModel]="filters.categoryId" (ngModelChange)="filtersChange.emit({ categoryId: $event || null })">
<option [ngValue]="null">{{ 'adminProducts.allCategories' | translate }}</option> <option [ngValue]="null">{{ 'adminProducts.allCategories' | translate }}</option>
@for (category of categories; track category.id) { @for (category of categories; track category.id) {
@@ -27,57 +27,56 @@
<option value="updated">{{ 'adminProducts.sortUpdated' | translate }}</option> <option value="updated">{{ 'adminProducts.sortUpdated' | translate }}</option>
</select> </select>
</div> </div>
<button type="button" (click)="create.emit()">{{ 'adminProducts.create' | translate }}</button> <app-button variant="primary" (click)="create.emit()">{{ 'adminProducts.create' | translate }}</app-button>
</div> </div>
@if (selectedIds.length > 0) { @if (selectedIds.length > 0) {
<div class="bulk-actions"> <div class="bulk-actions">
<button type="button" class="secondary" (click)="bulkVisibility.emit(true)">{{ 'adminProducts.bulkShow' | translate }}</button> <app-button variant="secondary" size="sm" (click)="bulkVisibility.emit(true)">{{ 'adminProducts.bulkShow' | translate }}</app-button>
<button type="button" class="secondary" (click)="bulkVisibility.emit(false)">{{ 'adminProducts.bulkHide' | translate }}</button> <app-button variant="secondary" size="sm" (click)="bulkVisibility.emit(false)">{{ 'adminProducts.bulkHide' | translate }}</app-button>
<button type="button" class="secondary danger" (click)="bulkDelete.emit()">{{ 'adminProducts.bulkDelete' | translate }}</button> <app-button variant="danger" size="sm" (click)="bulkDelete.emit()">{{ 'adminProducts.bulkDelete' | translate }}</app-button>
</div> </div>
} }
<div class="table-wrap"> <app-table>
<table> <thead>
<thead> <tr>
<th><input type="checkbox" (change)="selectAll.emit($any($event.target).checked)" /></th>
<th>{{ 'adminProducts.name' | translate }}</th>
<th>{{ 'backoffice.sku' | translate }}</th>
<th>{{ 'adminProducts.brand' | translate }}</th>
<th>{{ 'backoffice.price' | translate }}</th>
<th>{{ 'backoffice.status' | translate }}</th>
<th>{{ 'adminProducts.visibility' | translate }}</th>
<th>{{ 'adminProducts.actions' | translate }}</th>
</tr>
</thead>
<tbody>
@for (product of products; track product.id) {
<tr> <tr>
<th><input type="checkbox" (change)="selectAll.emit($any($event.target).checked)" /></th> <td><input type="checkbox" [checked]="isSelected(product.id)" (change)="selectionChange.emit({ id: product.id, checked: $any($event.target).checked })" /></td>
<th>{{ 'adminProducts.name' | translate }}</th> <td>{{ product.name }}</td>
<th>{{ 'backoffice.sku' | translate }}</th> <td>{{ product.sku }}</td>
<th>{{ 'adminProducts.brand' | translate }}</th> <td>{{ product.brand }}</td>
<th>{{ 'backoffice.price' | translate }}</th> <td>{{ product.price }} {{ product.currency }}</td>
<th>{{ 'backoffice.status' | translate }}</th> <td>{{ product.stockStatus }}</td>
<th>{{ 'adminProducts.visibility' | translate }}</th> <td>
<th>{{ 'adminProducts.actions' | translate }}</th> <app-badge [variant]="product.visible ? 'success' : 'neutral'">
{{ product.visible ? ('adminProducts.visible' | translate) : ('adminProducts.hidden' | translate) }}
</app-badge>
</td>
<td class="actions">
<app-button variant="secondary" size="sm" (click)="edit.emit(product.id)">{{ 'adminProducts.edit' | translate }}</app-button>
<app-button variant="secondary" size="sm" (click)="duplicate.emit(product.id)">{{ 'adminProducts.duplicate' | translate }}</app-button>
<app-button variant="danger" size="sm" (click)="delete.emit(product.id)">{{ 'adminProducts.delete' | translate }}</app-button>
</td>
</tr> </tr>
</thead> }
<tbody> </tbody>
@for (product of products; track product.id) { </app-table>
<tr>
<td><input type="checkbox" [checked]="isSelected(product.id)" (change)="selectionChange.emit({ id: product.id, checked: $any($event.target).checked })" /></td>
<td>{{ product.name }}</td>
<td>{{ product.sku }}</td>
<td>{{ product.brand }}</td>
<td>{{ product.price }} {{ product.currency }}</td>
<td>{{ product.stockStatus }}</td>
<td>{{ product.visible ? ('adminProducts.visible' | translate) : ('adminProducts.hidden' | translate) }}</td>
<td class="actions">
<button type="button" class="secondary" (click)="edit.emit(product.id)">{{ 'adminProducts.edit' | translate }}</button>
<button type="button" class="secondary" (click)="duplicate.emit(product.id)">{{ 'adminProducts.duplicate' | translate }}</button>
<button type="button" class="secondary danger" (click)="delete.emit(product.id)">{{ 'adminProducts.delete' | translate }}</button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="pager"> <div class="pager">
<span>{{ total }} {{ 'adminProducts.items' | translate }}</span> <span>{{ total }} {{ 'adminProducts.items' | translate }}</span>
<div class="pager-actions"> <app-pagination [currentPage]="filters.page" [totalPages]="totalPages()" (pageChange)="filtersChange.emit({ page: $event })" />
<button type="button" class="secondary" [disabled]="filters.page <= 1" (click)="filtersChange.emit({ page: filters.page - 1 })">{{ 'catalog.previousPage' | translate }}</button>
<button type="button" class="secondary" [disabled]="filters.page * filters.pageSize >= total" (click)="filtersChange.emit({ page: filters.page + 1 })">{{ 'catalog.nextPage' | translate }}</button>
</div>
</div> </div>
</section> </section>

View File

@@ -1,13 +1,8 @@
.admin-products-card { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--border-color, #d3dad9); border-radius: 16px; background: #fff; } .admin-products-card { display: grid; gap: 16px; padding: 16px; border: 1px solid var(--border-color, #d3dad9); border-radius: 16px; background: #fff; }
.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; } .toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 12px; align-items: flex-start; }
.filters { display: grid; grid-template-columns: repeat(5, minmax(140px, 1fr)); gap: 10px; flex: 1; } .filters { display: grid; grid-template-columns: repeat(5, minmax(140px, 1fr)); gap: 10px; flex: 1; }
input, select { min-height: 40px; padding: 0 10px; border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; } select { min-height: 40px; padding: 0 10px; border: 1px solid var(--border-color, #d3dad9); border-radius: 10px; }
button { min-height: 40px; border-radius: 10px; border: 1px solid #497671; background: #497671; color: #fff; padding: 0 12px; font-weight: 700; cursor: pointer; }
button.secondary { background: #fff; color: #1e3c38; border-color: var(--border-color, #d3dad9); }
button.danger { border-color: #b91c1c; color: #b91c1c; }
.bulk-actions, .pager, .pager-actions, .actions { display: flex; gap: 10px; align-items: center; } .bulk-actions, .pager, .pager-actions, .actions { display: flex; gap: 10px; align-items: center; }
.table-wrap { overflow: auto; } .pager { justify-content: space-between; flex-wrap: wrap; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; border-bottom: 1px solid #ececec; text-align: left; vertical-align: top; }
@media (max-width: 960px) { .filters { grid-template-columns: repeat(2, minmax(140px, 1fr)); } } @media (max-width: 960px) { .filters { grid-template-columns: repeat(2, minmax(140px, 1fr)); } }
@media (max-width: 640px) { .filters { grid-template-columns: 1fr; } .actions { flex-direction: column; align-items: stretch; } } @media (max-width: 640px) { .filters { grid-template-columns: 1fr; } .actions { flex-direction: column; align-items: stretch; } }

View File

@@ -2,11 +2,16 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { AdminProduct, AdminProductCategoryOption, AdminProductListFilters } from '../models/admin-product.model'; import { AdminProduct, AdminProductCategoryOption, AdminProductListFilters } 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 { BadgeComponent } from '../../../../shared/ui/badge/badge.component';
import { TableComponent } from '../../../../shared/ui/table/table.component';
import { PaginationComponent } from '../../../../shared/ui/pagination/pagination.component';
@Component({ @Component({
selector: 'app-admin-products-list', selector: 'app-admin-products-list',
standalone: true, standalone: true,
imports: [FormsModule, TranslatePipe], imports: [FormsModule, TranslatePipe, ButtonComponent, InputComponent, BadgeComponent, TableComponent, PaginationComponent],
templateUrl: './admin-products-list.component.html', templateUrl: './admin-products-list.component.html',
styleUrls: ['./admin-products-list.component.scss'], styleUrls: ['./admin-products-list.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
@@ -32,4 +37,8 @@ export class AdminProductsListComponent {
isSelected(id: string): boolean { isSelected(id: string): boolean {
return this.selectedIds.includes(id); return this.selectedIds.includes(id);
} }
totalPages(): number {
return Math.max(1, Math.ceil(this.total / this.filters.pageSize));
}
} }