Browse Source

feat(admin-ui): Implement variant channel assignment controls

Relates to #519
Michael Bromley 5 years ago
parent
commit
83a33b58e8
24 changed files with 333 additions and 68 deletions
  1. 15 15
      packages/admin-ui/i18n-coverage.json
  2. 8 4
      packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.html
  3. 40 15
      packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.ts
  4. 15 15
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  5. 4 0
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss
  6. 60 6
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  7. 21 0
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html
  8. 7 0
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.scss
  9. 10 0
      packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts
  10. 46 0
      packages/admin-ui/src/lib/core/src/common/generated-types.ts
  11. 28 0
      packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts
  12. 24 0
      packages/admin-ui/src/lib/core/src/data/providers/product-data.service.ts
  13. 2 5
      packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.html
  14. 7 2
      packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.ts
  15. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  16. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  17. 6 1
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  18. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  19. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  20. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  21. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  22. 5 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json
  23. 5 5
      packages/dev-server/dev-config.ts
  24. 0 0
      schema-admin.json

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

@@ -1,44 +1,44 @@
 {
-  "generatedOn": "2020-11-20T14:39:19.815Z",
-  "lastCommit": "60fd856bbcf3389d1b488e4f3e51a4eeaca85370",
+  "generatedOn": "2020-11-24T15:53:35.931Z",
+  "lastCommit": "799f30643a3e221e453246534d844bf448b046e5",
   "translationStatus": {
     "cs": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 688,
-      "percentage": 98
+      "percentage": 97
     },
     "de": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 597,
-      "percentage": 85
+      "percentage": 84
     },
     "en": {
-      "tokenCount": 703,
-      "translatedCount": 699,
+      "tokenCount": 708,
+      "translatedCount": 703,
       "percentage": 99
     },
     "es": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 455,
-      "percentage": 65
+      "percentage": 64
     },
     "pl": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 552,
-      "percentage": 79
+      "percentage": 78
     },
     "pt_BR": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 643,
       "percentage": 91
     },
     "zh_Hans": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 536,
       "percentage": 76
     },
     "zh_Hant": {
-      "tokenCount": 703,
+      "tokenCount": 708,
       "translatedCount": 536,
       "percentage": 76
     }

+ 8 - 4
packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.html

@@ -1,4 +1,9 @@
-<ng-template vdrDialogTitle>{{ 'catalog.assign-products-to-channel' | translate }}</ng-template>
+<ng-template vdrDialogTitle>
+    <ng-container *ngIf="isProductVariantMode; else productModeTitle">{{
+        'catalog.assign-variants-to-channel' | translate
+    }}</ng-container>
+    <ng-template #productModeTitle>{{ 'catalog.assign-products-to-channel' | translate }}</ng-template>
+</ng-template>
 
 <div class="flex">
     <clr-input-container>
@@ -7,6 +12,7 @@
             clrInput
             [multiple]="false"
             [includeDefaultChannel]="false"
+            [disableChannelIds]="currentChannelIds"
             [formControl]="selectedChannelIdControl"
         ></vdr-channel-assignment-control>
     </clr-input-container>
@@ -47,9 +53,7 @@
                     <ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
                         {{ row.pricePreview / 100 | currency: selectedChannel?.currencyCode }}
                     </ng-template>
-                    <ng-template #noChannelSelected>
-                        -
-                    </ng-template>
+                    <ng-template #noChannelSelected> - </ng-template>
                 </td>
             </tr>
         </tbody>

+ 40 - 15
packages/admin-ui/src/lib/catalog/src/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component.ts

@@ -1,13 +1,12 @@
 import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
-import { combineLatest, from, Observable } from 'rxjs';
-import { map, startWith, switchMap } from 'rxjs/operators';
-
 import { GetChannels, ProductVariantFragment } from '@vendure/admin-ui/core';
 import { NotificationService } from '@vendure/admin-ui/core';
 import { DataService } from '@vendure/admin-ui/core';
 import { Dialog } from '@vendure/admin-ui/core';
+import { combineLatest, from, Observable } from 'rxjs';
+import { map, startWith, switchMap } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-assign-products-to-channel-dialog',
@@ -26,6 +25,12 @@ export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<an
 
     // assigned by ModalService.fromComponent() call
     productIds: string[];
