Browse Source

feat(admin-ui): Support extended ConfigurableOperations

Relates to #135
Michael Bromley 6 years ago
parent
commit
8cc094185f
19 changed files with 275 additions and 161 deletions
  1. 3 2
      admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.html
  2. 6 1
      admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.ts
  3. 46 48
      admin-ui/src/app/common/generated-types.ts
  4. 17 17
      admin-ui/src/app/common/utilities/interpolate-description.spec.ts
  5. 4 4
      admin-ui/src/app/common/utilities/interpolate-description.ts
  6. 4 4
      admin-ui/src/app/data/definitions/collection-definitions.ts
  7. 8 20
      admin-ui/src/app/data/definitions/promotion-definitions.ts
  8. 24 0
      admin-ui/src/app/data/definitions/shared-definitions.ts
  9. 5 5
      admin-ui/src/app/data/definitions/shipping-definitions.ts
  10. 1 1
      admin-ui/src/app/data/providers/promotion-data.service.ts
  11. 5 3
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html
  12. 16 7
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts
  13. 3 7
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html
  14. 8 9
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.ts
  15. 4 2
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html
  16. 35 8
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts
  17. 11 16
      admin-ui/src/app/shared/components/configurable-input/configurable-input.component.html
  18. 74 6
      admin-ui/src/app/shared/components/configurable-input/configurable-input.component.ts
  19. 1 1
      packages/core/package.json

+ 3 - 2
admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.html

@@ -11,7 +11,7 @@
     <vdr-ab-right>
         <button
             class="btn btn-primary"
-            *ngIf="(isNew$ | async); else updateButton"
+            *ngIf="isNew$ | async; else updateButton"
             (click)="create()"
             [disabled]="detailForm.invalid || detailForm.pristine"
         >
@@ -29,7 +29,7 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="detailForm" *ngIf="(entity$ | async) as category">
+<form class="form" [formGroup]="detailForm" *ngIf="entity$ | async as category">
     <div class="clr-row">
         <div class="clr-col">
             <vdr-form-field [label]="'catalog.visibility' | translate" for="visibility">
@@ -79,6 +79,7 @@
                     (remove)="removeFilter($event)"
                     [facets]="facets$ | async"
                     [operation]="filter"
+                    [operationDefinition]="getFilterDefinition(filter)"
                     [formControlName]="i"
                     [activeChannel]="activeChannel$ | async"
                 ></vdr-configurable-input>

+ 6 - 1
admin-ui/src/app/catalog/components/collection-detail/collection-detail.component.ts

@@ -15,6 +15,7 @@ import { BaseDetailComponent } from '../../../common/base-detail.component';
 import {
     Collection,
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     ConfigurableOperationInput,
     CreateCollectionInput,
     CustomFieldConfig,
@@ -43,7 +44,7 @@ export class CollectionDetailComponent extends BaseDetailComponent<Collection.Fr
     detailForm: FormGroup;
     assetChanges: { assetIds?: string[]; featuredAssetId?: string } = {};
     filters: ConfigurableOperation[] = [];
-    allFilters: ConfigurableOperation[] = [];
+    allFilters: ConfigurableOperationDefinition[] = [];
     facets$: Observable<FacetWithValues.Fragment[]>;
     activeChannel$: Observable<GetActiveChannel.ActiveChannel>;
     @ViewChild('collectionContents', { static: false }) contentsComponent: CollectionContentsComponent;
@@ -90,6 +91,10 @@ export class CollectionDetailComponent extends BaseDetailComponent<Collection.Fr
         this.destroy();
     }
 
+    getFilterDefinition(filter: ConfigurableOperation): ConfigurableOperationDefinition | undefined {
+        return this.allFilters.find(f => f.code === filter.code);
+    }
+
     customFieldIsSet(name: string): boolean {
         return !!this.detailForm.get(['customFields', name]);
     }

+ 46 - 48
admin-ui/src/app/common/generated-types.ts

@@ -51,12 +51,6 @@ export type Adjustment = {
   amount: Scalars['Int'],
 };
 
