فهرست منبع

feat(admin-ui): Introduce typed versions of base list/detail components

This new base class makes use of the TypedDocumentNode as well as the new Angular inject()
function to massively improve type safety and also reduce boilerplate for list and detail
components.
Michael Bromley 2 سال پیش
والد
کامیت
cacc663de6
29فایلهای تغییر یافته به همراه1187 افزوده شده و 509 حذف شده
  1. 33 1
      packages/admin-ui/src/lib/catalog/src/catalog.module.ts
  2. 8 7
      packages/admin-ui/src/lib/catalog/src/catalog.routes.ts
  3. 18 20
      packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.component.html
  4. 1 25
      packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.types.ts
  5. 43 72
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts
  6. 31 0
      packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.graphql.ts
  7. 123 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html
  8. 0 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.scss
  9. 138 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts
  10. 99 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts
  11. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html
  12. 54 103
      packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.ts
  13. 44 0
      packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.graphql.ts
  14. 80 2
      packages/admin-ui/src/lib/core/src/common/base-detail.component.ts
  15. 69 3
      packages/admin-ui/src/lib/core/src/common/base-list.component.ts
  16. 2 0
      packages/admin-ui/src/lib/core/src/common/component-registry-types.ts
  17. 3 3
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  18. 321 238
      packages/admin-ui/src/lib/core/src/common/introspection-result.ts
  19. 2 1
      packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts
  20. 2 1
      packages/admin-ui/src/lib/core/src/data/providers/data.service.ts
  21. 1 1
      packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts
  22. 9 0
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts
  23. 9 0
      packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts
  24. 26 9
      packages/admin-ui/src/lib/core/src/providers/page/page.service.ts
  25. 12 6
      packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss
  26. 11 6
      packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.ts
  27. 44 0
      packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.graphql.ts
  28. 2 9
      packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.ts
  29. 1 1
      packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

+ 33 - 1
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -1,7 +1,13 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, ROUTES } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { BulkActionRegistryService, PageService, SharedModule } from '@vendure/admin-ui/core';
+import {
+    BulkActionRegistryService,
+    detailComponentWithResolver,
+    GetProductVariantDetailDocument,
+    PageService,
+    SharedModule,
+} from '@vendure/admin-ui/core';
 
 import { createRoutes } from './catalog.routes';
 import { ApplyFacetDialogComponent } from './components/apply-facet-dialog/apply-facet-dialog.component';
@@ -45,6 +51,7 @@ import {
 } from './components/product-list/product-list-bulk-actions';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component';
+import { ProductVariantDetailComponent } from './components/product-variant-detail/product-variant-detail.component';
 import { ProductVariantListComponent } from './components/product-variant-list/product-variant-list.component';
 import { ProductVariantsEditorComponent } from './components/product-variants-editor/product-variants-editor.component';
 import { ProductVariantsListComponent } from './components/product-variants-list/product-variants-list.component';
