Browse Source

feat(admin-ui): Use search for product list

Michael Bromley 7 years ago
parent
commit
b92addd50e

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

@@ -1,8 +1,29 @@
 <vdr-action-bar>
+    <vdr-ab-left>
+        <form class="clr-form search-form" [formGroup]="searchForm">
+            <input
+                type="text"
+                name="searchTerm"
+                formControlName="searchTerm"
+                [placeholder]="'catalog.search-product-name-or-code' | translate"
+                class="clr-input search-input"
+            />
+            <clr-checkbox-wrapper>
+                <input
+                    type="checkbox"
+                    clrCheckbox
+                    formControlName="groupByProduct"
+                    (ngModelChange)="groupByProduct = $event"
+                />
+                <label>{{ 'catalog.group-by-product' | translate }}</label>
+            </clr-checkbox-wrapper>
+        </form>
+    </vdr-ab-left>
     <vdr-ab-right>
         <a class="btn btn-primary" [routerLink]="['./create']">
             <clr-icon shape="plus"></clr-icon>
-            {{ 'catalog.create-new-product' | translate }}
+            <span class="full-label">{{ 'catalog.create-new-product' | translate }}</span>
+            <span class="compact-label">{{ 'common.create' | translate }}</span>
         </a>
     </vdr-ab-right>
 </vdr-action-bar>
@@ -17,21 +38,25 @@
 >
     <vdr-dt-column></vdr-dt-column>
     <vdr-dt-column>{{ 'common.name' | translate }}</vdr-dt-column>
-    <vdr-dt-column>{{ 'catalog.slug' | translate }}</vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
-    <ng-template let-product="item">
+    <ng-template let-result="item">
         <td class="left">
             <div class="image-placeholder">
-                <img [src]="product.featuredAsset?.preview + '?preset=tiny'" />
+                <img
+                    [src]="
+                        (groupByProduct
+                            ? result.productPreview
+                            : result.productVariantPreview || result.productPreview) + '?preset=tiny'
+                    "
+                />
             </div>
         </td>
-        <td class="left">{{ product.name }}</td>
-        <td class="left">{{ product.slug }}</td>
+        <td class="left">{{ groupByProduct ? result.productName : result.productVariantName }}</td>
         <td class="right">
             <vdr-table-row-action
                 iconShape="edit"
                 [label]="'common.edit' | translate"
-                [linkTo]="['./', product.id]"
+                [linkTo]="['./', result.productId]"
             ></vdr-table-row-action>
         </td>
     </ng-template>

+ 9 - 0
admin-ui/src/app/catalog/components/product-list/product-list.component.scss

@@ -5,3 +5,12 @@
     height: 50px;
     background-color: $color-grey-2;
 }
+.search-form {
+    display: flex;
+}
+.search-input {
+    min-width: 300px;
+    @media screen and (max-width: $breakpoint-small){
+        min-width: 100px;
+    }
+}

+ 46 - 5
admin-ui/src/app/catalog/components/product-list/product-list.component.ts

@@ -1,7 +1,9 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { Observable } from 'rxjs';
-import { GetProductList } from 'shared/generated-types';
+import { debounceTime, takeUntil } from 'rxjs/operators';
+import { GetProductList, SearchProducts } from 'shared/generated-types';
 
 import { BaseListComponent } from '../../../common/base-list.component';
 import { DataService } from '../../../data/providers/data.service';
@@ -11,12 +13,51 @@ import { DataService } from '../../../data/providers/data.service';
     templateUrl: './product-list.component.html',
     styleUrls: ['./product-list.component.scss'],
 })
