Browse Source

feat(admin-ui): Implement applying facets to ProductVariants

Relates to #2
Michael Bromley 7 years ago
parent
commit
9f5e7a6915
23 changed files with 359 additions and 22 deletions
  1. 1 0
      admin-ui/package.json
  2. 9 1
      admin-ui/src/app/catalog/catalog.module.ts
  3. 15 0
      admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.html
  4. 0 0
      admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.scss
  5. 24 0
      admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.ts
  6. 4 0
      admin-ui/src/app/catalog/components/facet-list/facet-list.component.html
  7. 10 0
      admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.html
  8. 0 0
      admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.scss
  9. 44 0
      admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.ts
  10. 5 3
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.html
  11. 24 1
      admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts
  12. 12 6
      admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html
  13. 10 0
      admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.scss
  14. 5 0
      admin-ui/src/app/data/fragments/product-fragments.ts
  15. 12 0
      admin-ui/src/app/data/mutations/product-mutations.ts
  16. 1 0
      admin-ui/src/app/data/providers/data.service.mock.ts
  17. 16 0
      admin-ui/src/app/data/providers/product-data.service.ts
  18. 2 4
      admin-ui/src/app/data/queries/facet-queries.ts
  19. 147 2
      admin-ui/src/app/data/types/gql-generated-types.ts
  20. 2 0
      admin-ui/src/app/shared/shared.module.ts
  21. 9 5
      admin-ui/src/i18n-messages/en.json
  22. 1 0
      admin-ui/src/styles/styles.scss
  23. 6 0
      admin-ui/yarn.lock

+ 1 - 0
admin-ui/package.json

@@ -26,6 +26,7 @@
     "@clr/angular": "^0.12.10",
     "@clr/icons": "^0.12.10",
     "@clr/ui": "^0.12.10",
+    "@ng-select/ng-select": "^2.5.0",
     "@ngx-translate/core": "^10.0.2",
     "@ngx-translate/http-loader": "^3.0.1",
     "@webcomponents/custom-elements": "1.0.0",

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

@@ -4,10 +4,12 @@ import { RouterModule } from '@angular/router';
 import { SharedModule } from '../shared/shared.module';
 
 import { catalogRoutes } from './catalog.routes';
+import { ApplyFacetDialogComponent } from './components/apply-facet-dialog/apply-facet-dialog.component';
 import { CreateOptionGroupDialogComponent } from './components/create-option-group-dialog/create-option-group-dialog.component';
 import { CreateOptionGroupFormComponent } from './components/create-option-group-form/create-option-group-form.component';
 import { FacetDetailComponent } from './components/facet-detail/facet-detail.component';
 import { FacetListComponent } from './components/facet-list/facet-list.component';
+import { FacetValueSelectorComponent } from './components/facet-value-selector/facet-value-selector.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
@@ -33,8 +35,14 @@ import { ProductResolver } from './providers/routing/product-resolver';
         FacetDetailComponent,
         GenerateProductVariantsComponent,
         ProductVariantsListComponent,
+        FacetValueSelectorComponent,
+        ApplyFacetDialogComponent,
+    ],
+    entryComponents: [
+        CreateOptionGroupDialogComponent,
+        SelectOptionGroupDialogComponent,
+        ApplyFacetDialogComponent,
     ],
-    entryComponents: [CreateOptionGroupDialogComponent, SelectOptionGroupDialogComponent],
     providers: [ProductResolver, FacetResolver],
 })
 export class CatalogModule {}

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