@@ -83,6 +90,7 @@ const CATALOG_COMPONENTS = [
     MoveCollectionsDialogComponent,
     ProductVariantListComponent,
     ProductDetail2Component,
+    ProductVariantDetailComponent,
 ];
 
 @NgModule({
@@ -135,6 +143,30 @@ export class CatalogModule {
             route: 'variants',
             component: ProductVariantListComponent,
         });
+        pageService.registerPageTab({
+            location: 'product-variant-detail',
+            tab: _('catalog.product-variants'),
+            route: '',
+            component: detailComponentWithResolver({
+                component: ProductVariantDetailComponent,
+                query: GetProductVariantDetailDocument,
+                getEntity: result => result.productVariant,
+                getBreadcrumbs: result => [
+                    {
+                        label: _('breadcrumb.products'),
+                        link: ['/catalog', 'products'],
+                    },
+                    {
+                        label: `${result.productVariant?.product.name}`,
+                        link: ['/catalog', 'products', result.productVariant?.product.id],
+                    },
+                    {
+                        label: `${result.productVariant?.name}`,
+                        link: ['variants', result.productVariant?.id],
+                    },
+                ],
+            }),
+        });
         pageService.registerPageTab({
             location: 'facet-list',
             tab: _('catalog.facets'),

+ 8 - 7
packages/admin-ui/src/lib/catalog/src/catalog.routes.ts

@@ -14,13 +14,7 @@ import { PageService } from '../../core/src/providers/page/page.service';
 import { PageComponent } from '../../core/src/shared/components/page/page.component';
 
 import { AssetDetailComponent } from './components/asset-detail/asset-detail.component';
-import { AssetListComponent } from './components/asset-list/asset-list.component';
-import { CollectionDetailComponent } from './components/collection-detail/collection-detail.component';
-import { CollectionListComponent } from './components/collection-list/collection-list.component';
 import { FacetDetailComponent } from './components/facet-detail/facet-detail.component';
-import { FacetListComponent } from './components/facet-list/facet-list.component';
-import { ProductDetailComponent } from './components/product-detail/product-detail.component';
-import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductOptionsEditorComponent } from './components/product-options-editor/product-options-editor.component';
 import { ProductVariantsEditorComponent } from './components/product-variants-editor/product-variants-editor.component';
 import { AssetResolver } from './providers/routing/asset-resolver';
@@ -43,13 +37,20 @@ export const createRoutes = (pageService: PageService): Route[] => [
         path: 'products/:id',
         component: PageComponent,
         resolve: createResolveData(ProductResolver),
-        // canDeactivate: [CanDeactivateDetailGuard],
         data: {
             locationId: 'product-detail',
             breadcrumb: productBreadcrumb,
         },
         children: pageService.getPageTabRoutes('product-detail'),
     },
+    {
+        path: 'products/:productId/variants/:id',
+        component: PageComponent,
+        data: {
+            locationId: 'product-variant-detail',
+        },
+        children: pageService.getPageTabRoutes('product-variant-detail'),
+    },
     {
         path: 'products/:id/manage-variants',
         component: ProductVariantsEditorComponent,

+ 18 - 20
packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.component.html

@@ -22,7 +22,7 @@
             </button>
             <ng-template #updateButton>
                 <button
-                    *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']"
+                    *vdrIfPermissions="updatePermissions"
                     class="btn btn-primary"
                     (click)="save()"
                     [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
@@ -39,7 +39,7 @@
         <vdr-page-detail-sidebar
             ><vdr-card>
                 <vdr-form-field [label]="'catalog.visibility' | translate" for="visibility">
-                    <clr-toggle-wrapper *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']">
+                    <clr-toggle-wrapper *vdrIfPermissions="updatePermissions">
                         <input
                             type="checkbox"
                             clrToggle
@@ -77,12 +77,12 @@
                     <vdr-facet-value-chip
                         *ngFor="let facetValue of facetValues$ | async"
                         [facetValue]="facetValue"
-                        [removable]="['UpdateCatalog', 'UpdateProduct'] | hasPermission"
+                        [removable]="updatePermissions | hasPermission"
                         (remove)="removeProductFacetValue(facetValue.id)"
                     ></vdr-facet-value-chip>
                     <button
                         class="btn btn-sm btn-secondary"
-                        *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']"
+                        *vdrIfPermissions="updatePermissions"
                         (click)="selectProductFacetValue()"
                     >
                         <clr-icon shape="plus"></clr-icon>
@@ -107,7 +107,7 @@
                         id="name"
                         type="text"
                         formControlName="name"
-                        [readonly]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
+                        [readonly]="!(updatePermissions | hasPermission)"
                         (input)="updateSlug($event.target.value)"
                     />
                 </vdr-form-field>
@@ -132,7 +132,7 @@
                         id="slug"
                         type="text"
                         formControlName="slug"
-                        [readonly]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
+                        [readonly]="!(updatePermissions | hasPermission)"
                     />
                 </vdr-form-field>
                 <vdr-form-field
@@ -142,25 +142,23 @@
                 >
                     <vdr-rich-text-editor
                         formControlName="description"
-                        [readonly]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
+                        [readonly]="!(updatePermissions | hasPermission)"
                     ></vdr-rich-text-editor>
                 </vdr-form-field>
             </vdr-card>
             <vdr-card [title]="'common.custom-fields' | translate" *ngIf="customFields.length">
-                <section formGroupName="customFields">
-                    <vdr-tabbed-custom-fields
-                        entityName="Product"
-                        [customFields]="customFields"
-                        [customFieldsFormGroup]="detailForm.get(['customFields'])"
-                        [readonly]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
-                    ></vdr-tabbed-custom-fields>
-                </section>
-                <vdr-custom-detail-component-host
-                    locationId="product-detail"
-                    [entity$]="entity$"
-                    [detailForm]="detailForm"
-                ></vdr-custom-detail-component-host>
+                <vdr-tabbed-custom-fields
+                    entityName="Product"
+                    [customFields]="customFields"
+                    [customFieldsFormGroup]="detailForm.get(['customFields'])"
+                    [readonly]="!(updatePermissions | hasPermission)"
+                ></vdr-tabbed-custom-fields>
             </vdr-card>
+            <vdr-custom-detail-component-host
+                locationId="product-detail"
+                [entity$]="entity$"
+                [detailForm]="detailForm"
+            ></vdr-custom-detail-component-host>
             <vdr-card [title]="'catalog.assets' | translate">
                 <vdr-assets
                     [assets]="assetChanges.assets || product.assets"

+ 1 - 25
packages/admin-ui/src/lib/catalog/src/components/product-detail2/product-detail.types.ts

@@ -1,30 +1,6 @@
-import { Asset, GlobalFlag } from '@vendure/admin-ui/core';
-
-export type TabName = 'details' | 'variants';
-
-export interface VariantFormValue {
-    id: string;
-    enabled: boolean;
-    sku: string;
-    name: string;
-    price: number;
-    priceWithTax: number;
-    taxCategoryId: string;
-    stockOnHand: number;
-    useGlobalOutOfStockThreshold: boolean;
-    outOfStockThreshold: number;
-    trackInventory: GlobalFlag;
-    facetValueIds: string[];
-    customFields?: any;
-}
+import { Asset } from '@vendure/admin-ui/core';
 
 export interface SelectedAssets {
     assets?: Asset[];
     featuredAsset?: Asset;
 }
-
-export interface PaginationConfig {
-    totalItems: number;
-    currentPage: number;
-    itemsPerPage: number;
-}

+ 43 - 72
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.component.ts

@@ -1,25 +1,17 @@
 import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
-    BaseListComponent,
     DataService,
-    DataTableService,
     FacetValueFormInputComponent,
-    GetProductListQuery,
-    GetProductListQueryVariables,
-    ItemOf,
     JobQueueService,
     JobState,
-    LanguageCode,
     ModalService,
     NotificationService,
-    ProductFilterParameter,
-    ProductSortParameter,
-    ServerConfigService,
+    ProductListQueryDocument,
+    TypedBaseListComponent,
 } from '@vendure/admin-ui/core';
-import { EMPTY, lastValueFrom, Observable } from 'rxjs';
-import { delay, switchMap, tap } from 'rxjs/operators';
+import { EMPTY, lastValueFrom } from 'rxjs';
+import { delay, switchMap } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-products-list',
@@ -27,39 +19,33 @@ import { delay, switchMap, tap } from 'rxjs/operators';
     styleUrls: ['./product-list.component.scss'],
 })
 export class ProductListComponent
-    extends BaseListComponent<
-        GetProductListQuery,
-        ItemOf<GetProductListQuery, 'products'>,
-        GetProductListQueryVariables
-    >
+    extends TypedBaseListComponent<typeof ProductListQueryDocument, 'products'>
     implements OnInit
 {
-    availableLanguages$: Observable<LanguageCode[]>;
-    contentLanguage$: Observable<LanguageCode>;
     pendingSearchIndexUpdates = 0;
-    readonly customFields = this.serverConfigService.getCustomFieldsFor('Product');
-
-    readonly filters = this.dataTableService
-        .createFilterCollection<ProductFilterParameter>()
+    readonly customFields = this.getCustomFieldConfig('Product');
+    readonly filters = this.createFilterCollection()
         .addDateFilters()
-        .addFilter({
-            name: 'id',
-            type: { kind: 'text' },
-            label: _('common.id'),
-            filterField: 'id',
-        })
-        .addFilter({
-            name: 'enabled',
-            type: { kind: 'boolean' },
-            label: _('common.enabled'),
-            filterField: 'enabled',
-        })
-        .addFilter({
-            name: 'slug',
-            type: { kind: 'text' },
-            label: _('common.slug'),
-            filterField: 'slug',
-        })
+        .addFilters([
+            {
+                name: 'id',
+                type: { kind: 'text' },
+                label: _('common.id'),
+                filterField: 'id',
+            },
+            {
+                name: 'enabled',
+                type: { kind: 'boolean' },
+                label: _('common.enabled'),
+                filterField: 'enabled',
+            },
+            {
+                name: 'slug',
+                type: { kind: 'text' },
+                label: _('common.slug'),
+                filterField: 'slug',
+            },
+        ])
         .addFilter({
             name: 'facetValues',
             type: {
@@ -94,33 +80,29 @@ export class ProductListComponent
         .addCustomFieldFilters(this.customFields)
         .connectToRoute(this.route);
 
-    readonly sorts = this.dataTableService
-        .createSortCollection<ProductSortParameter>()
+    readonly sorts = this.createSortCollection()
         .defaultSort('createdAt', 'DESC')
-        .addSort({ name: 'id' })
-        .addSort({ name: 'createdAt' })
-        .addSort({ name: 'updatedAt' })
-        .addSort({ name: 'name' })
-        .addSort({ name: 'slug' })
+        .addSorts([
+            { name: 'id' },
+            { name: 'createdAt' },
+            { name: 'updatedAt' },
+            { name: 'name' },
+            { name: 'slug' },
+        ])
         .addCustomFieldSorts(this.customFields)
         .connectToRoute(this.route);
 
     constructor(
-        private dataService: DataService,
+        protected dataService: DataService,
         private modalService: ModalService,
         private notificationService: NotificationService,
         private jobQueueService: JobQueueService,
-        private dataTableService: DataTableService,
-        private serverConfigService: ServerConfigService,
-        router: Router,
-        route: ActivatedRoute,
     ) {
-        super(router, route);
-        super.setQueryFn(
-            (args: any) => this.dataService.product.getProducts(args).refetchOnChannelChange(),
-            data => data.products,
-            // eslint-disable-next-line @typescript-eslint/no-shadow
-            (skip, take) => ({
+        super();
+        this.configure({
+            document: ProductListQueryDocument,
+            getItems: data => data.products,
+            setVariables: (skip, take) => ({
                 options: {
                     skip,
                     take,
@@ -133,23 +115,16 @@ export class ProductListComponent
                     sort: this.sorts.createSortInput(),
                 },
             }),
-        );
+            refreshListOnChanges: [this.sorts.valueChanges, this.filters.valueChanges],
+        });
     }
 
     ngOnInit() {
         super.ngOnInit();
-        this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
-        this.contentLanguage$ = this.dataService.client
-            .uiState()
-            .mapStream(({ uiState }) => uiState.contentLanguage)
-            .pipe(tap(() => this.refresh()));
-
         this.dataService.product
             .getPendingSearchIndexUpdates()
             .mapSingle(({ pendingSearchIndexUpdates }) => pendingSearchIndexUpdates)
             .subscribe(value => (this.pendingSearchIndexUpdates = value));
-
-        super.refreshListOnChanges(this.contentLanguage$, this.filters.valueChanges, this.sorts.valueChanges);
     }
 
     rebuildSearchIndex() {
@@ -208,8 +183,4 @@ export class ProductListComponent
                 },
             );
     }
-
-    setLanguage(code: LanguageCode) {
-        this.dataService.client.setContentLanguage(code).subscribe();
-    }
 }

+ 31 - 0
packages/admin-ui/src/lib/catalog/src/components/product-list/product-list.graphql.ts

@@ -0,0 +1,31 @@
+import { gql } from 'apollo-angular';
+
+export const PRODUCT_LIST_QUERY = gql`
+    query ProductListQuery($options: ProductListOptions) {
+        products(options: $options) {
+            items {
+                id
+                createdAt
+                updatedAt
+                enabled
+                languageCode
+                name
+                slug
+                featuredAsset {
+                    id
+                    createdAt
+                    updatedAt
+                    preview
+                    focalPoint {
+                        x
+                        y
+                    }
+                }
+                variantList {
+                    totalItems
+                }
+            }
+            totalItems
+        }
+    }
+`;

+ 123 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.html

@@ -0,0 +1,123 @@
+<vdr-page-block>
+    <vdr-action-bar>
+        <vdr-ab-left>
+            <div class="flex clr-flex-row"></div>
+            <vdr-language-selector
+                [disabled]="isNew$ | async"
+                [availableLanguageCodes]="availableLanguages$ | async"
+                [currentLanguageCode]="languageCode$ | async"
+                (languageCodeChange)="setLanguage($event)"
+            ></vdr-language-selector>
+        </vdr-ab-left>
+
+        <vdr-ab-right>
+            <vdr-action-bar-items locationId="product-detail"></vdr-action-bar-items>
+            <button
+                *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']"
+                class="btn btn-primary"
+                (click)="save()"
+                [disabled]="(detailForm.invalid || detailForm.pristine) && !assetsChanged()"
+            >
+                {{ 'common.update' | translate }}
+            </button>
+        </vdr-ab-right>
+    </vdr-action-bar>
+</vdr-page-block>
+<form class="form" [formGroup]="detailForm" *ngIf="entity$ | async as variant">
+    <vdr-page-detail-layout>
+        <vdr-page-detail-sidebar
+            ><vdr-card>
+                <vdr-form-field [label]="'catalog.visibility' | translate" for="visibility">
+                    <clr-toggle-wrapper *vdrIfPermissions="['UpdateCatalog', 'UpdateProduct']">
+                        <input
+                            type="checkbox"
+                            clrToggle
+                            name="enabled"
+                            [formControl]="detailForm.get(['enabled'])"
+                        />
+                        <label>{{ 'common.enabled' | translate }}</label>
+                    </clr-toggle-wrapper>
+                </vdr-form-field>
+            </vdr-card>
+
+            <vdr-card>
+                <vdr-page-entity-info *ngIf="entity$ | async as entity" [entity]="entity" />
+            </vdr-card>
+        </vdr-page-detail-sidebar>
+
+        <vdr-page-block>
+            <button type="submit" hidden x-data="prevents enter key from triggering other buttons"></button>
+            <vdr-card>
+                <vdr-form-field [label]="'common.name' | translate" for="name">
+                    <input
+                        id="name"
+                        type="text"
+                        formControlName="name"
+                        [readonly]="!(['UpdateCatalog', 'UpdateProduct'] | hasPermission)"
+                    />
+                </vdr-form-field>
+                <vdr-form-field [label]="'catalog.sku' | translate" for="sku">
+                    <input
+                        id="sku"
+                        type="text"
+                        formControlName="sku"
+                        [readonly]="!(updatePermissions | hasPermission)"
+                    />
+                </vdr-form-field>
+            </vdr-card>
+            <vdr-card [title]="'common.custom-fields' | translate" *ngIf="customFields.length">
+                <vdr-tabbed-custom-fields
+                    entityName="ProductVariant"
+                    [customFields]="customFields"
+                    [customFieldsFormGroup]="detailForm.get('customFields')"
+                    [readonly]="!(updatePermissions | hasPermission)"
+                />
+            </vdr-card>
+            <vdr-custom-detail-component-host
+                locationId="product-variant-detail"
+                [entity$]="entity$"
+                [detailForm]="detailForm"
+            />
+            <vdr-card [title]="'catalog.assets' | translate">
+                <vdr-assets
+                    [assets]="assetChanges.assets || variant.assets"
+                    [featuredAsset]="assetChanges.featuredAsset || variant.featuredAsset"
+                    [updatePermissions]="updatePermissions"
+                    (change)="assetChanges = $event"
+                />
+            </vdr-card>
+            <vdr-card [title]="'catalog.price-and-tax' | translate">
+                <vdr-form-field [label]="'catalog.tax-category' | translate" for="taxCategory">
+                    <select name="taxCategory" formControlName="taxCategoryId">
+                        <option *ngFor="let taxCategory of taxCategories$ | async" [value]="taxCategory.id">
+                            {{ taxCategory.name }}
+                        </option>
+                    </select>
+                </vdr-form-field>
+                <vdr-form-field [label]="'catalog.price' | translate" for="sku">
+                    <vdr-currency-input
+                        *ngIf="!(channelPriceIncludesTax$ | async)"
+                        [currencyCode]="variant.currencyCode"
+                        [readonly]="!(updatePermissions | hasPermission)"
+                        formControlName="price"
+                    />
+                    <vdr-currency-input
+                        *ngIf="channelPriceIncludesTax$ | async"
+                        [currencyCode]="variant.currencyCode"
+                        [readonly]="!(updatePermissions | hasPermission)"
+                        formControlName="priceWithTax"
+                    />
+                </vdr-form-field>
+                <vdr-variant-price-detail
+                    [price]="detailForm.get('price')!.value"
+                    [currencyCode]="variant.currencyCode"
+                    [priceIncludesTax]="channelPriceIncludesTax$ | async"
+                    [taxCategoryId]="detailForm.get('taxCategoryId')!.value"
+                />
+            </vdr-card>
+            <vdr-card [title]="'catalog.stock' | translate">
+
+            </vdr-card>
+        </vdr-page-block>
+    </vdr-page-detail-layout>
+</form>

+ 0 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.scss


+ 138 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.component.ts

@@ -0,0 +1,138 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { FormBuilder } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { ProductDetailService } from '@vendure/admin-ui/catalog';
+import {
+    BaseDetailComponent,
+    DataService,
+    findTranslation,
+    GetProductVariantDetailDocument,
+    GetProductVariantDetailQuery,
+    GlobalFlag,
+    ItemOf,
+    LanguageCode,
+    ModalService,
+    NotificationService,
+    Permission,
+    ServerConfigService,
+    TaxCategoryFragment,
+    TypedBaseDetailComponent,
+    TypedBaseListComponent,
+} from '@vendure/admin-ui/core';
+import { Observable } from 'rxjs';
+import { map, shareReplay, takeUntil } from 'rxjs/operators';
+import { SelectedAssets } from '../product-detail2/product-detail.types';
+
+export interface VariantFormValue {
+    id: string;
+    enabled: boolean;
+    sku: string;
+    name: string;
+    price: number;
+    priceWithTax: number;
+    taxCategoryId: string;
+    stockOnHand: number;
+    useGlobalOutOfStockThreshold: boolean;
+    outOfStockThreshold: number;
+    trackInventory: GlobalFlag;
+    facetValueIds: string[][];
+    customFields?: any;
+}
+
+@Component({
+    selector: 'vdr-product-variant-detail',
+    templateUrl: './product-variant-detail.component.html',
+    styleUrls: ['./product-variant-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ProductVariantDetailComponent
+    extends TypedBaseDetailComponent<typeof GetProductVariantDetailDocument, 'productVariant'>
+    implements OnInit
+{
+    public readonly updatePermissions = [Permission.UpdateCatalog, Permission.UpdateProduct];
+    readonly customFields = this.getCustomFieldConfig('ProductVariant');
+    detailForm = this.formBuilder.group<VariantFormValue>({
+        id: '',
+        enabled: false,
+        sku: '',
+        name: '',
+        price: 0,
+        priceWithTax: 0,
+        taxCategoryId: '',
+        stockOnHand: 0,
+        useGlobalOutOfStockThreshold: true,
+        outOfStockThreshold: 0,
+        trackInventory: GlobalFlag.TRUE,
+        facetValueIds: [],
+        customFields: this.formBuilder.group(
+            this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
+        ),
+    });
+    assetChanges: SelectedAssets = {};
+    taxCategories$: Observable<Array<ItemOf<GetProductVariantDetailQuery, 'taxCategories'>>>;
+    stockLocations$: Observable<ItemOf<GetProductVariantDetailQuery, 'stockLocations'>>;
+    channelPriceIncludesTax$: Observable<boolean>;
+
+    constructor(
+        route: ActivatedRoute,
+        router: Router,
+        serverConfigService: ServerConfigService,
+        private productDetailService: ProductDetailService,
+        private formBuilder: FormBuilder,
+        private modalService: ModalService,
+        private notificationService: NotificationService,
+        protected dataService: DataService,
+        private changeDetector: ChangeDetectorRef,
+    ) {
+        super(route, router, serverConfigService, dataService);
+    }
+
+    ngOnInit() {
+        this.init();
+        this.taxCategories$ = this.result$.pipe(map(data => data.taxCategories.items));
+        this.channelPriceIncludesTax$ = this.dataService.settings
+            .getActiveChannel('cache-first')
+            .refetchOnChannelChange()
+            .mapStream(data => data.activeChannel.pricesIncludeTax)
+            .pipe(shareReplay(1));
+    }
+
+    save() {
+        /**/
+    }
+
+    assetsChanged(): boolean {
+        return false;
+    }
+
+    protected setFormValues(
+        variant: NonNullable<GetProductVariantDetailQuery['productVariant']>,
+        languageCode: LanguageCode,
+    ): void {
+        const variantTranslation = findTranslation(variant, languageCode);
+        const facetValueIds = variant.facetValues.map(fv => fv.id);
+        this.detailForm.patchValue({
+            id: variant.id,
+            enabled: variant.enabled,
+            sku: variant.sku,
+            name: variantTranslation ? variantTranslation.name : '',
+            price: variant.price,
+            priceWithTax: variant.priceWithTax,
+            taxCategoryId: variant.taxCategory.id,
+            stockOnHand: variant.stockLevels[0].stockOnHand,
+            useGlobalOutOfStockThreshold: variant.useGlobalOutOfStockThreshold,
+            outOfStockThreshold: variant.outOfStockThreshold,
+            trackInventory: variant.trackInventory,
+            facetValueIds,
+        });
+
+        if (this.customFields.length) {
+            this.setCustomFieldFormValues(
+                this.customFields,
+                this.detailForm.get('customFields'),
+                variant,
+                variantTranslation,
+            );
+        }
+    }
+}

+ 99 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-detail/product-variant-detail.graphql.ts

@@ -0,0 +1,99 @@
+import { ASSET_FRAGMENT, PRODUCT_OPTION_FRAGMENT } from '@vendure/admin-ui/core';
+import { gql } from 'apollo-angular';
+
+export const GET_PRODUCT_VARIANT_DETAIL = gql`
+    query GetProductVariantDetail($id: ID!) {
+        productVariant(id: $id) {
+            id
+            createdAt
+            updatedAt
+            enabled
+            languageCode
+            name
+            price
+            currencyCode
+            priceWithTax
+            stockOnHand
+            stockAllocated
+            trackInventory
+            outOfStockThreshold
+            useGlobalOutOfStockThreshold
+            taxRateApplied {
+                id
+                name
+                value
+            }
+            taxCategory {
+                id
+                name
+            }
+            sku
+            options {
+                ...ProductOption
+            }
+            stockLevels {
+                id
+                createdAt
+                updatedAt
+                stockOnHand
+                stockAllocated
+                stockLocationId
+                stockLocation {
+                    id
+                    createdAt
+                    updatedAt
+                    name
+                }
+            }
+            facetValues {
+                id
+                code
+                name
+                facet {
+                    id
+                    name
+                }
+            }
+            featuredAsset {
+                ...Asset
+            }
+            assets {
+                ...Asset
+            }
+            translations {
+                id
+                languageCode
+                name
+            }
+            channels {
+                id
+                code
+            }
+            product {
+                id
+                name
+            }
+        }
+        stockLocations(options: { take: 100 }) {
+            items {
+                id
+                createdAt
+                updatedAt
+                name
+                description
+            }
+        }
+        taxCategories(options: { take: 100 }) {
+            items {
+                id
+                createdAt
+                updatedAt
+                name
+                isDefault
+            }
+            totalItems
+        }
+    }
+    ${PRODUCT_OPTION_FRAGMENT}
+    ${ASSET_FRAGMENT}
+`;

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.html

@@ -65,7 +65,7 @@
     </vdr-dt2-column>
     <vdr-dt2-column [heading]="'catalog.name' | translate" [optional]="false" [sort]="sorts.get('name')">
         <ng-template let-variant="item">
-            <a class="button-ghost" [routerLink]="['./', variant.id]"
+            <a class="button-ghost" [routerLink]="['/catalog/products', variant.productId, 'variants', variant.id]"
                 ><span>{{ variant.name }}</span
                 ><clr-icon shape="arrow right"
             /></a>

+ 54 - 103
packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.component.ts

@@ -1,26 +1,6 @@
 import { Component, Input, OnInit } from '@angular/core';
-import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import {
-    BaseListComponent,
-    DataService,
-    GetProductVariantListQuery,
-    GetProductVariantListQueryVariables,
-    ItemOf,
-    JobQueueService,
-    LanguageCode,
-    ModalService,
-    NavBuilderService,
-    NotificationService,
-    ProductFilterParameter,
-    ProductSortParameter,
-    ProductVariantFilterParameter,
-    ProductVariantSortParameter,
-    ServerConfigService,
-} from '@vendure/admin-ui/core';
-import { Observable } from 'rxjs';
-import { tap } from 'rxjs/operators';
-import { DataTableService } from '../../../../core/src/providers/data-table/data-table.service';
+import { ProductVariantListQueryDocument, TypedBaseListComponent } from '@vendure/admin-ui/core';
 
 @Component({
     selector: 'vdr-product-variant-list',
@@ -28,81 +8,66 @@ import { DataTableService } from '../../../../core/src/providers/data-table/data
     styleUrls: ['./product-variant-list.component.scss'],
 })
 export class ProductVariantListComponent
-    extends BaseListComponent<
-        GetProductVariantListQuery,
-        ItemOf<GetProductVariantListQuery, 'productVariants'>,
-        GetProductVariantListQueryVariables
-    >
+    extends TypedBaseListComponent<typeof ProductVariantListQueryDocument, 'productVariants'>
     implements OnInit
 {
     @Input() productId?: string;
     @Input() hideLanguageSelect = false;
-    availableLanguages$: Observable<LanguageCode[]>;
-    contentLanguage$: Observable<LanguageCode>;
-    readonly filters = this.dataTableService
-        .createFilterCollection<ProductVariantFilterParameter>()
+
+    readonly filters = this.createFilterCollection()
         .addDateFilters()
-        .addFilter({
-            name: 'id',
-            type: { kind: 'text' },
-            label: _('common.id'),
-            filterField: 'id',
-        })
-        .addFilter({
-            name: 'enabled',
-            type: { kind: 'boolean' },
-            label: _('common.enabled'),
-            filterField: 'enabled',
-        })
-        .addFilter({
-            name: 'sku',
-            type: { kind: 'text' },
-            label: _('catalog.sku'),
-            filterField: 'sku',
-        })
-        .addFilter({
-            name: 'price',
-            type: { kind: 'number', inputType: 'currency' },
-            label: _('common.price'),
-            filterField: 'price',
-        })
-        .addFilter({
-            name: 'priceWithTax',
-            type: { kind: 'number', inputType: 'currency' },
-            label: _('common.price-with-tax'),
-            filterField: 'priceWithTax',
-        })
+        .addFilters([
+            {
+                name: 'id',
+                type: { kind: 'text' },
+                label: _('common.id'),
+                filterField: 'id',
+            },
+            {
+                name: 'enabled',
+                type: { kind: 'boolean' },
+                label: _('common.enabled'),
+                filterField: 'enabled',
+            },
+            {
+                name: 'sku',
+                type: { kind: 'text' },
+                label: _('catalog.sku'),
+                filterField: 'sku',
+            },
+            {
+                name: 'price',
+                type: { kind: 'number', inputType: 'currency' },
+                label: _('common.price'),
+                filterField: 'price',
+            },
+            {
+                name: 'priceWithTax',
+                type: { kind: 'number', inputType: 'currency' },
+                label: _('common.price-with-tax'),
+                filterField: 'priceWithTax',
+            },
+        ])
         .connectToRoute(this.route);
 
-    readonly sorts = this.dataTableService
-        .createSortCollection<ProductVariantSortParameter>()
-        .defaultSort('createdAt', 'DESC')
-        .addSort({ name: 'id' })
-        .addSort({ name: 'createdAt' })
-        .addSort({ name: 'updatedAt' })
-        .addSort({ name: 'name' })
-        .addSort({ name: 'sku' })
-        .addSort({ name: 'price' })
-        .addSort({ name: 'priceWithTax' })
+    readonly sorts = this.createSortCollection()
+        .addSorts([
+            { name: 'id' },
+            { name: 'createdAt' },
+            { name: 'updatedAt' },
+            { name: 'name' },
+            { name: 'sku' },
+            { name: 'price' },
+            { name: 'priceWithTax' },
+        ])
         .connectToRoute(this.route);
 
-    constructor(
-        private dataService: DataService,
-        private modalService: ModalService,
-        private notificationService: NotificationService,
-        private jobQueueService: JobQueueService,
-        private serverConfigService: ServerConfigService,
-        private dataTableService: DataTableService,
-        private navBuilderService: NavBuilderService,
-        router: Router,
-        route: ActivatedRoute,
-    ) {
-        super(router, route);
-        super.setQueryFn(
-            (args: any) => this.dataService.product.getProductVariants(args).refetchOnChannelChange(),
-            data => data.productVariants,
-            // eslint-disable-next-line @typescript-eslint/no-shadow
-            (skip, take) => ({
+    constructor() {
+        super();
+        this.configure({
+            document: ProductVariantListQueryDocument,
+            getItems: data => data.productVariants,
+            setVariables: (skip, take) => ({
                 options: {
                     skip,
                     take,
@@ -116,21 +81,7 @@ export class ProductVariantListComponent
                     sort: this.sorts.createSortInput(),
                 },
             }),
-        );
-    }
-
-    ngOnInit() {
-        super.ngOnInit();
-
-        this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
-        this.contentLanguage$ = this.dataService.client
-            .uiState()
-            .mapStream(({ uiState }) => uiState.contentLanguage)
-            .pipe(tap(() => this.refresh()));
-        super.refreshListOnChanges(this.contentLanguage$, this.filters.valueChanges, this.sorts.valueChanges);
-    }
-
-    setLanguage(code: LanguageCode) {
-        this.dataService.client.setContentLanguage(code).subscribe();
+            refreshListOnChanges: [this.sorts.valueChanges, this.filters.valueChanges],
+        });
     }
 }

+ 44 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variant-list/product-variant-list.graphql.ts

@@ -0,0 +1,44 @@
+import { ASSET_FRAGMENT } from '@vendure/admin-ui/core';
+import { gql } from 'apollo-angular';
+
+export const PRODUCT_VARIANT_LIST_QUERY = gql`
+    query ProductVariantListQuery($options: ProductVariantListOptions!) {
+        productVariants(options: $options) {
+            items {
+                id
+                createdAt
+                updatedAt
+                productId
+                enabled
+                languageCode
+                name
+                price
+                currencyCode
+                priceWithTax
+                trackInventory
+                outOfStockThreshold
+                stockLevels {
+                    id
+                    createdAt
+                    updatedAt
+                    stockLocationId
+                    stockOnHand
+                    stockAllocated
+                    stockLocation {
+                        id
+                        createdAt
+                        updatedAt
+                        name
+                    }
+                }
+                useGlobalOutOfStockThreshold
+                sku
+                featuredAsset {
+                    ...Asset
+                }
+            }
+            totalItems
+        }
+    }
+    ${ASSET_FRAGMENT}
+`;

+ 80 - 2
packages/admin-ui/src/lib/core/src/common/base-detail.component.ts

@@ -1,10 +1,14 @@
+import { inject, Type } from '@angular/core';
 import { AbstractControl, UntypedFormGroup } from '@angular/forms';
-import { ActivatedRoute, Router } from '@angular/router';
+import { ActivatedRoute, ActivationStart, ResolveFn, Router } from '@angular/router';
+import { ResultOf, TypedDocumentNode } from '@graphql-typed-document-node/core';
+import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { combineLatest, Observable, of, Subject } from 'rxjs';
-import { distinctUntilChanged, map, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
+import { distinctUntilChanged, filter, map, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
 
 import { DataService } from '../data/providers/data.service';
 import { ServerConfigService } from '../data/server-config';
+import { BreadcrumbValue } from '../providers/breadcrumb/breadcrumb.service';
 
 import { DeactivateAware } from './deactivate-aware';
 import { CustomFieldConfig, CustomFields, LanguageCode } from './generated-types';
@@ -73,6 +77,10 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
             tap(entity => (this.id = entity.id)),
             shareReplay(1),
         );
+        this.setUpStreams();
+    }
+
+    protected setUpStreams() {
         this.isNew$ = this.entity$.pipe(
             map(entity => entity.id === ''),
             shareReplay(1),
@@ -155,3 +163,73 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt
         );
     }
 }
+
+export abstract class TypedBaseDetailComponent<
+    T extends TypedDocumentNode<any, any>,
+    Field extends keyof ResultOf<T>,
+> extends BaseDetailComponent<NonNullable<ResultOf<T>[Field]>> {
+    protected result$: Observable<ResultOf<T>>;
+    override init() {
+        this.entity$ = this.route.data.pipe(
+            switchMap(data =>
+                (data.detail.entity as Observable<ResultOf<T>[Field]>).pipe(takeUntil(this.destroy$)),
+            ),
+            tap(entity => (this.id = entity.id)),
+            shareReplay(1),
+        );
+        this.result$ = this.route.data.pipe(
+            map(data => data.detail.result),
+            shareReplay(1),
+        );
+        this.setUpStreams();
+    }
+}
+
+export function detailComponentWithResolver<
+    T extends TypedDocumentNode<any, { id: string }>,
+    Field extends keyof ResultOf<T>,
+>(config: {
+    component: Type<TypedBaseDetailComponent<T, Field>>;
+    query: T;
+    getEntity: (result: ResultOf<T>) => ResultOf<T>[Field];
+    getBreadcrumbs?: (entity: ResultOf<T>) => BreadcrumbValue;
+}) {
+    const resolveFn: ResolveFn<{ entity: Observable<ResultOf<T>[Field] | null>; result?: ResultOf<T> }> = (
+        route,
+        state,
+    ) => {
+        const router = inject(Router);
+        const dataService = inject(DataService);
+        const id = route.paramMap.get('id');
+
+        // Complete the entity stream upon navigating away
+        const navigateAway$ = router.events.pipe(filter(event => event instanceof ActivationStart));
+
+        if (id == null) {
+            throw new Error('No id found in route');
+        }
+        if (id === 'create') {
+            return of({ entity: of(null) });
+        } else {
+            const result$ = dataService
+                .query(config.query, { id })
+                .stream$.pipe(takeUntil(navigateAway$), shareReplay(1));
+            const entity$ = result$.pipe(map(result => config.getEntity(result as any)));
+            const entityStream$ = entity$.pipe(
+                switchMap(raw => entity$),
+                filter(notNullOrUndefined),
+            );
+            return result$.pipe(
+                map(result => ({
+                    entity: entityStream$,
+                    result,
+                })),
+            );
+        }
+    };
+    return {
+        resolveFn,
+        breadcrumbFn: (result: any) => config.getBreadcrumbs?.(result) ?? ([] as BreadcrumbValue[]),
+        component: config.component,
+    };
+}

+ 69 - 3
packages/admin-ui/src/lib/core/src/common/base-list.component.ts

@@ -1,11 +1,16 @@
-import { Directive, OnDestroy, OnInit } from '@angular/core';
+import { Directive, inject, OnDestroy, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router';
+import { ResultOf, TypedDocumentNode, VariablesOf } from '@graphql-typed-document-node/core';
 import { BehaviorSubject, combineLatest, merge, Observable, Subject } from 'rxjs';
-import { debounceTime, distinctUntilChanged, filter, map, shareReplay, takeUntil } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, filter, map, shareReplay, takeUntil, tap } from 'rxjs/operators';
+import { DataService } from '../data/providers/data.service';
 
 import { QueryResult } from '../data/query-result';
-import { GetFacetListQuery } from './generated-types';
+import { ServerConfigService } from '../data/server-config';
+import { DataTableFilterCollection } from '../providers/data-table/data-table-filter-collection';
+import { DataTableSortCollection } from '../providers/data-table/data-table-sort-collection';
+import { CustomFieldConfig, CustomFields, LanguageCode } from './generated-types';
 import { SelectionManager } from './utilities/selection-manager';
 
 export type ListQueryFn<R> = (take: number, skip: number, ...args: any[]) => QueryResult<R, any>;
@@ -239,3 +244,64 @@ export class BaseListComponent<ResultType, ItemType, VariableType extends Record
         });
     }
 }
+
+@Directive()
+export class TypedBaseListComponent<
+        T extends TypedDocumentNode<any, Vars>,
+        Field extends keyof ResultOf<T>,
+        Vars extends { options: { filter: any; sort: any } } = VariablesOf<T>,
+    >
+    extends BaseListComponent<ResultOf<T>, ItemOf<ResultOf<T>, Field>, VariablesOf<T>>
+    implements OnInit
+{
+    availableLanguages$: Observable<LanguageCode[]>;
+    contentLanguage$: Observable<LanguageCode>;
+
+    protected dataService = inject(DataService);
+    protected router = inject(Router);
+    protected serverConfigService = inject(ServerConfigService);
+    private refreshStreams: Array<Observable<any>> = [];
+    constructor() {
+        super(inject(Router), inject(ActivatedRoute));
+    }
+
+    protected configure(config: {
+        document: T;
+        getItems: (data: ResultOf<T>) => { items: Array<ItemOf<ResultOf<T>, Field>>; totalItems: number };
+        setVariables?: (skip: number, take: number) => VariablesOf<T>;
+        refreshListOnChanges?: Array<Observable<any>>;
+    }) {
+        super.setQueryFn(
+            (args: any) => this.dataService.query(config.document).refetchOnChannelChange(),
+            data => config.getItems(data),
+            (skip, take) => config.setVariables?.(skip, take) ?? ({} as any),
+        );
+        this.availableLanguages$ = this.serverConfigService.getAvailableLanguages();
+        this.contentLanguage$ = this.dataService.client
+            .uiState()
+            .mapStream(({ uiState }) => uiState.contentLanguage)
+            .pipe(tap(() => this.refresh()));
+        this.refreshStreams = config.refreshListOnChanges ?? [];
+    }
+
+    ngOnInit() {
+        super.ngOnInit();
+        super.refreshListOnChanges(this.contentLanguage$, ...this.refreshStreams);
+    }
+
+    createFilterCollection(): DataTableFilterCollection<NonNullable<NonNullable<Vars['options']>['filter']>> {
+        return new DataTableFilterCollection<NonNullable<Vars['options']['filter']>>(this.router);
+    }
+
+    createSortCollection(): DataTableSortCollection<NonNullable<NonNullable<Vars['options']>['sort']>> {
+        return new DataTableSortCollection<NonNullable<Vars['options']['sort']>>(this.router);
+    }
+
+    setLanguage(code: LanguageCode) {
+        this.dataService.client.setContentLanguage(code).subscribe();
+    }
+
+    getCustomFieldConfig(key: Exclude<keyof CustomFields, '__typename'>): CustomFieldConfig[] {
+        return this.serverConfigService.getCustomFieldsFor(key);
+    }
+}

+ 2 - 0
packages/admin-ui/src/lib/core/src/common/component-registry-types.ts

@@ -67,6 +67,7 @@ export type PageLocationId =
     | 'payment-method-list'
     | 'product-detail'
     | 'product-list'
+    | 'product-variant-detail'
     | 'promotion-detail'
     | 'promotion-list'
     | 'role-detail'
@@ -107,6 +108,7 @@ export type CustomDetailComponentLocationId =
     | 'order-detail'
     | 'payment-method-detail'
     | 'product-detail'
+    | 'product-variant-detail'
     | 'promotion-detail'
     | 'seller-detail'
     | 'shipping-method-detail'

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 3 - 3
packages/admin-ui/src/lib/core/src/common/generated-types.ts


+ 321 - 238
packages/admin-ui/src/lib/core/src/common/introspection-result.ts

@@ -1,241 +1,324 @@
 /* eslint-disable */
 
-export interface PossibleTypesResultData {
-    possibleTypes: {
-        [key: string]: string[];
-    };
-}
-const result: PossibleTypesResultData = {
-    possibleTypes: {
-        AddFulfillmentToOrderResult: [
-            'CreateFulfillmentError',
-            'EmptyOrderLineSelectionError',
-            'Fulfillment',
-            'FulfillmentStateTransitionError',
-            'InsufficientStockOnHandError',
-            'InvalidFulfillmentHandlerError',
-            'ItemsAlreadyFulfilledError',
-        ],
-        AddManualPaymentToOrderResult: ['ManualPaymentStateError', 'Order'],
-        ApplyCouponCodeResult: [
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'Order',
-        ],
-        AuthenticationResult: ['CurrentUser', 'InvalidCredentialsError'],
-        CancelOrderResult: [
-            'CancelActiveOrderError',
-            'EmptyOrderLineSelectionError',
-            'MultipleOrderError',
-            'Order',
-            'OrderStateTransitionError',
-            'QuantityTooGreatError',
-        ],
-        CancelPaymentResult: ['CancelPaymentError', 'Payment', 'PaymentStateTransitionError'],
-        CreateAssetResult: ['Asset', 'MimeTypeError'],
-        CreateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        CreateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        CreatePromotionResult: ['MissingConditionsError', 'Promotion'],
-        CustomField: [
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'LocaleTextCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'StringCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        CustomFieldConfig: [
-            'BooleanCustomFieldConfig',
-            'DateTimeCustomFieldConfig',
-            'FloatCustomFieldConfig',
-            'IntCustomFieldConfig',
-            'LocaleStringCustomFieldConfig',
-            'LocaleTextCustomFieldConfig',
-            'RelationCustomFieldConfig',
-            'StringCustomFieldConfig',
-            'TextCustomFieldConfig',
-        ],
-        ErrorResult: [
-            'AlreadyRefundedError',
-            'CancelActiveOrderError',
-            'CancelPaymentError',
-            'ChannelDefaultLanguageError',
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'CreateFulfillmentError',
-            'EmailAddressConflictError',
-            'EmptyOrderLineSelectionError',
-            'FacetInUseError',
-            'FulfillmentStateTransitionError',
-            'GuestCheckoutError',
-            'IneligibleShippingMethodError',
-            'InsufficientStockError',
-            'InsufficientStockOnHandError',
-            'InvalidCredentialsError',
-            'InvalidFulfillmentHandlerError',
-            'ItemsAlreadyFulfilledError',
-            'LanguageNotAvailableError',
-            'ManualPaymentStateError',
-            'MimeTypeError',
-            'MissingConditionsError',
-            'MultipleOrderError',
-            'NativeAuthStrategyError',
-            'NegativeQuantityError',
-            'NoActiveOrderError',
-            'NoChangesSpecifiedError',
-            'NothingToRefundError',
-            'OrderLimitError',
-            'OrderModificationError',
-            'OrderModificationStateError',
-            'OrderStateTransitionError',
-            'PaymentMethodMissingError',
-            'PaymentOrderMismatchError',
-            'PaymentStateTransitionError',
-            'ProductOptionInUseError',
-            'QuantityTooGreatError',
-            'RefundOrderStateError',
-            'RefundPaymentIdMissingError',
-            'RefundStateTransitionError',
-            'SettlePaymentError',
-        ],
-        ModifyOrderResult: [
-            'CouponCodeExpiredError',
-            'CouponCodeInvalidError',
-            'CouponCodeLimitError',
-            'InsufficientStockError',
-            'NegativeQuantityError',
-            'NoChangesSpecifiedError',
-            'Order',
-            'OrderLimitError',
-            'OrderModificationStateError',
-            'PaymentMethodMissingError',
-            'RefundPaymentIdMissingError',
-        ],
-        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',
-            'Province',
-            'Refund',
-            'Release',
-            'Return',
-            'Role',
-            'Sale',
-            'Seller',
-            'ShippingMethod',
-            'StockAdjustment',
-            'StockLevel',
-            'StockLocation',
-            'Surcharge',
-            'Tag',
-            'TaxCategory',
-            'TaxRate',
-            'User',
-            'Zone',
-        ],
-        PaginatedList: [
-            'AdministratorList',
-            'AssetList',
-            'ChannelList',
-            'CollectionList',
-            'CountryList',
-            'CustomerGroupList',
-            'CustomerList',
-            'FacetList',
-            'FacetValueList',
-            'HistoryEntryList',
-            'JobList',
-            'OrderList',
-            'PaymentMethodList',
-            'ProductList',
-            'ProductVariantList',
-            'PromotionList',
-            'ProvinceList',
-            'RoleList',
-            'SellerList',
-            'ShippingMethodList',
-            'StockLocationList',
-            'TagList',
-            'TaxCategoryList',
-            'TaxRateList',
-            'ZoneList',
-        ],
-        RefundOrderResult: [
-            'AlreadyRefundedError',
-            'MultipleOrderError',
-            'NothingToRefundError',
-            'OrderStateTransitionError',
-            'PaymentOrderMismatchError',
-            'QuantityTooGreatError',
-            'Refund',
-            'RefundOrderStateError',
-            'RefundStateTransitionError',
-        ],
-        Region: ['Country', 'Province'],
-        RemoveFacetFromChannelResult: ['Facet', 'FacetInUseError'],
-        RemoveOptionGroupFromProductResult: ['Product', 'ProductOptionInUseError'],
-        RemoveOrderItemsResult: ['Order', 'OrderModificationError'],
-        SearchResultPrice: ['PriceRange', 'SinglePrice'],
-        SetCustomerForDraftOrderResult: ['EmailAddressConflictError', 'Order'],
-        SetOrderShippingMethodResult: [
-            'IneligibleShippingMethodError',
-            'NoActiveOrderError',
-            'Order',
-            'OrderModificationError',
-        ],
-        SettlePaymentResult: [
-            'OrderStateTransitionError',
-            'Payment',
-            'PaymentStateTransitionError',
-            'SettlePaymentError',
-        ],
-        SettleRefundResult: ['Refund', 'RefundStateTransitionError'],
-        StockMovement: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
-        StockMovementItem: ['Allocation', 'Cancellation', 'Release', 'Return', 'Sale', 'StockAdjustment'],
-        TransitionFulfillmentToStateResult: ['Fulfillment', 'FulfillmentStateTransitionError'],
-        TransitionOrderToStateResult: ['Order', 'OrderStateTransitionError'],
-        TransitionPaymentToStateResult: ['Payment', 'PaymentStateTransitionError'],
-        UpdateChannelResult: ['Channel', 'LanguageNotAvailableError'],
-        UpdateCustomerResult: ['Customer', 'EmailAddressConflictError'],
-        UpdateGlobalSettingsResult: ['ChannelDefaultLanguageError', 'GlobalSettings'],
-        UpdateOrderItemsResult: [
-            'InsufficientStockError',
-            'NegativeQuantityError',
-            'Order',
-            'OrderLimitError',
-            'OrderModificationError',
-        ],
-        UpdatePromotionResult: ['MissingConditionsError', 'Promotion'],
-    },
+      export interface PossibleTypesResultData {
+        possibleTypes: {
+          [key: string]: string[]
+        }
+      }
+      const result: PossibleTypesResultData = {
+  "possibleTypes": {
+    "AddFulfillmentToOrderResult": [
+      "CreateFulfillmentError",
+      "EmptyOrderLineSelectionError",
+      "Fulfillment",
+      "FulfillmentStateTransitionError",
+      "InsufficientStockOnHandError",
+      "InvalidFulfillmentHandlerError",
+      "ItemsAlreadyFulfilledError"
+    ],
+    "AddManualPaymentToOrderResult": [
+      "ManualPaymentStateError",
+      "Order"
+    ],
+    "ApplyCouponCodeResult": [
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "Order"
+    ],
+    "AuthenticationResult": [
+      "CurrentUser",
+      "InvalidCredentialsError"
+    ],
+    "CancelOrderResult": [
+      "CancelActiveOrderError",
+      "EmptyOrderLineSelectionError",
+      "MultipleOrderError",
+      "Order",
+      "OrderStateTransitionError",
+      "QuantityTooGreatError"
+    ],
+    "CancelPaymentResult": [
+      "CancelPaymentError",
+      "Payment",
+      "PaymentStateTransitionError"
+    ],
+    "CreateAssetResult": [
+      "Asset",
+      "MimeTypeError"
+    ],
+    "CreateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "CreateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "CreatePromotionResult": [
+      "MissingConditionsError",
+      "Promotion"
+    ],
+    "CustomField": [
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "LocaleTextCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "StringCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "CustomFieldConfig": [
+      "BooleanCustomFieldConfig",
+      "DateTimeCustomFieldConfig",
+      "FloatCustomFieldConfig",
+      "IntCustomFieldConfig",
+      "LocaleStringCustomFieldConfig",
+      "LocaleTextCustomFieldConfig",
+      "RelationCustomFieldConfig",
+      "StringCustomFieldConfig",
+      "TextCustomFieldConfig"
+    ],
+    "ErrorResult": [
+      "AlreadyRefundedError",
+      "CancelActiveOrderError",
+      "CancelPaymentError",
+      "ChannelDefaultLanguageError",
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "CreateFulfillmentError",
+      "EmailAddressConflictError",
+      "EmptyOrderLineSelectionError",
+      "FacetInUseError",
+      "FulfillmentStateTransitionError",
+      "GuestCheckoutError",
+      "IneligibleShippingMethodError",
+      "InsufficientStockError",
+      "InsufficientStockOnHandError",
+      "InvalidCredentialsError",
+      "InvalidFulfillmentHandlerError",
+      "ItemsAlreadyFulfilledError",
+      "LanguageNotAvailableError",
+      "ManualPaymentStateError",
+      "MimeTypeError",
+      "MissingConditionsError",
+      "MultipleOrderError",
+      "NativeAuthStrategyError",
+      "NegativeQuantityError",
+      "NoActiveOrderError",
+      "NoChangesSpecifiedError",
+      "NothingToRefundError",
+      "OrderLimitError",
+      "OrderModificationError",
+      "OrderModificationStateError",
+      "OrderStateTransitionError",
+      "PaymentMethodMissingError",
+      "PaymentOrderMismatchError",
+      "PaymentStateTransitionError",
+      "ProductOptionInUseError",
+      "QuantityTooGreatError",
+      "RefundOrderStateError",
+      "RefundPaymentIdMissingError",
+      "RefundStateTransitionError",
+      "SettlePaymentError"
+    ],
+    "ModifyOrderResult": [
+      "CouponCodeExpiredError",
+      "CouponCodeInvalidError",
+      "CouponCodeLimitError",
+      "InsufficientStockError",
+      "NegativeQuantityError",
+      "NoChangesSpecifiedError",
+      "Order",
+      "OrderLimitError",
+      "OrderModificationStateError",
+      "PaymentMethodMissingError",
+      "RefundPaymentIdMissingError"
+    ],
+    "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",
+      "Province",
+      "Refund",
+      "Release",
+      "Return",
+      "Role",
+      "Sale",
+      "Seller",
+      "ShippingMethod",
+      "StockAdjustment",
+      "StockLevel",
+      "StockLocation",
+      "Surcharge",
+      "Tag",
+      "TaxCategory",
+      "TaxRate",
+      "User",
+      "Zone"
+    ],
+    "PaginatedList": [
+      "AdministratorList",
+      "AssetList",
+      "ChannelList",
+      "CollectionList",
+      "CountryList",
+      "CustomerGroupList",
+      "CustomerList",
+      "FacetList",
+      "FacetValueList",
+      "HistoryEntryList",
+      "JobList",
+      "OrderList",
+      "PaymentMethodList",
+      "ProductList",
+      "ProductVariantList",
+      "PromotionList",
+      "ProvinceList",
+      "RoleList",
+      "SellerList",
+      "ShippingMethodList",
+      "StockLocationList",
+      "TagList",
+      "TaxCategoryList",
+      "TaxRateList",
+      "ZoneList"
+    ],
+    "RefundOrderResult": [
+      "AlreadyRefundedError",
+      "MultipleOrderError",
+      "NothingToRefundError",
+      "OrderStateTransitionError",
+      "PaymentOrderMismatchError",
+      "QuantityTooGreatError",
+      "Refund",
+      "RefundOrderStateError",
+      "RefundStateTransitionError"
+    ],
+    "Region": [
+      "Country",
+      "Province"
+    ],
+    "RemoveFacetFromChannelResult": [
+      "Facet",
+      "FacetInUseError"
+    ],
+    "RemoveOptionGroupFromProductResult": [
+      "Product",
+      "ProductOptionInUseError"
+    ],
+    "RemoveOrderItemsResult": [
+      "Order",
+      "OrderModificationError"
+    ],
+    "SearchResultPrice": [
+      "PriceRange",
+      "SinglePrice"
+    ],
+    "SetCustomerForDraftOrderResult": [
+      "EmailAddressConflictError",
+      "Order"
+    ],
+    "SetOrderShippingMethodResult": [
+      "IneligibleShippingMethodError",
+      "NoActiveOrderError",
+      "Order",
+      "OrderModificationError"
+    ],
+    "SettlePaymentResult": [
+      "OrderStateTransitionError",
+      "Payment",
+      "PaymentStateTransitionError",
+      "SettlePaymentError"
+    ],
+    "SettleRefundResult": [
+      "Refund",
+      "RefundStateTransitionError"
+    ],
+    "StockMovement": [
+      "Allocation",
+      "Cancellation",
+      "Release",
+      "Return",
+      "Sale",
+      "StockAdjustment"
+    ],
+    "StockMovementItem": [
+      "Allocation",
+      "Cancellation",
+      "Release",
+      "Return",
+      "Sale",
+      "StockAdjustment"
+    ],
+    "TransitionFulfillmentToStateResult": [
+      "Fulfillment",
+      "FulfillmentStateTransitionError"
+    ],
+    "TransitionOrderToStateResult": [
+      "Order",
+      "OrderStateTransitionError"
+    ],
+    "TransitionPaymentToStateResult": [
+      "Payment",
+      "PaymentStateTransitionError"
+    ],
+    "UpdateChannelResult": [
+      "Channel",
+      "LanguageNotAvailableError"
+    ],
+    "UpdateCustomerResult": [
+      "Customer",
+      "EmailAddressConflictError"
+    ],
+    "UpdateGlobalSettingsResult": [
+      "ChannelDefaultLanguageError",
+      "GlobalSettings"
+    ],
+    "UpdateOrderItemsResult": [
+      "InsufficientStockError",
+      "NegativeQuantityError",
+      "Order",
+      "OrderLimitError",
+      "OrderModificationError"
+    ],
+    "UpdatePromotionResult": [
+      "MissingConditionsError",
+      "Promotion"
+    ]
+  }
 };
-export default result;
+      export default result;
+    

+ 2 - 1
packages/admin-ui/src/lib/core/src/data/providers/base-data.service.ts

@@ -1,6 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { MutationUpdaterFn, SingleExecutionResult, WatchQueryFetchPolicy } from '@apollo/client/core';
+import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 import { Apollo } from 'apollo-angular';
 import { DocumentNode } from 'graphql/language/ast';
@@ -33,7 +34,7 @@ export class BaseDataService {
      * Performs a GraphQL watch query
      */
     query<T, V extends Record<string, any> = Record<string, any>>(
-        query: DocumentNode,
+        query: DocumentNode | TypedDocumentNode<T, V>,
         variables?: V,
         fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network',
     ): QueryResult<T, V> {

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

@@ -1,5 +1,6 @@
 import { Injectable } from '@angular/core';
 import { MutationUpdaterFn, WatchQueryFetchPolicy } from '@apollo/client/core';
+import { TypedDocumentNode } from '@graphql-typed-document-node/core';
 import { DocumentNode } from 'graphql';
 import { Observable } from 'rxjs';
 
@@ -78,7 +79,7 @@ export class DataService {
      * ```
      */
     query<T, V extends Record<string, any> = Record<string, any>>(
-        query: DocumentNode,
+        query: DocumentNode | TypedDocumentNode<T, V>,
         variables?: V,
         fetchPolicy: WatchQueryFetchPolicy = 'cache-and-network',
     ): QueryResult<T, V> {

+ 1 - 1
packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts

@@ -1,7 +1,7 @@
 import { pick } from '@vendure/common/lib/pick';
 
-import { SortOrder } from '../../common/generated-types';
 import * as Codegen from '../../common/generated-types';
+import { SortOrder } from '../../common/generated-types';
 import {
     ADD_OPTION_GROUP_TO_PRODUCT,
     ADD_OPTION_TO_GROUP,

+ 9 - 0
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-filter-collection.ts

@@ -103,6 +103,15 @@ export class DataTableFilterCollection<FilterInput extends Record<string, any> =
         return this;
     }
 
+    addFilters<FilterType extends DataTableFilterType>(
+        configs: Array<DataTableFilterOptions<FilterInput, FilterType>>,
+    ): DataTableFilterCollection<FilterInput> {
+        for (const config of configs) {
+            this.addFilter(config);
+        }
+        return this;
+    }
+
     addDateFilters(): FilterInput extends {
         createdAt?: DateOperators | null;
         updatedAt?: DateOperators | null;

+ 9 - 0
packages/admin-ui/src/lib/core/src/providers/data-table/data-table-sort-collection.ts

@@ -34,6 +34,15 @@ export class DataTableSortCollection<
         return this as unknown as DataTableSortCollection<SortInput, [...Names, Name]>;
     }
 
+    addSorts<Name extends keyof SortInput>(
+        configs: Array<DataTableSortOptions<SortInput, Name>>,
+    ): DataTableSortCollection<SortInput, [...Names, Name]> {
+        for (const config of configs) {
+            this.addSort(config);
+        }
+        return this as unknown as DataTableSortCollection<SortInput, [...Names, Name]>;
+    }
+
     addCustomFieldSorts(customFields: CustomFieldConfig[]) {
         for (const config of customFields) {
             const type = config.type as CustomFieldType;

+ 26 - 9
packages/admin-ui/src/lib/core/src/providers/page/page.service.ts

@@ -1,6 +1,7 @@
 import { Injectable, Type } from '@angular/core';
 import { Route } from '@angular/router';
-import { BaseDetailComponent } from '../../common/base-detail.component';
+import { map } from 'rxjs/operators';
+import { BaseDetailComponent, detailComponentWithResolver } from '../../common/base-detail.component';
 import { PageLocationId } from '../../common/component-registry-types';
 import { CanDeactivateDetailGuard } from '../../shared/providers/routing/can-deactivate-detail-guard';
 
@@ -9,7 +10,7 @@ export interface PageTabConfig {
     tabIcon?: string;
     route: string;
     tab: string;
-    component: Type<any>;
+    component: Type<any> | ReturnType<typeof detailComponentWithResolver>;
 }
 
 @Injectable({
@@ -33,16 +34,28 @@ export class PageService {
     getPageTabRoutes(location: PageLocationId): Route[] {
         const configs = this.registry.get(location) || [];
         return configs.map(config => {
-            const guards =
-                typeof config.component.prototype.canDeactivate === 'function'
-                    ? [CanDeactivateDetailGuard]
-                    : [];
-            return {
+            const route: Route = {
                 path: config.route || '',
                 pathMatch: config.route ? 'prefix' : 'full',
-                component: config.component,
-                canDeactivate: guards,
             };
+
+            let component: Type<any>;
+            if (isComponentWithResolver(config.component)) {
+                const { component: cmp, breadcrumbFn, resolveFn } = config.component;
+                component = cmp;
+                route.resolve = { detail: config.component.resolveFn };
+                route.data = {
+                    breadcrumb: data => breadcrumbFn(data.detail.result),
+                };
+            } else {
+                component = config.component;
+            }
+            const guards =
+                typeof component.prototype.canDeactivate === 'function' ? [CanDeactivateDetailGuard] : [];
+            route.component = component;
+            route.canDeactivate = guards;
+
+            return route;
         });
     }
 
@@ -50,3 +63,7 @@ export class PageService {
         return this.registry.get(location) || [];
     }
 }
+
+function isComponentWithResolver(input: any): input is ReturnType<typeof detailComponentWithResolver> {
+    return input && input.hasOwnProperty('resolveFn');
+}

+ 12 - 6
packages/admin-ui/src/lib/core/src/shared/components/card/card.component.scss

@@ -1,22 +1,28 @@
 :host {
     display: block;
+    --card-padding: calc(var(--space-unit) * 3);
+    container-type: inline-size;
 }
+
 .card-container {
+    @container (max-width: 400px) {
+        --card-padding: calc(var(--space-unit) * 2);
+    }
     border: 1px solid var(--color-card-border);
     border-radius: var(--border-radius);
-    padding: calc(var(--space-unit) * 2) 0;
-    box-shadow: 0px 2px 4px 0px rgba(0,0,0,0.1);
+    padding: var(--card-padding) 0;
+    box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.05);
 
     &.padding-x {
-        padding-left: calc(var(--space-unit) * 2);
-        padding-right: calc(var(--space-unit) * 2);
+        padding-left: var(--card-padding);
+        padding-right: var(--card-padding);
     }
 }
 
 .title {
     font-size: var(--font-size-base);
-    margin-bottom: calc(var(--space-unit) * 2);
-    padding: 0 calc(var(--space-unit) * 2);
+    margin-bottom: var(--card-padding);
+    padding: 0 var(--card-padding);
 }
 .padding-x .title {
     padding: 0;

+ 11 - 6
packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.component.ts

@@ -3,7 +3,10 @@ import { PaginationInstance } from 'ngx-pagination';
 import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
 import { map, tap } from 'rxjs/operators';
 
-import { SearchProductsQuery } from '../../../common/generated-types';
+import {
+    GetProductVariantsForMultiSelectorDocument,
+    SearchProductsQuery,
+} from '../../../common/generated-types';
 import { SelectionManager } from '../../../common/utilities/selection-manager';
 import { DataService } from '../../../data/providers/data.service';
 import { Dialog } from '../../../providers/modal/modal.types';
@@ -95,11 +98,13 @@ export class ProductMultiSelectorDialogComponent implements OnInit, Dialog<Searc
                         this.changeDetector.markForCheck();
                     });
             } else {
-                this.dataService.product
-                    .getProductVariants({
-                        filter: {
-                            id: {
-                                in: this.initialSelectionIds,
+                this.dataService
+                    .query(GetProductVariantsForMultiSelectorDocument, {
+                        options: {
+                            filter: {
+                                id: {
+                                    in: this.initialSelectionIds,
+                                },
                             },
                         },
                     })

+ 44 - 0
packages/admin-ui/src/lib/core/src/shared/components/product-multi-selector-dialog/product-multi-selector-dialog.graphql.ts

@@ -0,0 +1,44 @@
+import { ASSET_FRAGMENT } from '@vendure/admin-ui/core';
+import { gql } from 'apollo-angular';
+
+export const GET_PRODUCT_VARIANTS_FOR_MULTI_SELECTOR = gql`
+    query GetProductVariantsForMultiSelector($options: ProductVariantListOptions!) {
+        productVariants(options: $options) {
+            items {
+                id
+                createdAt
+                updatedAt
+                productId
+                enabled
+                languageCode
+                name
+                price
+                currencyCode
+                priceWithTax
+                trackInventory
+                outOfStockThreshold
+                stockLevels {
+                    id
+                    createdAt
+                    updatedAt
+                    stockLocationId
+                    stockOnHand
+                    stockAllocated
+                    stockLocation {
+                        id
+                        createdAt
+                        updatedAt
+                        name
+                    }
+                }
+                useGlobalOutOfStockThreshold
+                sku
+                featuredAsset {
+                    ...Asset
+                }
+            }
+            totalItems
+        }
+    }
+    ${ASSET_FRAGMENT}
+`;

+ 2 - 9
packages/admin-ui/src/lib/order/src/components/select-address-dialog/select-address-dialog.component.ts

@@ -3,12 +3,10 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
 import {
     AddressFragment,
     CreateAddressInput,
-    CreateCustomerInput,
     DataService,
     Dialog,
     GetAvailableCountriesQuery,
-    GetCustomerAddressesQuery,
-    GetCustomerAddressesQueryVariables,
+    GetCustomerAddressesDocument,
     OrderAddressFragment,
 } from '@vendure/admin-ui/core';
 import { pick } from '@vendure/common/lib/pick';
@@ -17,8 +15,6 @@ import { tap } from 'rxjs/operators';
 
 import { Customer } from '../select-customer-dialog/select-customer-dialog.component';
 
-import { GET_CUSTOMER_ADDRESSES } from './select-address-dialog.graphql';
-
 @Component({
     selector: 'vdr-select-address-dialog',
     templateUrl: './select-address-dialog.component.html',
@@ -53,10 +49,7 @@ export class SelectAddressDialogComponent implements OnInit, Dialog<CreateAddres
         this.useExisting = !!this.customerId;
         this.addresses$ = this.customerId
             ? this.dataService
-                  .query<GetCustomerAddressesQuery, GetCustomerAddressesQueryVariables>(
-                      GET_CUSTOMER_ADDRESSES,
-                      { customerId: this.customerId },
-                  )
+                  .query(GetCustomerAddressesDocument, { customerId: this.customerId })
                   .mapSingle(({ customer }) => customer?.addresses ?? [])
                   .pipe(
                       tap(addresses => {

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/channel-detail/channel-detail.component.ts

@@ -68,7 +68,7 @@ export class ChannelDetailComponent
 
     ngOnInit() {
         this.init();
-        this.zones$ = this.dataService.settings.getZones({ take: 999 }).mapSingle(data => data.zones.items);
+        this.zones$ = this.dataService.settings.getZones({ take: 100 }).mapSingle(data => data.zones.items);
         // TODO: make this lazy-loaded autocomplete
         this.sellers$ = this.dataService.settings.getSellerList().mapSingle(data => data.sellers.items);
         this.availableLanguageCodes$ = this.serverConfigService.getAvailableLanguages();

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است