Selaa lähdekoodia

feat(admin-ui): Add live preview of Collection filter changes

Relates to #1530
Michael Bromley 3 vuotta sitten
vanhempi
sitoutus
ba6c64a115
26 muutettua tiedostoa jossa 510 lisäystä ja 248 poistoa
  1. 15 15
      packages/admin-ui/i18n-coverage.json
  2. 23 19
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html
  3. 23 0
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.scss
  4. 64 14
      packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.ts
  5. 20 2
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html
  6. 6 0
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.scss
  7. 18 2
      packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts
  8. 35 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  9. 262 195
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  10. 16 0
      packages/admin-ui/src/lib/core/src/data/definitions/collection-definitions.ts
  11. 12 0
      packages/admin-ui/src/lib/core/src/data/providers/collection-data.service.ts
  12. 1 0
      packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts
  13. 1 0
      packages/admin-ui/src/lib/core/src/shared/providers/routing/can-deactivate-detail-guard.ts
  14. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  15. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  16. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  17. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  18. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  19. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/it.json
  20. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  21. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  22. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json
  23. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/ru.json
  24. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/uk.json
  25. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  26. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

+ 15 - 15
packages/admin-ui/i18n-coverage.json

@@ -1,69 +1,69 @@
 {
-  "generatedOn": "2022-03-14T12:52:29.280Z",
-  "lastCommit": "1ab01194344fd5310fa4d7cd487c22de826d2744",
+  "generatedOn": "2022-04-21T20:04:59.938Z",
+  "lastCommit": "dbf3f1412a2da6f6dd53930557326f022eae62e7",
   "translationStatus": {
     "cs": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 591,
       "percentage": 92
     },
     "de": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 570,
       "percentage": 89
     },
     "en": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 640,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 623,
       "percentage": 97
     },
     "fr": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 613,
       "percentage": 96
     },
     "it": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 621,
       "percentage": 97
     },
     "pl": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 405,
       "percentage": 63
     },
     "pt_BR": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 589,
       "percentage": 92
     },
     "pt_PT": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 634,
       "percentage": 99
     },
     "ru": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 620,
       "percentage": 97
     },
     "uk": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 620,
       "percentage": 97
     },
     "zh_Hans": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 557,
       "percentage": 87
     },
     "zh_Hant": {
-      "tokenCount": 640,
+      "tokenCount": 641,
       "translatedCount": 385,
       "percentage": 60
     }

+ 23 - 19
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.html

@@ -10,22 +10,26 @@
         [formControl]="filterTermControl"
     />
 </div>
-<vdr-data-table
-    [items]="contents$ | async"
-    [itemsPerPage]="contentsItemsPerPage$ | async"
-    [totalItems]="contentsTotalItems$ | async"
-    [currentPage]="contentsCurrentPage$ | async"
-    (pageChange)="setContentsPageNumber($event)"
-    (itemsPerPageChange)="setContentsItemsPerPage($event)"
->
-    <ng-template let-variant="item">
-        <td class="left align-middle">{{ variant.name }}</td>
-        <td class="right align-middle">
-            <vdr-table-row-action
-                iconShape="edit"
-                [label]="'common.edit' | translate"
-                [linkTo]="['/catalog/products', variant.productId, { tab: 'variants' }]"
-            ></vdr-table-row-action>
-        </td>
-    </ng-template>
-</vdr-data-table>
+<div class="table-wrapper">
+    <div class="progress loop" [class.visible]="isLoading"></div>
+    <vdr-data-table
+        [class.loading]="isLoading"
+        [items]="contents$ | async"
+        [itemsPerPage]="contentsItemsPerPage$ | async"
+        [totalItems]="contentsTotalItems$ | async"
+        [currentPage]="contentsCurrentPage$ | async"
+        (pageChange)="setContentsPageNumber($event)"
+        (itemsPerPageChange)="setContentsItemsPerPage($event)"
+    >
+        <ng-template let-variant="item">
+            <td class="left align-middle">{{ variant.name }}</td>
+            <td class="right align-middle">
+                <vdr-table-row-action
+                    iconShape="edit"
+                    [label]="'common.edit' | translate"
+                    [linkTo]="['/catalog/products', variant.productId, { tab: 'variants' }]"
+                ></vdr-table-row-action>
+            </td>
+        </ng-template>
+    </vdr-data-table>
+</div>

+ 23 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.scss

@@ -19,7 +19,30 @@
 }
 
 :host {
+    display: block;
     ::ng-deep table {
         margin-top: -1px;
     }
 }
