Browse Source

feat(admin-ui): Implement updating of product variants

Michael Bromley 7 years ago
parent
commit
79a79caa44

+ 2 - 1
admin-ui/src/app/catalog/catalog.module.ts

@@ -8,6 +8,7 @@ import { CreateOptionGroupDialogComponent } from './components/create-option-gro
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductDetailComponent } from './components/product-detail/product-detail.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { ProductListComponent } from './components/product-list/product-list.component';
 import { SelectOptionGroupDialogComponent } from './components/select-option-group-dialog/select-option-group-dialog.component';
 import { SelectOptionGroupDialogComponent } from './components/select-option-group-dialog/select-option-group-dialog.component';
+import { ProductUpdaterService } from './providers/product-updater/product-updater.service';
 import { ProductResolver } from './providers/routing/product-resolver';
 import { ProductResolver } from './providers/routing/product-resolver';
 
 
 @NgModule({
 @NgModule({
@@ -20,6 +21,6 @@ import { ProductResolver } from './providers/routing/product-resolver';
         SelectOptionGroupDialogComponent,
         SelectOptionGroupDialogComponent,
     ],
     ],
     entryComponents: [CreateOptionGroupDialogComponent, SelectOptionGroupDialogComponent],
     entryComponents: [CreateOptionGroupDialogComponent, SelectOptionGroupDialogComponent],
-    providers: [ProductResolver],
+    providers: [ProductResolver, ProductUpdaterService],
 })
 })
 export class CatalogModule {}
 export class CatalogModule {}

+ 31 - 1
admin-ui/src/app/catalog/components/product-detail/product-detail.component.html

@@ -17,7 +17,7 @@
             type="submit"
             type="submit"
             [disabled]="productForm.invalid || productForm.pristine">{{ 'common.update' | translate }}</button>
             [disabled]="productForm.invalid || productForm.pristine">{{ 'common.update' | translate }}</button>
 
 
-    <section class="form-block">
+    <section class="form-block" formGroupName="product">
         <label>{{ 'catalog.product' | translate }}</label>
         <label>{{ 'catalog.product' | translate }}</label>
         <vdr-form-field [label]="'catalog.product-name' | translate" for="name">
         <vdr-form-field [label]="'catalog.product-name' | translate" for="name">
             <input id="name" type="text" formControlName="name">
             <input id="name" type="text" formControlName="name">
@@ -60,6 +60,36 @@
 
 
     <section class="form-block">
     <section class="form-block">
         <label>{{ 'catalog.product-variants' | translate }}</label>
         <label>{{ 'catalog.product-variants' | translate }}</label>
+
+        <table class="variants-list table" formArrayName="variants">
+            <thead>
+            <tr>
+                <th>{{ 'catalog.product-variant-table-sku' | translate }}</th>
+                <th>{{ 'catalog.product-variant-table-name' | translate }}</th>
+                <th>{{ 'catalog.product-variant-table-options' | translate }}</th>
+                <th>{{ 'catalog.product-variant-table-price' | translate }}</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr class="variant"
+                *ngFor="let variant of variants$ | async; let i = index"
+                [formGroupName]="i">
+                <td>
+                    <input type="text" formControlName="sku">
+                </td>
+                <td>
+                    <input type="text" formControlName="name">
+                </td>
+                <td>
+                    <div *ngFor="let option of variant.options">{{ option.name }}</div>
+                </td>
+                <td>
+                    <input type="number" min="0" formControlName="price">
+                </td>
+            </tr>
+            </tbody>
+        </table>
+
     </section>
     </section>
 
 
 </form>
 </form>

+ 9 - 0
admin-ui/src/app/catalog/components/product-detail/product-detail.component.scss

@@ -23,3 +23,12 @@
     border: none;
     border: none;
     color: $color-warning;
     color: $color-warning;
 }
 }
+
+.variants-list {
+
+    .variant {
+        input {
+            width: 100%;
+        }
+    }
+}

+ 85 - 23
admin-ui/src/app/catalog/components/product-detail/product-detail.component.ts

@@ -1,14 +1,21 @@
 import { Component, OnDestroy } from '@angular/core';
 import { Component, OnDestroy } from '@angular/core';
