Răsfoiți Sursa

feat(admin-ui): Implement simple update of product detail

Michael Bromley 7 ani în urmă
părinte
comite
2b543f46e1

+ 1 - 1
admin-ui/generate-graphql-types.ts

@@ -10,7 +10,7 @@ import { API_PATH, API_PORT } from '../shared/shared-constants';
 const API_URL = `http://localhost:${API_PORT}/${API_PATH}`;
 const SCHEMA_JSON_FILE = '../schema.json';
 const CLIENT_SCHEMA_FILES = './src/app/data/types/**/*.graphql';
-const CLIENT_QUERY_FILES = '"./src/app/data/(queries|mutations)/**/*.ts"';
+const CLIENT_QUERY_FILES = '"./src/app/data/(queries|mutations|fragments)/**/*.ts"';
 const TYPESCRIPT_DEFINITIONS_FILE = './src/app/data/types/gql-generated-types.ts';
 
 main().catch(e => {

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

@@ -6,6 +6,7 @@ import { SharedModule } from '../shared/shared.module';
 import { catalogRoutes } from './catalog.routes';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
+import { ProductResolver } from './providers/routing/product-resolver';
 
 @NgModule({
     imports: [
@@ -14,7 +15,7 @@ import { ProductListComponent } from './components/product-list/product-list.com
     ],
     exports: [],
     declarations: [ProductListComponent, ProductDetailComponent],
-    providers: [],
+    providers: [ProductResolver],
 })
 export class CatalogModule {
 }

+ 6 - 3
admin-ui/src/app/catalog/catalog.routes.ts

@@ -1,9 +1,9 @@
 import { Route } from '@angular/router';
 import { map } from 'rxjs/operators';
-import { BreadcrumbFunction } from '../core/components/breadcrumb/breadcrumb.component';
 import { DataService } from '../data/providers/data.service';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
+import { ProductResolver } from './providers/routing/product-resolver';
 
 export const catalogRoutes: Route[] = [
     {
@@ -16,6 +16,9 @@ export const catalogRoutes: Route[] = [
     {
         path: 'products/:id',
         component: ProductDetailComponent,
+        resolve: {
+            product: ProductResolver,
+        },
         data: {
             breadcrumb: productBreadcrumb,
         },
@@ -23,7 +26,7 @@ export const catalogRoutes: Route[] = [
 ];
 
 export function productBreadcrumb(data: any, params: any, dataService: DataService) {
-    return dataService.product.getProduct(params.id).single$.pipe(
+    return dataService.product.getProduct(params.id).stream$.pipe(
         map(productData => {
             return [
                    {
@@ -31,7 +34,7 @@ export function productBreadcrumb(data: any, params: any, dataService: DataServi
                        link: ['../', 'products'],
                    },
                    {
-                       label: productData.product.name,
+                       label: `#${params.id} (${productData.product.name})`,
                        link: [params.id],
                    },
                ];

+ 36 - 4
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -1,5 +1,37 @@
-<h1>Product Detail</h1>
+<clr-dropdown>
+    <button type="button" class="btn btn-outline-primary" clrDropdownTrigger>
+        <clr-icon shape="world"></clr-icon>
+        Language: {{ languageCode$ | async | uppercase }}
+        <clr-icon shape="caret down"></clr-icon>
+    </button>
+    <clr-dropdown-menu clrPosition="bottom-right" *clrIfOpen>
+        <button type="button"
+                *ngFor="let code of availableLanguages$ | async"
+                (click)="setLanguage(code)"
+                clrDropdownItem>{{ code }}</button>
+    </clr-dropdown-menu>
+</clr-dropdown>
 
-<pre>
-    {{ product$ | async | json }}
-</pre>
+<form class="form" [formGroup]="productForm" (ngSubmit)="save()">
+    <button class="btn btn-primary"
+            type="submit"
+            [disabled]="productForm.invalid || productForm.pristine">Update</button>
+
+    <section class="form-block">
+        <label>Product</label>
+        <vdr-form-field label="Product name" for="name">
+            <input id="name" type="text" formControlName="name">
+        </vdr-form-field>
+        <vdr-form-field label="Slug" for="slug">
+            <input id="slug" type="text" formControlName="slug">
+        </vdr-form-field>
+        <vdr-form-field label="Description" for="description">
+            <textarea id="description" formControlName="description"></textarea>
+        </vdr-form-field>
+    </section>
+
+    <section class="form-block">
+        <label>Product Variants</label>
+    </section>
+
+</form>

+ 70 - 11
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -1,26 +1,85 @@
 import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { Observable } from 'rxjs';
-
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { combineLatest, Observable, Subject } from 'rxjs';
+import { map, take, takeUntil, tap } from 'rxjs/operators';
 import { DataService } from '../../../data/providers/data.service';
+import { GetProductWithVariants_product, LanguageCode } from '../../../data/types/gql-generated-types';
 
 @Component({
     selector: 'vdr-product-detail',
     templateUrl: './product-detail.component.html',
     styleUrls: ['./product-detail.component.scss'],
 })
-export class ProductDetailComponent implements OnInit {
+export class ProductDetailComponent {
 
-    product$: Observable<any>;
+    product$: Observable<GetProductWithVariants_product>;
+    availableLanguages$: Observable<LanguageCode[]>;
+    languageCode$: Observable<LanguageCode>;
+    productForm: FormGroup;
+    private destroy$ = new Subject<void>();
 
     constructor(private dataService: DataService,
-                private route: ActivatedRoute) { }
+                private router: Router,
+                private route: ActivatedRoute,
+                private formBuilder: FormBuilder) {
+        this.product$ = this.route.snapshot.data.product;
+        this.productForm = this.formBuilder.group({
+            name: ['', Validators.required],
+            slug: '',
+            description: '',
+        });
+
+        this.languageCode$ = this.route.queryParamMap.pipe(
+            map(qpm => qpm.get('lang')),
+            map(lang => !lang ? LanguageCode.en : lang as LanguageCode),
+        );
+
+        this.availableLanguages$ = this.product$.pipe(
+            map(p => p.translations.map(t => t.languageCode)),
+        );
+
+        combineLatest(this.product$, this.languageCode$).pipe(
+            takeUntil(this.destroy$),
+        )
+            .subscribe(([product, languageCode]) => {
+                const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
+                if (currentTranslation) {
+                    this.productForm.setValue({
+                        name: currentTranslation.name,
+                        slug: currentTranslation.slug,
+                        description: currentTranslation.description,
+                    });
+                }
+            });
+    }
 
-    ngOnInit() {
-        const id = this.route.snapshot.paramMap.get('id');
-        if (id) {
-            this.product$ = this.dataService.product.getProduct(id).single$;
-        }
+    ngOnDestroy() {
+        this.destroy$.next();
+        this.destroy$.complete();
     }
 
+    setLanguage(code: LanguageCode) {
+        this.setQueryParam('lang', code);
+    }
+
+    save() {
+        combineLatest(this.product$, this.languageCode$).pipe(take(1))
+            .subscribe(([product, languageCode]) => {
+                const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
+                if (!currentTranslation) {
+                    return;
+                }
+                const index = product.translations.indexOf(currentTranslation);
+                const newTranslation = Object.assign({}, currentTranslation, this.productForm.value);
+                const newProduct = { ...product, ...{ translations: product.translations.slice() } };
+                newProduct.translations.splice(index, 1, newTranslation);
+                this.dataService.product.updateProduct(newProduct).subscribe();
+                this.productForm.markAsPristine();
+            });
+    }
+
+    private setQueryParam(key: string, value: any) {
+        this.router.navigate(['./'], { queryParams: { [key]: value }, relativeTo: this.route, queryParamsHandling: 'merge' });
+    }
 }

+ 30 - 0
admin-ui/src/app/catalog/providers/routing/product-resolver.ts

@@ -0,0 +1,30 @@
+
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { DataService } from '../../../data/providers/data.service';
+import { GetProductWithVariants_product } from '../../../data/types/gql-generated-types';
+import { finalize, map, take, tap } from 'rxjs/operators';
+/**
+ * Resolves the id from the path into a Customer entity.
+ */
+
+@Injectable()
+export class ProductResolver implements Resolve<Observable<GetProductWithVariants_product>> {
+
+    constructor(private dataService: DataService) {}
+
+    resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Observable<GetProductWithVariants_product>> {
+        const id = route.paramMap.get('id');
+
+        if (id) {
+            const stream = this.dataService.product.getProduct(id).mapStream(data => data.product);
+            return stream.pipe(
+                take(1),
+                map(() => stream),
+            );
+        } else {
+            return {} as any;
+        }
+    }
+}

+ 37 - 0
admin-ui/src/app/data/fragments/product-fragments.ts

@@ -0,0 +1,37 @@
+import gql from 'graphql-tag';
+
+export const PRODUCT_WITH_VARIANTS_FRAGMENT = gql`
+    fragment ProductWithVariants on Product {
+        id
+        languageCode
+        name
+        slug
+        image
+        description
+        translations {
+            languageCode
+            name
+            slug
+            description
+        },
+        variants {
+            id
+            languageCode
+            name
+            price
+            sku
+            image
+            options {
+                id
+                code
+                languageCode
+                name
+            }
+            translations {
+                id
+                languageCode
+                name
+            }
+        }
+    }
+`;

+ 11 - 0
admin-ui/src/app/data/mutations/product-mutations.ts

@@ -0,0 +1,11 @@
+import gql from 'graphql-tag';
+import { PRODUCT_WITH_VARIANTS_FRAGMENT } from '../fragments/product-fragments';
+
+export const UPDATE_PRODUCT = gql`
+    mutation UpdateProduct ($input: UpdateProductInput) {
+    	updateProduct(input: $input) {
+    		...ProductWithVariants
+    	}
+    },
+    ${PRODUCT_WITH_VARIANTS_FRAGMENT}
+`;

+ 28 - 5
admin-ui/src/app/data/providers/product-data.service.ts

@@ -1,6 +1,18 @@
-import { ID } from '../../../../../shared/shared-types';
-import { GET_PRODUCT_BY_ID, GET_PRODUCT_LIST } from '../queries/product-queries';
-import { GetProductById, GetProductByIdVariables, GetProductList, GetProductListVariables, LanguageCode } from '../types/gql-generated-types';
+import { Observable } from 'rxjs';
+import { DeepPartial, ID } from '../../../../../shared/shared-types';
+import { UPDATE_PRODUCT } from '../mutations/product-mutations';
+import { GET_PRODUCT_LIST, GET_PRODUCT_WITH_VARIANTS } from '../queries/product-queries';
+import {
+    GetProductList,
+    GetProductListVariables,
+    GetProductWithVariants, GetProductWithVariants_product,
+    GetProductWithVariantsVariables,
+    LanguageCode,
+    ProductWithVariants,
+    UpdateProduct,
+    UpdateProductInput,
+    UpdateProductVariables,
+} from '../types/gql-generated-types';
 import { QueryResult } from '../types/query-result';
 import { BaseDataService } from './base-data.service';
 
@@ -13,12 +25,23 @@ export class ProductDataService {
             .query<GetProductList, GetProductListVariables>(GET_PRODUCT_LIST, { take, skip, languageCode: LanguageCode.en });
     }
 
-    getProduct(id: ID): QueryResult<GetProductById, GetProductByIdVariables> {
+    getProduct(id: ID): QueryResult<GetProductWithVariants, GetProductWithVariantsVariables> {
         const stringId = id.toString();
-        return this.baseDataService.query<GetProductById, GetProductByIdVariables>(GET_PRODUCT_BY_ID, {
+        return this.baseDataService.query<GetProductWithVariants, GetProductWithVariantsVariables>(GET_PRODUCT_WITH_VARIANTS, {
             id: stringId,
             languageCode: LanguageCode.en,
         });
     }
 
+    updateProduct(product: GetProductWithVariants_product): Observable<UpdateProduct> {
+        const input: UpdateProductVariables = {
+            input: {
+                id: product.id,
+                image: product.image,
+                translations: product.translations,
+            },
+        };
+        return this.baseDataService.mutate<UpdateProduct, UpdateProductVariables>(UPDATE_PRODUCT, input);
+    }
+
 }

+ 5 - 13
admin-ui/src/app/data/queries/product-queries.ts

@@ -1,21 +1,13 @@
 import gql from 'graphql-tag';
+import { PRODUCT_WITH_VARIANTS_FRAGMENT } from '../fragments/product-fragments';
 
-export const GET_PRODUCT_BY_ID = gql`
-    query GetProductById($id: ID!, $languageCode: LanguageCode){
+export const GET_PRODUCT_WITH_VARIANTS = gql`
+    query GetProductWithVariants($id: ID!, $languageCode: LanguageCode){
         product(languageCode: $languageCode, id: $id) {
-            id
-            languageCode
-            name
-            slug
-            description
-            translations {
-                languageCode
-                name
-                slug
-                description
-            }
+            ...ProductWithVariants
         }
     }
+    ${PRODUCT_WITH_VARIANTS_FRAGMENT}
 `;
 
 export const GET_PRODUCT_LIST = gql`

+ 171 - 7
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -67,6 +67,69 @@ export interface LogOut {
 }
 
 
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: UpdateProduct
+// ====================================================
+
+export interface UpdateProduct_updateProduct_translations {
+  __typename: "ProductTranslation";
+  languageCode: LanguageCode;
+  name: string;
+  slug: string;
+  description: string | null;
+}
+
+export interface UpdateProduct_updateProduct_variants_options {
+  __typename: "ProductOption";
+  id: string;
+  code: string | null;
+  languageCode: LanguageCode | null;
+  name: string | null;
+}
+
+export interface UpdateProduct_updateProduct_variants_translations {
+  __typename: "ProductVariantTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface UpdateProduct_updateProduct_variants {
+  __typename: "ProductVariant";
+  id: string;
+  languageCode: LanguageCode;
+  name: string | null;
+  price: number | null;
+  sku: string | null;
+  image: string | null;
+  options: UpdateProduct_updateProduct_variants_options[];
+  translations: UpdateProduct_updateProduct_variants_translations[];
+}
+
+export interface UpdateProduct_updateProduct {
+  __typename: "Product";
+  id: string;
+  languageCode: LanguageCode;
+  name: string | null;
+  slug: string | null;
+  image: string | null;
+  description: string | null;
+  translations: UpdateProduct_updateProduct_translations[];
+  variants: UpdateProduct_updateProduct_variants[];
+}
+
+export interface UpdateProduct {
+  updateProduct: UpdateProduct_updateProduct;  // Update an existing Product
+}
+
+export interface UpdateProductVariables {
+  input?: UpdateProductInput | null;
+}
+
+
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 
@@ -107,10 +170,10 @@ export interface GetUserStatus {
 // This file was automatically generated and should not be edited.
 
 // ====================================================
-// GraphQL query operation: GetProductById
+// GraphQL query operation: GetProductWithVariants
 // ====================================================
 
-export interface GetProductById_product_translations {
+export interface GetProductWithVariants_product_translations {
   __typename: "ProductTranslation";
   languageCode: LanguageCode;
   name: string;
@@ -118,21 +181,50 @@ export interface GetProductById_product_translations {
   description: string | null;
 }
 
-export interface GetProductById_product {
+export interface GetProductWithVariants_product_variants_options {
+  __typename: "ProductOption";
+  id: string;
+  code: string | null;
+  languageCode: LanguageCode | null;
+  name: string | null;
+}
+
+export interface GetProductWithVariants_product_variants_translations {
+  __typename: "ProductVariantTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface GetProductWithVariants_product_variants {
+  __typename: "ProductVariant";
+  id: string;
+  languageCode: LanguageCode;
+  name: string | null;
+  price: number | null;
+  sku: string | null;
+  image: string | null;
+  options: GetProductWithVariants_product_variants_options[];
+  translations: GetProductWithVariants_product_variants_translations[];
+}
+
+export interface GetProductWithVariants_product {
   __typename: "Product";
   id: string;
   languageCode: LanguageCode;
   name: string | null;
   slug: string | null;
+  image: string | null;
   description: string | null;
-  translations: (GetProductById_product_translations | null)[] | null;
+  translations: GetProductWithVariants_product_translations[];
+  variants: GetProductWithVariants_product_variants[];
 }
 
-export interface GetProductById {
-  product: GetProductById_product;
+export interface GetProductWithVariants {
+  product: GetProductWithVariants_product;
 }
 
-export interface GetProductByIdVariables {
+export interface GetProductWithVariantsVariables {
   id: string;
   languageCode?: LanguageCode | null;
 }
@@ -170,6 +262,61 @@ export interface GetProductListVariables {
   languageCode?: LanguageCode | null;
 }
 
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL fragment: ProductWithVariants
+// ====================================================
+
+export interface ProductWithVariants_translations {
+  __typename: "ProductTranslation";
+  languageCode: LanguageCode;
+  name: string;
+  slug: string;
+  description: string | null;
+}
+
+export interface ProductWithVariants_variants_options {
+  __typename: "ProductOption";
+  id: string;
+  code: string | null;
+  languageCode: LanguageCode | null;
+  name: string | null;
+}
+
+export interface ProductWithVariants_variants_translations {
+  __typename: "ProductVariantTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface ProductWithVariants_variants {
+  __typename: "ProductVariant";
+  id: string;
+  languageCode: LanguageCode;
+  name: string | null;
+  price: number | null;
+  sku: string | null;
+  image: string | null;
+  options: ProductWithVariants_variants_options[];
+  translations: ProductWithVariants_variants_translations[];
+}
+
+export interface ProductWithVariants {
+  __typename: "Product";
+  id: string;
+  languageCode: LanguageCode;
+  name: string | null;
+  slug: string | null;
+  image: string | null;
+  description: string | null;
+  translations: ProductWithVariants_translations[];
+  variants: ProductWithVariants_variants[];
+}
+
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 
@@ -365,6 +512,23 @@ export enum LanguageCode {
   zu = "zu",
 }
 
+// 
+export interface UpdateProductInput {
+  id: string;
+  image?: string | null;
+  translations: (ProductTranslationInput | null)[];
+  optionGroupCodes?: (string | null)[] | null;
+}
+
+// 
+export interface ProductTranslationInput {
+  id?: string | null;
+  languageCode: LanguageCode;
+  name: string;
+  slug?: string | null;
+  description?: string | null;
+}
+
 //==============================================================
 // END Enums and Input Objects
 //==============================================================

+ 18 - 0
admin-ui/src/app/data/types/query-result.ts

@@ -32,4 +32,22 @@ export class QueryResult<T, V = Record<string, any>> {
         return this.queryRef;
     }
 
+    /**
+     * Returns a single-result Observable after applying the map function.
+     */
+    mapSingle<R>(mapFn: (item: T) => R): Observable<R> {
+        return this.single$.pipe(
+            map(mapFn),
+        );
+    }
+
+    /**
+     * Returns a multiple-result Observable after applying the map function.
+     */
+    mapStream<R>(mapFn: (item: T) => R): Observable<R> {
+        return this.stream$.pipe(
+            map(mapFn),
+        );
+    }
+
 }

Fișier diff suprimat deoarece este prea mare
+ 0 - 0
schema.json


Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff