Browse Source

Merge branch 'rework-configurable-ops'

Michael Bromley 6 years ago
parent
commit
d1bdaccc66
68 changed files with 935 additions and 674 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. 6 4
      admin-ui/src/app/common/introspection-result.ts
  5. 17 17
      admin-ui/src/app/common/utilities/interpolate-description.spec.ts
  6. 4 4
      admin-ui/src/app/common/utilities/interpolate-description.ts
  7. 4 4
      admin-ui/src/app/data/definitions/collection-definitions.ts
  8. 8 20
      admin-ui/src/app/data/definitions/promotion-definitions.ts
  9. 24 0
      admin-ui/src/app/data/definitions/shared-definitions.ts
  10. 5 5
      admin-ui/src/app/data/definitions/shipping-definitions.ts
  11. 1 1
      admin-ui/src/app/data/providers/promotion-data.service.ts
  12. 5 3
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.html
  13. 16 7
      admin-ui/src/app/marketing/components/promotion-detail/promotion-detail.component.ts
  14. 3 7
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.html
  15. 8 9
      admin-ui/src/app/settings/components/payment-method-detail/payment-method-detail.component.ts
  16. 4 2
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.html
  17. 35 8
      admin-ui/src/app/settings/components/shipping-method-detail/shipping-method-detail.component.ts
  18. 11 16
      admin-ui/src/app/shared/components/configurable-input/configurable-input.component.html
  19. 74 6
      admin-ui/src/app/shared/components/configurable-input/configurable-input.component.ts
  20. 19 28
      packages/common/src/generated-shop-types.ts
  21. 24 32
      packages/common/src/generated-types.ts
  22. 13 1
      packages/common/src/shared-types.ts
  23. 4 6
      packages/core/e2e/__snapshots__/collection.e2e-spec.ts.snap
  24. 45 49
      packages/core/e2e/__snapshots__/promotion.e2e-spec.ts.snap
  25. 15 17
      packages/core/e2e/collection.e2e-spec.ts
  26. 4 5
      packages/core/e2e/default-search-plugin.e2e-spec.ts
  27. 0 2
      packages/core/e2e/graphql/fragments.ts
  28. 47 40
      packages/core/e2e/graphql/generated-e2e-admin-types.ts
  29. 19 28
      packages/core/e2e/graphql/generated-e2e-shop-types.ts
  30. 4 3
      packages/core/e2e/order.e2e-spec.ts
  31. 34 20
      packages/core/e2e/promotion.e2e-spec.ts
  32. 2 3
      packages/core/e2e/shop-catalog.e2e-spec.ts
  33. 4 3
      packages/core/e2e/shop-order.e2e-spec.ts
  34. 54 43
      packages/core/e2e/stock-control.e2e-spec.ts
  35. 1 1
      packages/core/package.json
  36. 3 8
      packages/core/src/api/common/id-codec.service.ts
  37. 3 3
      packages/core/src/api/resolvers/admin/collection.resolver.ts
  38. 11 6
      packages/core/src/api/resolvers/admin/promotion.resolver.ts
  39. 5 5
      packages/core/src/api/resolvers/admin/shipping-method.resolver.ts
  40. 1 1
      packages/core/src/api/schema/admin-api/collection.api.graphql
  41. 2 1
      packages/core/src/api/schema/admin-api/promotion.api.graphql
  42. 2 2
      packages/core/src/api/schema/admin-api/shipping-method.api.graphql
  43. 16 23
      packages/core/src/api/schema/common/common-types.graphql
  44. 0 5
      packages/core/src/api/schema/type/promotion.type.graphql
  45. 98 24
      packages/core/src/common/configurable-operation.ts
  46. 6 4
      packages/core/src/config/collection/collection-filter.ts
  47. 23 9
      packages/core/src/config/collection/default-collection-filters.ts
  48. 4 4
      packages/core/src/config/payment-method/example-payment-method-config.ts
  49. 6 4
      packages/core/src/config/payment-method/payment-method-handler.ts
  50. 34 8
      packages/core/src/config/promotion/default-promotion-actions.ts
  51. 24 13
      packages/core/src/config/promotion/default-promotion-conditions.ts
  52. 6 8
      packages/core/src/config/promotion/promotion-action.ts
  53. 9 12
      packages/core/src/config/promotion/promotion-condition.ts
  54. 4 4
      packages/core/src/config/shipping-method/default-shipping-calculator.ts
  55. 3 3
      packages/core/src/config/shipping-method/default-shipping-eligibility-checker.ts
  56. 17 11
      packages/core/src/config/shipping-method/shipping-calculator.ts
  57. 10 9
      packages/core/src/config/shipping-method/shipping-eligibility-checker.ts
  58. 10 7
      packages/core/src/data-import/providers/populator/populator.ts
  59. 5 1
      packages/core/src/entity/shipping-method/shipping-method.entity.ts
  60. 20 15
      packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts
  61. 0 1
      packages/core/src/service/helpers/shipping-configuration/shipping-configuration.ts
  62. 21 8
      packages/core/src/service/services/collection.service.ts
  63. 7 12
      packages/core/src/service/services/payment-method.service.ts
  64. 7 12
      packages/core/src/service/services/promotion.service.ts
  65. 8 5
      packages/core/src/service/services/shipping-method.service.ts
  66. 0 0
      schema-admin.json
  67. 0 0
      schema-shop.json
  68. 1 1
      scripts/codegen/generate-graphql-types.ts

+ 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 {

+ 6 - 4
admin-ui/src/app/common/introspection-result.ts

@@ -1,12 +1,14 @@
+// tslint:disable
+
 export interface IntrospectionResultData {
     __schema: {
-        types: Array<{
+        types: {
             kind: string;
             name: string;
-            possibleTypes: Array<{
+            possibleTypes: {
                 name: string;
-            }>;
-        }>;
+            }[];
+        }[];
     };
 }
 

+ 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,
                 });
             }

+ 19 - 28
packages/common/src/generated-shop-types.ts

@@ -45,12 +45,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',
@@ -215,38 +209,35 @@ export type CollectionTranslation = {
 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'];
 };
 

+ 24 - 32
packages/common/src/generated-types.ts

@@ -50,12 +50,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',
@@ -291,38 +285,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'],
 };
 
@@ -2613,7 +2604,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>,
@@ -2637,13 +2628,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>,

+ 13 - 1
packages/common/src/shared-types.ts

@@ -47,7 +47,19 @@ export type ID = string | number;
  */
 export type CustomFieldType = 'string' | 'localeString' | 'int' | 'float' | 'boolean' | 'datetime';
 
