From e2b445b67a36859aa2b360f0061bd4afdc495540 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 24 Aug 2020 13:42:58 +0200 Subject: [PATCH] feat(admin-ui): Implement pagination & filtering of product variants Closes #411 --- packages/admin-ui/i18n-coverage.json | 22 ++-- .../product-detail.component.html | 21 +++- .../product-detail.component.scss | 16 +++ .../product-detail.component.ts | 76 +++++++---- .../product-variants-list.component.html | 42 ++++--- .../product-variants-list.component.scss | 7 ++ .../product-variants-list.component.ts | 119 +++++++++++------- .../src/lib/static/i18n-messages/de.json | 1 + .../src/lib/static/i18n-messages/en.json | 3 +- .../src/lib/static/i18n-messages/es.json | 1 + .../src/lib/static/i18n-messages/pl.json | 1 + .../src/lib/static/i18n-messages/zh_Hans.json | 1 + .../src/lib/static/i18n-messages/zh_Hant.json | 1 + 13 files changed, 211 insertions(+), 100 deletions(-) diff --git a/packages/admin-ui/i18n-coverage.json b/packages/admin-ui/i18n-coverage.json index 2aff0c73c6..a2b6abe421 100644 --- a/packages/admin-ui/i18n-coverage.json +++ b/packages/admin-ui/i18n-coverage.json @@ -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 } diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html index 9b59dabaae..554e36e89c 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html @@ -45,7 +45,12 @@ -
+ +
+ + +
{{ 'catalog.manage-variants' | translate }} diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss index 9e04fd4f74..e2a1573779 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss +++ b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss @@ -12,6 +12,21 @@ 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; } @@ -19,6 +34,7 @@ vdr-action-bar clr-toggle-wrapper { .view-mode { display: flex; justify-content: flex-end; + align-items: center; } .edit-variants-btn { diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts index b1cf11a7f5..5504fc312e 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts +++ b/packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts @@ -1,6 +1,6 @@ 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 { @@ -8,11 +8,15 @@ import { CreateProductInput, createUpdatedTranslatable, CustomFieldConfig, + DataService, FacetWithValues, flattenFacetValues, IGNORE_CAN_DEACTIVATE_GUARD, LanguageCode, + ModalService, + NotificationService, ProductWithVariants, + ServerConfigService, TaxCategory, UpdateProductInput, UpdateProductMutation, @@ -20,16 +24,17 @@ import { 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, @@ -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; @@ -79,6 +85,7 @@ export class ProductDetailComponent extends BaseDetailComponent; @@ -123,21 +130,35 @@ export class ProductDetailComponent extends BaseDetailComponent 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( @@ -147,12 +168,12 @@ export class ProductDetailComponent extends BaseDetailComponent { 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() { @@ -194,7 +215,7 @@ export class ProductDetailComponent extends BaseDetailComponent + switchMap(response => response ? this.dataService.product.removeProductsFromChannel({ channelId, @@ -207,7 +228,7 @@ export class ProductDetailComponent extends BaseDetailComponent { this.notificationService.success(_('catalog.notify-remove-product-from-channel-success')); }, - (err) => { + err => { this.notificationService.error(_('catalog.notify-remove-product-from-channel-error')); }, ); @@ -233,7 +254,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + this.isNew$.pipe(take(1)).subscribe(isNew => { if (isNew) { const slugControl = this.detailForm.get(['product', 'slug']); if (slugControl && slugControl.pristine) { @@ -244,7 +265,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + this.displayFacetValueModal().subscribe(facetValueIds => { if (facetValueIds) { const productGroup = this.getProductFormGroup(); const currentFacetValueIds = productGroup.value.facetValueIds; @@ -263,7 +284,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + err => { this.notificationService.error(_('common.notify-update-error'), { entity: 'ProductOption', }); @@ -275,7 +296,7 @@ export class ProductDetailComponent extends BaseDetailComponent id !== facetValueId), + facetValueIds: currentFacetValueIds.filter(id => id !== facetValueId), }); productGroup.markAsDirty(); } @@ -289,9 +310,9 @@ export class ProductDetailComponent extends BaseDetailComponent { 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({ @@ -308,7 +329,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + this.createVariantsConfig.variants.every(v => { return v.sku !== ''; }) ); @@ -316,14 +337,14 @@ export class ProductDetailComponent extends BaseDetailComponent { 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)), ); } @@ -358,7 +379,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + err => { // tslint:disable-next-line:no-console console.error(err); this.notificationService.error(_('common.notify-create-error'), { @@ -397,7 +418,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + result => { this.updateSlugAfterSave(result); this.detailForm.markAsPristine(); this.assetChanges = {}; @@ -407,7 +428,7 @@ export class ProductDetailComponent extends BaseDetailComponent { + err => { this.notificationService.error(_('common.notify-update-error'), { entity: 'Product', }); @@ -423,14 +444,14 @@ export class ProductDetailComponent extends BaseDetailComponent 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), }, }); @@ -452,9 +473,10 @@ export class ProductDetailComponent extends BaseDetailComponent { - 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 : '', @@ -543,7 +565,7 @@ export class ProductDetailComponent extends BaseDetailComponent 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`)); diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html b/packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html index 6ab2e733d5..325c773440 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html +++ b/packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.html @@ -1,10 +1,10 @@
- +
@@ -51,7 +51,9 @@
- +