Browse Source

feat(admin-ui): Create / update of FacetValues

Michael Bromley 7 years ago
parent
commit
a8c8ef6b44

+ 40 - 4
admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.html

@@ -32,11 +32,11 @@
 <form class="form" [formGroup]="facetForm" >
     <section class="form-block" formGroupName="facet">
         <label>{{ 'catalog.facet' | translate }}</label>
-        <vdr-form-field [label]="'catalog.code' | translate" for="code">
-            <input id="code" type="text" formControlName="code">
-        </vdr-form-field>
         <vdr-form-field [label]="'catalog.name' | translate" for="name">
-            <input id="name" type="text" formControlName="name">
+            <input id="name" type="text" formControlName="name" (input)="updateCode($event.target.value)">
+        </vdr-form-field>
+        <vdr-form-field [label]="'catalog.code' | translate" for="code" [readOnlyToggle]="true">
+            <input id="code" type="text" formControlName="code">
         </vdr-form-field>
 
         <section formGroupName="customFields" *ngIf="customFields.length">
@@ -48,4 +48,40 @@
             </ng-container>
         </section>
     </section>
+
+    <section class="form-block" *ngIf="!(isNew$ | async)">
+
+        <label>{{ 'catalog.facet-values' | translate }}</label>
+
+        <table class="facet-values-list table" formArrayName="values" *ngIf="0 < getValuesFormArray().length">
+            <thead>
+            <tr>
+                <th>{{ 'catalog.name' | translate }}</th>
+                <th>{{ 'catalog.code' | translate }}</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr class="variant"
+                *ngFor="let value of getValuesFormArray().controls; let i = index"
+                [formGroupName]="i">
+                <td>
+                    <input type="text" formControlName="name" (input)="updateValueCode($event.target.value, i)">
+                </td>
+                <td>
+                    <input type="text" formControlName="code" readonly>
+                </td>
+            </tr>
+            </tbody>
+        </table>
+
+        <div>
+            <button type="button"
+                    class="btn btn-primary"
+                    (click)="addFacetValue()">
+                <clr-icon shape="add"></clr-icon>
+                {{ 'catalog.add-facet-value' | translate }}
+            </button>
+        </div>
+
+    </section>
 </form>

+ 111 - 8
admin-ui/src/app/catalog/components/facet-detail/facet-detail.component.ts

@@ -1,27 +1,38 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
+import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { combineLatest, forkJoin, Observable, Subject } from 'rxjs';
 import { map, mergeMap, switchMap, take, takeUntil } from 'rxjs/operators';
 
 import { CustomFieldConfig } from '../../../../../../shared/shared-types';
+import { notNullOrUndefined } from '../../../../../../shared/shared-utils';
 import { createUpdatedTranslatable } from '../../../common/utilities/create-updated-translatable';
 import { getDefaultLanguage } from '../../../common/utilities/get-default-language';
+import { normalizeString } from '../../../common/utilities/normalize-string';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
 import { getServerConfig } from '../../../data/server-config';