-export type CustomFieldsObject = { [key: string]: any; };
+/**
+ * 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 type ConfigArgType = 'string' | 'int' | 'float' | 'boolean' | 'datetime' | 'facetValueIds';
+
+export type ConfigArgSubset<T extends ConfigArgType> = T;
+
+export type CustomFieldsObject = { [key: string]: any };
 
 /**
  * This interface describes the shape of the JSON config file used by the Admin UI.

+ 4 - 6
packages/core/e2e/__snapshots__/collection.e2e-spec.ts.snap

@@ -38,17 +38,16 @@ Object {
       "args": Array [
         Object {
           "name": "facetValueIds",
-          "type": "FACET_VALUE_IDS",
+          "type": "facetValueIds",
           "value": "[\\"T_1\\"]",
         },
         Object {
           "name": "containsAny",
-          "type": "BOOLEAN",
+          "type": "boolean",
           "value": "false",
         },
       ],
       "code": "facet-value-filter",
-      "description": "Filter by FacetValues",
     },
   ],
   "id": "T_3",
@@ -99,17 +98,16 @@ Object {
       "args": Array [
         Object {
           "name": "facetValueIds",
-          "type": "FACET_VALUE_IDS",
+          "type": "facetValueIds",
           "value": "[\\"T_3\\"]",
         },
         Object {
           "name": "containsAny",
-          "type": "BOOLEAN",
+          "type": "boolean",
           "value": "false",
         },
       ],
       "code": "facet-value-filter",
-      "description": "Filter by FacetValues",
     },
   ],
   "id": "T_5",

+ 45 - 49
packages/core/e2e/__snapshots__/promotion.e2e-spec.ts.snap

@@ -1,45 +1,46 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Promotion resolver adjustmentOperations 1`] = `
-Object {
-  "actions": Array [
-    Object {
-      "args": Array [
-        Object {
-          "name": "facetValueIds",
-          "type": "FACET_VALUE_IDS",
-          "value": null,
-        },
-      ],
-      "code": "promo_action",
-      "description": "description for promo_action",
-    },
-  ],
-  "conditions": Array [
-    Object {
-      "args": Array [
-        Object {
-          "name": "arg",
-          "type": "MONEY",
-          "value": null,
-        },
-      ],
-      "code": "promo_condition",
-      "description": "description for promo_condition",
-    },
-    Object {
-      "args": Array [
-        Object {
-          "name": "arg",
-          "type": "MONEY",
-          "value": null,
-        },
-      ],
-      "code": "promo_condition2",
-      "description": "description for promo_condition2",
-    },
-  ],
-}
+Array [
+  Object {
+    "args": Array [
+      Object {
+        "config": null,
+        "name": "facetValueIds",
+        "type": "facetValueIds",
+      },
+    ],
+    "code": "promo_action",
+    "description": "description for promo_action",
+  },
+]
+`;
+
+exports[`Promotion resolver adjustmentOperations 2`] = `
+Array [
+  Object {
+    "args": Array [
+      Object {
+        "config": null,
+        "name": "arg",
+        "type": "int",
+      },
+    ],
+    "code": "promo_condition",
+    "description": "description for promo_condition",
+  },
+  Object {
+    "args": Array [
+      Object {
+        "config": null,
+        "name": "arg",
+        "type": "int",
+      },
+    ],
+    "code": "promo_condition2",
+    "description": "description for promo_condition2",
+  },
+]
 `;
 
 exports[`Promotion resolver createPromotion 1`] = `
@@ -49,12 +50,11 @@ Object {
       "args": Array [
         Object {
           "name": "facetValueIds",
-          "type": "FACET_VALUE_IDS",
+          "type": "facetValueIds",
           "value": "[\\"T_1\\"]",
         },
       ],
       "code": "promo_action",
-      "description": "description for promo_action",
     },
   ],
   "conditions": Array [
@@ -62,12 +62,11 @@ Object {
       "args": Array [
         Object {
           "name": "arg",
-          "type": "MONEY",
+          "type": "int",
           "value": "500",
         },
       ],
       "code": "promo_condition",
-      "description": "description for promo_condition",
     },
   ],
   "enabled": true,
@@ -82,12 +81,11 @@ Object {
       "args": Array [
         Object {
           "name": "facetValueIds",
-          "type": "FACET_VALUE_IDS",
+          "type": "facetValueIds",
           "value": "[\\"T_1\\"]",
         },
       ],
       "code": "promo_action",
-      "description": "description for promo_action",
     },
   ],
   "conditions": Array [
@@ -95,23 +93,21 @@ Object {
       "args": Array [
         Object {
           "name": "arg",
-          "type": "MONEY",
+          "type": "int",
           "value": "90",
         },
       ],
       "code": "promo_condition",
-      "description": "description for promo_condition",
     },
     Object {
       "args": Array [
         Object {
           "name": "arg",
-          "type": "MONEY",
+          "type": "int",
           "value": "10",
         },
       ],
       "code": "promo_condition2",
-      "description": "description for promo_condition2",
     },
   ],
   "enabled": true,

+ 15 - 17
packages/core/e2e/collection.e2e-spec.ts

@@ -4,7 +4,6 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { pick } from '../../common/lib/pick';
-import { StringOperator } from '../src/common/configurable-operation';
 import {
     facetValueCollectionFilter,
     variantNameCollectionFilter,
@@ -14,7 +13,6 @@ import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import { COLLECTION_FRAGMENT, FACET_VALUE_FRAGMENT } from './graphql/fragments';
 import {
     Collection,
-    ConfigArgType,
     CreateCollection,
     CreateCollectionInput,
     CreateCollectionSelectVariants,
@@ -109,12 +107,12 @@ describe('Collection resolver', () => {
                                     {
                                         name: 'facetValueIds',
                                         value: `["${getFacetValueId('electronics')}"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `false`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -145,12 +143,12 @@ describe('Collection resolver', () => {
                                     {
                                         name: 'facetValueIds',
                                         value: `["${getFacetValueId('computers')}"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `false`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -176,12 +174,12 @@ describe('Collection resolver', () => {
                                     {
                                         name: 'facetValueIds',
                                         value: `["${getFacetValueId('pear')}"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `false`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -415,12 +413,12 @@ describe('Collection resolver', () => {
                                 {
                                     name: 'operator',
                                     value: 'contains',
-                                    type: ConfigArgType.STRING_OPERATOR,
+                                    type: 'string',
                                 },
                                 {
                                     name: 'term',
                                     value: 'laptop',
-                                    type: ConfigArgType.STRING,
+                                    type: 'string',
                                 },
                             ],
                         },
@@ -601,12 +599,12 @@ describe('Collection resolver', () => {
                                         value: `["${getFacetValueId('pear')}", "${getFacetValueId(
                                             'photo',
                                         )}"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `false`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -636,12 +634,12 @@ describe('Collection resolver', () => {
                                         value: `["${getFacetValueId('pear')}", "${getFacetValueId(
                                             'photo',
                                         )}"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `true`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -663,7 +661,7 @@ describe('Collection resolver', () => {
 
         describe('variantName filter', () => {
             async function createVariantNameFilteredCollection(
-                operator: StringOperator,
+                operator: string,
                 term: string,
             ): Promise<Collection.Fragment> {
                 const { createCollection } = await client.query<
@@ -681,12 +679,12 @@ describe('Collection resolver', () => {
                                     {
                                         name: 'operator',
                                         value: operator,
-                                        type: ConfigArgType.STRING_OPERATOR,
+                                        type: 'string',
                                     },
                                     {
                                         name: 'term',
                                         value: term,
-                                        type: ConfigArgType.STRING,
+                                        type: 'string',
                                     },
                                 ],
                             },

+ 4 - 5
packages/core/e2e/default-search-plugin.e2e-spec.ts

@@ -8,7 +8,6 @@ import { DefaultSearchPlugin } from '../src/plugin/default-search-plugin/default
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import {
-    ConfigArgType,
     CreateCollection,
     CreateFacet,
     GetRunningJobs,
@@ -390,12 +389,12 @@ describe('Default search plugin', () => {
                                     {
                                         name: 'facetValueIds',
                                         value: `["T_4"]`,
-                                        type: ConfigArgType.FACET_VALUE_IDS,
+                                        type: 'facetValueIds',
                                     },
                                     {
                                         name: 'containsAny',
                                         value: `false`,
-                                        type: ConfigArgType.BOOLEAN,
+                                        type: 'boolean',
                                     },
                                 ],
                             },
@@ -444,12 +443,12 @@ describe('Default search plugin', () => {
                                 {
                                     name: 'facetValueIds',
                                     value: `["T_3"]`,
-                                    type: ConfigArgType.FACET_VALUE_IDS,
+                                    type: 'facetValueIds',
                                 },
                                 {
                                     name: 'containsAny',
                                     value: `false`,
-                                    type: ConfigArgType.BOOLEAN,
+                                    type: 'boolean',
                                 },
                             ],
                         },

+ 0 - 2
packages/core/e2e/graphql/fragments.ts

@@ -149,7 +149,6 @@ export const CONFIGURABLE_FRAGMENT = gql`
             value
         }
         code
-        description
     }
 `;
 
@@ -239,7 +238,6 @@ export const COUNTRY_FRAGMENT = gql`
     }
 `;
 
-
 export const ADDRESS_FRAGMENT = gql`
     fragment Address on Address {
         id

+ 47 - 40
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -50,12 +50,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',
@@ -291,38 +285,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'];
 };
 
@@ -2547,7 +2538,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>;
@@ -2571,13 +2562,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>;
@@ -3828,7 +3820,7 @@ export type RoleFragment = { __typename?: 'Role' } & Pick<
 
 export type ConfigurableOperationFragment = { __typename?: 'ConfigurableOperation' } & Pick<
     ConfigurableOperation,
-    'code' | 'description'
+    'code'
 > & { args: Array<{ __typename?: 'ConfigArg' } & Pick<ConfigArg, 'name' | 'type' | 'value'>> };
 
 export type CollectionFragment = { __typename?: 'Collection' } & Pick<
@@ -4604,13 +4596,24 @@ export type UpdatePromotionMutation = { __typename?: 'Mutation' } & {
     updatePromotion: { __typename?: 'Promotion' } & PromotionFragment;
 };
 
+export type ConfigurableOperationDefFragment = { __typename?: 'ConfigurableOperationDefinition' } & Pick<
+    ConfigurableOperationDefinition,
+    'code' | 'description'
+> & {
+        args: Array<
+            { __typename?: 'ConfigArgDefinition' } & Pick<ConfigArgDefinition, 'name' | 'type' | 'config'>
+        >;
+    };
+
 export type GetAdjustmentOperationsQueryVariables = {};
 
 export type GetAdjustmentOperationsQuery = { __typename?: 'Query' } & {
-    adjustmentOperations: { __typename?: 'AdjustmentOperations' } & {
-        actions: Array<{ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment>;
-        conditions: Array<{ __typename?: 'ConfigurableOperation' } & ConfigurableOperationFragment>;
-    };
+    promotionActions: Array<
+        { __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment
+    >;
+    promotionConditions: Array<
+        { __typename?: 'ConfigurableOperationDefinition' } & ConfigurableOperationDefFragment
+    >;
 };
 
 export type GetRolesQueryVariables = {
@@ -5717,12 +5720,16 @@ export namespace UpdatePromotion {
     export type UpdatePromotion = PromotionFragment;
 }
 
+export namespace ConfigurableOperationDef {
+    export type Fragment = ConfigurableOperationDefFragment;
+    export type Args = NonNullable<ConfigurableOperationDefFragment['args'][0]>;
+}
+
 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 PromotionActions = ConfigurableOperationDefFragment;
+    export type PromotionConditions = ConfigurableOperationDefFragment;
 }
 
 export namespace GetRoles {

+ 19 - 28
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -45,12 +45,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',
@@ -215,38 +209,35 @@ export type CollectionTranslation = {
 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'];
 };
 

+ 4 - 3
packages/core/e2e/order.e2e-spec.ts

@@ -1,4 +1,5 @@
 /* tslint:disable:no-non-null-assertion */
+import { LanguageCode } from '@vendure/common/lib/generated-shop-types';
 import gql from 'graphql-tag';
 import path from 'path';
 