-import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { ActivatedRoute, Router } from '@angular/router';
-import { combineLatest, Observable, Subject } from 'rxjs';
+import { combineLatest, forkJoin, Observable, Subject } from 'rxjs';
 import { filter, map, mergeMap, take, takeUntil, tap } from 'rxjs/operators';
 import { filter, map, mergeMap, take, takeUntil, tap } from 'rxjs/operators';
 
 
 import { notNullOrUndefined } from '../../../../../../shared/shared-utils';
 import { notNullOrUndefined } from '../../../../../../shared/shared-utils';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
+import { _ } from '../../../core/providers/i18n/mark-for-extraction';
+import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 import { DataService } from '../../../data/providers/data.service';
-import { GetProductWithVariants_product, LanguageCode } from '../../../data/types/gql-generated-types';
+import {
+    GetProductWithVariants_product,
+    GetProductWithVariants_product_variants,
+    LanguageCode,
+} from '../../../data/types/gql-generated-types';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
 import { ModalService } from '../../../shared/providers/modal/modal.service';
+import { ProductUpdaterService } from '../../providers/product-updater/product-updater.service';
 import { CreateOptionGroupDialogComponent } from '../create-option-group-dialog/create-option-group-dialog.component';
 import { CreateOptionGroupDialogComponent } from '../create-option-group-dialog/create-option-group-dialog.component';
 import { SelectOptionGroupDialogComponent } from '../select-option-group-dialog/select-option-group-dialog.component';
 import { SelectOptionGroupDialogComponent } from '../select-option-group-dialog/select-option-group-dialog.component';
 
 