+    productVariantIds: string[] | undefined;
+    currentChannelIds: string[];
+
+    get isProductVariantMode(): boolean {
+        return this.productVariantIds != null;
+    }
 
     constructor(private dataService: DataService, private notificationService: NotificationService) {}
 
@@ -67,18 +72,33 @@ export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<an
     assign() {
         const selectedChannel = this.selectedChannel;
         if (selectedChannel) {
-            this.dataService.product
-                .assignProductsToChannel({
-                    channelId: selectedChannel.id,
-                    productIds: this.productIds,
-                    priceFactor: +this.priceFactorControl.value,
-                })
-                .subscribe(() => {
-                    this.notificationService.success(_('catalog.assign-product-to-channel-success'), {
-                        channel: selectedChannel.code,
+            if (!this.isProductVariantMode) {
+                this.dataService.product
+                    .assignProductsToChannel({
+                        channelId: selectedChannel.id,
+                        productIds: this.productIds,
+                        priceFactor: +this.priceFactorControl.value,
+                    })
+                    .subscribe(() => {
+                        this.notificationService.success(_('catalog.assign-product-to-channel-success'), {
+                            channel: selectedChannel.code,
+                        });
+                        this.resolveWith(true);
                     });
-                    this.resolveWith(true);
-                });
+            } else if (this.productVariantIds) {
+                this.dataService.product
+                    .assignVariantsToChannel({
+                        channelId: selectedChannel.id,
+                        productVariantIds: this.productVariantIds,
+                        priceFactor: +this.priceFactorControl.value,
+                    })
+                    .subscribe(() => {
+                        this.notificationService.success(_('catalog.assign-variant-to-channel-success'), {
+                            channel: selectedChannel.code,
+                        });
+                        this.resolveWith(true);
+                    });
+            }
         }
     }
 
@@ -92,7 +112,12 @@ export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<an
         for (let i = 0; i < this.productIds.length && variants.length < take; i++) {
             const productVariants = await this.dataService.product
                 .getProduct(this.productIds[i])
-                .mapSingle(({ product }) => product && product.variants)
+                .mapSingle(({ product }) => {
+                    const _variants = product ? product.variants : [];
+                    return _variants.filter(v =>
+                        this.isProductVariantMode ? this.productVariantIds?.includes(v.id) : true,
+                    );
+                })
                 .toPromise();
             variants.push(...(productVariants || []));
         }

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

@@ -65,21 +65,19 @@
                                     [label]="'common.channels' | translate"
                                     *vdrIfDefaultChannelActive
                                 >
-                                    <div class="flex">
-                                        <div class="product-channels">
-                                            <ng-container *ngFor="let channel of productChannels$ | async">
-                                                <vdr-chip
-                                                    *ngIf="!isDefaultChannel(channel.code)"
-                                                    icon="times-circle"
-                                                    (iconClick)="removeFromChannel(channel.id)"
-                                                >
-                                                    <vdr-channel-badge
-                                                        [channelCode]="channel.code"
-                                                    ></vdr-channel-badge>
-                                                    {{ channel.code | channelCodeToLabel }}
-                                                </vdr-chip>
-                                            </ng-container>
-                                        </div>
+                                    <div class="flex channel-assignment">
+                                        <ng-container *ngFor="let channel of productChannels$ | async">
+                                            <vdr-chip
+                                                *ngIf="!isDefaultChannel(channel.code)"
+                                                icon="times-circle"
+                                                (iconClick)="removeFromChannel(channel.id)"
+                                            >
+                                                <vdr-channel-badge
+                                                    [channelCode]="channel.code"
+                                                ></vdr-channel-badge>
+                                                {{ channel.code | channelCodeToLabel }}
+                                            </vdr-chip>
+                                        </ng-container>
                                         <button class="btn btn-sm" (click)="assignToChannel()">
                                             <clr-icon shape="layers"></clr-icon>
                                             {{ 'catalog.assign-to-channel' | translate }}
@@ -225,6 +223,8 @@
                         [customFields]="customVariantFields"
                         [customOptionFields]="customOptionFields"
                         [activeLanguage]="languageCode$ | async"
+                        (assignToChannel)="assignVariantToChannel($event)"
+                        (removeFromChannel)="removeVariantFromChannel($event)"
                         (assetChange)="variantAssetChange($event)"
                         (updateProductOption)="updateProductOption($event)"
                         (selectionChange)="selectedVariantIds = $event"

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

@@ -40,3 +40,7 @@ vdr-action-bar clr-toggle-wrapper {
 .edit-variants-btn {
     margin-top: 0;
 }
+
+.channel-assignment {
+    flex-wrap: wrap;
+}

+ 60 - 6
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -204,20 +204,74 @@ export class ProductDetailComponent
     }
 
     assignToChannel() {
+        this.productChannels$
+            .pipe(
+                take(1),
+                switchMap(channels => {
+                    return this.modalService.fromComponent(AssignProductsToChannelDialogComponent, {
+                        size: 'lg',
+                        locals: {
+                            productIds: [this.id],
+                            currentChannelIds: channels.map(c => c.id),
+                        },
+                    });
+                }),
+            )
+            .subscribe();
+    }
+
+    removeFromChannel(channelId: string) {
         this.modalService
+            .dialog({
+                title: _('catalog.remove-product-from-channel'),
+                buttons: [
+                    { type: 'secondary', label: _('common.cancel') },
+                    { type: 'danger', label: _('catalog.remove-from-channel'), returnValue: true },
+                ],
+            })
+            .pipe(
+                switchMap(response =>
+                    response
+                        ? this.dataService.product.removeProductsFromChannel({
+                              channelId,
+                              productIds: [this.id],
+                          })
+                        : EMPTY,
+                ),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('catalog.notify-remove-product-from-channel-success'));
+                },
+                err => {
+                    this.notificationService.error(_('catalog.notify-remove-product-from-channel-error'));
+                },
+            );
+    }
+
+    assignVariantToChannel(variant: ProductWithVariants.Variants) {
+        return this.modalService
             .fromComponent(AssignProductsToChannelDialogComponent, {
                 size: 'lg',
                 locals: {
                     productIds: [this.id],
+                    productVariantIds: [variant.id],
+                    currentChannelIds: variant.channels.map(c => c.id),
                 },
             })
             .subscribe();
     }
 
