فهرست منبع

feat(admin-ui): Implement PaymentMethod checker/handler UI

Relates to #469
Michael Bromley 5 سال پیش
والد
کامیت
15fc707076

+ 124 - 28
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -52,6 +52,8 @@ export type Query = {
   order?: Maybe<Order>;
   orders: OrderList;
   paymentMethod?: Maybe<PaymentMethod>;
+  paymentMethodEligibilityCheckers: Array<ConfigurableOperationDefinition>;
+  paymentMethodHandlers: Array<ConfigurableOperationDefinition>;
   paymentMethods: PaymentMethodList;
   /** Get a Product either by id or slug. If neither id nor slug is speicified, an error will result. */
   product?: Maybe<Product>;
@@ -353,6 +355,8 @@ export type Mutation = {
   createFacet: Facet;
   /** Create one or more FacetValues */
   createFacetValues: Array<FacetValue>;
+  /** Create existing PaymentMethod */
+  createPaymentMethod: PaymentMethod;
   /** Create a new Product */
   createProduct: Product;
   /** Create a new ProductOption within a ProductOptionGroup */
@@ -626,6 +630,11 @@ export type MutationCreateFacetValuesArgs = {
 };
 
 
+export type MutationCreatePaymentMethodArgs = {
+  input: CreatePaymentMethodInput;
+};
+
+
 export type MutationCreateProductArgs = {
   input: CreateProductInput;
 };
@@ -1143,6 +1152,7 @@ export type CreateChannelInput = {
   currencyCode: CurrencyCode;
   defaultTaxZoneId: Scalars['ID'];
   defaultShippingZoneId: Scalars['ID'];
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type UpdateChannelInput = {
@@ -1154,6 +1164,7 @@ export type UpdateChannelInput = {
   currencyCode?: Maybe<CurrencyCode>;
   defaultTaxZoneId?: Maybe<Scalars['ID']>;
   defaultShippingZoneId?: Maybe<Scalars['ID']>;
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 /** Returned if attempting to set a Channel's defaultLanguageCode to a language which is not enabled in GlobalSettings */
@@ -1465,7 +1476,7 @@ export type ImportInfo = {
 /**
  * @description
  * The state of a Job in the JobQueue
- *
+ * 
  * @docsCategory common
  */
 export enum JobState {
@@ -1915,11 +1926,23 @@ export type PaymentMethodList = PaginatedList & {
   totalItems: Scalars['Int'];
 };
 
+export type CreatePaymentMethodInput = {
+  name: Scalars['String'];
+  code: Scalars['String'];
+  description?: Maybe<Scalars['String']>;
+  enabled: Scalars['Boolean'];
+  checker?: Maybe<ConfigurableOperationInput>;
+  handler: ConfigurableOperationInput;
+};
+
 export type UpdatePaymentMethodInput = {
   id: Scalars['ID'];
+  name?: Maybe<Scalars['String']>;
   code?: Maybe<Scalars['String']>;
+  description?: Maybe<Scalars['String']>;
   enabled?: Maybe<Scalars['Boolean']>;
-  configArgs?: Maybe<Array<ConfigArgInput>>;
+  checker?: Maybe<ConfigurableOperationInput>;
+  handler?: Maybe<ConfigurableOperationInput>;
 };
 
 export type PaymentMethod = Node & {
@@ -1927,10 +1950,12 @@ export type PaymentMethod = Node & {
   id: Scalars['ID'];
   createdAt: Scalars['DateTime'];
   updatedAt: Scalars['DateTime'];
+  name: Scalars['String'];
   code: Scalars['String'];
+  description: Scalars['String'];
   enabled: Scalars['Boolean'];
-  configArgs: Array<ConfigArg>;
-  definition: ConfigurableOperationDefinition;
+  checker?: Maybe<ConfigurableOperation>;
+  handler: ConfigurableOperation;
 };
 
 export type Product = Node & {
@@ -2489,6 +2514,7 @@ export type Channel = Node & {
   defaultLanguageCode: LanguageCode;
   currencyCode: CurrencyCode;
   pricesIncludeTax: Scalars['Boolean'];
+  customFields?: Maybe<Scalars['JSON']>;
 };
 
 export type CollectionBreadcrumb = {
@@ -2543,7 +2569,7 @@ export enum DeletionResult {
  * @description
  * Permissions for administrators and customers. Used to control access to
  * GraphQL resolvers via the {@link Allow} decorator.
- *
+ * 
  * @docsCategory common
  */
 export enum Permission {
@@ -2891,6 +2917,25 @@ export type Success = {
   success: Scalars['Boolean'];
 };
 
+export type ShippingMethodQuote = {
+  __typename?: 'ShippingMethodQuote';
+  id: Scalars['ID'];
+  price: Scalars['Int'];
+  priceWithTax: Scalars['Int'];
+  name: Scalars['String'];
+  description: Scalars['String'];
+  /** Any optional metadata returned by the ShippingCalculator in the ShippingCalculationResult */
+  metadata?: Maybe<Scalars['JSON']>;
+};
+
+export type PaymentMethodQuote = {
+  __typename?: 'PaymentMethodQuote';
+  id: Scalars['ID'];
+  code: Scalars['String'];
+  isEligible: Scalars['Boolean'];
+  eligibilityMessage?: Maybe<Scalars['String']>;
+};
+
 export type Country = Node & {
   __typename?: 'Country';
   id: Scalars['ID'];
@@ -2921,7 +2966,7 @@ export type CountryList = PaginatedList & {
 /**
  * @description
  * ISO 4217 currency code
- *
+ * 
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -3458,7 +3503,7 @@ export type HistoryEntryList = PaginatedList & {
  * region or script modifier (e.g. de_AT). The selection available is based
  * on the [Unicode CLDR summary list](https://unicode-org.github.io/cldr-staging/charts/37/summary/root.html)
  * and includes the major spoken languages of the world and any widely-used variants.
- *
+ * 
  * @docsCategory common
  */
 export enum LanguageCode {
@@ -3815,16 +3860,6 @@ export type OrderList = PaginatedList & {
   totalItems: Scalars['Int'];
 };
 
-export type ShippingMethodQuote = {
-  __typename?: 'ShippingMethodQuote';
-  id: Scalars['ID'];
-  price: Scalars['Int'];
-  priceWithTax: Scalars['Int'];
-  name: Scalars['String'];
-  description: Scalars['String'];
-  metadata?: Maybe<Scalars['JSON']>;
-};
-
 export type ShippingLine = {
   __typename?: 'ShippingLine';
   shippingMethod: ShippingMethod;
@@ -3847,7 +3882,7 @@ export type OrderItem = Node & {
   unitPriceWithTax: Scalars['Int'];
   /**
    * The price of a single unit including discounts, excluding tax.
-   *
+   * 
    * If Order-level discounts have been applied, this will not be the
    * actual taxable unit price (see `proratedUnitPrice`), but is generally the
    * correct price to display to customers to avoid confusion
@@ -3887,7 +3922,7 @@ export type OrderLine = Node & {
   unitPriceWithTax: Scalars['Int'];
   /**
    * The price of a single unit including discounts, excluding tax.
-   *
+   * 
    * If Order-level discounts have been applied, this will not be the
    * actual taxable unit price (see `proratedUnitPrice`), but is generally the
    * correct price to display to customers to avoid confusion
@@ -4544,7 +4579,9 @@ export type OrderSortParameter = {
 export type PaymentMethodFilterParameter = {
   createdAt?: Maybe<DateOperators>;
   updatedAt?: Maybe<DateOperators>;
+  name?: Maybe<StringOperators>;
   code?: Maybe<StringOperators>;
+  description?: Maybe<StringOperators>;
   enabled?: Maybe<BooleanOperators>;
 };
 
@@ -4552,7 +4589,9 @@ export type PaymentMethodSortParameter = {
   id?: Maybe<SortOrder>;
   createdAt?: Maybe<SortOrder>;
   updatedAt?: Maybe<SortOrder>;
+  name?: Maybe<SortOrder>;
   code?: Maybe<SortOrder>;
+  description?: Maybe<SortOrder>;
 };
 
 export type ProductFilterParameter = {
@@ -4716,6 +4755,7 @@ export type NativeAuthInput = {
 export type CustomFields = {
   __typename?: 'CustomFields';
   Address: Array<CustomFieldConfig>;
+  Channel: Array<CustomFieldConfig>;
   Collection: Array<CustomFieldConfig>;
   Customer: Array<CustomFieldConfig>;
   Facet: Array<CustomFieldConfig>;
@@ -7076,13 +7116,13 @@ export type DeleteChannelMutation = { deleteChannel: (
 
 export type PaymentMethodFragment = (
   { __typename?: 'PaymentMethod' }
-  & Pick<PaymentMethod, 'id' | 'createdAt' | 'updatedAt' | 'code' | 'enabled'>
-  & { configArgs: Array<(
-    { __typename?: 'ConfigArg' }
-    & Pick<ConfigArg, 'name' | 'value'>
-  )>, definition: (
-    { __typename?: 'ConfigurableOperationDefinition' }
-    & ConfigurableOperationDefFragment
+  & Pick<PaymentMethod, 'id' | 'createdAt' | 'updatedAt' | 'name' | 'code' | 'description' | 'enabled'>
+  & { checker?: Maybe<(
+    { __typename?: 'ConfigurableOperation' }
+    & ConfigurableOperationFragment
+  )>, handler: (
+    { __typename?: 'ConfigurableOperation' }
+    & ConfigurableOperationFragment
   ) }
 );
 
@@ -7100,6 +7140,17 @@ export type GetPaymentMethodListQuery = { paymentMethods: (
     )> }
   ) };
 
+export type GetPaymentMethodOperationsQueryVariables = Exact<{ [key: string]: never; }>;
+
+
+export type GetPaymentMethodOperationsQuery = { paymentMethodEligibilityCheckers: Array<(
+    { __typename?: 'ConfigurableOperationDefinition' }
+    & ConfigurableOperationDefFragment
+  )>, paymentMethodHandlers: Array<(
+    { __typename?: 'ConfigurableOperationDefinition' }
+    & ConfigurableOperationDefFragment
+  )> };
+
 export type GetPaymentMethodQueryVariables = Exact<{
   id: Scalars['ID'];
 }>;
@@ -7110,6 +7161,16 @@ export type GetPaymentMethodQuery = { paymentMethod?: Maybe<(
     & PaymentMethodFragment
   )> };
 
+export type CreatePaymentMethodMutationVariables = Exact<{
+  input: CreatePaymentMethodInput;
+}>;
+
+
+export type CreatePaymentMethodMutation = { createPaymentMethod: (
+    { __typename?: 'PaymentMethod' }
+    & PaymentMethodFragment
+  ) };
+
 export type UpdatePaymentMethodMutationVariables = Exact<{
   input: UpdatePaymentMethodInput;
 }>;
@@ -7366,6 +7427,27 @@ export type GetServerConfigQuery = { globalSettings: (
         ) | (
           { __typename?: 'RelationCustomFieldConfig' }
           & CustomFields_RelationCustomFieldConfig_Fragment
+        )>, Channel: Array<(
+          { __typename?: 'StringCustomFieldConfig' }
+          & CustomFields_StringCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'LocaleStringCustomFieldConfig' }
+          & CustomFields_LocaleStringCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'IntCustomFieldConfig' }
+          & CustomFields_IntCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'FloatCustomFieldConfig' }
+          & CustomFields_FloatCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'BooleanCustomFieldConfig' }
+          & CustomFields_BooleanCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'DateTimeCustomFieldConfig' }
+          & CustomFields_DateTimeCustomFieldConfig_Fragment
+        ) | (
+          { __typename?: 'RelationCustomFieldConfig' }
+          & CustomFields_RelationCustomFieldConfig_Fragment
         )>, Collection: Array<(
           { __typename?: 'StringCustomFieldConfig' }
           & CustomFields_StringCustomFieldConfig_Fragment
@@ -9199,8 +9281,8 @@ export namespace DeleteChannel {
 
 export namespace PaymentMethod {
   export type Fragment = PaymentMethodFragment;
-  export type ConfigArgs = NonNullable<(NonNullable<PaymentMethodFragment['configArgs']>)[number]>;
-  export type Definition = (NonNullable<PaymentMethodFragment['definition']>);
+  export type Checker = (NonNullable<PaymentMethodFragment['checker']>);
+  export type Handler = (NonNullable<PaymentMethodFragment['handler']>);
 }
 
 export namespace GetPaymentMethodList {
@@ -9210,12 +9292,25 @@ export namespace GetPaymentMethodList {
   export type Items = NonNullable<(NonNullable<(NonNullable<GetPaymentMethodListQuery['paymentMethods']>)['items']>)[number]>;
 }
 
+export namespace GetPaymentMethodOperations {
+  export type Variables = GetPaymentMethodOperationsQueryVariables;
+  export type Query = GetPaymentMethodOperationsQuery;
+  export type PaymentMethodEligibilityCheckers = NonNullable<(NonNullable<GetPaymentMethodOperationsQuery['paymentMethodEligibilityCheckers']>)[number]>;
+  export type PaymentMethodHandlers = NonNullable<(NonNullable<GetPaymentMethodOperationsQuery['paymentMethodHandlers']>)[number]>;
+}
+
 export namespace GetPaymentMethod {
   export type Variables = GetPaymentMethodQueryVariables;
   export type Query = GetPaymentMethodQuery;
   export type PaymentMethod = (NonNullable<GetPaymentMethodQuery['paymentMethod']>);
 }
 
+export namespace CreatePaymentMethod {
+  export type Variables = CreatePaymentMethodMutationVariables;
+  export type Mutation = CreatePaymentMethodMutation;
+  export type CreatePaymentMethod = (NonNullable<CreatePaymentMethodMutation['createPaymentMethod']>);
+}
+
 export namespace UpdatePaymentMethod {
   export type Variables = UpdatePaymentMethodMutationVariables;
   export type Mutation = UpdatePaymentMethodMutation;
@@ -9297,6 +9392,7 @@ export namespace GetServerConfig {
   export type Permissions = NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['permissions']>)[number]>;
   export type CustomFieldConfig = (NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>);
   export type Address = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Address']>)[number]>;
+  export type Channel = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Channel']>)[number]>;
   export type Collection = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Collection']>)[number]>;
   export type Customer = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Customer']>)[number]>;
   export type Facet = NonNullable<(NonNullable<(NonNullable<(NonNullable<(NonNullable<GetServerConfigQuery['globalSettings']>)['serverConfig']>)['customFieldConfig']>)['Facet']>)[number]>;

+ 36 - 7
packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts

@@ -1,6 +1,10 @@
 import { gql } from 'apollo-angular';
 
-import { CONFIGURABLE_OPERATION_DEF_FRAGMENT, ERROR_RESULT_FRAGMENT } from './shared-definitions';
+import {
+    CONFIGURABLE_OPERATION_DEF_FRAGMENT,
+    CONFIGURABLE_OPERATION_FRAGMENT,
+    ERROR_RESULT_FRAGMENT,
+} from './shared-definitions';
 
 export const COUNTRY_FRAGMENT = gql`
     fragment Country on Country {
@@ -374,17 +378,18 @@ export const PAYMENT_METHOD_FRAGMENT = gql`
         id
         createdAt
         updatedAt
+        name
         code
+        description
         enabled
-        configArgs {
-            name
-            value
+        checker {
+            ...ConfigurableOperation
         }
-        definition {
-            ...ConfigurableOperationDef
+        handler {
+            ...ConfigurableOperation
         }
     }
-    ${CONFIGURABLE_OPERATION_DEF_FRAGMENT}
+    ${CONFIGURABLE_OPERATION_FRAGMENT}
 `;
 
 export const GET_PAYMENT_METHOD_LIST = gql`
@@ -399,6 +404,18 @@ export const GET_PAYMENT_METHOD_LIST = gql`
     ${PAYMENT_METHOD_FRAGMENT}
 `;
 
+export const GET_PAYMENT_METHOD_OPERATIONS = gql`
+    query GetPaymentMethodOperations {
+        paymentMethodEligibilityCheckers {
+            ...ConfigurableOperationDef
+        }
+        paymentMethodHandlers {
+            ...ConfigurableOperationDef
+        }
+    }
+    ${CONFIGURABLE_OPERATION_DEF_FRAGMENT}
+`;
+
 export const GET_PAYMENT_METHOD = gql`
     query GetPaymentMethod($id: ID!) {
         paymentMethod(id: $id) {
@@ -408,6 +425,15 @@ export const GET_PAYMENT_METHOD = gql`
     ${PAYMENT_METHOD_FRAGMENT}
 `;
 
+export const CREATE_PAYMENT_METHOD = gql`
+    mutation CreatePaymentMethod($input: CreatePaymentMethodInput!) {
+        createPaymentMethod(input: $input) {
+            ...PaymentMethod
+        }
+    }
+    ${PAYMENT_METHOD_FRAGMENT}
+`;
+
 export const UPDATE_PAYMENT_METHOD = gql`
     mutation UpdatePaymentMethod($input: UpdatePaymentMethodInput!) {
         updatePaymentMethod(input: $input) {
@@ -588,6 +614,9 @@ export const GET_SERVER_CONFIG = gql`
                     Address {
                         ...CustomFields
                     }
+                    Channel {
+                        ...CustomFields
+                    }
                     Collection {
                         ...CustomFields
                     }

+ 18 - 0
packages/admin-ui/src/lib/core/src/data/providers/settings-data.service.ts

@@ -8,6 +8,8 @@ import {
     CreateChannelInput,
     CreateCountry,
     CreateCountryInput,
+    CreatePaymentMethod,
+    CreatePaymentMethodInput,
     CreateTaxCategory,
     CreateTaxCategoryInput,
     CreateTaxRate,
@@ -32,6 +34,7 @@ import {
     GetJobsById,
     GetPaymentMethod,
     GetPaymentMethodList,
+    GetPaymentMethodOperations,
     GetTaxCategories,
     GetTaxCategory,
     GetTaxRate,
@@ -61,6 +64,7 @@ import {
     CANCEL_JOB,
     CREATE_CHANNEL,
     CREATE_COUNTRY,
+    CREATE_PAYMENT_METHOD,
     CREATE_TAX_CATEGORY,
     CREATE_TAX_RATE,
     CREATE_ZONE,
@@ -82,6 +86,7 @@ import {
     GET_JOB_QUEUE_LIST,
     GET_PAYMENT_METHOD,
     GET_PAYMENT_METHOD_LIST,
+    GET_PAYMENT_METHOD_OPERATIONS,
     GET_TAX_CATEGORIES,
     GET_TAX_CATEGORY,
     GET_TAX_RATE,
@@ -317,6 +322,15 @@ export class SettingsDataService {
         );
     }
 
+    createPaymentMethod(input: CreatePaymentMethodInput) {
+        return this.baseDataService.mutate<CreatePaymentMethod.Mutation, CreatePaymentMethod.Variables>(
+            CREATE_PAYMENT_METHOD,
+            {
+                input,
+            },
+        );
+    }
+
     updatePaymentMethod(input: UpdatePaymentMethodInput) {
         return this.baseDataService.mutate<UpdatePaymentMethod.Mutation, UpdatePaymentMethod.Variables>(
             UPDATE_PAYMENT_METHOD,
@@ -326,6 +340,10 @@ export class SettingsDataService {
         );
     }
 
+    getPaymentMethodOperations() {
+        return this.baseDataService.query<GetPaymentMethodOperations.Query>(GET_PAYMENT_METHOD_OPERATIONS);
+    }
+
     getGlobalSettings(fetchPolicy?: WatchQueryFetchPolicy) {
         return this.baseDataService.query<GetGlobalSettings.Query>(
             GET_GLOBAL_SETTINGS,

+ 8 - 0
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -67,6 +67,7 @@ export * from './data/server-config';
 export * from './data/utils/add-custom-fields';
 export * from './data/utils/get-server-location';
 export * from './data/utils/remove-readonly-custom-fields';
+export * from './data/utils/transform-relation-custom-field-inputs';
 export * from './providers/auth/auth.service';
 export * from './providers/component-registry/component-registry.service';
 export * from './providers/custom-field-component/custom-field-component.service';
@@ -175,6 +176,13 @@ export * from './shared/dynamic-form-inputs/number-form-input/number-form-input.
 export * from './shared/dynamic-form-inputs/password-form-input/password-form-input.component';
 export * from './shared/dynamic-form-inputs/product-selector-form-input/product-selector-form-input.component';
 export * from './shared/dynamic-form-inputs/register-dynamic-input-components';
+export * from './shared/dynamic-form-inputs/relation-form-input/asset/relation-asset-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/customer/relation-customer-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/product/relation-product-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/product-variant/relation-product-variant-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/relation-card/relation-card.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/relation-form-input.component';
+export * from './shared/dynamic-form-inputs/relation-form-input/relation-selector-dialog/relation-selector-dialog.component';
 export * from './shared/dynamic-form-inputs/select-form-input/select-form-input.component';
 export * from './shared/dynamic-form-inputs/text-form-input/text-form-input.component';
 export * from './shared/pipes/asset-preview.pipe';

+ 79 - 25
packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.html

@@ -17,7 +17,7 @@
                 *vdrIfPermissions="'UpdateSettings'"
                 class="btn btn-primary"
                 (click)="save()"
-                [disabled]="detailForm.pristine || detailForm.invalid"
+                [disabled]="detailForm.pristine || detailForm.invalid || !selectedHandler"
             >
                 {{ 'common.update' | translate }}
             </button>
@@ -25,8 +25,21 @@
     </vdr-ab-right>
 </vdr-action-bar>
 
-<form class="form" [formGroup]="detailForm">
-    <vdr-form-field [label]="'common.code' | translate" for="code">
+<form class="form" [formGroup]="detailForm" *ngIf="entity$ | async as paymentMethod">
+    <vdr-form-field [label]="'common.name' | translate" for="name">
+        <input
+            id="name"
+            type="text"
+            formControlName="name"
+            [readonly]="!('UpdateSettings' | hasPermission)"
+            (input)="updateCode(paymentMethod.code, $event.target.value)"
+        />
+    </vdr-form-field>
+    <vdr-form-field
+        [label]="'common.code' | translate"
+        for="code"
+        [readOnlyToggle]="'UpdateSettings' | hasPermission"
+    >
         <input
             id="code"
             type="text"
@@ -34,6 +47,11 @@
             [readonly]="!('UpdateSettings' | hasPermission)"
         />
     </vdr-form-field>
+    <vdr-rich-text-editor
+        formControlName="description"
+        [readonly]="!('UpdateSettings' | hasPermission)"
+        [label]="'common.description' | translate"
+    ></vdr-rich-text-editor>
     <vdr-form-field [label]="'common.enabled' | translate" for="description">
         <clr-toggle-wrapper>
             <input
@@ -46,28 +64,64 @@
         </clr-toggle-wrapper>
     </vdr-form-field>
 
-    <ng-container *ngIf="entity$ | async as paymentMethod">
-        <div
-            class="clr-row"
-            formGroupName="configArgs"
-            *ngIf="paymentMethod?.configArgs?.length && configArgsIsPopulated()"
-        >
-            <div class="clr-col">
-                <label>{{ 'settings.payment-method-config-options' | translate }}</label>
-                <section class="form-block" *ngFor="let arg of paymentMethod.definition.args; index as i">
-                    <vdr-form-field
-                        [label]="arg.label || arg.name"
-                        [for]="arg.name"
-                    >
-                        <vdr-dynamic-form-input
-                            [def]="paymentMethod.definition.args[i]"
-                            [formControlName]="arg.name"
-                            [control]="detailForm.get(['configArgs', arg.name])"
-                            [readonly]="!('UpdateSettings' | hasPermission)"
-                        ></vdr-dynamic-form-input>
-                    </vdr-form-field>
-                </section>
+    <div class="clr-row mt4">
+        <div class="clr-col">
+            <label class="clr-control-label">{{ 'settings.payment-eligibility-checker' | translate }}</label>
+            <vdr-configurable-input
+                *ngIf="selectedChecker && selectedCheckerDefinition"
+                [operation]="selectedChecker"
+                [operationDefinition]="selectedCheckerDefinition"
+                [readonly]="!('UpdateSettings' | hasPermission)"
+                (remove)="removeChecker()"
+                formControlName="checker"
+            ></vdr-configurable-input>
+            <div *ngIf="!selectedChecker || !selectedCheckerDefinition">
+                <vdr-dropdown>
+                    <button class="btn btn-outline" vdrDropdownTrigger>
+                        <clr-icon shape="plus"></clr-icon>
+                        {{ 'common.select' | translate }}
+                    </button>
+                    <vdr-dropdown-menu vdrPosition="bottom-left">
+                        <button
+                            *ngFor="let checker of checkers"
+                            type="button"
+                            vdrDropdownItem
+                            (click)="selectChecker(checker)"
+                        >
+                            {{ checker.description }}
+                        </button>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
+            </div>
+        </div>
+        <div class="clr-col">
+            <label class="clr-control-label">{{ 'settings.payment-handler' | translate }}</label>
+            <vdr-configurable-input
+                *ngIf="selectedHandler && selectedHandlerDefinition"
+                [operation]="selectedHandler"
+                [operationDefinition]="selectedHandlerDefinition"
+                [readonly]="!('UpdateSettings' | hasPermission)"
+                (remove)="removeHandler()"
+                formControlName="handler"
+            ></vdr-configurable-input>
+            <div *ngIf="!selectedHandler || !selectedHandlerDefinition">
+                <vdr-dropdown>
+                    <button class="btn btn-outline" vdrDropdownTrigger>
+                        <clr-icon shape="plus"></clr-icon>
+                        {{ 'common.select' | translate }}
+                    </button>
+                    <vdr-dropdown-menu vdrPosition="bottom-left">
+                        <button
+                            *ngFor="let handler of handlers"
+                            type="button"
+                            vdrDropdownItem
+                            (click)="selectHandler(handler)"
+                        >
+                            {{ handler.description }}
+                        </button>
+                    </vdr-dropdown-menu>
+                </vdr-dropdown>
             </div>
         </div>
-    </ng-container>
+    </div>
 </form>

+ 142 - 23
packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts

@@ -5,14 +5,21 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseDetailComponent,
     ConfigArgDefinition,
+    configurableDefinitionToInstance,
+    ConfigurableOperation,
+    ConfigurableOperationDefinition,
+    CreatePaymentMethodInput,
     DataService,
     encodeConfigArgValue,
     getConfigArgValue,
     NotificationService,
     PaymentMethod,
     ServerConfigService,
+    toConfigurableOperationInput,
     UpdatePaymentMethodInput,
 } from '@vendure/admin-ui/core';
+import { normalizeString } from '@vendure/common/lib/normalize-string';
+import { combineLatest } from 'rxjs';
 import { mergeMap, take } from 'rxjs/operators';
 
 @Component({
@@ -21,9 +28,16 @@ import { mergeMap, take } from 'rxjs/operators';
     styleUrls: ['./payment-method-detail.component.scss'],
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMethod.Fragment>
+export class PaymentMethodDetailComponent
+    extends BaseDetailComponent<PaymentMethod.Fragment>
     implements OnInit, OnDestroy {
     detailForm: FormGroup;
+    checkers: ConfigurableOperationDefinition[] = [];
+    handlers: ConfigurableOperationDefinition[] = [];
+    selectedChecker?: ConfigurableOperation | null;
+    selectedCheckerDefinition?: ConfigurableOperationDefinition;
+    selectedHandler?: ConfigurableOperation | null;
+    selectedHandlerDefinition?: ConfigurableOperationDefinition;
 
     constructor(
         router: Router,
@@ -37,21 +51,48 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
         super(route, router, serverConfigService, dataService);
         this.detailForm = this.formBuilder.group({
             code: ['', Validators.required],
+            name: ['', Validators.required],
+            description: '',
             enabled: [true, Validators.required],
-            configArgs: this.formBuilder.group({}),
+            checker: {},
+            handler: {},
         });
     }
 
     ngOnInit() {
         this.init();
+        combineLatest([
+            this.dataService.settings.getPaymentMethodOperations().single$,
+            this.entity$.pipe(take(1)),
+        ]).subscribe(([data, entity]) => {
+            this.checkers = data.paymentMethodEligibilityCheckers;
+            this.handlers = data.paymentMethodHandlers;
+            this.changeDetector.markForCheck();
+            this.selectedCheckerDefinition = data.paymentMethodEligibilityCheckers.find(
+                c => c.code === (entity.checker && entity.checker.code),
+            );
+            this.selectedHandlerDefinition = data.paymentMethodHandlers.find(
+                c => c.code === (entity.handler && entity.handler.code),
+            );
+        });
     }
 
     ngOnDestroy(): void {
         this.destroy();
     }
 
+    updateCode(currentCode: string, nameValue: string) {
+        if (!currentCode) {
+            const codeControl = this.detailForm.get(['code']);
+            if (codeControl && codeControl.pristine) {
+                codeControl.setValue(normalizeString(nameValue, '-'));
+            }
+        }
+    }
+
     getArgDef(paymentMethod: PaymentMethod.Fragment, argName: string): ConfigArgDefinition | undefined {
-        return paymentMethod.definition.args.find(a => a.name === argName);
+        // return paymentMethod.handler.args.find(a => a.name === argName);
+        return;
     }
 
     configArgsIsPopulated(): boolean {
@@ -62,20 +103,101 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
         return 0 < Object.keys(configArgsGroup.controls).length;
     }
 
+    selectChecker(checker: ConfigurableOperationDefinition) {
+        this.selectedCheckerDefinition = checker;
+        this.selectedChecker = configurableDefinitionToInstance(checker);
+        const formControl = this.detailForm.get('checker');
+        if (formControl) {
+            formControl.clearValidators();
+            formControl.updateValueAndValidity({ onlySelf: true });
+            formControl.patchValue(this.selectedChecker);
+        }
+        this.detailForm.markAsDirty();
+    }
+
+    selectHandler(handler: ConfigurableOperationDefinition) {
+        this.selectedHandlerDefinition = handler;
+        this.selectedHandler = configurableDefinitionToInstance(handler);
+        const formControl = this.detailForm.get('handler');
+        if (formControl) {
+            formControl.clearValidators();
+            formControl.updateValueAndValidity({ onlySelf: true });
+            formControl.patchValue(this.selectedHandler);
+        }
+        this.detailForm.markAsDirty();
+    }
+
+    removeChecker() {
+        this.selectedChecker = null;
+        this.detailForm.markAsDirty();
+    }
+
+    removeHandler() {
+        this.selectedHandler = null;
+        this.detailForm.markAsDirty();
+    }
+
+    create() {
+        const selectedChecker = this.selectedChecker;
+        const selectedHandler = this.selectedHandler;
+        if (!selectedHandler) {
+            return;
+        }
+        this.entity$
+            .pipe(
+                take(1),
+                mergeMap(({ id }) => {
+                    const formValue = this.detailForm.value;
+                    const input: CreatePaymentMethodInput = {
+                        name: formValue.name,
+                        code: formValue.code,
+                        description: formValue.description,
+                        enabled: formValue.enabled,
+                        checker: selectedChecker
+                            ? toConfigurableOperationInput(selectedChecker, formValue.checker)
+                            : null,
+                        handler: toConfigurableOperationInput(selectedHandler, formValue.handler),
+                    };
+                    return this.dataService.settings.createPaymentMethod(input);
+                }),
+            )
+            .subscribe(
+                data => {
+                    this.notificationService.success(_('common.notify-create-success'), {
+                        entity: 'PaymentMethod',
+                    });
+                    this.detailForm.markAsPristine();
+                    this.changeDetector.markForCheck();
+                },
+                err => {
+                    this.notificationService.error(_('common.notify-create-error'), {
+                        entity: 'PaymentMethod',
+                    });
+                },
+            );
+    }
+
     save() {
+        const selectedChecker = this.selectedChecker;
+        const selectedHandler = this.selectedHandler;
+        if (!selectedHandler) {
+            return;
+        }
         this.entity$
             .pipe(
                 take(1),
-                mergeMap(({ id, configArgs }) => {
+                mergeMap(({ id }) => {
                     const formValue = this.detailForm.value;
                     const input: UpdatePaymentMethodInput = {
                         id,
+                        name: formValue.name,
                         code: formValue.code,
+                        description: formValue.description,
                         enabled: formValue.enabled,
-                        configArgs: Object.entries<any>(formValue.configArgs).map(([name, value], i) => ({
-                            name,
-                            value: encodeConfigArgValue(value),
-                        })),
+                        checker: selectedChecker
+                            ? toConfigurableOperationInput(selectedChecker, formValue.checker)
+                            : null,
+                        handler: toConfigurableOperationInput(selectedHandler, formValue.handler),
                     };
                     return this.dataService.settings.updatePaymentMethod(input);
                 }),
@@ -98,23 +220,20 @@ export class PaymentMethodDetailComponent extends BaseDetailComponent<PaymentMet
 
     protected setFormValues(paymentMethod: PaymentMethod.Fragment): void {
         this.detailForm.patchValue({
+            name: paymentMethod.name,
             code: paymentMethod.code,
+            description: paymentMethod.description,
             enabled: paymentMethod.enabled,
+            checker: paymentMethod.checker || {},
+            handler: paymentMethod.handler || {},
         });
-        const configArgsGroup = this.detailForm.get('configArgs') as FormGroup;
-        if (configArgsGroup) {
-            for (const arg of paymentMethod.configArgs) {
-                let control = configArgsGroup.get(arg.name);
-                const def = this.getArgDef(paymentMethod, arg.name);
-                const value = def?.list === true && arg.value === '' ? [] : getConfigArgValue(arg.value);
-                if (control) {
-                    control.patchValue(value);
-                } else {
-                    control = this.formBuilder.control(value);
-                    configArgsGroup.addControl(arg.name, control);
-                }
-            }
-        }
-        this.changeDetector.markForCheck();
+        this.selectedChecker = paymentMethod.checker && {
+            code: paymentMethod.checker.code,
+            args: paymentMethod.checker.args.map(a => ({ ...a, value: getConfigArgValue(a.value) })),
+        };
+        this.selectedHandler = paymentMethod.handler && {
+            code: paymentMethod.handler.code,
+            args: paymentMethod.handler.args.map(a => ({ ...a, value: getConfigArgValue(a.value) })),
+        };
     }
 }

+ 3 - 2
packages/admin-ui/src/lib/settings/src/providers/routing/payment-method-resolver.ts

@@ -19,10 +19,11 @@ export class PaymentMethodResolver extends BaseEntityResolver<PaymentMethod.Frag
                 id: '',
                 createdAt: '',
                 updatedAt: '',
+                name: '',
                 code: '',
+                description: '',
                 enabled: true,
-                configArgs: [],
-                definition: {} as any,
+                handler: {} as any,
             },
             id => dataService.settings.getPaymentMethod(id).mapStream(data => data.paymentMethod),
         );

+ 10 - 0
packages/dev-server/dev-config.ts

@@ -8,14 +8,23 @@ import {
     DefaultSearchPlugin,
     dummyPaymentHandler,
     examplePaymentHandler,
+    LanguageCode,
     LogLevel,
     manualFulfillmentHandler,
+    PaymentMethodEligibilityChecker,
     VendureConfig,
 } from '@vendure/core';
 import { defaultEmailHandlers, EmailPlugin } from '@vendure/email-plugin';
 import path from 'path';
 import { ConnectionOptions } from 'typeorm';
 
+const testPaymentChecker = new PaymentMethodEligibilityChecker({
+    code: 'test-checker',
+    description: [{ languageCode: LanguageCode.en, value: 'test checker' }],
+    args: {},
+    check: (ctx, order) => true,
+});
+
 /**
  * Config settings used during development
  */
@@ -51,6 +60,7 @@ export const devConfig: VendureConfig = {
         ...getDbConfig(),
     },
     paymentOptions: {
+        paymentMethodEligibilityCheckers: [testPaymentChecker],
         paymentMethodHandlers: [dummyPaymentHandler],
     },
     customFields: {},