Browse Source

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

Relates to #600
Michael Bromley 5 years ago
parent
commit
69cd0d0641
19 changed files with 150 additions and 23 deletions
  1. 12 12
      packages/admin-ui/i18n-coverage.json
  2. 16 0
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html
  3. 12 0
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss
  4. 10 2
      packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts
  5. 1 1
      packages/admin-ui/src/lib/catalog/src/components/product-variants-editor/product-variants-editor.component.ts
  6. 1 1
      packages/admin-ui/src/lib/catalog/src/components/update-product-option-dialog/update-product-option-dialog.component.html
  7. 46 5
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts
  8. 24 0
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.spec.ts
  9. 17 0
      packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.ts
  10. 1 1
      packages/admin-ui/src/lib/catalog/src/public_api.ts
  11. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/cs.json
  12. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/de.json
  13. 2 1
      packages/admin-ui/src/lib/static/i18n-messages/en.json
  14. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/es.json
  15. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/fr.json
  16. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pl.json
  17. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/pt_BR.json
  18. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json
  19. 1 0
      packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json

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

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

+ 16 - 0
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.html

@@ -94,6 +94,22 @@
                                     (input)="updateSlug($event.target.value)"
                                     (input)="updateSlug($event.target.value)"
                                 />
                                 />
                             </vdr-form-field>
                             </vdr-form-field>
+                            <div
+                                class="auto-rename-wrapper"
+                                [class.visible]="(isNew$ | async) === false && detailForm.get(['product', 'name'])?.dirty"
+                            >
+                                <clr-checkbox-wrapper>
+                                    <input
+                                        clrCheckbox
+                                        type="checkbox"
+                                        id="auto-update"
+                                        formControlName="autoUpdateVariantNames"
+                                    />
+                                    <label>{{
+                                        'catalog.auto-update-product-variant-name' | translate
+                                    }}</label>
+                                </clr-checkbox-wrapper>
+                            </div>
                             <vdr-form-field
                             <vdr-form-field
                                 [label]="'catalog.slug' | translate"
                                 [label]="'catalog.slug' | translate"
                                 for="slug"
                                 for="slug"

+ 12 - 0
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.scss

@@ -44,3 +44,15 @@ vdr-action-bar clr-toggle-wrapper {
 .channel-assignment {
 .channel-assignment {
     flex-wrap: wrap;
     flex-wrap: wrap;
 }
 }
+
+.auto-rename-wrapper {
+    overflow: hidden;
+    max-height: 0;
+    padding-left: 9.5rem;
+    margin-bottom: 0;
+    transition: max-height 0.2s, margin-bottom 0.2s;
+    &.visible {
+        max-height: 24px;
+        margin-bottom: 12px;
+    }
+}

+ 10 - 2
packages/admin-ui/src/lib/catalog/src/components/product-detail/product-detail.component.ts

@@ -43,7 +43,7 @@ import {
     withLatestFrom,
     withLatestFrom,
 } from 'rxjs/operators';
 } from 'rxjs/operators';
 
 
