Browse Source

feat: Add FacetValues to Product entity

Relates to #46
Michael Bromley 7 years ago
parent
commit
c28faa91c1

+ 2 - 2
admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.html

@@ -1,4 +1,4 @@
-<ng-template vdrDialogTitle>{{ 'catalog.apply-facets' | translate }}</ng-template>
+<ng-template vdrDialogTitle>{{ 'catalog.add-facets' | translate }}</ng-template>
 
 
 <vdr-facet-value-selector
 <vdr-facet-value-selector
     [facets]="facets"
     [facets]="facets"
@@ -13,6 +13,6 @@
         [disabled]="selectedValues.length === 0"
         [disabled]="selectedValues.length === 0"
         class="btn btn-primary"
         class="btn btn-primary"
     >
     >
-        {{ 'catalog.apply-facets' | translate }}
+        {{ 'catalog.add-facets' | translate }}
     </button>
     </button>
 </ng-template>
 </ng-template>

+ 15 - 3
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -68,6 +68,18 @@
                                     ></vdr-custom-field-control>
                                     ></vdr-custom-field-control>
                                 </ng-container>
                                 </ng-container>
                             </section>
                             </section>
+
+                            <div class="facets">
+                                <vdr-facet-value-chip
+                                    *ngFor="let facetValue of (facetValues$ | async)"
+                                    [facetValue]="facetValue"
+                                    (remove)="removeProductFacetValue(facetValue.id)"
+                                ></vdr-facet-value-chip>
+                                <button class="btn btn-sm btn-secondary" (click)="selectProductFacetValue()">
+                                    <clr-icon shape="plus"></clr-icon>
+                                    {{ 'catalog.add-facets' | translate }}
+                                </button>
+                            </div>
                         </section>
                         </section>
                     </div>
                     </div>
                     <div class="clr-col-md-auto">
                     <div class="clr-col-md-auto">
@@ -99,14 +111,14 @@
                             [taxCategories]="taxCategories$ | async"
                             [taxCategories]="taxCategories$ | async"
                             (assetChange)="variantAssetChange($event)"
                             (assetChange)="variantAssetChange($event)"
                             (selectionChange)="selectedVariantIds = $event"
                             (selectionChange)="selectedVariantIds = $event"
-                            #productVariantsList
                         >
                         >
                             <button
                             <button
                                 class="btn btn-sm btn-secondary"
                                 class="btn btn-sm btn-secondary"
                                 [disabled]="selectedVariantIds.length === 0"
                                 [disabled]="selectedVariantIds.length === 0"
-                                (click)="selectFacetValue(productVariantsList.selectedVariantIds)"
+                                (click)="selectVariantFacetValue(selectedVariantIds)"
                             >
                             >
-                                {{ 'catalog.apply-facets' | translate }}
+                                <clr-icon shape="plus"></clr-icon>
+                                {{ 'catalog.add-facets' | translate }}
                             </button>
                             </button>
                         </vdr-product-variants-list>
                         </vdr-product-variants-list>
                     </ng-template>
                     </ng-template>

