From e72f0b3ef161ccccd777084f7a99d3f7c8ae669a Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 29 Jul 2020 13:05:01 +0200 Subject: [PATCH] feat(admin-ui): Implement list types for custom fields Relates to #416 --- packages/admin-ui/i18n-coverage.json | 18 +- .../lib/core/src/common/generated-types.ts | 19 ++- .../utilities/configurable-operation-utils.ts | 5 +- .../create-updated-translatable.spec.ts | 18 +- .../utilities/interpolate-description.spec.ts | 24 ++- .../utilities/interpolate-description.ts | 4 +- .../data/definitions/settings-definitions.ts | 1 + .../src/data/utils/add-custom-fields.spec.ts | 7 +- .../remove-readonly-custom-fields.spec.ts | 14 +- .../custom-field-component.service.ts | 2 +- .../custom-field-control.component.html | 14 +- .../custom-field-control.component.ts | 38 ++--- .../datetime-picker.component.ts | 2 +- .../datetime-picker.service.ts | 2 +- .../dropdown/dropdown-menu.component.ts | 4 +- .../shared/directives/disabled.directive.ts | 2 +- .../boolean-form-input.component.html | 2 +- .../dynamic-form-input.component.html | 4 +- .../dynamic-form-input.component.scss | 28 ++- .../dynamic-form-input.component.ts | 161 ++++++++++++------ .../src/lib/static/i18n-messages/de.json | 2 + .../src/lib/static/i18n-messages/en.json | 4 +- .../src/lib/static/i18n-messages/es.json | 2 + .../src/lib/static/i18n-messages/pl.json | 2 + .../src/lib/static/i18n-messages/zh_Hans.json | 2 + .../src/lib/static/i18n-messages/zh_Hant.json | 2 + 26 files changed, 241 insertions(+), 142 deletions(-) diff --git a/packages/admin-ui/i18n-coverage.json b/packages/admin-ui/i18n-coverage.json index ec0e90af15..2aff0c73c6 100644 --- a/packages/admin-ui/i18n-coverage.json +++ b/packages/admin-ui/i18n-coverage.json @@ -1,34 +1,34 @@ { - "generatedOn": "2020-07-14T09:36:35.279Z", - "lastCommit": "49c2ad4d53c15d4e25ba7795183084f60194d653", + "generatedOn": "2020-07-28T15:41:10.262Z", + "lastCommit": "2f4760e74e7b14caf171772e03c3095212eb17bc", "translationStatus": { "de": { - "tokenCount": 659, + "tokenCount": 661, "translatedCount": 609, "percentage": 92 }, "en": { - "tokenCount": 659, - "translatedCount": 659, + "tokenCount": 661, + "translatedCount": 660, "percentage": 100 }, "es": { - "tokenCount": 659, + "tokenCount": 661, "translatedCount": 467, "percentage": 71 }, "pl": { - "tokenCount": 659, + "tokenCount": 661, "translatedCount": 566, "percentage": 86 }, "zh_Hans": { - "tokenCount": 659, + "tokenCount": 661, "translatedCount": 550, "percentage": 83 }, "zh_Hant": { - "tokenCount": 659, + "tokenCount": 661, "translatedCount": 550, "percentage": 83 } diff --git a/packages/admin-ui/src/lib/core/src/common/generated-types.ts b/packages/admin-ui/src/lib/core/src/common/generated-types.ts index 4b14b88d42..3b1ecf14b9 100644 --- a/packages/admin-ui/src/lib/core/src/common/generated-types.ts +++ b/packages/admin-ui/src/lib/core/src/common/generated-types.ts @@ -187,6 +187,7 @@ export type BooleanCustomFieldConfig = CustomField & { __typename?: 'BooleanCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -1053,6 +1054,7 @@ export type CustomerSortParameter = { export type CustomField = { name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -1100,6 +1102,7 @@ export type DateTimeCustomFieldConfig = CustomField & { __typename?: 'DateTimeCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -1225,6 +1228,7 @@ export type FloatCustomFieldConfig = CustomField & { __typename?: 'FloatCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -1334,6 +1338,7 @@ export type IntCustomFieldConfig = CustomField & { __typename?: 'IntCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -1744,6 +1749,7 @@ export type LocaleStringCustomFieldConfig = CustomField & { __typename?: 'LocaleStringCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; label?: Maybe>; description?: Maybe>; readonly?: Maybe; @@ -3503,6 +3509,7 @@ export type StringCustomFieldConfig = CustomField & { __typename?: 'StringCustomFieldConfig'; name: Scalars['String']; type: Scalars['String']; + list: Scalars['Boolean']; length?: Maybe; label?: Maybe>; description?: Maybe>; @@ -6187,7 +6194,7 @@ export type UpdateGlobalSettingsMutation = ( type CustomFieldConfig_StringCustomFieldConfig_Fragment = ( { __typename?: 'StringCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe @@ -6199,7 +6206,7 @@ type CustomFieldConfig_StringCustomFieldConfig_Fragment = ( type CustomFieldConfig_LocaleStringCustomFieldConfig_Fragment = ( { __typename?: 'LocaleStringCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe @@ -6211,7 +6218,7 @@ type CustomFieldConfig_LocaleStringCustomFieldConfig_Fragment = ( type CustomFieldConfig_IntCustomFieldConfig_Fragment = ( { __typename?: 'IntCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe @@ -6223,7 +6230,7 @@ type CustomFieldConfig_IntCustomFieldConfig_Fragment = ( type CustomFieldConfig_FloatCustomFieldConfig_Fragment = ( { __typename?: 'FloatCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe @@ -6235,7 +6242,7 @@ type CustomFieldConfig_FloatCustomFieldConfig_Fragment = ( type CustomFieldConfig_BooleanCustomFieldConfig_Fragment = ( { __typename?: 'BooleanCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe @@ -6247,7 +6254,7 @@ type CustomFieldConfig_BooleanCustomFieldConfig_Fragment = ( type CustomFieldConfig_DateTimeCustomFieldConfig_Fragment = ( { __typename?: 'DateTimeCustomFieldConfig' } - & Pick + & Pick & { description?: Maybe diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts index ad8fc541da..6eac2cc155 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts @@ -1,4 +1,4 @@ -import { ConfigArgType } from '@vendure/common/lib/shared-types'; +import { ConfigArgType, CustomFieldType } from '@vendure/common/lib/shared-types'; import { assertNever } from '@vendure/common/lib/shared-utils'; import { ConfigArgDefinition } from '../generated-types'; @@ -27,7 +27,7 @@ export function getDefaultConfigArgValue(arg: ConfigArgDefinition): any { return arg.list ? [] : getDefaultConfigArgSingleValue(arg.type as ConfigArgType); } -export function getDefaultConfigArgSingleValue(type: ConfigArgType): any { +export function getDefaultConfigArgSingleValue(type: ConfigArgType | CustomFieldType): any { switch (type) { case 'boolean': return 'false'; @@ -37,6 +37,7 @@ export function getDefaultConfigArgSingleValue(type: ConfigArgType): any { case 'ID': return ''; case 'string': + case 'localeString': return ''; case 'datetime': return new Date(); diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts b/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts index 90a01adaac..ada7712dc4 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/create-updated-translatable.spec.ts @@ -113,8 +113,8 @@ describe('createUpdatedTranslatable()', () => { it('updates custom fields correctly', () => { const customFieldConfig: CustomFieldConfig[] = [ - { name: 'available', type: 'boolean' }, - { name: 'shortName', type: 'localeString' }, + { name: 'available', type: 'boolean', list: false }, + { name: 'shortName', type: 'localeString', list: false }, ]; product.customFields = { available: true, @@ -151,8 +151,8 @@ describe('createUpdatedTranslatable()', () => { it('updates custom fields when none initially exists', () => { const customFieldConfig: CustomFieldConfig[] = [ - { name: 'available', type: 'boolean' }, - { name: 'shortName', type: 'localeString' }, + { name: 'available', type: 'boolean', list: false }, + { name: 'shortName', type: 'localeString', list: false }, ]; const formValue = { @@ -184,11 +184,11 @@ describe('createUpdatedTranslatable()', () => { it('coerces empty customFields to correct type', () => { const customFieldConfig: CustomFieldConfig[] = [ - { name: 'a', type: 'boolean' }, - { name: 'b', type: 'int' }, - { name: 'c', type: 'float' }, - { name: 'd', type: 'datetime' }, - { name: 'e', type: 'string' }, + { name: 'a', type: 'boolean', list: false }, + { name: 'b', type: 'int', list: false }, + { name: 'c', type: 'float', list: false }, + { name: 'd', type: 'datetime', list: false }, + { name: 'e', type: 'string', list: false }, ]; const formValue = { diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.spec.ts b/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.spec.ts index e5929d0a53..f45f25be7e 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.spec.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.spec.ts @@ -5,7 +5,7 @@ import { interpolateDescription } from './interpolate-description'; describe('interpolateDescription()', () => { it('works for single argument', () => { const operation: Partial = { - args: [{ name: 'foo', type: 'string' }], + args: [{ name: 'foo', type: 'string', list: false }], description: 'The value is { foo }', }; const result = interpolateDescription(operation as any, { foo: 'val' }); @@ -15,7 +15,10 @@ describe('interpolateDescription()', () => { it('works for multiple arguments', () => { const operation: Partial = { - args: [{ name: 'foo', type: 'string' }, { name: 'bar', type: 'string' }], + args: [ + { name: 'foo', type: 'string', list: false }, + { name: 'bar', type: 'string', list: false }, + ], description: 'The value is { foo } and { bar }', }; const result = interpolateDescription(operation as any, { foo: 'val1', bar: 'val2' }); @@ -25,7 +28,7 @@ describe('interpolateDescription()', () => { it('is case-insensitive', () => { const operation: Partial = { - args: [{ name: 'foo', type: 'string' }], + args: [{ name: 'foo', type: 'string', list: false }], description: 'The value is { FOo }', }; const result = interpolateDescription(operation as any, { foo: 'val' }); @@ -35,7 +38,10 @@ describe('interpolateDescription()', () => { it('ignores whitespaces in interpolation', () => { const operation: Partial = { - args: [{ name: 'foo', type: 'string' }, { name: 'bar', type: 'string' }], + args: [ + { name: 'foo', type: 'string', list: false }, + { name: 'bar', type: 'string', list: false }, + ], description: 'The value is {foo} and { bar }', }; const result = interpolateDescription(operation as any, { foo: 'val1', bar: 'val2' }); @@ -43,9 +49,9 @@ describe('interpolateDescription()', () => { expect(result).toBe('The value is val1 and val2'); }); - it('formats money as a decimal', () => { + it('formats currency-form-input value as a decimal', () => { const operation: Partial = { - args: [{ name: 'price', type: 'int', config: { inputType: 'money' } }], + args: [{ name: 'price', type: 'int', list: false, ui: { component: 'currency-form-input' } }], description: 'The price is { price }', }; const result = interpolateDescription(operation as any, { price: 1234 }); @@ -55,7 +61,7 @@ describe('interpolateDescription()', () => { it('formats Date object as human-readable', () => { const operation: Partial = { - args: [{ name: 'date', type: 'datetime' }], + args: [{ name: 'date', type: 'datetime', list: false }], description: 'The date is { date }', }; const date = new Date('2017-09-15 00:00:00'); @@ -66,7 +72,7 @@ describe('interpolateDescription()', () => { it('formats date string object as human-readable', () => { const operation: Partial = { - args: [{ name: 'date', type: 'datetime' }], + args: [{ name: 'date', type: 'datetime', list: false }], description: 'The date is { date }', }; const date = '2017-09-15'; @@ -77,7 +83,7 @@ describe('interpolateDescription()', () => { it('correctly interprets falsy-looking values', () => { const operation: Partial = { - args: [{ name: 'foo', type: 'int' }], + args: [{ name: 'foo', type: 'int', list: false }], description: 'The value is { foo }', }; const result = interpolateDescription(operation as any, { foo: 0 }); diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.ts b/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.ts index 725a1b193a..45df9f6794 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/interpolate-description.ts @@ -19,9 +19,9 @@ export function interpolateDescription( } let formatted = value; const argDef = operation.args.find(arg => arg.name === normalizedArgName); - /*if (argDef && argDef.type === 'int' && argDef.config && argDef.config.inputType === 'money') { + if (argDef && argDef.type === 'int' && argDef.ui && argDef.ui.component === 'currency-form-input') { formatted = value / 100; - }*/ + } if (argDef && argDef.type === 'datetime' && value instanceof Date) { formatted = value.toLocaleDateString(); } diff --git a/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts b/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts index ec66a182c0..e69118448b 100644 --- a/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts +++ b/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts @@ -442,6 +442,7 @@ export const CUSTOM_FIELD_CONFIG_FRAGMENT = gql` fragment CustomFieldConfig on CustomField { name type + list description { languageCode value diff --git a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts index c48875faad..fc8b2a9702 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/add-custom-fields.spec.ts @@ -144,7 +144,10 @@ describe('addCustomFields()', () => { it('Adds customFields to Product fragment', () => { const customFieldsConfig: Partial = { - Product: [{ name: 'custom1', type: 'string' }, { name: 'custom2', type: 'boolean' }], + Product: [ + { name: 'custom1', type: 'string', list: false }, + { name: 'custom2', type: 'boolean', list: false }, + ], }; const result = addCustomFields(documentNode, customFieldsConfig as CustomFields); @@ -158,7 +161,7 @@ describe('addCustomFields()', () => { it('Adds customFields to Product translations', () => { const customFieldsConfig: Partial = { - Product: [{ name: 'customLocaleString', type: 'localeString' }], + Product: [{ name: 'customLocaleString', type: 'localeString', list: false }], }; const result = addCustomFields(documentNode, customFieldsConfig as CustomFields); diff --git a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts index d8d93b3ea7..3959277590 100644 --- a/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts +++ b/packages/admin-ui/src/lib/core/src/data/utils/remove-readonly-custom-fields.spec.ts @@ -5,8 +5,8 @@ import { removeReadonlyCustomFields } from './remove-readonly-custom-fields'; describe('removeReadonlyCustomFields', () => { it('readonly field and writable field', () => { const config: CustomFieldConfig[] = [ - { name: 'weight', type: 'int' }, - { name: 'rating', type: 'float', readonly: true }, + { name: 'weight', type: 'int', list: false }, + { name: 'rating', type: 'float', readonly: true, list: false }, ]; const entity = { id: 1, @@ -28,7 +28,7 @@ describe('removeReadonlyCustomFields', () => { }); it('single readonly field', () => { - const config: CustomFieldConfig[] = [{ name: 'rating', type: 'float', readonly: true }]; + const config: CustomFieldConfig[] = [{ name: 'rating', type: 'float', readonly: true, list: false }]; const entity = { id: 1, name: 'test', @@ -46,7 +46,9 @@ describe('removeReadonlyCustomFields', () => { }); it('readonly field in translation', () => { - const config: CustomFieldConfig[] = [{ name: 'alias', type: 'localeString', readonly: true }]; + const config: CustomFieldConfig[] = [ + { name: 'alias', type: 'localeString', readonly: true, list: false }, + ]; const entity = { id: 1, name: 'test', @@ -63,8 +65,8 @@ describe('removeReadonlyCustomFields', () => { it('wrapped in an input object', () => { const config: CustomFieldConfig[] = [ - { name: 'weight', type: 'int' }, - { name: 'rating', type: 'float', readonly: true }, + { name: 'weight', type: 'int', list: false }, + { name: 'rating', type: 'float', readonly: true, list: false }, ]; const entity = { input: { diff --git a/packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts b/packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts index fa142c1186..41764a046e 100644 --- a/packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/custom-field-component/custom-field-component.service.ts @@ -2,7 +2,6 @@ import { APP_INITIALIZER, ComponentFactory, ComponentFactoryResolver, - ComponentRef, Injectable, Injector, Provider, @@ -11,6 +10,7 @@ import { FormControl } from '@angular/forms'; import { Type } from '@vendure/common/lib/shared-types'; import { CustomFields, CustomFieldsFragment } from '../../common/generated-types'; + export type CustomFieldConfigType = CustomFieldsFragment; export interface CustomFieldControl { diff --git a/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html b/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html index 57445d3595..e5e873289d 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.html @@ -20,7 +20,16 @@ - + + + + diff --git a/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts index 3703dd6928..d0468a6146 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/custom-field-control/custom-field-control.component.ts @@ -1,9 +1,21 @@ -import { AfterViewInit, Component, ComponentFactory, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import { + AfterViewInit, + Component, + ComponentFactory, + Input, + OnInit, + ViewChild, + ViewContainerRef, +} from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { CustomFieldsFragment } from '../../../common/generated-types'; import { DataService } from '../../../data/providers/data.service'; -import { CustomFieldComponentService, CustomFieldControl, CustomFieldEntityName } from '../../../providers/custom-field-component/custom-field-component.service'; +import { + CustomFieldComponentService, + CustomFieldControl, + CustomFieldEntityName, +} from '../../../providers/custom-field-component/custom-field-component.service'; /** * This component renders the appropriate type of form input control based @@ -51,28 +63,6 @@ export class CustomFieldControlComponent implements OnInit, AfterViewInit { } } - get isTextInput(): boolean { - if (this.customField.__typename === 'StringCustomFieldConfig') { - return !this.customField.options; - } else { - return this.customField.__typename === 'LocaleStringCustomFieldConfig'; - } - } - - get isSelectInput(): boolean { - if (this.customField.__typename === 'StringCustomFieldConfig') { - return !!this.customField.options; - } - return false; - } - - get stringOptions() { - if (this.customField.__typename === 'StringCustomFieldConfig') { - return this.customField.options || []; - } - return []; - } - get min(): string | number | undefined | null { switch (this.customField.__typename) { case 'IntCustomFieldConfig': diff --git a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.ts index 5afcc77ce3..016517d7ef 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.component.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { DropdownComponent } from '../dropdown/dropdown.component'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts index 21c39a69de..65fc7fa81d 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts @@ -46,7 +46,7 @@ export class DatetimePickerService { selectDatetime(date: Date | string | dayjs.Dayjs | null) { let viewingValue: dayjs.Dayjs; let selectedValue: dayjs.Dayjs | null = null; - if (date == null) { + if (date == null || date === '') { viewingValue = dayjs(); } else { viewingValue = dayjs(date); diff --git a/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts index b07cbaf9a1..1546bb712d 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/dropdown/dropdown-menu.component.ts @@ -86,7 +86,9 @@ export class DropdownMenuComponent implements AfterViewInit, OnInit, OnDestroy { } ngOnDestroy(): void { - this.overlayRef.dispose(); + if (this.overlayRef) { + this.overlayRef.dispose(); + } if (this.backdropClickSub) { this.backdropClickSub.unsubscribe(); } diff --git a/packages/admin-ui/src/lib/core/src/shared/directives/disabled.directive.ts b/packages/admin-ui/src/lib/core/src/shared/directives/disabled.directive.ts index f32d54639b..f50f82dd70 100644 --- a/packages/admin-ui/src/lib/core/src/shared/directives/disabled.directive.ts +++ b/packages/admin-ui/src/lib/core/src/shared/directives/disabled.directive.ts @@ -13,7 +13,7 @@ export class DisabledDirective { if (!this.formControlName || !this.formControlName.control) { return; } - if (val === false) { + if (!!val === false) { this.formControlName.control.enable({ emitEvent: false }); } else { this.formControlName.control.disable({ emitEvent: false }); diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.html b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.html index 22e30a8385..90f9f9f49e 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/boolean-form-input/boolean-form-input.component.html @@ -3,6 +3,6 @@ type="checkbox" clrCheckbox [formControl]="formControl" - [vdrDisabled]="readonly" + [vdrDisabled]="!!readonly" /> diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.html b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.html index 79540f8c2b..a42ccb51ea 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.html +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.html @@ -5,7 +5,7 @@
-
@@ -14,7 +14,7 @@
diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.scss b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.scss index 9214f1ef5c..a08b5bb35e 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.scss @@ -1,27 +1,39 @@ -@import "variables"; +@import 'variables'; + +.list-container { + border: 1px solid $color-grey-300; + border-radius: 3px; + padding: 12px; +} .list-item-row { + font-size: 13px; display: flex; align-items: center; + margin: 3px 0; } + .drag-placeholder { - min-height: 120px; - background-color: $color-grey-300; transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } .cdk-drag-preview { - box-sizing: border-box; + font-size: 13px; + background-color: $color-grey-100; + opacity: 0.8; border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12); + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); } .cdk-drag-placeholder { - opacity: 0; + opacity: 0.1; } .cdk-drag-animating { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } + +.cdk-drop-list-dragging .list-item-row:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); +} diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts index e99f78aa2b..0e4ae37aa5 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/dynamic-form-input/dynamic-form-input.component.ts @@ -20,13 +20,15 @@ import { ViewContainerRef, } from '@angular/core'; import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { getConfigArgValue, getDefaultConfigArgSingleValue } from '@vendure/admin-ui/core'; +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 { assertNever } from '@vendure/common/lib/shared-utils'; import { simpleDeepClone } from '@vendure/common/lib/simple-deep-clone'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +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 { ComponentRegistryService } from '../../../providers/component-registry/component-registry.service'; @@ -55,7 +57,7 @@ type InputListItem = { }) export class DynamicFormInputComponent implements OnInit, OnChanges, AfterViewInit, OnDestroy, ControlValueAccessor { - @Input() def: ConfigArgDefinition; + @Input() def: ConfigArgDefinition | CustomFieldConfig; @Input() readonly: boolean; @Input() control: FormControl; @ViewChild('single', { read: ViewContainerRef }) singleViewContainer: ViewContainerRef; @@ -68,6 +70,7 @@ export class DynamicFormInputComponent private componentType: Type; private onChange: (val: any) => void; private onTouch: () => void; + private renderList$ = new Subject(); private destroy$ = new Subject(); constructor( @@ -78,11 +81,21 @@ export class DynamicFormInputComponent ) {} ngOnInit() { - const componentType = this.componentRegistryService.getInputComponent( - this.getInputComponentConfig(this.def).component, - ); + const componentId = this.getInputComponentConfig(this.def).component; + const componentType = this.componentRegistryService.getInputComponent(componentId); if (componentType) { this.componentType = componentType; + } else { + // tslint:disable-next-line:no-console + console.error( + `No form input component registered with the id "${componentId}". Using the default input instead.`, + ); + const defaultComponentType = this.componentRegistryService.getInputComponent( + this.getInputComponentConfig({ ...this.def, ui: undefined } as any).component, + ); + if (defaultComponentType) { + this.componentType = defaultComponentType; + } } } @@ -108,44 +121,51 @@ export class DynamicFormInputComponent this.control, ); } else { - const arrayValue = Array.isArray(this.control.value) - ? this.control.value - : !!this.control.value - ? [this.control.value] - : []; - this.listItems = arrayValue.map( - value => - ({ - id: this.listId++, - control: new FormControl(getConfigArgValue(value)), - } as InputListItem), - ); - let firstRenderHasOccurred = false; + let formArraySub: Subscription | undefined; const renderListInputs = (viewContainerRefs: QueryList) => { - viewContainerRefs.forEach((ref, i) => { - const listItem = this.listItems[i]; - if (!this.listFormArray.controls.includes(listItem.control)) { - this.listFormArray.push(listItem.control); - listItem.componentRef = this.renderInputComponent(factory, ref, listItem.control); + if (viewContainerRefs.length) { + if (formArraySub) { + formArraySub.unsubscribe(); } - }); - firstRenderHasOccurred = true; + this.listFormArray = new FormArray([]); + this.listItems.forEach(i => i.componentRef?.destroy()); + viewContainerRefs.forEach((ref, i) => { + const listItem = this.listItems?.[i]; + if (listItem) { + this.listFormArray.push(listItem.control); + listItem.componentRef = this.renderInputComponent( + factory, + ref, + listItem.control, + ); + } + }); + + formArraySub = this.listFormArray.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(val => { + this.control.markAsTouched(); + this.control.markAsDirty(); + this.onChange(val); + this.control.patchValue(val, { emitEvent: false }); + }); + } }; + // initial render this.listItemContainers.changes - .pipe(takeUntil(this.destroy$)) - .subscribe((refs: QueryList) => { - renderListInputs(refs); - }); + .pipe(take(1)) + .subscribe(val => renderListInputs(this.listItemContainers)); - this.listFormArray.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(val => { - if (firstRenderHasOccurred) { - this.control.markAsTouched(); - this.control.markAsDirty(); - this.onChange(val); - } - this.control.patchValue(val, { emitEvent: false }); - }); + // render on changes to the list + this.renderList$ + .pipe( + switchMap(() => this.listItemContainers.changes.pipe(take(1))), + takeUntil(this.destroy$), + ) + .subscribe(() => { + renderListInputs(this.listItemContainers); + }); } } setTimeout(() => this.changeDetectorRef.markForCheck()); @@ -171,7 +191,7 @@ export class DynamicFormInputComponent private updateBindings(changes: SimpleChanges, componentRef: ComponentRef) { if ('def' in changes) { - componentRef.instance.config = this.def.ui; + componentRef.instance.config = this.isConfigArgDef(this.def) ? this.def.ui : this.def; } if ('readonly' in changes) { componentRef.instance.readonly = this.readonly; @@ -184,23 +204,33 @@ export class DynamicFormInputComponent } addListItem() { + if (!this.listItems) { + this.listItems = []; + } this.listItems.push({ id: this.listId++, control: new FormControl(getDefaultConfigArgSingleValue(this.def.type as ConfigArgType)), }); + this.renderList$.next(); } moveListItem(event: CdkDragDrop) { - moveItemInArray(this.listItems, event.previousIndex, event.currentIndex); - this.listFormArray.removeAt(event.previousIndex); - this.listFormArray.insert(event.currentIndex, event.item.data.control); + if (this.listItems) { + moveItemInArray(this.listItems, event.previousIndex, event.currentIndex); + this.listFormArray.removeAt(event.previousIndex); + this.listFormArray.insert(event.currentIndex, event.item.data.control); + this.renderList$.next(); + } } removeListItem(item: InputListItem) { - const index = this.listItems.findIndex(i => i === item); - item.componentRef?.destroy(); - this.listFormArray.removeAt(index); - this.listItems = this.listItems.filter(i => i !== item); + if (this.listItems) { + const index = this.listItems.findIndex(i => i === item); + item.componentRef?.destroy(); + this.listFormArray.removeAt(index); + this.listItems = this.listItems.filter(i => i !== item); + this.renderList$.next(); + } } private renderInputComponent( @@ -210,7 +240,7 @@ export class DynamicFormInputComponent ) { const componentRef = viewContainerRef.createComponent(factory); const { instance } = componentRef; - instance.config = simpleDeepClone(this.def.ui); + instance.config = simpleDeepClone(this.isConfigArgDef(this.def) ? this.def.ui : this.def); instance.formControl = formControl; instance.readonly = this.readonly; componentRef.injector.get(ChangeDetectorRef).markForCheck(); @@ -226,17 +256,38 @@ export class DynamicFormInputComponent } writeValue(obj: any): void { - /* empty */ + if (Array.isArray(obj)) { + if (obj.length === this.listItems.length) { + obj.forEach((value, index) => { + const control = this.listItems[index]?.control; + control.patchValue(getConfigArgValue(value), { emitEvent: false }); + }); + } else { + this.listItems = obj.map( + value => + ({ + id: this.listId++, + control: new FormControl(getConfigArgValue(value)), + } as InputListItem), + ); + this.renderList$.next(); + } + } else { + this.listItems = []; + this.renderList$.next(); + } + this.changeDetectorRef.markForCheck(); } - private getInputComponentConfig(argDef: ConfigArgDefinition): InputComponentConfig { - if (argDef?.ui?.component) { + private getInputComponentConfig(argDef: ConfigArgDefinition | CustomFieldConfig): InputComponentConfig { + if (this.isConfigArgDef(argDef) && argDef?.ui?.component) { return argDef.ui; } - const type = argDef?.type as ConfigArgType; + const type = argDef?.type as ConfigArgType | CustomFieldType; switch (type) { case 'string': - if (argDef.ui?.options) { + case 'localeString': + if (this.isConfigArgDef(argDef) && argDef.ui?.options) { return { component: 'select-form-input' }; } else { return { component: 'text-form-input' }; @@ -254,4 +305,8 @@ export class DynamicFormInputComponent assertNever(type); } } + + private isConfigArgDef(def: ConfigArgDefinition | CustomFieldConfig): def is ConfigArgDefinition { + return (def as ConfigArgDefinition)?.__typename === 'ConfigArgDefinition'; + } } diff --git a/packages/admin-ui/src/lib/static/i18n-messages/de.json b/packages/admin-ui/src/lib/static/i18n-messages/de.json index c940bb6397..3459f59956 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/de.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/de.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "Aktionen", + "add-item-to-list": "", "add-new-variants": "{count, plural, one {1 Variante} other {{count} Varianten}} hinzufügen", "add-note": "", "available-languages": "Verfügbare Sprachen", @@ -190,6 +191,7 @@ "public": "Öffentlich", "remember-me": "Logindaten merken", "remove": "Entfernen", + "remove-item-from-list": "", "results-count": "{ count } {count, plural, one {Ergebnis} other {Ergebnisse}}", "select": "Auswählen...", "select-display-language": "Anzeigesprache wählen", diff --git a/packages/admin-ui/src/lib/static/i18n-messages/en.json b/packages/admin-ui/src/lib/static/i18n-messages/en.json index 93d1f56faa..6cd9fb0a79 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/en.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/en.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "Actions", + "add-item-to-list": "Add item to list", "add-new-variants": "Add {count, plural, one {1 variant} other {{count} variants}}", "add-note": "Add note", "available-languages": "Available languages", @@ -190,6 +191,7 @@ "public": "Public", "remember-me": "Remember me", "remove": "Remove", + "remove-item-from-list": "Remove item from list", "results-count": "{ count } {count, plural, one {result} other {results}}", "select": "Select...", "select-display-language": "Select display language", @@ -688,4 +690,4 @@ "job-result": "Job result", "job-state": "Job state" } -} \ No newline at end of file +} diff --git a/packages/admin-ui/src/lib/static/i18n-messages/es.json b/packages/admin-ui/src/lib/static/i18n-messages/es.json index 029fc15705..2c0de76a3d 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/es.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/es.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "Acciones", + "add-item-to-list": "", "add-new-variants": "", "add-note": "Añadir nota", "available-languages": "Idiomas disponibles", @@ -190,6 +191,7 @@ "public": "Público", "remember-me": "Recordarme", "remove": "Borrar", + "remove-item-from-list": "", "results-count": "", "select": "Seleccionar...", "select-display-language": "Seleccionar idioma de interfaz", diff --git a/packages/admin-ui/src/lib/static/i18n-messages/pl.json b/packages/admin-ui/src/lib/static/i18n-messages/pl.json index 5629cc01e4..21aea2f7d6 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/pl.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/pl.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "Akcje", + "add-item-to-list": "", "add-new-variants": "Dodaj {count, plural, one {1 wariant} other {{count} wariantów}}", "add-note": "", "available-languages": "Dostępne języki", @@ -190,6 +191,7 @@ "public": "Publiczne", "remember-me": "Zapamiętaj mnie", "remove": "Usuń", + "remove-item-from-list": "", "results-count": "{ count } {count, plural, one {wynik} other {wyników}}", "select": "Wybrano...", "select-display-language": "Wybierz język", diff --git a/packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json b/packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json index 58b2ef878a..0cdcceac40 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/zh_Hans.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "操作", + "add-item-to-list": "", "add-new-variants": "添加{count}个商品规格", "add-note": "", "available-languages": "可用语言", @@ -190,6 +191,7 @@ "public": "公开", "remember-me": "记住我", "remove": "删除", + "remove-item-from-list": "", "results-count": "{count, plural, 0{无} other {{count}个过滤结果}}", "select": "选择...", "select-display-language": "选择显示语言", diff --git a/packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json b/packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json index 423a3439dd..84c9e00153 100644 --- a/packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json +++ b/packages/admin-ui/src/lib/static/i18n-messages/zh_Hant.json @@ -134,6 +134,7 @@ "common": { "ID": "ID", "actions": "操作", + "add-item-to-list": "", "add-new-variants": "新增{count}個商品規格", "add-note": "", "available-languages": "可用語言", @@ -190,6 +191,7 @@ "public": "公開", "remember-me": "記住登入帳號", "remove": "移除", + "remove-item-from-list": "", "results-count": "{count, plural, 0{無} other {{count}個篩選結果}}", "select": "選擇...", "select-display-language": "選擇顯示語言",