+vdr-data-table {
+    opacity: 1;
+    transition: opacity 0.3s;
+    &.loading {
+        opacity: 0.5;
+    }
+}
+.table-wrapper {
+    position: relative;
+}
+.progress {
+    position: absolute;
+    top: 0;
+    left: 0;
+    overflow: hidden;
+    height: 6px;
+    opacity: 0;
+    transition: opacity 0.1s;
+    &.visible {
+        opacity: 1;
+    }
+}

+ 64 - 14
packages/admin-ui/src/lib/catalog/src/components/collection-contents/collection-contents.component.ts

@@ -11,20 +11,29 @@ import {
 } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
 import {
+    CollectionFilterParameter,
+    ConfigurableOperationInput,
+    DataService,
+    GetCollectionContents,
+    LocalStorageService,
+} from '@vendure/admin-ui/core';
+import { BehaviorSubject, combineLatest, Observable, of, onErrorResumeNext, Subject } from 'rxjs';
+import {
+    catchError,
     debounceTime,
     distinctUntilChanged,
+    filter,
+    finalize,
     map,
+    retry,
+    retryWhen,
     startWith,
     switchMap,
     takeUntil,
     tap,
 } from 'rxjs/operators';
 
-import { GetCollectionContents } from '@vendure/admin-ui/core';
-import { DataService } from '@vendure/admin-ui/core';
-
 @Component({
     selector: 'vdr-collection-contents',
     templateUrl: './collection-contents.component.html',
@@ -33,6 +42,8 @@ import { DataService } from '@vendure/admin-ui/core';
 })
 export class CollectionContentsComponent implements OnInit, OnChanges, OnDestroy {
     @Input() collectionId: string;
+    @Input() updatedFilters: ConfigurableOperationInput[] | undefined;
+    @Input() previewUpdatedFilters = false;
     @ContentChild(TemplateRef, { static: true }) headerTemplate: TemplateRef<any>;
 
     contents$: Observable<GetCollectionContents.Items[]>;
@@ -40,7 +51,9 @@ export class CollectionContentsComponent implements OnInit, OnChanges, OnDestroy
     contentsItemsPerPage$: Observable<number>;
     contentsCurrentPage$: Observable<number>;
     filterTermControl = new FormControl('');
+    isLoading = false;
     private collectionIdChange$ = new BehaviorSubject<string>('');
+    private filterChanges$ = new BehaviorSubject<ConfigurableOperationInput[]>([]);
     private refresh$ = new BehaviorSubject<boolean>(true);
     private destroy$ = new Subject<void>();
 
@@ -67,37 +80,73 @@ export class CollectionContentsComponent implements OnInit, OnChanges, OnDestroy
             startWith(''),
         );
 
