Browse Source

feat(admin-ui): Unify CustomFieldControl type with FormInputComponent

Relates to #415

BREAKING CHANGE: If you use custom field controls in the Admin UI, you'll need to slightly modify the component class: the `customFieldConfig` property has been renamed to `config` and a required `readonly: boolean;` field should be added. This is part of an effort to unify the way custom input components work across different parts of the Admin UI.
Michael Bromley 5 years ago
parent
commit
9e22347dd4
19 changed files with 223 additions and 225 deletions
  1. 2 3
      packages/admin-ui/src/lib/core/src/common/component-registry-types.ts
  2. 3 26
      packages/admin-ui/src/lib/core/src/core.module.ts
  3. 3 3
      packages/admin-ui/src/lib/core/src/providers/component-registry/component-registry.service.ts
  4. 25 61
      packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts
  5. 12 1
      packages/admin-ui/src/lib/core/src/public_api.ts
  6. 4 59
      packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html
  7. 33 45
      packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts
  8. 3 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.ts
  9. 3 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/currency-form-input/currency-form-input.component.ts
  10. 1 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/date-form-input/date-form-input.component.html
  11. 3 5
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/date-form-input/date-form-input.component.ts
  12. 16 9
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts
  13. 4 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.ts
  14. 8 1
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/number-form-input/number-form-input.component.html
  15. 3 5
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/number-form-input/number-form-input.component.ts
  16. 94 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/register-dynamic-input-components.ts
  17. 3 4
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.ts
  18. 2 0
      packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/text-form-input/text-form-input.component.ts
  19. 1 0
      packages/admin-ui/src/lib/order/src/public_api.ts

+ 2 - 3
packages/admin-ui/src/lib/core/src/common/component-registry-types.ts

@@ -1,13 +1,12 @@
 import { FormControl } from '@angular/forms';
 
-export interface FormInputComponent {
+export interface FormInputComponent<C = InputComponentConfig> {
     isListInput?: boolean;
     readonly: boolean;
     formControl: FormControl;
-    config: InputComponentConfig;
+    config: C;
 }
 
 export type InputComponentConfig = {
-    component: string;
     [prop: string]: any;
 };

+ 3 - 26
packages/admin-ui/src/lib/core/src/core.module.ts

@@ -1,6 +1,6 @@
 import { PlatformLocation } from '@angular/common';
 import { HttpClient } from '@angular/common/http';
-import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-translate/core';
@@ -17,18 +17,11 @@ import { OverlayHostComponent } from './components/overlay-host/overlay-host.com
 import { UiLanguageSwitcherDialogComponent } from './components/ui-language-switcher-dialog/ui-language-switcher-dialog.component';
 import { UserMenuComponent } from './components/user-menu/user-menu.component';
 import { DataModule } from './data/data.module';
-import { ComponentRegistryService } from './providers/component-registry/component-registry.service';
 import { CustomHttpTranslationLoader } from './providers/i18n/custom-http-loader';
 import { InjectableTranslateMessageFormatCompiler } from './providers/i18n/custom-message-format-compiler';
 import { I18nService } from './providers/i18n/i18n.service';
 import { LocalStorageService } from './providers/local-storage/local-storage.service';
-import { BooleanFormInputComponent } from './shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component';
-import { CurrencyFormInputComponent } from './shared/dynamic-form-inputs/currency-form-input/currency-form-input.component';
-import { DateFormInputComponent } from './shared/dynamic-form-inputs/date-form-input/date-form-input.component';
-import { FacetValueFormInputComponent } from './shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component';
-import { NumberFormInputComponent } from './shared/dynamic-form-inputs/number-form-input/number-form-input.component';
-import { SelectFormInputComponent } from './shared/dynamic-form-inputs/select-form-input/select-form-input.component';
-import { TextFormInputComponent } from './shared/dynamic-form-inputs/text-form-input/text-form-input.component';
+import { registerDefaultFormInputs } from './shared/dynamic-form-inputs/register-dynamic-input-components';
 import { SharedModule } from './shared/shared.module';
 
 @NgModule({
@@ -46,23 +39,7 @@ import { SharedModule } from './shared/shared.module';
             compiler: { provide: TranslateCompiler, useClass: InjectableTranslateMessageFormatCompiler },
         }),
     ],