-import { FacetWithValues, LanguageCode } from '../../../data/types/gql-generated-types';
+import {
+    CreateFacetValueInput,
+    FacetWithValues,
+    FacetWithValues_values,
+    LanguageCode,
+    UpdateFacetValueInput,
+} from '../../../data/types/gql-generated-types';
 
 @Component({
     selector: 'vdr-facet-detail',
     templateUrl: './facet-detail.component.html',
     styleUrls: ['./facet-detail.component.scss'],
+    changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class FacetDetailComponent implements OnInit, OnDestroy {
     facet$: Observable<FacetWithValues>;
+    values$: Observable<FacetWithValues_values[]>;
     availableLanguages$: Observable<LanguageCode[]>;
     customFields: CustomFieldConfig[];
+    customValueFields: CustomFieldConfig[];
     languageCode$: Observable<LanguageCode>;
     isNew$: Observable<boolean>;
     facetForm: FormGroup;
@@ -37,7 +48,9 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
 
     ngOnInit() {
         this.customFields = getServerConfig().customFields.Facet || [];
+        this.customValueFields = getServerConfig().customFields.FacetValue || [];
         this.facet$ = this.route.data.pipe(switchMap(data => data.facet));
+        this.values$ = this.facet$.pipe(map(facet => facet.values));
         this.facetForm = this.formBuilder.group({
             facet: this.formBuilder.group({
                 code: ['', Validators.required],
@@ -46,6 +59,7 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
                     this.customFields.reduce((hash, field) => ({ ...hash, [field.name]: '' }), {}),
                 ),
             }),
+            values: this.formBuilder.array([]),
         });
 
         this.isNew$ = this.facet$.pipe(map(facet => facet.id === ''));
@@ -66,6 +80,20 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
         this.destroy$.complete();
     }
 
+    updateCode(nameValue: string) {
+        const codeControl = this.facetForm.get(['facet', 'code']);
+        if (codeControl && codeControl.pristine) {
+            codeControl.setValue(normalizeString(nameValue, '-'));
+        }
+    }
+
+    updateValueCode(nameValue: string, index: number) {
+        const codeControl = this.facetForm.get(['values', index, 'code']);
+        if (codeControl && codeControl.pristine) {
+            codeControl.setValue(normalizeString(nameValue, '-'));
+        }
+    }
+
     setLanguage(code: LanguageCode) {
         this.setQueryParam('lang', code);
     }
@@ -74,6 +102,17 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
         return !!this.facetForm.get(['facet', 'customFields', name]);
     }
 
+    getValuesFormArray(): FormArray {
+        return this.facetForm.get('values') as FormArray;
+    }
+
+    addFacetValue() {
+        const valuesFormArray = this.facetForm.get('values') as FormArray | null;
+        if (valuesFormArray) {
+            valuesFormArray.insert(valuesFormArray.length, this.formBuilder.group({ name: '', code: '' }));
+        }
+    }
+
     create() {
         const facetForm = this.facetForm.get('facet');
         if (!facetForm || !facetForm.dirty) {
@@ -113,11 +152,27 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
                             updateOperations.push(this.dataService.facet.updateFacet(newFacet));
                         }
                     }
-                    /* const variantsArray = this.facetForm.get('variants');
-                    if (variantsArray && variantsArray.dirty) {
-                        const newVariants = this.getUpdatedFacetValues(facet, variantsArray as FormArray, languageCode);
-                        updateOperations.push(this.dataService.facet.updateFacetVariants(newVariants));
-                    }*/
+                    const valuesArray = this.facetForm.get('values');
+                    if (valuesArray && valuesArray.dirty) {
+                        const newValues: CreateFacetValueInput[] = (valuesArray as FormArray).controls
+                            .filter(c => !c.value.id)
+                            .map(c => ({
+                                facetId: facet.id,
+                                code: c.value.code,
+                                translations: [{ name: c.value.name, languageCode }],
+                            }));
+                        if (newValues.length) {
+                            updateOperations.push(this.dataService.facet.createFacetValues(newValues));
+                        }
+                        const updatedValues = this.getUpdatedFacetValues(
+                            facet,
+                            valuesArray as FormArray,
+                            languageCode,
+                        );
+                        if (updatedValues.length) {
+                            updateOperations.push(this.dataService.facet.updateFacetValues(updatedValues));
+                        }
+                    }
 
                     return forkJoin(updateOperations);
                 }),
@@ -163,6 +218,22 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
                     }
                 }
             }
+
+            const valuesFormArray = this.facetForm.get('values') as FormArray;
+            facet.values.forEach((value, i) => {
+                const variantTranslation = value.translations.find(t => t.languageCode === languageCode);
+                const group = {
+                    id: value.id,
+                    code: value.code,
+                    name: variantTranslation ? variantTranslation.name : '',
+                };
+                const existing = valuesFormArray.at(i);
+                if (existing) {
+                    existing.setValue(group);
+                } else {
+                    valuesFormArray.insert(i, this.formBuilder.group(group));
+                }
+            });
         }
     }
 
@@ -181,6 +252,38 @@ export class FacetDetailComponent implements OnInit, OnDestroy {
         });
     }
 
