Browse Source

feat(admin-ui): Add support for bulk facet channel assignment/removal

Relates to #853
Michael Bromley 3 years ago
parent
commit
647857ce5a

+ 11 - 2
packages/admin-ui/src/lib/catalog/src/catalog.module.ts

@@ -8,6 +8,8 @@ import { AssetDetailComponent } from './components/asset-detail/asset-detail.com
 import { AssetListComponent } from './components/asset-list/asset-list.component';
 import { AssetsComponent } from './components/assets/assets.component';
 import { AssignProductsToChannelDialogComponent } from './components/assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
+import { AssignToChannelDialogComponent } from './components/assign-to-channel-dialog/assign-to-channel-dialog.component';
+import { BulkAddFacetValuesDialogComponent } from './components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component';
 import { CollectionContentsComponent } from './components/collection-contents/collection-contents.component';
 import { CollectionDetailComponent } from './components/collection-detail/collection-detail.component';
 import { CollectionListComponent } from './components/collection-list/collection-list.component';
@@ -15,7 +17,11 @@ import { CollectionTreeNodeComponent } from './components/collection-tree/collec
 import { CollectionTreeComponent } from './components/collection-tree/collection-tree.component';
 import { ConfirmVariantDeletionDialogComponent } from './components/confirm-variant-deletion-dialog/confirm-variant-deletion-dialog.component';
 import { FacetDetailComponent } from './components/facet-detail/facet-detail.component';
-import { deleteFacetsBulkAction } from './components/facet-list/facet-list-bulk-actions';
+import {
+    assignFacetsToChannelBulkAction,
+    deleteFacetsBulkAction,
+    removeFacetsFromChannelBulkAction,
+} from './components/facet-list/facet-list-bulk-actions';
 import { FacetListComponent } from './components/facet-list/facet-list.component';
 import { GenerateProductVariantsComponent } from './components/generate-product-variants/generate-product-variants.component';
 import { OptionValueInputComponent } from './components/option-value-input/option-value-input.component';
@@ -32,7 +38,6 @@ import { ProductVariantsListComponent } from './components/product-variants-list
 import { ProductVariantsTableComponent } from './components/product-variants-table/product-variants-table.component';
 import { UpdateProductOptionDialogComponent } from './components/update-product-option-dialog/update-product-option-dialog.component';
 import { VariantPriceDetailComponent } from './components/variant-price-detail/variant-price-detail.component';