-    removeFromChannel(channelId: string) {
+    removeVariantFromChannel({
+        channelId,
+        variant,
+    }: {
+        channelId: string;
+        variant: ProductWithVariants.Variants;
+    }) {
         this.modalService
             .dialog({
-                title: _('catalog.remove-product-from-channel'),
+                title: _('catalog.remove-product-variant-from-channel'),
                 buttons: [
                     { type: 'secondary', label: _('common.cancel') },
                     { type: 'danger', label: _('catalog.remove-from-channel'), returnValue: true },
@@ -226,19 +280,19 @@ export class ProductDetailComponent
             .pipe(
                 switchMap(response =>
                     response
-                        ? this.dataService.product.removeProductsFromChannel({
+                        ? this.dataService.product.removeVariantsFromChannel({
                               channelId,
-                              productIds: [this.id],
+                              productVariantIds: [variant.id],
                           })
                         : EMPTY,
                 ),
             )
             .subscribe(
                 () => {
-                    this.notificationService.success(_('catalog.notify-remove-product-from-channel-success'));
+                    this.notificationService.success(_('catalog.notify-remove-variant-from-channel-success'));
                 },
                 err => {
-                    this.notificationService.error(_('catalog.notify-remove-product-from-channel-error'));
+                    this.notificationService.error(_('catalog.notify-remove-variant-from-channel-error'));
                 },
             );
     }

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

@@ -260,6 +260,27 @@
                     </div>
                 </div>
             </div>
+            <ng-container *vdrIfMultichannel>
+                <div class="card-block" *vdrIfDefaultChannelActive>
+                    <div class="flex channel-assignment">
+                        <ng-container *ngFor="let channel of variant.channels">
+                            <vdr-chip
+                                *ngIf="!isDefaultChannel(channel.code)"
+                                icon="times-circle"
+                                [title]="'catalog.remove-from-channel' | translate"
+                                (iconClick)="removeFromChannel.emit({ channelId: channel.id, variant: variant })"
+                            >
+                                <vdr-channel-badge [channelCode]="channel.code"></vdr-channel-badge>
+                                {{ channel.code | channelCodeToLabel }}
+                            </vdr-chip>
+                        </ng-container>
+                        <button class="btn btn-sm" (click)="assignToChannel.emit(variant)">
+                            <clr-icon shape="layers"></clr-icon>
+                            {{ 'catalog.assign-to-channel' | translate }}
+                        </button>
+                    </div>
+                </div>
+            </ng-container>
         </ng-container>
     </div>
     <div class="table-footer">

+ 7 - 0
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.scss

@@ -131,6 +131,13 @@
     }
 }
 
+.channel-assignment {
+    justify-content: flex-end;
+    .btn {
+        margin: 6px 12px 6px 0;
+    }
+}
+
 .out-of-stock-threshold-wrapper {
     display: flex;
     flex-direction: column;

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

@@ -26,6 +26,7 @@ import {
     TaxCategory,
     UpdateProductOptionInput,
 } from '@vendure/admin-ui/core';
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { PaginationInstance } from 'ngx-pagination';
 import { Subscription } from 'rxjs';
@@ -54,6 +55,11 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     @Input() customFields: CustomFieldConfig[];
     @Input() customOptionFields: CustomFieldConfig[];
     @Input() activeLanguage: LanguageCode;
+    @Output() assignToChannel = new EventEmitter<ProductWithVariants.Variants>();
+    @Output() removeFromChannel = new EventEmitter<{
+        channelId: string;
+        variant: ProductWithVariants.Variants;
+    }>();
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() selectionChange = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
@@ -116,6 +122,10 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
         }
     }
 
+    isDefaultChannel(channelCode: string): boolean {
+        return channelCode === DEFAULT_CHANNEL_CODE;
+    }
+
     trackById(index: number, item: ProductWithVariants.Variants) {
         return item.id;
     }

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

@@ -5517,6 +5517,9 @@ export type ProductVariantFragment = (
   )>, translations: Array<(
     { __typename?: 'ProductVariantTranslation' }
     & Pick<ProductVariantTranslation, 'id' | 'languageCode' | 'name'>
+  )>, channels: Array<(
+    { __typename?: 'Channel' }
+    & Pick<Channel, 'id' | 'code'>
   )> }
 );
 
@@ -5915,6 +5918,20 @@ export type AssignProductsToChannelMutation = { assignProductsToChannel: Array<(
     )> }
   )> };
 