+    /**
+     * Given an array of facet values and the values from the facetForm, this method creates an new array
+     * which can be persisted to the API.
+     */
+    private getUpdatedFacetValues(
+        facet: FacetWithValues,
+        valuesFormArray: FormArray,
+        languageCode: LanguageCode,
+    ): UpdateFacetValueInput[] {
+        const dirtyValues = facet.values.filter((v, i) => {
+            const formRow = valuesFormArray.get(i.toString());
+            return formRow && formRow.dirty && formRow.value.id;
+        });
+        const dirtyValueValues = valuesFormArray.controls
+            .filter(c => c.dirty && c.value.id)
+            .map(c => c.value);
+
+        if (dirtyValues.length !== dirtyValueValues.length) {
+            throw new Error(_(`error.facet-value-form-values-do-not-match`));
+        }
+        return dirtyValues
+            .map((value, i) => {
+                return createUpdatedTranslatable(
+                    value,
+                    dirtyValueValues[i],
+                    this.customValueFields,
+                    languageCode,
+                );
+            })
+            .filter(notNullOrUndefined);
+    }
+
     private setQueryParam(key: string, value: any) {
         this.router.navigate(['./'], {
             queryParams: { [key]: value },

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

@@ -14,9 +14,9 @@ import { NotificationService } from '../../../core/providers/notification/notifi
 import { DataService } from '../../../data/providers/data.service';
 import { getServerConfig } from '../../../data/server-config';
 import {
-    GetProductWithVariants_product_variants,
     LanguageCode,
     ProductWithVariants,
+    ProductWithVariants_variants,
     UpdateProductInput,
     UpdateProductVariantInput,
 } from '../../../data/types/gql-generated-types';
@@ -30,7 +30,7 @@ import { ProductVariantsWizardComponent } from '../product-variants-wizard/produ
 })
 export class ProductDetailComponent implements OnInit, OnDestroy {
     product$: Observable<ProductWithVariants>;
-    variants$: Observable<GetProductWithVariants_product_variants[]>;
+    variants$: Observable<ProductWithVariants_variants[]>;
     availableLanguages$: Observable<LanguageCode[]>;
     customFields: CustomFieldConfig[];
     customVariantFields: CustomFieldConfig[];

+ 16 - 9
admin-ui/src/app/data/fragments/facet-fragments.ts

@@ -1,7 +1,7 @@
 import gql from 'graphql-tag';
 
-export const FACET_WITH_VALUES_FRAGMENT = gql`
-    fragment FacetWithValues on Facet {
+export const FACET_VALUE_FRAGMENT = gql`
+    fragment FacetValue on FacetValue {
         id
         languageCode
         code
@@ -11,16 +11,23 @@ export const FACET_WITH_VALUES_FRAGMENT = gql`
             languageCode
             name
         }
-        values {
+    }
+`;
+
+export const FACET_WITH_VALUES_FRAGMENT = gql`
+    fragment FacetWithValues on Facet {
+        id
+        languageCode
+        code
+        name
+        translations {
             id
             languageCode
-            code
             name
-            translations {
-                id
-                languageCode
-                name
-            }
+        }
+        values {
+            ...FacetValue
         }
     }
+    ${FACET_VALUE_FRAGMENT}
 `;

+ 21 - 3
admin-ui/src/app/data/mutations/facet-mutations.ts

@@ -1,9 +1,9 @@
 import gql from 'graphql-tag';
 
-import { FACET_WITH_VALUES_FRAGMENT } from '../fragments/facet-fragments';
+import { FACET_VALUE_FRAGMENT, FACET_WITH_VALUES_FRAGMENT } from '../fragments/facet-fragments';
 
 export const CREATE_FACET = gql`
-    mutation CreateFacet($input: CreateFacetInput) {
+    mutation CreateFacet($input: CreateFacetInput!) {
         createFacet(input: $input) {
             ...FacetWithValues
         }
@@ -12,10 +12,28 @@ export const CREATE_FACET = gql`
 `;
 
 export const UPDATE_FACET = gql`
-    mutation UpdateFacet($input: UpdateFacetInput) {
+    mutation UpdateFacet($input: UpdateFacetInput!) {
         updateFacet(input: $input) {
             ...FacetWithValues
         }
     }
     ${FACET_WITH_VALUES_FRAGMENT}
 `;
+
+export const CREATE_FACET_VALUES = gql`
+    mutation CreateFacetValues($input: [CreateFacetValueInput!]!) {
+        createFacetValues(input: $input) {
+            ...FacetValue
+        }
+    }
+    ${FACET_VALUE_FRAGMENT}
+`;
+
+export const UPDATE_FACET_VALUES = gql`
+    mutation UpdateFacetValues($input: [UpdateFacetValueInput!]!) {
+        updateFacetValues(input: $input) {
+            ...FacetValue
+        }
+    }
+    ${FACET_VALUE_FRAGMENT}
+`;

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

@@ -61,5 +61,7 @@ export class MockDataService implements DataServiceMock {
         getFacet: spyQueryResult('getFacet'),
         createFacet: spyObservable('createFacet'),
         updateFacet: spyObservable('updateFacet'),
+        createFacetValues: spyObservable('createFacetValues'),
+        updateFacetValues: spyObservable('updateFacetValues'),
     };
 }

+ 40 - 1
admin-ui/src/app/data/providers/facet-data.service.ts

@@ -2,11 +2,19 @@ import { Observable } from 'rxjs';
 
 import { getDefaultLanguage } from '../../common/utilities/get-default-language';
 import { addCustomFields } from '../add-custom-fields';
-import { CREATE_FACET, UPDATE_FACET } from '../mutations/facet-mutations';
+import {
+    CREATE_FACET,
+    CREATE_FACET_VALUES,
+    UPDATE_FACET,
+    UPDATE_FACET_VALUES,
+} from '../mutations/facet-mutations';
 import { GET_FACET_LIST, GET_FACET_WITH_VALUES } from '../queries/facet-queries';
 import {
     CreateFacet,
     CreateFacetInput,
+    CreateFacetValueInput,
+    CreateFacetValues,
+    CreateFacetValuesVariables,
     CreateFacetVariables,
     GetFacetList,
     GetFacetListVariables,
@@ -14,6 +22,9 @@ import {
     GetFacetWithValuesVariables,
     UpdateFacet,
     UpdateFacetInput,
+    UpdateFacetValueInput,
+    UpdateFacetValues,
+    UpdateFacetValuesVariables,
     UpdateFacetVariables,
 } from '../types/gql-generated-types';
 import { QueryResult } from '../types/query-result';
@@ -72,4 +83,32 @@ export class FacetDataService {
             input,
         );
     }
+
+    createFacetValues(facetValues: CreateFacetValueInput[]): Observable<CreateFacetValues> {
+        const input: CreateFacetValuesVariables = {
+            input: facetValues.map(fv => ({
+                facetId: fv.facetId,
+                code: fv.code,
+                translations: fv.translations,
+            })),
+        };
+        return this.baseDataService.mutate<CreateFacetValues, CreateFacetValuesVariables>(
+            addCustomFields(CREATE_FACET_VALUES),
+            input,
+        );
+    }
+
+    updateFacetValues(facetValues: UpdateFacetValueInput[]): Observable<UpdateFacetValues> {
+        const input: UpdateFacetValuesVariables = {
+            input: facetValues.map(fv => ({
+                id: fv.id,
+                code: fv.code,
+                translations: fv.translations,
+            })),
+        };
+        return this.baseDataService.mutate<UpdateFacetValues, UpdateFacetValuesVariables>(
+            addCustomFields(UPDATE_FACET_VALUES),
+            input,
+        );
+    }
 }

+ 100 - 2
admin-ui/src/app/data/types/gql-generated-types.ts

@@ -46,7 +46,7 @@ export interface CreateFacet {
 }
 
 export interface CreateFacetVariables {
-  input?: CreateFacetInput | null;
+  input: CreateFacetInput;
 }
 
 /* tslint:disable */
@@ -97,7 +97,75 @@ export interface UpdateFacet {
 }
 
 export interface UpdateFacetVariables {
-  input?: UpdateFacetInput | null;
+  input: UpdateFacetInput;
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: CreateFacetValues
+// ====================================================
+
+export interface CreateFacetValues_createFacetValues_translations {
+  __typename: "FacetValueTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface CreateFacetValues_createFacetValues {
+  __typename: "FacetValue";
+  id: string;
+  languageCode: LanguageCode | null;
+  code: string;
+  name: string;
+  translations: CreateFacetValues_createFacetValues_translations[];
+}
+
+export interface CreateFacetValues {
+  /**
+   * Create one or more FacetValues
+   */
+  createFacetValues: CreateFacetValues_createFacetValues[];
+}
+
+export interface CreateFacetValuesVariables {
+  input: CreateFacetValueInput[];
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: UpdateFacetValues
+// ====================================================
+
+export interface UpdateFacetValues_updateFacetValues_translations {
+  __typename: "FacetValueTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface UpdateFacetValues_updateFacetValues {
+  __typename: "FacetValue";
+  id: string;
+  languageCode: LanguageCode | null;
+  code: string;
+  name: string;
+  translations: UpdateFacetValues_updateFacetValues_translations[];
+}
+
+export interface UpdateFacetValues {
+  /**
+   * Update one or more FacetValues
+   */
+  updateFacetValues: UpdateFacetValues_updateFacetValues[];
+}
+
+export interface UpdateFacetValuesVariables {
+  input: UpdateFacetValueInput[];
 }
 
 /* tslint:disable */
@@ -878,6 +946,29 @@ export interface FacetWithValues {
 /* tslint:disable */
 // This file was automatically generated and should not be edited.
 
+// ====================================================
+// GraphQL fragment: FacetValue
+// ====================================================
+
+export interface FacetValue_translations {
+  __typename: "FacetValueTranslation";
+  id: string;
+  languageCode: LanguageCode;
+  name: string;
+}
+
+export interface FacetValue {
+  __typename: "FacetValue";
+  id: string;
+  languageCode: LanguageCode | null;
+  code: string;
+  name: string;
+  translations: FacetValue_translations[];
+}
+
+/* tslint:disable */
+// This file was automatically generated and should not be edited.
+
 // ====================================================
 // GraphQL fragment: ProductVariant
 // ====================================================
@@ -1225,6 +1316,7 @@ export interface FacetTranslationInput {
 }
 
 export interface CreateFacetValueInput {
+  facetId: string;
   code: string;
   translations: FacetValueTranslationInput[];
 }
@@ -1250,6 +1342,12 @@ export interface UpdateFacetCustomFieldsInput {
   searchable?: boolean | null;
 }
 
+export interface UpdateFacetValueInput {
+  id: string;
+  code: string;
+  translations: FacetValueTranslationInput[];
+}
+
 export interface UpdateProductInput {
   id: string;
   image?: string | null;

+ 9 - 1
admin-ui/src/i18n-messages/en.json

@@ -6,6 +6,7 @@
   },
   "catalog": {
     "ID": "ID",
+    "add-facet-value": "Add facet value",
     "code": "Code",
     "confirm-generate-product-variants": "Click 'Finish' to generate {count} product variants.",
     "create-group": "Create option group",
@@ -13,14 +14,20 @@
     "create-new-option-group": "Create new option group",
     "create-new-product": "Create new product",
     "description": "Description",
+    "facet": "Facet",
+    "facet-values": "Facet values",
     "filter-by-group-name": "Filter by group name",
     "generate-product-variants": "Generate product variants",
     "generate-variants-default-only": "This product does not have options",
     "generate-variants-with-options": "This product has options",
     "name": "Name",
+    "notify-create-facet-error": "An error occurred, could not create facet",
+    "notify-create-facet-success": "Created new facet",
     "notify-create-new-option-group": "Created new option group",
-    "notify-create-product-error": "An error occured, could not create product",
+    "notify-create-product-error": "An error occurred, could not create product",
     "notify-create-product-success": "Created new product",
+    "notify-update-facet-error": "An error occurred, could not update facet\n\n { error }",
+    "notify-update-facet-success": "Updated facet",
     "notify-update-product-error": "An error occurred, could not update product\n\n { error }",
     "notify-update-product-success": "Updated product",
     "option-group-code": "Code",
@@ -46,6 +53,7 @@
     "confirm": "Confirm",
     "create": "Create",
     "edit": "Edit",
+    "edit-field": "Edit field",
     "finish": "Finish",
     "language": "Language",
     "log-out": "Log out",