+ 86 - 29
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -2,7 +2,7 @@ import { Location } from '@angular/common';
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
-import { BehaviorSubject, combineLatest, forkJoin, Observable } from 'rxjs';
+import { BehaviorSubject, combineLatest, forkJoin, merge, Observable } from 'rxjs';
 import { map, mergeMap, skip, take, withLatestFrom } from 'rxjs/operators';
 import { map, mergeMap, skip, take, withLatestFrom } from 'rxjs/operators';
 import {
 import {
     CreateProductInput,
     CreateProductInput,
@@ -16,9 +16,11 @@ import {
 import { normalizeString } from 'shared/normalize-string';
 import { normalizeString } from 'shared/normalize-string';
 import { CustomFieldConfig } from 'shared/shared-types';
 import { CustomFieldConfig } from 'shared/shared-types';
 import { notNullOrUndefined } from 'shared/shared-utils';
 import { notNullOrUndefined } from 'shared/shared-utils';
+import { unique } from 'shared/unique';
 
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import { createUpdatedTranslatable } from '../../../common/utilities/create-updated-translatable';
 import { createUpdatedTranslatable } from '../../../common/utilities/create-updated-translatable';
+import { flattenFacetValues } from '../../../common/utilities/flatten-facet-values';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 import { DataService } from '../../../data/providers/data.service';
@@ -60,6 +62,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
     productForm: FormGroup;
     productForm: FormGroup;
     assetChanges: SelectedAssets = {};
     assetChanges: SelectedAssets = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
+    facetValues$: Observable<ProductWithVariants.FacetValues[]>;
     facets$ = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
     facets$ = new BehaviorSubject<FacetWithValues.Fragment[]>([]);
     selectedVariantIds: string[] = [];
     selectedVariantIds: string[] = [];
 
 
@@ -82,6 +85,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 name: ['', Validators.required],
                 name: ['', Validators.required],
                 slug: '',
                 slug: '',
                 description: '',
                 description: '',
+                facetValueIds: [[]],
                 customFields: this.formBuilder.group(
                 customFields: this.formBuilder.group(
                     this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
                     this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
                 ),
                 ),
@@ -98,6 +102,25 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             .getTaxCategories()
             .getTaxCategories()
             .mapSingle(data => data.taxCategories);
             .mapSingle(data => data.taxCategories);
         this.activeTab$ = this.route.queryParamMap.pipe(map(qpm => qpm.get('tab') as any));
         this.activeTab$ = this.route.queryParamMap.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.
+        const productFacetValues$ = this.product$.pipe(map(product => product.facetValues));
+        const allFacetValues$ = this.facets$.pipe(map(flattenFacetValues));
+
+        const productGroup = this.getProductFormGroup();
+        const formChangeFacetValues$ = combineLatest(
+            productGroup.valueChanges.pipe(map(val => val.facetValueIds as string[])),
+            allFacetValues$,
+        ).pipe(
+            map(([facetValueIds, facetValues]) =>
+                facetValueIds.map(id => facetValues.find(fv => fv.id === id)).filter(notNullOrUndefined),
+            ),
+        );
+
+        this.facetValues$ = merge(productFacetValues$, formChangeFacetValues$);
     }
     }
 
 
     ngOnDestroy() {
     ngOnDestroy() {
@@ -142,41 +165,43 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         });
         });
     }
     }
 
 
+    selectProductFacetValue() {
+        this.displayFacetValueModal().subscribe(facetValueIds => {
+            if (facetValueIds) {
+                const productGroup = this.getProductFormGroup();
+                const currentFacetValueIds = productGroup.value.facetValueIds;
+                productGroup.patchValue({
+                    facetValueIds: unique([...currentFacetValueIds, ...facetValueIds]),
+                });
+                productGroup.markAsDirty();
+            }
+        });
+    }
+
+    removeProductFacetValue(facetValueId: string) {
+        const productGroup = this.getProductFormGroup();
+        const currentFacetValueIds = productGroup.value.facetValueIds;
+        productGroup.patchValue({
+            facetValueIds: currentFacetValueIds.filter(id => id !== facetValueId),
+        });
+    }
+
     /**
     /**
      * Opens a dialog to select FacetValues to apply to the select ProductVariants.
      * Opens a dialog to select FacetValues to apply to the select ProductVariants.
      */
      */
