Skip to content

Commit

Permalink
feat(admin-ui): Enable assigning Products to Channels
Browse files Browse the repository at this point in the history
Relates to #12
  • Loading branch information
michaelbromley committed Nov 8, 2019
1 parent 3a6c277 commit 59b9c91
Show file tree
Hide file tree
Showing 32 changed files with 707 additions and 95 deletions.
9 changes: 8 additions & 1 deletion packages/admin-ui/src/app/catalog/catalog.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { catalogRoutes } from './catalog.routes';
import { ApplyFacetDialogComponent } from './components/apply-facet-dialog/apply-facet-dialog.component';
import { AssetListComponent } from './components/asset-list/asset-list.component';
import { AssetPreviewComponent } from './components/asset-preview/asset-preview.component';
import { AssignProductsToChannelDialogComponent } from './components/assign-products-to-channel-dialog/assign-products-to-channel-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';
Expand Down Expand Up @@ -57,8 +58,14 @@ import { ProductVariantsResolver } from './providers/routing/product-variants-re
OptionValueInputComponent,
UpdateProductOptionDialogComponent,
ProductVariantsEditorComponent,
AssignProductsToChannelDialogComponent,
],
entryComponents: [
ApplyFacetDialogComponent,
AssetPreviewComponent,
UpdateProductOptionDialogComponent,
AssignProductsToChannelDialogComponent,
],
entryComponents: [ApplyFacetDialogComponent, AssetPreviewComponent, UpdateProductOptionDialogComponent],
providers: [
ProductResolver,
FacetResolver,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<ng-template vdrDialogTitle>{{ 'catalog.assign-products-to-channel' | translate }}</ng-template>

<div class="flex">
<clr-input-container>
<label>{{ 'common.channel' | translate }}</label>
<vdr-channel-assignment-control
clrInput
[multiple]="false"
[includeDefaultChannel]="false"
[formControl]="selectedChannelIdControl"
></vdr-channel-assignment-control>
</clr-input-container>
<div class="flex-spacer"></div>
<clr-input-container>
<label>{{ 'catalog.price-conversion-factor' | translate }}</label>
<input clrInput type="number" min="0" max="99999" [formControl]="priceFactorControl" />
</clr-input-container>
</div>

<div class="channel-price-preview">
<label class="clr-control-label">{{ 'catalog.channel-price-preview' | translate }}</label>
<table class="table">
<thead>
<tr>
<th>{{ 'common.name' | translate }}</th>
<th>
{{
'catalog.price-in-channel'
| translate: { channel: currentChannel?.code | channelCodeToLabel | translate }
}}
</th>
<th>
<ng-template [ngIf]="selectedChannel" [ngIfElse]="noSelection">
{{ 'catalog.price-in-channel' | translate: { channel: selectedChannel?.code } }}
</ng-template>
<ng-template #noSelection>
{{ 'catalog.no-channel-selected' | translate }}
</ng-template>
</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let row of variantsPreview$ | async">
<td>{{ row.name }}</td>
<td>{{ row.price / 100 | currency: currentChannel?.currencyCode }}</td>
<td>
<ng-template [ngIf]="selectedChannel" [ngIfElse]="noChannelSelected">
{{ row.pricePreview / 100 | currency: selectedChannel?.currencyCode }}
</ng-template>
<ng-template #noChannelSelected>
-
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>

<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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import "variables";

vdr-channel-assignment-control {
min-width: 200px;
}

.channel-price-preview {
margin-top: 24px;
table.table {
margin-top: 6px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { NotificationService } from '@vendure/admin-ui/src/app/core/providers/notification/notification.service';
import { combineLatest, from, Observable } from 'rxjs';
import { map, startWith, switchMap } from 'rxjs/operators';

import { GetChannels, ProductVariantFragment } from '../../../common/generated-types';
import { _ } from '../../../core/providers/i18n/mark-for-extraction';
import { DataService } from '../../../data/providers/data.service';
import { Dialog } from '../../../shared/providers/modal/modal.service';

@Component({
selector: 'vdr-assign-products-to-channel-dialog',
templateUrl: './assign-products-to-channel-dialog.component.html',
styleUrls: ['./assign-products-to-channel-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AssignProductsToChannelDialogComponent implements OnInit, Dialog<any> {
selectedChannel: GetChannels.Channels | null | undefined;
currentChannel: GetChannels.Channels;
availableChannels: GetChannels.Channels[];
resolveWith: (result?: any) => void;
variantsPreview$: Observable<Array<{ id: string; name: string; price: number; pricePreview: number }>>;
priceFactorControl = new FormControl(1);
selectedChannelIdControl = new FormControl();

// assigned by ModalService.fromComponent() call
productIds: string[];

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);
});

this.variantsPreview$ = combineLatest(
from(this.getTopVariants(10)),
this.priceFactorControl.valueChanges.pipe(startWith(1)),
).pipe(
map(([variants, factor]) => {
return variants.map(v => ({
id: v.id,
name: v.name,
price: v.price,
pricePreview: v.price * +factor,
}));
}),
);
}

selectChannel(channelIds: string[]) {
this.selectedChannel = this.availableChannels.find(c => c.id === channelIds[0]);
}

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,
});
this.resolveWith(true);
});
}
}

cancel() {
this.resolveWith();
}

