Ver código fonte

perf(admin-ui): Lazy-load facet values for selector component

Relates to #1404
Michael Bromley 3 anos atrás
pai
commit
33506081f5
21 arquivos alterados com 242 adições e 293 exclusões
  1. 0 1
      packages/admin-ui/src/lib/catalog/src/components/apply-facet-dialog/apply-facet-dialog.component.html
  2. 1 3
      packages/admin-ui/src/lib/catalog/src/components/apply-facet-dialog/apply-facet-dialog.component.ts
  3. 0 1
      packages/admin-ui/src/lib/catalog/src/components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component.html
  4. 0 1
      packages/admin-ui/src/lib/catalog/src/components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component.ts
  5. 1 2
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  6. 26 65
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  7. 8 15
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list-bulk-actions.ts
  8. 2 8
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html
  9. 38 36
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts
  10. 0 4
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  11. 63 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  12. 1 0
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  13. 0 89
      packages/admin-ui/src/lib/core/src/common/utilities/flatten-facet-values.spec.ts
  14. 0 8
      packages/admin-ui/src/lib/core/src/common/utilities/flatten-facet-values.ts
  15. 12 0
      packages/admin-ui/src/lib/core/src/data/definitions/facet-definitions.ts
  16. 14 2
      packages/admin-ui/src/lib/core/src/data/providers/facet-data.service.ts
  17. 0 1
      packages/admin-ui/src/lib/core/src/public_api.ts
  18. 6 5
      packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.html
  19. 66 33
      packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.ts
  20. 0 2
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.html
  21. 4 17
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.ts

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/apply-facet-dialog/apply-facet-dialog.component.html

@@ -1,7 +1,6 @@
 <ng-template vdrDialogTitle>{{ 'catalog.add-facets' | translate }}</ng-template>
 
 <vdr-facet-value-selector
-    [facets]="facets"
     (selectedValuesChange)="selectedValues = $event"
 ></vdr-facet-value-selector>
 

+ 1 - 3
packages/admin-ui/src/lib/catalog/src/components/apply-facet-dialog/apply-facet-dialog.component.ts

@@ -5,7 +5,7 @@ import {
     Component,
     ViewChild,
 } from '@angular/core';
