Просмотр исходного кода

refactor(admin-ui): Move data product detail data fetching into service

Michael Bromley 6 лет назад
Родитель
Сommit
894ea7b3e1

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

@@ -27,6 +27,7 @@ import { ProductSearchInputComponent } from './components/product-search-input/p
 import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component';
 import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component';
 import { VariantPriceDetailComponent } from './components/variant-price-detail/variant-price-detail.component';
+import { ProductDetailService } from './providers/product-detail.service';
 import { CollectionResolver } from './providers/routing/collection-resolver';
 import { FacetResolver } from './providers/routing/facet-resolver';
 import { ProductResolver } from './providers/routing/product-resolver';
@@ -59,6 +60,6 @@ import { ProductResolver } from './providers/routing/product-resolver';
         OptionValueInputComponent,
     ],
     entryComponents: [AssetPickerDialogComponent, ApplyFacetDialogComponent, AssetPreviewComponent],
-    providers: [ProductResolver, FacetResolver, CollectionResolver],
+    providers: [ProductResolver, FacetResolver, CollectionResolver, ProductDetailService],
 })
 export class CatalogModule {}

+ 17 - 111
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -2,8 +2,8 @@ import { Location } from '@angular/common';
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { BehaviorSubject, combineLatest, forkJoin, merge, Observable, of } from 'rxjs';
-import { distinctUntilChanged, map, mergeMap, shareReplay, skip, take, withLatestFrom } from 'rxjs/operators';
+import { combineLatest, merge, Observable } from 'rxjs';
+import { distinctUntilChanged, map, mergeMap, take, withLatestFrom } from 'rxjs/operators';
 import { normalizeString } from 'shared/normalize-string';
 import { CustomFieldConfig } from 'shared/shared-types';
 import { notNullOrUndefined } from 'shared/shared-utils';