-    providers: [
-        { provide: MESSAGE_FORMAT_CONFIG, useFactory: getLocales },
-        {
-            provide: APP_INITIALIZER,
-            multi: true,
-            useFactory: (registry: ComponentRegistryService) => () => {
-                registry.registerInputComponent('boolean-form-input', BooleanFormInputComponent);
-                registry.registerInputComponent('currency-form-input', CurrencyFormInputComponent);
-                registry.registerInputComponent('date-form-input', DateFormInputComponent);
-                registry.registerInputComponent('facet-value-form-input', FacetValueFormInputComponent);
-                registry.registerInputComponent('number-form-input', NumberFormInputComponent);
-                registry.registerInputComponent('select-form-input', SelectFormInputComponent);
-                registry.registerInputComponent('text-form-input', TextFormInputComponent);
-            },
-            deps: [ComponentRegistryService],
-        },
-    ],
+    providers: [{ provide: MESSAGE_FORMAT_CONFIG, useFactory: getLocales }, registerDefaultFormInputs()],
     exports: [SharedModule, OverlayHostComponent],
     declarations: [
         AppShellComponent,

+ 3 - 3
packages/admin-ui/src/lib/core/src/providers/component-registry/component-registry.service.ts

@@ -6,9 +6,9 @@ import { FormInputComponent, InputComponentConfig } from '../../common/component
     providedIn: 'root',
 })
 export class ComponentRegistryService {
-    private inputComponentMap = new Map<string, Type<FormInputComponent>>();
+    private inputComponentMap = new Map<string, Type<FormInputComponent<any>>>();
 
-    registerInputComponent(id: string, component: Type<FormInputComponent>) {
+    registerInputComponent(id: string, component: Type<FormInputComponent<any>>) {
         if (this.inputComponentMap.has(id)) {
             throw new Error(
                 `Cannot register an InputComponent with the id "${id}", as one with that id already exists`,
@@ -17,7 +17,7 @@ export class ComponentRegistryService {
         this.inputComponentMap.set(id, component);
     }
 
-    getInputComponent(id: string): Type<FormInputComponent> | undefined {
+    getInputComponent(id: string): Type<FormInputComponent<any>> | undefined {
         return this.inputComponentMap.get(id);
     }
 }

+ 25 - 61
packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts

@@ -1,57 +1,16 @@
-import {
-    APP_INITIALIZER,
-    ComponentFactory,
-    ComponentFactoryResolver,
-    Injectable,
-    Injector,
-    Provider,
-} from '@angular/core';
-import { FormControl } from '@angular/forms';
+import { ComponentFactoryResolver, Injectable } from '@angular/core';
 import { Type } from '@vendure/common/lib/shared-types';
 
+import { FormInputComponent } from '../../common/component-registry-types';
 import { CustomFields, CustomFieldsFragment } from '../../common/generated-types';
+import { ComponentRegistryService } from '../component-registry/component-registry.service';
 
 export type CustomFieldConfigType = CustomFieldsFragment;
 
-export interface CustomFieldControl {
-    formControl: FormControl;
-    customFieldConfig: CustomFieldConfigType;
-}
+export interface CustomFieldControl extends FormInputComponent<CustomFieldConfigType> {}
 
 export type CustomFieldEntityName = Exclude<keyof CustomFields, '__typename'>;
 
-/**
- * @description
- * Registers a custom component to act as the form input control for the given custom field.
- * This should be used in the NgModule `providers` array of your ui extension module.
- *
- * @example
- * ```TypeScript
- * \@NgModule({
- *   imports: [SharedModule],
- *   declarations: [MyCustomFieldControl],
- *   providers: [
- *       registerCustomFieldComponent('Product', 'someCustomField', MyCustomFieldControl),
- *   ],
- * })
- * export class MyUiExtensionModule {}
- * ```
- */
-export function registerCustomFieldComponent(
-    entity: CustomFieldEntityName,
-    fieldName: string,
-    component: Type<CustomFieldControl>,
-): Provider {
-    return {
-        provide: APP_INITIALIZER,
-        multi: true,
-        useFactory: (customFieldComponentService: CustomFieldComponentService) => () => {
-            customFieldComponentService.registerCustomFieldComponent(entity, fieldName, component);
-        },
-        deps: [CustomFieldComponentService],
-    };
-}
-
 /**
  * This service allows the registration of custom controls for customFields.
  */
@@ -59,9 +18,10 @@ export function registerCustomFieldComponent(
     providedIn: 'root',
 })
 export class CustomFieldComponentService {
-    private registry: { [K in CustomFieldEntityName]?: { [name: string]: Type<CustomFieldControl> } } = {};
-
-    constructor(private componentFactoryResolver: ComponentFactoryResolver, private injector: Injector) {}
+    constructor(
+        private componentFactoryResolver: ComponentFactoryResolver,
+        private componentRegistryService: ComponentRegistryService,
+    ) {}
 
     /**
      * Register a CustomFieldControl component to be used with the specified customField and entity.
@@ -71,21 +31,25 @@ export class CustomFieldComponentService {
         fieldName: string,
         component: Type<CustomFieldControl>,
     ) {
-        if (!this.registry[entity]) {
-            this.registry[entity] = {};
-        }
-        Object.assign(this.registry[entity], { [fieldName]: component });
+        const id = this.generateId(entity, fieldName, true);
+        this.componentRegistryService.registerInputComponent(id, component);
     }
 
-    getCustomFieldComponent(
-        entity: CustomFieldEntityName,
-        fieldName: string,
-    ): ComponentFactory<CustomFieldControl> | undefined {
-        const entityFields = this.registry[entity];
-        const componentType = entityFields && entityFields[fieldName];
-        if (componentType) {
-            const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentType);
-            return componentFactory;
+    /**
+     * Checks whether a custom component is registered for the given entity custom field,
+     * and if so returns the ID of that component.
+     */
+    customFieldComponentExists(entity: CustomFieldEntityName, fieldName: string): string | undefined {
+        const id = this.generateId(entity, fieldName, true);
+        return this.componentRegistryService.getInputComponent(id) ? id : undefined;
+    }
+
+    private generateId(entity: CustomFieldEntityName, fieldName: string, isCustomField: boolean) {
+        let id = entity;
+        if (isCustomField) {
+            id += '-customFields';
         }
+        id += '-' + fieldName;
+        return id;
     }
 }

+ 12 - 1
packages/admin-ui/src/lib/core/src/public_api.ts

@@ -5,15 +5,16 @@ export * from './app.config';
 export * from './common/base-detail.component';
 export * from './common/base-entity-resolver';
 export * from './common/base-list.component';
+export * from './common/component-registry-types';
 export * from './common/deactivate-aware';
 export * from './common/detail-breadcrumb';
 export * from './common/generated-types';
 export * from './common/introspection-result-wrapper';
 export * from './common/introspection-result';
 export * from './common/language-translation-strings';
+export * from './common/utilities/configurable-operation-utils';
 export * from './common/utilities/create-updated-translatable';
 export * from './common/utilities/flatten-facet-values';
-export * from './common/utilities/configurable-operation-utils';
 export * from './common/utilities/get-default-ui-language';
 export * from './common/utilities/interpolate-description';
 export * from './common/utilities/string-to-color';
@@ -65,6 +66,7 @@ export * from './data/utils/add-custom-fields';
 export * from './data/utils/get-server-location';
 export * from './data/utils/remove-readonly-custom-fields';
 export * from './providers/auth/auth.service';
+export * from './providers/component-registry/component-registry.service';
 export * from './providers/custom-field-component/custom-field-component.service';
 export * from './providers/guard/auth.guard';
 export * from './providers/health-check/health-check.service';
@@ -152,6 +154,15 @@ export * from './shared/directives/if-default-channel-active.directive';
 export * from './shared/directives/if-directive-base';
 export * from './shared/directives/if-multichannel.directive';
 export * from './shared/directives/if-permissions.directive';
+export * from './shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component';
+export * from './shared/dynamic-form-inputs/currency-form-input/currency-form-input.component';
+export * from './shared/dynamic-form-inputs/date-form-input/date-form-input.component';
+export * from './shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component';
+export * from './shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component';
+export * from './shared/dynamic-form-inputs/number-form-input/number-form-input.component';
+export * from './shared/dynamic-form-inputs/register-dynamic-input-components';
+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';
 export * from './shared/pipes/channel-label.pipe';
 export * from './shared/pipes/currency-name.pipe';

+ 4 - 59
packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html

@@ -2,77 +2,22 @@
     <label for="basic" class="clr-control-label">{{ customField | customFieldLabel }}</label>
     <div class="clr-control-container">
         <div class="clr-input-wrapper">
-            <ng-container *ngIf="hasCustomControl; else standardControls">
-                <div #customComponentPlaceholder></div>
-            </ng-container>
-            <ng-template #standardControls>
-                <ng-container *ngTemplateOutlet="inputs"></ng-container>
-            </ng-template>
+            <ng-container *ngTemplateOutlet="inputs"></ng-container>
         </div>
     </div>
 </div>
 <vdr-form-field [label]="customField | customFieldLabel" [for]="customField.name" *ngIf="!compact">
-    <ng-container *ngIf="hasCustomControl; else standardControls">
-        <div #customComponentPlaceholder></div>
-    </ng-container>
-    <ng-template #standardControls>
-        <ng-container *ngTemplateOutlet="inputs"></ng-container>
-    </ng-template>
+    <ng-container *ngTemplateOutlet="inputs"></ng-container>
 </vdr-form-field>
+
 <ng-template #inputs>
     <ng-container [formGroup]="formGroup">
         <vdr-dynamic-form-input
             [formControlName]="customField.name"
             [readonly]="readonly || customField.readonly"
             [control]="formGroup.get(customField.name)"
-            [def]="customField"
+            [def]="getFieldDefinition()"
         >
         </vdr-dynamic-form-input>
     </ng-container>
-    <!--<input
-        *ngIf="isTextInput"
-        type="text"
-        [id]="customField.name"
-        [pattern]="$any(customField).pattern"
-        [formControl]="formGroup.get(customField.name)"
-        [readonly]="readonly || customField.readonly"
-    />
-    <select
-        *ngIf="isSelectInput"
-        clrSelect
-        [formControl]="formGroup.get(customField.name)"
-        [disabled]="readonly || customField.readonly"
-    >
-        <option *ngFor="let option of stringOptions" [value]="option.value">
-            {{ option | customFieldLabel }}
-        </option>
-    </select>
-    <input
-        *ngIf="customField.type === 'int' || customField.type === 'float'"
-        type="number"
-        [id]="customField.name"
-        [min]="min"
-        [max]="max"
-        [step]="step"
-        [formControl]="formGroup.get(customField.name)"
-        [readonly]="readonly || customField.readonly"
-    />
-    <clr-toggle-wrapper *ngIf="customField.type === 'boolean'">
-        <input
-            type="checkbox"
-            clrToggle
-            [id]="customField.name"
-            [formControl]="formGroup.get(customField.name)"
-            [attr.disabled]="readonly || customField.readonly"
-        />
-    </clr-toggle-wrapper>
-    <vdr-datetime-picker
-        *ngIf="customField.type === 'datetime'"
-        [id]="customField.name"
-        [formControl]="formGroup.get(customField.name)"
-        [min]="min"
-        [max]="max"
-        [readonly]="readonly || customField.readonly"
-    >
-    </vdr-datetime-picker>-->
 </ng-template>

+ 33 - 45
packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts

@@ -9,7 +9,8 @@ import {
 } from '@angular/core';
 import { FormControl, FormGroup } from '@angular/forms';
 
-import { CustomFieldsFragment } from '../../../common/generated-types';
+import { InputComponentConfig } from '../../../common/component-registry-types';
+import { CustomFieldConfig, CustomFieldsFragment } from '../../../common/generated-types';
 import { DataService } from '../../../data/providers/data.service';
 import {
     CustomFieldComponentService,
@@ -26,7 +27,7 @@ import {
     templateUrl: './custom-field-control.component.html',
     styleUrls: ['./custom-field-control.component.scss'],
 })
-export class CustomFieldControlComponent implements OnInit, AfterViewInit {
+export class CustomFieldControlComponent {
     @Input() entityName: CustomFieldEntityName;
     @Input('customFieldsFormGroup') formGroup: FormGroup;
     @Input() customField: CustomFieldsFragment;
@@ -43,56 +44,43 @@ export class CustomFieldControlComponent implements OnInit, AfterViewInit {
         private customFieldComponentService: CustomFieldComponentService,
     ) {}
 
-    ngOnInit(): void {
-        this.customComponentFactory = this.customFieldComponentService.getCustomFieldComponent(
+    getFieldDefinition(): CustomFieldConfig & { ui?: InputComponentConfig } {
+        const config: CustomFieldsFragment & { ui?: InputComponentConfig } = {
+            ...this.customField,
+        };
+        const id = this.customFieldComponentService.customFieldComponentExists(
             this.entityName,
             this.customField.name,
         );
-        this.hasCustomControl = !!this.customComponentFactory;
-    }
-
-    ngAfterViewInit(): void {
-        if (this.customComponentFactory) {
-            const customComponentRef = this.customComponentPlaceholder.createComponent(
-                this.customComponentFactory,
-            );
-            customComponentRef.instance.customFieldConfig = this.customField;
-            customComponentRef.instance.formControl = this.formGroup.get(
-                this.customField.name,
-            ) as FormControl;
-        }
-    }
-
-    get min(): string | number | undefined | null {
-        switch (this.customField.__typename) {
-            case 'IntCustomFieldConfig':
-                return this.customField.intMin;
-            case 'FloatCustomFieldConfig':
-                return this.customField.floatMin;
-            case 'DateTimeCustomFieldConfig':
-                return this.customField.datetimeMin;
+        if (id) {
+            config.ui = { component: id };
         }
-    }
-
-    get max(): string | number | undefined | null {
-        switch (this.customField.__typename) {
-            case 'IntCustomFieldConfig':
-                return this.customField.intMax;
-            case 'FloatCustomFieldConfig':
-                return this.customField.floatMax;
-            case 'DateTimeCustomFieldConfig':
-                return this.customField.datetimeMax;
-        }
-    }
-
-    get step(): string | number | undefined | null {
-        switch (this.customField.__typename) {
+        switch (config.__typename) {
             case 'IntCustomFieldConfig':
-                return this.customField.intStep;
+                return {
+                    ...config,
+                    min: config.intMin,
+                    max: config.intMax,
+                    step: config.intStep,
+                };
             case 'FloatCustomFieldConfig':
-                return this.customField.floatStep;
+                return {
+                    ...config,
+                    min: config.floatMin,
+                    max: config.floatMax,
+                    step: config.floatStep,
+                };
             case 'DateTimeCustomFieldConfig':
-                return this.customField.datetimeStep;
+                return {
+                    ...config,
+                    min: config.datetimeMin,
+                    max: config.datetimeMax,
+                    step: config.datetimeStep,
+                };
+            default:
+                return {
+                    ...config,
+                };
         }
     }
 }

+ 3 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.ts

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 
 import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
 
@@ -10,7 +11,8 @@ import { FormInputComponent, InputComponentConfig } from '../../../common/compon
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class BooleanFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'boolean-form-input';
     readonly: boolean;
     formControl: FormControl;
-    config: InputComponentConfig;
+    config: DefaultFormComponentConfig<'boolean-form-input'>;
 }

+ 3 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/currency-form-input/currency-form-input.component.ts

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 import { Observable } from 'rxjs';
 
 import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
@@ -13,10 +14,11 @@ import { DataService } from '../../../data/providers/data.service';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class CurrencyFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'currency-form-input';
     @Input() readonly: boolean;
     formControl: FormControl;
     currencyCode$: Observable<CurrencyCode>;
-    config: InputComponentConfig;
+    config: DefaultFormComponentConfig<'currency-form-input'>;
 
     constructor(private dataService: DataService) {
         this.currencyCode$ = this.dataService.settings

+ 1 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/date-form-input/date-form-input.component.html

@@ -2,6 +2,7 @@
     [formControl]="formControl"
     [min]="config.min"
     [max]="config.max"
+    [yearRange]="config.yearRange"
     [readonly]="readonly"
 >
 </vdr-datetime-picker>

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

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 
 import { FormInputComponent } from '../../../common/component-registry-types';
 
@@ -10,11 +11,8 @@ import { FormInputComponent } from '../../../common/component-registry-types';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DateFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'date-form-input';
     @Input() readonly: boolean;
     formControl: FormControl;
-    config: {
-        component: string;
-        min: string;
-        max: string;
-    };
+    config: DefaultFormComponentConfig<'date-form-input'>;
 }

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

@@ -20,17 +20,18 @@ import {
     ViewContainerRef,
 } from '@angular/core';
 import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
-import { CustomFieldConfig, getConfigArgValue, getDefaultConfigArgSingleValue } from '@vendure/admin-ui/core';
-import { omit } from '@vendure/common/lib/omit';
-import { ConfigArgType } from '@vendure/common/lib/shared-types';
+import { ConfigArgType, CustomFieldType, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 import { assertNever } from '@vendure/common/lib/shared-utils';
 import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone';
 import { Subject, Subscription } from 'rxjs';
 import { switchMap, take, takeUntil } from 'rxjs/operators';
 
-import { CustomFieldType } from '../../../../../../../../common/src/shared-types';
-import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
-import { ConfigArgDefinition } from '../../../common/generated-types';
+import { FormInputComponent } from '../../../common/component-registry-types';
+import { ConfigArgDefinition, CustomFieldConfig } from '../../../common/generated-types';
+import {
+    getConfigArgValue,
+    getDefaultConfigArgSingleValue,
+} from '../../../common/utilities/configurable-operation-utils';
 import { ComponentRegistryService } from '../../../providers/component-registry/component-registry.service';
 
 type InputListItem = {
@@ -279,8 +280,10 @@ export class DynamicFormInputComponent
         this.changeDetectorRef.markForCheck();
     }
 
-    private getInputComponentConfig(argDef: ConfigArgDefinition | CustomFieldConfig): InputComponentConfig {
-        if (this.isConfigArgDef(argDef) && argDef?.ui?.component) {
+    private getInputComponentConfig(
+        argDef: ConfigArgDefinition | CustomFieldConfig,
+    ): { component: DefaultFormComponentId } {
+        if (this.hasUiConfig(argDef) && argDef.ui.component) {
             return argDef.ui;
         }
         const type = argDef?.type as ConfigArgType | CustomFieldType;
@@ -300,7 +303,7 @@ export class DynamicFormInputComponent
             case 'datetime':
                 return { component: 'date-form-input' };
             case 'ID':
-                return { component: 'string-form-input' };
+                return { component: 'text-form-input' };
             default:
                 assertNever(type);
         }
@@ -309,4 +312,8 @@ export class DynamicFormInputComponent
     private isConfigArgDef(def: ConfigArgDefinition | CustomFieldConfig): def is ConfigArgDefinition {
         return (def as ConfigArgDefinition)?.__typename === 'ConfigArgDefinition';
     }
+
+    private hasUiConfig(def: unknown): def is { ui: { component: string } } {
+        return typeof def === 'object' && typeof (def as any)?.ui?.component === 'string';
+    }
 }

+ 4 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/facet-value-form-input/facet-value-form-input.component.ts

@@ -1,10 +1,12 @@
 import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 import { FormControl } from '@angular/forms';
-import { DataService, FacetWithValues } from '@vendure/admin-ui/core';
+import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 import { Observable } from 'rxjs';
 import { shareReplay } from 'rxjs/operators';
 
 import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
+import { FacetWithValues } from '../../../common/generated-types';
+import { DataService } from '../../../data/providers/data.service';
 
 @Component({
     selector: 'vdr-facet-value-form-input',
@@ -13,6 +15,7 @@ import { FormInputComponent, InputComponentConfig } from '../../../common/compon
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class FacetValueFormInputComponent implements FormInputComponent, OnInit {
+    static readonly id: DefaultFormComponentId = 'facet-value-form-input';
     readonly isListInput = true;
     readonly: boolean;
     formControl: FormControl;

+ 8 - 1
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/number-form-input/number-form-input.component.html

@@ -1,3 +1,10 @@
 <vdr-affixed-input [suffix]="config?.suffix" [prefix]="config?.prefix">
-    <input type="number" [readonly]="readonly" [formControl]="formControl" />
+    <input
+        type="number"
+        [readonly]="readonly"
+        [min]="config?.min"
+        [max]="config?.max"
+        [step]="config?.step"
+        [formControl]="formControl"
+    />
 </vdr-affixed-input>

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

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 
 import { FormInputComponent } from '../../../common/component-registry-types';
 
@@ -10,11 +11,8 @@ import { FormInputComponent } from '../../../common/component-registry-types';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class NumberFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'number-form-input';
     @Input() readonly: boolean;
     formControl: FormControl;
-    config: {
-        component: string;
-        prefix?: string;
-        suffix?: string;
-    };
+    config: DefaultFormComponentConfig<'number-form-input'>;
 }

+ 94 - 0
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/register-dynamic-input-components.ts

@@ -0,0 +1,94 @@
+import { APP_INITIALIZER, FactoryProvider, Provider, Type } from '@angular/core';
+
+import { FormInputComponent } from '../../common/component-registry-types';
+import { ComponentRegistryService } from '../../providers/component-registry/component-registry.service';
+import {
+    CustomFieldComponentService,
+    CustomFieldControl,
+    CustomFieldEntityName,
+} from '../../providers/custom-field-component/custom-field-component.service';
+
+import { BooleanFormInputComponent } from './boolean-form-input/boolean-form-input.component';
+import { CurrencyFormInputComponent } from './currency-form-input/currency-form-input.component';
+import { DateFormInputComponent } from './date-form-input/date-form-input.component';
+import { FacetValueFormInputComponent } from './facet-value-form-input/facet-value-form-input.component';
+import { NumberFormInputComponent } from './number-form-input/number-form-input.component';
+import { SelectFormInputComponent } from './select-form-input/select-form-input.component';
+import { TextFormInputComponent } from './text-form-input/text-form-input.component';
+
+export const defaultFormInputs = [
+    BooleanFormInputComponent,
+    CurrencyFormInputComponent,
+    DateFormInputComponent,
+    FacetValueFormInputComponent,
+    NumberFormInputComponent,
+    SelectFormInputComponent,
+    TextFormInputComponent,
+];
+
+/**
+ * @description
+ * Registers a custom FormInputComponent which can be used to control the argument inputs
+ * of a {@link ConfigurableOperationDef} (e.g. CollectionFilter, ShippingMethod etc)
+ *
+ * @example
+ * ```TypeScript
+ * \@NgModule({
+ *   imports: [SharedModule],
+ *   declarations: [MyCustomFieldControl],
+ *   providers: [
+ *       registerFormInputComponent('my-custom-input', MyCustomFieldControl),
+ *   ],
+ * })
+ * export class MyUiExtensionModule {}
+ * ```
+ */
+export function registerFormInputComponent(id: string, component: Type<FormInputComponent>): FactoryProvider {
+    return {
+        provide: APP_INITIALIZER,
+        multi: true,
+        useFactory: (registry: ComponentRegistryService) => () => {
+            registry.registerInputComponent(id, component);
+        },
+        deps: [ComponentRegistryService],
+    };
+}
+
+/**
+ * @description
+ * Registers a custom component to act as the form input control for the given custom field.
+ * This should be used in the NgModule `providers` array of your ui extension module.
+ *
+ * @example
+ * ```TypeScript
+ * \@NgModule({
+ *   imports: [SharedModule],
+ *   declarations: [MyCustomFieldControl],
+ *   providers: [
+ *       registerCustomFieldComponent('Product', 'someCustomField', MyCustomFieldControl),
+ *   ],
+ * })
+ * export class MyUiExtensionModule {}
+ * ```
+ */
+export function registerCustomFieldComponent(
+    entity: CustomFieldEntityName,
+    fieldName: string,
+    component: Type<CustomFieldControl>,
+): FactoryProvider {
+    return {
+        provide: APP_INITIALIZER,
+        multi: true,
+        useFactory: (customFieldComponentService: CustomFieldComponentService) => () => {
+            customFieldComponentService.registerCustomFieldComponent(entity, fieldName, component);
+        },
+        deps: [CustomFieldComponentService],
+    };
+}
+
+/**
+ * Registers the default form input components.
+ */
+export function registerDefaultFormInputs(): FactoryProvider[] {
+    return defaultFormInputs.map(cmp => registerFormInputComponent(cmp.id, cmp));
+}

+ 3 - 4
packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/select-form-input/select-form-input.component.ts

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentConfig, DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 
 import { FormInputComponent } from '../../../common/component-registry-types';
 
@@ -10,10 +11,8 @@ import { FormInputComponent } from '../../../common/component-registry-types';
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class SelectFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'select-form-input';
     @Input() readonly: boolean;
     formControl: FormControl;
-    config: {
-        component: string;
-        options: Array<{ value: string; label?: string }>;
-    };
+    config: DefaultFormComponentConfig<'select-form-input'>;
 }

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

@@ -1,5 +1,6 @@
 import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { FormControl } from '@angular/forms';
+import { DefaultFormComponentId } from '@vendure/common/lib/shared-types';
 
 import { FormInputComponent, InputComponentConfig } from '../../../common/component-registry-types';
 
@@ -10,6 +11,7 @@ import { FormInputComponent, InputComponentConfig } from '../../../common/compon
     changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class TextFormInputComponent implements FormInputComponent {
+    static readonly id: DefaultFormComponentId = 'text-form-input';
     readonly: boolean;
     formControl: FormControl;
     config: InputComponentConfig;

+ 1 - 0
packages/admin-ui/src/lib/order/src/public_api.ts

@@ -13,6 +13,7 @@ export * from './components/order-process-graph/constants';
 export * from './components/order-process-graph/order-process-edge.component';
 export * from './components/order-process-graph/order-process-graph.component';
 export * from './components/order-process-graph/order-process-node.component';
+export * from './components/order-process-graph/types';
 export * from './components/order-process-graph-dialog/order-process-graph-dialog.component';
 export * from './components/payment-detail/payment-detail.component';
 export * from './components/payment-state-label/payment-state-label.component';