-        const collection$ = combineLatest(
+        const filterChanges$ = this.filterChanges$.asObservable().pipe(
+            filter(() => this.previewUpdatedFilters),
+            tap(() => this.setContentsPageNumber(1)),
+            startWith([]),
+        );
+
+        const fetchUpdate$ = combineLatest(
             this.collectionIdChange$,
             this.contentsCurrentPage$,
             this.contentsItemsPerPage$,
             filterTerm$,
+            filterChanges$,
             this.refresh$,
-        ).pipe(
+        );
+
+        const collection$ = fetchUpdate$.pipe(
             takeUntil(this.destroy$),
-            switchMap(([id, currentPage, itemsPerPage, filterTerm]) => {
+            tap(() => (this.isLoading = true)),
+            debounceTime(50),
+            switchMap(([id, currentPage, itemsPerPage, filterTerm, filters]) => {
                 const take = itemsPerPage;
                 const skip = (currentPage - 1) * itemsPerPage;
                 if (id) {
-                    return this.dataService.collection
-                        .getCollectionContents(id, take, skip, filterTerm)
-                        .mapSingle(data => data.collection);
+                    if (filters.length && this.previewUpdatedFilters) {
+                        const filterClause = filterTerm
+                            ? ({ name: { contains: filterTerm } } as CollectionFilterParameter)
+                            : undefined;
+                        return this.dataService.collection
+                            .previewCollectionVariants(
+                                {
+                                    collectionId: id,
+                                    filters,
+                                },
+                                {
+                                    take,
+                                    skip,
+                                    filter: filterClause,
+                                },
+                            )
+                            .mapSingle(data => data.previewCollectionVariants)
+                            .pipe(catchError(() => of({ items: [], totalItems: 0 })));
+                    } else {
+                        return this.dataService.collection
+                            .getCollectionContents(id, take, skip, filterTerm)
+                            .mapSingle(data => data.collection?.productVariants);
+                    }
                 } else {
                     return of(null);
                 }
             }),
+            tap(() => (this.isLoading = false)),
+            finalize(() => (this.isLoading = false)),
         );
 
-        this.contents$ = collection$.pipe(map(result => (result ? result.productVariants.items : [])));
-        this.contentsTotalItems$ = collection$.pipe(
-            map(result => (result ? result.productVariants.totalItems : 0)),
-        );
+        this.contents$ = collection$.pipe(map(result => (result ? result.items : [])));
+        this.contentsTotalItems$ = collection$.pipe(map(result => (result ? result.totalItems : 0)));
     }
 
     ngOnChanges(changes: SimpleChanges): void {
         if ('collectionId' in changes) {
             this.collectionIdChange$.next(changes.collectionId.currentValue);
         }
+        if ('updatedFilters' in changes) {
+            if (this.updatedFilters) {
+                this.filterChanges$.next(this.updatedFilters);
+            }
+        }
     }
 
     ngOnDestroy() {
@@ -121,6 +170,7 @@ export class CollectionContentsComponent implements OnInit, OnChanges, OnDestroy
         this.router.navigate(['./', { ...this.route.snapshot.params, [key]: value }], {
             relativeTo: this.route,
             queryParamsHandling: 'merge',
+            replaceUrl: true,
         });
     }
 }

+ 20 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.html

@@ -45,7 +45,9 @@
                         [vdrDisabled]="!(updatePermission | hasPermission)"
                     />
                     <label class="visible-toggle">
-                        <ng-container *ngIf="detailForm.value.visible; else private">{{ 'catalog.public' | translate }}</ng-container>
+                        <ng-container *ngIf="detailForm.value.visible; else private">{{
+                            'catalog.public' | translate
+                        }}</ng-container>
                         <ng-template #private>{{ 'catalog.private' | translate }}</ng-template>
                     </label>
                 </clr-toggle-wrapper>
@@ -134,13 +136,29 @@
             </div>
         </div>
         <div class="clr-col">
-            <vdr-collection-contents [collectionId]="id" #collectionContents>
+            <vdr-collection-contents
+                [collectionId]="id"
+                [updatedFilters]="updatedFilters$ | async"
+                [previewUpdatedFilters]="livePreview"
+                #collectionContents
+            >
                 <ng-template let-count>
                     <div class="contents-title">
                         {{ 'catalog.collection-contents' | translate }} ({{
                             'common.results-count' | translate: { count: count }
                         }})
                     </div>
+                    <clr-checkbox-wrapper [class.disabled]="detailForm.get('filters')?.pristine">
+                        <input
+                            type="checkbox"
+                            clrCheckbox
+                            [ngModelOptions]="{ standalone: true }"
+                            [disabled]="detailForm.get('filters')?.pristine"
+                            [ngModel]="livePreview"
+                            (ngModelChange)="toggleLivePreview()"
+                        />
+                        <label>{{ 'catalog.live-preview-contents' | translate }}</label>
+                    </clr-checkbox-wrapper>
                 </ng-template>
             </vdr-collection-contents>
         </div>

+ 6 - 0
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.scss

@@ -1,3 +1,9 @@
 .visible-toggle {
     margin-top: -3px !important;
 }
+clr-checkbox-wrapper {
+    transition: opacity 0.3s;
+    &.disabled {
+        opacity: 0.5;
+    }
+}

+ 18 - 2
packages/admin-ui/src/lib/catalog/src/components/collection-detail/collection-detail.component.ts

@@ -24,6 +24,7 @@ import {
     findTranslation,
     getConfigArgValue,
     LanguageCode,
+    LocalStorageService,
     ModalService,
     NotificationService,
     Permission,
@@ -32,8 +33,8 @@ import {
     UpdateCollectionInput,
 } from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
-import { combineLatest } from 'rxjs';
-import { mergeMap, take } from 'rxjs/operators';
+import { combineLatest, Observable } from 'rxjs';
+import { debounceTime, map, mergeMap, take, filter } from 'rxjs/operators';
 
 import { CollectionContentsComponent } from '../collection-contents/collection-contents.component';
 
@@ -52,6 +53,8 @@ export class CollectionDetailComponent
     assetChanges: { assets?: Asset[]; featuredAsset?: Asset } = {};
     filters: ConfigurableOperation[] = [];
     allFilters: ConfigurableOperationDefinition[] = [];
+    updatedFilters$: Observable<ConfigurableOperationInput[]>;
+    livePreview = false;
     readonly updatePermission = [Permission.UpdateCatalog, Permission.UpdateCollection];
     @ViewChild('collectionContents') contentsComponent: CollectionContentsComponent;
 
@@ -64,6 +67,7 @@ export class CollectionDetailComponent
         private formBuilder: FormBuilder,
         private notificationService: NotificationService,
         private modalService: ModalService,
+        private localStorageService: LocalStorageService,
     ) {
         super(route, router, serverConfigService, dataService);
         this.customFields = this.getCustomFieldConfig('Collection');
@@ -77,6 +81,7 @@ export class CollectionDetailComponent
                 this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
             ),
         });