+export type AssignVariantsToChannelMutationVariables = Exact<{
+  input: AssignProductVariantsToChannelInput;
+}>;
+
+
+export type AssignVariantsToChannelMutation = { assignProductVariantsToChannel: Array<(
+    { __typename?: 'ProductVariant' }
+    & Pick<ProductVariant, 'id'>
+    & { channels: Array<(
+      { __typename?: 'Channel' }
+      & Pick<Channel, 'id' | 'code'>
+    )> }
+  )> };
+
 export type RemoveProductsFromChannelMutationVariables = Exact<{
   input: RemoveProductsFromChannelInput;
 }>;
@@ -5929,6 +5946,20 @@ export type RemoveProductsFromChannelMutation = { removeProductsFromChannel: Arr
     )> }
   )> };
 
+export type RemoveVariantsFromChannelMutationVariables = Exact<{
+  input: RemoveProductVariantsFromChannelInput;
+}>;
+
+
+export type RemoveVariantsFromChannelMutation = { removeProductVariantsFromChannel: Array<(
+    { __typename?: 'ProductVariant' }
+    & Pick<ProductVariant, 'id'>
+    & { channels: Array<(
+      { __typename?: 'Channel' }
+      & Pick<Channel, 'id' | 'code'>
+    )> }
+  )> };
+
 export type GetProductVariantQueryVariables = Exact<{
   id: Scalars['ID'];
 }>;