-export type AdjustmentOperations = {
-  __typename?: 'AdjustmentOperations',
-  conditions: Array<ConfigurableOperation>,
-  actions: Array<ConfigurableOperation>,
-};
-
 export enum AdjustmentType {
   TAX = 'TAX',
   PROMOTION = 'PROMOTION',
@@ -292,38 +286,35 @@ export type CollectionTranslationInput = {
 export type ConfigArg = {
   __typename?: 'ConfigArg',
   name: Scalars['String'],
-  type: ConfigArgType,
-  value?: Maybe<Scalars['String']>,
+  type: Scalars['String'],
+  value: Scalars['String'],
 };
 
-export type ConfigArgInput = {
+export type ConfigArgDefinition = {
+  __typename?: 'ConfigArgDefinition',
   name: Scalars['String'],
-  type: ConfigArgType,
-  value?: Maybe<Scalars['String']>,
+  type: Scalars['String'],
+  label?: Maybe<Scalars['String']>,
+  description?: Maybe<Scalars['String']>,
+  config?: Maybe<Scalars['JSON']>,
 };
 
-/** Certain entities allow arbitrary configuration arguments to be specified which can then
- * be set in the admin-ui and used in the business logic of the app. These are the valid
- * data types of such arguments. The data type influences:
- * 
- * 1. How the argument form field is rendered in the admin-ui
- * 2. The JavaScript type into which the value is coerced before being passed to the business logic.
- */
-export enum ConfigArgType {
-  PERCENTAGE = 'PERCENTAGE',
-  MONEY = 'MONEY',
-  INT = 'INT',
-  STRING = 'STRING',
-  DATETIME = 'DATETIME',
-  BOOLEAN = 'BOOLEAN',
-  FACET_VALUE_IDS = 'FACET_VALUE_IDS',
-  STRING_OPERATOR = 'STRING_OPERATOR'
-}
+export type ConfigArgInput = {
+  name: Scalars['String'],
+  type: Scalars['String'],
+  value: Scalars['String'],
+};
 
 export type ConfigurableOperation = {
   __typename?: 'ConfigurableOperation',
   code: Scalars['String'],
   args: Array<ConfigArg>,
+};
+
+export type ConfigurableOperationDefinition = {
+  __typename?: 'ConfigurableOperationDefinition',
+  code: Scalars['String'],
+  args: Array<ConfigArgDefinition>,
   description: Scalars['String'],
 };
 
@@ -2635,7 +2626,7 @@ export type Query = {
   activeChannel: Channel,
   collections: CollectionList,
   collection?: Maybe<Collection>,
-  collectionFilters: Array<ConfigurableOperation>,
+  collectionFilters: Array<ConfigurableOperationDefinition>,
   countries: CountryList,
   country?: Maybe<Country>,
   customerGroups: Array<CustomerGroup>,
@@ -2659,13 +2650,14 @@ export type Query = {
   product?: Maybe<Product>,
   promotion?: Maybe<Promotion>,
   promotions: PromotionList,
-  adjustmentOperations: AdjustmentOperations,
+  promotionConditions: Array<ConfigurableOperationDefinition>,
+  promotionActions: Array<ConfigurableOperationDefinition>,
   roles: RoleList,
   role?: Maybe<Role>,
   shippingMethods: ShippingMethodList,
   shippingMethod?: Maybe<ShippingMethod>,
-  shippingEligibilityCheckers: Array<ConfigurableOperation>,
-  shippingCalculators: Array<ConfigurableOperation>,
+  shippingEligibilityCheckers: Array<ConfigurableOperationDefinition>,
+  shippingCalculators: Array<ConfigurableOperationDefinition>,
   testShippingMethod: TestShippingMethodResult,
   taxCategories: Array<TaxCategory>,
   taxCategory?: Maybe<TaxCategory>,
@@ -3552,7 +3544,7 @@ export type GetUiStateQuery = ({ __typename?: 'Query' } & { uiState: ({ __typena
 export type GetCollectionFiltersQueryVariables = {};
 
 
-export type GetCollectionFiltersQuery = ({ __typename?: 'Query' } & { collectionFilters: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
+export type GetCollectionFiltersQuery = ({ __typename?: 'Query' } & { collectionFilters: Array<({ __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment)> });
 
 export type CollectionFragment = ({ __typename?: 'Collection' } & Pick<Collection, 'id' | 'name' | 'description' | 'isPrivate' | 'languageCode'> & { featuredAsset: Maybe<({ __typename?: 'Asset' } & AssetFragment)>, assets: Array<({ __typename?: 'Asset' } & AssetFragment)>, filters: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, translations: Array<({ __typename?: 'CollectionTranslation' } & Pick<CollectionTranslation, 'id' | 'languageCode' | 'name' | 'description'>)>, parent: Maybe<({ __typename?: 'Collection' } & Pick<Collection, 'id' | 'name'>)>, children: Maybe<Array<({ __typename?: 'Collection' } & Pick<Collection, 'id' | 'name'>)>> });
 
@@ -3915,8 +3907,6 @@ export type DeleteProductVariantMutationVariables = {
 
 export type DeleteProductVariantMutation = ({ __typename?: 'Mutation' } & { deleteProductVariant: ({ __typename?: 'DeletionResponse' } & Pick<DeletionResponse, 'result' | 'message'>) });
 
-export type ConfigurableOperationFragment = ({ __typename?: 'ConfigurableOperation' } & Pick<ConfigurableOperation, 'code' | 'description'> & { args: Array<({ __typename?: 'ConfigArg' } & Pick<ConfigArg, 'name' | 'type' | 'value'>)> });
-
 export type PromotionFragment = ({ __typename?: 'Promotion' } & Pick<Promotion, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'enabled'> & { conditions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, actions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
 
 export type GetPromotionListQueryVariables = {
@@ -3936,7 +3926,7 @@ export type GetPromotionQuery = ({ __typename?: 'Query' } & { promotion: Maybe<(
 export type GetAdjustmentOperationsQueryVariables = {};
 
 
-export type GetAdjustmentOperationsQuery = ({ __typename?: 'Query' } & { adjustmentOperations: ({ __typename?: 'AdjustmentOperations' } & { actions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, conditions: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> }) });
+export type GetAdjustmentOperationsQuery = ({ __typename?: 'Query' } & { promotionConditions: Array<({ __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment)>, promotionActions: Array<({ __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment)> });
 
 export type CreatePromotionMutationVariables = {
   input: CreatePromotionInput
@@ -4223,6 +4213,10 @@ export type TestShippingMethodQueryVariables = {
 
 export type TestShippingMethodQuery = ({ __typename?: 'Query' } & { testShippingMethod: ({ __typename?: 'TestShippingMethodResult' } & Pick<TestShippingMethodResult, 'eligible'> & { price: Maybe<({ __typename?: 'ShippingPrice' } & Pick<ShippingPrice, 'price' | 'priceWithTax'>)> }) });
 
+export type ConfigurableOperationFragment = ({ __typename?: 'ConfigurableOperation' } & Pick<ConfigurableOperation, 'code'> & { args: Array<({ __typename?: 'ConfigArg' } & Pick<ConfigArg, 'name' | 'type' | 'value'>)> });
+
+export type ConfigurableOperationDefFragment = ({ __typename?: 'ConfigurableOperationDefinition' } & Pick<ConfigurableOperationDefinition, 'code' | 'description'> & { args: Array<({ __typename?: 'ConfigArgDefinition' } & Pick<ConfigArgDefinition, 'name' | 'type' | 'config'>)> });
+
 export type ShippingMethodFragment = ({ __typename?: 'ShippingMethod' } & Pick<ShippingMethod, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'description'> & { checker: ({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment), calculator: ({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment) });
 
 export type GetShippingMethodListQueryVariables = {
@@ -4242,7 +4236,7 @@ export type GetShippingMethodQuery = ({ __typename?: 'Query' } & { shippingMetho
 export type GetShippingMethodOperationsQueryVariables = {};
 
 
-export type GetShippingMethodOperationsQuery = ({ __typename?: 'Query' } & { shippingEligibilityCheckers: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)>, shippingCalculators: Array<({ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment)> });
+export type GetShippingMethodOperationsQuery = ({ __typename?: 'Query' } & { shippingEligibilityCheckers: Array<({ __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment)>, shippingCalculators: Array<({ __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment)> });
 
 export type CreateShippingMethodMutationVariables = {
   input: CreateShippingMethodInput
@@ -4398,7 +4392,7 @@ export namespace GetUiState {
 export namespace GetCollectionFilters {
   export type Variables = GetCollectionFiltersQueryVariables;
   export type Query = GetCollectionFiltersQuery;
-  export type CollectionFilters = ConfigurableOperationFragment;
+  export type CollectionFilters = ConfigurableOperationDefFragment;
 }
 
 export namespace Collection {
@@ -4809,11 +4803,6 @@ export namespace DeleteProductVariant {
   export type DeleteProductVariant = DeleteProductVariantMutation['deleteProductVariant'];
 }
 
-export namespace ConfigurableOperation {
-  export type Fragment = ConfigurableOperationFragment;
-  export type Args = (NonNullable<ConfigurableOperationFragment['args'][0]>);
-}
-
 export namespace Promotion {
   export type Fragment = PromotionFragment;
   export type Conditions = ConfigurableOperationFragment;
@@ -4836,9 +4825,8 @@ export namespace GetPromotion {
 export namespace GetAdjustmentOperations {
   export type Variables = GetAdjustmentOperationsQueryVariables;
   export type Query = GetAdjustmentOperationsQuery;
-  export type AdjustmentOperations = GetAdjustmentOperationsQuery['adjustmentOperations'];
-  export type Actions = ConfigurableOperationFragment;
-  export type Conditions = ConfigurableOperationFragment;
+  export type PromotionConditions = ConfigurableOperationDefFragment;
+  export type PromotionActions = ConfigurableOperationDefFragment;
 }
 
 export namespace CreatePromotion {
@@ -5176,6 +5164,16 @@ export namespace TestShippingMethod {
   export type Price = (NonNullable<TestShippingMethodQuery['testShippingMethod']['price']>);
 }
 
+export namespace ConfigurableOperation {
+  export type Fragment = ConfigurableOperationFragment;
+  export type Args = (NonNullable<ConfigurableOperationFragment['args'][0]>);
+}
+
+export namespace ConfigurableOperationDef {
+  export type Fragment = ConfigurableOperationDefFragment;
+  export type Args = (NonNullable<ConfigurableOperationDefFragment['args'][0]>);
+}
+
 export namespace ShippingMethod {
   export type Fragment = ShippingMethodFragment;
   export type Checker = ConfigurableOperationFragment;
@@ -5198,8 +5196,8 @@ export namespace GetShippingMethod {
 export namespace GetShippingMethodOperations {
   export type Variables = GetShippingMethodOperationsQueryVariables;
   export type Query = GetShippingMethodOperationsQuery;
-  export type ShippingEligibilityCheckers = ConfigurableOperationFragment;
-  export type ShippingCalculators = ConfigurableOperationFragment;
+  export type ShippingEligibilityCheckers = ConfigurableOperationDefFragment;
+  export type ShippingCalculators = ConfigurableOperationDefFragment;
 }
 
 export namespace CreateShippingMethod {

+ 17 - 17
admin-ui/src/app/common/utilities/interpolate-description.spec.ts

@@ -1,11 +1,11 @@
-import { ConfigArgType, ConfigurableOperation } from '../generated-types';
+import { ConfigurableOperationDefinition } from '../generated-types';
 
 import { interpolateDescription } from './interpolate-description';
 
 describe('interpolateDescription()', () => {
     it('works for single argument', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'foo', type: ConfigArgType.STRING }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'foo', type: 'string' }],
             description: 'The value is { foo }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val' });
@@ -14,8 +14,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('works for multiple arguments', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'foo', type: ConfigArgType.STRING }, { name: 'bar', type: ConfigArgType.STRING }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'foo', type: 'string' }, { name: 'bar', type: 'string' }],
             description: 'The value is { foo } and { bar }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val1', bar: 'val2' });
@@ -24,8 +24,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('is case-insensitive', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'foo', type: ConfigArgType.STRING }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'foo', type: 'string' }],
             description: 'The value is { FOo }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val' });
@@ -34,8 +34,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('ignores whitespaces in interpolation', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'foo', type: ConfigArgType.STRING }, { name: 'bar', type: ConfigArgType.STRING }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'foo', type: 'string' }, { name: 'bar', type: 'string' }],
             description: 'The value is {foo} and {      bar    }',
         };
         const result = interpolateDescription(operation as any, { foo: 'val1', bar: 'val2' });
@@ -44,8 +44,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('formats money as a decimal', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'price', type: ConfigArgType.MONEY }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'price', type: 'int', config: { inputType: 'money' } }],
             description: 'The price is { price }',
         };
         const result = interpolateDescription(operation as any, { price: 1234 });
@@ -54,8 +54,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('formats Date object as human-readable', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'date', type: ConfigArgType.DATETIME }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'date', type: 'datetime' }],
             description: 'The date is { date }',
         };
         const date = new Date('2017-09-15 00:00:00');
@@ -65,8 +65,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('formats date string object as human-readable', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'date', type: ConfigArgType.DATETIME }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'date', type: 'datetime' }],
             description: 'The date is { date }',
         };
         const date = '2017-09-15';
@@ -76,8 +76,8 @@ describe('interpolateDescription()', () => {
     });
 
     it('correctly interprets falsy-looking values', () => {
-        const operation: Partial<ConfigurableOperation> = {
-            args: [{ name: 'foo', type: ConfigArgType.INT }],
+        const operation: Partial<ConfigurableOperationDefinition> = {
+            args: [{ name: 'foo', type: 'int' }],
             description: 'The value is { foo }',
         };
         const result = interpolateDescription(operation as any, { foo: 0 });

+ 4 - 4
admin-ui/src/app/common/utilities/interpolate-description.ts

@@ -1,10 +1,10 @@
-import { ConfigArgType, ConfigurableOperation } from '../generated-types';
+import { ConfigurableOperationDefinition } from '../generated-types';
 
 /**
  * Interpolates the description of an ConfigurableOperation with the given values.
  */
 export function interpolateDescription(
-    operation: ConfigurableOperation,
+    operation: ConfigurableOperationDefinition,
     values: { [name: string]: any },
 ): string {
     if (!operation) {
@@ -19,10 +19,10 @@ export function interpolateDescription(
         }
         let formatted = value;
         const argDef = operation.args.find(arg => arg.name === normalizedArgName);
-        if (argDef && argDef.type === ConfigArgType.MONEY) {
+        if (argDef && argDef.type === 'int' && argDef.config && argDef.config.inputType === 'money') {
             formatted = value / 100;
         }
-        if (argDef && argDef.type === ConfigArgType.DATETIME && value instanceof Date) {
+        if (argDef && argDef.type === 'datetime' && value instanceof Date) {
             formatted = value.toLocaleDateString();
         }
         return formatted;

+ 4 - 4
admin-ui/src/app/data/definitions/collection-definitions.ts

@@ -1,15 +1,15 @@
 import gql from 'graphql-tag';
 
 import { ASSET_FRAGMENT } from './product-definitions';
-import { CONFIGURABLE_FRAGMENT } from './promotion-definitions';
+import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 
 export const GET_COLLECTION_FILTERS = gql`
     query GetCollectionFilters {
         collectionFilters {
-            ...ConfigurableOperation
+            ...ConfigurableOperationDef
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_DEF_FRAGMENT}
 `;
 
 export const COLLECTION_FRAGMENT = gql`
@@ -44,7 +44,7 @@ export const COLLECTION_FRAGMENT = gql`
         }
     }
     ${ASSET_FRAGMENT}
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;
 
 export const GET_COLLECTION_LIST = gql`

+ 8 - 20
admin-ui/src/app/data/definitions/promotion-definitions.ts

@@ -1,16 +1,6 @@
 import gql from 'graphql-tag';
 
-export const CONFIGURABLE_FRAGMENT = gql`
-    fragment ConfigurableOperation on ConfigurableOperation {
-        args {
-            name
-            type
-            value
-        }
-        code
-        description
-    }
-`;
+import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 
 export const PROMOTION_FRAGMENT = gql`
     fragment Promotion on Promotion {
@@ -26,7 +16,7 @@ export const PROMOTION_FRAGMENT = gql`
             ...ConfigurableOperation
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;
 
 export const GET_PROMOTION_LIST = gql`
@@ -52,16 +42,14 @@ export const GET_PROMOTION = gql`
 
 export const GET_ADJUSTMENT_OPERATIONS = gql`
     query GetAdjustmentOperations {
-        adjustmentOperations {
-            actions {
-                ...ConfigurableOperation
-            }
-            conditions {
-                ...ConfigurableOperation
-            }
+        promotionConditions {
+            ...ConfigurableOperationDef
+        }
+        promotionActions {
+            ...ConfigurableOperationDef
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_DEF_FRAGMENT}
 `;
 
 export const CREATE_PROMOTION = gql`

+ 24 - 0
admin-ui/src/app/data/definitions/shared-definitions.ts

@@ -0,0 +1,24 @@
+import gql from 'graphql-tag';
+
+export const CONFIGURABLE_OPERATION_FRAGMENT = gql`
+    fragment ConfigurableOperation on ConfigurableOperation {
+        args {
+            name
+            type
+            value
+        }
+        code
+    }
+`;
+
+export const CONFIGURABLE_OPERATION_DEF_FRAGMENT = gql`
+    fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
+        args {
+            name
+            type
+            config
+        }
+        code
+        description
+    }
+`;

+ 5 - 5
admin-ui/src/app/data/definitions/shipping-definitions.ts

@@ -1,6 +1,6 @@
 import gql from 'graphql-tag';
 
-import { CONFIGURABLE_FRAGMENT } from './promotion-definitions';
+import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, CONFIGURABLE_OPERATION_FRAGMENT } from './shared-definitions';
 
 export const SHIPPING_METHOD_FRAGMENT = gql`
     fragment ShippingMethod on ShippingMethod {
@@ -16,7 +16,7 @@ export const SHIPPING_METHOD_FRAGMENT = gql`
             ...ConfigurableOperation
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;
 
 export const GET_SHIPPING_METHOD_LIST = gql`
@@ -43,13 +43,13 @@ export const GET_SHIPPING_METHOD = gql`
 export const GET_SHIPPING_METHOD_OPERATIONS = gql`
     query GetShippingMethodOperations {
         shippingEligibilityCheckers {
-            ...ConfigurableOperation
+            ...ConfigurableOperationDef
         }
         shippingCalculators {
-            ...ConfigurableOperation
+            ...ConfigurableOperationDef
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_DEF_FRAGMENT}
 `;
 
 export const CREATE_SHIPPING_METHOD = gql`

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

@@ -38,7 +38,7 @@ export class PromotionDataService {
         });
     }
 
-    getAdjustmentOperations() {
+    getPromotionActionsAndConditions() {
         return this.baseDataService.query<GetAdjustmentOperations.Query>(GET_ADJUSTMENT_OPERATIONS);
     }
 

+ 5 - 3
admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html

@@ -4,7 +4,7 @@
     <vdr-ab-right>
         <button
             class="btn btn-primary"
-            *ngIf="(isNew$ | async); else updateButton"
+            *ngIf="isNew$ | async; else updateButton"
             (click)="create()"
             [disabled]="!saveButtonEnabled()"
         >
@@ -31,6 +31,7 @@
                     (remove)="removeCondition($event)"
                     [facets]="facets$ | async"
                     [operation]="condition"
+                    [operationDefinition]="getConditionDefinition(condition)"
                     [formControlName]="i"
                     [activeChannel]="activeChannel$ | async"
                 ></vdr-configurable-input>
@@ -49,7 +50,7 @@
                             vdrDropdownItem
                             (click)="addCondition(condition)"
                         >
-                            {{ condition.code }}
+                            {{ condition.description }}
                         </button>
                     </vdr-dropdown-menu>
                 </vdr-dropdown>
@@ -62,6 +63,7 @@
                 (remove)="removeAction($event)"
                 [facets]="facets$ | async"
                 [operation]="action"
+                [operationDefinition]="getActionDefinition(action)"
                 [formControlName]="i"
                 [activeChannel]="activeChannel$ | async"
             ></vdr-configurable-input>
@@ -78,7 +80,7 @@
                             vdrDropdownItem
                             (click)="addAction(action)"
                         >
-                            {{ action.code }}
+                            {{ action.description }}
                         </button>
                     </vdr-dropdown-menu>
                 </vdr-dropdown>

+ 16 - 7
admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts

@@ -7,6 +7,7 @@ import { mergeMap, shareReplay, take } from 'rxjs/operators';
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import {
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     ConfigurableOperationInput,
     CreatePromotionInput,
     FacetWithValues,
@@ -35,8 +36,8 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
     facets$: Observable<FacetWithValues.Fragment[]>;
     activeChannel$: Observable<GetActiveChannel.ActiveChannel>;
 
-    private allConditions: ConfigurableOperation[] = [];
-    private allActions: ConfigurableOperation[] = [];
+    private allConditions: ConfigurableOperationDefinition[] = [];
+    private allActions: ConfigurableOperationDefinition[] = [];
 
     constructor(
         router: Router,
@@ -63,9 +64,9 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
             .pipe(shareReplay(1));
 
         this.promotion$ = this.entity$;
-        this.dataService.promotion.getAdjustmentOperations().single$.subscribe(data => {
-            this.allActions = data.adjustmentOperations.actions;
-            this.allConditions = data.adjustmentOperations.conditions;
+        this.dataService.promotion.getPromotionActionsAndConditions().single$.subscribe(data => {
+            this.allActions = data.promotionActions;
+            this.allConditions = data.promotionConditions;
         });
         this.activeChannel$ = this.dataService.settings
             .getActiveChannel()
@@ -76,14 +77,22 @@ export class PromotionDetailComponent extends BaseDetailComponent<Promotion.Frag
         this.destroy();
     }
 
-    getAvailableConditions(): ConfigurableOperation[] {
+    getAvailableConditions(): ConfigurableOperationDefinition[] {
         return this.allConditions.filter(o => !this.conditions.find(c => c.code === o.code));
     }
 
-    getAvailableActions(): ConfigurableOperation[] {
+    getConditionDefinition(condition: ConfigurableOperation): ConfigurableOperationDefinition | undefined {
+        return this.allConditions.find(c => c.code === condition.code);
+    }
+
+    getAvailableActions(): ConfigurableOperationDefinition[] {
         return this.allActions.filter(o => !this.actions.find(a => a.code === o.code));
     }
 
+    getActionDefinition(action: ConfigurableOperation): ConfigurableOperationDefinition | undefined {
+        return this.allActions.find(c => c.code === action.code);
+    }
+
     saveButtonEnabled(): boolean {
         return (
             this.detailForm.dirty &&

+ 3 - 7
admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html

@@ -35,17 +35,13 @@
         <div class="clr-col">
             <label>{{ 'settings.payment-method-config-options' | translate }}</label>
             <section class="form-block" *ngFor="let arg of (entity$ | async).configArgs">
-                <vdr-form-field [label]="arg.name" [for]="arg.name" *ngIf="arg.type === ConfigArgType.STRING">
+                <vdr-form-field [label]="arg.name" [for]="arg.name" *ngIf="getType(arg) === 'string'">
                     <input [id]="arg.name" type="text" [formControlName]="arg.name" />
                 </vdr-form-field>
-                <vdr-form-field [label]="arg.name" [for]="arg.name" *ngIf="arg.type === ConfigArgType.INT">
+                <vdr-form-field [label]="arg.name" [for]="arg.name" *ngIf="getType(arg) === 'int'">
                     <input [id]="arg.name" type="number" [formControlName]="arg.name" />
                 </vdr-form-field>
-                <vdr-form-field
-                    [label]="arg.name"
-                    [for]="arg.name"
-                    *ngIf="arg.type === ConfigArgType.BOOLEAN"
-                >
+                <vdr-form-field [label]="arg.name" [for]="arg.name" *ngIf="getType(arg) === 'boolean'">
                     <input type="checkbox" [id]="arg.name" [formControlName]="arg.name" clrCheckbox />
                 </vdr-form-field>
             </section>

+ 8 - 9
admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.ts

@@ -2,14 +2,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
 import { mergeMap, take } from 'rxjs/operators';
+import { ConfigArgSubset, ConfigArgType } from 'shared/shared-types';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
-import {
-    ConfigArg,
-    ConfigArgType,
-    PaymentMethod,
-    UpdatePaymentMethodInput,
-} from '../../../common/generated-types';
+import { ConfigArg, PaymentMethod, UpdatePaymentMethodInput } from '../../../common/generated-types';
 import { _ } from '../../../core/providers/i18n/mark-for-extraction';
 import { NotificationService } from '../../../core/providers/notification/notification.service';
 import { DataService } from '../../../data/providers/data.service';
@@ -24,7 +20,6 @@ import { ServerConfigService } from '../../../data/server-config';
 export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMethod.Fragment>
     implements OnInit, OnDestroy {
     detailForm: FormGroup;
-    readonly ConfigArgType = ConfigArgType;
 
     constructor(
         router: Router,
@@ -51,6 +46,10 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
         this.destroy();
     }
 
+    getType(arg: PaymentMethod.ConfigArgs): ConfigArgSubset<'int' | 'string' | 'boolean'> {
+        return arg.type as any;
+    }
+
     save() {
         this.entity$
             .pipe(
@@ -106,9 +105,9 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
 
     private parseArgValue(arg: ConfigArg): string | number | boolean {
         switch (arg.type) {
-            case ConfigArgType.INT:
+            case 'int':
                 return Number.parseInt(arg.value || '0', 10);
-            case ConfigArgType.BOOLEAN:
+            case 'boolean':
                 return arg.value === 'false' ? false : true;
             default:
                 return arg.value || '';

+ 4 - 2
admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html

@@ -36,6 +36,7 @@
             <vdr-configurable-input
                 *ngIf="selectedChecker"
                 [operation]="selectedChecker"
+                [operationDefinition]="selectedCheckerDefinition"
                 [activeChannel]="activeChannel$ | async"
                 (remove)="selectedChecker = null"
                 formControlName="checker"
@@ -51,7 +52,7 @@
                             *ngFor="let checker of checkers"
                             type="button"
                             vdrDropdownItem
-                            (click)="selectedChecker = checker"
+                            (click)="selectChecker(checker)"
                         >
                             {{ checker.description }}
                         </button>
@@ -64,6 +65,7 @@
             <vdr-configurable-input
                 *ngIf="selectedCalculator"
                 [operation]="selectedCalculator"
+                [operationDefinition]="selectedCalculatorDefinition"
                 [activeChannel]="activeChannel$ | async"
                 (remove)="selectedCalculator = null"
                 formControlName="calculator"
@@ -79,7 +81,7 @@
                             *ngFor="let calculator of calculators"
                             type="button"
                             vdrDropdownItem
-                            (click)="selectedCalculator = calculator"
+                            (click)="selectCalculator(calculator)"
                         >
                             {{ calculator.description }}
                         </button>

+ 35 - 8
admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts

@@ -1,12 +1,14 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
 import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { merge, Observable, of, Subject } from 'rxjs';
+import { combineLatest, merge, Observable, of, Subject } from 'rxjs';
 import { mergeMap, switchMap, take, takeUntil } from 'rxjs/operators';
 
 import { BaseDetailComponent } from '../../../common/base-detail.component';
 import {
+    ConfigArg,
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     ConfigurableOperationInput,
     CreateShippingMethodInput,
     GetActiveChannel,
@@ -31,10 +33,12 @@ import { TestOrderLine } from '../test-order-builder/test-order-builder.componen
 export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingMethod.Fragment>
     implements OnInit, OnDestroy {
     detailForm: FormGroup;
-    checkers: ConfigurableOperation[] = [];
-    calculators: ConfigurableOperation[] = [];
+    checkers: ConfigurableOperationDefinition[] = [];
+    calculators: ConfigurableOperationDefinition[] = [];
     selectedChecker?: ConfigurableOperation;
+    selectedCheckerDefinition?: ConfigurableOperationDefinition;
     selectedCalculator?: ConfigurableOperation;
+    selectedCalculatorDefinition?: ConfigurableOperationDefinition;
     activeChannel$: Observable<GetActiveChannel.ActiveChannel>;
     testAddress: TestAddress;
     testOrderLines: TestOrderLine[];
@@ -62,10 +66,19 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
 
     ngOnInit() {
         this.init();
-        this.dataService.shippingMethod.getShippingMethodOperations().single$.subscribe(data => {
+        combineLatest(
+            this.dataService.shippingMethod.getShippingMethodOperations().single$,
+            this.entity$.pipe(take(1)),
+        ).subscribe(([data, entity]) => {
             this.checkers = data.shippingEligibilityCheckers;
             this.calculators = data.shippingCalculators;
             this.changeDetector.markForCheck();
+            this.selectedCheckerDefinition = data.shippingEligibilityCheckers.find(
+                c => c.code === (entity.checker && entity.checker.code),
+            );
+            this.selectedCalculatorDefinition = data.shippingCalculators.find(
+                c => c.code === (entity.calculator && entity.calculator.code),
+            );
         });
 
         this.activeChannel$ = this.dataService.settings
@@ -107,12 +120,26 @@ export class ShippingMethodDetailComponent extends BaseDetailComponent<ShippingM
         this.destroy();
     }
 
-    selectChecker(checker: ConfigurableOperation) {
-        this.selectedChecker = checker;
+    selectChecker(checker: ConfigurableOperationDefinition) {
+        this.selectedCheckerDefinition = checker;
+        this.selectedChecker = this.configurableDefinitionToInstance(checker);
+    }
+
+    selectCalculator(calculator: ConfigurableOperationDefinition) {
+        this.selectedCalculatorDefinition = calculator;
+        this.selectedCalculator = this.configurableDefinitionToInstance(calculator);
     }
 
-    selectCalculator(calculator: ConfigurableOperation) {
-        this.selectedCalculator = calculator;
+    private configurableDefinitionToInstance(def: ConfigurableOperationDefinition): ConfigurableOperation {
+        return {
+            ...def,
+            args: def.args.map(arg => {
+                return {
+                    ...arg,
+                    value: '',
+                };
+            }),
+        } as ConfigurableOperation;
     }
 
     create() {

+ 11 - 16
admin-ui/src/app/shared/components/configurable-input/configurable-input.component.html

@@ -4,51 +4,46 @@
         <form [formGroup]="form" *ngIf="operation" class="operation-inputs">
             <div *ngFor="let arg of operation.args" class="arg-row">
                 <label>{{ arg.name | sentenceCase }}</label>
-                <clr-checkbox-wrapper *ngIf="arg.type === ConfigArgType.BOOLEAN">
+                <clr-checkbox-wrapper *ngIf="getArgType(arg) === 'boolean'">
                     <input type="checkbox" clrCheckbox [formControlName]="arg.name" [id]="arg.name" />
                 </clr-checkbox-wrapper>
                 <input
-                    *ngIf="arg.type === ConfigArgType.INT"
+                    *ngIf="isIntInput(arg)"
                     [name]="arg.name"
                     type="number"
                     step="1"
                     [formControlName]="arg.name"
                 />
                 <input
-                    *ngIf="arg.type === ConfigArgType.STRING"
+                    *ngIf="isStringWithoutOptions(arg)"
                     [name]="arg.name"
                     type="text"
                     [formControlName]="arg.name"
                 />
                 <input
-                    *ngIf="arg.type === ConfigArgType.DATETIME"
+                    *ngIf="getArgType(arg) === 'datetime'"
                     [name]="arg.name"
                     type="date"
                     [formControlName]="arg.name"
                 />
                 <vdr-currency-input
-                    *ngIf="arg.type === ConfigArgType.MONEY"
+                    *ngIf="isMoneyInput(arg)"
                     [formControlName]="arg.name"
                     [currencyCode]="activeChannel?.currencyCode"
                 ></vdr-currency-input>
                 <vdr-percentage-suffix-input
-                    *ngIf="arg.type === ConfigArgType.PERCENTAGE"
+                    *ngIf="isPercentageInput(arg)"
                     [formControlName]="arg.name"
                 ></vdr-percentage-suffix-input>
                 <vdr-facet-value-selector
                     [facets]="facets"
                     [formControlName]="arg.name"
-                    *ngIf="arg.type === ConfigArgType.FACET_VALUE_IDS && facets"
+                    *ngIf="getArgType(arg) === 'facetValueIds' && facets"
                 ></vdr-facet-value-selector>
-                <select
-                    clrSelect
-                    [formControlName]="arg.name"
-                    *ngIf="arg.type === ConfigArgType.STRING_OPERATOR"
-                >
-                    <option value="contains">contains</option>
-                    <option value="doesNotContain">does not contain</option>
-                    <option value="startsWith">starts with</option>
-                    <option value="endsWith">ends with</option>
+                <select clrSelect [formControlName]="arg.name" *ngIf="isStringWithOptions(arg)">
+                    <option *ngFor="let option of getStringOptions(arg)" [value]="option.value">
+                        {{ option.label || option.value }}
+                    </option>
                 </select>
             </div>
         </form>

+ 74 - 6
admin-ui/src/app/shared/components/configurable-input/configurable-input.component.ts

@@ -21,10 +21,13 @@ import {
     Validators,
 } from '@angular/forms';
 import { Subscription } from 'rxjs';
+import { StringFieldOption } from 'shared/generated-types';
+import { ConfigArgType } from 'shared/shared-types';
 
 import {
-    ConfigArgType,
+    ConfigArg,
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     FacetWithValues,
     GetActiveChannel,
 } from '../../../common/generated-types';
@@ -52,7 +55,8 @@ import { interpolateDescription } from '../../../common/utilities/interpolate-de
     ],
 })
 export class ConfigurableInputComponent implements OnChanges, OnDestroy, ControlValueAccessor, Validator {
-    @Input() operation: ConfigurableOperation;
+    @Input() operation?: ConfigurableOperation;
+    @Input() operationDefinition?: ConfigurableOperationDefinition;
     @Input() facets: FacetWithValues.Fragment[] = [];
     @Input() activeChannel: GetActiveChannel.ActiveChannel;
     @Output() remove = new EventEmitter<ConfigurableOperation>();
@@ -60,11 +64,14 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
     onChange: (val: any) => void;
     onTouch: () => void;
     form = new FormGroup({});
-    ConfigArgType = ConfigArgType;
     private subscription: Subscription;
 
     interpolateDescription(): string {
-        return interpolateDescription(this.operation, this.form.value);
+        if (this.operationDefinition) {
+            return interpolateDescription(this.operationDefinition, this.form.value);
+        } else {
+            return '';
+        }
     }
 
     ngOnChanges(changes: SimpleChanges) {
@@ -101,15 +108,76 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
         }
     }
 
+    isIntInput(arg: ConfigArg): boolean {
+        if (this.getArgType(arg) === 'int') {
+            const config = this.getArgConfig(arg);
+            return !!(!config || config.inputType === 'default');
+        }
+        return false;
+    }
+
+    isMoneyInput(arg: ConfigArg): boolean {
+        if (this.getArgType(arg) === 'int') {
+            const config = this.getArgConfig(arg);
+            return !!(config && config.inputType === 'money');
+        }
+        return false;
+    }
+
+    isPercentageInput(arg: ConfigArg): boolean {
+        if (this.getArgType(arg) === 'int') {
+            const config = this.getArgConfig(arg);
+            return !!(config && config.inputType === 'percentage');
+        }
+        return false;
+    }
+
+    isStringWithOptions(arg: ConfigArg): boolean {
+        if (this.getArgType(arg) === 'string') {
+            return 0 < this.getStringOptions(arg).length;
+        }
+        return false;
+    }
+
+    isStringWithoutOptions(arg: ConfigArg): boolean {
+        if (this.getArgType(arg) === 'string') {
+            return this.getStringOptions(arg).length === 0;
+        }
+        return false;
+    }
+
+    getStringOptions(arg: ConfigArg): StringFieldOption[] {
+        if (this.getArgType(arg) === 'string') {
+            const config = this.getArgConfig(arg);
+            return (config && config.options) || [];
+        }
+        return [];
+    }
+
+    getArgType(arg: ConfigArg): ConfigArgType {
+        return arg.type as ConfigArgType;
+    }
+
+    private getArgConfig(arg: ConfigArg): Record<string, any> | undefined {
+        if (this.operationDefinition) {
+            const match = this.operationDefinition.args.find(argDef => argDef.name === arg.name);
+            return match && match.config;
+        }
+    }
+
     private createForm() {
+        if (!this.operation) {
+            return;
+        }
         if (this.subscription) {
             this.subscription.unsubscribe();
         }
         this.form = new FormGroup({});
+
         if (this.operation.args) {
             for (const arg of this.operation.args) {
                 let value: any = arg.value;
-                if (arg.type === ConfigArgType.BOOLEAN) {
+                if (arg.type === 'boolean') {
                     value = arg.value === 'true';
                 }
                 this.form.addControl(arg.name, new FormControl(value, Validators.required));
@@ -119,7 +187,7 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
         this.subscription = this.form.valueChanges.subscribe(value => {
             if (this.onChange) {
                 this.onChange({
-                    code: this.operation.code,
+                    code: this.operation && this.operation.code,
                     args: value,
                 });
             }

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@vendure/core",
-  "version": "0.1.2-beta.10",
+  "version": "0.1.2-beta.11",
   "description": "A modern, headless ecommerce framework",
   "repository": {
     "type": "git",