+        this.livePreview = this.localStorageService.get('livePreviewCollectionContents') ?? false;
     }
 
     ngOnInit() {
@@ -84,6 +89,12 @@ export class CollectionDetailComponent
         this.dataService.collection.getCollectionFilters().single$.subscribe(res => {
             this.allFilters = res.collectionFilters;
         });
+        const filtersFormArray = this.detailForm.get('filters') as FormArray;
+        this.updatedFilters$ = filtersFormArray.statusChanges.pipe(
+            debounceTime(200),
+            filter(() => filtersFormArray.touched),
+            map(status => this.mapOperationsToInputs(this.filters, filtersFormArray.value)),
+        );
     }
 
     ngOnDestroy() {
@@ -220,6 +231,11 @@ export class CollectionDetailComponent
         return super.canDeactivate() && !this.assetChanges.assets && !this.assetChanges.featuredAsset;
     }
 
+    toggleLivePreview() {
+        this.livePreview = !this.livePreview;
+        this.localStorageService.set('livePreviewCollectionContents', this.livePreview);
+    }
+
     /**
      * Sets the values of the form on changes to the category or current language.
      */

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

@@ -3790,6 +3790,11 @@ export type PermissionDefinition = {
   assignable: Scalars['Boolean'];
 };
 
+export type PreviewCollectionVariantsInput = {
+  collectionId: Scalars['ID'];
+  filters: Array<ConfigurableOperationInput>;
+};
+
 /** The price range where the result has more than one price */
 export type PriceRange = {
   __typename?: 'PriceRange';
@@ -4164,6 +4169,8 @@ export type Query = {
   paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
   paymentMethods: PaymentMethodList;
   pendingSearchIndexUpdates: Scalars['Int'];
+  /** Used for real-time previews of the contents of a Collection */
+  previewCollectionVariants: ProductVariantList;
   /** Get a Product either by id or slug. If neither id nor slug is specified, an error will result. */
   product?: Maybe<Product>;
   productOptionGroup?: Maybe<ProductOptionGroup>;
@@ -4316,6 +4323,12 @@ export type QueryPaymentMethodsArgs = {
 };
 
 
+export type QueryPreviewCollectionVariantsArgs = {
+  input: PreviewCollectionVariantsInput;
+  options?: Maybe<ProductVariantListOptions>;
+};
+
+
 export type QueryProductArgs = {
   id?: Maybe<Scalars['ID']>;
   slug?: Maybe<Scalars['String']>;
@@ -5823,6 +5836,21 @@ export type GetCollectionContentsQuery = { collection?: Maybe<(
     ) }
   )> };
 
+export type PreviewCollectionContentsQueryVariables = Exact<{
+  input: PreviewCollectionVariantsInput;
+  options?: Maybe<ProductVariantListOptions>;
+}>;
+
+
+export type PreviewCollectionContentsQuery = { previewCollectionVariants: (
+    { __typename?: 'ProductVariantList' }
+    & Pick<ProductVariantList, 'totalItems'>
+    & { items: Array<(
+      { __typename?: 'ProductVariant' }
+      & Pick<ProductVariant, 'id' | 'productId' | 'name'>
+    )> }
+  ) };
+
 export type AddressFragment = (
   { __typename?: 'Address' }
   & Pick<Address, 'id' | 'createdAt' | 'updatedAt' | 'fullName' | 'company' | 'streetLine1' | 'streetLine2' | 'city' | 'province' | 'postalCode' | 'phoneNumber' | 'defaultShippingAddress' | 'defaultBillingAddress'>
@@ -9392,6 +9420,13 @@ export namespace GetCollectionContents {
   export type Items = NonNullable<(NonNullable<(NonNullable<(NonNullable<GetCollectionContentsQuery['collection']>)['productVariants']>)['items']>)[number]>;
 }
 
+export namespace PreviewCollectionContents {
+  export type Variables = PreviewCollectionContentsQueryVariables;
+  export type Query = PreviewCollectionContentsQuery;
+  export type PreviewCollectionVariants = (NonNullable<PreviewCollectionContentsQuery['previewCollectionVariants']>);
+  export type Items = NonNullable<(NonNullable<(NonNullable<PreviewCollectionContentsQuery['previewCollectionVariants']>)['items']>)[number]>;
+}
+
 export namespace Address {
   export type Fragment = AddressFragment;
   export type Country = (NonNullable<AddressFragment['country']>);

+ 262 - 195
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,198 +1,265 @@
 // tslint:disable
 
-export interface PossibleTypesResultData {
-    possibleTypes: {
-        [key: string]: string[];
-    };
-}
-const result: PossibleTypesResultData = {
-    possibleTypes: {
-        AddFulfillmentToOrderResult: [
-            'Fulfillment',
-            'EmptyOrderLineSelectionError',
-            'ItemsAlreadyFulfilledError',
-            'InsufficientStockOnHandError',
-            'InvalidFulfillmentHandlerError',
-            'FulfillmentStateTransitionError',
-            'CreateFulfillmentError',
-        ],
-        AddManualPaymentToOrderResult: ['Order', 'ManualPaymentStateError'],
-        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
-        CancelOrderResult: [
-            'Order',
-            'EmptyOrderLineSelectionError',
-            'QuantityTooGreatError',
-            'MultipleOrderError',
-            'CancelActiveOrderError',
-            'OrderStateTransitionError',
-        ],
-        CreateAssetResult: ['Asset', 'MimeTypeError'],
-        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        CreatePromotionResult: ['Promotion', 'MissingConditionsError'],
-        CustomField: [
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'StringCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        CustomFieldConfig: [
-            'StringCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        ErrorResult: [
-            'AlreadyRefundedError',
-            'CancelActiveOrderError',
-            'ChannelDefaultLanguageError',
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'CreateFulfillmentError',
-            'EmailAddressConflictError',
-            'EmptyOrderLineSelectionError',
-            'FulfillmentStateTransitionError',
-            'InsufficientStockError',
-            'InsufficientStockOnHandError',
-            'InvalidCredentialsError',
-            'InvalidFulfillmentHandlerError',
-            'ItemsAlreadyFulfilledError',
-            'LanguageNotAvailableError',
-            'ManualPaymentStateError',
-            'MimeTypeError',
-            'MissingConditionsError',
-            'MultipleOrderError',
-            'NativeAuthStrategyError',
-            'NegativeQuantityError',
-            'NoChangesSpecifiedError',
-            'NothingToRefundError',
-            'OrderLimitError',
-            'OrderModificationStateError',
-            'OrderStateTransitionError',
-            'PaymentMethodMissingError',
-            'PaymentOrderMismatchError',
-            'PaymentStateTransitionError',
-            'ProductOptionInUseError',
-            'QuantityTooGreatError',
-            'RefundOrderStateError',
-            'RefundPaymentIdMissingError',
-            'RefundStateTransitionError',
-            'SettlePaymentError',
-        ],
-        ModifyOrderResult: [
-            'Order',
-            'NoChangesSpecifiedError',
-            'OrderModificationStateError',
-            'PaymentMethodMissingError',
-            'RefundPaymentIdMissingError',
-            'OrderLimitError',
-            'NegativeQuantityError',
-            'InsufficientStockError',
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-        ],
-        NativeAuthenticationResult: ['CurrentUser', 'InvalidCredentialsError', 'NativeAuthStrategyError'],
-        Node: [
-            'Address',
-            'Administrator',
-            'Allocation',
-            'Asset',
-            'AuthenticationMethod',
-            'Cancellation',
-            'Channel',
-            'Collection',
-            'Country',
-            'Customer',
-            'CustomerGroup',
-            'Facet',
-            'FacetValue',
-            'Fulfillment',
-            'HistoryEntry',
-            'Job',
-            'Order',
-            'OrderItem',
-            'OrderLine',
-            'OrderModification',
-            'Payment',
-            'PaymentMethod',
-            'Product',
-            'ProductOption',
-            'ProductOptionGroup',
-            'ProductVariant',
-            'Promotion',
-            'Refund',
-            'Release',
-            'Return',
-            'Role',
-            'Sale',
-            'ShippingMethod',
-            'StockAdjustment',
-            'Surcharge',
-            'Tag',
-            'TaxCategory',
-            'TaxRate',
-            'User',
-            'Zone',
-        ],
-        PaginatedList: [
-            'AdministratorList',
-            'AssetList',
-            'CollectionList',
-            'CountryList',
-            'CustomerGroupList',
-            'CustomerList',
-            'FacetList',
-            'HistoryEntryList',
-            'JobList',
-            'OrderList',
-            'PaymentMethodList',
-            'ProductList',
-            'ProductVariantList',
-            'PromotionList',
-            'RoleList',
-            'ShippingMethodList',
-            'TagList',
-            'TaxRateList',
-        ],
-        RefundOrderResult: [
-            'Refund',
-            'QuantityTooGreatError',
-            'NothingToRefundError',
-            'OrderStateTransitionError',
-            'MultipleOrderError',
-            'PaymentOrderMismatchError',
-            'RefundOrderStateError',
-            'AlreadyRefundedError',
-            'RefundStateTransitionError',
-        ],
-        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
-        SearchResultPrice: ['PriceRange', 'SinglePrice'],
-        SettlePaymentResult: [
-            'Payment',
-            'SettlePaymentError',
-            'PaymentStateTransitionError',
-            'OrderStateTransitionError',
-        ],
-        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
-        StockMovement: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
-        StockMovementItem: ['StockAdjustment', 'Allocation', 'Sale', 'Cancellation', 'Return', 'Release'],
-        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
-        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
-        TransitionPaymentToStateResult: ['Payment', 'PaymentStateTransitionError'],
-        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        UpdateGlobalSettingsResult: ['GlobalSettings', 'ChannelDefaultLanguageError'],
-        UpdatePromotionResult: ['Promotion', 'MissingConditionsError'],
-    },
+      export interface PossibleTypesResultData {
+        possibleTypes: {
+          [key: string]: string[]
+        }
+      }
+      const result: PossibleTypesResultData = {
+  "possibleTypes": {
+    "AddFulfillmentToOrderResult": [
+      "Fulfillment",
+      "EmptyOrderLineSelectionError",
+      "ItemsAlreadyFulfilledError",
+      "InsufficientStockOnHandError",
+      "InvalidFulfillmentHandlerError",
+      "FulfillmentStateTransitionError",
+      "CreateFulfillmentError"
+    ],
+    "AddManualPaymentToOrderResult": [
+      "Order",
+      "ManualPaymentStateError"
+    ],
+    "AuthenticationResult": [
+      "CurrentUser",
+      "InvalidCredentialsError"
+    ],
+    "CancelOrderResult": [
+      "Order",
+      "EmptyOrderLineSelectionError",
+      "QuantityTooGreatError",
+      "MultipleOrderError",
+      "CancelActiveOrderError",
+      "OrderStateTransitionError"
+    ],
+    "CreateAssetResult": [
+      "Asset",
+      "MimeTypeError"
+    ],
+    "CreateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "CreateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "CreatePromotionResult": [
+      "Promotion",
+      "MissingConditionsError"
+    ],
+    "CustomField": [
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "StringCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "CustomFieldConfig": [
+      "StringCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "ErrorResult": [
+      "AlreadyRefundedError",
+      "CancelActiveOrderError",
+      "ChannelDefaultLanguageError",
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "CreateFulfillmentError",
+      "EmailAddressConflictError",
+      "EmptyOrderLineSelectionError",
+      "FulfillmentStateTransitionError",
+      "InsufficientStockError",
+      "InsufficientStockOnHandError",
+      "InvalidCredentialsError",
+      "InvalidFulfillmentHandlerError",
+      "ItemsAlreadyFulfilledError",
+      "LanguageNotAvailableError",
+      "ManualPaymentStateError",
+      "MimeTypeError",
+      "MissingConditionsError",
+      "MultipleOrderError",
+      "NativeAuthStrategyError",
+      "NegativeQuantityError",
+      "NoChangesSpecifiedError",
+      "NothingToRefundError",
+      "OrderLimitError",
+      "OrderModificationStateError",
+      "OrderStateTransitionError",
+      "PaymentMethodMissingError",
+      "PaymentOrderMismatchError",
+      "PaymentStateTransitionError",
+      "ProductOptionInUseError",
+      "QuantityTooGreatError",
+      "RefundOrderStateError",
+      "RefundPaymentIdMissingError",
+      "RefundStateTransitionError",
+      "SettlePaymentError"
+    ],
+    "ModifyOrderResult": [
+      "Order",
+      "NoChangesSpecifiedError",
+      "OrderModificationStateError",
+      "PaymentMethodMissingError",
+      "RefundPaymentIdMissingError",
+      "OrderLimitError",
+      "NegativeQuantityError",
+      "InsufficientStockError",
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError"
+    ],
+    "NativeAuthenticationResult": [
+      "CurrentUser",
+      "InvalidCredentialsError",
+      "NativeAuthStrategyError"
+    ],
+    "Node": [
+      "Address",
+      "Administrator",
+      "Allocation",
+      "Asset",
+      "AuthenticationMethod",
+      "Cancellation",
+      "Channel",
+      "Collection",
+      "Country",
+      "Customer",
+      "CustomerGroup",
+      "Facet",
+      "FacetValue",
+      "Fulfillment",
+      "HistoryEntry",
+      "Job",
+      "Order",
+      "OrderItem",
+      "OrderLine",
+      "OrderModification",
+      "Payment",
+      "PaymentMethod",
+      "Product",
+      "ProductOption",
+      "ProductOptionGroup",
+      "ProductVariant",
+      "Promotion",
+      "Refund",
+      "Release",
+      "Return",
+      "Role",
+      "Sale",
+      "ShippingMethod",
+      "StockAdjustment",
+      "Surcharge",
+      "Tag",
+      "TaxCategory",
+      "TaxRate",
+      "User",
+      "Zone"
+    ],
+    "PaginatedList": [
+      "AdministratorList",
+      "AssetList",
+      "CollectionList",
+      "CountryList",
+      "CustomerGroupList",
+      "CustomerList",
+      "FacetList",
+      "HistoryEntryList",
+      "JobList",
+      "OrderList",
+      "PaymentMethodList",
+      "ProductList",
+      "ProductVariantList",
+      "PromotionList",
+      "RoleList",
+      "ShippingMethodList",
+      "TagList",
+      "TaxRateList"
+    ],
+    "RefundOrderResult": [
+      "Refund",
+      "QuantityTooGreatError",
+      "NothingToRefundError",
+      "OrderStateTransitionError",
+      "MultipleOrderError",
+      "PaymentOrderMismatchError",
+      "RefundOrderStateError",
+      "AlreadyRefundedError",
+      "RefundStateTransitionError"
+    ],
+    "RemoveOptionGroupFromProductResult": [
+      "Product",
+      "ProductOptionInUseError"
+    ],
+    "SearchResultPrice": [
+      "PriceRange",
+      "SinglePrice"
+    ],
+    "SettlePaymentResult": [
+      "Payment",
+      "SettlePaymentError",
+      "PaymentStateTransitionError",
+      "OrderStateTransitionError"
+    ],
+    "SettleRefundResult": [
+      "Refund",
+      "RefundStateTransitionError"
+    ],
+    "StockMovement": [
+      "Allocation",
+      "Cancellation",
+      "Release",
+      "Return",
+      "Sale",
+      "StockAdjustment"
+    ],
+    "StockMovementItem": [
+      "StockAdjustment",
+      "Allocation",
+      "Sale",
+      "Cancellation",
+      "Return",
+      "Release"
+    ],
+    "TransitionFulfillmentToStateResult": [
+      "Fulfillment",
+      "FulfillmentStateTransitionError"
+    ],
+    "TransitionOrderToStateResult": [
+      "Order",
+      "OrderStateTransitionError"
+    ],
+    "TransitionPaymentToStateResult": [
+      "Payment",
+      "PaymentStateTransitionError"
+    ],
+    "UpdateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "UpdateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "UpdateGlobalSettingsResult": [
+      "GlobalSettings",
+      "ChannelDefaultLanguageError"
+    ],
+    "UpdatePromotionResult": [
+      "Promotion",
+      "MissingConditionsError"
+    ]
+  }
 };
-export default result;
+      export default result;
+    

+ 16 - 0
packages/admin-ui/src/lib/core/src/data/definitions/collection-definitions.ts

@@ -134,3 +134,19 @@ export const GET_COLLECTION_CONTENTS = gql`
         }
     }
 `;
+
+export const PREVIEW_COLLECTION_CONTENTS = gql`
+    query PreviewCollectionContents(
+        $input: PreviewCollectionVariantsInput!
+        $options: ProductVariantListOptions
+    ) {
+        previewCollectionVariants(input: $input, options: $options) {
+            items {
+                id
+                productId
+                name
+            }
+            totalItems
+        }
+    }
+`;

+ 12 - 0
packages/admin-ui/src/lib/core/src/data/providers/collection-data.service.ts

@@ -13,6 +13,10 @@ import {
     GetCollectionList,
     MoveCollection,
     MoveCollectionInput,
+    PreviewCollectionContentsQuery,
+    PreviewCollectionContentsQueryVariables,
+    PreviewCollectionVariantsInput,
+    ProductVariantListOptions,
     UpdateCollection,
     UpdateCollectionInput,
 } from '../../common/generated-types';
@@ -24,6 +28,7 @@ import {
     GET_COLLECTION_FILTERS,
     GET_COLLECTION_LIST,
     MOVE_COLLECTION,
+    PREVIEW_COLLECTION_CONTENTS,
     UPDATE_COLLECTION,
 } from '../definitions/collection-definitions';
 
@@ -108,6 +113,13 @@ export class CollectionDataService {
         );
     }
 
+    previewCollectionVariants(input: PreviewCollectionVariantsInput, options: ProductVariantListOptions) {
+        return this.baseDataService.query<
+            PreviewCollectionContentsQuery,
+            PreviewCollectionContentsQueryVariables
+        >(PREVIEW_COLLECTION_CONTENTS, { input, options });
+    }
+
     getCollectionContents(id: string, take: number = 10, skip: number = 0, filterTerm?: string) {
         const filter = filterTerm
             ? ({ name: { contains: filterTerm } } as CollectionFilterParameter)

+ 1 - 0
packages/admin-ui/src/lib/core/src/providers/local-storage/local-storage.service.ts

@@ -13,6 +13,7 @@ export type LocalStorageTypeMap = {
     orderListLastCustomFilters: any;
     dashboardWidgetLayout: WidgetLayoutDefinition;
     activeTheme: string;
+    livePreviewCollectionContents: boolean;
 };
 
 export type LocalStorageLocationBasedTypeMap = {

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/providers/routing/can-deactivate-detail-guard.ts

@@ -17,6 +17,7 @@ export class CanDeactivateDetailGuard implements CanDeactivate<DeactivateAware>
         currentState: RouterStateSnapshot,
         nextState?: RouterStateSnapshot,
     ): boolean | Observable<boolean> {
+        console.log(currentRoute, currentState, nextState);
         if (!component.canDeactivate()) {
             return this.modalService
                 .dialog({

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtrovat dle jména nebo SKU",
     "filters": "Filtry",
     "group-by-product": "Seskupovat varianty",
+    "live-preview-contents": "",
     "manage-variants": "Správa variant",
     "move-down": "Posunout dolů",
     "move-to": "Posunout",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Nach Name oder Artikelnummer filtern",
     "filters": "Filter",
     "group-by-product": "Nach Produkt gruppieren",
+    "live-preview-contents": "",
     "manage-variants": "Varianten verwalten",
     "move-down": "Nach unten bewegen",
     "move-to": "Verschieben nach",

+ 2 - 1
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filter by name or SKU",
     "filters": "Filters",
     "group-by-product": "Group by product",
+    "live-preview-contents": "Live-preview contents",
     "manage-variants": "Manage variants",
     "move-down": "Move down",
     "move-to": "Move to",
@@ -671,4 +672,4 @@
     "job-result": "Job result",
     "job-state": "Job state"
   }
-}
+}

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtrar por código de referencia",
     "filters": "Filtros",
     "group-by-product": "Agrupar por producto",
+    "live-preview-contents": "",
     "manage-variants": "Gestionar variantes",
     "move-down": "Mover abajo",
     "move-to": "Mover a",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtrer par nom ou UGS",
     "filters": "Filtres",
     "group-by-product": "Grouper par produit",
+    "live-preview-contents": "",
     "manage-variants": "Gérer les variations",
     "move-down": "Déplacer vers le bas",
     "move-to": "Déplacer à",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/it.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtra per nome o SKU",
     "filters": "Filtri",
     "group-by-product": "Raggruppa per prodotto",
+    "live-preview-contents": "",
     "manage-variants": "Gestione varianti",
     "move-down": "Sposta in basso",
     "move-to": "Sposta in",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "",
     "filters": "Filtry",
     "group-by-product": "Grupuj po produkcie",
+    "live-preview-contents": "",
     "manage-variants": "Zarządzaj wariantami",
     "move-down": "Przesuń w dół",
     "move-to": "Przesuń do",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtrar por nome ou SKU",
     "filters": "Filtros",
     "group-by-product": "Agrupar por produto",
+    "live-preview-contents": "",
     "manage-variants": "Gerência das variações",
     "move-down": "Mover para baixo",
     "move-to": "Mover para",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_PT.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Filtrar por nome ou SKU",
     "filters": "Filtros",
     "group-by-product": "Agrupar por produto",
+    "live-preview-contents": "",
     "manage-variants": "Gerir variações",
     "move-down": "Mover para baixo",
     "move-to": "Mover para",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/ru.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Фильтр по имени или артикулу (SKU)",
     "filters": "Фильтры",
     "group-by-product": "Группировать по товару",
+    "live-preview-contents": "",
     "manage-variants": "Управление вариантами",
     "move-down": "Двигать вниз",
     "move-to": "Двигать к",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/uk.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "Фільтр по імені або артикулу (SKU)",
     "filters": "Фільтри",
     "group-by-product": "Групувати по товару",
+    "live-preview-contents": "",
     "manage-variants": "Управління варіантами",
     "move-down": "Рухати вниз",
     "move-to": "Рухати до",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "按名字或商品编码过滤",
     "filters": "过滤条件",
     "group-by-product": "按商品分组显示",
+    "live-preview-contents": "",
     "manage-variants": "商品规格管理",
     "move-down": "向下移",
     "move-to": "移至",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -98,6 +98,7 @@
     "filter-by-name-or-sku": "",
     "filters": "篩選條件",
     "group-by-product": "按商品分组顯示",
+    "live-preview-contents": "",
     "manage-variants": "商品規格管理",
     "move-down": "下移",
     "move-to": "移至",