-import { BulkAddFacetValuesDialogComponent } from './components/bulk-add-facet-values-dialog/bulk-add-facet-values-dialog.component';
 
 const CATALOG_COMPONENTS = [
     ProductListComponent,
@@ -59,6 +64,7 @@ const CATALOG_COMPONENTS = [
     ConfirmVariantDeletionDialogComponent,
     ProductOptionsEditorComponent,
     BulkAddFacetValuesDialogComponent,
+    AssignToChannelDialogComponent,
 ];
 
 @NgModule({
@@ -71,6 +77,9 @@ export class CatalogModule {
         bulkActionRegistryService.registerBulkAction(assignFacetValuesToProductsBulkAction);
         bulkActionRegistryService.registerBulkAction(assignProductsToChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteProductsBulkAction);
+
+        bulkActionRegistryService.registerBulkAction(assignFacetsToChannelBulkAction);
+        bulkActionRegistryService.registerBulkAction(removeFacetsFromChannelBulkAction);
         bulkActionRegistryService.registerBulkAction(deleteFacetsBulkAction);
     }
 }

+ 24 - 0
packages/admin-ui/src/lib/catalog/src/components/assign-to-channel-dialog/assign-to-channel-dialog.component.html

@@ -0,0 +1,24 @@
+<ng-template vdrDialogTitle>
+    {{ 'common.assign-to-channel' | translate }}
+</ng-template>
+<clr-input-container class="mb4">
+    <label>{{ 'common.channel' | translate }}</label>
+    <vdr-channel-assignment-control
+        clrInput
+        [multiple]="false"
+        [includeDefaultChannel]="false"
+        [formControl]="selectedChannelIdControl"
+    ></vdr-channel-assignment-control>
+</clr-input-container>
+
+<ng-template vdrDialogButtons>
+    <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
+    <button type="submit" (click)="assign()" [disabled]="!selectedChannel" class="btn btn-primary">
+        <ng-template [ngIf]="selectedChannel" [ngIfElse]="noSelection">
+            {{ 'catalog.assign-to-named-channel' | translate: { channelCode: selectedChannel?.code } }}
+        </ng-template>
+        <ng-template #noSelection>
+            {{ 'catalog.no-channel-selected' | translate }}
+        </ng-template>
+    </button>
+</ng-template>

+ 4 - 0
packages/admin-ui/src/lib/catalog/src/components/assign-to-channel-dialog/assign-to-channel-dialog.component.scss

@@ -0,0 +1,4 @@
+
+vdr-channel-assignment-control {
+    min-width: 200px;
+}

+ 54 - 0
packages/admin-ui/src/lib/catalog/src/components/assign-to-channel-dialog/assign-to-channel-dialog.component.ts

@@ -0,0 +1,54 @@
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
+import { DataService, Dialog, GetChannels, NotificationService } from '@vendure/admin-ui/core';
+import { combineLatest } from 'rxjs';
+
+@Component({
+    selector: 'vdr-assign-to-channel-dialog',
+    templateUrl: './assign-to-channel-dialog.component.html',
+    styleUrls: ['./assign-to-channel-dialog.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AssignToChannelDialogComponent implements OnInit, Dialog<GetChannels.Channels> {
+    selectedChannel: GetChannels.Channels | null | undefined;
+    currentChannel: GetChannels.Channels;
+    availableChannels: GetChannels.Channels[];
+    resolveWith: (result?: GetChannels.Channels) => void;
+    selectedChannelIdControl = new FormControl();
+
+    // assigned by ModalService.fromComponent() call
+
+    constructor(private dataService: DataService, private notificationService: NotificationService) {}
+
+    ngOnInit() {
+        const activeChannelId$ = this.dataService.client
+            .userStatus()
+            .mapSingle(({ userStatus }) => userStatus.activeChannelId);
+        const allChannels$ = this.dataService.settings.getChannels().mapSingle(data => data.channels);
+
+        combineLatest(activeChannelId$, allChannels$).subscribe(([activeChannelId, channels]) => {
+            // tslint:disable-next-line:no-non-null-assertion
+            this.currentChannel = channels.find(c => c.id === activeChannelId)!;
+            this.availableChannels = channels;
+        });
+
+        this.selectedChannelIdControl.valueChanges.subscribe(ids => {
+            this.selectChannel(ids);
+        });
+    }
+
+    selectChannel(channelIds: string[]) {
+        this.selectedChannel = this.availableChannels.find(c => c.id === channelIds[0]);
+    }
+
+    assign() {
+        const selectedChannel = this.selectedChannel;
+        if (selectedChannel) {
+            this.resolveWith(selectedChannel);
+        }
+    }
+
+    cancel() {
+        this.resolveWith();
+    }
+}

+ 150 - 31
packages/admin-ui/src/lib/catalog/src/components/facet-list/facet-list-bulk-actions.ts

@@ -8,9 +8,12 @@ import {
     ModalService,
     NotificationService,
 } from '@vendure/admin-ui/core';
+import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { unique } from '@vendure/common/lib/unique';
 import { EMPTY, of } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { map, mapTo, switchMap } from 'rxjs/operators';
+
+import { AssignToChannelDialogComponent } from '../assign-to-channel-dialog/assign-to-channel-dialog.component';
 
 export const deleteFacetsBulkAction: BulkAction<GetFacetList.Items, FacetListComponent> = {
     location: 'facet-list',
@@ -94,33 +97,149 @@ export const deleteFacetsBulkAction: BulkAction<GetFacetList.Items, FacetListCom
     },
 };
 
-// export const assignProductsToChannelBulkAction: BulkAction<SearchProducts.Items, ProductListComponent> = {
-//     location: 'product-list',
-//     label: _('catalog.assign-to-channel'),
-//     icon: 'layers',
-//     isVisible: ({ injector }) => {
-//         return injector
-//             .get(DataService)
-//             .client.userStatus()
-//             .mapSingle(({ userStatus }) => 1 < userStatus.channels.length)
-//             .toPromise();
-//     },
-//     onClick: ({ injector, selection, hostComponent, clearSelection }) => {
-//         const modalService = injector.get(ModalService);
-//         const dataService = injector.get(DataService);
-//         const notificationService = injector.get(NotificationService);
-//         modalService
-//             .fromComponent(AssignProductsToChannelDialogComponent, {
-//                 size: 'lg',
-//                 locals: {
-//                     productIds: unique(selection.map(p => p.productId)),
-//                     currentChannelIds: [],
-//                 },
-//             })
-//             .subscribe(result => {
-//                 if (result) {
-//                     clearSelection();
-//                 }
-//             });
-//     },
-// };
+export const assignFacetsToChannelBulkAction: BulkAction<GetFacetList.Items, FacetListComponent> = {
+    location: 'facet-list',
+    label: _('catalog.assign-to-channel'),
+    icon: 'layers',
+    isVisible: ({ injector }) => {
+        return injector
+            .get(DataService)
+            .client.userStatus()
+            .mapSingle(({ userStatus }) => 1 < userStatus.channels.length)
+            .toPromise();
+    },
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        const dataService = injector.get(DataService);
+        const notificationService = injector.get(NotificationService);
+        modalService
+            .fromComponent(AssignToChannelDialogComponent, {
+                size: 'md',
+                locals: {},
+            })
+            .pipe(
+                switchMap(result => {
+                    if (result) {
+                        return dataService.facet
+                            .assignFacetsToChannel({
+                                facetIds: selection.map(f => f.id),
+                                channelId: result.id,
+                            })
+                            .pipe(mapTo(result));
+                    } else {
+                        return EMPTY;
+                    }
+                }),
+            )
+            .subscribe(result => {
+                notificationService.success(_('catalog.assign-facets-to-channel-success'), {
+                    channel: result.code,
+                });
+                clearSelection();
+            });
+    },
+};
+
+export const removeFacetsFromChannelBulkAction: BulkAction<GetFacetList.Items, FacetListComponent> = {
+    location: 'facet-list',
+    label: _('common.remove-from-active-channel'),
+    icon: 'layers',
+    iconClass: 'is-warning',
+    isVisible: ({ injector }) => {
+        return injector
+            .get(DataService)
+            .client.userStatus()
+            .mapSingle(({ userStatus }) => {
+                if (userStatus.channels.length === 1) {
+                    return false;
+                }
+                const defaultChannelId = userStatus.channels.find(c => c.code === DEFAULT_CHANNEL_CODE)?.id;
+                return userStatus.activeChannelId !== defaultChannelId;
+            })
+            .toPromise();
+    },
+    onClick: ({ injector, selection, hostComponent, clearSelection }) => {
+        const modalService = injector.get(ModalService);
+        const dataService = injector.get(DataService);
+        const notificationService = injector.get(NotificationService);
+
+        function showModalAndDelete(facetIds: string[], message?: string) {
+            return modalService
+                .dialog({
+                    title: _('catalog.confirm-remove-facets-from-channel'),
+                    translationVars: {
+                        count: selection.length,
+                    },
+                    size: message ? 'lg' : 'md',
+                    body: message,
+                    buttons: [
+                        { type: 'secondary', label: _('common.cancel') },
+                        {
+                            type: 'danger',
+                            label: message ? _('common.force-remove') : _('common.remove'),
+                            returnValue: true,
+                        },
+                    ],
+                })
+                .pipe(
+                    switchMap(res =>
+                        res
+                            ? dataService.client
+                                  .userStatus()
+                                  .mapSingle(({ userStatus }) => userStatus.activeChannelId)
+                                  .pipe(
+                                      switchMap(activeChannelId =>
+                                          activeChannelId
+                                              ? dataService.facet.removeFacetsFromChannel({
+                                                    channelId: activeChannelId,
+                                                    facetIds,
+                                                    force: !!message,
+                                                })
+                                              : EMPTY,
+                                      ),
+                                      map(res2 => res2.removeFacetsFromChannel),
+                                  )
+                            : of([]),
+                    ),
+                );
+        }
+
+        showModalAndDelete(unique(selection.map(f => f.id)))
+            .pipe(
+                switchMap(result => {
+                    let removedCount = 0;
+                    const errors: string[] = [];
+                    const errorIds: string[] = [];
+                    let i = 0;
+                    for (const item of result) {
+                        if (item.__typename === 'Facet') {
+                            removedCount++;
+                        } else if (item.__typename === 'FacetInUseError') {
+                            errors.push(item.message);
+                            errorIds.push(selection[i]?.id);
+                        }
+                        i++;
+                    }
+                    if (0 < errorIds.length) {
+                        return showModalAndDelete(errorIds, errors.join('\n')).pipe(
+                            map(result2 => {
+                                const deletedCount2 = result2.filter(r => r.__typename === 'Facet').length;
+                                return removedCount + deletedCount2;
+                            }),
+                        );
+                    } else {
+                        return of(removedCount);
+                    }
+                }),
+            )
+            .subscribe(removedCount => {
+                if (removedCount) {
+                    hostComponent.refresh();
+                    clearSelection();
+                    notificationService.success(_('common.notify-remove-facets-from-channel-success'), {
+                        count: removedCount,
+                    });
+                }
+            });
+    },
+};

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

@@ -1505,6 +1505,7 @@ export type FacetInUseError = ErrorResult & {
   __typename?: 'FacetInUseError';
   errorCode: ErrorCode;
   message: Scalars['String'];
+  facetCode: Scalars['String'];
   productCount: Scalars['Int'];
   variantCount: Scalars['Int'];
 };
@@ -6440,6 +6441,29 @@ export type GetFacetWithValuesQuery = { facet?: Maybe<(
     & FacetWithValuesFragment
   )> };
 
+export type AssignFacetsToChannelMutationVariables = Exact<{
+  input: AssignFacetsToChannelInput;
+}>;
+
+
+export type AssignFacetsToChannelMutation = { assignFacetsToChannel: Array<(
+    { __typename?: 'Facet' }
+    & Pick<Facet, 'id'>
+  )> };
+
+export type RemoveFacetsFromChannelMutationVariables = Exact<{
+  input: RemoveFacetsFromChannelInput;
+}>;
+
+
+export type RemoveFacetsFromChannelMutation = { removeFacetsFromChannel: Array<(
+    { __typename?: 'Facet' }
+    & Pick<Facet, 'id'>
+  ) | (
+    { __typename?: 'FacetInUseError' }
+    & Pick<FacetInUseError, 'errorCode' | 'message' | 'variantCount' | 'productCount'>
+  )> };
+
 export type DiscountFragment = (
   { __typename?: 'Discount' }
   & Pick<Discount, 'adjustmentSource' | 'amount' | 'amountWithTax' | 'description' | 'type'>
@@ -9929,6 +9953,20 @@ export namespace GetFacetWithValues {
   export type Facet = (NonNullable<GetFacetWithValuesQuery['facet']>);
 }
 
+export namespace AssignFacetsToChannel {
+  export type Variables = AssignFacetsToChannelMutationVariables;
+  export type Mutation = AssignFacetsToChannelMutation;
+  export type AssignFacetsToChannel = NonNullable<(NonNullable<AssignFacetsToChannelMutation['assignFacetsToChannel']>)[number]>;
+}
+
+export namespace RemoveFacetsFromChannel {
+  export type Variables = RemoveFacetsFromChannelMutationVariables;
+  export type Mutation = RemoveFacetsFromChannelMutation;
+  export type RemoveFacetsFromChannel = NonNullable<(NonNullable<RemoveFacetsFromChannelMutation['removeFacetsFromChannel']>)[number]>;
+  export type FacetInlineFragment = (DiscriminateUnion<NonNullable<(NonNullable<RemoveFacetsFromChannelMutation['removeFacetsFromChannel']>)[number]>, { __typename?: 'Facet' }>);
+  export type FacetInUseErrorInlineFragment = (DiscriminateUnion<NonNullable<(NonNullable<RemoveFacetsFromChannelMutation['removeFacetsFromChannel']>)[number]>, { __typename?: 'FacetInUseError' }>);
+}
+
 export namespace Discount {
   export type Fragment = DiscountFragment;
 }

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

@@ -126,3 +126,27 @@ export const GET_FACET_WITH_VALUES = gql`
     }
     ${FACET_WITH_VALUES_FRAGMENT}
 `;
+
+export const ASSIGN_FACETS_TO_CHANNEL = gql`
+    mutation AssignFacetsToChannel($input: AssignFacetsToChannelInput!) {
+        assignFacetsToChannel(input: $input) {
+            id
+        }
+    }
+`;
+
+export const REMOVE_FACETS_FROM_CHANNEL = gql`
+    mutation RemoveFacetsFromChannel($input: RemoveFacetsFromChannelInput!) {
+        removeFacetsFromChannel(input: $input) {
+            ... on Facet {
+                id
+            }
+            ... on FacetInUseError {
+                errorCode
+                message
+                variantCount
+                productCount
+            }
+        }
+    }
+`;

+ 25 - 0
packages/admin-ui/src/lib/core/src/data/providers/facet-data.service.ts

@@ -1,6 +1,9 @@
 import { pick } from '@vendure/common/lib/pick';
 
 import {
+    AssignFacetsToChannel,
+    AssignFacetsToChannelInput,
+    AssignProductsToChannelInput,
     CreateFacet,
     CreateFacetInput,
     CreateFacetValueInput,
@@ -10,12 +13,15 @@ import {
     DeleteFacetValues,
     GetFacetList,
     GetFacetWithValues,
+    RemoveFacetsFromChannel,
+    RemoveFacetsFromChannelInput,
     UpdateFacet,
     UpdateFacetInput,
     UpdateFacetValueInput,
     UpdateFacetValues,
 } from '../../common/generated-types';
 import {
+    ASSIGN_FACETS_TO_CHANNEL,
     CREATE_FACET,
     CREATE_FACET_VALUES,
     DELETE_FACET,
@@ -23,6 +29,7 @@ import {
     DELETE_FACETS,
     GET_FACET_LIST,
     GET_FACET_WITH_VALUES,
+    REMOVE_FACETS_FROM_CHANNEL,
     UPDATE_FACET,
     UPDATE_FACET_VALUES,
 } from '../definitions/facet-definitions';
@@ -111,4 +118,22 @@ export class FacetDataService {
             },
         );
     }
+
+    assignFacetsToChannel(input: AssignFacetsToChannelInput) {
+        return this.baseDataService.mutate<AssignFacetsToChannel.Mutation, AssignFacetsToChannel.Variables>(
+            ASSIGN_FACETS_TO_CHANNEL,
+            {
+                input,
+            },
+        );
+    }
+
+    removeFacetsFromChannel(input: RemoveFacetsFromChannelInput) {
+        return this.baseDataService.mutate<
+            RemoveFacetsFromChannel.Mutation,
+            RemoveFacetsFromChannel.Variables
+        >(REMOVE_FACETS_FROM_CHANNEL, {
+            input,
+        });
+    }
 }