-export class ProductListComponent extends BaseListComponent<GetProductList.Query, GetProductList.Items> {
+export class ProductListComponent
+    extends BaseListComponent<SearchProducts.Query, SearchProducts.Items, SearchProducts.Variables>
+    implements OnInit {
+    searchForm: FormGroup | undefined;
+    groupByProduct = true;
     constructor(private dataService: DataService, router: Router, route: ActivatedRoute) {
         super(router, route);
         super.setQueryFn(
-            (...args: any[]) => this.dataService.product.getProducts(...args),
-            data => data.products,
+            (...args: any[]) =>
+                this.dataService.product.searchProducts(this.getFormValue('searchTerm', ''), ...args),
+            data => data.search,
+            (skip, take) => ({
+                input: {
+                    skip,
+                    take,
+                    term: this.getFormValue('searchTerm', ''),
+                    groupByProduct: this.getFormValue('groupByProduct', true),
+                },
+            }),
         );
     }
+
+    ngOnInit() {
+        super.ngOnInit();
+        this.searchForm = new FormGroup({
+            searchTerm: new FormControl(''),
+            groupByProduct: new FormControl(true),
+        });
+        this.searchForm.valueChanges
+            .pipe(
+                debounceTime(250),
+                takeUntil(this.destroy$),
+            )
+            .subscribe(() => this.refresh());
+    }
+
+    private getFormValue<T>(controlName: string, defaultValue: T): T {
+        if (!this.searchForm) {
+            return defaultValue;
+        }
+        const control = this.searchForm.get(controlName);
+        if (control) {
+            return control.value;
+        } else {
+            throw new Error(`Form does not contain a control named ${controlName}`);
+        }
+    }
 }

+ 14 - 4
admin-ui/src/app/common/base-list.component.ts

@@ -7,19 +7,22 @@ import { QueryResult } from '../data/query-result';
 
 export type ListQueryFn<R> = (take: number, skip: number, ...args: any[]) => QueryResult<R, any>;
 export type MappingFn<T, R> = (result: R) => { items: T[]; totalItems: number };
+export type OnPageChangeFn<V> = (skip: number, take: number) => V;
 
 /**
  * This is a base class which implements the logic required to fetch and manipluate
  * a list of data from a query which returns a PaginatedList type.
  */
-export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestroy {
+export class BaseListComponent<ResultType, ItemType, VariableType = any> implements OnInit, OnDestroy {
     items$: Observable<ItemType[]>;
     totalItems$: Observable<number>;
     itemsPerPage$: Observable<number>;
     currentPage$: Observable<number>;
-    private destroy$ = new Subject<void>();
+    protected destroy$ = new Subject<void>();
     private listQueryFn: ListQueryFn<ResultType>;
     private mappingFn: MappingFn<ItemType, ResultType>;
+    private onPageChangeFn: OnPageChangeFn<VariableType> = (skip, take) =>
+        ({ options: { skip, take } } as any);
     private refresh$ = new BehaviorSubject<undefined>(undefined);
 
     constructor(private router: Router, private route: ActivatedRoute) {}
@@ -27,9 +30,16 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
     /**
      * Sets the fetch function for the list being implemented.
      */
-    setQueryFn(listQueryFn: ListQueryFn<ResultType>, mappingFn: MappingFn<ItemType, ResultType>) {
+    setQueryFn(
+        listQueryFn: ListQueryFn<ResultType>,
+        mappingFn: MappingFn<ItemType, ResultType>,
+        onPageChangeFn?: OnPageChangeFn<VariableType>,
+    ) {
         this.listQueryFn = listQueryFn;
         this.mappingFn = mappingFn;
+        if (onPageChangeFn) {
+            this.onPageChangeFn = onPageChangeFn;
+        }
     }
 
     ngOnInit() {
@@ -43,7 +53,7 @@ export class BaseListComponent<ResultType, ItemType> implements OnInit, OnDestro
         const fetchPage = ([currentPage, itemsPerPage, _]: [number, number, undefined]) => {
             const take = itemsPerPage;
             const skip = (currentPage - 1) * itemsPerPage;
-            listQuery.ref.refetch({ options: { skip, take } });
+            listQuery.ref.refetch(this.onPageChangeFn(skip, take));
         };
 
         this.items$ = listQuery.stream$.pipe(map(data => this.mappingFn(data).items));

+ 17 - 0
admin-ui/src/app/data/definitions/product-definitions.ts

@@ -374,3 +374,20 @@ export const MOVE_PRODUCT_CATEGORY = gql`
     }
     ${PRODUCT_CATEGORY_FRAGMENT}
 `;
+
+export const SEARCH_PRODUCTS = gql`
+    query SearchProducts($input: SearchInput!) {
+        search(input: $input) {
+            totalItems
+            items {
+                productId
+                productName
+                productPreview
+                productVariantId
+                productVariantName
+                productVariantPreview
+                sku
+            }
+        }
+    }
+`;

+ 13 - 0
admin-ui/src/app/data/providers/product-data.service.ts

@@ -19,6 +19,7 @@ import {
     MoveProductCategory,
     MoveProductCategoryInput,
     RemoveOptionGroupFromProduct,
+    SearchProducts,
     UpdateProduct,
     UpdateProductCategory,
     UpdateProductCategoryInput,
@@ -44,6 +45,7 @@ import {
     GET_PRODUCT_WITH_VARIANTS,
     MOVE_PRODUCT_CATEGORY,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
+    SEARCH_PRODUCTS,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_CATEGORY,
     UPDATE_PRODUCT_VARIANTS,
@@ -54,6 +56,17 @@ import { BaseDataService } from './base-data.service';
 export class ProductDataService {
     constructor(private baseDataService: BaseDataService) {}
 
+    searchProducts(term: string, take: number = 10, skip: number = 0) {
+        return this.baseDataService.query<SearchProducts.Query, SearchProducts.Variables>(SEARCH_PRODUCTS, {
+            input: {
+                term,
+                take,
+                skip,
+                groupByProduct: true,
+            },
+        });
+    }
+
     getProducts(take: number = 10, skip: number = 0) {
         return this.baseDataService.query<GetProductList.Query, GetProductList.Variables>(GET_PRODUCT_LIST, {
             options: {

+ 3 - 3
admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts

@@ -144,7 +144,7 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
     ): AdjustmentOperationInput {
         return {
             code: operation.code,
-            arguments: Object.values(formValueOperations.args).map((value, j) => ({
+            arguments: Object.values(formValueOperations.args || {}).map((value, j) => ({
                 name: operation.args[j].name,
                 value: value.toString(),
             })),
@@ -155,8 +155,8 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
         this.shippingMethodForm.patchValue({
             description: shippingMethod.description,
             code: shippingMethod.code,
-            checker: shippingMethod.checker,
-            calculator: shippingMethod.calculator,
+            checker: shippingMethod.checker || {},
+            calculator: shippingMethod.calculator || {},
         });
         this.selectedChecker = shippingMethod.checker;
         this.selectedCalculator = shippingMethod.calculator;

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

@@ -41,6 +41,7 @@
     "generate-product-variants": "Generate product variants",
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-with-options": "This product has options",
+    "group-by-product": "Group by product",
     "move-down": "Move down",
     "move-to": "Move to",
     "move-up": "Move up",
@@ -61,6 +62,7 @@
     "product-name": "Product name",
     "product-variants": "Product variants",
     "remove-asset": "Remove asset",
+    "search-product-name-or-code": "Search by product name or code",
     "select-assets": "Select assets",
     "select-option-group": "Select option group",
     "selected-option-groups": "Selected option groups",

+ 18 - 0
admin-ui/src/styles/styles.scss

@@ -21,3 +21,21 @@ a:link, a:visited {
 .table {
     border-color: $color-grey-2;
 }
+
+.full-label, .compact-label {
+    margin-left: 6px;
+}
+.full-label {
+    display: none;
+}
+.compact-label {
+    display: initial;
+}
+@media screen and (min-width: $breakpoint-small) {
+    .full-label {
+        display: initial;
+    }
+    .compact-label {
+        display: none;
+    }
+}

+ 27 - 0
shared/generated-types.ts

@@ -5575,6 +5575,33 @@ export namespace MoveProductCategory {
     export type MoveProductCategory = ProductCategory.Fragment;
 }
 
+export namespace SearchProducts {
+    export type Variables = {
+        input: SearchInput;
+    };
+
+    export type Query = {
+        __typename?: 'Query';
+        search: Search;
+    };
+
+    export type Search = {
+        __typename?: 'SearchResponse';
+        totalItems: number;
+        items: Items[];
+    };
+
+    export type Items = {
+        __typename?: 'SearchResult';
+        productName: string;
+        productVariantName: string;
+        productId: string;
+        productVariantId: string;
+        productPreview: string;
+        sku: string;
+    };
+}
+
 export namespace GetPromotionList {
     export type Variables = {
         options?: PromotionListOptions | null;