private async getTopVariants(take: number): Promise<ProductVariantFragment[]> {
const variants: ProductVariantFragment[] = [];

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)
.toPromise();
variants.push(...(productVariants || []));
}
return variants.slice(0, take);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class CollectionListComponent implements OnInit {
) {}

ngOnInit() {
this.queryResult = this.dataService.collection.getCollections(99999, 0);
this.queryResult = this.dataService.collection.getCollections(99999, 0).refetchOnChannelChange();
this.items$ = this.queryResult.mapStream(data => data.collections.items);
this.activeCollectionId$ = this.route.paramMap.pipe(
map(pm => pm.get('contents')),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ export class FacetListComponent extends BaseListComponent<GetFacetList.Query, Ge
route: ActivatedRoute,
) {
super(router, route);
super.setQueryFn((...args: any[]) => this.dataService.facet.getFacets(...args), data => data.facets);
super.setQueryFn(
(...args: any[]) => this.dataService.facet.getFacets(...args).refetchOnChannelChange(),
data => data.facets,
);
}

toggleDisplayLimit(facet: GetFacetList.Items) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@
<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>
</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-field [label]="'catalog.product-name' | translate" for="name">
<input
id="name"
Expand Down Expand Up @@ -103,9 +119,11 @@
[removable]="'UpdateCatalog' | hasPermission"
(remove)="removeProductFacetValue(facetValue.id)"
></vdr-facet-value-chip>
<button class="btn btn-sm btn-secondary"
*vdrIfPermissions="'UpdateCatalog'"
(click)="selectProductFacetValue()">
<button
class="btn btn-sm btn-secondary"
*vdrIfPermissions="'UpdateCatalog'"
(click)="selectProductFacetValue()"
>
<clr-icon shape="plus"></clr-icon>
{{ 'catalog.add-facets' | translate }}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ import { Location } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
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 { combineLatest, EMPTY, merge, Observable } from 'rxjs';
import { distinctUntilChanged, map, mergeMap, switchMap, take, withLatestFrom } from 'rxjs/operators';
import {
distinctUntilChanged,
map,
mergeMap,
switchMap,
take,
takeUntil,
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';
import { IGNORE_CAN_DEACTIVATE_GUARD } from 'src/app/shared/providers/routing/can-deactivate-detail-guard';
Expand Down Expand Up @@ -71,6 +82,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
detailForm: FormGroup;
assetChanges: SelectedAssets = {};
variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
productChannels$: Observable<ProductWithVariants.Channels[]>;
facetValues$: Observable<ProductWithVariants.FacetValues[]>;
facets$: Observable<FacetWithValues.Fragment[]>;
selectedVariantIds: string[] = [];
Expand Down Expand Up @@ -110,7 +122,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
this.init();
this.product$ = this.entity$;
this.variants$ = this.product$.pipe(map(product => product.variants));
this.taxCategories$ = this.productDetailService.getTaxCategories();
this.taxCategories$ = this.productDetailService.getTaxCategories().pipe(takeUntil(this.destroy$));
this.activeTab$ = this.route.paramMap.pipe(map(qpm => qpm.get('tab') as any));

// FacetValues are provided initially by the nested array of the
Expand Down Expand Up @@ -138,6 +150,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
);

this.facetValues$ = merge(productFacetValues$, formChangeFacetValues$);
this.productChannels$ = this.product$.pipe(map(p => p.channels));
}

ngOnDestroy() {
Expand All @@ -153,6 +166,21 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
});
}

isDefaultChannel(channelCode: string): boolean {
return channelCode === DEFAULT_CHANNEL_CODE;
}

assignToChannel() {
this.modalService
.fromComponent(AssignProductsToChannelDialogComponent, {
size: 'lg',
locals: {
productIds: [this.id],
},
})
.subscribe();
}

customFieldIsSet(name: string): boolean {
return !!this.detailForm.get(['product', 'customFields', name]);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { EMPTY, Observable } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { delay, map, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators';

import { BaseListComponent } from '../../../common/base-list.component';
Expand Down Expand Up @@ -36,7 +36,8 @@ export class ProductListComponent
) {
super(router, route);
super.setQueryFn(
(...args: any[]) => this.dataService.product.searchProducts(this.searchTerm, ...args),
(...args: any[]) =>
this.dataService.product.searchProducts(this.searchTerm, ...args).refetchOnChannelChange(),
data => data.search,
// tslint:disable-next-line:no-shadowed-variable
(skip, take) => ({
Expand All @@ -53,7 +54,8 @@ export class ProductListComponent

ngOnInit() {
super.ngOnInit();
this.facetValues$ = this.listQuery.mapStream(data => data.search.facetValues);
this.facetValues$ = this.result$.pipe(map(data => data.search.facetValues));
// this.facetValues$ = of([]);
this.route.queryParamMap
.pipe(
map(qpm => qpm.get('q')),
Expand Down
2 changes: 1 addition & 1 deletion packages/admin-ui/src/app/common/base-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export abstract class BaseDetailComponent<Entity extends { id: string; updatedAt

init() {
this.entity$ = this.route.data.pipe(
switchMap(data => data.entity as Observable<Entity>),
switchMap(data => (data.entity as Observable<Entity>).pipe(takeUntil(this.destroy$))),
tap(entity => (this.id = entity.id)),
shareReplay(1),
);
Expand Down
Loading

0 comments on commit 59b9c91

Please sign in to comment.