Browse Source

feat(admin-ui): Table view for product variants

Michael Bromley 6 years ago
parent
commit
058749a179

+ 2 - 0
admin-ui/src/app/catalog/catalog.module.ts

@@ -24,6 +24,7 @@ import { ProductAssetsComponent } from './components/product-assets/product-asse
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component';
+import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component';
 import { ProductVariantsWizardComponent } from './components/product-variants-wizard/product-variants-wizard.component';
 import { SelectOptionGroupDialogComponent } from './components/select-option-group-dialog/select-option-group-dialog.component';
 import { SelectOptionGroupComponent } from './components/select-option-group/select-option-group.component';
@@ -59,6 +60,7 @@ import { ProductResolver } from './providers/routing/product-resolver';
         CollectionTreeComponent,
         CollectionTreeNodeComponent,
         CollectionContentsComponent,
+        ProductVariantsTableComponent,
     ],
     entryComponents: [
         AssetPickerDialogComponent,

+ 4 - 1
admin-ui/src/app/catalog/catalog.routes.ts

@@ -1,8 +1,11 @@
 import { Route } from '@angular/router';
-import { FacetWithValues, ProductWithVariants } from 'shared/generated-types';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { FacetWithValues, OrderWithLines, ProductWithVariants } from 'shared/generated-types';
 
 import { createResolveData } from '../common/base-entity-resolver';
 import { detailBreadcrumb } from '../common/detail-breadcrumb';
+import { BreadcrumbValue } from '../core/components/breadcrumb/breadcrumb.component';
 import { _ } from '../core/providers/i18n/mark-for-extraction';
 import { CanDeactivateDetailGuard } from '../shared/providers/routing/can-deactivate-detail-guard';
 

+ 29 - 10
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -106,29 +106,48 @@
             </button>
             <clr-tab-content *clrIfActive="(activeTab$ | async) === 'variants'">
                 <section class="form-block" *ngIf="!(isNew$ | async)">
+                    <div class="view-mode">
+                        <div class="btn-group btn-sm">
+                            <button
+                                class="btn btn-secondary-outline"
+                                (click)="variantDisplayMode = 'card'"
+                                [class.btn-primary]="variantDisplayMode === 'card'"
+                            >
+                                <clr-icon shape="list"></clr-icon>
+                                {{ 'catalog.display-variant-cards' | translate }}
+                            </button>
+                            <button
+                                class="btn"
+                                (click)="variantDisplayMode = 'table'"
+                                [class.btn-primary]="variantDisplayMode === 'table'"
+                            >
+                                <clr-icon shape="table"></clr-icon>
+                                {{ 'catalog.display-variant-table' | translate }}
+                            </button>
+                        </div>
+                    </div>
                     <vdr-generate-product-variants
                         *ngIf="(variants$ | async)?.length === 0; else variants"
                         [product]="product"
                     ></vdr-generate-product-variants>
 
                     <ng-template #variants>
+                        <vdr-product-variants-table
+                            *ngIf="variantDisplayMode === 'table'"
+                            [variants]="variants$ | async"
+                            [optionGroups]="product.optionGroups"
+                            [productVariantsFormArray]="detailForm.get('variants')"
+                        ></vdr-product-variants-table>
                         <vdr-product-variants-list
+                            *ngIf="variantDisplayMode === 'card'"
                             [variants]="variants$ | async"
                             [facets]="facets$ | async"
                             [productVariantsFormArray]="detailForm.get('variants')"
                             [taxCategories]="taxCategories$ | async"
                             (assetChange)="variantAssetChange($event)"
                             (selectionChange)="selectedVariantIds = $event"
-                        >
-                            <button
-                                class="btn btn-sm btn-secondary"
-                                [disabled]="selectedVariantIds.length === 0"
-                                (click)="selectVariantFacetValue(selectedVariantIds)"
-                            >
-                                <clr-icon shape="plus"></clr-icon>
-                                {{ 'catalog.add-facets' | translate }}
-                            </button>
-                        </vdr-product-variants-list>
+                            (selectFacetValueClick)="selectVariantFacetValue($event)"
+                        ></vdr-product-variants-list>
                     </ng-template>
                 </section>
             </clr-tab-content>

+ 3 - 11
admin-ui/src/app/catalog/components/product-detail/product-detail.component.scss

@@ -8,19 +8,11 @@
     }
 }
 
-.option-groups-list {
-
-}
-
 .group-name {
     padding-right: 6px;
 }
 
