Skip to content

Commit

Permalink
feat(admin-ui): Implement pagination & filtering of product variants
Browse files Browse the repository at this point in the history
Closes #411
  • Loading branch information
michaelbromley committed Aug 25, 2020
1 parent 7f9b6d7 commit e2b445b
Show file tree
Hide file tree
Showing 13 changed files with 211 additions and 100 deletions.
22 changes: 11 additions & 11 deletions packages/admin-ui/i18n-coverage.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
{
"generatedOn": "2020-07-28T15:41:10.262Z",
"lastCommit": "2f4760e74e7b14caf171772e03c3095212eb17bc",
"generatedOn": "2020-08-24T10:59:42.914Z",
"lastCommit": "6efa98be6c5120f7d12f2ef5662edabb40725bd9",
"translationStatus": {
"de": {
"tokenCount": 661,
"translatedCount": 609,
"tokenCount": 662,
"translatedCount": 610,
"percentage": 92
},
"en": {
"tokenCount": 661,
"translatedCount": 660,
"tokenCount": 662,
"translatedCount": 662,
"percentage": 100
},
"es": {
"tokenCount": 661,
"tokenCount": 662,
"translatedCount": 467,
"percentage": 71
},
"pl": {
"tokenCount": 661,
"tokenCount": 662,
"translatedCount": 566,
"percentage": 86
"percentage": 85
},
"zh_Hans": {
"tokenCount": 661,
"tokenCount": 662,
"translatedCount": 550,
"percentage": 83
},
"zh_Hant": {
"tokenCount": 661,
"tokenCount": 662,
"translatedCount": 550,
"percentage": 83
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
</vdr-ab-right>
</vdr-action-bar>

<form class="form" [formGroup]="detailForm" *ngIf="product$ | async as product" (keydown.enter)="$event.preventDefault()">
<form
class="form"
[formGroup]="detailForm"
*ngIf="product$ | async as product"
(keydown.enter)="$event.preventDefault()"
>
<clr-tabs>
<clr-tab>
<button clrTabLink (click)="navigateToTab('details')">
Expand Down Expand Up @@ -165,7 +170,7 @@ <h4>{{ 'catalog.product-variants' | translate }}</h4>
<clr-tab-content *clrIfActive="(activeTab$ | async) === 'variants'">
<section class="form-block">
<div class="view-mode">
<div class="btn-group btn-sm">
<div class="btn-group">
<button
class="btn btn-secondary-outline"
(click)="variantDisplayMode = 'card'"
Expand All @@ -183,11 +188,21 @@ <h4>{{ 'catalog.product-variants' | translate }}</h4>
{{ 'catalog.display-variant-table' | translate }}
</button>
</div>
<div class="variant-filter">
<input
[formControl]="filterInput"
class="clr-input"
[placeholder]="'catalog.filter-by-name-or-sku' | translate"
/>
<button class="icon-button" (click)="filterInput.setValue('')">
<clr-icon shape="times"></clr-icon>
</button>
</div>
<div class="flex-spacer"></div>
<a
*vdrIfPermissions="'UpdateCatalog'"
[routerLink]="['./', 'manage-variants']"
class="btn btn-secondary btn-sm edit-variants-btn"
class="btn btn-secondary edit-variants-btn"
>
<clr-icon shape="add-text"></clr-icon>
{{ 'catalog.manage-variants' | translate }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,29 @@ vdr-action-bar clr-toggle-wrapper {
margin-top: 12px;
}

.variant-filter {
flex: 1;
display: flex;
input {
flex: 1;
max-width: initial;
border-radius: 3px 0 0 3px !important;
}
.icon-button {
border: 1px solid $color-grey-300;
border-radius: 0 3px 3px 0;
border-left: none;
}
}

.group-name {
padding-right: 6px;
}

.view-mode {
display: flex;
justify-content: flex-end;
align-items: center;
}

.edit-variants-btn {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
import { Location } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
import {
BaseDetailComponent,
CreateProductInput,
createUpdatedTranslatable,
CustomFieldConfig,
DataService,
FacetWithValues,
flattenFacetValues,
IGNORE_CAN_DEACTIVATE_GUARD,
LanguageCode,
ModalService,
NotificationService,
ProductWithVariants,
ServerConfigService,
TaxCategory,
UpdateProductInput,
UpdateProductMutation,
UpdateProductOptionInput,
UpdateProductVariantInput,
UpdateProductVariantsMutation,
} from '@vendure/admin-ui/core';
import { DataService, ModalService, NotificationService, ServerConfigService } from '@vendure/admin-ui/core';
import { normalizeString } from '@vendure/common/lib/normalize-string';
import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
import { unique } from '@vendure/common/lib/unique';
import { combineLatest, EMPTY, merge, Observable } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
map,
mergeMap,
startWith,
switchMap,
take,
takeUntil,
Expand All @@ -44,6 +49,7 @@ import { VariantAssetChange } from '../product-variants-list/product-variants-li

export type TabName = 'details' | 'variants';
export interface VariantFormValue {
id: string;
enabled: boolean;
sku: string;
name: string;
Expand Down Expand Up @@ -79,6 +85,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
customOptionGroupFields: CustomFieldConfig[];
customOptionFields: CustomFieldConfig[];
detailForm: FormGroup;
filterInput = new FormControl('');
assetChanges: SelectedAssets = {};
variantAssetChanges: { [variantId: string]: SelectedAssets } = {};
productChannels$: Observable<ProductWithVariants.Channels[]>;
Expand Down Expand Up @@ -123,21 +130,35 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
ngOnInit() {
this.init();
this.product$ = this.entity$;
this.variants$ = this.product$.pipe(map((product) => product.variants));
const variants$ = this.product$.pipe(map(product => product.variants));
const filterTerm$ = this.filterInput.valueChanges.pipe(startWith(''), debounceTime(50));
this.variants$ = combineLatest(variants$, filterTerm$).pipe(
map(([variants, term]) => {
return term
? variants.filter(v => {
const lcTerm = term.toLocaleLowerCase();
return (
v.name.toLocaleLowerCase().includes(term) ||
v.sku.toLocaleLowerCase().includes(term)
);
})
: variants;
}),
);
this.taxCategories$ = this.productDetailService.getTaxCategories().pipe(takeUntil(this.destroy$));
this.activeTab$ = this.route.paramMap.pipe(map((qpm) => qpm.get('tab') as any));
this.activeTab$ = this.route.paramMap.pipe(map(qpm => qpm.get('tab') as any));

// FacetValues are provided initially by the nested array of the
// Product entity, but once a fetch to get all Facets is made (as when
// opening the FacetValue selector modal), then these additional values
// are concatenated onto the initial array.
this.facets$ = this.productDetailService.getFacets();
const productFacetValues$ = this.product$.pipe(map((product) => product.facetValues));
const productFacetValues$ = this.product$.pipe(map(product => product.facetValues));
const allFacetValues$ = this.facets$.pipe(map(flattenFacetValues));
const productGroup = this.getProductFormGroup();

const formFacetValueIdChanges$ = productGroup.valueChanges.pipe(
map((val) => val.facetValueIds as string[]),
map(val => val.facetValueIds as string[]),
distinctUntilChanged(),
);
const formChangeFacetValues$ = combineLatest(
Expand All @@ -147,12 +168,12 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
).pipe(
map(([ids, productFacetValues, allFacetValues]) => {
const combined = [...productFacetValues, ...allFacetValues];
return ids.map((id) => combined.find((fv) => fv.id === id)).filter(notNullOrUndefined);
return ids.map(id => combined.find(fv => fv.id === id)).filter(notNullOrUndefined);
}),
);

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

ngOnDestroy() {
Expand Down Expand Up @@ -194,7 +215,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
],
})
.pipe(
switchMap((response) =>
switchMap(response =>
response
? this.dataService.product.removeProductsFromChannel({
channelId,
Expand All @@ -207,7 +228,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
() => {
this.notificationService.success(_('catalog.notify-remove-product-from-channel-success'));
},
(err) => {
err => {
this.notificationService.error(_('catalog.notify-remove-product-from-channel-error'));
},
);
Expand All @@ -233,7 +254,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
* If creating a new product, automatically generate the slug based on the product name.
*/
updateSlug(nameValue: string) {
this.isNew$.pipe(take(1)).subscribe((isNew) => {
this.isNew$.pipe(take(1)).subscribe(isNew => {
if (isNew) {
const slugControl = this.detailForm.get(['product', 'slug']);
if (slugControl && slugControl.pristine) {
Expand All @@ -244,7 +265,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
}

selectProductFacetValue() {
this.displayFacetValueModal().subscribe((facetValueIds) => {
this.displayFacetValueModal().subscribe(facetValueIds => {
if (facetValueIds) {
const productGroup = this.getProductFormGroup();
const currentFacetValueIds = productGroup.value.facetValueIds;
Expand All @@ -263,7 +284,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
entity: 'ProductOption',
});
},
(err) => {
err => {
this.notificationService.error(_('common.notify-update-error'), {
entity: 'ProductOption',
});
Expand All @@ -275,7 +296,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
const productGroup = this.getProductFormGroup();
const currentFacetValueIds = productGroup.value.facetValueIds;
productGroup.patchValue({
facetValueIds: currentFacetValueIds.filter((id) => id !== facetValueId),
facetValueIds: currentFacetValueIds.filter(id => id !== facetValueId),
});
productGroup.markAsDirty();
}
Expand All @@ -289,9 +310,9 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
.subscribe(([facetValueIds, variants]) => {
if (facetValueIds) {
for (const variantId of selectedVariantIds) {
const index = variants.findIndex((v) => v.id === variantId);
const index = variants.findIndex(v => v.id === variantId);
const variant = variants[index];
const existingFacetValueIds = variant ? variant.facetValues.map((fv) => fv.id) : [];
const existingFacetValueIds = variant ? variant.facetValues.map(fv => fv.id) : [];
const variantFormGroup = this.detailForm.get(['variants', index]);
if (variantFormGroup) {
variantFormGroup.patchValue({
Expand All @@ -308,22 +329,22 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
variantsToCreateAreValid(): boolean {
return (
0 < this.createVariantsConfig.variants.length &&
this.createVariantsConfig.variants.every((v) => {
this.createVariantsConfig.variants.every(v => {
return v.sku !== '';
})
);
}

private displayFacetValueModal(): Observable<string[] | undefined> {
return this.productDetailService.getFacets().pipe(
mergeMap((facets) =>
mergeMap(facets =>
this.modalService.fromComponent(ApplyFacetDialogComponent, {
size: 'md',
closable: true,
locals: { facets },
}),
),
map((facetValues) => facetValues && facetValues.map((v) => v.id)),
map(facetValues => facetValues && facetValues.map(v => v.id)),
);
}

Expand Down Expand Up @@ -358,7 +379,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
this.detailForm.markAsPristine();
this.router.navigate(['../', productId], { relativeTo: this.route });
},
(err) => {
err => {
// tslint:disable-next-line:no-console
console.error(err);
this.notificationService.error(_('common.notify-create-error'), {
Expand Down Expand Up @@ -397,7 +418,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
}),
)
.subscribe(
(result) => {
result => {
this.updateSlugAfterSave(result);
this.detailForm.markAsPristine();
this.assetChanges = {};
Expand All @@ -407,7 +428,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
});
this.changeDetector.markForCheck();
},
(err) => {
err => {
this.notificationService.error(_('common.notify-update-error'), {
entity: 'Product',
});
Expand All @@ -423,14 +444,14 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
* Sets the values of the form on changes to the product or current language.
*/
protected setFormValues(product: ProductWithVariants.Fragment, languageCode: LanguageCode) {
const currentTranslation = product.translations.find((t) => t.languageCode === languageCode);
const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
this.detailForm.patchValue({
product: {
enabled: product.enabled,
name: currentTranslation ? currentTranslation.name : '',
slug: currentTranslation ? currentTranslation.slug : '',
description: currentTranslation ? currentTranslation.description : '',
facetValueIds: product.facetValues.map((fv) => fv.id),
facetValueIds: product.facetValues.map(fv => fv.id),
},
});

Expand All @@ -452,9 +473,10 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria

const variantsFormArray = this.detailForm.get('variants') as FormArray;
product.variants.forEach((variant, i) => {
const variantTranslation = variant.translations.find((t) => t.languageCode === languageCode);
const facetValueIds = variant.facetValues.map((fv) => fv.id);
const variantTranslation = variant.translations.find(t => t.languageCode === languageCode);
const facetValueIds = variant.facetValues.map(fv => fv.id);
const group: VariantFormValue = {
id: variant.id,
enabled: variant.enabled,
sku: variant.sku,
name: variantTranslation ? variantTranslation.name : '',
Expand Down Expand Up @@ -543,7 +565,7 @@ export class ProductDetailComponent extends BaseDetailComponent<ProductWithVaria
const formRow = variantsFormArray.get(i.toString());
return formRow && formRow.dirty;
});
const dirtyVariantValues = variantsFormArray.controls.filter((c) => c.dirty).map((c) => c.value);
const dirtyVariantValues = variantsFormArray.controls.filter(c => c.dirty).map(c => c.value);

if (dirtyVariants.length !== dirtyVariantValues.length) {
throw new Error(_(`error.product-variant-form-values-do-not-match`));
Expand Down
Loading

0 comments on commit e2b445b

Please sign in to comment.