Kaynağa Gözat

feat(admin-ui): Enable removal of Product from Channel

Relates to #12
Michael Bromley 6 yıl önce
ebeveyn
işleme
27eea68f4e

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

@@ -55,22 +55,33 @@
                 <div class="clr-row">
                     <div class="clr-col">
                         <section class="form-block" formGroupName="product">
-                            <vdr-form-item [label]="'common.channels' | translate" *vdrIfMultichannel>
-                                <div class="flex">
-                                    <div class="product-channels">
-                                        <ng-container *ngFor="let channel of productChannels$ | async">
-                                            <vdr-chip *ngIf="!isDefaultChannel(channel.code)">
-                                                <vdr-channel-badge [channelCode]="channel.code"></vdr-channel-badge>
-                                                {{ channel.code | channelCodeToLabel }}
-                                            </vdr-chip>
-                                        </ng-container>
+                            <ng-container *vdrIfMultichannel>
+                                <vdr-form-item
+                                    [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>
+                                        <button class="btn btn-sm" (click)="assignToChannel()">
+                                            <clr-icon shape="layers"></clr-icon>
+                                            {{ 'catalog.assign-to-channel' | translate }}
+                                        </button>
                                     </div>
-                                    <button class="btn btn-sm" (click)="assignToChannel()">
-                                        <clr-icon shape="layers"></clr-icon>
-                                        {{ 'catalog.assign-to-channel' | translate }}
-                                    </button>
-                                </div>
-                            </vdr-form-item>
+                                </vdr-form-item>
+                            </ng-container>
                             <vdr-form-field [label]="'catalog.product-name' | translate" for="name">
                                 <input
                                     id="name"

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

@@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { AssignProductsToChannelDialogComponent } from '@vendure/admin-ui/src/app/catalog/components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
+import { DataService } from '@vendure/admin-ui/src/app/data/providers/data.service';
 import { combineLatest, EMPTY, merge, Observable } from 'rxjs';
 import {
     distinctUntilChanged,
@@ -14,7 +15,6 @@ import {
     withLatestFrom,
 } from 'rxjs/operators';
 import { normalizeString } from 'shared/normalize-string';
-import { pick } from 'shared/pick';
 import { DEFAULT_CHANNEL_CODE } from 'shared/shared-constants';
 import { notNullOrUndefined } from 'shared/shared-utils';
 import { unique } from 'shared/unique';
@@ -97,6 +97,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
         private formBuilder: FormBuilder,
         private modalService: ModalService,
         private notificationService: NotificationService,
+        private dataService: DataService,
         private location: Location,
         private changeDetector: ChangeDetectorRef,
     ) {
@@ -181,6 +182,35 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
             .subscribe();
     }
 
+    removeFromChannel(channelId: string) {
+        this.modalService
+            .dialog({
+                title: _('catalog.remove-product-from-channel'),
+                buttons: [
+                    { type: 'seconday', 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'));
+                },
+            );
+    }
+
     customFieldIsSet(name: string): boolean {
         return !!this.detailForm.get(['product', 'customFields', name]);
     }

+ 14 - 0
packages/admin-ui/src/app/common/generated-types.ts

@@ -4109,6 +4109,13 @@ export type AssignProductsToChannelMutationVariables = {
 
 export type AssignProductsToChannelMutation = ({ __typename?: 'Mutation' } & { assignProductsToChannel: Array<({ __typename?: 'Product' } & Pick<Product, 'id'> & { channels: Array<({ __typename?: 'Channel' } & Pick<Channel, 'id' | 'code'>)> })> });
 
+export type RemoveProductsFromChannelMutationVariables = {
+  input: RemoveProductsFromChannelInput
+};
+
+
+export type RemoveProductsFromChannelMutation = ({ __typename?: 'Mutation' } & { removeProductsFromChannel: Array<({ __typename?: 'Product' } & Pick<Product, 'id'> & { channels: Array<({ __typename?: 'Channel' } & Pick<Channel, 'id' | 'code'>)> })> });
+
 export type PromotionFragment = ({ __typename?: 'Promotion' } & Pick<Promotion, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'enabled' | 'couponCode' | 'perCustomerUsageLimit' | 'startsAt' | 'endsAt'> & { conditions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, actions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
 
 export type GetPromotionListQueryVariables = {
@@ -5083,6 +5090,13 @@ export namespace AssignProductsToChannel {
   export type Channels = (NonNullable<(NonNullable<AssignProductsToChannelMutation['assignProductsToChannel'][0]>)['channels'][0]>);
 }
 
+export namespace RemoveProductsFromChannel {
+  export type Variables = RemoveProductsFromChannelMutationVariables;
+  export type Mutation = RemoveProductsFromChannelMutation;
+  export type RemoveProductsFromChannel = (NonNullable<RemoveProductsFromChannelMutation['removeProductsFromChannel'][0]>);
+  export type Channels = (NonNullable<(NonNullable<RemoveProductsFromChannelMutation['removeProductsFromChannel'][0]>)['channels'][0]>);
+}
+
 export namespace Promotion {
   export type Fragment = PromotionFragment;
   export type Conditions = ConfigurableOperationFragment;

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

@@ -450,3 +450,15 @@ export const ASSIGN_PRODUCTS_TO_CHANNEL = gql`
         }
     }
 `;
+
+export const REMOVE_PRODUCTS_FROM_CHANNEL = gql`
+    mutation RemoveProductsFromChannel($input: RemoveProductsFromChannelInput!) {
+        removeProductsFromChannel(input: $input) {
+            id
+            channels {
+                id
+                code
+            }
+        }
+    }
+`;

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

@@ -23,6 +23,8 @@ import {
     GetProductWithVariants,
     Reindex,
     RemoveOptionGroupFromProduct,
+    RemoveProductsFromChannel,
+    RemoveProductsFromChannelInput,
     SearchProducts,
     SortOrder,
     UpdateProduct,
@@ -49,6 +51,7 @@ import {
     GET_PRODUCT_VARIANT_OPTIONS,
     GET_PRODUCT_WITH_VARIANTS,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
+    REMOVE_PRODUCTS_FROM_CHANNEL,
     SEARCH_PRODUCTS,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT_OPTION,
@@ -270,4 +273,13 @@ export class ProductDataService {
             input,
         });
     }
+
+    removeProductsFromChannel(input: RemoveProductsFromChannelInput) {
+        return this.baseDataService.mutate<
+            RemoveProductsFromChannel.Mutation,
+            RemoveProductsFromChannel.Variables
+        >(REMOVE_PRODUCTS_FROM_CHANNEL, {
+            input,
+        });
+    }
 }

+ 41 - 0
packages/admin-ui/src/app/shared/directives/if-default-channel-active.directive.ts

@@ -0,0 +1,41 @@
+import { ChangeDetectorRef, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
+import { tap } from 'rxjs/operators';
+import { DEFAULT_CHANNEL_CODE } from 'shared/shared-constants';
+
+import { UserStatus } from '../../common/generated-types';
+import { DataService } from '../../data/providers/data.service';
+
+import { IfDirectiveBase } from './if-directive-base';
+
+@Directive({
+    selector: '[vdrIfDefaultChannelActive]',
+})
+export class IfDefaultChannelActiveDirective extends IfDirectiveBase<[]> {
+    constructor(
+        _viewContainer: ViewContainerRef,
+        templateRef: TemplateRef<any>,
+        private dataService: DataService,
+        private changeDetectorRef: ChangeDetectorRef,
+    ) {
+        super(_viewContainer, templateRef, () => {
+            return this.dataService.client
+                .userStatus()
+                .mapStream(({ userStatus }) => this.defaultChannelIsActive(userStatus))
+                .pipe(tap(() => this.changeDetectorRef.markForCheck()));
+        });
+    }
+
+    /**
+     * A template to show if the current user does not have the speicified permission.
+     */
+    @Input()
+    set vdrIfMultichannelElse(templateRef: TemplateRef<any> | null) {
+        this.setElseTemplate(templateRef);
+    }
+
+    private defaultChannelIsActive(userStatus: UserStatus): boolean {
+        const defaultChannel = userStatus.channels.find(c => c.code === DEFAULT_CHANNEL_CODE);
+
+        return !!(defaultChannel && userStatus.activeChannelId === defaultChannel.id);
+    }
+}

+ 12 - 2
packages/admin-ui/src/app/shared/directives/if-permissions.directive.ts

@@ -1,5 +1,13 @@
-import { Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef } from '@angular/core';
+import {
+    ChangeDetectorRef,
+    Directive,
+    EmbeddedViewRef,
+    Input,
+    TemplateRef,
+    ViewContainerRef,
+} from '@angular/core';
 import { of } from 'rxjs';
+import { tap } from 'rxjs/operators';
 
 import { Permission } from '../../common/generated-types';
 import { DataService } from '../../data/providers/data.service';
@@ -26,6 +34,7 @@ export class IfPermissionsDirective extends IfDirectiveBase<[Permission | null]>
         _viewContainer: ViewContainerRef,
         templateRef: TemplateRef<any>,
         private dataService: DataService,
+        private changeDetectorRef: ChangeDetectorRef,
     ) {
         super(_viewContainer, templateRef, permission => {
             if (!permission) {
@@ -33,7 +42,8 @@ export class IfPermissionsDirective extends IfDirectiveBase<[Permission | null]>
             }
             return this.dataService.client
                 .userStatus()
-                .mapSingle(({ userStatus }) => userStatus.permissions.includes(permission));
+                .mapSingle(({ userStatus }) => userStatus.permissions.includes(permission))
+                .pipe(tap(() => this.changeDetectorRef.markForCheck()));
         });
     }
 

+ 1 - 0
packages/admin-ui/src/app/shared/shared-declarations.ts

@@ -34,6 +34,7 @@ export { FormFieldControlDirective } from './components/form-field/form-field-co
 export { FormFieldComponent } from './components/form-field/form-field.component';
 export { FormItemComponent } from './components/form-item/form-item.component';
 export { FormattedAddressComponent } from './components/formatted-address/formatted-address.component';
+export { IfDefaultChannelActiveDirective } from './directives/if-default-channel-active.directive';
 export { IfMultichannelDirective } from './directives/if-multichannel.directive';
 export { IfPermissionsDirective } from './directives/if-permissions.directive';
 export {

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

@@ -50,6 +50,7 @@ import {
     FormFieldComponent,
     FormFieldControlDirective,
     FormItemComponent,
+    IfDefaultChannelActiveDirective,
     IfMultichannelDirective,
     IfPermissionsDirective,
     ItemsPerPageControlsComponent,
@@ -138,6 +139,7 @@ const DECLARATIONS = [
     ChannelBadgeComponent,
     ChannelAssignmentControlComponent,
     ChannelLabelPipe,
+    IfDefaultChannelActiveDirective,
 ];
 
 @NgModule({

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

@@ -37,6 +37,7 @@
     "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?",
     "confirm-adding-options-delete-default-title": "Delete default variant?",
+    "confirm-delete-channel": "Delete channel?",
     "confirm-delete-collection": "Delete collection?",
     "confirm-delete-collection-and-children-body": "Deleting this collection will also delete all child collections",
     "confirm-delete-country": "Delete country?",
@@ -69,6 +70,8 @@
     "no-featured-asset": "No featured asset",
     "no-selection": "No selection",
     "notify-create-assets-success": "Created {count, plural, one {new Asset} other {{count} new Assets}}",
+    "notify-remove-product-from-channel-error": "Could not remove product from channel",
+    "notify-remove-product-from-channel-success": "Successfully removed product from channel",
     "open-asset-source": "Open asset source",
     "option": "Option",
     "option-name": "Option name",
@@ -91,7 +94,9 @@
     "reindex-successful": "Indexed {count, plural, one {product variant} other {{count} product variants}} in {time}ms",
     "reindexing": "Rebuilding search index",
     "remove-asset": "Remove asset",
+    "remove-from-channel": "Remove from channel",
     "remove-option": "Remove option",
+    "remove-product-from-channel": "Remove product from channel",
     "search-asset-name": "Search assets by name",
     "search-for-term": "Search for term",
     "search-product-name-or-code": "Search by product name or code",