feat(admin-products): adopt Design System primitives in Products List
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
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:
@@ -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>
|
||||||
|
|||||||
@@ -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; } }
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user