Skip to content

Commit

Permalink
feat(admin-ui): Enable removal of Product from Channel
Browse files Browse the repository at this point in the history
Relates to #12
  • Loading branch information
michaelbromley committed Nov 13, 2019
1 parent b295e52 commit 27eea68
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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]);
}
Expand Down
14 changes: 14 additions & 0 deletions packages/admin-ui/src/app/common/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions packages/admin-ui/src/app/data/definitions/product-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 12 additions & 0 deletions packages/admin-ui/src/app/data/providers/product-data.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
GetProductWithVariants,
Reindex,
RemoveOptionGroupFromProduct,
RemoveProductsFromChannel,
RemoveProductsFromChannelInput,
SearchProducts,
SortOrder,
UpdateProduct,
Expand All @@ -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,
Expand Down Expand Up @@ -270,4 +273,13 @@ export class ProductDataService {
input,
});
}

removeProductsFromChannel(input: RemoveProductsFromChannelInput) {
return this.baseDataService.mutate<
RemoveProductsFromChannel.Mutation,
RemoveProductsFromChannel.Variables
>(REMOVE_PRODUCTS_FROM_CHANNEL, {
input,
});
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,14 +34,16 @@ export class IfPermissionsDirective extends IfDirectiveBase<[Permission | null]>
_viewContainer: ViewContainerRef,
templateRef: TemplateRef<any>,
private dataService: DataService,
private changeDetectorRef: ChangeDetectorRef,
) {
super(_viewContainer, templateRef, permission => {
if (!permission) {
return of(false);
}
return this.dataService.client
.userStatus()
.mapSingle(({ userStatus }) => userStatus.permissions.includes(permission));
.mapSingle(({ userStatus }) => userStatus.permissions.includes(permission))
.pipe(tap(() => this.changeDetectorRef.markForCheck()));
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/admin-ui/src/app/shared/shared-declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/admin-ui/src/app/shared/shared.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
FormFieldComponent,
FormFieldControlDirective,
FormItemComponent,
IfDefaultChannelActiveDirective,
IfMultichannelDirective,
IfPermissionsDirective,
ItemsPerPageControlsComponent,
Expand Down Expand Up @@ -138,6 +139,7 @@ const DECLARATIONS = [
ChannelBadgeComponent,
ChannelAssignmentControlComponent,
ChannelLabelPipe,
IfDefaultChannelActiveDirective,
];

@NgModule({
Expand Down
5 changes: 5 additions & 0 deletions packages/admin-ui/src/i18n-messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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?",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit 27eea68

Please sign in to comment.