Parcourir la source

feat(admin-ui): Auto update ProductVariant name with ProductOption name

Relates to #600
Michael Bromley il y a 5 ans
Parent
commit
0e98cb55c7

+ 14 - 14
packages/admin-ui/i18n-coverage.json

@@ -1,49 +1,49 @@
 {
-  "generatedOn": "2021-01-11T09:10:41.279Z",
-  "lastCommit": "12c46425e453d3e160a27aed41f14bd23120c9f2",
+  "generatedOn": "2021-01-11T10:15:44.646Z",
+  "lastCommit": "b1b363d91d14b3484a30d889db74491e8813c5f0",
   "translationStatus": {
     "cs": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 687,
-      "percentage": 92
+      "percentage": 91
     },
     "de": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 596,
       "percentage": 79
     },
     "en": {
-      "tokenCount": 750,
-      "translatedCount": 749,
+      "tokenCount": 751,
+      "translatedCount": 750,
       "percentage": 100
     },
     "es": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 458,
       "percentage": 61
     },
     "fr": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 692,
       "percentage": 92
     },
     "pl": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 551,
       "percentage": 73
     },
     "pt_BR": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 642,
-      "percentage": 86
+      "percentage": 85
     },
     "zh_Hans": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 533,
       "percentage": 71
     },
     "zh_Hant": {
-      "tokenCount": 750,
+      "tokenCount": 751,
       "translatedCount": 533,
       "percentage": 71
     }

+ 21 - 14
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -29,7 +29,7 @@ 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 { combineLatest, EMPTY, merge, Observable, of } from 'rxjs';
 import {
     debounceTime,
     distinctUntilChanged,
@@ -347,19 +347,26 @@ export class ProductDetailComponent
         });
     }
 
