Переглянути джерело

feat(admin-ui): Fulfillment dialog accepts handler-defined arguments

Relates to #529
Michael Bromley 5 роки тому
батько
коміт
c787241795

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

@@ -1307,7 +1307,7 @@ export type UpdateGlobalSettingsResult = GlobalSettings | ChannelDefaultLanguage
 /**
  * @description
  * The state of a Job in the JobQueue
- *
+ * 
  * @docsCategory common
  */
 export enum JobState {
@@ -2057,7 +2057,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 {
@@ -2324,7 +2324,7 @@ export type EmailAddressConflictError = ErrorResult & {
 /**
  * @description
  * ISO 4217 currency code
- *
+ * 
  * @docsCategory common
  */
 export enum CurrencyCode {
@@ -2758,7 +2758,7 @@ export type CustomFieldConfig = StringCustomFieldConfig | LocaleStringCustomFiel
  * 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 {
@@ -5219,7 +5219,7 @@ export type OrderDetailFragment = (
     & Pick<Promotion, 'id' | 'couponCode'>
   )>, shippingMethod?: Maybe<(
     { __typename?: 'ShippingMethod' }
-    & Pick<ShippingMethod, 'id' | 'code' | 'name' | 'description'>
+    & Pick<ShippingMethod, 'id' | 'code' | 'name' | 'fulfillmentHandlerCode' | 'description'>
   )>, shippingAddress?: Maybe<(
     { __typename?: 'OrderAddress' }
     & OrderAddressFragment

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

@@ -1,7 +1,12 @@
 import { ConfigArgType, CustomFieldType } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 
-import { ConfigArgDefinition } from '../generated-types';
+import {
+    ConfigArgDefinition,
+    ConfigurableOperation,
+    ConfigurableOperationDefinition,
+    ConfigurableOperationInput,
+} from '../generated-types';
 
 /**
  * ConfigArg values are always stored as strings. If they are not primitives, then
@@ -20,6 +25,58 @@ export function encodeConfigArgValue(value: any): string {
     return Array.isArray(value) ? JSON.stringify(value) : (value ?? '').toString();
 }
 
+/**
+ * Creates an empty ConfigurableOperation object based on the definition.
+ */
+export function configurableDefinitionToInstance(
+    def: ConfigurableOperationDefinition,
+): ConfigurableOperation {
+    return {
+        ...def,
+        args: def.args.map(arg => {
+            return {
+                ...arg,
+                value: getDefaultConfigArgValue(arg),
+            };
+        }),
+    } as ConfigurableOperation;
+}
+
+/**
+ * Converts an object of the type:
+ * ```
+ * {
+ *     code: 'my-operation',
+ *     args: {
+ *         someProperty: 'foo'
+ *     }
+ * }
+ * ```
+ * to the format defined by the ConfigurableOperationInput GraphQL input type:
+ * ```
+ * {
+ *     code: 'my-operation',
+ *     args: [
+ *         { name: 'someProperty', value: 'foo' }
+ *     ]
+ * }
+ * ```
+ */
+export function toConfigurableOperationInput(
+    operation: ConfigurableOperation,
+    formValueOperations: any,
+): ConfigurableOperationInput {
+    return {
+        code: operation.code,
+        arguments: Object.values<any>(formValueOperations.args || {}).map((value, j) => ({
+            name: operation.args[j].name,
+            value: value.hasOwnProperty('value')
+                ? encodeConfigArgValue((value as any).value)
+                : encodeConfigArgValue(value),
+        })),
+    };
+}
+
 /**
  * Returns a default value based on the type of the config arg.
  */

+ 1 - 0
packages/admin-ui/src/lib/core/src/data/definitions/order-definitions.ts

@@ -141,6 +141,7 @@ export const ORDER_DETAIL_FRAGMENT = gql`
             id
             code
             name
+            fulfillmentHandlerCode
             description
         }
         shippingAddress {

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

@@ -15,7 +15,7 @@
             </div>
         </form>
     </div>
-    <div class="card-footer" *ngIf="!readonly">
+    <div class="card-footer" *ngIf="!readonly && removable">
         <button class="btn btn-sm btn-link btn-warning" (click)="remove.emit(operation)">
             <clr-icon shape="times"></clr-icon>
             {{ 'common.remove' | translate }}

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

@@ -59,6 +59,7 @@ export class ConfigurableInputComponent implements OnChanges, OnDestroy, Control
     @Input() operation?: ConfigurableOperation;
     @Input() operationDefinition?: ConfigurableOperationDefinition;
     @Input() readonly = false;
+    @Input() removable = true;
     @Output() remove = new EventEmitter<ConfigurableOperation>();
     argValues: { [name: string]: any } = {};
     onChange: (val: any) => void;

+ 8 - 10
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.html

@@ -19,7 +19,7 @@
                 [class.ignore]="getUnfulfilledCount(line) === 0"
             >
                 <td class="align-middle thumb">
-                    <img *ngIf="line.featuredAsset" [src]="line.featuredAsset | assetPreview:'tiny'" />
+                    <img *ngIf="line.featuredAsset" [src]="line.featuredAsset | assetPreview: 'tiny'" />
                 </td>
                 <td class="align-middle name">{{ line.productVariant.name }}</td>
                 <td class="align-middle sku">{{ line.productVariant.sku }}</td>
@@ -43,20 +43,18 @@
         <h6>{{ 'order.shipping-method' | translate }}</h6>
         {{ order.shippingMethod?.name }}
         <strong>{{ order.shipping / 100 | currency: order.currencyCode }}</strong>
-        <clr-input-container>
-            <label>{{ 'order.fulfillment-method' | translate }}</label>
-            <input clrInput placeholder="" name="method" [(ngModel)]="method" required />
-        </clr-input-container>
-        <clr-input-container>
-            <label>{{ 'order.tracking-code' | translate }}</label>
-            <input clrInput placeholder="" name="trackingCode" [(ngModel)]="trackingCode" />
-        </clr-input-container>
+        <vdr-configurable-input
+            [operationDefinition]="fulfillmentHandlerDef"
+            [operation]="fulfillmentHandler"
+            [formControl]="fulfillmentHandlerControl"
+            [removable]="false"
+        ></vdr-configurable-input>
     </div>
 </div>
 
 <ng-template vdrDialogButtons>
     <button type="button" class="btn" (click)="cancel()">{{ 'common.cancel' | translate }}</button>
-    <button type="submit" (click)="select()" [disabled]="!method" class="btn btn-primary">
+    <button type="submit" (click)="select()" [disabled]="!canSubmit()" class="btn btn-primary">
         {{ 'order.create-fulfillment' | translate }}
     </button>
 </ng-template>

+ 37 - 14
packages/admin-ui/src/lib/order/src/components/fulfill-order-dialog/fulfill-order-dialog.component.ts

@@ -1,14 +1,17 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core';
+import { FormControl } from '@angular/forms';
 import {
+    configurableDefinitionToInstance,
+    ConfigurableOperation,
+    ConfigurableOperationDefinition,
     DataService,
     Dialog,
     FulfillOrderInput,
     GlobalFlag,
     OrderDetail,
     OrderDetailFragment,
+    toConfigurableOperationInput,
 } from '@vendure/admin-ui/core';
-import { Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
 
 @Component({
     selector: 'vdr-fulfill-order-dialog',
@@ -17,12 +20,15 @@ import { map } from 'rxjs/operators';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, OnInit {
-    order: OrderDetailFragment;
     resolveWith: (result?: FulfillOrderInput) => void;
-    method = '';
-    trackingCode = '';
+    fulfillmentHandlerDef: ConfigurableOperationDefinition;
+    fulfillmentHandler: ConfigurableOperation;
+    fulfillmentHandlerControl = new FormControl();
     fulfillmentQuantities: { [lineId: string]: { fulfillCount: number; max: number } } = {};
 
+    // Provided by modalService.fromComponent() call
+    order: OrderDetailFragment;
+
     constructor(private dataService: DataService, private changeDetector: ChangeDetectorRef) {}
 
     ngOnInit(): void {
@@ -37,9 +43,17 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
             this.changeDetector.markForCheck();
         });
 
-        if (this.order.shippingMethod) {
-            this.method = this.order.shippingMethod.name;
-        }
+        this.dataService.shippingMethod
+            .getShippingMethodOperations()
+            .mapSingle(data => data.fulfillmentHandlers)
+            .subscribe(handlers => {
+                this.fulfillmentHandlerDef =
+                    handlers.find(h => h.code === this.order.shippingMethod?.fulfillmentHandlerCode) ||
+                    handlers[0];
+                this.fulfillmentHandler = configurableDefinitionToInstance(this.fulfillmentHandlerDef);
+                this.fulfillmentHandlerControl.patchValue(this.fulfillmentHandler);
+                this.changeDetector.markForCheck();
+            });
     }
 
     getFulfillableCount(line: OrderDetail.Lines, globalTrackInventory: boolean): number {
@@ -56,6 +70,17 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
         return line.quantity - fulfilled;
     }
 
+    canSubmit(): boolean {
+        const totalCount = Object.values(this.fulfillmentQuantities).reduce(
+            (total, { fulfillCount }) => total + fulfillCount,
+            0,
+        );
+        const formIsValid =
+            this.fulfillmentHandlerDef?.args.length === 0 ||
+            (this.fulfillmentHandlerControl.valid && this.fulfillmentHandlerControl.touched);
+        return formIsValid && 0 < totalCount;
+    }
+
     select() {
         const lines = Object.entries(this.fulfillmentQuantities).map(([orderLineId, { fulfillCount }]) => ({
             orderLineId,
@@ -63,12 +88,10 @@ export class FulfillOrderDialogComponent implements Dialog<FulfillOrderInput>, O
         }));
         this.resolveWith({
             lines,
-            handler: {
-                code: 'foo',
-                arguments: [],
-            },
-            // trackingCode: this.trackingCode,
-            // method: this.method,
+            handler: toConfigurableOperationInput(
+                this.fulfillmentHandler,
+                this.fulfillmentHandlerControl.value,
+            ),
         });
     }
 

+ 1 - 1
packages/admin-ui/src/lib/order/src/components/order-detail/order-detail.component.html

@@ -205,7 +205,7 @@
                 </tr>
                 <tr class="shipping">
                     <td class="left clr-align-middle">{{ 'order.shipping' | translate }}</td>
-                    <td class="clr-align-middle">{{ order.shippingMethod?.description }}</td>
+                    <td class="clr-align-middle">{{ order.shippingMethod?.name }}</td>
                     <td [attr.colspan]="3 + visibleOrderLineCustomFields.length"></td>
                     <ng-container *ngIf="showElided"><td></td></ng-container>
                     <td class="clr-align-middle">

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

@@ -4,30 +4,25 @@ import { ActivatedRoute, Router } from '@angular/router';
 import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
 import {
     BaseDetailComponent,
+    configurableDefinitionToInstance,
     ConfigurableOperation,
     ConfigurableOperationDefinition,
-    ConfigurableOperationInput,
-    CreateFacetInput,
     CreateShippingMethodInput,
     createUpdatedTranslatable,
     CustomFieldConfig,
     DataService,
-    encodeConfigArgValue,
-    FacetWithValues,
     GetActiveChannel,
     getConfigArgValue,
-    getDefaultConfigArgValue,
     LanguageCode,
     NotificationService,
     ServerConfigService,
     ShippingMethod,
     TestShippingMethodInput,
     TestShippingMethodResult,
-    UpdateFacetInput,
+    toConfigurableOperationInput,
     UpdateShippingMethodInput,
 } from '@vendure/admin-ui/core';
 import { normalizeString } from '@vendure/common/lib/normalize-string';
-import { ConfigArgType } from '@vendure/common/lib/shared-types';
 import { combineLatest, merge, Observable, of, Subject } from 'rxjs';
 import { mergeMap, switchMap, take, takeUntil } from 'rxjs/operators';
 
@@ -114,11 +109,8 @@ export class ShippingMethodDetailComponent
                 const input: TestShippingMethodInput = {
                     shippingAddress: { ...address, streetLine1: 'test' },
                     lines: lines.map(l => ({ productVariantId: l.id, quantity: l.quantity })),
-                    checker: this.toAdjustmentOperationInput(this.selectedChecker, formValue.checker),
-                    calculator: this.toAdjustmentOperationInput(
-                        this.selectedCalculator,
-                        formValue.calculator,
-                    ),
+                    checker: toConfigurableOperationInput(this.selectedChecker, formValue.checker),
+                    calculator: toConfigurableOperationInput(this.selectedCalculator, formValue.calculator),
                 };
                 return this.dataService.shippingMethod
                     .testShippingMethod(input)
@@ -155,7 +147,7 @@ export class ShippingMethodDetailComponent
 
     selectChecker(checker: ConfigurableOperationDefinition) {
         this.selectedCheckerDefinition = checker;
-        this.selectedChecker = this.configurableDefinitionToInstance(checker);
+        this.selectedChecker = configurableDefinitionToInstance(checker);
         const formControl = this.detailForm.get('checker');
         if (formControl) {
             formControl.patchValue(this.selectedChecker);
@@ -165,7 +157,7 @@ export class ShippingMethodDetailComponent
 
     selectCalculator(calculator: ConfigurableOperationDefinition) {
         this.selectedCalculatorDefinition = calculator;
-        this.selectedCalculator = this.configurableDefinitionToInstance(calculator);
+        this.selectedCalculator = configurableDefinitionToInstance(calculator);
         const formControl = this.detailForm.get('calculator');
         if (formControl) {
             formControl.patchValue(this.selectedCalculator);
@@ -173,18 +165,6 @@ export class ShippingMethodDetailComponent
         this.detailForm.markAsDirty();
     }
 
-    private configurableDefinitionToInstance(def: ConfigurableOperationDefinition): ConfigurableOperation {
-        return {
-            ...def,
-            args: def.args.map(arg => {
-                return {
-                    ...arg,
-                    value: getDefaultConfigArgValue(arg),
-                };
-            }),
-        } as ConfigurableOperation;
-    }
-
     create() {
         const selectedChecker = this.selectedChecker;
         const selectedCalculator = this.selectedCalculator;
@@ -202,8 +182,8 @@ export class ShippingMethodDetailComponent
                             this.detailForm,
                             languageCode,
                         ) as CreateShippingMethodInput),
-                        checker: this.toAdjustmentOperationInput(selectedChecker, formValue.checker),
-                        calculator: this.toAdjustmentOperationInput(selectedCalculator, formValue.calculator),
+                        checker: toConfigurableOperationInput(selectedChecker, formValue.checker),
+                        calculator: toConfigurableOperationInput(selectedCalculator, formValue.calculator),
                     };
                     return this.dataService.shippingMethod.createShippingMethod(input);
                 }),
@@ -242,8 +222,8 @@ export class ShippingMethodDetailComponent
                             this.detailForm,
                             languageCode,
                         ) as UpdateShippingMethodInput),
-                        checker: this.toAdjustmentOperationInput(selectedChecker, formValue.checker),
-                        calculator: this.toAdjustmentOperationInput(selectedCalculator, formValue.calculator),
+                        checker: toConfigurableOperationInput(selectedChecker, formValue.checker),
+                        calculator: toConfigurableOperationInput(selectedCalculator, formValue.calculator),
                     };
                     return this.dataService.shippingMethod.updateShippingMethod(input);
                 }),
@@ -313,24 +293,6 @@ export class ShippingMethodDetailComponent
         return { ...input, fulfillmentHandler: formValue.fulfillmentHandler };
     }
 
-    /**
-     * Maps an array of conditions or actions to the input format expected by the GraphQL API.
-     */
-    private toAdjustmentOperationInput(
-        operation: ConfigurableOperation,
-        formValueOperations: any,
-    ): ConfigurableOperationInput {
-        return {
-            code: operation.code,
-            arguments: Object.values<any>(formValueOperations.args || {}).map((value, j) => ({
-                name: operation.args[j].name,
-                value: value.hasOwnProperty('value')
-                    ? encodeConfigArgValue((value as any).value)
-                    : encodeConfigArgValue(value),
-            })),
-        };
-    }
-
     protected setFormValues(shippingMethod: ShippingMethod.Fragment, languageCode: LanguageCode): void {
         const currentTranslation = shippingMethod.translations.find(t => t.languageCode === languageCode);
         this.detailForm.patchValue({

+ 20 - 2
packages/dev-server/dev-config.ts

@@ -31,13 +31,15 @@ const customFulfillmentHandler = new FulfillmentHandler({
     args: {
         preferredService: {
             type: 'string',
-            config: {
+            ui: {
+                component: 'select-form-input',
                 options: [{ value: 'first_class' }, { value: 'priority' }, { value: 'standard' }],
             },
         },
     },
     createFulfillment: async (ctx, orders, orderItems, args) => {
         return {
+            method: `Ship-o-matic ${args.preferredService}`,
             trackingCode: 'SHIP-' + Math.random().toString(36).substr(3),
         };
     },
@@ -46,6 +48,22 @@ const customFulfillmentHandler = new FulfillmentHandler({
     },
 });
 
+const pickupFulfillmentHandler = new FulfillmentHandler({
+    code: 'customer-collect',
+    description: [
+        {
+            languageCode: LanguageCode.en,
+            value: 'Customer collect fulfillment',
+        },
+    ],
+    args: {},
+    createFulfillment: async (ctx, orders, orderItems, args) => {
+        return {
+            method: `Customer collect`,
+        };
+    },
+});
+
 /**
  * Config settings used during development
  */
@@ -89,7 +107,7 @@ export const devConfig: VendureConfig = {
         importAssetsDir: path.join(__dirname, 'import-assets'),
     },
     shippingOptions: {
-        fulfillmentHandlers: [manualFulfillmentHandler, customFulfillmentHandler],
+        fulfillmentHandlers: [manualFulfillmentHandler, customFulfillmentHandler, pickupFulfillmentHandler],
     },
     plugins: [
         AssetServerPlugin.init({