-import { Dialog, FacetValue, FacetValueSelectorComponent, FacetWithValues } from '@vendure/admin-ui/core';
+import { Dialog, FacetValue, FacetValueSelectorComponent } from '@vendure/admin-ui/core';
 
 @Component({
     selector: 'vdr-apply-facet-dialog',
@@ -17,8 +17,6 @@ export class ApplyFacetDialogComponent implements Dialog<FacetValue[]>, AfterVie
     @ViewChild(FacetValueSelectorComponent) private selector: FacetValueSelectorComponent;
     resolveWith: (result?: FacetValue[]) => void;
     selectedValues: FacetValue[] = [];
-    // Provided by caller
-    facets: FacetWithValues.Fragment[];
 
     constructor(private changeDetector: ChangeDetectorRef) {}
 

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component.html

@@ -8,7 +8,6 @@
             {{ 'catalog.add-facet-value' | translate }}
         </div>
         <vdr-facet-value-selector
-            [facets]="facets"
             (selectedValuesChange)="selectedValues = $event"
         ></vdr-facet-value-selector>
     </div>

+ 0 - 1
packages/admin-ui/src/lib/catalog/src/components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component.ts

@@ -51,7 +51,6 @@ export class BulkAddFacetValuesDialogComponent
     /* provided by call to ModalService */
     mode: 'product' | 'variant' = 'product';
     ids?: string[];
-    facets: FacetWithValuesFragment[] = [];
     state: 'loading' | 'ready' | 'saving' = 'loading';
 
     selectedValues: FacetWithValuesFragment[] = [];

+ 1 - 2
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html

@@ -251,7 +251,7 @@
                         [variants]="variants$ | async"
                         [paginationConfig]="paginationConfig$ | async"
                         [channelPriceIncludesTax]="channelPriceIncludesTax$ | async"
-                        [facets]="facets$ | async"
+                        [pendingFacetValueChanges]="variantFacetValueChanges"
                         [optionGroups]="product.optionGroups"
                         [productVariantsFormArray]="detailForm.get('variants')"
                         [taxCategories]="taxCategories$ | async"
@@ -264,7 +264,6 @@
                         (assetChange)="variantAssetChange($event)"
                         (updateProductOption)="updateProductOption($event)"
                         (selectionChange)="selectedVariantIds = $event"
-                        (selectFacetValueClick)="selectVariantFacetValue($event)"
                     ></vdr-product-variants-list>
                 </section>
                 <div class="pagination-row mt4" *ngIf="10 < (paginationConfig$ | async)?.totalItems">

+ 26 - 65
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -10,9 +10,8 @@ import {
     createUpdatedTranslatable,
     CustomFieldConfig,
     DataService,
-    FacetWithValues,
+    FacetValueFragment,
     findTranslation,
-    flattenFacetValues,
     getChannelCodeFromUserStatus,
     GetProductWithVariants,
     GlobalFlag,
@@ -23,6 +22,7 @@ import {
     Permission,
     ProductDetail,
     ProductVariant,
+    ProductVariantFragment,
     ServerConfigService,
     TaxCategory,
     unicodePatternValidator,
@@ -36,10 +36,11 @@ import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { unique } from '@vendure/common/lib/unique';
-import { BehaviorSubject, combineLatest, EMPTY, from, merge, Observable } from 'rxjs';
+import { BehaviorSubject, combineLatest, concat, EMPTY, from, merge, Observable } from 'rxjs';
 import {
     debounceTime,
     distinctUntilChanged,
+    filter,
     map,
     mergeMap,
     shareReplay,
@@ -47,6 +48,7 @@ import {
     skipUntil,
     startWith,
     switchMap,
+    switchMapTo,
     take,
     takeUntil,
     tap,
@@ -110,10 +112,9 @@ export class ProductDetailComponent
     filterInput = new FormControl('');
     assetChanges: SelectedAssets = {};
     variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
-    variantFacetValueChanges: { [variantId: string]: string[] } = {};
+    variantFacetValueChanges: { [variantId: string]: ProductVariantFragment['facetValues'] } = {};
     productChannels$: Observable<ProductDetail.Channels[]>;
     facetValues$: Observable<ProductDetail.FacetValues[]>;
-    facets$: Observable<FacetWithValues.Fragment[]>;
     totalItems$: Observable<number>;
     currentPage$ = new BehaviorSubject(1);
     itemsPerPage$ = new BehaviorSubject(10);
@@ -214,31 +215,23 @@ export class ProductDetailComponent
                 this.buildVariantFormArray(variants, languageCode);
             });
 
-        // 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();
-
-        const formFacetValueIdChanges$ = productGroup.valueChanges.pipe(
-            map(val => val.facetValueIds as string[]),
+        // tslint:disable-next-line:no-non-null-assertion
+        const formFacetValueIdChanges$ = productGroup.get('facetValueIds')!.valueChanges.pipe(
+            skip(1),
             distinctUntilChanged(),
+            switchMap(ids =>
+                this.dataService.facet
+                    .getFacetValues({ filter: { id: { in: ids } } })
+                    .mapSingle(({ facetValues }) => facetValues.items),
+            ),
+            shareReplay(1),
         );
-        const formChangeFacetValues$ = combineLatest(
-            formFacetValueIdChanges$,
-            productFacetValues$,
-            allFacetValues$,
-        ).pipe(
-            map(([ids, productFacetValues, allFacetValues]) => {
-                const combined = [...productFacetValues, ...allFacetValues];
-                return ids.map(id => combined.find(fv => fv.id === id)).filter(notNullOrUndefined);
-            }),
+        this.facetValues$ = concat(
+            productFacetValues$.pipe(take(1)),
+            productFacetValues$.pipe(switchMapTo(formFacetValueIdChanges$)),
         );
-
-        this.facetValues$ = merge(productFacetValues$, formChangeFacetValues$);
         this.productChannels$ = this.product$.pipe(map(p => p.channels));
         this.channelPriceIncludesTax$ = this.dataService.settings
             .getActiveChannel('cache-first')
@@ -455,35 +448,6 @@ export class ProductDetailComponent
         productGroup.markAsDirty();
     }
 
-    /**
-     * Opens a dialog to select FacetValues to apply to the select ProductVariants.
-     */
-    selectVariantFacetValue(selectedVariantIds: string[]) {
-        this.displayFacetValueModal()
-            .pipe(withLatestFrom(this.variants$))
-            .subscribe(([facetValueIds, variants]) => {
-                if (facetValueIds) {
-                    for (const variantId of selectedVariantIds) {
-                        const index = variants.findIndex(v => v.id === variantId);
-                        const variant = variants[index];
-                        const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
-                        const variantFormGroup = (this.detailForm.get('variants') as FormArray).controls.find(
-                            c => c.value.id === variantId,
-                        );
-                        if (variantFormGroup) {
-                            const uniqueFacetValueIds = unique([...existingFacetValueIds, ...facetValueIds]);
-                            variantFormGroup.patchValue({
-                                facetValueIds: uniqueFacetValueIds,
-                            });
-                            variantFormGroup.markAsDirty();
-                            this.variantFacetValueChanges[variantId] = uniqueFacetValueIds;
-                        }
-                    }
-                    this.changeDetector.markForCheck();
-                }
-            });
-    }
-
     variantsToCreateAreValid(): boolean {
         return (
             0 < this.createVariantsConfig.variants.length &&
@@ -494,16 +458,12 @@ export class ProductDetailComponent
     }
 
     private displayFacetValueModal(): Observable<string[] | undefined> {
-        return this.productDetailService.getFacets().pipe(
-            mergeMap(facets =>
-                this.modalService.fromComponent(ApplyFacetDialogComponent, {
-                    size: 'md',
-                    closable: true,
-                    locals: { facets },
-                }),
-            ),
-            map(facetValues => facetValues && facetValues.map(v => v.id)),
-        );
+        return this.modalService
+            .fromComponent(ApplyFacetDialogComponent, {
+                size: 'md',
+                closable: true,
+            })
+            .pipe(map(facetValues => facetValues && facetValues.map(v => v.id)));
     }
 
     create() {
@@ -589,6 +549,7 @@ export class ProductDetailComponent
                     this.detailForm.markAsPristine();
                     this.assetChanges = {};
                     this.variantAssetChanges = {};
+                    this.variantFacetValueChanges = {};
                     this.notificationService.success(_('common.notify-update-success'), {
                         entity: 'Product',
                     });
@@ -638,7 +599,7 @@ export class ProductDetailComponent
             const variantTranslation = findTranslation(variant, languageCode);
             const pendingFacetValueChanges = this.variantFacetValueChanges[variant.id];
             const facetValueIds = pendingFacetValueChanges
-                ? pendingFacetValueChanges
+                ? pendingFacetValueChanges.map(fv => fv.id)
                 : variant.facetValues.map(fv => fv.id);
             const group: VariantFormValue = {
                 id: variant.id,

+ 8 - 15
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list-bulk-actions.ts

@@ -182,21 +182,14 @@ export const assignFacetValuesToProductsBulkAction: BulkAction<SearchProducts.It
             mode === 'product'
                 ? unique(selection.map(p => p.productId))
                 : unique(selection.map(p => p.productVariantId));
-        return dataService.facet
-            .getAllFacets()
-            .mapSingle(data => data.facets.items)
-            .pipe(
-                switchMap(facets =>
-                    modalService.fromComponent(BulkAddFacetValuesDialogComponent, {
-                        size: 'xl',
-                        locals: {
-                            facets,
-                            mode,
-                            ids,
-                        },
-                    }),
-                ),
-            )
+        return modalService
+            .fromComponent(BulkAddFacetValuesDialogComponent, {
+                size: 'xl',
+                locals: {
+                    mode,
+                    ids,
+                },
+            })
             .subscribe(result => {
                 if (result) {
                     notificationService.success(_('common.notify-bulk-update-success'), {

+ 2 - 8
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html

@@ -252,13 +252,7 @@
                     <div class="flex-spacer"></div>
                     <div class="facets">
                         <vdr-facet-value-chip
-                            *ngFor="let facetValue of existingFacetValues(variant)"
-                            [facetValue]="facetValue"
-                            [removable]="updatePermission | hasPermission"
-                            (remove)="removeFacetValue(variant, facetValue.id)"
-                        ></vdr-facet-value-chip>
-                        <vdr-facet-value-chip
-                            *ngFor="let facetValue of pendingFacetValues(variant)"
+                            *ngFor="let facetValue of currentOrPendingFacetValues(variant)"
                             [facetValue]="facetValue"
                             [removable]="updatePermission | hasPermission"
                             (remove)="removeFacetValue(variant, facetValue.id)"
@@ -266,7 +260,7 @@
                         <button
                             *vdrIfPermissions="updatePermission"
                             class="btn btn-sm btn-secondary"
-                            (click)="selectFacetValueClick.emit([variant.id])"
+                            (click)="selectFacetValue(variant)"
                         >
                             <clr-icon shape="plus"></clr-icon>
                             {{ 'catalog.add-facets' | translate }}

+ 38 - 36
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts

@@ -4,19 +4,14 @@ import {
     Component,
     EventEmitter,
     Input,
-    OnChanges,
     OnDestroy,
     OnInit,
     Output,
-    SimpleChanges,
 } from '@angular/core';
 import { FormArray, FormGroup } from '@angular/forms';
 import {
     CustomFieldConfig,
     DataService,
-    FacetValue,
-    FacetWithValues,
-    flattenFacetValues,
     GlobalFlag,
     LanguageCode,
     ModalService,
@@ -24,15 +19,16 @@ import {
     ProductDetail,
     ProductOptionFragment,
     ProductVariant,
+    ProductVariantFragment,
     TaxCategory,
     UpdateProductOptionInput,
 } from '@vendure/admin-ui/core';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
-import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
-import { PaginationInstance } from 'ngx-pagination';
+import { unique } from '@vendure/common/lib/unique';
 import { Subscription } from 'rxjs';
 import { debounceTime, distinctUntilChanged, map } from 'rxjs/operators';
 
+import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { AssetChange } from '../assets/assets.component';
 import {
     PaginationConfig,
@@ -51,18 +47,18 @@ export interface VariantAssetChange extends AssetChange {
     styleUrls: ['./product-variants-list.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestroy {
+export class ProductVariantsListComponent implements OnInit, OnDestroy {
     @Input('productVariantsFormArray') formArray: FormArray;
     @Input() variants: ProductVariant.Fragment[];
     @Input() paginationConfig: PaginationConfig;
     @Input() channelPriceIncludesTax: boolean;
     @Input() taxCategories: TaxCategory[];
-    @Input() facets: FacetWithValues.Fragment[];
     @Input() optionGroups: ProductDetail.OptionGroups[];
     @Input() customFields: CustomFieldConfig[];
     @Input() customOptionFields: CustomFieldConfig[];
     @Input() activeLanguage: LanguageCode;
     @Input() pendingAssetChanges: { [variantId: string]: SelectedAssets };
+    @Input() pendingFacetValueChanges: { [variantId: string]: ProductVariantFragment['facetValues'] };
     @Output() assignToChannel = new EventEmitter<ProductVariant.Fragment>();
     @Output() removeFromChannel = new EventEmitter<{
         channelId: string;
@@ -70,7 +66,6 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     }>();
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() selectionChange = new EventEmitter<string[]>();
-    @Output() selectFacetValueClick = new EventEmitter<string[]>();
     @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput & { autoUpdate: boolean }>();
     selectedVariantIds: string[] = [];
     formGroupMap = new Map<string, FormGroup>();
@@ -78,7 +73,6 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     globalTrackInventory: boolean;
     globalOutOfStockThreshold: number;
     readonly updatePermission = [Permission.UpdateCatalog, Permission.UpdateProduct];
-    private facetValues: FacetValue.Fragment[];
     private subscription: Subscription;
 
     constructor(
@@ -110,12 +104,6 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         this.buildFormGroupMap();
     }
 
-    ngOnChanges(changes: SimpleChanges) {
-        if ('facets' in changes && !!changes['facets'].currentValue) {
-            this.facetValues = flattenFacetValues(this.facets);
-        }
-    }
-
     ngOnDestroy() {
         if (this.subscription) {
             this.subscription.unsubscribe();
@@ -209,27 +197,35 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         return translation.name;
     }
 
-    pendingFacetValues(variant: ProductVariant.Fragment) {
-        if (this.facets) {
-            const formFacetValueIds = this.getFacetValueIds(variant.id);
-            const variantFacetValueIds = variant.facetValues.map(fv => fv.id);
-            return formFacetValueIds
-                .filter(x => !variantFacetValueIds.includes(x))
-                .map(id => this.facetValues.find(fv => fv.id === id))
-                .filter(notNullOrUndefined);
-        } else {
-            return [];
-        }
+    currentOrPendingFacetValues(variant: ProductVariant.Fragment) {
+        return this.pendingFacetValueChanges[variant.id] ?? variant.facetValues;
     }
 
-    existingFacetValues(variant: ProductVariant.Fragment) {
-        const formFacetValueIds = this.getFacetValueIds(variant.id);
-        const intersection = [...formFacetValueIds].filter(x =>
-            variant.facetValues.map(fv => fv.id).includes(x),
-        );
-        return intersection
-            .map(id => variant.facetValues.find(fv => fv.id === id))
-            .filter(notNullOrUndefined);
+    selectFacetValue(variant: ProductVariantFragment) {
+        return this.modalService
+            .fromComponent(ApplyFacetDialogComponent, {
+                size: 'md',
+                closable: true,
+            })
+            .subscribe(facetValues => {
+                if (facetValues) {
+                    const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
+                    const variantFormGroup = this.formArray.controls.find(c => c.value.id === variant.id);
+                    if (variantFormGroup) {
+                        const uniqueFacetValueIds = unique([
+                            ...existingFacetValueIds,
+                            ...facetValues.map(fv => fv.id),
+                        ]);
+                        variantFormGroup.patchValue({ facetValueIds: uniqueFacetValueIds });
+                        variantFormGroup.markAsDirty();
+                        if (!this.pendingFacetValueChanges[variant.id]) {
+                            this.pendingFacetValueChanges[variant.id] = variant.facetValues.slice(0);
+                        }
+                        this.pendingFacetValueChanges[variant.id].push(...facetValues);
+                    }
+                    this.changeDetector.markForCheck();
+                }
+            });
     }
 
     removeFacetValue(variant: ProductVariant.Fragment, facetValueId: string) {
@@ -242,6 +238,12 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
                 facetValueIds: newValue,
             });
             formGroup.markAsDirty();
+            if (!this.pendingFacetValueChanges[variant.id]) {
+                this.pendingFacetValueChanges[variant.id] = variant.facetValues.slice(0);
+            }
+            this.pendingFacetValueChanges[variant.id] = this.pendingFacetValueChanges[variant.id].filter(
+                fv => fv.id !== facetValueId,
+            );
         }
     }
 

+ 0 - 4
packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -33,10 +33,6 @@ import { replaceLast } from './replace-last';
 export class ProductDetailService {
     constructor(private dataService: DataService) {}
 
-    getFacets(): Observable<FacetWithValues.Fragment[]> {
-        return this.dataService.facet.getAllFacets().mapSingle(data => data.facets.items);
-    }
-
     getTaxCategories() {
         return this.dataService.settings
             .getTaxCategories()

+ 63 - 0
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -1594,6 +1594,34 @@ export type FacetValueFilterInput = {
   or?: Maybe<Array<Scalars['ID']>>;
 };
 
+export type FacetValueFilterParameter = {
+  id?: Maybe<IdOperators>;
+  createdAt?: Maybe<DateOperators>;
+  updatedAt?: Maybe<DateOperators>;
+  languageCode?: Maybe<StringOperators>;
+  name?: Maybe<StringOperators>;
+  code?: Maybe<StringOperators>;
+};
+
+export type FacetValueList = PaginatedList & {
+  __typename?: 'FacetValueList';
+  items: Array<FacetValue>;
+  totalItems: Scalars['Int'];
+};
+
+export type FacetValueListOptions = {
+  /** Skips the first n results, for use in pagination */
+  skip?: Maybe<Scalars['Int']>;
+  /** Takes n results, for use in pagination */
+  take?: Maybe<Scalars['Int']>;
+  /** Specifies which properties to sort the results by */
+  sort?: Maybe<FacetValueSortParameter>;
+  /** Allows the results to be filtered */
+  filter?: Maybe<FacetValueFilterParameter>;
+  /** Specifies whether multiple "filter" arguments should be combines with a logical AND or OR operation. Defaults to AND. */
+  filterOperator?: Maybe<LogicalOperator>;
+};
+
 /**
  * Which FacetValues are present in the products returned
  * by the search, and in what quantity.
@@ -1604,6 +1632,14 @@ export type FacetValueResult = {
   count: Scalars['Int'];
 };
 
+export type FacetValueSortParameter = {
+  id?: Maybe<SortOrder>;
+  createdAt?: Maybe<SortOrder>;
+  updatedAt?: Maybe<SortOrder>;
+  name?: Maybe<SortOrder>;
+  code?: Maybe<SortOrder>;
+};
+
 export type FacetValueTranslation = {
   __typename?: 'FacetValueTranslation';
   id: Scalars['ID'];
@@ -4400,6 +4436,7 @@ export type Query = {
   /** Returns a list of eligible shipping methods for the draft Order */
   eligibleShippingMethodsForDraftOrder: Array<ShippingMethodQuote>;
   facet?: Maybe<Facet>;
+  facetValues: FacetValueList;
   facets: FacetList;
   fulfillmentHandlers: Array<ConfigurableOperationDefinition>;
   globalSettings: GlobalSettings;
@@ -4531,6 +4568,11 @@ export type QueryFacetArgs = {
 };
 
 
+export type QueryFacetValuesArgs = {
+  options?: Maybe<FacetValueListOptions>;
+};
+
+
 export type QueryFacetsArgs = {
   options?: Maybe<FacetListOptions>;
 };
@@ -6614,6 +6656,20 @@ export type GetFacetListQuery = { facets: (
     )> }
   ) };
 
+export type GetFacetValueListQueryVariables = Exact<{
+  options?: Maybe<FacetValueListOptions>;
+}>;
+
+
+export type GetFacetValueListQuery = { facetValues: (
+    { __typename?: 'FacetValueList' }
+    & Pick<FacetValueList, 'totalItems'>
+    & { items: Array<(
+      { __typename?: 'FacetValue' }
+      & FacetValueFragment
+    )> }
+  ) };
+
 export type GetFacetWithValuesQueryVariables = Exact<{
   id: Scalars['ID'];
 }>;
@@ -10354,6 +10410,13 @@ export namespace GetFacetList {
   export type Items = NonNullable<(NonNullable<(NonNullable<GetFacetListQuery['facets']>)['items']>)[number]>;
 }
 
+export namespace GetFacetValueList {
+  export type Variables = GetFacetValueListQueryVariables;
+  export type Query = GetFacetValueListQuery;
+  export type FacetValues = (NonNullable<GetFacetValueListQuery['facetValues']>);
+  export type Items = NonNullable<(NonNullable<(NonNullable<GetFacetValueListQuery['facetValues']>)['items']>)[number]>;
+}
+
 export namespace GetFacetWithValues {
   export type Variables = GetFacetWithValuesQueryVariables;
   export type Query = GetFacetWithValuesQuery;

+ 1 - 0
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -190,6 +190,7 @@
       "CustomerGroupList",
       "CustomerList",
       "FacetList",
+      "FacetValueList",
       "HistoryEntryList",
       "JobList",
       "OrderList",

+ 0 - 89
packages/admin-ui/src/lib/core/src/common/utilities/flatten-facet-values.spec.ts

@@ -1,89 +0,0 @@
-import { isPrivate } from '@babel/types';
-
-import { FacetWithValues, LanguageCode } from '../generated-types';
-
-import { flattenFacetValues } from './flatten-facet-values';
-
-describe('flattenFacetValues()', () => {
-    it('works', () => {
-        const facetValue1 = {
-            id: '1',
-            createdAt: '',
-            updatedAt: '',
-            languageCode: LanguageCode.en,
-            code: 'Balistreri,_Lesch_and_Crooks',
-            name: 'Balistreri, Lesch and Crooks',
-            translations: [],
-            facet: {} as any,
-        };
-        const facetValue2 = {
-            id: '2',
-            createdAt: '',
-            updatedAt: '',
-            languageCode: LanguageCode.en,
-            code: 'Rodriguez_-_Von',
-            name: 'Rodriguez - Von',
-            translations: [],
-            facet: {} as any,
-        };
-        const facetValue3 = {
-            id: '3',
-            createdAt: '',
-            updatedAt: '',
-            languageCode: LanguageCode.en,
-            code: 'Hahn_and_Sons',
-            name: 'Hahn and Sons',
-            translations: [],
-            facet: {} as any,
-        };
-        const facetValue4 = {
-            id: '4',
-            createdAt: '',
-            updatedAt: '',
-            languageCode: LanguageCode.en,
-            code: 'Balistreri,_Lesch_and_Crooks',
-            name: 'Balistreri, Lesch and Crooks',
-            translations: [],
-            facet: {} as any,
-        };
-        const facetValue5 = {
-            id: '5',
-            createdAt: '',
-            updatedAt: '',
-            languageCode: LanguageCode.en,
-            code: 'Rodriguez_-_Von',
-            name: 'Rodriguez - Von',
-            translations: [],
-            facet: {} as any,
-        };
-
-        const input: FacetWithValues.Fragment[] = [
-            {
-                id: '1',
-                createdAt: '',
-                updatedAt: '',
-                isPrivate: false,
-                languageCode: LanguageCode.en,
-                code: 'brand',
-                name: 'Brand',
-                translations: [],
-                values: [facetValue1, facetValue2, facetValue3],
-            },
-            {
-                id: '2',
-                createdAt: '',
-                updatedAt: '',
-                isPrivate: false,
-                languageCode: LanguageCode.en,
-                code: 'type',
-                name: 'Type',
-                translations: [],
-                values: [facetValue4, facetValue5],
-            },
-        ];
-
-        const result = flattenFacetValues(input);
-
-        expect(result).toEqual([facetValue1, facetValue2, facetValue3, facetValue4, facetValue5]);
-    });
-});

+ 0 - 8
packages/admin-ui/src/lib/core/src/common/utilities/flatten-facet-values.ts

@@ -1,8 +0,0 @@
-import { FacetValue, FacetWithValues } from '../generated-types';
-
-export function flattenFacetValues(facetsWithValues: FacetWithValues.Fragment[]): FacetValue.Fragment[] {
-    return facetsWithValues.reduce(
-        (flattened, facet) => flattened.concat(facet.values),
-        [] as FacetValue.Fragment[],
-    );
-}

+ 12 - 0
packages/admin-ui/src/lib/core/src/data/definitions/facet-definitions.ts

@@ -118,6 +118,18 @@ export const GET_FACET_LIST = gql`
     ${FACET_WITH_VALUES_FRAGMENT}
 `;
 
+export const GET_FACET_VALUE_LIST = gql`
+    query GetFacetValueList($options: FacetValueListOptions) {
+        facetValues(options: $options) {
+            items {
+                ...FacetValue
+            }
+            totalItems
+        }
+    }
+    ${FACET_VALUE_FRAGMENT}
+`;
+
 export const GET_FACET_WITH_VALUES = gql`
     query GetFacetWithValues($id: ID!) {
         facet(id: $id) {

+ 14 - 2
packages/admin-ui/src/lib/core/src/data/providers/facet-data.service.ts

@@ -1,9 +1,9 @@
+import { WatchQueryFetchPolicy } from '@apollo/client/core';
 import { pick } from '@vendure/common/lib/pick';
 
 import {
     AssignFacetsToChannel,
     AssignFacetsToChannelInput,
-    AssignProductsToChannelInput,
     CreateFacet,
     CreateFacetInput,
     CreateFacetValueInput,
@@ -11,7 +11,10 @@ import {
     DeleteFacet,
     DeleteFacets,
     DeleteFacetValues,
+    FacetValueListOptions,
     GetFacetList,
+    GetFacetValueListQuery,
+    GetFacetValueListQueryVariables,
     GetFacetWithValues,
     RemoveFacetsFromChannel,
     RemoveFacetsFromChannelInput,
@@ -25,9 +28,10 @@ import {
     CREATE_FACET,
     CREATE_FACET_VALUES,
     DELETE_FACET,
-    DELETE_FACET_VALUES,
     DELETE_FACETS,
+    DELETE_FACET_VALUES,
     GET_FACET_LIST,
+    GET_FACET_VALUE_LIST,
     GET_FACET_WITH_VALUES,
     REMOVE_FACETS_FROM_CHANNEL,
     UPDATE_FACET,
@@ -48,6 +52,14 @@ export class FacetDataService {
         });
     }
 
+    getFacetValues(options: FacetValueListOptions, fetchPolicy?: WatchQueryFetchPolicy) {
+        return this.baseDataService.query<GetFacetValueListQuery, GetFacetValueListQueryVariables>(
+            GET_FACET_VALUE_LIST,
+            { options },
+            fetchPolicy,
+        );
+    }
+
     getAllFacets() {
         return this.baseDataService.query<GetFacetList.Query, GetFacetList.Variables>(GET_FACET_LIST, {});
     }

+ 0 - 1
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -16,7 +16,6 @@ export * from './common/utilities/bulk-action-utils';
 export * from './common/utilities/configurable-operation-utils';
 export * from './common/utilities/create-updated-translatable';
 export * from './common/utilities/find-translation';
-export * from './common/utilities/flatten-facet-values';
 export * from './common/utilities/get-default-ui-language';
 export * from './common/utilities/interpolate-description';
 export * from './common/utilities/selection-manager';

+ 6 - 5
packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.html

@@ -1,8 +1,9 @@
 <ng-select
-    [items]="facetValues"
+    [items]="searchResults$ | async"
     [addTag]="false"
     [hideSelected]="true"
-    bindValue="id"
+    [loading]="searchLoading"
+    [typeahead]="searchInput$"
     multiple="true"
     appendTo="body"
     bindLabel="name"
@@ -12,8 +13,8 @@
 >
     <ng-template ng-label-tmp let-item="item" let-clear="clear">
         <vdr-facet-value-chip
-            *ngIf="item.value; else facetNotFound"
-            [facetValue]="item.value"
+            *ngIf="item; else facetNotFound"
+            [facetValue]="item"
             [removable]="!readonly"
             (remove)="clear(item)"
         ></vdr-facet-value-chip>
@@ -24,6 +25,6 @@
         </ng-template>
     </ng-template>
     <ng-template ng-option-tmp let-item="item">
-        <vdr-facet-value-chip [facetValue]="item.value" [removable]="false"></vdr-facet-value-chip>
+        <vdr-facet-value-chip [facetValue]="item" [removable]="false"></vdr-facet-value-chip>
     </ng-template>
 </ng-select>

+ 66 - 33
packages/admin-ui/src/lib/core/src/shared/components/facet-value-selector/facet-value-selector.component.ts

@@ -1,26 +1,22 @@
 import {
     ChangeDetectionStrategy,
+    ChangeDetectorRef,
     Component,
     EventEmitter,
     Input,
+    OnDestroy,
     OnInit,
     Output,
     ViewChild,
 } from '@angular/core';
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 import { NgSelectComponent } from '@ng-select/ng-select';
+import { concat, merge, Observable, of, Subject, Subscription } from 'rxjs';
+import { debounceTime, distinctUntilChanged, mapTo, switchMap, tap } from 'rxjs/operators';
 
-import { FacetValue, FacetWithValues } from '../../../common/generated-types';
-import { flattenFacetValues } from '../../../common/utilities/flatten-facet-values';
+import { FacetValue, FacetValueFragment } from '../../../common/generated-types';
 import { DataService } from '../../../data/providers/data.service';
 
-export type FacetValueSeletorItem = {
-    name: string;
-    facetName: string;
-    id: string;
-    value: FacetValue.Fragment;
-};
-
 /**
  * @description
  * A form control for selecting facet values.
@@ -56,30 +52,71 @@ export type FacetValueSeletorItem = {
         },
     ],
 })
-export class FacetValueSelectorComponent implements OnInit, ControlValueAccessor {
-    @Output() selectedValuesChange = new EventEmitter<FacetValue.Fragment[]>();
-    @Input() facets: FacetWithValues.Fragment[];
+export class FacetValueSelectorComponent implements OnInit, OnDestroy, ControlValueAccessor {
+    @Output() selectedValuesChange = new EventEmitter<FacetValueFragment[]>();
     @Input() readonly = false;
-    @Input() transformControlValueAccessorValue: (value: FacetValueSeletorItem[]) => any[] = value => value;
+    @Input() transformControlValueAccessorValue: (value: FacetValueFragment[]) => any[] = value => value;
+    searchInput$ = new Subject<string>();
+    searchLoading = false;
+    searchResults$: Observable<FacetValueFragment[]>;
+    selectedIds$ = new Subject<string[]>();
 
     @ViewChild(NgSelectComponent) private ngSelect: NgSelectComponent;
 
-    facetValues: FacetValueSeletorItem[] = [];
     onChangeFn: (val: any) => void;
     onTouchFn: () => void;
     disabled = false;
-    value: Array<string | FacetValue.Fragment>;
-    constructor(private dataService: DataService) {}
+    value: Array<string | FacetValueFragment>;
+    private subscription: Subscription;
+    constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}
 
-    ngOnInit() {
-        this.facetValues = flattenFacetValues(this.facets).map(this.toSelectorItem);
+    ngOnInit(): void {
+        this.initSearchResults();
     }
 
-    onChange(selected: FacetValueSeletorItem[]) {
+    private initSearchResults() {
+        const searchItems$ = this.searchInput$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+            tap(() => (this.searchLoading = true)),
+            switchMap(term => {
+                if (!term) {
+                    return of([]);
+                }
+                return this.dataService.facet
+                    .getFacetValues({ take: 10, filter: { name: { contains: term } } })
+                    .mapSingle(result => result.facetValues.items);
+            }),
+            tap(() => (this.searchLoading = false)),
+        );
+        this.subscription = this.selectedIds$
+            .pipe(
+                switchMap(ids => {
+                    if (!ids.length) {
+                        return of([]);
+                    }
+                    return this.dataService.facet
+                        .getFacetValues({ take: 10, filter: { id: { in: ids } } }, 'cache-first')
+                        .mapSingle(result => result.facetValues.items);
+                }),
+            )
+            .subscribe(val => {
+                this.value = val;
+                this.changeDetectorRef.markForCheck();
+            });
+
+        const clear$ = this.selectedValuesChange.pipe(mapTo([]));
+        this.searchResults$ = concat(of([]), merge(searchItems$, clear$));
+    }
+    ngOnDestroy() {
+        this.subscription?.unsubscribe();
+    }
+
+    onChange(selected: FacetValueFragment[]) {
         if (this.readonly) {
             return;
         }
-        this.selectedValuesChange.emit(selected.map(s => s.value));
+        this.selectedValuesChange.emit(selected);
         if (this.onChangeFn) {
             const transformedValue = this.transformControlValueAccessorValue(selected);
             this.onChangeFn(transformedValue);
@@ -103,10 +140,11 @@ export class FacetValueSelectorComponent implements OnInit, ControlValueAccessor
     }
 
     writeValue(obj: string | FacetValue.Fragment[] | Array<string | number> | null): void {
+        let valueIds: string[] | undefined;
         if (typeof obj === 'string') {
             try {
-                const facetIds = JSON.parse(obj) as string[];
-                this.value = facetIds;
+                const facetValueIds = JSON.parse(obj) as string[];
+                valueIds = facetValueIds;
             } catch (err) {
                 // TODO: log error
                 throw err;
@@ -115,19 +153,14 @@ export class FacetValueSelectorComponent implements OnInit, ControlValueAccessor
             const isIdArray = (input: unknown[]): input is Array<string | number> =>
                 input.every(i => typeof i === 'number' || typeof i === 'string');
             if (isIdArray(obj)) {
-                this.value = obj.map(fv => fv.toString());
+                valueIds = obj.map(fv => fv.toString());
             } else {
-                this.value = obj.map(fv => fv.id);
+                valueIds = obj.map(fv => fv.id);
             }
         }
+        if (valueIds) {
+            // this.value = valueIds;
+            this.selectedIds$.next(valueIds);
+        }
     }
-
-    private toSelectorItem = (facetValue: FacetValue.Fragment): FacetValueSeletorItem => {
-        return {
-            name: facetValue.name,
-            facetName: facetValue.facet.name,
-            id: facetValue.id,
-            value: facetValue,
-        };
-    };
 }

+ 0 - 2
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.html

@@ -1,7 +1,5 @@
 <vdr-facet-value-selector
-    *ngIf="facets$ | async as facets"
     [readonly]="readonly"
-    [facets]="facets"
     [formControl]="formControl"
     [transformControlValueAccessorValue]="valueTransformFn"
 ></vdr-facet-value-selector>

+ 4 - 17
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.ts

@@ -1,13 +1,9 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
-import { Observable } from 'rxjs';
-import { shareReplay } from 'rxjs/operators';
 
 import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
-import { FacetWithValues } from '../../../common/generated-types';
-import { DataService } from '../../../data/providers/data.service';
-import { FacetValueSeletorItem } from '../../components/facet-value-selector/facet-value-selector.component';
+import { FacetValueFragment } from '../../../common/generated-types';
 
 /**
  * @description
@@ -23,23 +19,14 @@ import { FacetValueSeletorItem } from '../../components/facet-value-selector/fac
     styleUrls: ['./facet-value-form-input.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class FacetValueFormInputComponent implements FormInputComponent, OnInit {
+export class FacetValueFormInputComponent implements FormInputComponent {
     static readonly id: DefaultFormComponentId = 'facet-value-form-input';
     readonly isListInput = true;
     readonly: boolean;
     formControl: FormControl;
-    facets$: Observable<FacetWithValues.Fragment[]>;
     config: InputComponentConfig;
-    constructor(private dataService: DataService) {}
 
-    ngOnInit() {
-        this.facets$ = this.dataService.facet
-            .getAllFacets()
-            .mapSingle(data => data.facets.items)
-            .pipe(shareReplay(1));
-    }
-
-    valueTransformFn = (values: FacetValueSeletorItem[]) => {
+    valueTransformFn = (values: FacetValueFragment[]) => {
         const isUsedInConfigArg = this.config.__typename === 'ConfigArgDefinition';
         if (isUsedInConfigArg) {
             return JSON.stringify(values.map(s => s.id));