-    updateProductOption(input: UpdateProductOptionInput) {
-        this.productDetailService.updateProductOption(input).subscribe(
-            () => {
-                this.notificationService.success(_('common.notify-update-success'), {
-                    entity: 'ProductOption',
-                });
-            },
-            err => {
-                this.notificationService.error(_('common.notify-update-error'), {
-                    entity: 'ProductOption',
-                });
-            },
-        );
+    updateProductOption(input: UpdateProductOptionInput & { autoUpdate: boolean }) {
+        combineLatest(this.product$, this.languageCode$)
+            .pipe(
+                take(1),
+                mergeMap(([product, languageCode]) =>
+                    this.productDetailService.updateProductOption(input, product, languageCode),
+                ),
+            )
+            .subscribe(
+                () => {
+                    this.notificationService.success(_('common.notify-update-success'), {
+                        entity: 'ProductOption',
+                    });
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-update-error'), {
+                        entity: 'ProductOption',
+                    });
+                },
+            );
     }
 
     removeProductFacetValue(facetValueId: string) {

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/components/product-variants-list/product-variants-list.component.ts

@@ -64,7 +64,7 @@ export class ProductVariantsListComponent implements OnChanges, OnInit, OnDestro
     @Output() assetChange = new EventEmitter<VariantAssetChange>();
     @Output() selectionChange = new EventEmitter<string[]>();
     @Output() selectFacetValueClick = new EventEmitter<string[]>();
-    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput>();
+    @Output() updateProductOption = new EventEmitter<UpdateProductOptionInput & { autoUpdate: boolean }>();
     selectedVariantIds: string[] = [];
     pagination: PaginationInstance = {
         currentPage: 1,

+ 9 - 1
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html

@@ -12,6 +12,10 @@
 <vdr-form-field [label]="'common.code' | translate" for="code">
     <input id="code" type="text" #codeInput="ngModel" required [(ngModel)]="code" pattern="[a-z0-9_-]+" />
 </vdr-form-field>
+<clr-checkbox-wrapper>
+    <input type="checkbox" clrCheckbox [(ngModel)]="updateVariantName" />
+    <label>{{ 'catalog.auto-update-product-variant-name' | translate }}</label>
+</clr-checkbox-wrapper>
 <section *ngIf="customFields.length">
     <label>{{ 'common.custom-fields' | translate }}</label>
     <ng-container *ngFor="let customField of customFields">
@@ -30,7 +34,11 @@
     <button
         type="submit"
         (click)="update()"
-        [disabled]="nameInput.invalid || codeInput.invalid || (nameInput.pristine && codeInput.pristine && customFieldsForm.pristine)"
+        [disabled]="
+            nameInput.invalid ||
+            codeInput.invalid ||
+            (nameInput.pristine && codeInput.pristine && customFieldsForm.pristine)
+        "
         class="btn btn-primary"
     >
         {{ 'catalog.update-product-option' | translate }}

+ 6 - 4
packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.ts

@@ -16,8 +16,10 @@ import { normalizeString } from '@vendure/common/lib/normalize-string';
     styleUrls: ['./update-product-option-dialog.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductOptionInput>, OnInit {
-    resolveWith: (result?: UpdateProductOptionInput) => void;
+export class UpdateProductOptionDialogComponent
+    implements Dialog<UpdateProductOptionInput & { autoUpdate: boolean }>, OnInit {
+    resolveWith: (result?: UpdateProductOptionInput & { autoUpdate: boolean }) => void;
+    updateVariantName = true;
     // Provided by caller
     productOption: ProductVariant.Options;
     activeLanguage: LanguageCode;
@@ -29,7 +31,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
 
     ngOnInit(): void {
         const currentTranslation = this.productOption.translations.find(
-            (t) => t.languageCode === this.activeLanguage,
+            t => t.languageCode === this.activeLanguage,
         );
         this.name = currentTranslation?.name ?? '';
         this.code = this.productOption.code;
@@ -64,7 +66,7 @@ export class UpdateProductOptionDialogComponent implements Dialog<UpdateProductO
                 name: '',
             },
         });
-        this.resolveWith(result);
+        this.resolveWith({ ...result, autoUpdate: this.updateVariantName });
     }
 
     cancel() {

+ 58 - 16
packages/admin-ui/src/lib/catalog/src/providers/product-detail.service.ts

@@ -6,6 +6,7 @@ import {
     DeletionResult,
     FacetWithValues,
     LanguageCode,
+    ProductWithVariants,
     UpdateProductInput,
     UpdateProductMutation,
     UpdateProductOptionInput,
@@ -30,13 +31,13 @@ export class ProductDetailService {
     constructor(private dataService: DataService) {}
 
     getFacets(): Observable<FacetWithValues.Fragment[]> {
-        return this.dataService.facet.getAllFacets().mapSingle((data) => data.facets.items);
+        return this.dataService.facet.getAllFacets().mapSingle(data => data.facets.items);
     }
 
     getTaxCategories() {
         return this.dataService.settings
             .getTaxCategories()
-            .mapSingle((data) => data.taxCategories)
+            .mapSingle(data => data.taxCategories)
             .pipe(shareReplay(1));
     }
 
@@ -46,14 +47,14 @@ export class ProductDetailService {
         languageCode: LanguageCode,
     ) {
         const createProduct$ = this.dataService.product.createProduct(input);
-        const nonEmptyOptionGroups = createVariantsConfig.groups.filter((g) => 0 < g.values.length);
+        const nonEmptyOptionGroups = createVariantsConfig.groups.filter(g => 0 < g.values.length);
         const createOptionGroups$ = this.createProductOptionGroups(nonEmptyOptionGroups, languageCode);
 
         return forkJoin(createProduct$, createOptionGroups$).pipe(
             mergeMap(([{ createProduct }, optionGroups]) => {
                 const addOptionsToProduct$ = optionGroups.length
                     ? forkJoin(
-                          optionGroups.map((optionGroup) => {
+                          optionGroups.map(optionGroup => {
                               return this.dataService.product.addOptionGroupToProduct({
                                   productId: createProduct.id,
                                   optionGroupId: optionGroup.id,
@@ -68,10 +69,10 @@ export class ProductDetailService {
                 );
             }),
             mergeMap(({ createProduct, optionGroups }) => {
-                const variants = createVariantsConfig.variants.map((v) => {
+                const variants = createVariantsConfig.variants.map(v => {
                     const optionIds = optionGroups.length
                         ? v.optionValues.map((optionName, index) => {
-                              const option = optionGroups[index].options.find((o) => o.name === optionName);
+                              const option = optionGroups[index].options.find(o => o.name === optionName);
                               if (!option) {
                                   throw new Error(
                                       `Could not find a matching ProductOption "${optionName}" when creating variant`,
@@ -85,7 +86,7 @@ export class ProductDetailService {
                         optionIds,
                     };
                 });
-                const options = optionGroups.map((og) => og.options).reduce((flat, o) => [...flat, ...o], []);
+                const options = optionGroups.map(og => og.options).reduce((flat, o) => [...flat, ...o], []);
                 return this.createProductVariants(createProduct, variants, options, languageCode);
             }),
         );
@@ -94,17 +95,17 @@ export class ProductDetailService {
     createProductOptionGroups(groups: Array<{ name: string; values: string[] }>, languageCode: LanguageCode) {
         return groups.length
             ? forkJoin(
-                  groups.map((c) => {
+                  groups.map(c => {
                       return this.dataService.product
                           .createProductOptionGroups({
                               code: normalizeString(c.name, '-'),
                               translations: [{ languageCode, name: c.name }],
-                              options: c.values.map((v) => ({
+                              options: c.values.map(v => ({
                                   code: normalizeString(v, '-'),
                                   translations: [{ languageCode, name: v }],
                               })),
                           })
-                          .pipe(map((data) => data.createProductOptionGroup));
+                          .pipe(map(data => data.createProductOptionGroup));
                   }),
               )
             : of([]);
@@ -116,12 +117,12 @@ export class ProductDetailService {
         options: Array<{ id: string; name: string }>,
         languageCode: LanguageCode,
     ) {
-        const variants: CreateProductVariantInput[] = variantData.map((v) => {
+        const variants: CreateProductVariantInput[] = variantData.map(v => {
             const name = options.length
                 ? `${product.name} ${v.optionIds
-                      .map((id) => options.find((o) => o.id === id))
+                      .map(id => options.find(o => o.id === id))
                       .filter(notNullOrUndefined)
-                      .map((o) => o.name)
+                      .map(o => o.name)
                       .join(' ')}`
                 : product.name;
             return {
@@ -157,13 +158,54 @@ export class ProductDetailService {
         return forkJoin(updateOperations);
     }
 
-    updateProductOption(input: UpdateProductOptionInput) {
-        return this.dataService.product.updateProductOption(input);
+    updateProductOption(
+        input: UpdateProductOptionInput & { autoUpdate: boolean },
+        product: ProductWithVariants.Fragment,
+        languageCode: LanguageCode,
+    ) {
+        let updateProductVariantNames$: Observable<any> = of([]);
+        if (input.autoUpdate) {
+            // Update any ProductVariants' names which include the option name
+            let oldOptionName: string | undefined;
+            const newOptionName = input.translations?.find(t => t.languageCode === languageCode)?.name;
+            if (!newOptionName) {
+                updateProductVariantNames$ = of([]);
+            }
+            const variantsToUpdate: UpdateProductVariantInput[] = [];
+            for (const variant of product.variants) {
+                if (variant.options.map(o => o.id).includes(input.id)) {
+                    if (!oldOptionName) {
+                        oldOptionName = variant.options
+                            .find(o => o.id === input.id)
+                            ?.translations.find(t => t.languageCode === languageCode)?.name;
+                    }
+                    const variantName =
+                        variant.translations.find(t => t.languageCode === languageCode)?.name || '';
+                    if (oldOptionName && newOptionName && variantName.includes(oldOptionName)) {
+                        variantsToUpdate.push({
+                            id: variant.id,
+                            translations: [
+                                {
+                                    languageCode,
+                                    name: variantName.replace(oldOptionName, newOptionName),
+                                },
+                            ],
+                        });
+                    }
+                }
+            }
+            if (variantsToUpdate.length) {
+                updateProductVariantNames$ = this.dataService.product.updateProductVariants(variantsToUpdate);
+            }
+        }
+        return this.dataService.product
+            .updateProductOption(input)
+            .pipe(mergeMap(() => updateProductVariantNames$));
     }
 
     deleteProductVariant(id: string, productId: string) {
         return this.dataService.product.deleteProductVariant(id).pipe(
-            switchMap((result) => {
+            switchMap(result => {
                 if (result.deleteProductVariant.result === DeletionResult.DELETED) {
                     return this.dataService.product.getProduct(productId).single$;
                 } else {

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/cs.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Přiřadit do { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Náhled ceny v kanálu",
     "collection-contents": "Obsah kolekce",
     "confirm-adding-options-delete-default-body": "Přidáním vlastností k tomuto produktu způsobí vymazání nýnější výchozí varianty. Chcete pokračovat?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/de.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Zuweisen an { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Kanal-Preisvorschau",
     "collection-contents": "Inhalt der Sammlung",
     "confirm-adding-options-delete-default-body": "Das Hinzufügen von Optionen zu diesem Produkt führt dazu, dass die vorhandene Standardvariante gelöscht wird. Möchten Sie fortfahren?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/en.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Assign to { channelCode }",
     "assign-variant-to-channel-success": "Successfully assigned product variant to \"{ channel }\"",
     "assign-variants-to-channel": "Assign product variants to channel",
+    "auto-update-product-variant-name": "Automatically update the names of ProductVariants using this option",
     "channel-price-preview": "Channel price preview",
     "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?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/es.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Asignar a { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "collection-contents": "Contenidos de la colección",
     "confirm-adding-options-delete-default-body": "Añadir optiones a este producto eliminará la variante por defecto. ¿Desea continuar?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/fr.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Attribuer à { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "collection-contents": "Contenu de la Collection",
     "confirm-adding-options-delete-default-body": "L'ajout d'options à ce produit supprimera les variations existantes par défaut. Voulez-vous continuer ?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pl.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Przypisz do { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Podgląd cen kanału",
     "collection-contents": "Zawartość kolekcji",
     "confirm-adding-options-delete-default-body": "Dodawanie opcji spowoduje, że obecna domyślna opcja zostanie usunięta. Czy chcesz kontynuować?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Atribuir a { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
     "confirm-adding-options-delete-default-body": "Adicionar opções a este produto fará com que a variante padrão existente seja excluída. Você deseja continuar?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道价格预览",
     "collection-contents": "系列产品",
     "confirm-adding-options-delete-default-body": "添加新规格到此产品会导致含此规格的产品被删除,确认继续吗?",

+ 1 - 0
packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
+    "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道價格覽",
     "collection-contents": "系列產品",
     "confirm-adding-options-delete-default-body": "新增規格到此產品會引致包含此規格的產品被移除,確認繼續吗?",