@@ -0,0 +1,15 @@
+<ng-template vdrDialogTitle>{{ 'catalog.apply-facets' | translate }}</ng-template>
+
+<vdr-facet-value-selector (selectedValuesChange)="selectedValues = $event"></vdr-facet-value-selector>
+
+<ng-template vdrDialogButtons>
+    <button type="button"
+            class="btn"
+            (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit"
+            (click)="selectValues()"
+            [disabled]="selectedValues.length === 0"
+            class="btn btn-primary">
+        {{ 'catalog.apply-facets' | translate }}
+    </button>
+</ng-template>

+ 0 - 0
admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.scss


+ 24 - 0
admin-ui/src/app/catalog/components/apply-facet-dialog/apply-facet-dialog.component.ts

@@ -0,0 +1,24 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+import { FacetValue, ProductOptionGroup } from '../../../data/types/gql-generated-types';
+import { Dialog } from '../../../shared/providers/modal/modal.service';
+
+@Component({
+    selector: 'vdr-apply-facet-dialog',
+    templateUrl: './apply-facet-dialog.component.html',
+    styleUrls: ['./apply-facet-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ApplyFacetDialogComponent implements Dialog<FacetValue[]> {
+    existingOptionGroups: Array<Partial<ProductOptionGroup>>;
+    resolveWith: (result?: FacetValue[]) => void;
+    selectedValues: FacetValue[] = [];
+
+    selectValues() {
+        this.resolveWith(this.selectedValues);
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+}

+ 4 - 0
admin-ui/src/app/catalog/components/facet-list/facet-list.component.html

@@ -16,11 +16,15 @@
     <vdr-dt-column>{{ 'catalog.ID' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'catalog.code' | translate }}</vdr-dt-column>
     <vdr-dt-column>{{ 'catalog.name' | translate }}</vdr-dt-column>
+    <vdr-dt-column>{{ 'catalog.values' | translate }}</vdr-dt-column>
     <vdr-dt-column></vdr-dt-column>
     <ng-template let-facet="item">
         <td class="left">{{ facet.id }}</td>
         <td class="left">{{ facet.code }}</td>
         <td class="left">{{ facet.name }}</td>
+        <td class="left">
+            <vdr-chip *ngFor="let value of facet.values">{{ value.name }}</vdr-chip>
+        </td>
         <td class="right">
             <vdr-table-row-action iconShape="edit"
                                   [label]="'common.edit' | translate"

+ 10 - 0
admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.html

@@ -0,0 +1,10 @@
+<ng-select [items]="facetValues$ | async"
+           [addTag]="true"
+           [hideSelected]="true"
+           groupBy="facetName"
+           bindValue="value"
+           multiple="true"
+           appendTo="body"
+           bindLabel="name"
+           (change)="onChange($event)">
+</ng-select>

+ 0 - 0
admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.scss


+ 44 - 0
admin-ui/src/app/catalog/components/facet-value-selector/facet-value-selector.component.ts

@@ -0,0 +1,44 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
+import { Observable } from 'rxjs';
+
+import { DataService } from '../../../data/providers/data.service';
+import { FacetValue } from '../../../data/types/gql-generated-types';
+
+export type FacetValueSeletorItem = { name: string; facetName: string; id: string; value: FacetValue };
+
+@Component({
+    selector: 'vdr-facet-value-selector',
+    templateUrl: './facet-value-selector.component.html',
+    styleUrls: ['./facet-value-selector.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class FacetValueSelectorComponent implements OnInit {
+    @Output() selectedValuesChange = new EventEmitter<FacetValue[]>();
+
+    facetValues$: Observable<FacetValueSeletorItem[]>;
+    constructor(private dataService: DataService) {}
+
+    ngOnInit() {
+        this.facetValues$ = this.dataService.facet.getFacets(9999999, 0).mapSingle(result => {
+            return result.facets.items.reduce(
+                (flattened, facet) => {
+                    return flattened.concat(
+                        facet.values.map(value => {
+                            return {
+                                name: value.name,
+                                facetName: facet.name,
+                                id: value.id,
+                                value,
+                            };
+                        }),
+                    );
+                },
+                [] as FacetValueSeletorItem[],
+            );
+        });
+    }
+
+    onChange(selected: FacetValueSeletorItem[]) {
+        this.selectedValuesChange.emit(selected.map(s => s.value));
+    }
+}

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

@@ -61,9 +61,11 @@
             </vdr-form-item>
 
             <vdr-product-variants-list [variants]="variants$ | async"
-                                       [productVariantsFormArray]="productForm.get('variants')">
-                <label>Apply to selected</label>
-
+                                       [productVariantsFormArray]="productForm.get('variants')"
+                                       #productVariantsList>
+                <button class="btn btn-sm btn-secondary" (click)="selectFacetValue(productVariantsList.selectedVariantIds)">
+                    {{ 'catalog.apply-facets' | translate }}
+                </button>
             </vdr-product-variants-list>
         </ng-template>
 

+ 24 - 1
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -1,7 +1,7 @@
 import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { combineLatest, forkJoin, Observable, Subject } from 'rxjs';
+import { combineLatest, EMPTY, forkJoin, Observable, Subject } from 'rxjs';
 import { map, mergeMap, switchMap, take, takeUntil } from 'rxjs/operators';
 
 import { CustomFieldConfig } from '../../../../../../shared/shared-types';
@@ -21,6 +21,7 @@ import {
     UpdateProductVariantInput,
 } from '../../../data/types/gql-generated-types';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
+import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 
 @Component({
     selector: 'vdr-product-detail',
@@ -104,6 +105,28 @@ export class ProductDetailComponent implements OnInit, OnDestroy {
         });
     }
 
+    /**
+     * Opens a dialog to select FacetValues to apply to the select ProductVariants.
+     */
+    selectFacetValue(selectedVariantIds: string[]) {
+        this.modalService
+            .fromComponent(ApplyFacetDialogComponent, { size: 'md' })
+            .pipe(
+                map(facetValues => facetValues && facetValues.map(v => v.id)),
+                mergeMap(facetValueIds => {
+                    if (facetValueIds) {
+                        return this.dataService.product.applyFacetValuesToProductVariants(
+                            facetValueIds,
+                            selectedVariantIds,
+                        );
+                    } else {
+                        return EMPTY;
+                    }
+                }),
+            )
+            .subscribe();
+    }
+
     create() {
         const productGroup = this.productForm.get('product');
         if (!productGroup || !productGroup.dirty) {

+ 12 - 6
admin-ui/src/app/catalog/components/product-variants-list/product-variants-list.component.html

@@ -1,5 +1,7 @@
-<div class="multi-actions" *ngIf="selectedVariantIds.length">
-    <ng-content></ng-content>
+<div class="with-selected">
+    <ng-container *ngIf="selectedVariantIds.length">
+        <label>{{ 'catalog.with-selected' | translate }}:</label><ng-content></ng-content>
+    </ng-container>
 </div>
 <table class="variants-list table">
     <thead>
@@ -9,10 +11,11 @@
                                [selected]="areAllSelected()"
                                (selectedChange)="toggleSelectAll()"></vdr-select-toggle>
         </th>
-        <th>{{ 'catalog.product-variant-table-sku' | translate }}</th>
-        <th>{{ 'catalog.product-variant-table-name' | translate }}</th>
-        <th>{{ 'catalog.product-variant-table-options' | translate }}</th>
-        <th>{{ 'catalog.product-variant-table-price' | translate }}</th>
+        <th>{{ 'catalog.sku' | translate }}</th>
+        <th>{{ 'catalog.name' | translate }}</th>
+        <th>{{ 'catalog.facets' | translate }}</th>
+        <th>{{ 'catalog.options' | translate }}</th>
+        <th>{{ 'catalog.price' | translate }}</th>
     </tr>
     </thead>
     <tbody>
@@ -29,6 +32,9 @@
         <td>
             <input type="text" [formControl]="formArray.get([i, 'name'])">
         </td>
+        <td>
+            <vdr-chip *ngFor="let facetValue of variant.facetValues">{{ facetValue.name }}</vdr-chip>
+        </td>
         <td>
             <vdr-chip *ngFor="let option of variant.options">{{ option.name }}</vdr-chip>
         </td>

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

@@ -0,0 +1,10 @@
+.with-selected {
+    min-height: 40px;
+    > label {
+        margin-right: 12px;
+    }
+}
+
+.table {
+    margin-top: 0;
+}

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

@@ -14,6 +14,11 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
             languageCode
             name
         }
+        facetValues {
+            id
+            code
+            name
+        }
         translations {
             id
             languageCode

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

@@ -86,3 +86,15 @@ export const REMOVE_OPTION_GROUP_FROM_PRODUCT = gql`
         }
     }
 `;
+
+export const APPLY_FACET_VALUE_TO_PRODUCT_VARIANTS = gql`
+    mutation ApplyFacetValuesToProductVariants($facetValueIds: [ID!]!, $productVariantIds: [ID!]!) {
+        applyFacetValuesToProductVariants(
+            facetValueIds: $facetValueIds
+            productVariantIds: $productVariantIds
+        ) {
+            ...ProductVariant
+        }
+    }
+    ${PRODUCT_VARIANT_FRAGMENT}
+`;

+ 1 - 0
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -51,6 +51,7 @@ export class MockDataService implements DataServiceMock {
         removeOptionGroupFromProduct: spyObservable('removeOptionGroupFromProduct'),
         getProductOptionGroups: spyQueryResult('getProductOptionGroups'),
         generateProductVariants: spyObservable('generateProductVariants'),
+        applyFacetValuesToProductVariants: spyObservable('applyFacetValuesToProductVariants'),
     };
     user = {
         checkLoggedIn: spyObservable('checkLoggedIn'),

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

@@ -5,6 +5,7 @@ import { pick } from '../../common/utilities/pick';
 import { addCustomFields } from '../add-custom-fields';
 import {
     ADD_OPTION_GROUP_TO_PRODUCT,
+    APPLY_FACET_VALUE_TO_PRODUCT_VARIANTS,
     CREATE_PRODUCT,
     CREATE_PRODUCT_OPTION_GROUP,
     GENERATE_PRODUCT_VARIANTS,
@@ -20,6 +21,8 @@ import {
 import {
     AddOptionGroupToProduct,
     AddOptionGroupToProductVariables,
+    ApplyFacetValuesToProductVariants,
+    ApplyFacetValuesToProductVariantsVariables,
     CreateProduct,
     CreateProductInput,
     CreateProductOptionGroup,
@@ -152,4 +155,17 @@ export class ProductDataService {
             },
         );
     }
+
+    applyFacetValuesToProductVariants(
+        facetValueIds: string[],
+        productVariantIds: string[],
+    ): Observable<ApplyFacetValuesToProductVariants> {
+        return this.baseDataService.mutate<
+            ApplyFacetValuesToProductVariants,
+            ApplyFacetValuesToProductVariantsVariables
+        >(APPLY_FACET_VALUE_TO_PRODUCT_VARIANTS, {
+            facetValueIds,
+            productVariantIds,
+        });
+    }
 }

+ 2 - 4
admin-ui/src/app/data/queries/facet-queries.ts

@@ -6,14 +6,12 @@ export const GET_FACET_LIST = gql`
     query GetFacetList($options: FacetListOptions, $languageCode: LanguageCode) {
         facets(languageCode: $languageCode, options: $options) {
             items {
-                id
-                languageCode
-                code
-                name
+                ...FacetWithValues
             }
             totalItems
         }
     }
+    ${FACET_WITH_VALUES_FRAGMENT}
 `;
 
 export const GET_FACET_WITH_VALUES = gql`

+ 147 - 2
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -277,6 +277,13 @@ export interface UpdateProduct_updateProduct_variants_options {
   name: string | null;
 }
 
+export interface UpdateProduct_updateProduct_variants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface UpdateProduct_updateProduct_variants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -293,6 +300,7 @@ export interface UpdateProduct_updateProduct_variants {
   sku: string;
   image: string | null;
   options: UpdateProduct_updateProduct_variants_options[];
+  facetValues: UpdateProduct_updateProduct_variants_facetValues[];
   translations: UpdateProduct_updateProduct_variants_translations[];
 }
 
@@ -351,6 +359,13 @@ export interface CreateProduct_createProduct_variants_options {
   name: string | null;
 }
 
+export interface CreateProduct_createProduct_variants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface CreateProduct_createProduct_variants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -367,6 +382,7 @@ export interface CreateProduct_createProduct_variants {
   sku: string;
   image: string | null;
   options: CreateProduct_createProduct_variants_options[];
+  facetValues: CreateProduct_createProduct_variants_facetValues[];
   translations: CreateProduct_createProduct_variants_translations[];
 }
 
@@ -425,6 +441,13 @@ export interface GenerateProductVariants_generateVariantsForProduct_variants_opt
   name: string | null;
 }
 
+export interface GenerateProductVariants_generateVariantsForProduct_variants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface GenerateProductVariants_generateVariantsForProduct_variants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -441,6 +464,7 @@ export interface GenerateProductVariants_generateVariantsForProduct_variants {
   sku: string;
   image: string | null;
   options: GenerateProductVariants_generateVariantsForProduct_variants_options[];
+  facetValues: GenerateProductVariants_generateVariantsForProduct_variants_facetValues[];
   translations: GenerateProductVariants_generateVariantsForProduct_variants_translations[];
 }
 
@@ -485,6 +509,13 @@ export interface UpdateProductVariants_updateProductVariants_options {
   name: string | null;
 }
 
+export interface UpdateProductVariants_updateProductVariants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface UpdateProductVariants_updateProductVariants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -501,6 +532,7 @@ export interface UpdateProductVariants_updateProductVariants {
   sku: string;
   image: string | null;
   options: UpdateProductVariants_updateProductVariants_options[];
+  facetValues: UpdateProductVariants_updateProductVariants_facetValues[];
   translations: UpdateProductVariants_updateProductVariants_translations[];
 }
 
@@ -641,16 +673,95 @@ export interface RemoveOptionGroupFromProductVariables {
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 
+// ====================================================
+// GraphQL mutation operation: ApplyFacetValuesToProductVariants
+// ====================================================
+
+export interface ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_options {
+  __typename: "ProductOption";
+  id: string;
+  code: string | null;
+  languageCode: LanguageCode | null;
+  name: string | null;
+}
+
+export interface ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
+export interface ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_translations {
+  __typename: "ProductVariantTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants {
+  __typename: "ProductVariant";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+  price: number;
+  sku: string;
+  image: string | null;
+  options: ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_options[];
+  facetValues: ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_facetValues[];
+  translations: ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants_translations[];
+}
+
+export interface ApplyFacetValuesToProductVariants {
+  /**
+   * Applies a FacetValue to the given ProductVariants
+   */
+  applyFacetValuesToProductVariants: ApplyFacetValuesToProductVariants_applyFacetValuesToProductVariants[];
+}
+
+export interface ApplyFacetValuesToProductVariantsVariables {
+  facetValueIds: string[];
+  productVariantIds: string[];
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
 // ====================================================
 // GraphQL query operation: GetFacetList
 // ====================================================
 
+export interface GetFacetList_facets_items_translations {
+  __typename: "FacetTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface GetFacetList_facets_items_values_translations {
+  __typename: "FacetValueTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface GetFacetList_facets_items_values {
+  __typename: "FacetValue";
+  id: string;
+  languageCode: LanguageCode | null;
+  code: string;
+  name: string;
+  translations: GetFacetList_facets_items_values_translations[];
+}
+
 export interface GetFacetList_facets_items {
   __typename: "Facet";
   id: string;
   languageCode: LanguageCode;
   code: string;
   name: string;
+  translations: GetFacetList_facets_items_translations[];
+  values: GetFacetList_facets_items_values[];
 }
 
 export interface GetFacetList_facets {
@@ -798,6 +909,13 @@ export interface GetProductWithVariants_product_variants_options {
   name: string | null;
 }
 
+export interface GetProductWithVariants_product_variants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface GetProductWithVariants_product_variants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -814,6 +932,7 @@ export interface GetProductWithVariants_product_variants {
   sku: string;
   image: string | null;
   options: GetProductWithVariants_product_variants_options[];
+  facetValues: GetProductWithVariants_product_variants_facetValues[];
   translations: GetProductWithVariants_product_variants_translations[];
 }
 
@@ -981,6 +1100,13 @@ export interface ProductVariant_options {
   name: string | null;
 }
 
+export interface ProductVariant_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface ProductVariant_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -997,6 +1123,7 @@ export interface ProductVariant {
   sku: string;
   image: string | null;
   options: ProductVariant_options[];
+  facetValues: ProductVariant_facetValues[];
   translations: ProductVariant_translations[];
 }
 
@@ -1031,6 +1158,13 @@ export interface ProductWithVariants_variants_options {
   name: string | null;
 }
 
+export interface ProductWithVariants_variants_facetValues {
+  __typename: "FacetValue";
+  id: string;
+  code: string;
+  name: string;
+}
+
 export interface ProductWithVariants_variants_translations {
   __typename: "ProductVariantTranslation";
   id: string;
@@ -1047,6 +1181,7 @@ export interface ProductWithVariants_variants {
   sku: string;
   image: string | null;
   options: ProductWithVariants_variants_options[];
+  facetValues: ProductWithVariants_variants_facetValues[];
   translations: ProductWithVariants_variants_translations[];
 }
 
@@ -1316,11 +1451,16 @@ export interface CreateFacetInput {
   customFields?: CreateFacetCustomFieldsInput | null;
 }
 
+export interface CreateFacetValueCustomFieldsInput {
+  link?: string | null;
+  available?: boolean | null;
+}
+
 export interface CreateFacetValueInput {
   facetId: string;
   code: string;
   translations: FacetValueTranslationInput[];
-  customFields?: any | null;
+  customFields?: CreateFacetValueCustomFieldsInput | null;
 }
 
 export interface CreateProductCustomFieldsInput {
@@ -1472,11 +1612,16 @@ export interface UpdateFacetInput {
   customFields?: UpdateFacetCustomFieldsInput | null;
 }
 
+export interface UpdateFacetValueCustomFieldsInput {
+  link?: string | null;
+  available?: boolean | null;
+}
+
 export interface UpdateFacetValueInput {
   id: string;
   code: string;
   translations: FacetValueTranslationInput[];
-  customFields?: any | null;
+  customFields?: UpdateFacetValueCustomFieldsInput | null;
 }
 
 export interface UpdateProductCustomFieldsInput {

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

@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 import { ClarityModule } from '@clr/angular';
+import { NgSelectModule } from '@ng-select/ng-select';
 import { TranslateModule } from '@ngx-translate/core';
 import { NgxPaginationModule } from 'ngx-pagination';
 
@@ -35,6 +36,7 @@ const IMPORTS = [
     FormsModule,
     ReactiveFormsModule,
     RouterModule,
+    NgSelectModule,
     NgxPaginationModule,
     TranslateModule,
 ];

+ 9 - 5
admin-ui/src/i18n-messages/en.json

@@ -7,6 +7,7 @@
   "catalog": {
     "ID": "ID",
     "add-facet-value": "Add facet value",
+    "apply-facets": "Apply facets",
     "code": "Code",
     "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.",
     "create-group": "Create option group",
@@ -16,6 +17,7 @@
     "description": "Description",
     "facet": "Facet",
     "facet-values": "Facet values",
+    "facets": "Facets",
     "filter-by-group-name": "Filter by group name",
     "generate-product-variants": "Generate product variants",
     "generate-variants-default-only": "This product does not have options",
@@ -34,18 +36,19 @@
     "option-group-name": "Option group name",
     "option-group-options-label": "Options",
     "option-group-options-tooltip": "Enter each option on a new line in the default language ({ defaultLanguage })",
+    "options": "Options",
+    "price": "Price",
     "product": "Product",
     "product-name": "Product name",
     "product-option-groups": "Option groups",
-    "product-variant-table-name": "Variant name",
-    "product-variant-table-options": "Options",
-    "product-variant-table-price": "Price",
-    "product-variant-table-sku": "SKU",
     "product-variants": "Product variants",
     "select-option-group": "Select option group",
     "selected-option-groups": "Selected option groups",
+    "sku": "SKU",
     "slug": "Slug",
-    "truncated-options-count": "{count} further {count, plural, one {option} other {options}}"
+    "truncated-options-count": "{count} further {count, plural, one {option} other {options}}",
+    "values": "Values",
+    "with-selected": "With selected"
   },
   "common": {
     "back": "Back",
@@ -65,6 +68,7 @@
     "username": "Username"
   },
   "error": {
+    "facet-value-form-values-do-not-match": "The number of values in the facet form does not match the actual number of values",
     "product-variant-form-values-do-not-match": "The number of variants in the product form does not match the actual number of variants"
   },
   "nav": {

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

@@ -1,5 +1,6 @@
 /* You can add global styles to this file, and also import other style files */
 @import "variables";
+@import "~@ng-select/ng-select/themes/default.theme.css";
 
 a:link, a:visited {
     color: darken($color-brand, 20%);

+ 6 - 0
admin-ui/yarn.lock

@@ -259,6 +259,12 @@
     call-me-maybe "^1.0.1"
     glob-to-regexp "^0.3.0"
 
+"@ng-select/ng-select@^2.5.0":
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/@ng-select/ng-select/-/ng-select-2.5.0.tgz#8a8e2ce67c1c1f51bfb6c59af885b1cfafbc65aa"
+  dependencies:
+    tslib "^1.9.0"
+
 "@ngtools/webpack@6.1.5":
   version "6.1.5"
   resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-6.1.5.tgz#134639686eb4519885ed27665babdf80ce098263"