@@ -1145,7 +1146,7 @@ describe('Orders resolver', () => {
  */
 const twoStagePaymentMethod = new PaymentMethodHandler({
     code: 'authorize-only-payment-method',
-    description: 'Test Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {
@@ -1170,7 +1171,7 @@ const twoStagePaymentMethod = new PaymentMethodHandler({
  */
 const singleStageRefundablePaymentMethod = new PaymentMethodHandler({
     code: 'single-stage-refundable-payment-method',
-    description: 'Test Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {
@@ -1197,7 +1198,7 @@ const singleStageRefundablePaymentMethod = new PaymentMethodHandler({
  */
 const failsToSettlePaymentMethod = new PaymentMethodHandler({
     code: 'fails-to-settle-payment-method',
-    description: 'Test Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {

+ 34 - 20
packages/core/e2e/promotion.e2e-spec.ts

@@ -2,13 +2,13 @@ import { pick } from '@vendure/common/lib/pick';
 import gql from 'graphql-tag';
 import path from 'path';
 
+import { LanguageCode } from '../../common/lib/generated-types';
 import { PromotionAction, PromotionOrderAction } from '../src/config/promotion/promotion-action';
 import { PromotionCondition } from '../src/config/promotion/promotion-condition';
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
-import { CONFIGURABLE_FRAGMENT, PROMOTION_FRAGMENT } from './graphql/fragments';
+import { PROMOTION_FRAGMENT } from './graphql/fragments';
 import {
-    ConfigArgType,
     CreatePromotion,
     DeletePromotion,
     DeletionResult,
@@ -68,7 +68,7 @@ describe('Promotion resolver', () => {
                     conditions: [
                         {
                             code: promoCondition.code,
-                            arguments: [{ name: 'arg', value: '500', type: ConfigArgType.MONEY }],
+                            arguments: [{ name: 'arg', value: '500', type: 'int' }],
                         },
                     ],
                     actions: [
@@ -78,7 +78,7 @@ describe('Promotion resolver', () => {
                                 {
                                     name: 'facetValueIds',
                                     value: '["T_1"]',
-                                    type: ConfigArgType.FACET_VALUE_IDS,
+                                    type: 'facetValueIds',
                                 },
                             ],
                         },
@@ -99,11 +99,11 @@ describe('Promotion resolver', () => {
                     conditions: [
                         {
                             code: promoCondition.code,
-                            arguments: [{ name: 'arg', value: '90', type: ConfigArgType.MONEY }],
+                            arguments: [{ name: 'arg', value: '90', type: 'int' }],
                         },
                         {
                             code: promoCondition2.code,
-                            arguments: [{ name: 'arg', value: '10', type: ConfigArgType.MONEY }],
+                            arguments: [{ name: 'arg', value: '10', type: 'int' }],
                         },
                     ],
                 },
@@ -135,7 +135,8 @@ describe('Promotion resolver', () => {
             GET_ADJUSTMENT_OPERATIONS,
         );
 
-        expect(result.adjustmentOperations).toMatchSnapshot();
+        expect(result.promotionActions).toMatchSnapshot();
+        expect(result.promotionConditions).toMatchSnapshot();
     });
 
     describe('deletion', () => {
@@ -149,7 +150,10 @@ describe('Promotion resolver', () => {
 
         it('deletes a promotion', async () => {
             promotionToDelete = allPromotions[0];
-            const result = await client.query<DeletePromotion.Mutation, DeletePromotion.Variables>(DELETE_PROMOTION, { id: promotionToDelete.id });
+            const result = await client.query<DeletePromotion.Mutation, DeletePromotion.Variables>(
+                DELETE_PROMOTION,
+                { id: promotionToDelete.id },
+            );
 
             expect(result.deletePromotion).toEqual({ result: DeletionResult.DELETED });
         });
@@ -188,8 +192,8 @@ describe('Promotion resolver', () => {
 function generateTestCondition(code: string): PromotionCondition<any> {
     return new PromotionCondition({
         code,
-        description: `description for ${code}`,
-        args: { arg: ConfigArgType.MONEY },
+        description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
+        args: { arg: { type: 'int' } },
         check: (order, args) => true,
     });
 }
@@ -197,8 +201,8 @@ function generateTestCondition(code: string): PromotionCondition<any> {
 function generateTestAction(code: string): PromotionAction<any> {
     return new PromotionOrderAction({
         code,
-        description: `description for ${code}`,
-        args: { facetValueIds: ConfigArgType.FACET_VALUE_IDS },
+        description: [{ languageCode: LanguageCode.en, value: `description for ${code}` }],
+        args: { facetValueIds: { type: 'facetValueIds' } },
         execute: (order, args) => {
             return 42;
         },
@@ -252,16 +256,26 @@ export const UPDATE_PROMOTION = gql`
     ${PROMOTION_FRAGMENT}
 `;
 
+export const CONFIGURABLE_DEF_FRAGMENT = gql`
+    fragment ConfigurableOperationDef on ConfigurableOperationDefinition {
+        args {
+            name
+            type
+            config
+        }
+        code
+        description
+    }
+`;
+
 export const GET_ADJUSTMENT_OPERATIONS = gql`
     query GetAdjustmentOperations {
-        adjustmentOperations {
-            actions {
-                ...ConfigurableOperation
-            }
-            conditions {
-                ...ConfigurableOperation
-            }
+        promotionActions {
+            ...ConfigurableOperationDef
+        }
+        promotionConditions {
+            ...ConfigurableOperationDef
         }
     }
-    ${CONFIGURABLE_FRAGMENT}
+    ${CONFIGURABLE_DEF_FRAGMENT}
 `;

+ 2 - 3
packages/core/e2e/shop-catalog.e2e-spec.ts

@@ -6,7 +6,6 @@ import { facetValueCollectionFilter } from '../src/config/collection/default-col
 
 import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
 import {
-    ConfigArgType,
     CreateCollection,
     CreateFacet,
     DisableProduct,
@@ -249,12 +248,12 @@ describe('Shop catalog', () => {
                                 {
                                     name: 'facetValueIds',
                                     value: `["${category.values[3].id}"]`,
-                                    type: ConfigArgType.FACET_VALUE_IDS,
+                                    type: 'facetValueIds',
                                 },
                                 {
                                     name: 'containsAny',
                                     value: `false`,
-                                    type: ConfigArgType.BOOLEAN,
+                                    type: 'boolean',
                                 },
                             ],
                         },

+ 4 - 3
packages/core/e2e/shop-order.e2e-spec.ts

@@ -9,6 +9,7 @@ import {
     GetCountryList,
     GetCustomer,
     GetCustomerList,
+    LanguageCode,
     UpdateCountry,
 } from './graphql/generated-e2e-admin-types';
 import {
@@ -868,7 +869,7 @@ describe('Shop orders', () => {
 
 const testPaymentMethod = new PaymentMethodHandler({
     code: 'test-payment-method',
-    description: 'Test Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {
@@ -885,7 +886,7 @@ const testPaymentMethod = new PaymentMethodHandler({
 
 const testFailingPaymentMethod = new PaymentMethodHandler({
     code: 'test-failing-payment-method',
-    description: 'Test Failing Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Failing Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {
@@ -901,7 +902,7 @@ const testFailingPaymentMethod = new PaymentMethodHandler({
 
 const testErrorPaymentMethod = new PaymentMethodHandler({
     code: 'test-error-payment-method',
-    description: 'Test Error Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Error Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {

+ 54 - 43
packages/core/e2e/stock-control.e2e-spec.ts

@@ -10,14 +10,26 @@ import { VARIANT_WITH_STOCK_FRAGMENT } from './graphql/fragments';
 import {
     CreateAddressInput,
     GetStockMovement,
+    LanguageCode,
     StockMovementType,
     UpdateProductVariantInput,
     UpdateStock,
     VariantWithStockFragment,
 } from './graphql/generated-e2e-admin-types';
-import { AddItemToOrder, AddPaymentToOrder, PaymentInput, SetShippingAddress, TransitionToState } from './graphql/generated-e2e-shop-types';
+import {
+    AddItemToOrder,
+    AddPaymentToOrder,
+    PaymentInput,
+    SetShippingAddress,
+    TransitionToState,
+} from './graphql/generated-e2e-shop-types';
 import { GET_STOCK_MOVEMENT } from './graphql/shared-definitions';
-import { ADD_ITEM_TO_ORDER, ADD_PAYMENT, SET_SHIPPING_ADDRESS, TRANSITION_TO_STATE } from './graphql/shop-definitions';
+import {
+    ADD_ITEM_TO_ORDER,
+    ADD_PAYMENT,
+    SET_SHIPPING_ADDRESS,
+    TRANSITION_TO_STATE,
+} from './graphql/shop-definitions';
 import { TestAdminClient, TestShopClient } from './test-client';
 import { TestServer } from './test-server';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
@@ -64,61 +76,57 @@ describe('Stock control', () => {
         });
 
         it('updating ProductVariant with same stockOnHand does not create a StockMovement', async () => {
-            const { updateProductVariants } = await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(
-                UPDATE_STOCK_ON_HAND,
-                {
-                    input: [
-                        {
-                            id: variants[0].id,
-                            stockOnHand: variants[0].stockOnHand,
-                        },
-                    ] as UpdateProductVariantInput[],
-                },
-            );
+            const { updateProductVariants } = await adminClient.query<
+                UpdateStock.Mutation,
+                UpdateStock.Variables
+            >(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
 
             expect(updateProductVariants[0]!.stockMovements.items).toEqual([]);
             expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(0);
         });
 
         it('increasing stockOnHand creates a StockMovement with correct quantity', async () => {
-            const { updateProductVariants } = await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(
-                UPDATE_STOCK_ON_HAND,
-                {
-                    input: [
-                        {
-                            id: variants[0].id,
-                            stockOnHand: variants[0].stockOnHand + 5,
-                        },
-                    ] as UpdateProductVariantInput[],
-                },
-            );
+            const { updateProductVariants } = await adminClient.query<
+                UpdateStock.Mutation,
+                UpdateStock.Variables
+            >(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand + 5,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
 
             expect(updateProductVariants[0]!.stockOnHand).toBe(5);
             expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(1);
-            expect(updateProductVariants[0]!.stockMovements.items[0].type).toBe(
-                StockMovementType.ADJUSTMENT,
-            );
+            expect(updateProductVariants[0]!.stockMovements.items[0].type).toBe(StockMovementType.ADJUSTMENT);
             expect(updateProductVariants[0]!.stockMovements.items[0].quantity).toBe(5);
         });
 
         it('decreasing stockOnHand creates a StockMovement with correct quantity', async () => {
-            const { updateProductVariants } = await adminClient.query<UpdateStock.Mutation, UpdateStock.Variables>(
-                UPDATE_STOCK_ON_HAND,
-                {
-                    input: [
-                        {
-                            id: variants[0].id,
-                            stockOnHand: variants[0].stockOnHand + 5 - 2,
-                        },
-                    ] as UpdateProductVariantInput[],
-                },
-            );
+            const { updateProductVariants } = await adminClient.query<
+                UpdateStock.Mutation,
+                UpdateStock.Variables
+            >(UPDATE_STOCK_ON_HAND, {
+                input: [
+                    {
+                        id: variants[0].id,
+                        stockOnHand: variants[0].stockOnHand + 5 - 2,
+                    },
+                ] as UpdateProductVariantInput[],
+            });
 
             expect(updateProductVariants[0]!.stockOnHand).toBe(3);
             expect(updateProductVariants[0]!.stockMovements.totalItems).toEqual(2);
-            expect(updateProductVariants[0]!.stockMovements.items[1].type).toBe(
-                StockMovementType.ADJUSTMENT,
-            );
+            expect(updateProductVariants[0]!.stockMovements.items[1].type).toBe(StockMovementType.ADJUSTMENT);
             expect(updateProductVariants[0]!.stockMovements.items[1].quantity).toBe(-2);
         });
 
@@ -189,7 +197,10 @@ describe('Stock control', () => {
         });
 
         it('creates a Sale when order completed', async () => {
-            const { addPaymentToOrder } = await shopClient.query<AddPaymentToOrder.Mutation, AddPaymentToOrder.Variables>(ADD_PAYMENT, {
+            const { addPaymentToOrder } = await shopClient.query<
+                AddPaymentToOrder.Mutation,
+                AddPaymentToOrder.Variables
+            >(ADD_PAYMENT, {
                 input: {
                     method: testPaymentMethod.code,
                     metadata: {},
@@ -227,7 +238,7 @@ describe('Stock control', () => {
 
 const testPaymentMethod = new PaymentMethodHandler({
     code: 'test-payment-method',
-    description: 'Test Payment Method',
+    description: [{ languageCode: LanguageCode.en, value: 'Test Payment Method' }],
     args: {},
     createPayment: (order, args, metadata) => {
         return {

+ 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",

+ 3 - 8
packages/core/src/api/common/id-codec.service.ts

@@ -1,10 +1,5 @@
 import { Injectable } from '@nestjs/common';
-import {
-    ConfigArgType,
-    ConfigurableOperation,
-    ConfigurableOperationInput,
-} from '@vendure/common/lib/generated-types';
-import { ID } from '@vendure/common/lib/shared-types';
+import { ConfigurableOperation, ConfigurableOperationInput } from '@vendure/common/lib/generated-types';
 
 import { ConfigService } from '../../config/config.service';
 
@@ -37,7 +32,7 @@ export class IdCodecService {
         const inputArray = Array.isArray(input) ? input : [input];
         for (const operationInput of inputArray) {
             for (const arg of operationInput.arguments) {
-                if (arg.type === ConfigArgType.FACET_VALUE_IDS && arg.value) {
+                if (arg.type === 'facetValueIds' && arg.value) {
                     const ids = JSON.parse(arg.value) as string[];
                     const decodedIds = ids.map(id => this.decode(id));
                     arg.value = JSON.stringify(decodedIds);
@@ -59,7 +54,7 @@ export class IdCodecService {
         const inputArray = Array.isArray(input) ? input : [input];
         for (const operation of inputArray) {
             for (const arg of operation.args) {
-                if (arg.type === ConfigArgType.FACET_VALUE_IDS && arg.value) {
+                if (arg.type === 'facetValueIds' && arg.value) {
                     const ids = JSON.parse(arg.value) as string[];
                     const encoded = ids.map(id => this.encode(id));
                     arg.value = JSON.stringify(encoded);

+ 3 - 3
packages/core/src/api/resolvers/admin/collection.resolver.ts

@@ -1,6 +1,6 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
-    ConfigurableOperation,
+    ConfigurableOperationDefinition,
     DeletionResponse,
     MutationCreateCollectionArgs,
     MutationDeleteCollectionArgs,
@@ -34,8 +34,8 @@ export class CollectionResolver {
     async collectionFilters(
         @Ctx() ctx: RequestContext,
         @Args() args: QueryCollectionsArgs,
-    ): Promise<ConfigurableOperation[]> {
-        return this.collectionService.getAvailableFilters();
+    ): Promise<ConfigurableOperationDefinition[]> {
+        return this.collectionService.getAvailableFilters(ctx);
     }
 
     @Query()

+ 11 - 6
packages/core/src/api/resolvers/admin/promotion.resolver.ts

@@ -1,12 +1,12 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
+    DeletionResponse,
     MutationCreatePromotionArgs,
     MutationDeletePromotionArgs,
-    DeletionResponse,
+    MutationUpdatePromotionArgs,
     Permission,
     QueryPromotionArgs,
     QueryPromotionsArgs,
-    MutationUpdatePromotionArgs,
 } from '@vendure/common/lib/generated-types';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
@@ -41,9 +41,14 @@ export class PromotionResolver {
 
     @Query()
     @Allow(Permission.ReadSettings)
-    adjustmentOperations(@Ctx() ctx: RequestContext) {
-        // TODO: split this into 2 queries, one for PromotionConditions and one for PromotionActions
-        return this.promotionService.getAdjustmentOperations();
+    promotionConditions(@Ctx() ctx: RequestContext) {
+        return this.promotionService.getPromotionConditions(ctx);
+    }
+
+    @Query()
+    @Allow(Permission.ReadSettings)
+    promotionActions(@Ctx() ctx: RequestContext) {
+        return this.promotionService.getPromotionActions(ctx);
     }
 
     @Mutation()
@@ -83,5 +88,5 @@ export class PromotionResolver {
             this.idCodecService.encodeConfigurableOperation(collection.actions);
         }
         return collection;
-    }
+    };
 }

+ 5 - 5
packages/core/src/api/resolvers/admin/shipping-method.resolver.ts

@@ -1,6 +1,6 @@
 import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
-    ConfigurableOperation,
+    ConfigurableOperationDefinition,
     MutationCreateShippingMethodArgs,
     MutationUpdateShippingMethodArgs,
     Permission,
@@ -38,14 +38,14 @@ export class ShippingMethodResolver {
 
     @Query()
     @Allow(Permission.ReadSettings)
-    shippingEligibilityCheckers(@Args() args: QueryShippingMethodArgs): ConfigurableOperation[] {
-        return this.shippingMethodService.getShippingEligibilityCheckers();
+    shippingEligibilityCheckers(@Ctx() ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.shippingMethodService.getShippingEligibilityCheckers(ctx);
     }
 
     @Query()
     @Allow(Permission.ReadSettings)
-    shippingCalculators(@Args() args: QueryShippingMethodArgs): ConfigurableOperation[] {
-        return this.shippingMethodService.getShippingCalculators();
+    shippingCalculators(@Ctx() ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.shippingMethodService.getShippingCalculators(ctx);
     }
 
     @Mutation()

+ 1 - 1
packages/core/src/api/schema/admin-api/collection.api.graphql

@@ -1,7 +1,7 @@
 type Query {
     collections(options: CollectionListOptions): CollectionList!
     collection(id: ID!): Collection
-    collectionFilters: [ConfigurableOperation!]!
+    collectionFilters: [ConfigurableOperationDefinition!]!
 }
 
 type Mutation {

+ 2 - 1
packages/core/src/api/schema/admin-api/promotion.api.graphql

@@ -1,7 +1,8 @@
 type Query {
     promotion(id: ID!): Promotion
     promotions(options: PromotionListOptions): PromotionList!
-    adjustmentOperations: AdjustmentOperations!
+    promotionConditions: [ConfigurableOperationDefinition!]!
+    promotionActions: [ConfigurableOperationDefinition!]!
 }
 
 type Mutation {

+ 2 - 2
packages/core/src/api/schema/admin-api/shipping-method.api.graphql

@@ -1,8 +1,8 @@
 type Query {
     shippingMethods(options: ShippingMethodListOptions): ShippingMethodList!
     shippingMethod(id: ID!): ShippingMethod
-    shippingEligibilityCheckers: [ConfigurableOperation!]!
-    shippingCalculators: [ConfigurableOperation!]!
+    shippingEligibilityCheckers: [ConfigurableOperationDefinition!]!
+    shippingCalculators: [ConfigurableOperationDefinition!]!
     testShippingMethod(input: TestShippingMethodInput!): TestShippingMethodResult!
 }
 

+ 16 - 23
packages/core/src/api/schema/common/common-types.graphql

@@ -20,35 +20,28 @@ type Adjustment {
     amount: Int!
 }
 
-"""
-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.
-
-"""
-enum ConfigArgType {
-    PERCENTAGE
-    MONEY
-    INT
-    STRING
-    DATETIME
-    BOOLEAN
-    FACET_VALUE_IDS
-    STRING_OPERATOR
+type ConfigArg {
+    name: String!
+    type: String!
+    value: String!
 }
 
-type ConfigArg {
+type ConfigArgDefinition {
     name: String!
-    type: ConfigArgType!
-    value: String
+    type: String!
+    label: String
+    description: String
+    config: JSON
 }
 
 type ConfigurableOperation {
     code: String!
     args: [ConfigArg!]!
+}
+
+type ConfigurableOperationDefinition {
+    code: String!
+    args: [ConfigArgDefinition!]!
     description: String!
 }
 
@@ -66,8 +59,8 @@ type DeletionResponse {
 
 input ConfigArgInput {
     name: String!
-    type: ConfigArgType!
-    value: String
+    type: String!
+    value: String!
 }
 
 input ConfigurableOperationInput {

+ 0 - 5
packages/core/src/api/schema/type/promotion.type.graphql

@@ -8,11 +8,6 @@ type Promotion implements Node {
     actions: [ConfigurableOperation!]!
 }
 
-type AdjustmentOperations {
-    conditions: [ConfigurableOperation!]!
-    actions: [ConfigurableOperation!]!
-}
-
 type PromotionList implements PaginatedList {
     items: [Promotion!]!
     totalItems: Int!

+ 98 - 24
packages/core/src/common/configurable-operation.ts

@@ -1,13 +1,49 @@
 // prettier-ignore
-import { ConfigArg, ConfigArgType, ConfigurableOperation } from '@vendure/common/lib/generated-types';
+import {
+    ConfigArg,
+    ConfigArgDefinition,
+    ConfigurableOperationDefinition,
+    LanguageCode,
+    LocalizedString,
+    Maybe,
+    StringFieldOption,
+} from '@vendure/common/lib/generated-types';
+import { ConfigArgType } from '@vendure/common/lib/shared-types';
+import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 
+import { RequestContext } from '../api/common/request-context';
+
+import { DEFAULT_LANGUAGE_CODE } from './constants';
 import { InternalServerError } from './error/errors';
 
-export type ConfigArgs<T extends ConfigArgType> = {
-    [name: string]: T;
+export type LocalizedStringArray = Array<Omit<LocalizedString, '__typename'>>;
+
+export interface ConfigArgCommonDef<T extends ConfigArgType> {
+    type: T;
+    label?: LocalizedStringArray;
+    description?: LocalizedStringArray;
+}
+
+export type WithArgConfig<T> = {
+    config?: T;
 };
 
-export type StringOperator = 'startsWith' | 'endsWith' | 'contains' | 'doesNotContain';
+export type StringArgConfig = WithArgConfig<{
+    options?: Maybe<StringFieldOption[]>;
+}>;
+export type IntArgConfig = WithArgConfig<{
+    inputType?: 'default' | 'percentage' | 'money';
+}>;
+
+export type ConfigArgDef<T extends ConfigArgType> = T extends 'string'
+    ? ConfigArgCommonDef<'string'> & StringArgConfig
+    : T extends 'int'
+    ? ConfigArgCommonDef<'int'> & IntArgConfig
+    : ConfigArgCommonDef<T> & WithArgConfig<never>;
+
+export type ConfigArgs<T extends ConfigArgType> = {
+    [name: string]: ConfigArgDef<T>;
+};
 
 // prettier-ignore
 /**
@@ -15,16 +51,14 @@ export type StringOperator = 'startsWith' | 'endsWith' | 'contains' | 'doesNotCo
  * in business logic.
  */
 export type ConfigArgValues<T extends ConfigArgs<any>> = {
-    [K in keyof T]: T[K] extends ConfigArgType.INT | ConfigArgType.MONEY | ConfigArgType.PERCENTAGE
+    [K in keyof T]: T[K] extends ConfigArgDef<'int' | 'float'>
         ? number
-        : T[K] extends ConfigArgType.DATETIME
+        : T[K] extends ConfigArgDef<'datetime'>
             ? Date
-            : T[K] extends ConfigArgType.BOOLEAN
+            : T[K] extends ConfigArgDef<'boolean'>
                 ? boolean
-                : T[K] extends ConfigArgType.FACET_VALUE_IDS
+                : T[K] extends ConfigArgDef<'facetValueIds'>
                     ? string[]
-                    : T[K] extends ConfigArgType.STRING_OPERATOR
-                        ? StringOperator
                         : string
 };
 
@@ -35,21 +69,64 @@ export type ConfigArgValues<T extends ConfigArgs<any>> = {
 export interface ConfigurableOperationDef {
     code: string;
     args: ConfigArgs<any>;
-    description: string;
+    description: LocalizedStringArray;
 }
 
 /**
  * Convert a ConfigurableOperationDef into a ConfigurableOperation object, typically
  * so that it can be sent via the API.
  */
-export function configurableDefToOperation(def: ConfigurableOperationDef): ConfigurableOperation {
+export function configurableDefToOperation(
+    ctx: RequestContext,
+    def: ConfigurableOperationDef,
+): ConfigurableOperationDefinition {
     return {
         code: def.code,
-        description: def.description,
-        args: Object.entries(def.args).map(([name, type]) => ({ name, type })),
+        description: localizeString(def.description, ctx.languageCode),
+        args: Object.entries(def.args).map(
+            ([name, arg]) =>
+                ({
+                    name,
+                    type: arg.type,
+                    config: localizeConfig(arg, ctx.languageCode),
+                    label: arg.label && localizeString(arg.label, ctx.languageCode),
+                    description: arg.description && localizeString(arg.description, ctx.languageCode),
+                } as Required<ConfigArgDefinition>),
+        ),
     };
 }
 
+function localizeConfig(
+    arg: StringArgConfig | IntArgConfig | WithArgConfig<undefined>,
+    languageCode: LanguageCode,
+): any {
+    const { config } = arg;
+    if (!config) {
+        return config;
+    }
+    const clone = simpleDeepClone(config);
+    const options: Maybe<StringFieldOption[]> = (clone as any).options;
+    if (options) {
+        for (const option of options) {
+            if (option.label) {
+                (option as any).label = localizeString(option.label, languageCode);
+            }
+        }
+    }
+    return clone;
+}
+
+function localizeString(stringArray: LocalizedStringArray, languageCode: LanguageCode): string {
+    let match = stringArray.find(x => x.languageCode === languageCode);
+    if (!match) {
+        match = stringArray.find(x => x.languageCode === DEFAULT_LANGUAGE_CODE);
+    }
+    if (!match) {
+        match = stringArray[0];
+    }
+    return match.value;
+}
+
 /**
  * Coverts an array of ConfigArgs into a hash object:
  *
@@ -59,7 +136,7 @@ export function configurableDefToOperation(def: ConfigurableOperationDef): Confi
  * to:
  * { foo: 'bar' }
  **/
-export function argsArrayToHash<T>(args: ConfigArg[]): ConfigArgValues<T> {
+export function argsArrayToHash<T extends ConfigArgs<any>>(args: ConfigArg[]): ConfigArgValues<T> {
     const output: ConfigArgValues<T> = {} as any;
     for (const arg of args) {
         if (arg && arg.value != null) {
@@ -69,20 +146,17 @@ export function argsArrayToHash<T>(args: ConfigArg[]): ConfigArgValues<T> {
     return output;
 }
 
-function coerceValueToType<T>(arg: ConfigArg): ConfigArgValues<T>[keyof T] {
+function coerceValueToType<T extends ConfigArgs<any>>(arg: ConfigArg): ConfigArgValues<T>[keyof T] {
     switch (arg.type as ConfigArgType) {
-        case ConfigArgType.STRING:
-        case ConfigArgType.STRING_OPERATOR:
+        case 'string':
             return arg.value as any;
-        case ConfigArgType.INT:
-        case ConfigArgType.MONEY:
-        case ConfigArgType.PERCENTAGE:
+        case 'int':
             return Number.parseInt(arg.value || '', 10) as any;
-        case ConfigArgType.DATETIME:
+        case 'datetime':
             return Date.parse(arg.value || '') as any;
-        case ConfigArgType.BOOLEAN:
+        case 'boolean':
             return !!(arg.value && (arg.value.toLowerCase() === 'true' || arg.value === '1')) as any;
-        case ConfigArgType.FACET_VALUE_IDS:
+        case 'facetValueIds':
             try {
                 return JSON.parse(arg.value as any);
             } catch (err) {

+ 6 - 4
packages/core/src/config/collection/collection-filter.ts

@@ -1,4 +1,5 @@
-import { ConfigArg, ConfigArgType } from '@vendure/common/lib/generated-types';
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset, ConfigArgType } from '@vendure/common/lib/shared-types';
 import { SelectQueryBuilder } from 'typeorm';
 
 import {
@@ -6,10 +7,11 @@ import {
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 
-export type CollectionFilterArgType = ConfigArgType.FACET_VALUE_IDS | ConfigArgType.STRING | ConfigArgType.STRING_OPERATOR | ConfigArgType.BOOLEAN;
+export type CollectionFilterArgType = ConfigArgSubset<'facetValueIds' | 'string' | 'boolean'>;
 export type CollectionFilterArgs = ConfigArgs<CollectionFilterArgType>;
 
 export type ApplyCollectionFilterFn<T extends CollectionFilterArgs> = (
@@ -20,14 +22,14 @@ export type ApplyCollectionFilterFn<T extends CollectionFilterArgs> = (
 export interface CollectionFilterConfig<T extends CollectionFilterArgs> {
     args: T;
     code: string;
-    description: string;
+    description: LocalizedStringArray;
     apply: ApplyCollectionFilterFn<T>;
 }
 
 export class CollectionFilter<T extends CollectionFilterArgs = {}> implements ConfigurableOperationDef {
     readonly code: string;
     readonly args: CollectionFilterArgs;
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     private readonly applyFn: ApplyCollectionFilterFn<T>;
 
     constructor(config: CollectionFilterConfig<T>) {

+ 23 - 9
packages/core/src/config/collection/default-collection-filters.ts

@@ -1,6 +1,8 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Brackets } from 'typeorm';
 
+import { UserInputError } from '../../common/error/errors';
+
 import { CollectionFilter } from './collection-filter';
 
 /**
@@ -8,11 +10,11 @@ import { CollectionFilter } from './collection-filter';
  */
 export const facetValueCollectionFilter = new CollectionFilter({
     args: {
-        facetValueIds: ConfigArgType.FACET_VALUE_IDS,
-        containsAny: ConfigArgType.BOOLEAN,
+        facetValueIds: { type: 'facetValueIds' },
+        containsAny: { type: 'boolean' },
     },
     code: 'facet-value-filter',
-    description: 'Filter by FacetValues',
+    description: [{ languageCode: LanguageCode.en, value: 'Filter by FacetValues' }],
     apply: (qb, args) => {
         if (args.facetValueIds.length) {
             qb.leftJoin('productVariant.product', 'product')
@@ -22,8 +24,8 @@ export const facetValueCollectionFilter = new CollectionFilter({
                     new Brackets(qb1 => {
                         const ids = args.facetValueIds;
                         return qb1
-                            .where(`productFacetValues.id IN (:...ids)`, {ids})
-                            .orWhere(`variantFacetValues.id IN (:...ids)`, {ids});
+                            .where(`productFacetValues.id IN (:...ids)`, { ids })
+                            .orWhere(`variantFacetValues.id IN (:...ids)`, { ids });
                     }),
                 )
                 .groupBy('productVariant.id')
@@ -38,11 +40,21 @@ export const facetValueCollectionFilter = new CollectionFilter({
 
 export const variantNameCollectionFilter = new CollectionFilter({
     args: {
-        operator: ConfigArgType.STRING_OPERATOR,
-        term: ConfigArgType.STRING,
+        operator: {
+            type: 'string',
+            config: {
+                options: [
+                    { value: 'startsWith' },
+                    { value: 'endsWith' },
+                    { value: 'contains' },
+                    { value: 'doesNotContain' },
+                ],
+            },
+        },
+        term: { type: 'string' },
     },
     code: 'variant-name-filter',
-    description: 'Filter by ProductVariant name',
+    description: [{ languageCode: LanguageCode.en, value: 'Filter by ProductVariant name' }],
     apply: (qb, args) => {
         qb.leftJoin('productVariant.translations', 'translation');
         switch (args.operator) {
@@ -54,6 +66,8 @@ export const variantNameCollectionFilter = new CollectionFilter({
                 return qb.andWhere('translation.name LIKE :term', { term: `${args.term}%` });
             case 'endsWith':
                 return qb.andWhere('translation.name LIKE :term', { term: `%${args.term}` });
+            default:
+                throw new UserInputError(`${args.operator} is not a valid operator`);
         }
     },
 });

+ 4 - 4
packages/core/src/config/payment-method/example-payment-method-config.ts

@@ -1,4 +1,4 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 
 import { CreatePaymentResult, PaymentMethodHandler } from './payment-method-handler';
 
@@ -26,10 +26,10 @@ const gripeSDK = {
  */
 export const examplePaymentHandler = new PaymentMethodHandler({
     code: 'example-payment-provider',
-    description: 'Example Payment Provider',
+    description: [{ languageCode: LanguageCode.en, value: 'Example Payment Provider' }],
     args: {
-        automaticCapture: ConfigArgType.BOOLEAN,
-        apiKey: ConfigArgType.STRING,
+        automaticCapture: { type: 'boolean' },
+        apiKey: { type: 'string' },
     },
     createPayment: async (order, args, metadata): Promise<CreatePaymentResult> => {
         try {

+ 6 - 4
packages/core/src/config/payment-method/payment-method-handler.ts

@@ -1,10 +1,12 @@
-import { ConfigArg, ConfigArgType, RefundOrderInput } from '@vendure/common/lib/generated-types';
+import { ConfigArg, RefundOrderInput } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 
 import {
     argsArrayToHash,
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { StateMachineConfig } from '../../common/finite-state-machine';
 import { Order } from '../../entity/order/order.entity';
@@ -15,7 +17,7 @@ import {
 } from '../../service/helpers/payment-state-machine/payment-state';
 import { RefundState } from '../../service/helpers/refund-state-machine/refund-state';
 
-export type PaymentMethodArgType = ConfigArgType.INT | ConfigArgType.STRING | ConfigArgType.BOOLEAN;
+export type PaymentMethodArgType = ConfigArgSubset<'int' | 'string' | 'boolean'>;
 export type PaymentMethodArgs = ConfigArgs<PaymentMethodArgType>;
 export type OnTransitionStartReturnType = ReturnType<Required<StateMachineConfig<any>>['onTransitionStart']>;
 
@@ -135,7 +137,7 @@ export interface PaymentMethodConfigOptions<T extends PaymentMethodArgs = Paymen
      * @description
      * A human-readable description for the payment method.
      */
-    description: string;
+    description: LocalizedStringArray;
     /**
      * @description
      * This function provides the logic for creating a payment. For example,
@@ -229,7 +231,7 @@ export class PaymentMethodHandler<T extends PaymentMethodArgs = PaymentMethodArg
     /** @internal */
     readonly code: string;
     /** @internal */
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     /** @internal */
     readonly args: T;
     private readonly createPaymentFn: CreatePaymentFn<T>;

+ 34 - 8
packages/core/src/config/promotion/default-promotion-actions.ts

@@ -1,23 +1,37 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 
 import { PromotionItemAction, PromotionOrderAction } from './promotion-action';
 
 export const orderPercentageDiscount = new PromotionOrderAction({
     code: 'order_percentage_discount',
-    args: { discount: ConfigArgType.PERCENTAGE },
+    args: {
+        discount: {
+            type: 'int',
+            config: {
+                inputType: 'percentage',
+            },
+        },
+    },
     execute(order, args) {
         return -order.subTotal * (args.discount / 100);
     },
-    description: 'Discount order by { discount }%',
+    description: [{ languageCode: LanguageCode.en, value: 'Discount order by { discount }%' }],
 });
 
 export const itemPercentageDiscount = new PromotionItemAction({
     code: 'item_percentage_discount',
-    args: { discount: ConfigArgType.PERCENTAGE },
+    args: {
+        discount: {
+            type: 'int',
+            config: {
+                inputType: 'percentage',
+            },
+        },
+    },
     execute(orderItem, orderLine, args) {
         return -orderLine.unitPrice * (args.discount / 100);
     },
-    description: 'Discount every item by { discount }%',
+    description: [{ languageCode: LanguageCode.en, value: 'Discount every item by { discount }%' }],
 });
 
 export const buy1Get1Free = new PromotionItemAction({
@@ -32,19 +46,31 @@ export const buy1Get1Free = new PromotionItemAction({
         }
         return 0;
     },
-    description: 'Buy 1 get 1 free',
+    description: [{ languageCode: LanguageCode.en, value: 'Buy 1 get 1 free' }],
 });
 
 export const discountOnItemWithFacets = new PromotionItemAction({
     code: 'facet_based_discount',
-    args: { discount: ConfigArgType.PERCENTAGE, facets: ConfigArgType.FACET_VALUE_IDS },
+    args: {
+        discount: {
+            type: 'int',
+            config: {
+                inputType: 'percentage',
+            },
+        },
+        facets: {
+            type: 'facetValueIds',
+        },
+    },
     async execute(orderItem, orderLine, args, { hasFacetValues }) {
         if (await hasFacetValues(orderLine, args.facets)) {
             return -orderLine.unitPrice * (args.discount / 100);
         }
         return 0;
     },
-    description: 'Discount products with these facets by { discount }%',
+    description: [
+        { languageCode: LanguageCode.en, value: 'Discount products with these facets by { discount }%' },
+    ],
 });
 
 export const defaultPromotionActions = [

+ 24 - 13
packages/core/src/config/promotion/default-promotion-conditions.ts

@@ -1,15 +1,15 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 
 import { Order } from '../../entity/order/order.entity';
 
 import { PromotionCondition } from './promotion-condition';
 
 export const minimumOrderAmount = new PromotionCondition({
-    description: 'If order total is greater than { amount }',
+    description: [{ languageCode: LanguageCode.en, value: 'If order total is greater than { amount }' }],
     code: 'minimum_order_amount',
     args: {
-        amount: ConfigArgType.MONEY,
-        taxInclusive: ConfigArgType.BOOLEAN,
+        amount: { type: 'int', config: { inputType: 'money' } },
+        taxInclusive: { type: 'boolean' },
     },
     check(order, args) {
         if (args.taxInclusive) {
@@ -23,8 +23,11 @@ export const minimumOrderAmount = new PromotionCondition({
 
 export const dateRange = new PromotionCondition({
     code: 'date_range',
-    description: 'If Order placed between { start } and { end }',
-    args: { start: ConfigArgType.DATETIME, end: ConfigArgType.DATETIME },
+    description: [{ languageCode: LanguageCode.en, value: 'If Order placed between { start } and { end }' }],
+    args: {
+        start: { type: 'datetime' },
+        end: { type: 'datetime' },
+    },
     check(order: Order, args) {
         const now = new Date();
         return args.start < now && now < args.end;
@@ -33,19 +36,27 @@ export const dateRange = new PromotionCondition({
 
 export const atLeastNOfProduct = new PromotionCondition({
     code: 'at_least_n_of_product',
-    description: 'Buy at least { minimum } of any product',
-    args: { minimum: ConfigArgType.INT },
+    description: [{ languageCode: LanguageCode.en, value: 'Buy at least { minimum } of any product' }],
+    args: { minimum: { type: 'int' } },
     check(order: Order, args) {
-        return order.lines.reduce((result, item) => {
-            return result || item.quantity >= args.minimum;
-        }, false as boolean);
+        return order.lines.reduce(
+            (result, item) => {
+                return result || item.quantity >= args.minimum;
+            },
+            false as boolean,
+        );
     },
 });
 
 export const atLeastNWithFacets = new PromotionCondition({
     code: 'at_least_n_with_facets',
-    description: 'Buy at least { minimum } products with the given facets',
-    args: { minimum: ConfigArgType.INT, facets: ConfigArgType.FACET_VALUE_IDS },
+    description: [
+        { languageCode: LanguageCode.en, value: 'Buy at least { minimum } products with the given facets' },
+    ],
+    args: {
+        minimum: { type: 'int' },
+        facets: { type: 'facetValueIds' },
+    },
     async check(order: Order, args, { hasFacetValues }) {
         let matches = 0;
         for (const line of order.lines) {

+ 6 - 8
packages/core/src/config/promotion/promotion-action.ts

@@ -1,10 +1,12 @@
-import { ConfigArg, ConfigArgType } from '@vendure/common/lib/generated-types';
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 
 import {
     argsArrayToHash,
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { OrderItem } from '../../entity/order-item/order-item.entity';
 import { OrderLine } from '../../entity/order-line/order-line.entity';
@@ -12,11 +14,7 @@ import { Order } from '../../entity/order/order.entity';
 
 import { PromotionUtils } from './promotion-condition';
 
-export type PromotionActionArgType =
-    | ConfigArgType.PERCENTAGE
-    | ConfigArgType.MONEY
-    | ConfigArgType.INT
-    | ConfigArgType.FACET_VALUE_IDS;
+export type PromotionActionArgType = ConfigArgSubset<'int' | 'facetValueIds'>;
 export type PromotionActionArgs = ConfigArgs<PromotionActionArgType>;
 
 export type ExecutePromotionItemActionFn<T extends PromotionActionArgs> = (
@@ -34,7 +32,7 @@ export type ExecutePromotionOrderActionFn<T extends PromotionActionArgs> = (
 export interface PromotionActionConfig<T extends PromotionActionArgs> {
     args: T;
     code: string;
-    description: string;
+    description: LocalizedStringArray;
     priorityValue?: number;
 }
 export interface PromotionItemActionConfig<T extends PromotionActionArgs> extends PromotionActionConfig<T> {
@@ -54,7 +52,7 @@ export abstract class PromotionAction<T extends PromotionActionArgs = {}>
     implements ConfigurableOperationDef {
     readonly code: string;
     readonly args: PromotionActionArgs;
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     readonly priorityValue: number;
 
     protected constructor(config: PromotionActionConfig<T>) {

+ 9 - 12
packages/core/src/config/promotion/promotion-condition.ts

@@ -1,22 +1,19 @@
-import { ConfigArg, ConfigArgType } from '@vendure/common/lib/generated-types';
-import { ID } from '@vendure/common/lib/shared-types';
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset, ID } from '@vendure/common/lib/shared-types';
 
 import {
     argsArrayToHash,
     ConfigArgs,
     ConfigArgValues,
     ConfigurableOperationDef,
+    LocalizedStringArray,
 } from '../../common/configurable-operation';
 import { OrderLine } from '../../entity';
 import { Order } from '../../entity/order/order.entity';
 
-export type PromotionConditionArgType =
-    | ConfigArgType.INT
-    | ConfigArgType.MONEY
-    | ConfigArgType.STRING
-    | ConfigArgType.DATETIME
-    | ConfigArgType.BOOLEAN
-    | ConfigArgType.FACET_VALUE_IDS;
+export type PromotionConditionArgType = ConfigArgSubset<
+    'int' | 'string' | 'datetime' | 'boolean' | 'facetValueIds'
+>;
 export type PromotionConditionArgs = ConfigArgs<PromotionConditionArgType>;
 
 /**
@@ -58,7 +55,7 @@ export type CheckPromotionConditionFn<T extends PromotionConditionArgs> = (
  */
 export class PromotionCondition<T extends PromotionConditionArgs = {}> implements ConfigurableOperationDef {
     readonly code: string;
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     readonly args: PromotionConditionArgs;
     readonly priorityValue: number;
     private readonly checkFn: CheckPromotionConditionFn<T>;
@@ -67,7 +64,7 @@ export class PromotionCondition<T extends PromotionConditionArgs = {}> implement
         args: T;
         check: CheckPromotionConditionFn<T>;
         code: string;
-        description: string;
+        description: LocalizedStringArray;
         priorityValue?: number;
     }) {
         this.code = config.code;
@@ -78,6 +75,6 @@ export class PromotionCondition<T extends PromotionConditionArgs = {}> implement
     }
 
     async check(order: Order, args: ConfigArg[], utils: PromotionUtils): Promise<boolean> {
-        return await this.checkFn(order, argsArrayToHash<T>(args), utils);
+        return this.checkFn(order, argsArrayToHash<T>(args), utils);
     }
 }

+ 4 - 4
packages/core/src/config/shipping-method/default-shipping-calculator.ts

@@ -1,13 +1,13 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 
 import { ShippingCalculator } from './shipping-calculator';
 
 export const defaultShippingCalculator = new ShippingCalculator({
     code: 'default-shipping-calculator',
-    description: 'Default Flat-Rate Shipping Calculator',
+    description: [{ languageCode: LanguageCode.en, value: 'Default Flat-Rate Shipping Calculator' }],
     args: {
-        rate: ConfigArgType.MONEY,
-        taxRate: ConfigArgType.PERCENTAGE,
+        rate: { type: 'int', config: { inputType: 'money' } },
+        taxRate: { type: 'int', config: { inputType: 'percentage' } },
     },
     calculate: (order, args) => {
         return { price: args.rate, priceWithTax: args.rate * ((100 + args.taxRate) / 100) };

+ 3 - 3
packages/core/src/config/shipping-method/default-shipping-eligibility-checker.ts

@@ -1,12 +1,12 @@
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 
 import { ShippingEligibilityChecker } from './shipping-eligibility-checker';
 
 export const defaultShippingEligibilityChecker = new ShippingEligibilityChecker({
     code: 'default-shipping-eligibility-checker',
-    description: 'Default Shipping Eligibility Checker',
+    description: [{ languageCode: LanguageCode.en, value: 'Default Shipping Eligibility Checker' }],
     args: {
-        orderMinimum: ConfigArgType.MONEY,
+        orderMinimum: { type: 'int', config: { inputType: 'money' } },
     },
     check: (order, args) => {
         return order.total >= args.orderMinimum;

+ 17 - 11
packages/core/src/config/shipping-method/shipping-calculator.ts

@@ -1,15 +1,16 @@
-import { ConfigArg, ConfigArgType } from '@vendure/common/lib/generated-types';
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 
-import { ConfigArgs, ConfigurableOperationDef } from '../../common/configurable-operation';
-import { argsArrayToHash, ConfigArgValues } from '../../common/configurable-operation';
+import {
+    argsArrayToHash,
+    ConfigArgs,
+    ConfigArgValues,
+    ConfigurableOperationDef,
+    LocalizedStringArray,
+} from '../../common/configurable-operation';
 import { Order } from '../../entity/order/order.entity';
 
-export type ShippingCalculatorArgType =
-    | ConfigArgType.INT
-    | ConfigArgType.MONEY
-    | ConfigArgType.STRING
-    | ConfigArgType.PERCENTAGE
-    | ConfigArgType.BOOLEAN;
+export type ShippingCalculatorArgType = ConfigArgSubset<'int' | 'string' | 'boolean'>;
 export type ShippingCalculatorArgs = ConfigArgs<ShippingCalculatorArgType>;
 
 export type ShippingPrice = {
@@ -53,12 +54,17 @@ export class ShippingCalculator<T extends ShippingCalculatorArgs = {}> implement
     /** @internal */
     readonly code: string;
     /** @internal */
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     /** @internal */
     readonly args: ShippingCalculatorArgs;
     private readonly calculateFn: CalculateShippingFn<T>;
 
-    constructor(config: { args: T; calculate: CalculateShippingFn<T>; code: string; description: string }) {
+    constructor(config: {
+        args: T;
+        calculate: CalculateShippingFn<T>;
+        code: string;
+        description: LocalizedStringArray;
+    }) {
         this.code = config.code;
         this.description = config.description;
         this.args = config.args;

+ 10 - 9
packages/core/src/config/shipping-method/shipping-eligibility-checker.ts

@@ -1,14 +1,15 @@
-import { ConfigArg, ConfigArgType } from '@vendure/common/lib/generated-types';
+import { ConfigArg } from '@vendure/common/lib/generated-types';
+import { ConfigArgSubset } from '@vendure/common/lib/shared-types';
 
-import { ConfigArgs, ConfigurableOperationDef } from '../../common/configurable-operation';
+import {
+    ConfigArgs,
+    ConfigurableOperationDef,
+    LocalizedStringArray,
+} from '../../common/configurable-operation';
 import { argsArrayToHash, ConfigArgValues } from '../../common/configurable-operation';
 import { Order } from '../../entity/order/order.entity';
 
-export type ShippingEligibilityCheckerArgType =
-    | ConfigArgType.INT
-    | ConfigArgType.MONEY
-    | ConfigArgType.STRING
-    | ConfigArgType.BOOLEAN;
+export type ShippingEligibilityCheckerArgType = ConfigArgSubset<'int' | 'string' | 'boolean'>;
 export type ShippingEligibilityCheckerArgs = ConfigArgs<ShippingEligibilityCheckerArgType>;
 
 /**
@@ -49,7 +50,7 @@ export class ShippingEligibilityChecker<T extends ShippingEligibilityCheckerArgs
     /** @internal */
     readonly code: string;
     /** @internal */
-    readonly description: string;
+    readonly description: LocalizedStringArray;
     /** @internal */
     readonly args: ShippingEligibilityCheckerArgs;
     private readonly checkFn: CheckShippingEligibilityCheckerFn<T>;
@@ -58,7 +59,7 @@ export class ShippingEligibilityChecker<T extends ShippingEligibilityCheckerArgs
         args: T;
         check: CheckShippingEligibilityCheckerFn<T>;
         code: string;
-        description: string;
+        description: LocalizedStringArray;
     }) {
         this.code = config.code;
         this.description = config.description;

+ 10 - 7
packages/core/src/data-import/providers/populator/populator.ts

@@ -1,5 +1,5 @@
 import { Injectable } from '@nestjs/common';
-import { ConfigArgType, LanguageCode } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
 import { ID } from '@vendure/common/lib/shared-types';
 import { notNullOrUndefined } from '@vendure/common/lib/shared-utils';
@@ -91,7 +91,10 @@ export class Populator {
 
             let featuredAssetId: string | null = null;
             if (collectionDef.featuredAssetFilename) {
-                const asset = await this.assetService.findByFileName(collectionDef.featuredAssetFilename, false);
+                const asset = await this.assetService.findByFileName(
+                    collectionDef.featuredAssetFilename,
+                    false,
+                );
                 if (asset) {
                     featuredAssetId = asset.id as string;
                 }
@@ -114,13 +117,13 @@ export class Populator {
                         arguments: [
                             {
                                 name: 'facetValueIds',
-                                type: ConfigArgType.FACET_VALUE_IDS,
+                                type: 'facetValueIds',
                                 value: JSON.stringify(facetValueIds),
                             },
                             {
                                 name: 'containsAny',
                                 value: `false`,
-                                type: ConfigArgType.BOOLEAN,
+                                type: 'boolean',
                             },
                         ],
                     },
@@ -219,13 +222,13 @@ export class Populator {
             await this.shippingMethodService.create({
                 checker: {
                     code: defaultShippingEligibilityChecker.code,
-                    arguments: [{ name: 'orderMinimum', value: '0', type: ConfigArgType.MONEY }],
+                    arguments: [{ name: 'orderMinimum', value: '0', type: 'int' }],
                 },
                 calculator: {
                     code: defaultShippingCalculator.code,
                     arguments: [
-                        { name: 'rate', value: method.price.toString(), type: ConfigArgType.MONEY },
-                        { name: 'taxRate', value: '0', type: ConfigArgType.PERCENTAGE },
+                        { name: 'rate', value: method.price.toString(), type: 'int' },
+                        { name: 'taxRate', value: '0', type: 'int' },
                     ],
                 },
                 description: method.name,

+ 5 - 1
packages/core/src/entity/shipping-method/shipping-method.entity.ts

@@ -48,7 +48,11 @@ export class ShippingMethod extends VendureEntity implements ChannelAware {
     async apply(order: Order): Promise<ShippingPrice | undefined> {
         const calculator = this.allCalculators[this.calculator.code];
         if (calculator) {
-            return calculator.calculate(order, this.calculator.args);
+            const { price, priceWithTax } = await calculator.calculate(order, this.calculator.args);
+            return {
+                price: Math.round(price),
+                priceWithTax: Math.round(priceWithTax),
+            };
         }
     }
 

+ 20 - 15
packages/core/src/service/helpers/order-calculator/order-calculator.spec.ts

@@ -1,5 +1,5 @@
 import { Test } from '@nestjs/testing';
-import { ConfigArgType } from '@vendure/common/lib/generated-types';
+import { LanguageCode } from '@vendure/common/lib/generated-types';
 import { Omit } from '@vendure/common/lib/omit';
 import { Connection } from 'typeorm';
 
@@ -131,16 +131,16 @@ describe('OrderCalculator', () => {
         const alwaysTrueCondition = new PromotionCondition({
             args: {},
             code: 'always_true_condition',
-            description: '',
+            description: [{ languageCode: LanguageCode.en, value: '' }],
             check() {
                 return true;
             },
         });
 
         const orderTotalCondition = new PromotionCondition({
-            args: { minimum: ConfigArgType.MONEY },
+            args: { minimum: { type: 'int' } },
             code: 'order_total_condition',
-            description: '',
+            description: [{ languageCode: LanguageCode.en, value: '' }],
             check(order, args) {
                 return args.minimum <= order.total;
             },
@@ -148,7 +148,7 @@ describe('OrderCalculator', () => {
 
         const fixedPriceItemAction = new PromotionItemAction({
             code: 'fixed_price_item_action',
-            description: '',
+            description: [{ languageCode: LanguageCode.en, value: '' }],
             args: {},
             execute(item) {
                 return -item.unitPrice + 42;
@@ -157,7 +157,7 @@ describe('OrderCalculator', () => {
 
         const fixedPriceOrderAction = new PromotionOrderAction({
             code: 'fixed_price_item_action',
-            description: '',
+            description: [{ languageCode: LanguageCode.en, value: '' }],
             args: {},
             execute(order) {
                 return -order.total + 42;
@@ -190,7 +190,7 @@ describe('OrderCalculator', () => {
                 conditions: [
                     {
                         code: orderTotalCondition.code,
-                        args: [{ name: 'minimum', type: ConfigArgType.MONEY, value: '100' }],
+                        args: [{ name: 'minimum', type: 'int', value: '100' }],
                     },
                 ],
                 promotionConditions: [orderTotalCondition],
@@ -222,9 +222,14 @@ describe('OrderCalculator', () => {
 
         it('interaction between promotions', async () => {
             const orderQuantityCondition = new PromotionCondition({
-                args: { minimum: ConfigArgType.INT },
+                args: { minimum: { type: 'int' } },
                 code: 'order_quantity_condition',
-                description: 'Passes if any order line has at least the minimum quantity',
+                description: [
+                    {
+                        languageCode: LanguageCode.en,
+                        value: 'Passes if any order line has at least the minimum quantity',
+                    },
+                ],
                 check(_order, args) {
                     for (const line of _order.lines) {
                         if (args.minimum <= line.quantity) {
@@ -237,11 +242,11 @@ describe('OrderCalculator', () => {
 
             const orderPercentageDiscount = new PromotionOrderAction({
                 code: 'order_percentage_discount',
-                args: { discount: ConfigArgType.PERCENTAGE },
+                args: { discount: { type: 'int' } },
                 execute(_order, args) {
                     return -_order.subTotal * (args.discount / 100);
                 },
-                description: 'Discount order by { discount }%',
+                description: [{ languageCode: LanguageCode.en, value: 'Discount order by { discount }%' }],
             });
 
             const promotion1 = new Promotion({
@@ -250,14 +255,14 @@ describe('OrderCalculator', () => {
                 conditions: [
                     {
                         code: orderQuantityCondition.code,
-                        args: [{ name: 'minimum', type: ConfigArgType.INT, value: '3' }],
+                        args: [{ name: 'minimum', type: 'int', value: '3' }],
                     },
                 ],
                 promotionConditions: [orderQuantityCondition],
                 actions: [
                     {
                         code: orderPercentageDiscount.code,
-                        args: [{ name: 'discount', type: ConfigArgType.PERCENTAGE, value: '50' }],
+                        args: [{ name: 'discount', type: 'int', value: '50' }],
                     },
                 ],
                 promotionActions: [orderPercentageDiscount],
@@ -269,14 +274,14 @@ describe('OrderCalculator', () => {
                 conditions: [
                     {
                         code: orderTotalCondition.code,
-                        args: [{ name: 'minimum', type: ConfigArgType.MONEY, value: '100' }],
+                        args: [{ name: 'minimum', type: 'int', value: '100' }],
                     },
                 ],
                 promotionConditions: [orderTotalCondition],
                 actions: [
                     {
                         code: orderPercentageDiscount.code,
-                        args: [{ name: 'discount', type: ConfigArgType.PERCENTAGE, value: '10' }],
+                        args: [{ name: 'discount', type: 'int', value: '10' }],
                     },
                 ],
                 promotionActions: [orderPercentageDiscount],

+ 0 - 1
packages/core/src/service/helpers/shipping-configuration/shipping-configuration.ts

@@ -41,7 +41,6 @@ export class ShippingConfiguration {
     ): ConfigurableOperation {
         const output: ConfigurableOperation = {
             code: input.code,
-            description: checkerOrCalculator.description,
             args: input.arguments,
         };
         return output;

+ 21 - 8
packages/core/src/service/services/collection.service.ts

@@ -2,11 +2,12 @@ import { OnModuleInit } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import {
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     CreateCollectionInput,
     DeletionResponse,
     DeletionResult,
     MoveCollectionInput,
-    UpdateCollectionInput
+    UpdateCollectionInput,
 } from '@vendure/common/lib/generated-types';
 import { pick } from '@vendure/common/lib/pick';
 import { ROOT_COLLECTION_NAME } from '@vendure/common/lib/shared-constants';
@@ -22,7 +23,10 @@ import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
 import { CollectionFilter } from '../../config/collection/collection-filter';
-import { facetValueCollectionFilter, variantNameCollectionFilter } from '../../config/collection/default-collection-filters';
+import {
+    facetValueCollectionFilter,
+    variantNameCollectionFilter,
+} from '../../config/collection/default-collection-filters';
 import { CollectionTranslation } from '../../entity/collection/collection-translation.entity';
 import { Collection } from '../../entity/collection/collection.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
@@ -40,7 +44,10 @@ import { FacetValueService } from './facet-value.service';
 
 export class CollectionService implements OnModuleInit {
     private rootCategories: { [channelCode: string]: Collection } = {};
-    private availableFilters: Array<CollectionFilter<any>> = [facetValueCollectionFilter, variantNameCollectionFilter];
+    private availableFilters: Array<CollectionFilter<any>> = [
+        facetValueCollectionFilter,
+        variantNameCollectionFilter,
+    ];
 
     constructor(
         @InjectConnection() private connection: Connection,
@@ -104,8 +111,8 @@ export class CollectionService implements OnModuleInit {
         return translateDeep(collection, ctx.languageCode, ['parent']);
     }
 
-    getAvailableFilters(): ConfigurableOperation[] {
-        return this.availableFilters.map(configurableDefToOperation);
+    getAvailableFilters(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.availableFilters.map(x => configurableDefToOperation(ctx, x));
     }
 
     async getParent(ctx: RequestContext, collectionId: ID): Promise<Collection | undefined> {
@@ -170,7 +177,11 @@ export class CollectionService implements OnModuleInit {
      * Returns the descendants of a Collection as a flat array. The depth of the traversal can be limited
      * with the maxDepth argument. So to get only the immediate children, set maxDepth = 1.
      */
-    async getDescendants(ctx: RequestContext, rootId: ID, maxDepth: number = Number.MAX_SAFE_INTEGER): Promise<Array<Translated<Collection>>> {
+    async getDescendants(
+        ctx: RequestContext,
+        rootId: ID,
+        maxDepth: number = Number.MAX_SAFE_INTEGER,
+    ): Promise<Array<Translated<Collection>>> {
         const getChildren = async (id: ID, _descendants: Collection[] = [], depth = 1) => {
             const children = await this.connection
                 .getRepository(Collection)
@@ -336,7 +347,7 @@ export class CollectionService implements OnModuleInit {
                     args: filter.arguments.map((inputArg, i) => {
                         return {
                             name: inputArg.name,
-                            type: match.args[inputArg.name],
+                            type: match.args[inputArg.name].type,
                             value: inputArg.value,
                         };
                     }),
@@ -363,7 +374,9 @@ export class CollectionService implements OnModuleInit {
             ...(collection.filters || []),
         ]);
         const postIds = collection.productVariants.map(v => v.id);
-        await this.connection.getRepository(Collection).save(collection, { chunk: Math.ceil(collection.productVariants.length / 500) });
+        await this.connection
+            .getRepository(Collection)
+            .save(collection, { chunk: Math.ceil(collection.productVariants.length / 500) });
 
         const preIdsSet = new Set(preIds);
         const postIdsSet = new Set(postIds);

+ 7 - 12
packages/core/src/service/services/payment-method.service.ts

@@ -1,11 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
-import {
-    ConfigArg,
-    ConfigArgType,
-    RefundOrderInput,
-    UpdatePaymentMethodInput,
-} from '@vendure/common/lib/generated-types';
+import { ConfigArg, RefundOrderInput, UpdatePaymentMethodInput } from '@vendure/common/lib/generated-types';
 import { omit } from '@vendure/common/lib/omit';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
@@ -201,12 +196,12 @@ export class PaymentMethodService {
         existingConfigArgs: ConfigArg[],
     ): ConfigArg[] {
         let configArgs: ConfigArg[] = [];
-        for (const [name, type] of Object.entries(handler.args)) {
+        for (const [name, def] of Object.entries(handler.args)) {
             if (!existingConfigArgs.find(ca => ca.name === name)) {
                 configArgs.push({
                     name,
-                    type,
-                    value: this.getDefaultValue(type),
+                    type: def.type,
+                    value: this.getDefaultValue(def.type),
                 });
             }
         }
@@ -216,11 +211,11 @@ export class PaymentMethodService {
 
     private getDefaultValue(type: PaymentMethodArgType): string {
         switch (type) {
-            case ConfigArgType.STRING:
+            case 'string':
                 return '';
-            case ConfigArgType.BOOLEAN:
+            case 'boolean':
                 return 'false';
-            case ConfigArgType.INT:
+            case 'int':
                 return '0';
             default:
                 assertNever(type);

+ 7 - 12
packages/core/src/service/services/promotion.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import {
     ConfigurableOperation,
+    ConfigurableOperationDefinition,
     ConfigurableOperationInput,
     CreatePromotionInput,
     DeletionResponse,
@@ -62,17 +63,12 @@ export class PromotionService {
         return this.connection.manager.findOne(Promotion, adjustmentSourceId, { where: { deletedAt: null } });
     }
 
-    /**
-     * Returns all available AdjustmentOperations.
-     */
-    getAdjustmentOperations(): {
-        conditions: ConfigurableOperation[];
-        actions: ConfigurableOperation[];
-    } {
-        return {
-            conditions: this.availableConditions.map(configurableDefToOperation),
-            actions: this.availableActions.map(configurableDefToOperation),
-        };
+    getPromotionConditions(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.availableConditions.map(x => configurableDefToOperation(ctx, x));
+    }
+
+    getPromotionActions(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.availableActions.map(x => configurableDefToOperation(ctx, x));
     }
 
     /**
@@ -134,7 +130,6 @@ export class PromotionService {
         const match = this.getAdjustmentOperationByCode(type, input.code);
         const output: ConfigurableOperation = {
             code: input.code,
-            description: match.description,
             args: input.arguments,
         };
         return output;

+ 8 - 5
packages/core/src/service/services/shipping-method.service.ts

@@ -1,7 +1,7 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import {
-    ConfigurableOperation,
+    ConfigurableOperationDefinition,
     CreateShippingMethodInput,
     UpdateShippingMethodInput,
 } from '@vendure/common/lib/generated-types';
@@ -9,6 +9,7 @@ import { omit } from '@vendure/common/lib/omit';
 import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { Connection } from 'typeorm';
 
+import { RequestContext } from '../../api/common/request-context';
 import { configurableDefToOperation } from '../../common/configurable-operation';
 import { EntityNotFoundError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
@@ -86,12 +87,14 @@ export class ShippingMethodService {
         return assertFound(this.findOne(shippingMethod.id));
     }
 
-    getShippingEligibilityCheckers(): ConfigurableOperation[] {
-        return this.shippingConfiguration.shippingEligibilityCheckers.map(configurableDefToOperation);
+    getShippingEligibilityCheckers(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.shippingConfiguration.shippingEligibilityCheckers.map(x =>
+            configurableDefToOperation(ctx, x),
+        );
     }
 
-    getShippingCalculators(): ConfigurableOperation[] {
-        return this.shippingConfiguration.shippingCalculators.map(configurableDefToOperation);
+    getShippingCalculators(ctx: RequestContext): ConfigurableOperationDefinition[] {
+        return this.shippingConfiguration.shippingCalculators.map(x => configurableDefToOperation(ctx, x));
     }
 
     getActiveShippingMethods(channel: Channel): ShippingMethod[] {

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


File diff suppressed because it is too large
+ 0 - 0
schema-shop.json


+ 1 - 1
scripts/codegen/generate-graphql-types.ts

@@ -72,7 +72,7 @@ Promise.all([
                 [path.join(__dirname, '../../admin-ui/src/app/common/introspection-result.ts')]: {
                     schema: [ADMIN_SCHEMA_OUTPUT_FILE, path.join(__dirname, 'client-schema.ts')],
                     documents: CLIENT_QUERY_FILES,
-                    plugins: ['fragment-matcher'],
+                    plugins: [{ add: '// tslint:disable' }, 'fragment-matcher'],
                     config,
                 },
                 [path.join(__dirname, '../../packages/common/src/generated-types.ts')]: {

Some files were not shown because too many files changed in this diff