-    selectFacetValue(selectedVariantIds: string[]) {
-        this.dataService.facet
-            .getFacets(9999999, 0)
-            .mapSingle(data => data.facets.items)
-            .subscribe(items => this.facets$.next(items));
-
-        this.facets$
-            .pipe(
-                skip(1),
-                take(1),
-                mergeMap(facets =>
-                    this.modalService.fromComponent(ApplyFacetDialogComponent, {
-                        size: 'md',
-                        locals: { facets },
-                    }),
-                ),
-                map(facetValues => facetValues && facetValues.map(v => v.id)),
-                withLatestFrom(this.variants$),
-            )
+    selectVariantFacetValue(selectedVariantIds: string[]) {
+        this.displayFacetValueModal()
+            .pipe(withLatestFrom(this.variants$))
             .subscribe(([facetValueIds, variants]) => {
             .subscribe(([facetValueIds, variants]) => {
                 if (facetValueIds) {
                 if (facetValueIds) {
                     for (const variantId of selectedVariantIds) {
                     for (const variantId of selectedVariantIds) {
                         const index = variants.findIndex(v => v.id === variantId);
                         const index = variants.findIndex(v => v.id === variantId);
                         const variant = variants[index];
                         const variant = variants[index];
                         const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
                         const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
-                        const uniqueFacetValueIds = Array.from(
-                            new Set([...existingFacetValueIds, ...facetValueIds]),
-                        );
                         const variantFormGroup = this.productForm.get(['variants', index]);
                         const variantFormGroup = this.productForm.get(['variants', index]);
                         if (variantFormGroup) {
                         if (variantFormGroup) {
                             variantFormGroup.patchValue({
                             variantFormGroup.patchValue({
-                                facetValueIds: uniqueFacetValueIds,
+                                facetValueIds: unique([...existingFacetValueIds, ...facetValueIds]),
                             });
                             });
                             variantFormGroup.markAsDirty();
                             variantFormGroup.markAsDirty();
                         }
                         }
@@ -186,9 +211,32 @@ 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),
+            mergeMap(facets =>
+                this.modalService.fromComponent(ApplyFacetDialogComponent, {
+                    size: 'md',
+                    locals: { facets },
+                }),
+            ),
+            map(facetValues => facetValues && facetValues.map(v => v.id)),
+        );
+    }
+
     create() {
     create() {
-        const productGroup = this.productForm.get('product');
-        if (!productGroup || !productGroup.dirty) {
+        const productGroup = this.getProductFormGroup();
+        if (!productGroup.dirty) {
             return;
             return;
         }
         }
         combineLatest(this.product$, this.languageCode$)
         combineLatest(this.product$, this.languageCode$)
@@ -226,10 +274,10 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             .pipe(
             .pipe(
                 take(1),
                 take(1),
                 mergeMap(([product, languageCode]) => {
                 mergeMap(([product, languageCode]) => {
-                    const productGroup = this.productForm.get('product');
+                    const productGroup = this.getProductFormGroup();
                     const updateOperations: Array<Observable<any>> = [];
                     const updateOperations: Array<Observable<any>> = [];
 
 
-                    if ((productGroup && productGroup.dirty) || this.assetsChanged()) {
+                    if (productGroup.dirty || this.assetsChanged()) {
                         const newProduct = this.getUpdatedProduct(
                         const newProduct = this.getUpdatedProduct(
                             product,
                             product,
                             productGroup as FormGroup,
                             productGroup as FormGroup,
@@ -280,6 +328,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                     name: currentTranslation.name,
                     name: currentTranslation.name,
                     slug: currentTranslation.slug,
                     slug: currentTranslation.slug,
                     description: currentTranslation.description,
                     description: currentTranslation.description,
+                    facetValueIds: product.facetValues.map(fv => fv.id),
                 },
                 },
             });
             });
 
 
@@ -350,7 +399,11 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
                 description: product.description || '',
                 description: product.description || '',
             },
             },
         });
         });
-        return { ...updatedProduct, ...this.assetChanges } as UpdateProductInput | CreateProductInput;
+        return {
+            ...updatedProduct,
+            ...this.assetChanges,
+            facetValueIds: productFormGroup.value.facetValueIds,
+        } as UpdateProductInput | CreateProductInput;
     }
     }
 
 
     /**
     /**
@@ -391,4 +444,8 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             })
             })
             .filter(notNullOrUndefined);
             .filter(notNullOrUndefined);
     }
     }
+
+    private getProductFormGroup(): FormGroup {
+        return this.productForm.get('product') as FormGroup;
+    }
 }
 }

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

@@ -20,6 +20,7 @@ export class ProductResolver extends BaseEntityResolver<ProductWithVariants.Frag
                 description: '',
                 description: '',
                 translations: [],
                 translations: [],
                 optionGroups: [],
                 optionGroups: [],
+                facetValues: [],
                 variants: [],
                 variants: [],
             },
             },
             id => this.dataService.product.getProduct(id).mapStream(data => data.product),
             id => this.dataService.product.getProduct(id).mapStream(data => data.product),

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

@@ -88,6 +88,15 @@ export const PRODUCT_WITH_VARIANTS_FRAGMENT = gql`
         variants {
         variants {
             ...ProductVariant
             ...ProductVariant
         }
         }
+        facetValues {
+            id
+            code
+            name
+            facet {
+                id
+                name
+            }
+        }
     }
     }
     ${PRODUCT_VARIANT_FRAGMENT}
     ${PRODUCT_VARIANT_FRAGMENT}
     ${ASSET_FRAGMENT}
     ${ASSET_FRAGMENT}

+ 15 - 2
admin-ui/src/app/data/providers/product-data.service.ts

@@ -76,7 +76,13 @@ export class ProductDataService {
 
 
     createProduct(product: CreateProductInput) {
     createProduct(product: CreateProductInput) {
         const input: CreateProduct.Variables = {
         const input: CreateProduct.Variables = {
-            input: pick(product, ['translations', 'customFields', 'assetIds', 'featuredAssetId']),
+            input: pick(product, [
+                'translations',
+                'customFields',
+                'assetIds',
+                'featuredAssetId',
+                'facetValueIds',
+            ]),
         };
         };
         return this.baseDataService.mutate<CreateProduct.Mutation, CreateProduct.Variables>(
         return this.baseDataService.mutate<CreateProduct.Mutation, CreateProduct.Variables>(
             CREATE_PRODUCT,
             CREATE_PRODUCT,
@@ -86,7 +92,14 @@ export class ProductDataService {
 
 
     updateProduct(product: UpdateProductInput) {
     updateProduct(product: UpdateProductInput) {
         const input: UpdateProduct.Variables = {
         const input: UpdateProduct.Variables = {
-            input: pick(product, ['id', 'translations', 'customFields', 'assetIds', 'featuredAssetId']),
+            input: pick(product, [
+                'id',
+                'translations',
+                'customFields',
+                'assetIds',
+                'featuredAssetId',
+                'facetValueIds',
+            ]),
         };
         };
         return this.baseDataService.mutate<UpdateProduct.Mutation, UpdateProduct.Variables>(
         return this.baseDataService.mutate<UpdateProduct.Mutation, UpdateProduct.Variables>(
             UPDATE_PRODUCT,
             UPDATE_PRODUCT,

+ 1 - 1
admin-ui/src/i18n-messages/en.json

@@ -25,7 +25,7 @@
     "add-asset-to-product": "Add {count, plural, 0 {assets} one {1 asset} other {{count} assets}} to product",
     "add-asset-to-product": "Add {count, plural, 0 {assets} one {1 asset} other {{count} assets}} to product",
     "add-facet": "Add facet",
     "add-facet": "Add facet",
     "add-facet-value": "Add facet value",
     "add-facet-value": "Add facet value",
-    "apply-facets": "Apply facets",
+    "add-facets": "Add facets",
     "assets-selected-count": "{ count } assets selected",
     "assets-selected-count": "{ count } assets selected",
     "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.",
     "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.",
     "create-group": "Create option group",
     "create-group": "Create option group",

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 3 - 0
server/e2e/__snapshots__/product.e2e-spec.ts.snap

@@ -4,6 +4,7 @@ exports[`Product resolver product mutation createProduct creates a new Product 1
 Object {
 Object {
   "assets": Array [],
   "assets": Array [],
   "description": "A baked potato",
   "description": "A baked potato",
+  "facetValues": Array [],
   "featuredAsset": null,
   "featuredAsset": null,
   "id": "T_21",
   "id": "T_21",
   "languageCode": "en",
   "languageCode": "en",
@@ -32,6 +33,7 @@ exports[`Product resolver product mutation updateProduct updates a Product 1`] =
 Object {
 Object {
   "assets": Array [],
   "assets": Array [],
   "description": "A blob of mashed potato",
   "description": "A blob of mashed potato",
+  "facetValues": Array [],
   "featuredAsset": null,
   "featuredAsset": null,
   "id": "T_21",
   "id": "T_21",
   "languageCode": "en",
   "languageCode": "en",
@@ -79,6 +81,7 @@ Object {
     },
     },
   ],
   ],
   "description": "en Accusantium sed libero repudiandae.",
   "description": "en Accusantium sed libero repudiandae.",
+  "facetValues": Array [],
   "featuredAsset": Object {
   "featuredAsset": Object {
     "fileSize": 4,
     "fileSize": 4,
     "id": "T_5",
     "id": "T_5",

+ 13 - 0
server/e2e/product.e2e-spec.ts

@@ -311,6 +311,19 @@ describe('Product resolver', () => {
             expect(result.updateProduct.featuredAsset!.id).toBe(assets[0].id);
             expect(result.updateProduct.featuredAsset!.id).toBe(assets[0].id);
         });
         });
 
 
+        it('updateProduct updates FacetValues', async () => {
+            const result = await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(
+                UPDATE_PRODUCT,
+                {
+                    input: {
+                        id: newProduct.id,
+                        facetValueIds: ['T_1'],
+                    },
+                },
+            );
+            expect(result.updateProduct.facetValues.length).toEqual(1);
+        });
+
         it('updateProduct errors with an invalid productId', async () => {
         it('updateProduct errors with an invalid productId', async () => {
             try {
             try {
                 await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {
                 await client.query<UpdateProduct.Mutation, UpdateProduct.Variables>(UPDATE_PRODUCT, {

+ 2 - 2
server/src/api/resolvers/product.resolver.ts

@@ -65,7 +65,7 @@ export class ProductResolver {
 
 
     @Mutation()
     @Mutation()
     @Allow(Permission.CreateCatalog)
     @Allow(Permission.CreateCatalog)
-    @Decode('assetIds', 'featuredAssetId')
+    @Decode('assetIds', 'featuredAssetId', 'facetValueIds')
     async createProduct(
     async createProduct(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: CreateProductMutationArgs,
         @Args() args: CreateProductMutationArgs,
@@ -76,7 +76,7 @@ export class ProductResolver {
 
 
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
-    @Decode('assetIds', 'featuredAssetId')
+    @Decode('assetIds', 'featuredAssetId', 'facetValueIds')
     async updateProduct(
     async updateProduct(
         @Ctx() ctx: RequestContext,
         @Ctx() ctx: RequestContext,
         @Args() args: UpdateProductMutationArgs,
         @Args() args: UpdateProductMutationArgs,

+ 5 - 0
server/src/entity/product/product.entity.ts

@@ -7,6 +7,7 @@ import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
 import { Channel } from '../channel/channel.entity';
 import { Channel } from '../channel/channel.entity';
 import { CustomProductFields } from '../custom-entity-fields';
 import { CustomProductFields } from '../custom-entity-fields';
+import { FacetValue } from '../facet-value/facet-value.entity';
 import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
 import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
 import { ProductVariant } from '../product-variant/product-variant.entity';
 import { ProductVariant } from '../product-variant/product-variant.entity';
 
 
@@ -40,6 +41,10 @@ export class Product extends VendureEntity implements Translatable, HasCustomFie
     @JoinTable()
     @JoinTable()
     optionGroups: ProductOptionGroup[];
     optionGroups: ProductOptionGroup[];
 
 
+    @ManyToMany(type => FacetValue)
+    @JoinTable()
+    facetValues: FacetValue[];
+
     @Column(type => CustomProductFields)
     @Column(type => CustomProductFields)
     customFields: CustomProductFields;
     customFields: CustomProductFields;
 
 

+ 3 - 0
server/src/entity/product/product.graphql

@@ -10,6 +10,7 @@ type Product implements Node {
     assets: [Asset!]!
     assets: [Asset!]!
     variants: [ProductVariant!]!
     variants: [ProductVariant!]!
     optionGroups: [ProductOptionGroup!]!
     optionGroups: [ProductOptionGroup!]!
+    facetValues: [FacetValue!]!
     translations: [ProductTranslation!]!
     translations: [ProductTranslation!]!
 }
 }
 
 
@@ -34,6 +35,7 @@ input ProductTranslationInput {
 input CreateProductInput {
 input CreateProductInput {
     featuredAssetId: ID
     featuredAssetId: ID
     assetIds: [ID!]
     assetIds: [ID!]
+    facetValueIds: [ID!]
     translations: [ProductTranslationInput!]!
     translations: [ProductTranslationInput!]!
 }
 }
 
 
@@ -41,5 +43,6 @@ input UpdateProductInput {
     id: ID!
     id: ID!
     featuredAssetId: ID
     featuredAssetId: ID
     assetIds: [ID!]
     assetIds: [ID!]
+    facetValueIds: [ID!]
     translations: [ProductTranslationInput!]
     translations: [ProductTranslationInput!]
 }
 }

+ 27 - 4
server/src/service/services/product.service.ts

@@ -18,6 +18,7 @@ import { TranslatableSaver } from '../helpers/translatable-saver/translatable-sa
 import { translateDeep } from '../helpers/utils/translate-entity';
 import { translateDeep } from '../helpers/utils/translate-entity';
 
 
 import { ChannelService } from './channel.service';
 import { ChannelService } from './channel.service';
+import { FacetValueService } from './facet-value.service';
 import { ProductVariantService } from './product-variant.service';
 import { ProductVariantService } from './product-variant.service';
 import { TaxRateService } from './tax-rate.service';
 import { TaxRateService } from './tax-rate.service';
 
 
@@ -28,6 +29,7 @@ export class ProductService {
         private channelService: ChannelService,
         private channelService: ChannelService,
         private assetUpdater: AssetUpdater,
         private assetUpdater: AssetUpdater,
         private productVariantService: ProductVariantService,
         private productVariantService: ProductVariantService,
+        private facetValueService: FacetValueService,
         private taxRateService: TaxRateService,
         private taxRateService: TaxRateService,
         private listQueryBuilder: ListQueryBuilder,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
         private translatableSaver: TranslatableSaver,
@@ -37,14 +39,25 @@ export class ProductService {
         ctx: RequestContext,
         ctx: RequestContext,
         options?: ListQueryOptions<Product>,
         options?: ListQueryOptions<Product>,
     ): Promise<PaginatedList<Translated<Product>>> {
     ): Promise<PaginatedList<Translated<Product>>> {
-        const relations = ['featuredAsset', 'assets', 'optionGroups', 'channels'];
+        const relations = [
+            'featuredAsset',
+            'assets',
+            'optionGroups',
+            'channels',
+            'facetValues',
+            'facetValues.facet',
+        ];
 
 
         return this.listQueryBuilder
         return this.listQueryBuilder
             .build(Product, options, { relations, channelId: ctx.channelId })
             .build(Product, options, { relations, channelId: ctx.channelId })
             .getManyAndCount()
             .getManyAndCount()
             .then(async ([products, totalItems]) => {
             .then(async ([products, totalItems]) => {
                 const items = products.map(product =>
                 const items = products.map(product =>
-                    translateDeep(product, ctx.languageCode, ['optionGroups']),
+                    translateDeep(product, ctx.languageCode, [
+                        'optionGroups',
+                        'facetValues',
+                        ['facetValues', 'facet'],
+                    ]),
                 );
                 );
                 return {
                 return {
                     items,
                     items,
@@ -54,12 +67,16 @@ export class ProductService {
     }
     }
 
 
     async findOne(ctx: RequestContext, productId: ID): Promise<Translated<Product> | undefined> {
     async findOne(ctx: RequestContext, productId: ID): Promise<Translated<Product> | undefined> {
-        const relations = ['featuredAsset', 'assets', 'optionGroups'];
+        const relations = ['featuredAsset', 'assets', 'optionGroups', 'facetValues', 'facetValues.facet'];
         const product = await this.connection.manager.findOne(Product, productId, { relations });
         const product = await this.connection.manager.findOne(Product, productId, { relations });
         if (!product) {
         if (!product) {
             return;
             return;
         }
         }
-        return translateDeep(product, ctx.languageCode, ['optionGroups']);
+        return translateDeep(product, ctx.languageCode, [
+            'optionGroups',
+            'facetValues',
+            ['facetValues', 'facet'],
+        ]);
     }
     }
 
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
@@ -69,6 +86,9 @@ export class ProductService {
             translationType: ProductTranslation,
             translationType: ProductTranslation,
             beforeSave: async p => {
             beforeSave: async p => {
                 this.channelService.assignToChannels(p, ctx);
                 this.channelService.assignToChannels(p, ctx);
+                if (input.facetValueIds) {
+                    p.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
+                }
                 await this.assetUpdater.updateEntityAssets(p, input);
                 await this.assetUpdater.updateEntityAssets(p, input);
             },
             },
         });
         });
@@ -81,6 +101,9 @@ export class ProductService {
             entityType: Product,
             entityType: Product,
             translationType: ProductTranslation,
             translationType: ProductTranslation,
             beforeSave: async p => {
             beforeSave: async p => {
+                if (input.facetValueIds) {
+                    p.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
+                }
                 await this.assetUpdater.updateEntityAssets(p, input);
                 await this.assetUpdater.updateEntityAssets(p, input);
             },
             },
         });
         });

+ 24 - 0
shared/generated-types.ts

@@ -544,6 +544,7 @@ export interface Product extends Node {
     assets: Asset[];
     assets: Asset[];
     variants: ProductVariant[];
     variants: ProductVariant[];
     optionGroups: ProductOptionGroup[];
     optionGroups: ProductOptionGroup[];
+    facetValues: FacetValue[];
     translations: ProductTranslation[];
     translations: ProductTranslation[];
     customFields?: Json | null;
     customFields?: Json | null;
 }
 }
@@ -1253,6 +1254,7 @@ export interface UpdateProductOptionGroupInput {
 export interface CreateProductInput {
 export interface CreateProductInput {
     featuredAssetId?: string | null;
     featuredAssetId?: string | null;
     assetIds?: string[] | null;
     assetIds?: string[] | null;
+    facetValueIds?: string[] | null;
     translations: ProductTranslationInput[];
     translations: ProductTranslationInput[];
     customFields?: Json | null;
     customFields?: Json | null;
 }
 }
@@ -1270,6 +1272,7 @@ export interface UpdateProductInput {
     id: string;
     id: string;
     featuredAssetId?: string | null;
     featuredAssetId?: string | null;
     assetIds?: string[] | null;
     assetIds?: string[] | null;
+    facetValueIds?: string[] | null;
     translations?: ProductTranslationInput[] | null;
     translations?: ProductTranslationInput[] | null;
     customFields?: Json | null;
     customFields?: Json | null;
 }
 }
@@ -3692,6 +3695,7 @@ export namespace ProductResolvers {
         assets?: AssetsResolver<Asset[], any, Context>;
         assets?: AssetsResolver<Asset[], any, Context>;
         variants?: VariantsResolver<ProductVariant[], any, Context>;
         variants?: VariantsResolver<ProductVariant[], any, Context>;
         optionGroups?: OptionGroupsResolver<ProductOptionGroup[], any, Context>;
         optionGroups?: OptionGroupsResolver<ProductOptionGroup[], any, Context>;
+        facetValues?: FacetValuesResolver<FacetValue[], any, Context>;
         translations?: TranslationsResolver<ProductTranslation[], any, Context>;
         translations?: TranslationsResolver<ProductTranslation[], any, Context>;
         customFields?: CustomFieldsResolver<Json | null, any, Context>;
         customFields?: CustomFieldsResolver<Json | null, any, Context>;
     }
     }
@@ -3723,6 +3727,11 @@ export namespace ProductResolvers {
         Parent,
         Parent,
         Context
         Context
     >;
     >;
+    export type FacetValuesResolver<R = FacetValue[], Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
     export type TranslationsResolver<R = ProductTranslation[], Parent = any, Context = any> = Resolver<
     export type TranslationsResolver<R = ProductTranslation[], Parent = any, Context = any> = Resolver<
         R,
         R,
         Parent,
         Parent,
@@ -6334,6 +6343,7 @@ export namespace ProductWithVariants {
         translations: Translations[];
         translations: Translations[];
         optionGroups: OptionGroups[];
         optionGroups: OptionGroups[];
         variants: Variants[];
         variants: Variants[];
+        facetValues: FacetValues[];
     };
     };
 
 
     export type FeaturedAsset = Asset.Fragment;
     export type FeaturedAsset = Asset.Fragment;
@@ -6357,6 +6367,20 @@ export namespace ProductWithVariants {
     };
     };
 
 
     export type Variants = ProductVariant.Fragment;
     export type Variants = ProductVariant.Fragment;
+
+    export type FacetValues = {
+        __typename?: 'FacetValue';
+        id: string;
+        code: string;
+        name: string;
+        facet: Facet;
+    };
+
+    export type Facet = {
+        __typename?: 'Facet';
+        id: string;
+        name: string;
+    };
 }
 }
 
 
 export namespace ProductOptionGroup {
 export namespace ProductOptionGroup {

Some files were not shown because too many files changed in this diff