@@ -7812,6 +7843,7 @@ export namespace ProductVariant {
   export type FeaturedAsset = (NonNullable<ProductVariantFragment['featuredAsset']>);
   export type Assets = NonNullable<(NonNullable<ProductVariantFragment['assets']>)[number]>;
   export type Translations = NonNullable<(NonNullable<ProductVariantFragment['translations']>)[number]>;
+  export type Channels = NonNullable<(NonNullable<ProductVariantFragment['channels']>)[number]>;
 }
 
 export namespace ProductWithVariants {
@@ -8007,6 +8039,13 @@ export namespace AssignProductsToChannel {
   export type Channels = NonNullable<(NonNullable<NonNullable<(NonNullable<AssignProductsToChannelMutation['assignProductsToChannel']>)[number]>['channels']>)[number]>;
 }
 
+export namespace AssignVariantsToChannel {
+  export type Variables = AssignVariantsToChannelMutationVariables;
+  export type Mutation = AssignVariantsToChannelMutation;
+  export type AssignProductVariantsToChannel = NonNullable<(NonNullable<AssignVariantsToChannelMutation['assignProductVariantsToChannel']>)[number]>;
+  export type Channels = NonNullable<(NonNullable<NonNullable<(NonNullable<AssignVariantsToChannelMutation['assignProductVariantsToChannel']>)[number]>['channels']>)[number]>;
+}
+
 export namespace RemoveProductsFromChannel {
   export type Variables = RemoveProductsFromChannelMutationVariables;
   export type Mutation = RemoveProductsFromChannelMutation;
@@ -8014,6 +8053,13 @@ export namespace RemoveProductsFromChannel {
   export type Channels = NonNullable<(NonNullable<NonNullable<(NonNullable<RemoveProductsFromChannelMutation['removeProductsFromChannel']>)[number]>['channels']>)[number]>;
 }
 
+export namespace RemoveVariantsFromChannel {
+  export type Variables = RemoveVariantsFromChannelMutationVariables;
+  export type Mutation = RemoveVariantsFromChannelMutation;
+  export type RemoveProductVariantsFromChannel = NonNullable<(NonNullable<RemoveVariantsFromChannelMutation['removeProductVariantsFromChannel']>)[number]>;
+  export type Channels = NonNullable<(NonNullable<NonNullable<(NonNullable<RemoveVariantsFromChannelMutation['removeProductVariantsFromChannel']>)[number]>['channels']>)[number]>;
+}
+
 export namespace GetProductVariant {
   export type Variables = GetProductVariantQueryVariables;
   export type Query = GetProductVariantQuery;

+ 28 - 0
packages/admin-ui/src/lib/core/src/data/definitions/product-definitions.ts

@@ -101,6 +101,10 @@ export const PRODUCT_VARIANT_FRAGMENT = gql`
             languageCode
             name
         }
+        channels {
+            id
+            code
+        }
     }
     ${PRODUCT_OPTION_FRAGMENT}
     ${ASSET_FRAGMENT}
@@ -554,6 +558,18 @@ export const ASSIGN_PRODUCTS_TO_CHANNEL = gql`
     }
 `;
 
+export const ASSIGN_VARIANTS_TO_CHANNEL = gql`
+    mutation AssignVariantsToChannel($input: AssignProductVariantsToChannelInput!) {
+        assignProductVariantsToChannel(input: $input) {
+            id
+            channels {
+                id
+                code
+            }
+        }
+    }
+`;
+
 export const REMOVE_PRODUCTS_FROM_CHANNEL = gql`
     mutation RemoveProductsFromChannel($input: RemoveProductsFromChannelInput!) {
         removeProductsFromChannel(input: $input) {
@@ -566,6 +582,18 @@ export const REMOVE_PRODUCTS_FROM_CHANNEL = gql`
     }
 `;
 
+export const REMOVE_VARIANTS_FROM_CHANNEL = gql`
+    mutation RemoveVariantsFromChannel($input: RemoveProductVariantsFromChannelInput!) {
+        removeProductVariantsFromChannel(input: $input) {
+            id
+            channels {
+                id
+                code
+            }
+        }
+    }
+`;
+
 export const GET_PRODUCT_VARIANT = gql`
     query GetProductVariant($id: ID!) {
         productVariant(id: $id) {

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

@@ -5,6 +5,8 @@ import {
     AddOptionToGroup,
     AssignProductsToChannel,
     AssignProductsToChannelInput,
+    AssignProductVariantsToChannelInput,
+    AssignVariantsToChannel,
     CreateAssets,
     CreateProduct,
     CreateProductInput,
@@ -30,6 +32,8 @@ import {
     RemoveOptionGroupFromProduct,
     RemoveProductsFromChannel,
     RemoveProductsFromChannelInput,
+    RemoveProductVariantsFromChannelInput,
+    RemoveVariantsFromChannel,
     SearchProducts,
     SortOrder,
     UpdateAsset,
@@ -45,6 +49,7 @@ import {
     ADD_OPTION_GROUP_TO_PRODUCT,
     ADD_OPTION_TO_GROUP,
     ASSIGN_PRODUCTS_TO_CHANNEL,
+    ASSIGN_VARIANTS_TO_CHANNEL,
     CREATE_ASSETS,
     CREATE_PRODUCT,
     CREATE_PRODUCT_OPTION_GROUP,
@@ -63,6 +68,7 @@ import {
     PRODUCT_SELECTOR_SEARCH,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     REMOVE_PRODUCTS_FROM_CHANNEL,
+    REMOVE_VARIANTS_FROM_CHANNEL,
     SEARCH_PRODUCTS,
     UPDATE_ASSET,
     UPDATE_PRODUCT,
@@ -329,4 +335,22 @@ export class ProductDataService {
             input,
         });
     }
+
+    assignVariantsToChannel(input: AssignProductVariantsToChannelInput) {
+        return this.baseDataService.mutate<
+            AssignVariantsToChannel.Mutation,
+            AssignVariantsToChannel.Variables
+        >(ASSIGN_VARIANTS_TO_CHANNEL, {
+            input,
+        });
+    }
+
+    removeVariantsFromChannel(input: RemoveProductVariantsFromChannelInput) {
+        return this.baseDataService.mutate<
+            RemoveVariantsFromChannel.Mutation,
+            RemoveVariantsFromChannel.Variables
+        >(REMOVE_VARIANTS_FROM_CHANNEL, {
+            input,
+        });
+    }
 }

+ 2 - 5
packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.html

@@ -1,7 +1,4 @@
 <ng-select
-    [items]="channels$ | async"
-    bindLabel="code"
-    bindValue="id"
     appendTo="body"
     [addTag]="false"
     [multiple]="multiple"
@@ -17,9 +14,9 @@
         <vdr-channel-badge [channelCode]="item.code"></vdr-channel-badge>
         <span class="channel-label">{{ item.code | channelCodeToLabel | translate }}</span>
     </ng-template>
-    <ng-template ng-option-tmp let-item="item">
+    <ng-option *ngFor="let item of channels$ | async" [value]="item" [disabled]="channelIsDisabled(item.id)">
         <vdr-channel-badge [channelCode]="item.code"></vdr-channel-badge>
         {{ item.code | channelCodeToLabel | translate }}
-    </ng-template>
+    </ng-option>
 </ng-select>
 

+ 7 - 2
packages/admin-ui/src/lib/core/src/shared/components/channel-assignment-control/channel-assignment-control.component.ts

@@ -23,6 +23,7 @@ import { DataService } from '../../../data/providers/data.service';
 export class ChannelAssignmentControlComponent implements OnInit, ControlValueAccessor {
     @Input() multiple = true;
     @Input() includeDefaultChannel = true;
+    @Input() disableChannelIds: string[] = [];
 
     channels$: Observable<CurrentUserChannel[]>;
     value: string[] = [];
@@ -37,7 +38,7 @@ export class ChannelAssignmentControlComponent implements OnInit, ControlValueAc
             .userStatus()
             .single$.pipe(
                 map(({ userStatus }) =>
-                    userStatus.channels.filter((c) =>
+                    userStatus.channels.filter(c =>
                         this.includeDefaultChannel ? true : c.code !== DEFAULT_CHANNEL_CODE,
                     ),
                 ),
@@ -68,9 +69,13 @@ export class ChannelAssignmentControlComponent implements OnInit, ControlValueAc
         }
     }
 
+    channelIsDisabled(id: string) {
+        return this.disableChannelIds.includes(id);
+    }
+
     valueChanged(value: CurrentUserChannel[] | CurrentUserChannel | undefined) {
         if (Array.isArray(value)) {
-            this.onChange(value.map((c) => c.id));
+            this.onChange(value.map(c => c.id));
         } else {
             this.onChange([value ? value.id : undefined]);
         }

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "Přiřadit produkty do kanálu",
     "assign-to-channel": "Přiřadit do kanálu",
     "assign-to-named-channel": "Přiřadit do { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Náhled ceny v kanálu",
     "collection-contents": "Obsah kolekce",
     "confirm-adding-options-delete-default-body": "Přidáním vlastností k tomuto produktu způsobí vymazání nýnější výchozí varianty. Chcete pokračovat?",
@@ -99,6 +101,8 @@
     "no-selection": "Žádný výběr",
     "notify-remove-product-from-channel-error": "Produkt se nepovedlo odebrat z kanálu",
     "notify-remove-product-from-channel-success": "Produkt byl úspěšně odebrán z kanálu",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Volba",
     "option-name": "Jméno volby",
     "option-values": "Hodnoty volby",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Odebrat z kanálu",
     "remove-option": "Odebrat volbu",
     "remove-product-from-channel": "Odebrat produkt z kanálu",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "Hledat výraz",
     "search-product-name-or-code": "Hledat produkt dle jména, nebo kódu",
     "sku": "SKU",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "Produkte dem Kanal zuweisen",
     "assign-to-channel": "Zuweisung an Kanal",
     "assign-to-named-channel": "Zuweisen an { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Kanal-Preisvorschau",
     "collection-contents": "Inhalt der Sammlung",
     "confirm-adding-options-delete-default-body": "Das Hinzufügen von Optionen zu diesem Produkt führt dazu, dass die vorhandene Standardvariante gelöscht wird. Möchten Sie fortfahren?",
@@ -99,6 +101,8 @@
     "no-selection": "Keine Auswahl",
     "notify-remove-product-from-channel-error": "Das Produkt konnte nicht aus dem Kanal entfernt werden",
     "notify-remove-product-from-channel-success": "Das Produkt erfolgreich aus dem Kanal entfernt",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Option",
     "option-name": "Optionsname",
     "option-values": "Optionswerte",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Aus dem Kanal entfernen",
     "remove-option": "Option entfernen",
     "remove-product-from-channel": "Produkt aus dem Kanal entfernen",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "Suche nach Begriff",
     "search-product-name-or-code": "Suche nach Produktname oder -code",
     "sku": "Artikelnummer",

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

@@ -53,10 +53,12 @@
     "add-facet-value": "Add facet value",
     "add-facets": "Add facets",
     "add-option": "Add option",
-    "assign-product-to-channel-success": "Successfully assigned Product to \"{ channel }\"",
+    "assign-product-to-channel-success": "Successfully assigned product to \"{ channel }\"",
     "assign-products-to-channel": "Assign products to channel",
     "assign-to-channel": "Assign to channel",
     "assign-to-named-channel": "Assign to { channelCode }",
+    "assign-variant-to-channel-success": "Successfully assigned product variant to \"{ channel }\"",
+    "assign-variants-to-channel": "Assign product variants to channel",
     "channel-price-preview": "Channel price preview",
     "collection-contents": "Collection contents",
     "confirm-adding-options-delete-default-body": "Adding options to this product will cause the existing default variant to be deleted. Do you wish to proceed?",
@@ -99,6 +101,8 @@
     "no-selection": "No selection",
     "notify-remove-product-from-channel-error": "Could not remove product from channel",
     "notify-remove-product-from-channel-success": "Successfully removed product from channel",
+    "notify-remove-variant-from-channel-error": "Could not remove product variant from channel",
+    "notify-remove-variant-from-channel-success": "Successfully removed product variant from channel",
     "option": "Option",
     "option-name": "Option name",
     "option-values": "Option values",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Remove from channel",
     "remove-option": "Remove option",
     "remove-product-from-channel": "Remove product from channel",
+    "remove-product-variant-from-channel": "Remove product variant from channel",
     "search-for-term": "Search for term",
     "search-product-name-or-code": "Search by product name or code",
     "sku": "SKU",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "Asignar product a canal de ventas",
     "assign-to-channel": "Asignar a canal de ventas",
     "assign-to-named-channel": "Asignar a { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "collection-contents": "Contenidos de la colección",
     "confirm-adding-options-delete-default-body": "Añadir optiones a este producto eliminará la variante por defecto. ¿Desea continuar?",
@@ -99,6 +101,8 @@
     "no-selection": "Sin selección",
     "notify-remove-product-from-channel-error": "",
     "notify-remove-product-from-channel-success": "",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Opción",
     "option-name": "Nombre de la opción",
     "option-values": "Valores de la opción",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Eliminar de canal de ventas",
     "remove-option": "Eliminar opción",
     "remove-product-from-channel": "Eliminar product de canal de ventas",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "",
     "search-product-name-or-code": "Buscar por nombre o código de producto",
     "sku": "SKU",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "Przypisz produkt do kanału",
     "assign-to-channel": "Przypisz do kanału",
     "assign-to-named-channel": "Przypisz do { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Podgląd cen kanału",
     "collection-contents": "Zawartość kolekcji",
     "confirm-adding-options-delete-default-body": "Dodawanie opcji spowoduje, że obecna domyślna opcja zostanie usunięta. Czy chcesz kontynuować?",
@@ -99,6 +101,8 @@
     "no-selection": "Brak zaznaczenia",
     "notify-remove-product-from-channel-error": "Błąd usuwania produktu z kanału",
     "notify-remove-product-from-channel-success": "Produkt pomyślnie usunięty z kanału",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Opcje",
     "option-name": "Nazwa opcji",
     "option-values": "Wartość opcji",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Usuń z kanału",
     "remove-option": "Usuń opcje",
     "remove-product-from-channel": "Usuń produkt z kanału",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "Szukaj frazy",
     "search-product-name-or-code": "Szukaj produktu po nazwie lub kodzie",
     "sku": "SKU",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "Atribuir produtos ao canal",
     "assign-to-channel": "Atribuir ao canal",
     "assign-to-named-channel": "Atribuir a { channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
     "confirm-adding-options-delete-default-body": "Adicionar opções a este produto fará com que a variante padrão existente seja excluída. Você deseja continuar?",
@@ -99,6 +101,8 @@
     "no-selection": "Nenhuma seleção",
     "notify-remove-product-from-channel-error": "Não foi possível remover o produto do canal",
     "notify-remove-product-from-channel-success": "Produto removido com sucesso do canal",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "Opção",
     "option-name": "Nome da opção",
     "option-values": "Valor da opção",
@@ -121,6 +125,7 @@
     "remove-from-channel": "Excluir do canal",
     "remove-option": "Excluir opção",
     "remove-product-from-channel": "Excluir produto do canal",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "Pesquisar termo",
     "search-product-name-or-code": "Pesquisar por nome ou código do produto",
     "sku": "SKU",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "分配产品到销售渠道",
     "assign-to-channel": "分配至销售渠道",
     "assign-to-named-channel": "分配到{ channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "渠道价格预览",
     "collection-contents": "系列产品",
     "confirm-adding-options-delete-default-body": "添加新规格到此产品会导致含此规格的产品被删除,确认继续吗?",
@@ -99,6 +101,8 @@
     "no-selection": "尚未选择",
     "notify-remove-product-from-channel-error": "从渠道中移除商品失败",
     "notify-remove-product-from-channel-success": "成功从渠道中移除商品",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "规格",
     "option-name": "规格名称",
     "option-values": "规格列表(按回车键添加)",
@@ -121,6 +125,7 @@
     "remove-from-channel": "从销售渠道移除",
     "remove-option": "移除选项",
     "remove-product-from-channel": "从销售渠道移除商品",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "输入搜索条目",
     "search-product-name-or-code": "输入要搜索的商品名称或商品编码",
     "sku": "商品库存编码",

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

@@ -57,6 +57,8 @@
     "assign-products-to-channel": "分配產品到渠道",
     "assign-to-channel": "分配至渠道",
     "assign-to-named-channel": "分配到{ channelCode }",
+    "assign-variant-to-channel-success": "",
+    "assign-variants-to-channel": "",
     "channel-price-preview": "渠道價格覽",
     "collection-contents": "系列產品",
     "confirm-adding-options-delete-default-body": "新增規格到此產品會引致包含此規格的產品被移除,確認繼續吗?",
@@ -99,6 +101,8 @@
     "no-selection": "尚未選擇",
     "notify-remove-product-from-channel-error": "從渠道中移除商品失敗",
     "notify-remove-product-from-channel-success": "成功從渠道中移除商品",
+    "notify-remove-variant-from-channel-error": "",
+    "notify-remove-variant-from-channel-success": "",
     "option": "規格",
     "option-name": "規格名稱",
     "option-values": "規格列表(按輸入鍵新增)",
@@ -121,6 +125,7 @@
     "remove-from-channel": "從渠道移除",
     "remove-option": "移除選項",
     "remove-product-from-channel": "從渠道移除商品",
+    "remove-product-variant-from-channel": "",
     "search-for-term": "輸入搜索條目",
     "search-product-name-or-code": "輸入要搜索的商品名稱或商品編碼",
     "sku": "商品庫存編碼",

+ 5 - 5
packages/dev-server/dev-config.ts

@@ -64,12 +64,12 @@ export const devConfig: VendureConfig = {
             assetUploadDir: path.join(__dirname, 'assets'),
             port: 5002,
         }),
-        // DefaultSearchPlugin,
+        DefaultSearchPlugin,
         DefaultJobQueuePlugin,
-        ElasticsearchPlugin.init({
-            host: 'http://localhost',
-            port: 9200,
-        }),
+        // ElasticsearchPlugin.init({
+        //     host: 'http://localhost',
+        //     port: 9200,
+        // }),
         EmailPlugin.init({
             devMode: true,
             handlers: defaultEmailHandlers,

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


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