@@ -13,7 +13,6 @@ import { IGNORE_CAN_DEACTIVATE_GUARD } from 'src/app/shared/providers/routing/ca
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import {
     CreateProductInput,
-    CreateProductVariantInput,
     FacetWithValues,
     LanguageCode,
     ProductWithVariants,
@@ -27,9 +26,9 @@ import { createUpdatedTranslatable } from '../../../common/utilities/create-upda
 import { flattenFacetValues } from '../../../common/utilities/flatten-facet-values';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
-import { DataService } from '../../../data/providers/data.service';
 import { ServerConfigService } from '../../../data/server-config';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
+import { ProductDetailService } from '../../providers/product-detail.service';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
 import { VariantAssetChange } from '../product-variants-list/product-variants-list.component';
@@ -71,7 +70,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     assetChanges: SelectedAssets = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
     facetValues$: Observable<ProductWithVariants.FacetValues[]>;
-    facets$ = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
+    facets$: Observable<FacetWithValues.Fragment[]>;
     selectedVariantIds: string[] = [];
     variantDisplayMode: 'card' | 'table' = 'card';
     createVariantsConfig: CreateProductVariantsConfig = { groups: [], variants: [] };
@@ -80,7 +79,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         route: ActivatedRoute,
         router: Router,
         serverConfigService: ServerConfigService,
-        private dataService: DataService,
+        private productDetailService: ProductDetailService,
         private formBuilder: FormBuilder,
         private modalService: ModalService,
         private notificationService: NotificationService,
@@ -109,16 +108,14 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         this.init();
         this.product$ = this.entity$;
         this.variants$ = this.product$.pipe(map(product => product.variants));
-        this.taxCategories$ = this.dataService.settings
-            .getTaxCategories()
-            .mapSingle(data => data.taxCategories)
-            .pipe(shareReplay(1));
+        this.taxCategories$ = this.productDetailService.getTaxCategories();
         this.activeTab$ = this.route.paramMap.pipe(map(qpm => qpm.get('tab') as any));
 
         // FacetValues are provided initially by the nested array of the
         // Product entity, but once a fetch to get all Facets is made (as when
         // opening the FacetValue selector modal), then these additional values
         // are concatenated onto the initial array.
+        this.facets$ = this.productDetailService.getFacets();
         const productFacetValues$ = this.product$.pipe(map(product => product.facetValues));
         const allFacetValues$ = this.facets$.pipe(map(flattenFacetValues));
         const productGroup = this.getProductFormGroup();
@@ -241,18 +238,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     }
 
     private displayFacetValueModal(): Observable<string[] | undefined> {
-        let skipValue = 0;
-        if (this.facets$.value.length === 0) {
-            this.dataService.facet
-                .getFacets(9999999, 0)
-                .mapSingle(data => data.facets.items)
-                .subscribe(items => this.facets$.next(items));
-            skipValue = 1;
-        }
-
-        return this.facets$.pipe(
-            skip(skipValue),
-            take(1),
+        return this.productDetailService.getFacets().pipe(
             mergeMap(facets =>
                 this.modalService.fromComponent(ApplyFacetDialogComponent, {
                     size: 'md',
@@ -277,86 +263,11 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                         productGroup as FormGroup,
                         languageCode,
                     ) as CreateProductInput;
-                    const createProduct$ = this.dataService.product.createProduct(newProduct);
-
-                    const createOptionGroups$ = this.createVariantsConfig.groups.length
-                        ? forkJoin(
-                              this.createVariantsConfig.groups.map(c => {
-                                  return this.dataService.product.createProductOptionGroups({
-                                      code: normalizeString(c.name, '-'),
-                                      translations: [{ languageCode, name: c.name }],
-                                      options: c.values.map(v => ({
-                                          code: normalizeString(v, '-'),
-                                          translations: [{ languageCode, name: v }],
-                                      })),
-                                  });
-                              }),
-                          )
-                        : of([]);
-
-                    return forkJoin(createProduct$, createOptionGroups$).pipe(
-                        mergeMap(([{ createProduct }, createOptionGroups]) => {
-                            const optionGroups = createOptionGroups.map(g => g.createProductOptionGroup);
-                            const addOptionsToProduct$ = optionGroups.length
-                                ? forkJoin(
-                                      optionGroups.map(optionGroup => {
-                                          return this.dataService.product.addOptionGroupToProduct({
-                                              productId: createProduct.id,
-                                              optionGroupId: optionGroup.id,
-                                          });
-                                      }),
-                                  )
-                                : of([]);
-                            return addOptionsToProduct$.pipe(
-                                map(() => {
-                                    return { createProduct, optionGroups, languageCode };
-                                }),
-                            );
-                        }),
-                    );
-                }),
-                mergeMap(({ createProduct, optionGroups, languageCode }) => {
-                    const variants: CreateProductVariantInput[] = this.createVariantsConfig.variants.map(
-                        v => {
-                            const optionIds = optionGroups.length
-                                ? v.optionValues.map((optionName, index) => {
-                                      const option = optionGroups[index].options.find(
-                                          o => o.name === optionName,
-                                      );
-                                      if (!option) {
-                                          throw new Error(
-                                              `Could not find a matching ProductOption "${optionName}" when creating variant`,
-                                          );
-                                      }
-                                      return option.id;
-                                  })
-                                : [];
-                            const name = optionGroups.length
-                                ? `${createProduct.name} ${v.optionValues.join(' ')}`
-                                : createProduct.name;
-                            return {
-                                productId: createProduct.id,
-                                price: v.price,
-                                sku: v.sku,
-                                stockOnHand: v.stock,
-                                translations: [
-                                    {
-                                        languageCode,
-                                        name,
-                                    },
-                                ],
-                                optionIds,
-                            };
-                        },
+                    return this.productDetailService.createProduct(
+                        newProduct,
+                        this.createVariantsConfig,
+                        languageCode,
                     );
-                    return this.dataService.product
-                        .createProductVariants(variants)
-                        .pipe(
-                            map(({ createProductVariants }) => ({
-                                createProductVariants,
-                                productId: createProduct.id,
-                            })),
-                        );
                 }),
             )
             .subscribe(
@@ -385,31 +296,26 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 take(1),
                 mergeMap(([product, languageCode]) => {
                     const productGroup = this.getProductFormGroup();
-                    const updateOperations: Array<
-                        Observable<UpdateProductMutation | UpdateProductVariantsMutation>
-                    > = [];
+                    let productInput: UpdateProductInput | undefined;
+                    let variantsInput: UpdateProductVariantInput[] | undefined;
 
                     if (productGroup.dirty || this.assetsChanged()) {
-                        const newProduct = this.getUpdatedProduct(
+                        productInput = this.getUpdatedProduct(
                             product,
                             productGroup as FormGroup,
                             languageCode,
                         ) as UpdateProductInput;
-                        if (newProduct) {
-                            updateOperations.push(this.dataService.product.updateProduct(newProduct));
-                        }
                     }
                     const variantsArray = this.detailForm.get('variants');
                     if ((variantsArray && variantsArray.dirty) || this.variantAssetsChanged()) {
-                        const newVariants = this.getUpdatedProductVariants(
+                        variantsInput = this.getUpdatedProductVariants(
                             product,
                             variantsArray as FormArray,
                             languageCode,
                         );
-                        updateOperations.push(this.dataService.product.updateProductVariants(newVariants));
                     }
 
-                    return forkJoin(updateOperations);
+                    return this.productDetailService.updateProduct(languageCode, productInput, variantsInput);
                 }),
             )
             .subscribe(

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

@@ -55,7 +55,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     }
 
     ngOnChanges(changes: SimpleChanges) {
-        if ('facets' in changes) {
+        if ('facets' in changes && !!changes['facets'].currentValue) {
             this.facetValues = flattenFacetValues(this.facets);
         }
     }

+ 144 - 0
admin-ui/src/app/catalog/providers/product-detail.service.ts

@@ -0,0 +1,144 @@
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, forkJoin, Observable, of } from 'rxjs';
+import { map, mergeMap, shareReplay, skip } from 'rxjs/operators';
+import { normalizeString } from 'shared/normalize-string';
+
+import {
+    CreateProductInput,
+    CreateProductVariantInput,
+    FacetWithValues,
+    LanguageCode,
+    UpdateProductInput,
+    UpdateProductMutation,
+    UpdateProductVariantInput,
+    UpdateProductVariantsMutation,
+} from '../../common/generated-types';
+import { DataService } from '../../data/providers/data.service';
+import { CreateProductVariantsConfig } from '../components/generate-product-variants/generate-product-variants.component';
+
+/**
+ * Handles the logic for making the API calls to perform CRUD operations on a Product and its related
+ * entities. This logic was extracted out of the component because it became too large and hard to follow.
+ */
+@Injectable()
+export class ProductDetailService {
+    private facetsSubject = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
+
+    constructor(private dataService: DataService) {}
+
+    getFacets(): Observable<FacetWithValues.Fragment[]> {
+        let skipValue = 0;
+        if (this.facetsSubject.value.length === 0) {
+            this.dataService.facet
+                .getFacets(9999999, 0)
+                .mapSingle(data => data.facets.items)
+                .subscribe(items => this.facetsSubject.next(items));
+            skipValue = 1;
+        }
+
+        return this.facetsSubject.pipe(skip(skipValue));
+    }
+
+    getTaxCategories() {
+        return this.dataService.settings
+            .getTaxCategories()
+            .mapSingle(data => data.taxCategories)
+            .pipe(shareReplay(1));
+    }
+
+    createProduct(
+        input: CreateProductInput,
+        createVariantsConfig: CreateProductVariantsConfig,
+        languageCode: LanguageCode,
+    ) {
+        const createProduct$ = this.dataService.product.createProduct(input);
+
+        const createOptionGroups$ = createVariantsConfig.groups.length
+            ? forkJoin(
+                  createVariantsConfig.groups.map(c => {
+                      return this.dataService.product.createProductOptionGroups({
+                          code: normalizeString(c.name, '-'),
+                          translations: [{ languageCode, name: c.name }],
+                          options: c.values.map(v => ({
+                              code: normalizeString(v, '-'),
+                              translations: [{ languageCode, name: v }],
+                          })),
+                      });
+                  }),
+              )
+            : of([]);
+
+        return forkJoin(createProduct$, createOptionGroups$).pipe(
+            mergeMap(([{ createProduct }, createOptionGroups]) => {
+                const optionGroups = createOptionGroups.map(g => g.createProductOptionGroup);
+                const addOptionsToProduct$ = optionGroups.length
+                    ? forkJoin(
+                          optionGroups.map(optionGroup => {
+                              return this.dataService.product.addOptionGroupToProduct({
+                                  productId: createProduct.id,
+                                  optionGroupId: optionGroup.id,
+                              });
+                          }),
+                      )
+                    : of([]);
+                return addOptionsToProduct$.pipe(
+                    map(() => {
+                        return { createProduct, optionGroups };
+                    }),
+                );
+            }),
+            mergeMap(({ createProduct, optionGroups }) => {
+                const variants: CreateProductVariantInput[] = createVariantsConfig.variants.map(v => {
+                    const optionIds = optionGroups.length
+                        ? v.optionValues.map((optionName, index) => {
+                              const option = optionGroups[index].options.find(o => o.name === optionName);
+                              if (!option) {
+                                  throw new Error(
+                                      `Could not find a matching ProductOption "${optionName}" when creating variant`,
+                                  );
+                              }
+                              return option.id;
+                          })
+                        : [];
+                    const name = optionGroups.length
+                        ? `${createProduct.name} ${v.optionValues.join(' ')}`
+                        : createProduct.name;
+                    return {
+                        productId: createProduct.id,
+                        price: v.price,
+                        sku: v.sku,
+                        stockOnHand: v.stock,
+                        translations: [
+                            {
+                                languageCode,
+                                name,
+                            },
+                        ],
+                        optionIds,
+                    };
+                });
+                return this.dataService.product.createProductVariants(variants).pipe(
+                    map(({ createProductVariants }) => ({
+                        createProductVariants,
+                        productId: createProduct.id,
+                    })),
+                );
+            }),
+        );
+    }
+
+    updateProduct(
+        languageCode: LanguageCode,
+        productInput?: UpdateProductInput,
+        variantInput?: UpdateProductVariantInput[],
+    ) {
+        const updateOperations: Array<Observable<UpdateProductMutation | UpdateProductVariantsMutation>> = [];
+        if (productInput) {
+            updateOperations.push(this.dataService.product.updateProduct(productInput));
+        }
+        if (variantInput) {
+            updateOperations.push(this.dataService.product.updateProductVariants(variantInput));
+        }
+        return forkJoin(updateOperations);
+    }
+}

+ 38 - 11
admin-ui/src/app/common/generated-types.ts

@@ -459,6 +459,11 @@ export type CreateFacetValueWithFacetInput = {
   translations: Array<FacetValueTranslationInput>,
 };
 
+export type CreateGroupOptionInput = {
+  code: Scalars['String'],
+  translations: Array<ProductOptionGroupTranslationInput>,
+};
+
 export type CreateProductInput = {
   featuredAssetId?: Maybe<Scalars['ID']>,
   assetIds?: Maybe<Array<Scalars['ID']>>,
@@ -470,11 +475,12 @@ export type CreateProductInput = {
 export type CreateProductOptionGroupInput = {
   code: Scalars['String'],
   translations: Array<ProductOptionGroupTranslationInput>,
-  options: Array<CreateProductOptionInput>,
+  options: Array<CreateGroupOptionInput>,
   customFields?: Maybe<Scalars['JSON']>,
 };
 
 export type CreateProductOptionInput = {
+  productOptionGroupId: Scalars['ID'],
   code: Scalars['String'],
   translations: Array<ProductOptionGroupTranslationInput>,
   customFields?: Maybe<Scalars['JSON']>,
@@ -1547,12 +1553,12 @@ export type Mutation = {
   assignRoleToAdministrator: Administrator,
   /** Create a new Asset */
   createAssets: Array<Asset>,
+  login: LoginResult,
+  logout: Scalars['Boolean'],
   /** Create a new Channel */
   createChannel: Channel,
   /** Update an existing Channel */
   updateChannel: Channel,
-  login: LoginResult,
-  logout: Scalars['Boolean'],
   /** Create a new Collection */
   createCollection: Collection,
   /** Update an existing Collection */
@@ -1611,6 +1617,10 @@ export type Mutation = {
   createProductOptionGroup: ProductOptionGroup,
   /** Update an existing ProductOptionGroup */
   updateProductOptionGroup: ProductOptionGroup,
+  /** Create a new ProductOption within a ProductOptionGroup */
+  createProductOption: ProductOption,
+  /** Create a new ProductOption within a ProductOptionGroup */
+  updateProductOption: ProductOption,
   reindex: JobInfo,
   /** Create a new Product */
   createProduct: Product,
@@ -1686,6 +1696,13 @@ export type MutationCreateAssetsArgs = {
 };
 
 
+export type MutationLoginArgs = {
+  username: Scalars['String'],
+  password: Scalars['String'],
+  rememberMe?: Maybe<Scalars['Boolean']>
+};
+
+
 export type MutationCreateChannelArgs = {
   input: CreateChannelInput
 };
@@ -1696,13 +1713,6 @@ export type MutationUpdateChannelArgs = {
 };
 
 
-export type MutationLoginArgs = {
-  username: Scalars['String'],
-  password: Scalars['String'],
-  rememberMe?: Maybe<Scalars['Boolean']>
-};
-
-
 export type MutationCreateCollectionArgs = {
   input: CreateCollectionInput
 };
@@ -1874,6 +1884,16 @@ export type MutationUpdateProductOptionGroupArgs = {
 };
 
 
+export type MutationCreateProductOptionArgs = {
+  input: CreateProductOptionInput
+};
+
+
+export type MutationUpdateProductOptionArgs = {
+  input: UpdateProductOptionInput
+};
+
+
 export type MutationCreateProductArgs = {
   input: CreateProductInput
 };
@@ -2513,10 +2533,10 @@ export type Query = {
   administrator?: Maybe<Administrator>,
   assets: AssetList,
   asset?: Maybe<Asset>,
+  me?: Maybe<CurrentUser>,
   channels: Array<Channel>,
   channel?: Maybe<Channel>,
   activeChannel: Channel,
-  me?: Maybe<CurrentUser>,
   collections: CollectionList,
   collection?: Maybe<Collection>,
   collectionFilters: Array<ConfigurableOperation>,
@@ -3170,6 +3190,13 @@ export type UpdateProductOptionGroupInput = {
   customFields?: Maybe<Scalars['JSON']>,
 };
 
+export type UpdateProductOptionInput = {
+  id: Scalars['ID'],
+  code?: Maybe<Scalars['String']>,
+  translations?: Maybe<Array<ProductOptionGroupTranslationInput>>,
+  customFields?: Maybe<Scalars['JSON']>,
+};
+
 export type UpdateProductVariantInput = {
   id: Scalars['ID'],
   enabled?: Maybe<Scalars['Boolean']>,