Browse Source

feat(admin-ui): Support "required" & "defaultValue" in ConfigArgs

Relates to #643
Michael Bromley 5 years ago
parent
commit
6e5e482969

+ 5 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -2689,6 +2689,8 @@ export type ConfigArgDefinition = {
   name: Scalars['String'];
   type: Scalars['String'];
   list: Scalars['Boolean'];
+  required: Scalars['Boolean'];
+  defaultValue?: Maybe<Scalars['String']>;
   label?: Maybe<Scalars['String']>;
   description?: Maybe<Scalars['String']>;
   ui?: Maybe<Scalars['JSON']>;
@@ -2715,6 +2717,7 @@ export type DeletionResponse = {
 
 export type ConfigArgInput = {
   name: Scalars['String'];
+  /** A JSON stringified representation of the actual value */
   value: Scalars['String'];
 };
 
@@ -3728,6 +3731,7 @@ export type OrderAddress = {
   country?: Maybe<Scalars['String']>;
   countryCode?: Maybe<Scalars['String']>;
   phoneNumber?: Maybe<Scalars['String']>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type OrderList = PaginatedList & {
@@ -7441,7 +7445,7 @@ export type ConfigurableOperationDefFragment = (
   & Pick<ConfigurableOperationDefinition, 'code' | 'description'>
   & { args: Array<(
     { __typename?: 'ConfigArgDefinition' }
-    & Pick<ConfigArgDefinition, 'name' | 'type' | 'list' | 'ui' | 'label'>
+    & Pick<ConfigArgDefinition, 'name' | 'type' | 'required' | 'defaultValue' | 'list' | 'ui' | 'label'>
   )> }
 );
 

+ 20 - 20
packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts

@@ -77,28 +77,28 @@ export function toConfigurableOperationInput(
     };
 }
 
+export function configurableOperationValueIsValid(
+    def?: ConfigurableOperationDefinition,
+    value?: { code: string; args: { [key: string]: string } },
+) {
+    if (!def || !value) {
+        return false;
+    }
+    if (def.code !== value.code) {
+        return false;
+    }
+    for (const argDef of def.args) {
+        const argVal = value.args[argDef.name];
+        if (argDef.required && (argVal == null || argVal === '' || argVal === '0')) {
+            return false;
+        }
+    }
+    return true;
+}
+
 /**
  * Returns a default value based on the type of the config arg.
  */
 export function getDefaultConfigArgValue(arg: ConfigArgDefinition): any {
-    return arg.list ? [] : getDefaultConfigArgSingleValue(arg.type as ConfigArgType);
-}
-
-export function getDefaultConfigArgSingleValue(type: ConfigArgType | CustomFieldType): any {
-    switch (type) {
-        case 'boolean':
-            return 'false';
-        case 'int':
-        case 'float':
-            return '0';
-        case 'ID':
-            return '';
-        case 'string':
-        case 'localeString':
-            return '';
-        case 'datetime':
-            return new Date();
-        default:
-            assertNever(type);
-    }
+    return arg.list ? [] : arg.defaultValue || null; // getDefaultConfigArgSingleValue(arg.type as ConfigArgType);
 }

+ 18 - 10
packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.spec.ts

@@ -5,7 +5,7 @@ import { interpolateDescription } from './interpolate-description';
 describe('interpolateDescription()', () => {
     it('works for single argument', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'foo', type: 'string', list: false }],
+            args: [{ name: 'foo', type: 'string', list: false, required: false }],
             description: 'The value is { foo }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val' });
@@ -16,8 +16,8 @@ describe('interpolateDescription()', () => {
     it('works for multiple arguments', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
             args: [
-                { name: 'foo', type: 'string', list: false },
-                { name: 'bar', type: 'string', list: false },
+                { name: 'foo', type: 'string', list: false, required: false },
+                { name: 'bar', type: 'string', list: false, required: false },
             ],
             description: 'The value is { foo } and { bar }',
         };
@@ -28,7 +28,7 @@ describe('interpolateDescription()', () => {
 
     it('is case-insensitive', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'foo', type: 'string', list: false }],
+            args: [{ name: 'foo', type: 'string', list: false, required: false }],
             description: 'The value is { FOo }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val' });
@@ -39,8 +39,8 @@ describe('interpolateDescription()', () => {
     it('ignores whitespaces in interpolation', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
             args: [
-                { name: 'foo', type: 'string', list: false },
-                { name: 'bar', type: 'string', list: false },
+                { name: 'foo', type: 'string', list: false, required: false },
+                { name: 'bar', type: 'string', list: false, required: false },
             ],
             description: 'The value is {foo} and {      bar    }',
         };
@@ -51,7 +51,15 @@ describe('interpolateDescription()', () => {
 
     it('formats currency-form-input value as a decimal', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'price', type: 'int', list: false, ui: { component: 'currency-form-input' } }],
+            args: [
+                {
+                    name: 'price',
+                    type: 'int',
+                    list: false,
+                    ui: { component: 'currency-form-input' },
+                    required: false,
+                },
+            ],
             description: 'The price is { price }',
         };
         const result = interpolateDescription(operation as any, { price: 1234 });
@@ -61,7 +69,7 @@ describe('interpolateDescription()', () => {
 
     it('formats Date object as human-readable', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'date', type: 'datetime', list: false }],
+            args: [{ name: 'date', type: 'datetime', list: false, required: false }],
             description: 'The date is { date }',
         };
         const date = new Date('2017-09-15 00:00:00');
@@ -72,7 +80,7 @@ describe('interpolateDescription()', () => {
 
     it('formats date string object as human-readable', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'date', type: 'datetime', list: false }],
+            args: [{ name: 'date', type: 'datetime', list: false, required: false }],
             description: 'The date is { date }',
         };
         const date = '2017-09-15';
@@ -83,7 +91,7 @@ describe('interpolateDescription()', () => {
 
     it('correctly interprets falsy-looking values', () => {
         const operation: Partial<ConfigurableOperationDefinition> = {
-            args: [{ name: 'foo', type: 'int', list: false }],
+            args: [{ name: 'foo', type: 'int', list: false, required: false }],
             description: 'The value is { foo }',
         };
         const result = interpolateDescription(operation as any, { foo: 0 });

+ 2 - 0
packages/admin-ui/src/lib/core/src/data/definitions/shared-definitions.ts

@@ -15,6 +15,8 @@ export const CONFIGURABLE_OPERATION_DEF_FRAGMENT = gql`
         args {
             name
             type
+            required
+            defaultValue
             list
             ui
             label

+ 1 - 1
packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts

@@ -133,7 +133,7 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
                 if (value === undefined) {
                     value = getDefaultConfigArgValue(arg);
                 }
-                const validators = arg.list ? undefined : Validators.required;
+                const validators = arg.list ? undefined : arg.required ? Validators.required : undefined;
                 this.form.addControl(arg.name, new FormControl(value, validators));
             }
         }

+ 9 - 5
packages/admin-ui/src/lib/core/src/shared/components/currency-input/currency-input.component.ts

@@ -9,7 +9,7 @@ import {
     SimpleChanges,
 } from '@angular/core';
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { Observable } from 'rxjs';
+import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
 import { map } from 'rxjs/operators';
 
 import { DataService } from '../../../data/providers/data.service';
@@ -41,20 +41,21 @@ export class CurrencyInputComponent implements ControlValueAccessor, OnInit, OnC
     onChange: (val: any) => void;
     onTouch: () => void;
     _decimalValue: string;
+    private currencyCode$ = new BehaviorSubject<string>('');
 
     constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) {}
 
     ngOnInit() {
         const languageCode$ = this.dataService.client.uiState().mapStream(data => data.uiState.language);
-        const shouldPrefix$ = languageCode$.pipe(
-            map(languageCode => {
-                if (!this.currencyCode) {
+        const shouldPrefix$ = combineLatest(languageCode$, this.currencyCode$).pipe(
+            map(([languageCode, currencyCode]) => {
+                if (!currencyCode) {
                     return '';
                 }
                 const locale = languageCode.replace(/_/g, '-');
                 const localised = new Intl.NumberFormat(locale, {
                     style: 'currency',
-                    currency: this.currencyCode,
+                    currency: currencyCode,
                     currencyDisplay: 'symbol',
                 }).format(undefined as any);
                 return localised.indexOf('NaN') > 0;
@@ -68,6 +69,9 @@ export class CurrencyInputComponent implements ControlValueAccessor, OnInit, OnC
         if ('value' in changes) {
             this.writeValue(changes['value'].currentValue);
         }
+        if ('currencyCode' in changes) {
+            this.currencyCode$.next(this.currencyCode);
+        }
     }
 
     registerOnChange(fn: any) {

+ 2 - 5
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts

@@ -29,10 +29,7 @@ import { switchMap, take, takeUntil } from 'rxjs/operators';
 
 import { FormInputComponent } from '../../../common/component-registry-types';
 import { ConfigArgDefinition, CustomFieldConfig } from '../../../common/generated-types';
-import {
-    getConfigArgValue,
-    getDefaultConfigArgSingleValue,
-} from '../../../common/utilities/configurable-operation-utils';
+import { getConfigArgValue } from '../../../common/utilities/configurable-operation-utils';
 import { ComponentRegistryService } from '../../../providers/component-registry/component-registry.service';
 
 type InputListItem = {
@@ -211,7 +208,7 @@ export class DynamicFormInputComponent
         }
         this.listItems.push({
             id: this.listId++,
-            control: new FormControl(getDefaultConfigArgSingleValue(this.def.type as ConfigArgType)),
+            control: new FormControl((this.def as ConfigArgDefinition).defaultValue ?? null),
         });
         this.renderList$.next();
     }

+ 5 - 2
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts

@@ -4,6 +4,7 @@ import {
     configurableDefinitionToInstance,
     ConfigurableOperation,
     ConfigurableOperationDefinition,
+    configurableOperationValueIsValid,
     DataService,
     Dialog,
     FulfillOrderInput,
@@ -77,8 +78,10 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
             0,
         );
         const formIsValid =
-            this.fulfillmentHandlerDef?.args.length === 0 ||
-            (this.fulfillmentHandlerControl.valid && this.fulfillmentHandlerControl.touched);
+            configurableOperationValueIsValid(
+                this.fulfillmentHandlerDef,
+                this.fulfillmentHandlerControl.value,
+            ) && this.fulfillmentHandlerControl.valid;
         return formIsValid && 0 < totalCount;
     }
 

+ 1 - 1
packages/admin-ui/src/lib/settings/src/components/shipping-method-detail/shipping-method-detail.component.ts

@@ -70,7 +70,7 @@ export class ShippingMethodDetailComponent
             code: ['', Validators.required],
             name: ['', Validators.required],
             description: '',
-            fulfillmentHandler: '',
+            fulfillmentHandler: ['', Validators.required],
             checker: {},
             calculator: {},
             customFields: this.formBuilder.group(