@@ -19,6 +26,7 @@ import { SelectOptionGroupDialogComponent } from '../select-option-group-dialog/
 })
 })
 export class ProductDetailComponent implements OnDestroy {
 export class ProductDetailComponent implements OnDestroy {
     product$: Observable<GetProductWithVariants_product>;
     product$: Observable<GetProductWithVariants_product>;
+    variants$: Observable<GetProductWithVariants_product_variants[]>;
     availableLanguages$: Observable<LanguageCode[]>;
     availableLanguages$: Observable<LanguageCode[]>;
     languageCode$: Observable<LanguageCode>;
     languageCode$: Observable<LanguageCode>;
     productForm: FormGroup;
     productForm: FormGroup;
@@ -30,12 +38,18 @@ export class ProductDetailComponent implements OnDestroy {
         private route: ActivatedRoute,
         private route: ActivatedRoute,
         private formBuilder: FormBuilder,
         private formBuilder: FormBuilder,
         private modalService: ModalService,
         private modalService: ModalService,
+        private notificationService: NotificationService,
+        private productUpdaterService: ProductUpdaterService,
     ) {
     ) {
         this.product$ = this.route.snapshot.data.product;
         this.product$ = this.route.snapshot.data.product;
+        this.variants$ = this.product$.pipe(map(product => product.variants));
         this.productForm = this.formBuilder.group({
         this.productForm = this.formBuilder.group({
-            name: ['', Validators.required],
-            slug: '',
-            description: '',
+            product: this.formBuilder.group({
+                name: ['', Validators.required],
+                slug: '',
+                description: '',
+            }),
+            variants: this.formBuilder.array([]),
         });
         });
 
 
         this.languageCode$ = this.route.queryParamMap.pipe(
         this.languageCode$ = this.route.queryParamMap.pipe(
@@ -50,10 +64,32 @@ export class ProductDetailComponent implements OnDestroy {
             .subscribe(([product, languageCode]) => {
             .subscribe(([product, languageCode]) => {
                 const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
                 const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
                 if (currentTranslation) {
                 if (currentTranslation) {
-                    this.productForm.setValue({
-                        name: currentTranslation.name,
-                        slug: currentTranslation.slug,
-                        description: currentTranslation.description,
+                    this.productForm.patchValue({
+                        product: {
+                            name: currentTranslation.name,
+                            slug: currentTranslation.slug,
+                            description: currentTranslation.description,
+                        },
+                    });
+
+                    const variantsFormArray = this.productForm.get('variants') as FormArray;
+                    product.variants.forEach((variant, i) => {
+                        const variantTranslation = variant.translations.find(
+                            t => t.languageCode === languageCode,
+                        );
+
+                        const group = {
+                            sku: variant.sku,
+                            name: variantTranslation ? variantTranslation.name : '',
+                            price: variant.price,
+                        };
+
+                        const existing = variantsFormArray.at(i);
+                        if (existing) {
+                            existing.setValue(group);
+                        } else {
+                            variantsFormArray.insert(i, this.formBuilder.group(group));
+                        }
                     });
                     });
                 }
                 }
             });
             });
@@ -141,19 +177,45 @@ export class ProductDetailComponent implements OnDestroy {
 
 
     save() {
     save() {
         combineLatest(this.product$, this.languageCode$)
         combineLatest(this.product$, this.languageCode$)
-            .pipe(take(1))
-            .subscribe(([product, languageCode]) => {
-                const currentTranslation = product.translations.find(t => t.languageCode === languageCode);
-                if (!currentTranslation) {
-                    return;
-                }
-                const index = product.translations.indexOf(currentTranslation);
-                const newTranslation = Object.assign({}, currentTranslation, this.productForm.value);
-                const newProduct = { ...product, ...{ translations: product.translations.slice() } };
-                newProduct.translations.splice(index, 1, newTranslation);
-                this.dataService.product.updateProduct(newProduct).subscribe();
-                this.productForm.markAsPristine();
-            });
+            .pipe(
+                take(1),
+                mergeMap(([product, languageCode]) => {
+                    const productGroup = this.productForm.get('product');
+                    const updateOperations: Array<Observable<any>> = [];
+
+                    if (productGroup && productGroup.dirty) {
+                        const newProduct = this.productUpdaterService.getUpdatedProduct(
+                            product,
+                            productGroup.value,
+                            languageCode,
+                        );
+                        if (newProduct) {
+                            updateOperations.push(this.dataService.product.updateProduct(newProduct));
+                        }
+                    }
+                    const variantsArray = this.productForm.get('variants');
+                    if (variantsArray && variantsArray.dirty) {
+                        const newVariants = this.productUpdaterService.getUpdatedProductVariants(
+                            product.variants,
+                            variantsArray.value,
+                            languageCode,
+                        );
+                        updateOperations.push(this.dataService.product.updateProductVariants(newVariants));
+                    }
+
+                    return forkJoin(updateOperations);
+                }),
+            )
+            .subscribe(
+                () => {
+                    this.productForm.markAsPristine();
+                    this.productForm.markAsPristine();
+                    this.notificationService.success(_('catalog.notify-update-product-success'));
+                },
+                err => {
+                    this.notificationService.success(_('catalog.notify-update-product-error'));
+                },
+            );
     }
     }
 
 
     private setQueryParam(key: string, value: any) {
     private setQueryParam(key: string, value: any) {

+ 158 - 0
admin-ui/src/app/catalog/providers/product-updater/product-updater.service.spec.ts

@@ -0,0 +1,158 @@
+import { TestBed } from '@angular/core/testing';
+
+import { GetProductWithVariants_product, LanguageCode } from '../../../data/types/gql-generated-types';
+
+import { ProductUpdaterService } from './product-updater.service';
+
+// tslint:disable:no-non-null-assertion
+describe('ProductUpdaterService', () => {
+    let productUpdaterService: ProductUpdaterService;
+    let product: any;
+
+    beforeEach(() => {
+        TestBed.configureTestingModule({
+            providers: [ProductUpdaterService],
+        });
+        productUpdaterService = TestBed.get(ProductUpdaterService);
+    });
+
+    beforeEach(() => {
+        product = {
+            id: '1',
+            languageCode: LanguageCode.en,
+            name: 'Old Name EN',
+            image: 'old-image.jpg',
+            translations: [
+                { languageCode: LanguageCode.en, name: 'Old Name EN' },
+                { languageCode: LanguageCode.de, name: 'Old Name DE' },
+            ],
+            variants: [
+                {
+                    id: '11',
+                    name: 'Old Variant 1 Name EN',
+                    sku: '11A',
+                    price: 11,
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'Old Variant 1 Name EN' },
+                        { languageCode: LanguageCode.de, name: 'Old Variant 1 Name DE' },
+                    ],
+                },
+                {
+                    id: '12',
+                    name: 'Old Variant 2 Name EN',
+                    sku: '12A',
+                    price: 12,
+                    translations: [
+                        { languageCode: LanguageCode.en, name: 'Old Variant 2 Name EN' },
+                        { languageCode: LanguageCode.de, name: 'Old Variant 2 Name DE' },
+                    ],
+                },
+            ],
+        } as Partial<GetProductWithVariants_product>;
+    });
+
+    describe('getUpdatedProduct()', () => {
+        it('returns a clone', () => {
+            const formValue = {};
+            const result = productUpdaterService.getUpdatedProduct(product, formValue, LanguageCode.en);
+
+            expect(result).not.toBe(product);
+        });
+
+        it('returns undefined if the specified translation does not exist', () => {
+            const formValue = {
+                name: 'New Name EN',
+            };
+            const result = productUpdaterService.getUpdatedProduct(product, formValue, LanguageCode.aa);
+
+            expect(result).toBe(undefined);
+        });
+
+        it('updates the non-translatable properties', () => {
+            const formValue = {
+                image: 'new-image.jpg',
+            };
+
+            const result = productUpdaterService.getUpdatedProduct(product, formValue, LanguageCode.en);
+
+            if (!result) {
+                fail('Expected result to be truthy');
+                return;
+            }
+
+            expect(result.image).toBe('new-image.jpg');
+        });
+
+        it('updates only the specified translation', () => {
+            const formValue = {
+                name: 'New Name EN',
+            };
+
+            const result = productUpdaterService.getUpdatedProduct(product, formValue, LanguageCode.en);
+
+            if (!result) {
+                fail('Expected result to be truthy');
+                return;
+            }
+
+            expect(result.translations).not.toBe(product.translations);
+            expect(result.translations[0]!.name).toBe('New Name EN');
+            expect(result.translations[1]!.name).toBe('Old Name DE');
+        });
+    });
+
+    describe('getUpdatedProductVariants()', () => {
+        it('returns a new array and cloned objects within the array', () => {
+            const formValue = [{ name: 'New Variant 1 Name EN' }, { name: 'New Variant 2 Name EN' }];
+            const result = productUpdaterService.getUpdatedProductVariants(
+                product.variants,
+                formValue,
+                LanguageCode.en,
+            );
+
+            expect(result).not.toBe(product.variants);
+            expect(result[0]).not.toBe(product.variants[0]);
+            expect(result[1]).not.toBe(product.variants[1]);
+        });
+
+        it('throws if the length of the formValues array does not match the number of variants', () => {
+            const formValue = [{ name: 'New Variant 1 Name EN' }];
+            const invoke = () =>
+                productUpdaterService.getUpdatedProductVariants(product.variants, formValue, LanguageCode.en);
+
+            expect(invoke).toThrowError('error.product-variant-form-values-do-not-match');
+        });
+
+        it('updates the non-translatable properties', () => {
+            const formValue = [{ price: 98, sku: '11B' }, { price: 99, sku: '12B' }];
+
+            const result = productUpdaterService.getUpdatedProductVariants(
+                product.variants,
+                formValue,
+                LanguageCode.en,
+            );
+
+            expect(result[0].price).toBe(98);
+            expect(result[0].sku).toBe('11B');
+            expect(result[1].price).toBe(99);
+            expect(result[1].sku).toBe('12B');
+        });
+
+        it('updates only the specified translation', () => {
+            const formValue = [{ name: 'New Variant 1 Name EN' }, { name: 'New Variant 2 Name EN' }];
+
+            const result = productUpdaterService.getUpdatedProductVariants(
+                product.variants,
+                formValue,
+                LanguageCode.en,
+            );
+
+            expect(result[0].translations).not.toBe(product.variants[0].translations);
+            expect(result[0].translations[0].name).toBe('New Variant 1 Name EN');
+            expect(result[0].translations[1].name).toBe('Old Variant 1 Name DE');
+            expect(result[1].translations).not.toBe(product.variants[1].translations);
+            expect(result[1].translations[0].name).toBe('New Variant 2 Name EN');
+            expect(result[1].translations[1].name).toBe('Old Variant 2 Name DE');
+        });
+    });
+});

+ 82 - 0
admin-ui/src/app/catalog/providers/product-updater/product-updater.service.ts

@@ -0,0 +1,82 @@
+import { Injectable } from '@angular/core';
+
+import { notNullOrUndefined } from '../../../../../../shared/shared-utils';
+import { _ } from '../../../core/providers/i18n/mark-for-extraction';
+import {
+    GetProductWithVariants_product,
+    GetProductWithVariants_product_variants,
+    LanguageCode,
+    UpdateProductInput,
+    UpdateProductVariantInput,
+} from '../../../data/types/gql-generated-types';
+
+/**
+ * A utility class containing method used to prepare product data from the ProductDetailComponent for
+ * sending to the API.
+ */
+@Injectable()
+export class ProductUpdaterService {
+    /**
+     * Given a product and the value of the productForm, this method creates an updated copy of the product which
+     * can then be persisted to the API.
+     */
+    getUpdatedProduct(
+        product: GetProductWithVariants_product,
+        formValue: { [key: string]: any },
+        languageCode: LanguageCode,
+    ): UpdateProductInput | undefined {
+        return this.createUpdatedTranslatable(product, formValue, languageCode);
+    }
+
+    /**
+     * Given an array of product variants and the values from the productForm, this method creates an new array
+     * which can be persisted to the API.
+     */
+    getUpdatedProductVariants(
+        variants: GetProductWithVariants_product_variants[],
+        formValue: Array<{ [key: string]: any }>,
+        languageCode: LanguageCode,
+    ): UpdateProductVariantInput[] {
+        if (variants.length !== formValue.length) {
+            throw new Error(_(`error.product-variant-form-values-do-not-match`));
+        }
+        return variants
+            .map((variant, i) => {
+                return this.createUpdatedTranslatable(variant, formValue[i], languageCode);
+            })
+            .filter(notNullOrUndefined);
+    }
+
+    private createUpdatedTranslatable<T extends { translations: any[] }>(
+        translatable: T,
+        updatedFields: { [key: string]: any },
+        languageCode: LanguageCode,
+    ): T | undefined {
+        const currentTranslation = translatable.translations.find(t => t.languageCode === languageCode);
+        if (!currentTranslation) {
+            return;
+        }
+        const index = translatable.translations.indexOf(currentTranslation);
+        const newTranslation = this.patchObject(currentTranslation, updatedFields);
+        const newTranslatable = {
+            ...(this.patchObject(translatable, updatedFields) as any),
+            ...{ translations: translatable.translations.slice() },
+        };
+        newTranslatable.translations.splice(index, 1, newTranslation);
+        return newTranslatable;
+    }
+
+    /**
+     * Returns a shallow clone of `obj` with any properties contained in `patch` overwriting
+     * those of `obj`.
+     */
+    private patchObject<T extends { [key: string]: any }>(obj: T, patch: { [key: string]: any }): T {
+        const clone = Object.assign({}, obj);
+        Object.keys(clone).forEach(key => {
+            if (patch.hasOwnProperty(key)) {
+                clone[key] = patch[key];
+            }
+        });
+        return clone;
+    }
+}

+ 1 - 1
admin-ui/src/app/core/providers/i18n/mark-for-extraction.ts

@@ -3,6 +3,6 @@
  * The purpose of this function is to mark strings for extraction by ngx-translate-extract.
  * The purpose of this function is to mark strings for extraction by ngx-translate-extract.
  * See https://github.com/biesbjerg/ngx-translate-extract/tree/7d5d38e6a17c2232407bf6b0bc65808d5f81208d#mark-strings-for-extraction-using-a-marker-function
  * See https://github.com/biesbjerg/ngx-translate-extract/tree/7d5d38e6a17c2232407bf6b0bc65808d5f81208d#mark-strings-for-extraction-using-a-marker-function
  */
  */
-export function _(key: string | string[]): string | string[] {
+export function _<T extends string | string[]>(key: T): T {
     return key;
     return key;
 }
 }

+ 15 - 0
admin-ui/src/app/data/mutations/product-mutations.ts

@@ -14,6 +14,21 @@ export const UPDATE_PRODUCT = gql`
     ${PRODUCT_WITH_VARIANTS_FRAGMENT}
     ${PRODUCT_WITH_VARIANTS_FRAGMENT}
 `;
 `;
 
 
+export const UPDATE_PRODUCT_VARIANTS = gql`
+    mutation UpdateProductVariants($input: [UpdateProductVariantInput!]!) {
+        updateProductVariants(input: $input) {
+            id
+            name
+            price
+            sku
+            translations {
+                id
+                name
+            }
+        }
+    }
+`;
+
 export const CREATE_PRODUCT_OPTION_GROUP = gql`
 export const CREATE_PRODUCT_OPTION_GROUP = gql`
     mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
     mutation CreateProductOptionGroup($input: CreateProductOptionGroupInput!) {
         createProductOptionGroup(input: $input) {
         createProductOptionGroup(input: $input) {

+ 1 - 0
admin-ui/src/app/data/providers/data.service.mock.ts

@@ -44,6 +44,7 @@ export class MockDataService implements DataServiceMock {
         getProducts: spyQueryResult('getProducts'),
         getProducts: spyQueryResult('getProducts'),
         getProduct: spyQueryResult('getProduct'),
         getProduct: spyQueryResult('getProduct'),
         updateProduct: spyObservable('updateProduct'),
         updateProduct: spyObservable('updateProduct'),
+        updateProductVariants: spyObservable('updateProductVariants'),
         createProductOptionGroups: spyObservable('createProductOptionGroups'),
         createProductOptionGroups: spyObservable('createProductOptionGroups'),
         addOptionGroupToProduct: spyObservable('addOptionGroupToProduct'),
         addOptionGroupToProduct: spyObservable('addOptionGroupToProduct'),
         removeOptionGroupFromProduct: spyObservable('removeOptionGroupFromProduct'),
         removeOptionGroupFromProduct: spyObservable('removeOptionGroupFromProduct'),

+ 20 - 0
admin-ui/src/app/data/providers/product-data.service.ts

@@ -6,6 +6,7 @@ import {
     CREATE_PRODUCT_OPTION_GROUP,
     CREATE_PRODUCT_OPTION_GROUP,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     REMOVE_OPTION_GROUP_FROM_PRODUCT,
     UPDATE_PRODUCT,
     UPDATE_PRODUCT,
+    UPDATE_PRODUCT_VARIANTS,
 } from '../mutations/product-mutations';
 } from '../mutations/product-mutations';
 import {
 import {
     GET_PRODUCT_LIST,
     GET_PRODUCT_LIST,
@@ -29,6 +30,9 @@ import {
     UpdateProduct,
     UpdateProduct,
     UpdateProductInput,
     UpdateProductInput,
     UpdateProductVariables,
     UpdateProductVariables,
+    UpdateProductVariantInput,
+    UpdateProductVariants,
+    UpdateProductVariantsVariables,
 } from '../types/gql-generated-types';
 } from '../types/gql-generated-types';
 import { QueryResult } from '../types/query-result';
 import { QueryResult } from '../types/query-result';
 
 
@@ -66,6 +70,22 @@ export class ProductDataService {
         return this.baseDataService.mutate<UpdateProduct, UpdateProductVariables>(UPDATE_PRODUCT, input);
         return this.baseDataService.mutate<UpdateProduct, UpdateProductVariables>(UPDATE_PRODUCT, input);
     }
     }
 
 
+    updateProductVariants(variants: UpdateProductVariantInput[]): Observable<UpdateProductVariants> {
+        const input: UpdateProductVariantsVariables = {
+            input: variants.map(v => ({
+                id: v.id,
+                translations: v.translations,
+                sku: v.sku,
+                image: v.image,
+                price: v.price,
+            })),
+        };
+        return this.baseDataService.mutate<UpdateProductVariants, UpdateProductVariantsVariables>(
+            UPDATE_PRODUCT_VARIANTS,
+            input,
+        );
+    }
+
     createProductOptionGroups(
     createProductOptionGroups(
         productOptionGroup: CreateProductOptionGroupInput,
         productOptionGroup: CreateProductOptionGroupInput,
     ): Observable<CreateProductOptionGroup> {
     ): Observable<CreateProductOptionGroup> {

+ 70 - 24
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -118,9 +118,9 @@ export interface UpdateProduct_updateProduct_variants {
     __typename: 'ProductVariant';
     __typename: 'ProductVariant';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    price: number | null;
-    sku: string | null;
+    name: string;
+    price: number;
+    sku: string;
     image: string | null;
     image: string | null;
     options: UpdateProduct_updateProduct_variants_options[];
     options: UpdateProduct_updateProduct_variants_options[];
     translations: UpdateProduct_updateProduct_variants_translations[];
     translations: UpdateProduct_updateProduct_variants_translations[];
@@ -130,10 +130,10 @@ export interface UpdateProduct_updateProduct {
     __typename: 'Product';
     __typename: 'Product';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    slug: string | null;
-    image: string | null;
-    description: string | null;
+    name: string;
+    slug: string;
+    image: string;
+    description: string;
     translations: UpdateProduct_updateProduct_translations[];
     translations: UpdateProduct_updateProduct_translations[];
     optionGroups: UpdateProduct_updateProduct_optionGroups[];
     optionGroups: UpdateProduct_updateProduct_optionGroups[];
     variants: UpdateProduct_updateProduct_variants[];
     variants: UpdateProduct_updateProduct_variants[];
@@ -150,6 +150,36 @@ export interface UpdateProductVariables {
 /* tslint:disable */
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 // This file was automatically generated and should not be edited.
 
 
+// ====================================================
+// GraphQL mutation operation: UpdateProductVariants
+// ====================================================
+
+export interface UpdateProductVariants_updateProductVariants_translations {
+    __typename: 'ProductVariantTranslation';
+    id: string;
+    name: string;
+}
+
+export interface UpdateProductVariants_updateProductVariants {
+    __typename: 'ProductVariant';
+    id: string;
+    name: string;
+    price: number;
+    sku: string;
+    translations: UpdateProductVariants_updateProductVariants_translations[];
+}
+
+export interface UpdateProductVariants {
+    updateProductVariants: (UpdateProductVariants_updateProductVariants | null)[]; // Update existing ProductVariants
+}
+
+export interface UpdateProductVariantsVariables {
+    input: UpdateProductVariantInput[];
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
 // ====================================================
 // ====================================================
 // GraphQL mutation operation: CreateProductOptionGroup
 // GraphQL mutation operation: CreateProductOptionGroup
 // ====================================================
 // ====================================================
@@ -352,9 +382,9 @@ export interface GetProductWithVariants_product_variants {
     __typename: 'ProductVariant';
     __typename: 'ProductVariant';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    price: number | null;
-    sku: string | null;
+    name: string;
+    price: number;
+    sku: string;
     image: string | null;
     image: string | null;
     options: GetProductWithVariants_product_variants_options[];
     options: GetProductWithVariants_product_variants_options[];
     translations: GetProductWithVariants_product_variants_translations[];
     translations: GetProductWithVariants_product_variants_translations[];
@@ -364,10 +394,10 @@ export interface GetProductWithVariants_product {
     __typename: 'Product';
     __typename: 'Product';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    slug: string | null;
-    image: string | null;
-    description: string | null;
+    name: string;
+    slug: string;
+    image: string;
+    description: string;
     translations: GetProductWithVariants_product_translations[];
     translations: GetProductWithVariants_product_translations[];
     optionGroups: GetProductWithVariants_product_optionGroups[];
     optionGroups: GetProductWithVariants_product_optionGroups[];
     variants: GetProductWithVariants_product_variants[];
     variants: GetProductWithVariants_product_variants[];
@@ -393,9 +423,9 @@ export interface GetProductList_products_items {
     __typename: 'Product';
     __typename: 'Product';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    slug: string | null;
-    description: string | null;
+    name: string;
+    slug: string;
+    description: string;
 }
 }
 
 
 export interface GetProductList_products {
 export interface GetProductList_products {
@@ -489,9 +519,9 @@ export interface ProductWithVariants_variants {
     __typename: 'ProductVariant';
     __typename: 'ProductVariant';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    price: number | null;
-    sku: string | null;
+    name: string;
+    price: number;
+    sku: string;
     image: string | null;
     image: string | null;
     options: ProductWithVariants_variants_options[];
     options: ProductWithVariants_variants_options[];
     translations: ProductWithVariants_variants_translations[];
     translations: ProductWithVariants_variants_translations[];
@@ -501,10 +531,10 @@ export interface ProductWithVariants {
     __typename: 'Product';
     __typename: 'Product';
     id: string;
     id: string;
     languageCode: LanguageCode;
     languageCode: LanguageCode;
-    name: string | null;
-    slug: string | null;
-    image: string | null;
-    description: string | null;
+    name: string;
+    slug: string;
+    image: string;
+    description: string;
     translations: ProductWithVariants_translations[];
     translations: ProductWithVariants_translations[];
     optionGroups: ProductWithVariants_optionGroups[];
     optionGroups: ProductWithVariants_optionGroups[];
     variants: ProductWithVariants_variants[];
     variants: ProductWithVariants_variants[];
@@ -757,6 +787,22 @@ export interface ProductTranslationInput {
     description?: string | null;
     description?: string | null;
 }
 }
 
 
+//
+export interface UpdateProductVariantInput {
+    id: string;
+    translations: ProductVariantTranslationInput[];
+    sku: string;
+    image?: string | null;
+    price: number;
+}
+
+//
+export interface ProductVariantTranslationInput {
+    id?: string | null;
+    languageCode: LanguageCode;
+    name: string;
+}
+
 //
 //
 export interface CreateProductOptionGroupInput {
 export interface CreateProductOptionGroupInput {
     code: string;
     code: string;