-.variants-list {
-
-    .variant {
-        input {
-            width: 100%;
-        }
-    }
+.view-mode {
+    display: flex;
+    justify-content: flex-end;
 }

+ 4 - 2
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { BehaviorSubject, combineLatest, forkJoin, merge, Observable } from 'rxjs';
-import { distinctUntilChanged, map, mergeMap, skip, take, withLatestFrom } from 'rxjs/operators';
+import { distinctUntilChanged, map, mergeMap, shareReplay, skip, take, withLatestFrom } from 'rxjs/operators';
 import {
     CreateProductInput,
     FacetWithValues,
@@ -69,6 +69,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     facetValues$: Observable<ProductWithVariants.FacetValues[]>;
     facets$ = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
     selectedVariantIds: string[] = [];
+    variantDisplayMode: 'card' | 'table' = 'card';
 
     constructor(
         route: ActivatedRoute,
@@ -105,7 +106,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         this.variants$ = this.product$.pipe(map(product => product.variants));
         this.taxCategories$ = this.dataService.settings
             .getTaxCategories()
-            .mapSingle(data => data.taxCategories);
+            .mapSingle(data => data.taxCategories)
+            .pipe(shareReplay(1));
         this.activeTab$ = this.route.paramMap.pipe(map(qpm => qpm.get('tab') as any));
 
         // FacetValues are provided initially by the nested array of the

+ 1 - 1
admin-ui/src/app/catalog/components/product-list/product-list.component.html

@@ -41,7 +41,7 @@
     <vdr-dt-column></vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
-    <ng-template let-result="item">
+    <ng-template let-result="item" #foo>
         <td class="left align-middle">
             <div class="image-placeholder">
                 <img

+ 64 - 0
admin-ui/src/app/catalog/components/product-variants-table/product-variants-table.component.html

@@ -0,0 +1,64 @@
+<vdr-data-table [items]="variants">
+    <vdr-dt-column></vdr-dt-column>
+    <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.sku' | translate }}</vdr-dt-column>
+    <ng-container *ngFor="let optionGroup of optionGroups">
+        <vdr-dt-column>{{ optionGroup.name }}</vdr-dt-column>
+    </ng-container>
+    <vdr-dt-column>{{ 'catalog.price' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.stock-on-hand' | translate }}</vdr-dt-column>
+    <ng-template let-variant="item" let-i="index">
+        <ng-container [formGroup]="formArray.at(i)">
+            <td class="left align-middle">
+                <div class="card-img">
+                    <div class="featured-asset">
+                        <img
+                            *ngIf="variant.featuredAsset"
+                            [src]="variant.featuredAsset!.preview + '?preset=tiny'"
+                        />
+                        <div class="placeholder" *ngIf="!variant.featuredAsset">
+                            <clr-icon shape="image" size="48"></clr-icon>
+                        </div>
+                    </div>
+                </div>
+            </td>
+            <td class="left align-middle">
+                <clr-input-container>
+                    <input
+                        clrInput
+                        type="text"
+                        formControlName="name"
+                        [placeholder]="'common.name' | translate"
+                    />
+                </clr-input-container>
+            </td>
+            <td class="left align-middle">
+                <clr-input-container>
+                    <input
+                        clrInput
+                        type="text"
+                        formControlName="sku"
+                        [placeholder]="'catalog.sku' | translate"
+                    />
+                </clr-input-container>
+            </td>
+            <ng-container *ngFor="let option of variant.options">
+                <td class="left align-middle">{{ option.name }}</td>
+            </ng-container>
+            <td class="left align-middle price">
+                <clr-input-container>
+                    <vdr-currency-input
+                        clrInput
+                        [currencyCode]="variant.currencyCode"
+                        formControlName="price"
+                    ></vdr-currency-input>
+                </clr-input-container>
+            </td>
+            <td class="left align-middle stock">
+                <clr-input-container>
+                    <input clrInput type="number" min="0" step="1" formControlName="stockOnHand" />
+                </clr-input-container>
+            </td>
+        </ng-container>
+    </ng-template>
+</vdr-data-table>

+ 15 - 0
admin-ui/src/app/catalog/components/product-variants-table/product-variants-table.component.scss

@@ -0,0 +1,15 @@
+@import "variables";
+
+.placeholder {
+    color: $color-grey-3;
+}
+
+input {
+    width: 100%;
+}
+
+.stock, .price {
+    input {
+        max-width: 96px;
+    }
+}

+ 15 - 0
admin-ui/src/app/catalog/components/product-variants-table/product-variants-table.component.ts

@@ -0,0 +1,15 @@
+import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { FormArray } from '@angular/forms';
+import { ProductWithVariants } from 'shared/generated-types';
+
+@Component({
+    selector: 'vdr-product-variants-table',
+    templateUrl: './product-variants-table.component.html',
+    styleUrls: ['./product-variants-table.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ProductVariantsTableComponent {
+    @Input('productVariantsFormArray') formArray: FormArray;
+    @Input() variants: ProductWithVariants.Variants[];
+    @Input() optionGroups: ProductWithVariants.OptionGroups[];
+}

+ 2 - 2
admin-ui/src/app/catalog/components/variant-price-detail/variant-price-detail.component.ts

@@ -26,10 +26,10 @@ export class VariantPriceDetailComponent implements OnInit, OnChanges {
 
     ngOnInit() {
         const taxRates$ = this.dataService.settings
-            .getTaxRates(99999, 0)
+            .getTaxRates(99999, 0, 'cache-first')
             .mapStream(data => data.taxRates.items);
         const activeChannel$ = this.dataService.settings
-            .getActiveChannel()
+            .getActiveChannel('cache-first')
             .mapStream(data => data.activeChannel);
 
         this.taxRate$ = combineLatest(activeChannel$, taxRates$, this.taxCategoryIdChange$).pipe(

+ 14 - 7
admin-ui/src/app/data/providers/settings-data.service.ts

@@ -1,3 +1,4 @@
+import { FetchPolicy } from 'apollo-client';
 import {
     AddMembersToZone,
     CreateChannel,
@@ -184,13 +185,17 @@ export class SettingsDataService {
         );
     }
 
-    getTaxRates(take: number = 10, skip: number = 0) {
-        return this.baseDataService.query<GetTaxRateList.Query, GetTaxRateList.Variables>(GET_TAX_RATE_LIST, {
-            options: {
-                take,
-                skip,
+    getTaxRates(take: number = 10, skip: number = 0, fetchPolicy?: FetchPolicy) {
+        return this.baseDataService.query<GetTaxRateList.Query, GetTaxRateList.Variables>(
+            GET_TAX_RATE_LIST,
+            {
+                options: {
+                    take,
+                    skip,
+                },
             },
-        });
+            fetchPolicy,
+        );
     }
 
     getTaxRate(id: string) {
@@ -221,9 +226,11 @@ export class SettingsDataService {
         });
     }
 
-    getActiveChannel() {
+    getActiveChannel(fetchPolicy?: FetchPolicy) {
         return this.baseDataService.query<GetActiveChannel.Query, GetActiveChannel.Variables>(
             GET_ACTIVE_CHANNEL,
+            {},
+            fetchPolicy,
         );
     }
 

+ 5 - 0
admin-ui/src/app/shared/components/currency-input/currency-input.component.scss

@@ -1,3 +1,8 @@
 :host {
     padding: 0;
 }
+
+input {
+    width: 100%;
+    max-width: 96px;
+}

+ 6 - 3
admin-ui/src/app/shared/components/data-table/data-table.component.html

@@ -1,4 +1,4 @@
-<ng-container *ngIf="!items || (items && items.length); else: emptyPlaceholder">
+<ng-container *ngIf="!items || (items && items.length); else emptyPlaceholder">
     <table class="table">
         <thead>
             <tr>
@@ -23,7 +23,8 @@
                                   itemsPerPage: itemsPerPage,
                                   currentPage: currentPage,
                                   totalItems: totalItems
-                              })
+                              });
+                    index as i
                 "
             >
                 <td *ngIf="isRowSelectedFn">
@@ -33,7 +34,9 @@
                         (selectedChange)="rowSelectChange.emit(item)"
                     ></vdr-select-toggle>
                 </td>
-                <ng-container *ngTemplateOutlet="rowTemplate; context: { item: item }"></ng-container>
+                <ng-container
+                    *ngTemplateOutlet="rowTemplate; context: { item: item, index: i }"
+                ></ng-container>
             </tr>
         </tbody>
     </table>

+ 6 - 0
admin-ui/src/app/shared/components/data-table/data-table.component.scss

@@ -1,5 +1,11 @@
 @import "variables";
 
+:host {
+    display: block;
+    max-width: 100%;
+    overflow: auto;
+}
+
 thead th {
     position: sticky;
     top: 24px;

+ 4 - 2
admin-ui/src/i18n-messages/en.json

@@ -38,6 +38,8 @@
     "create-new-facet": "Create new facet",
     "create-new-option-group": "Create new option group",
     "create-new-product": "Create new product",
+    "display-variant-cards": "View details",
+    "display-variant-table": "View as table",
     "drop-files-to-upload": "Drop files to upload",
     "facet-values": "Facet values",
     "filter-by-group-name": "Filter by group name",
@@ -61,8 +63,8 @@
     "options": "Options",
     "original-asset-size": "Source size",
     "price": "Price",
-    "price-includes-tax-at": "Price includes tax at { rate }%",
-    "price-with-tax-in-default-zone": "Price with { rate }% tax: { price }",
+    "price-includes-tax-at": "Includes tax at { rate }%",
+    "price-with-tax-in-default-zone": "Inc. { rate }% tax: { price }",
     "private": "Private",
     "product-details": "Product details",
     "product-name": "Product name",