-import { ProductDetailService } from '../../providers/product-detail.service';
+import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { ApplyFacetDialogComponent } from '../apply-facet-dialog/apply-facet-dialog.component';
 import { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { AssignProductsToChannelDialogComponent } from '../assign-products-to-channel-dialog/assign-products-to-channel-dialog.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
 import { CreateProductVariantsConfig } from '../generate-product-variants/generate-product-variants.component';
@@ -121,6 +121,7 @@ export class ProductDetailComponent
             product: this.formBuilder.group({
             product: this.formBuilder.group({
                 enabled: true,
                 enabled: true,
                 name: ['', Validators.required],
                 name: ['', Validators.required],
+                autoUpdateVariantNames: true,
                 slug: '',
                 slug: '',
                 description: '',
                 description: '',
                 facetValueIds: [[]],
                 facetValueIds: [[]],
@@ -492,7 +493,14 @@ export class ProductDetailComponent
                         );
                         );
                     }
                     }
 
 
-                    return this.productDetailService.updateProduct(productInput, variantsInput);
+                    return this.productDetailService.updateProduct({
+                        product,
+                        languageCode,
+                        autoUpdate:
+                            this.detailForm.get(['product', 'autoUpdateVariantNames'])?.value ?? false,
+                        productInput,
+                        variantsInput,
+                    });
                 }),
                 }),
             )
             )
             .subscribe(
             .subscribe(

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

@@ -19,7 +19,7 @@ import { generateAllCombinations, notNullOrUndefined } from '@vendure/common/lib
 import { EMPTY, forkJoin, Observable, of } from 'rxjs';
 import { EMPTY, forkJoin, Observable, of } from 'rxjs';
 import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
 import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
 
 
-import { ProductDetailService } from '../../providers/product-detail.service';
+import { ProductDetailService } from '../../providers/product-detail/product-detail.service';
 
 
 export interface VariantInfo {
 export interface VariantInfo {
     productVariantId?: string;
     productVariantId?: string;

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

@@ -14,7 +14,7 @@
 </vdr-form-field>
 </vdr-form-field>
 <clr-checkbox-wrapper>
 <clr-checkbox-wrapper>
     <input type="checkbox" clrCheckbox [(ngModel)]="updateVariantName" />
     <input type="checkbox" clrCheckbox [(ngModel)]="updateVariantName" />
-    <label>{{ 'catalog.auto-update-product-variant-name' | translate }}</label>
+    <label>{{ 'catalog.auto-update-option-variant-name' | translate }}</label>
 </clr-checkbox-wrapper>
 </clr-checkbox-wrapper>
 <section *ngIf="customFields.length">
 <section *ngIf="customFields.length">
     <label>{{ 'common.custom-fields' | translate }}</label>
     <label>{{ 'common.custom-fields' | translate }}</label>

+ 46 - 5
packages/admin-ui/src/lib/catalog/src/providers/product-detail.service.ts → packages/admin-ui/src/lib/catalog/src/providers/product-detail/product-detail.service.ts

@@ -18,7 +18,9 @@ import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
 import { forkJoin, Observable, of, throwError } from 'rxjs';
 import { forkJoin, Observable, of, throwError } from 'rxjs';
 import { map, mergeMap, shareReplay, switchMap } from 'rxjs/operators';
 import { map, mergeMap, shareReplay, switchMap } from 'rxjs/operators';
 
 
-import { CreateProductVariantsConfig } from '../components/generate-product-variants/generate-product-variants.component';
+import { CreateProductVariantsConfig } from '../../components/generate-product-variants/generate-product-variants.component';
+
+import { replaceLast } from './replace-last';
 
 
 /**
 /**
  * Handles the logic for making the API calls to perform CRUD operations on a Product and its related
  * Handles the logic for making the API calls to perform CRUD operations on a Product and its related
@@ -147,13 +149,52 @@ export class ProductDetailService {
         );
         );
     }
     }
 
 
-    updateProduct(productInput?: UpdateProductInput, variantInput?: UpdateProductVariantInput[]) {
+    updateProduct(updateOptions: {
+        product: ProductWithVariants.Fragment;
+        languageCode: LanguageCode;
+        autoUpdate: boolean;
+        productInput?: UpdateProductInput;
+        variantsInput?: UpdateProductVariantInput[];
+    }) {
+        const { product, languageCode, autoUpdate, productInput, variantsInput } = updateOptions;
         const updateOperations: Array<Observable<UpdateProductMutation | UpdateProductVariantsMutation>> = [];
         const updateOperations: Array<Observable<UpdateProductMutation | UpdateProductVariantsMutation>> = [];
+        const updateVariantsInput = variantsInput || [];
         if (productInput) {
         if (productInput) {
             updateOperations.push(this.dataService.product.updateProduct(productInput));
             updateOperations.push(this.dataService.product.updateProduct(productInput));
+
+            const productOldName = product.translations?.find(t => t.languageCode === languageCode)?.name;
+            const productNewName = productInput.translations?.find(t => t.languageCode === languageCode)
+                ?.name;
+            if (productOldName && productNewName && autoUpdate) {
+                for (const variant of product.variants) {
+                    const currentVariantName =
+                        variant.translations.find(t => t.languageCode === languageCode)?.name || '';
+                    let variantInput: UpdateProductVariantInput;
+                    const existingVariantInput = updateVariantsInput.find(i => i.id === variant.id);
+                    if (existingVariantInput) {
+                        variantInput = existingVariantInput;
+                    } else {
+                        variantInput = {
+                            id: variant.id,
+                            translations: [{ languageCode, name: currentVariantName }],
+                        };
+                        updateVariantsInput.push(variantInput);
+                    }
+                    const variantTranslation = variantInput.translations?.find(
+                        t => t.languageCode === languageCode,
+                    );
+                    if (variantTranslation) {
+                        variantTranslation.name = replaceLast(
+                            variantTranslation.name,
+                            productOldName,
+                            productNewName,
+                        );
+                    }
+                }
+            }
         }
         }
-        if (variantInput) {
-            updateOperations.push(this.dataService.product.updateProductVariants(variantInput));
+        if (updateVariantsInput.length) {
+            updateOperations.push(this.dataService.product.updateProductVariants(updateVariantsInput));
         }
         }
         return forkJoin(updateOperations);
         return forkJoin(updateOperations);
     }
     }
@@ -187,7 +228,7 @@ export class ProductDetailService {
                             translations: [
                             translations: [
                                 {
                                 {
                                     languageCode,
                                     languageCode,
-                                    name: variantName.replace(oldOptionName, newOptionName),
+                                    name: replaceLast(variantName, oldOptionName, newOptionName),
                                 },
                                 },
                             ],
                             ],
                         });
                         });

+ 24 - 0
packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.spec.ts

@@ -0,0 +1,24 @@
+import { replaceLast } from './replace-last';
+
+describe('replaceLast()', () => {
+    it('leaves non-matching strings intact', () => {
+        expect(replaceLast('foo bar baz', 'find', 'replace')).toBe('foo bar baz');
+    });
+    it('term is at start of target', () => {
+        expect(replaceLast('find bar baz', 'find', 'replace')).toBe('replace bar baz');
+    });
+    it('term is at end of target', () => {
+        expect(replaceLast('foo bar find', 'find', 'replace')).toBe('foo bar replace');
+    });
+    it('replaces last of 2 occurrences', () => {
+        expect(replaceLast('foo find bar find', 'find', 'replace')).toBe('foo find bar replace');
+    });
+    it('replaces last of 2 consecutive occurrences', () => {
+        expect(replaceLast('foo find find bar', 'find', 'replace')).toBe('foo find replace bar');
+    });
+    it('replaces last of 3 occurrences', () => {
+        expect(replaceLast('find foo find bar find baz', 'find', 'replace')).toBe(
+            'find foo find bar replace baz',
+        );
+    });
+});

+ 17 - 0
packages/admin-ui/src/lib/catalog/src/providers/product-detail/replace-last.ts

@@ -0,0 +1,17 @@
+/**
+ * @description
+ * Like String.prototype.replace(), but replaces the last instance
+ * rather than the first.
+ */
+export function replaceLast(target: string | undefined | null, search: string, replace: string): string {
+    if (!target) {
+        return '';
+    }
+    const lastIndex = target.lastIndexOf(search);
+    if (lastIndex === -1) {
+        return target;
+    }
+    const head = target.substr(0, lastIndex);
+    const tail = target.substr(lastIndex).replace(search, replace);
+    return head + tail;
+}

+ 1 - 1
packages/admin-ui/src/lib/catalog/src/public_api.ts

@@ -25,7 +25,7 @@ export * from './components/product-variants-list/product-variants-list.componen
 export * from './components/product-variants-table/product-variants-table.component';
 export * from './components/product-variants-table/product-variants-table.component';
 export * from './components/update-product-option-dialog/update-product-option-dialog.component';
 export * from './components/update-product-option-dialog/update-product-option-dialog.component';
 export * from './components/variant-price-detail/variant-price-detail.component';
 export * from './components/variant-price-detail/variant-price-detail.component';
-export * from './providers/product-detail.service';
+export * from './providers/product-detail/product-detail.service';
 export * from './providers/routing/asset-resolver';
 export * from './providers/routing/asset-resolver';
 export * from './providers/routing/collection-resolver';
 export * from './providers/routing/collection-resolver';
 export * from './providers/routing/facet-resolver';
 export * from './providers/routing/facet-resolver';

+ 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-to-named-channel": "Přiřadit do { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Náhled ceny v kanálu",
     "channel-price-preview": "Náhled ceny v kanálu",
     "collection-contents": "Obsah kolekce",
     "collection-contents": "Obsah kolekce",

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

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Zuweisen an { channelCode }",
     "assign-to-named-channel": "Zuweisen an { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Kanal-Preisvorschau",
     "channel-price-preview": "Kanal-Preisvorschau",
     "collection-contents": "Inhalt der Sammlung",
     "collection-contents": "Inhalt der Sammlung",

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

@@ -60,7 +60,8 @@
     "assign-to-named-channel": "Assign to { channelCode }",
     "assign-to-named-channel": "Assign to { channelCode }",
     "assign-variant-to-channel-success": "Successfully assigned product variant to \"{ channel }\"",
     "assign-variant-to-channel-success": "Successfully assigned product variant to \"{ channel }\"",
     "assign-variants-to-channel": "Assign product variants 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",
+    "auto-update-option-variant-name": "Automatically update the names of ProductVariants using this option",
+    "auto-update-product-variant-name": "Automatically update the names of ProductVariants",
     "channel-price-preview": "Channel price preview",
     "channel-price-preview": "Channel price preview",
     "collection-contents": "Collection contents",
     "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-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-to-named-channel": "Asignar a { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "channel-price-preview": "Vista previa de precio para el canal de ventas",
     "collection-contents": "Contenidos de la colección",
     "collection-contents": "Contenidos de la colección",

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

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Attribuer à { channelCode }",
     "assign-to-named-channel": "Attribuer à { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "channel-price-preview": "Prévisualisation du prix du canal",
     "collection-contents": "Contenu de la Collection",
     "collection-contents": "Contenu de la Collection",

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

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "Przypisz do { channelCode }",
     "assign-to-named-channel": "Przypisz do { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Podgląd cen kanału",
     "channel-price-preview": "Podgląd cen kanału",
     "collection-contents": "Zawartość kolekcji",
     "collection-contents": "Zawartość kolekcji",

+ 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-to-named-channel": "Atribuir a { channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "Visualizar preço do canal",
     "channel-price-preview": "Visualizar preço do canal",
     "collection-contents": "Conteúdo da categoria",
     "collection-contents": "Conteúdo da categoria",

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

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道价格预览",
     "channel-price-preview": "渠道价格预览",
     "collection-contents": "系列产品",
     "collection-contents": "系列产品",

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

@@ -60,6 +60,7 @@
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-to-named-channel": "分配到{ channelCode }",
     "assign-variant-to-channel-success": "",
     "assign-variant-to-channel-success": "",
     "assign-variants-to-channel": "",
     "assign-variants-to-channel": "",
+    "auto-update-option-variant-name": "",
     "auto-update-product-variant-name": "",
     "auto-update-product-variant-name": "",
     "channel-price-preview": "渠道價格覽",
     "channel-price-preview": "渠道價格覽",
     "collection-contents": "系列產品",
     "collection-contents": "系列產品",