From 19489a0035648bf8d6249273a8e645225816d2bf Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 22 Sep 2020 15:06:08 -1000 Subject: [PATCH 01/15] feat: Add additional fields to UIOptions --- .../src/components/fields/AdditionalField.tsx | 29 ++++++++ .../src/components/fields/FieldSets.tsx | 13 ++-- .../components/fields/ObjectArrayField.tsx | 5 +- .../src/components/fields/ObjectField.tsx | 47 ++++++++---- .../fields/__tests__/FieldSets.test.tsx | 30 ++++++-- .../fields/__tests__/ObjectField.test.tsx | 11 +++ .../src/components/fields/index.ts | 2 +- .../src/utils/__tests__/getFieldSets.test.ts | 72 +++++++++++++++---- .../__tests__/getOrderedProperties.test.ts | 32 +++++++++ .../adaptive-form/src/utils/getFieldSets.ts | 41 ----------- .../adaptive-form/src/utils/getFieldsets.ts | 57 +++++++++++++++ .../src/utils/getOrderedProperties.ts | 23 ++++-- .../packages/adaptive-form/src/utils/index.ts | 2 +- .../src/utils/resolveFieldWidget.ts | 4 +- .../extension-client/src/types/formSchema.ts | 15 +++- .../src/Fields/AdaptiveDialogField.tsx | 17 ----- .../schema-editor/src/formSchema.ts | 4 +- .../ui-plugins/schema-editor/src/uiOptions.ts | 2 +- .../src/BeginSkillDialogField.tsx | 19 ----- .../select-skill-dialog/src/ComboBoxField.tsx | 40 ++++------- .../src/SelectSkillDialogField.tsx | 17 ++--- .../src/SkillEndpointField.tsx | 13 +--- .../src/__tests__/SelectSkillDialog.test.tsx | 11 +-- .../src/__tests__/SkillEndpointField.test.tsx | 5 +- .../select-skill-dialog/src/index.ts | 23 ++++-- 25 files changed, 335 insertions(+), 199 deletions(-) create mode 100644 Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx delete mode 100644 Composer/packages/adaptive-form/src/utils/getFieldSets.ts create mode 100644 Composer/packages/adaptive-form/src/utils/getFieldsets.ts delete mode 100644 Composer/packages/ui-plugins/schema-editor/src/Fields/AdaptiveDialogField.tsx delete mode 100644 Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx diff --git a/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx b/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx new file mode 100644 index 0000000000..91eadc5fd4 --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React from 'react'; +import { FieldProps, FieldWidget } from '@bfc/extension-client'; + +import { FieldLabel } from '../FieldLabel'; +import { schemaField } from '../SchemaField'; + +interface AdditionalFieldProps extends FieldProps { + name: string; + field: FieldWidget; + helpLink?: string; + description?: string; + label?: string | false; + required?: boolean; +} + +export const AdditionalField: React.FC = (props) => { + const { id, depth, description, field: Field, helpLink, label, required } = props; + + return ( +
+ + +
+ ); +}; diff --git a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx index a608c7d4dc..ff794b0a05 100644 --- a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx @@ -5,25 +5,22 @@ import React from 'react'; import { FieldProps } from '@bfc/extension-client'; import { CollapseField } from '../CollapseField'; -import { getFieldSets } from '../../utils'; +import { getFieldsets } from '../../utils'; import { ObjectField } from './ObjectField'; -export const FieldSets: React.FC> = (props) => { +export const Fieldsets: React.FC> = (props) => { const { schema, uiOptions: baseUiOptions, value } = props; - const { fieldSets: _, ...uiOptions } = baseUiOptions; - const fieldSets = getFieldSets(schema, baseUiOptions, value); + const fieldsets = getFieldsets(schema, baseUiOptions, value); return ( - {fieldSets.map(({ fields, schema, ...rest }, key) => ( - + {fieldsets.map(({ schema, uiOptions, title, defaultExpanded }, key) => ( + ))} ); }; - -export default FieldSets; diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx index 169c0a7ef9..fc97c20daa 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -80,7 +80,10 @@ const ObjectArrayField: React.FC> = (props) => { itemSchema && typeof itemSchema !== 'boolean' ? itemSchema : {}, uiOptions, value - ).filter((property) => Array.isArray(property) || !isPropertyHidden(uiOptions, value, property)); + ).filter( + (property) => + Array.isArray(property) || (typeof property !== 'object' && !isPropertyHidden(uiOptions, value, property)) + ) as (string | string[])[]; const stackArrayItems = useMemo(() => { const allOrderProps = orderedProperties.reduce((all: string[], prop: string | string[]) => { diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx index 1f7a9aad0f..abfba38247 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx @@ -6,8 +6,10 @@ import { FieldProps } from '@bfc/extension-client'; import { getOrderedProperties } from '../../utils'; import { FormRow } from '../FormRow'; +import { AdditionalField } from './AdditionalField'; + const ObjectField: React.FC> = function ObjectField(props) { - const { schema, uiOptions, depth, value, label, ...rest } = props; + const { definitions, schema, uiOptions, depth, value, label, onChange, ...rest } = props; if (!schema) { return null; @@ -31,18 +33,37 @@ const ObjectField: React.FC> = function ObjectField(props) { return ( - {orderedProperties.map((row) => ( - - ))} + {orderedProperties.map((row) => { + if (typeof row === 'string' || Array.isArray(row)) { + return ( + + ); + } else { + return ( + + ); + } + })} ); }; diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx index 6a95fb28d4..db2af962b1 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { render, fireEvent, act } from '@bfc/test-utils'; import assign from 'lodash/assign'; -import { FieldSets } from '../FieldSets'; +import { Fieldsets } from '../Fieldsets'; import { fieldProps } from './testUtils'; @@ -19,14 +19,14 @@ const schema = { function renderSubject(overrides = {}) { const props = assign({}, fieldProps(), overrides); - return render(); + return render(); } -describe('', () => { +describe('', () => { it('renders an object with two field sets', async () => { const onChange = jest.fn(); const uiOptions = { - fieldSets: [ + fieldsets: [ { title: 'set 1', fields: ['name'], @@ -56,4 +56,26 @@ describe('', () => { expect(onChange).toHaveBeenLastCalledWith({ city: 'Seattle' }); }); + + it('renders additional fields', async () => { + const uiOptions = { + additionalFields: [ + { name: 'additionalField', field: () =>
Additional Field
, label: 'Additional Field Label' }, + ], + fieldsets: [ + { + title: 'set 1', + fields: ['name'], + }, + { + title: 'set 2', + }, + ], + }; + + const { findByText } = renderSubject({ schema, uiOptions, value: {} }); + + await findByText('Additional Field'); + await findByText('Additional Field Label'); + }); }); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx index d32b77c474..cf21cef73f 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx @@ -56,6 +56,17 @@ describe.only('', () => { expect(getAllByTestId('FormRow')).toHaveLength(3); }); + it('renders additional fields', () => { + const uiOptions = { + additionalFields: [ + { name: 'additionalField', field: () =>
Additional Field
, label: 'Additional Field Label' }, + ], + }; + const { getByText } = renderSubject({ uiOptions, schema }); + getByText('Additional Field'); + getByText('Additional Field Label'); + }); + it('can edit a specific property', () => { const onChange = jest.fn(); const value = { diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index fdb9c1c115..0707ca70ff 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -4,7 +4,7 @@ export * from './ArrayField'; export * from './BooleanField'; export * from './EditableField'; export * from './ExpressionField/ExpressionField'; -export * from './FieldSets'; +export * from './Fieldsets'; export * from './IntentField'; export * from './JsonField'; export * from './NumberField'; diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts index bcc1f0cbe9..c6213b28fe 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts @@ -3,7 +3,7 @@ import { JSONSchema7 } from 'json-schema'; -import { getFieldSets } from '../getFieldSets'; +import { getFieldsets } from '../getFieldsets'; const schema = { properties: { @@ -18,13 +18,13 @@ const schema = { }, } as JSONSchema7; -describe('getFieldSets', () => { +describe('getFieldsets', () => { it('should return a single field set containing all the properties', () => { const uiOptions = { - fieldSets: [{ title: 'set1' }], + fieldsets: [{ title: 'set1' }], }; - const result = getFieldSets(schema, uiOptions, {}); + const result = getFieldsets(schema, uiOptions, {}); expect(result).toEqual([ expect.objectContaining({ @@ -47,7 +47,7 @@ describe('getFieldSets', () => { it('should return two sets', () => { const uiOptions = { - fieldSets: [ + fieldsets: [ { title: 'set1', fields: ['two', 'four', 'six'], @@ -59,7 +59,7 @@ describe('getFieldSets', () => { ], }; - const result = getFieldSets(schema, uiOptions, {}); + const result = getFieldsets(schema, uiOptions, {}); expect(result).toEqual([ expect.objectContaining({ @@ -88,33 +88,81 @@ describe('getFieldSets', () => { ]); }); + it('should include additional fields', () => { + const uiOptions: any = { + additionalFields: [{ name: 'additionalField', field: 'field' }], + fieldsets: [ + { + title: 'set1', + fields: ['two', 'four', 'six'], + }, + { + title: 'set2', + fields: ['*', 'additionalField'], + }, + ], + }; + + const result = getFieldsets(schema, uiOptions, {}); + + expect(result).toEqual([ + expect.objectContaining({ + fields: ['two', 'four', 'six'], + title: 'set1', + schema: { + properties: { + two: { type: 'string' }, + four: { type: 'object' }, + six: { type: 'object' }, + }, + }, + uiOptions: { additionalFields: [] }, + }), + expect.objectContaining({ + fields: ['one', 'three', 'five', 'seven', 'additionalField'], + title: 'set2', + schema: { + properties: { + one: { type: 'string' }, + three: { type: 'number' }, + five: { type: 'object' }, + seven: { type: 'boolean' }, + }, + }, + uiOptions: { + additionalFields: [{ name: 'additionalField', field: 'field' }], + }, + }), + ]); + }); + it('should throw an error for multiple wildcards', () => { const uiOptions = { - fieldSets: [{ title: 'set1', fields: ['two', '*', 'six'] }, { title: 'set2' }], + fieldsets: [{ title: 'set1', fields: ['two', '*', 'six'] }, { title: 'set2' }], }; - expect(() => getFieldSets(schema, uiOptions, {})).toThrow('multiple wildcards'); + expect(() => getFieldsets(schema, uiOptions, {})).toThrow('multiple wildcards'); }); it('should throw an error for missing fields', () => { const uiOptions = { - fieldSets: [ + fieldsets: [ { title: 'set1', fields: ['two', 'four', 'six'] }, { title: 'set2', fields: ['one'] }, ], }; - expect(() => getFieldSets(schema, uiOptions, {})).toThrow('missing fields'); + expect(() => getFieldsets(schema, uiOptions, {})).toThrow('missing fields'); }); it('should throw an error for duplicate fields', () => { const uiOptions = { - fieldSets: [ + fieldsets: [ { title: 'set1', fields: ['two', 'four', 'six'] }, { title: 'set2', fields: ['two', '*'] }, ], }; - expect(() => getFieldSets(schema, uiOptions, {})).toThrow('duplicate fields'); + expect(() => getFieldsets(schema, uiOptions, {})).toThrow('duplicate fields'); }); }); diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts index 8c4d4d950f..eb149eb2de 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts @@ -78,4 +78,36 @@ describe('getOrderedProperties', () => { 'multiple wildcards' ); }); + + it('throws an exception if additional field name already exists in the schema', () => { + const uiOptions = { additionalFields: [{ name: 'one', field: 'field' }] }; + expect(() => getOrderedProperties(schema, uiOptions, data)).toThrow( + 'additional field name already exists in schema' + ); + }); + + it('includes additional fields', () => { + const uiOptions = { additionalFields: [{ name: 'additionalField', field: 'field' }] }; + expect(getOrderedProperties(schema, uiOptions, data)).toEqual( + expect.arrayContaining([{ name: 'additionalField', field: 'field' }]) + ); + }); + + it('can include additional fields in the order', () => { + const uiOptions = { + additionalFields: [{ name: 'additionalField', field: 'field' }], + order: ['one', 'two', 'additionalField', '*'], + }; + + expect(getOrderedProperties(schema, uiOptions, data)).toEqual([ + 'one', + 'two', + { name: 'additionalField', field: 'field' }, + 'three', + 'four', + 'five', + 'six', + 'seven', + ]); + }); }); diff --git a/Composer/packages/adaptive-form/src/utils/getFieldSets.ts b/Composer/packages/adaptive-form/src/utils/getFieldSets.ts deleted file mode 100644 index 2ebf01bf32..0000000000 --- a/Composer/packages/adaptive-form/src/utils/getFieldSets.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { JSONSchema7, UIOptions } from '@bfc/extension-client'; -import formatMessage from 'format-message'; -import difference from 'lodash/difference'; -import flatten from 'lodash/flatten'; -import keys from 'lodash/keys'; -import pick from 'lodash/pick'; - -import { getHiddenProperties } from './getHiddenProperties'; - -export const getFieldSets = (schema: JSONSchema7, uiOptions: UIOptions, value: any) => { - const { fieldSets: baseFieldSets = [] } = uiOptions; - const { properties } = schema; - - const hiddenProperties = getHiddenProperties(uiOptions, value); - const allFields: string[] = keys(properties).filter( - (field) => !(field.startsWith('$') || hiddenProperties.has(field)) - ); - const fields: string[] = flatten(baseFieldSets.map(({ fields = ['*'] }) => fields)); - const restFields = difference(allFields, fields); - - if (fields.filter((field) => field === '*').length > 1) { - throw new Error(formatMessage('multiple wildcards')); - } else if (!fields.includes('*') && allFields.some((field) => !fields.includes(field))) { - throw new Error(formatMessage('missing fields')); - } else if (fields.length !== new Set(fields).size) { - throw new Error(formatMessage('duplicate fields')); - } - - return baseFieldSets.map(({ fields = ['*'], ...rest }) => { - const restIdx = fields.indexOf('*'); - - if (restIdx > -1) { - fields.splice(restIdx, 1, ...restFields); - } - - return { ...rest, fields, schema: { ...schema, properties: pick(schema.properties, fields) } as JSONSchema7 }; - }); -}; diff --git a/Composer/packages/adaptive-form/src/utils/getFieldsets.ts b/Composer/packages/adaptive-form/src/utils/getFieldsets.ts new file mode 100644 index 0000000000..252357c1fd --- /dev/null +++ b/Composer/packages/adaptive-form/src/utils/getFieldsets.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Fieldset, JSONSchema7, UIOptions } from '@bfc/extension-client'; +import formatMessage from 'format-message'; +import difference from 'lodash/difference'; +import flatten from 'lodash/flatten'; +import keys from 'lodash/keys'; +import pick from 'lodash/pick'; + +import { getHiddenProperties } from './getHiddenProperties'; + +interface FieldSetConfig extends Fieldset { + schema: JSONSchema7; + uiOptions: UIOptions; +} + +export const getFieldsets = (baseSchema: JSONSchema7, baseUiOptions: UIOptions, value: any): FieldSetConfig[] => { + const { additionalFields = [], fieldsets: baseFieldsets = [] } = baseUiOptions; + const { properties } = baseSchema; + + const hiddenProperties = getHiddenProperties(baseUiOptions, value); + const additionalFieldsNames = additionalFields.map(({ name }) => name); + const allFields: string[] = [ + ...keys(properties).filter((field) => !(field.startsWith('$') || hiddenProperties.has(field))), + ...additionalFieldsNames, + ]; + + const fields: string[] = flatten(baseFieldsets.map(({ fields = ['*'] }) => fields)); + const restFields = difference(allFields, fields); + + if (fields.filter((field) => field === '*').length > 1) { + throw new Error(formatMessage('multiple wildcards')); + } else if (!fields.includes('*') && allFields.some((field) => !fields.includes(field))) { + throw new Error(formatMessage('missing fields')); + } else if (fields.length !== new Set(fields).size) { + throw new Error(formatMessage('duplicate fields')); + } + + return baseFieldsets.map(({ fields = ['*'], ...rest }) => { + const restIdx = fields.indexOf('*'); + + if (restIdx > -1) { + fields.splice(restIdx, 1, ...restFields); + } + + const uiOptions = { + ...baseUiOptions, + additionalFields: additionalFields.filter(({ name }) => fields.includes(name)), + }; + delete uiOptions.fieldsets; + + const schema = { ...baseSchema, properties: pick(properties, fields) } as JSONSchema7; + + return { ...rest, fields, uiOptions, schema }; + }); +}; diff --git a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts index 9f330ae78f..6f5379cf62 100644 --- a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts +++ b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { UIOptions, JSONSchema7 } from '@bfc/extension-client'; +import { UIOptions, JSONSchema7, AdditionalField } from '@bfc/extension-client'; import cloneDeep from 'lodash/cloneDeep'; import formatMessage from 'format-message'; import { getHiddenProperties } from './getHiddenProperties'; -type OrderConfig = (string | [string, string])[]; +type OrderConfig = (string | [string, string] | AdditionalField)[]; export function getOrderedProperties( schema: JSONSchema7, @@ -15,9 +15,11 @@ export function getOrderedProperties( data: any ): OrderConfig { const uiOptions = cloneDeep(baseUiOptions); - const { order = ['*'] } = uiOptions; + const { additionalFields = [], order = ['*'] } = uiOptions; const hiddenFieldSet = getHiddenProperties(uiOptions, data); + const additionalFieldsNames = additionalFields.map(({ name }) => name); + const additionalFieldsNamesSet = new Set(additionalFieldsNames); const uiOrder = typeof order === 'function' ? order(data) : order || []; const orderedFieldSet = new Set(); const orderedFields = uiOrder.reduce((allFields, field) => { @@ -42,14 +44,14 @@ export function getOrderedProperties( allFields.push(...fieldTuple); } } else { - if (!hiddenFieldSet.has(field) && schema.properties?.[field]) { + if (!hiddenFieldSet.has(field) && (schema.properties?.[field] || additionalFieldsNamesSet.has(field))) { orderedFieldSet.add(field); allFields.push(field); } } return allFields; - }, [] as OrderConfig); + }, [] as (string | [string, string])[]); const allProperties = Object.keys(schema.properties ?? {}).filter( (p) => !p.startsWith('$') && !hiddenFieldSet.has(p) @@ -63,6 +65,8 @@ export function getOrderedProperties( errorMsg = formatMessage('no wildcard'); } else if (restIdx !== orderedFields.lastIndexOf('*')) { errorMsg = formatMessage('multiple wildcards'); + } else if (allProperties.some((property) => additionalFieldsNamesSet.has(property))) { + errorMsg = formatMessage('additional field name already exists in schema'); } if (errorMsg) { @@ -76,7 +80,7 @@ export function getOrderedProperties( } } - const restFields = Object.keys(schema.properties || {}).filter((p) => { + const restFields = [...Object.keys(schema.properties || {}), ...additionalFieldsNames].filter((p) => { return !orderedFieldSet.has(p) && !p.startsWith('$'); }); @@ -86,5 +90,10 @@ export function getOrderedProperties( orderedFields.splice(restIdx, 1, ...restFields); } - return orderedFields; + return orderedFields.map((field) => { + if (!Array.isArray(field) && additionalFieldsNamesSet.has(field)) { + return additionalFields.find(({ name }) => name === field) || field; + } + return field; + }); } diff --git a/Composer/packages/adaptive-form/src/utils/index.ts b/Composer/packages/adaptive-form/src/utils/index.ts index a83f95e3c4..645e1f3a16 100644 --- a/Composer/packages/adaptive-form/src/utils/index.ts +++ b/Composer/packages/adaptive-form/src/utils/index.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. export * from './arrayUtils'; -export * from './getFieldSets'; +export * from './getFieldsets'; export * from './getHiddenProperties'; export * from './getOrderedProperties'; export * from './getUIOptions'; diff --git a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts index 07d67b1134..73e86e2a38 100644 --- a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts +++ b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts @@ -72,8 +72,10 @@ export function resolveFieldWidget( case 'object': if (schema.additionalProperties) { return DefaultFields.OpenObjectField; + } else if (uiOptions?.fieldsets) { + return DefaultFields.Fieldsets; } else { - return uiOptions?.fieldSets ? DefaultFields.FieldSets : DefaultFields.ObjectField; + return DefaultFields.ObjectField; } } } diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index c07a175cb3..d06111edcc 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -9,13 +9,24 @@ type UIOptionValue = R | UIOptionFunc; // eslint-disable-next-line @typescript-eslint/no-explicit-any type UIOptionFunc = (data: D) => R; -interface FieldSet { +export interface AdditionalField { + name: string; + field: FieldWidget; + label?: string | false; + description?: string; + helpLink?: string; + required?: boolean; +} + +export interface Fieldset { title: string; fields?: string[]; defaultExpanded?: boolean; } export interface UIOptions { + /** Array of custom fields that are added to the form */ + additionalFields?: AdditionalField[]; /** Description override. */ description?: UIOptionValue; /** Field widget override. */ @@ -35,7 +46,7 @@ export interface UIOptions { */ hidden?: UIOptionValue; /** Organizes fields into collapsible sets */ - fieldSets?: FieldSet[]; + fieldsets?: Fieldset[]; /** Label override. */ label?: UIOptionValue; /** Set order of fields. Use * for all other fields. */ diff --git a/Composer/packages/ui-plugins/schema-editor/src/Fields/AdaptiveDialogField.tsx b/Composer/packages/ui-plugins/schema-editor/src/Fields/AdaptiveDialogField.tsx deleted file mode 100644 index d2a234c39c..0000000000 --- a/Composer/packages/ui-plugins/schema-editor/src/Fields/AdaptiveDialogField.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React from 'react'; -import { SchemaField } from '@bfc/adaptive-form'; -import { FieldProps } from '@bfc/extension-client'; - -import { SchemaEditorField } from './SchemaEditorField'; - -export const AdaptiveDialogField: React.FC = ({ uiOptions, ...rest }) => { - return ( - - - - - ); -}; diff --git a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts index 17aecf7aa0..cfd5959aa9 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts +++ b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts @@ -4,12 +4,12 @@ import { UISchema } from '@bfc/extension-client'; import { SDKKinds } from '@bfc/shared'; -import { AdaptiveDialogField } from './Fields/AdaptiveDialogField'; +import { SchemaEditorField } from './Fields/SchemaEditorField'; const uiSchema: UISchema = { [SDKKinds.AdaptiveDialog]: { form: { - field: AdaptiveDialogField, + additionalFields: [{ field: SchemaEditorField, name: 'schemaEditor' }], }, }, }; diff --git a/Composer/packages/ui-plugins/schema-editor/src/uiOptions.ts b/Composer/packages/ui-plugins/schema-editor/src/uiOptions.ts index 6f3c23e03a..1e0b26f298 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/uiOptions.ts +++ b/Composer/packages/ui-plugins/schema-editor/src/uiOptions.ts @@ -17,7 +17,7 @@ const objectSerializer = { export const uiOptions: UIOptions = { label: false, - fieldSets: [{ title: formatMessage('Dialog Interface') }], + fieldsets: [{ title: formatMessage('Dialog Interface') }], properties: { dialogValue: { serializer: objectSerializer, diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx deleted file mode 100644 index a2b919c38e..0000000000 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/BeginSkillDialogField.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React from 'react'; -import { FieldProps } from '@bfc/extension-client'; -import { ObjectField } from '@bfc/adaptive-form'; - -import { SelectSkillDialog } from './SelectSkillDialogField'; - -export const BeginSkillDialogField: React.FC = (props) => { - const { value, onChange } = props; - - return ( - - - - - ); -}; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx index ee526c4dab..01b71f58ae 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { ComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox'; -import { FieldLabel } from '@bfc/adaptive-form'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { ISelectableOption } from 'office-ui-fabric-react/lib/utilities/selectableOption'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; @@ -12,25 +11,13 @@ export const ADD_DIALOG = 'ADD_DIALOG'; interface ComboBoxFieldProps { comboboxTitle: string | null; - options: IComboBoxOption[]; - onChange: any; - required?: boolean; - description: string; id: string; - label: string; + options: IComboBoxOption[]; value: string; + onChange: any; } -export const ComboBoxField: React.FC = ({ - comboboxTitle, - description, - id, - label, - options, - value = '', - required, - onChange, -}) => { +export const ComboBoxField: React.FC = ({ comboboxTitle, id, options, value = '', onChange }) => { const onRenderOption: IRenderFunction = (option) => option ? (
@@ -44,18 +31,15 @@ export const ComboBoxField: React.FC = ({ ) : null; return ( - - - - + ); }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx index ef0cca7244..0890ac78d4 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx @@ -1,13 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -/** @jsx jsx */ -import { jsx } from '@emotion/core'; import React, { useState } from 'react'; import { IComboBoxOption, SelectableOptionMenuItemType } from 'office-ui-fabric-react/lib/ComboBox'; -import { useShellApi } from '@bfc/extension-client'; +import { FieldProps, useShellApi } from '@bfc/extension-client'; import formatMessage from 'format-message'; -import { schemaField } from '@bfc/adaptive-form'; import { getSkillNameFromSetting, Skill } from '@bfc/shared'; import { Link } from 'office-ui-fabric-react/lib/components/Link/Link'; @@ -24,11 +21,7 @@ export const settingReferences = (skillName: string) => ({ skillAppId: referBySettings(skillName, 'msAppId'), }); -export const SelectSkillDialog: React.FC<{ - value: any; - onChange: (value: any) => void; -}> = (props) => { - const { value, onChange } = props; +export const SelectSkillDialogField: React.FC = ({ value, onChange }) => { const { shellApi, skills = [] } = useShellApi(); const { addSkillDialog, displayManifestModal } = shellApi; const [comboboxTitle, setComboboxTitle] = useState(null); @@ -73,12 +66,10 @@ export const SelectSkillDialog: React.FC<{ }; return ( -
+ {formatMessage('Show skill manifest')} -
+ ); }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx index 3e21d71a9a..9f7d1bb3d4 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx @@ -6,15 +6,13 @@ import { jsx } from '@emotion/core'; import React, { useMemo } from 'react'; import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown'; import { FieldProps, useShellApi } from '@bfc/extension-client'; -import { FieldLabel } from '@bfc/adaptive-form'; import { getSkillNameFromSetting, Skill } from '@bfc/shared'; -export const SkillEndpointField: React.FC = (props) => { - const { description, label, required, uiOptions, value } = props; +export const SkillEndpointField: React.FC = ({ value }) => { const { shellApi, skillsSettings, skills = [] } = useShellApi(); const { updateSkillSetting } = shellApi; - const id = getSkillNameFromSetting(value); + const id = getSkillNameFromSetting(value.skillEndpoint); const skill = skills.find(({ id: skillId }) => skillId === id) || ({} as Skill); const { endpointUrl, msAppId } = skillsSettings[id] || {}; @@ -41,10 +39,5 @@ export const SkillEndpointField: React.FC = (props) => { } }; - return ( - - - - - ); + return ; }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx index f30715afc5..c811dad7f4 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { act, fireEvent, getAllByRole, render } from '@bfc/test-utils'; import { EditorExtension } from '@bfc/extension-client'; -import { SelectSkillDialog, settingReferences } from '../SelectSkillDialogField'; +import { SelectSkillDialogField, settingReferences } from '../SelectSkillDialogField'; import { skills } from './constants'; @@ -13,7 +13,7 @@ const renderSelectSkillDialog = ({ addSkillDialog = jest.fn(), onChange = jest.f const props = { value: {}, onChange, - }; + } as any; const shell = { addSkillDialog, @@ -25,7 +25,7 @@ const renderSelectSkillDialog = ({ addSkillDialog = jest.fn(), onChange = jest.f return render( - + ); }; @@ -60,9 +60,4 @@ describe('Select Skill Dialog', () => { expect(addSkillDialog).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ ...settingReferences('test-skill') }); }); - - it('should display label', async () => { - const { findByText } = renderSelectSkillDialog(); - await findByText('Skill Dialog Name'); - }); }); diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SkillEndpointField.test.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SkillEndpointField.test.tsx index 05a21d8cce..e0b2378b65 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SkillEndpointField.test.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SkillEndpointField.test.tsx @@ -14,9 +14,8 @@ import { skills } from './constants'; const projectId = '123.abc'; -const renderSkillEndpointField = ({ value = '', updateSkillSetting = jest.fn() } = {}) => { +const renderSkillEndpointField = ({ value = {}, updateSkillSetting = jest.fn() } = {}) => { const props = { - label: 'Skill endpoint', value, } as any; @@ -41,7 +40,7 @@ describe('Begin Skill Dialog', () => { const updateSkillSetting = jest.fn(); const { baseElement, findByRole } = renderSkillEndpointField({ updateSkillSetting, - value: `=settings.skill['${skills[0].id}'].endpointUrl`, + value: { skillEndpoint: `=settings.skill['${skills[0].id}'].endpointUrl` }, }); const listbox = await findByRole('listbox'); diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts index 3240a8bf28..80cb84d4a2 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts @@ -3,22 +3,31 @@ import { PluginConfig } from '@bfc/extension-client'; import { SDKKinds } from '@bfc/shared'; +import formatMessage from 'format-message'; -import { BeginSkillDialogField } from './BeginSkillDialogField'; +import { SelectSkillDialogField } from './SelectSkillDialogField'; import { SkillEndpointField } from './SkillEndpointField'; const config: PluginConfig = { uiSchema: { [SDKKinds.BeginSkill]: { form: { - order: ['skillEndpoint', '*', 'resultProperty', 'activityProcessed'], - hidden: ['botId', 'skillAppId', 'skillHostEndpoint'], - field: BeginSkillDialogField, - properties: { - skillEndpoint: { + additionalFields: [ + { + field: SelectSkillDialogField, + name: 'selectSkillDialog', + label: formatMessage('Skill Dialog Name'), + description: formatMessage('Name of skill dialog to call'), + }, + { field: SkillEndpointField, + name: 'selectSkillEndpoint', + label: formatMessage('Skill Endpoint'), + description: formatMessage('The /api/messages endpoint for the skill.'), }, - }, + ], + order: ['selectSkillDialog', 'selectSkillEndpoint', '*', 'resultProperty', 'activityProcessed'], + hidden: ['skillEndpoint', 'botId', 'skillAppId', 'skillHostEndpoint'], }, }, }, From ceaa846715940b83902c72d37157312c50b8303b Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 22 Sep 2020 17:06:42 -1000 Subject: [PATCH 02/15] update file reference --- .../src/components/fields/__tests__/FieldSets.test.tsx | 2 +- Composer/packages/adaptive-form/src/components/fields/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx index db2af962b1..1975ae1317 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { render, fireEvent, act } from '@bfc/test-utils'; import assign from 'lodash/assign'; -import { Fieldsets } from '../Fieldsets'; +import { Fieldsets } from '../FieldSets'; import { fieldProps } from './testUtils'; diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index 0707ca70ff..fdb9c1c115 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -4,7 +4,7 @@ export * from './ArrayField'; export * from './BooleanField'; export * from './EditableField'; export * from './ExpressionField/ExpressionField'; -export * from './Fieldsets'; +export * from './FieldSets'; export * from './IntentField'; export * from './JsonField'; export * from './NumberField'; From 68513aba0cdc10e2139c14f4c4169839bd0033c0 Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 22 Sep 2020 17:30:18 -1000 Subject: [PATCH 03/15] lint --- .../src/components/fields/FieldSets.tsx | 2 +- .../src/components/fields/PivotFieldsets.tsx | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx diff --git a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx index ff794b0a05..11153888de 100644 --- a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx @@ -17,7 +17,7 @@ export const Fieldsets: React.FC> = (props) => { return ( {fieldsets.map(({ schema, uiOptions, title, defaultExpanded }, key) => ( - + ))} diff --git a/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx b/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx new file mode 100644 index 0000000000..ac09ce7f44 --- /dev/null +++ b/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { useState } from 'react'; +import { FieldProps } from '@bfc/extension-client'; +import { IPivotStyles, Pivot, PivotItem, PivotLinkSize } from 'office-ui-fabric-react/lib/components/Pivot'; +import camelCase from 'lodash/camelCase'; + +import { getFieldsets } from '../../utils'; + +import { ObjectField } from './ObjectField'; + +const styles: { tabs: Partial } = { + tabs: { + root: { + display: 'flex', + }, + link: { + flex: 1, + }, + linkIsSelected: { + flex: 1, + }, + }, +}; + +export const PivotFieldsets: React.FC> = (props) => { + const { schema, uiOptions: baseUiOptions, value } = props; + const [focusedTab, setFocusedTab] = useState(); + const fieldsets = getFieldsets(schema, baseUiOptions, value); + + const handleTabChange = (item?: PivotItem) => { + if (item) { + setFocusedTab(item.props.itemKey); + // shellApi.onFocusSteps(focusedSteps, item.props.itemKey); + } + }; + + return ( +
+ + {fieldsets.map(({ schema, uiOptions, title }, key) => ( + + + + ))} + +
+ ); + + return ; +}; From c70e88aebdf41c223ccbde88872602ff4ec67a8b Mon Sep 17 00:00:00 2001 From: TJ Durnford Date: Tue, 22 Sep 2020 17:36:21 -1000 Subject: [PATCH 04/15] Delete PivotFieldsets.tsx --- .../src/components/fields/PivotFieldsets.tsx | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx diff --git a/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx b/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx deleted file mode 100644 index ac09ce7f44..0000000000 --- a/Composer/packages/adaptive-form/src/components/fields/PivotFieldsets.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React, { useState } from 'react'; -import { FieldProps } from '@bfc/extension-client'; -import { IPivotStyles, Pivot, PivotItem, PivotLinkSize } from 'office-ui-fabric-react/lib/components/Pivot'; -import camelCase from 'lodash/camelCase'; - -import { getFieldsets } from '../../utils'; - -import { ObjectField } from './ObjectField'; - -const styles: { tabs: Partial } = { - tabs: { - root: { - display: 'flex', - }, - link: { - flex: 1, - }, - linkIsSelected: { - flex: 1, - }, - }, -}; - -export const PivotFieldsets: React.FC> = (props) => { - const { schema, uiOptions: baseUiOptions, value } = props; - const [focusedTab, setFocusedTab] = useState(); - const fieldsets = getFieldsets(schema, baseUiOptions, value); - - const handleTabChange = (item?: PivotItem) => { - if (item) { - setFocusedTab(item.props.itemKey); - // shellApi.onFocusSteps(focusedSteps, item.props.itemKey); - } - }; - - return ( -
- - {fieldsets.map(({ schema, uiOptions, title }, key) => ( - - - - ))} - -
- ); - - return ; -}; From e5024b401391e0cc781f2934e600c9313f2f31d5 Mon Sep 17 00:00:00 2001 From: TJ Date: Wed, 23 Sep 2020 15:07:17 -1000 Subject: [PATCH 05/15] removed additionalFields option --- .../adaptive-form/src/components/FormRow.tsx | 18 ++++- .../src/components/__tests__/FormRow.test.tsx | 7 +- .../src/components/fields/AdditionalField.tsx | 25 +++---- .../src/components/fields/FieldSets.tsx | 4 +- .../components/fields/ObjectArrayField.tsx | 5 +- .../src/components/fields/ObjectField.tsx | 67 +++++-------------- .../fields/__tests__/FieldSets.test.tsx | 6 +- .../fields/__tests__/ObjectField.test.tsx | 17 +---- .../src/components/fields/index.ts | 1 + .../src/utils/__tests__/getFieldSets.test.ts | 20 ++++-- .../__tests__/getOrderedProperties.test.ts | 32 --------- .../adaptive-form/src/utils/getFieldsets.ts | 24 +++---- .../src/utils/getOrderedProperties.ts | 21 ++---- .../utils/getSchemaWithAdditionalFields.ts | 17 +++++ .../packages/adaptive-form/src/utils/index.ts | 1 + .../src/utils/resolveFieldWidget.ts | 4 ++ .../extension-client/src/types/formSchema.ts | 11 --- .../schema-editor/src/formSchema.ts | 6 +- .../src/SelectSkillDialogField.tsx | 2 +- .../src/SkillEndpointField.tsx | 2 +- .../select-skill-dialog/src/index.ts | 14 ++-- 21 files changed, 118 insertions(+), 186 deletions(-) create mode 100644 Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index 5114b4c8b2..a3d2e9cc5c 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -11,7 +11,7 @@ import { isPropertyHidden, resolvePropSchema } from '../utils'; import { SchemaField } from './SchemaField'; export interface FormRowProps extends Omit { - onChange: (field: string) => (data: any) => void; + onChange: (data: any) => void; row: string | [string, string]; } @@ -43,6 +43,18 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { const newUiOptions = (uiOptions.properties?.[field] as UIOptions) ?? {}; newUiOptions.intellisenseScopes = intellisenseScopes; + const handleChange = (data: any) => { + const newData = { ...value }; + + if (typeof data === 'undefined' || (typeof data === 'string' && data.length === 0)) { + delete newData[field]; + } else { + newData[field] = data; + } + + onChange(newData); + }; + return { id: `${id}.${field}`, schema: fieldSchema ?? {}, @@ -52,8 +64,8 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { rawErrors: rawErrors?.[field], required: required.includes(field), uiOptions: newUiOptions, - value: value && value[field], - onChange: onChange(field), + value: fieldSchema?.$role === 'additionalField' ? value : value && value[field], + onChange: fieldSchema?.$role === 'additionalField' ? onChange : handleChange, depth, definitions, transparentBorder, diff --git a/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx b/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx index 61289013d0..c494ac8f94 100644 --- a/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx +++ b/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx @@ -8,9 +8,8 @@ import { FormRow, FormRowProps, getRowProps } from '../FormRow'; jest.mock('../SchemaField', () => ({ SchemaField: () =>
})); -const fieldChangeMock = jest.fn(); const field: FormRowProps = { - onChange: jest.fn().mockReturnValue(fieldChangeMock), + onChange: jest.fn(), row: '', definitions: {}, depth: 0, @@ -78,7 +77,7 @@ describe('getRowProps', () => { }); it('binds the onChange to the field', () => { - expect(getRowProps(field, 'single').onChange).toEqual(fieldChangeMock); - expect(field.onChange).toHaveBeenCalledWith('single'); + expect(getRowProps(field, 'single').onChange('test')); + expect(field.onChange).toHaveBeenCalledWith({ single: 'test' }); }); }); diff --git a/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx b/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx index 91eadc5fd4..3fdb0cff1e 100644 --- a/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx @@ -3,27 +3,18 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React from 'react'; -import { FieldProps, FieldWidget } from '@bfc/extension-client'; +import { FieldProps } from '@bfc/extension-client'; import { FieldLabel } from '../FieldLabel'; -import { schemaField } from '../SchemaField'; -interface AdditionalFieldProps extends FieldProps { - name: string; - field: FieldWidget; - helpLink?: string; - description?: string; - label?: string | false; - required?: boolean; -} +export const AdditionalField: React.FC = (props) => { + const { id, description, label, required, uiOptions } = props; + const { field: Field, helpLink } = uiOptions; -export const AdditionalField: React.FC = (props) => { - const { id, depth, description, field: Field, helpLink, label, required } = props; - - return ( -
+ return Field ? ( + -
- ); + + ) : null; }; diff --git a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx index 11153888de..29472506b7 100644 --- a/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/FieldSets.tsx @@ -9,7 +9,7 @@ import { getFieldsets } from '../../utils'; import { ObjectField } from './ObjectField'; -export const Fieldsets: React.FC> = (props) => { +const Fieldsets: React.FC> = (props) => { const { schema, uiOptions: baseUiOptions, value } = props; const fieldsets = getFieldsets(schema, baseUiOptions, value); @@ -24,3 +24,5 @@ export const Fieldsets: React.FC> = (props) => { ); }; + +export { Fieldsets }; diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx index fc97c20daa..169c0a7ef9 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -80,10 +80,7 @@ const ObjectArrayField: React.FC> = (props) => { itemSchema && typeof itemSchema !== 'boolean' ? itemSchema : {}, uiOptions, value - ).filter( - (property) => - Array.isArray(property) || (typeof property !== 'object' && !isPropertyHidden(uiOptions, value, property)) - ) as (string | string[])[]; + ).filter((property) => Array.isArray(property) || !isPropertyHidden(uiOptions, value, property)); const stackArrayItems = useMemo(() => { const allOrderProps = orderedProperties.reduce((all: string[], prop: string | string[]) => { diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx index abfba38247..9ceb696ea4 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx @@ -3,67 +3,34 @@ import React from 'react'; import { FieldProps } from '@bfc/extension-client'; -import { getOrderedProperties } from '../../utils'; +import { getOrderedProperties, getSchemaWithAdditionalFields } from '../../utils'; import { FormRow } from '../FormRow'; -import { AdditionalField } from './AdditionalField'; - const ObjectField: React.FC> = function ObjectField(props) { - const { definitions, schema, uiOptions, depth, value, label, onChange, ...rest } = props; + const { definitions, schema: baseSchema, uiOptions, depth, value, label, onChange, ...rest } = props; - if (!schema) { + if (!baseSchema) { return null; } - const newDepth = depth + 1; - - const handleChange = (field: string) => (data: any) => { - const newData = { ...value }; - - if (typeof data === 'undefined' || (typeof data === 'string' && data.length === 0)) { - delete newData[field]; - } else { - newData[field] = data; - } - - props.onChange(newData); - }; - + const schema = getSchemaWithAdditionalFields(baseSchema, uiOptions); const orderedProperties = getOrderedProperties(schema, uiOptions, value); return ( - {orderedProperties.map((row) => { - if (typeof row === 'string' || Array.isArray(row)) { - return ( - - ); - } else { - return ( - - ); - } - })} + {orderedProperties.map((row) => ( + + ))} ); }; diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx index 1975ae1317..d3a7dbf85f 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx @@ -59,9 +59,9 @@ describe('', () => { it('renders additional fields', async () => { const uiOptions = { - additionalFields: [ - { name: 'additionalField', field: () =>
Additional Field
, label: 'Additional Field Label' }, - ], + properties: { + additionalField: { field: () =>
Additional Field
, label: 'Additional Field Label' }, + }, fieldsets: [ { title: 'set 1', diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx index cf21cef73f..e45d78ba2d 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectField.test.tsx @@ -16,9 +16,9 @@ jest.mock('../../FormRow', () => ({ { try { - onChange(row)(JSON.parse(e.target.value)); + onChange(JSON.parse(e.target.value)); } catch { - onChange(row)(e.target.value); + onChange(e.target.value); } }} /> @@ -56,17 +56,6 @@ describe.only('', () => { expect(getAllByTestId('FormRow')).toHaveLength(3); }); - it('renders additional fields', () => { - const uiOptions = { - additionalFields: [ - { name: 'additionalField', field: () =>
Additional Field
, label: 'Additional Field Label' }, - ], - }; - const { getByText } = renderSubject({ uiOptions, schema }); - getByText('Additional Field'); - getByText('Additional Field Label'); - }); - it('can edit a specific property', () => { const onChange = jest.fn(); const value = { @@ -77,6 +66,6 @@ describe.only('', () => { const { container } = renderSubject({ onChange, schema, value }); const input = container.querySelectorAll('input')[0]; fireEvent.change(input, { target: { value: 'new name' } }); - expect(onChange).toHaveBeenCalledWith({ name: 'new name', age: 21 }); + expect(onChange).toHaveBeenCalledWith('new name'); }); }); diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index 24246f3a97..177273c055 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +export * from './AdditionalField'; export * from './ArrayField'; export * from './BooleanField'; export * from './CustomRecognizerField'; diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts index c6213b28fe..b1fe477d21 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts @@ -20,7 +20,7 @@ const schema = { describe('getFieldsets', () => { it('should return a single field set containing all the properties', () => { - const uiOptions = { + const uiOptions: any = { fieldsets: [{ title: 'set1' }], }; @@ -46,7 +46,7 @@ describe('getFieldsets', () => { }); it('should return two sets', () => { - const uiOptions = { + const uiOptions: any = { fieldsets: [ { title: 'set1', @@ -90,7 +90,6 @@ describe('getFieldsets', () => { it('should include additional fields', () => { const uiOptions: any = { - additionalFields: [{ name: 'additionalField', field: 'field' }], fieldsets: [ { title: 'set1', @@ -101,6 +100,11 @@ describe('getFieldsets', () => { fields: ['*', 'additionalField'], }, ], + properties: { + additionalField: { + field: 'field', + }, + }, }; const result = getFieldsets(schema, uiOptions, {}); @@ -116,7 +120,10 @@ describe('getFieldsets', () => { six: { type: 'object' }, }, }, - uiOptions: { additionalFields: [] }, + uiOptions: { + order: ['two', 'four', 'six'], + properties: {}, + }, }), expect.objectContaining({ fields: ['one', 'three', 'five', 'seven', 'additionalField'], @@ -130,14 +137,15 @@ describe('getFieldsets', () => { }, }, uiOptions: { - additionalFields: [{ name: 'additionalField', field: 'field' }], + order: ['one', 'three', 'five', 'seven', 'additionalField'], + properties: { additionalField: { field: 'field' } }, }, }), ]); }); it('should throw an error for multiple wildcards', () => { - const uiOptions = { + const uiOptions: any = { fieldsets: [{ title: 'set1', fields: ['two', '*', 'six'] }, { title: 'set2' }], }; diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts index eb149eb2de..8c4d4d950f 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getOrderedProperties.test.ts @@ -78,36 +78,4 @@ describe('getOrderedProperties', () => { 'multiple wildcards' ); }); - - it('throws an exception if additional field name already exists in the schema', () => { - const uiOptions = { additionalFields: [{ name: 'one', field: 'field' }] }; - expect(() => getOrderedProperties(schema, uiOptions, data)).toThrow( - 'additional field name already exists in schema' - ); - }); - - it('includes additional fields', () => { - const uiOptions = { additionalFields: [{ name: 'additionalField', field: 'field' }] }; - expect(getOrderedProperties(schema, uiOptions, data)).toEqual( - expect.arrayContaining([{ name: 'additionalField', field: 'field' }]) - ); - }); - - it('can include additional fields in the order', () => { - const uiOptions = { - additionalFields: [{ name: 'additionalField', field: 'field' }], - order: ['one', 'two', 'additionalField', '*'], - }; - - expect(getOrderedProperties(schema, uiOptions, data)).toEqual([ - 'one', - 'two', - { name: 'additionalField', field: 'field' }, - 'three', - 'four', - 'five', - 'six', - 'seven', - ]); - }); }); diff --git a/Composer/packages/adaptive-form/src/utils/getFieldsets.ts b/Composer/packages/adaptive-form/src/utils/getFieldsets.ts index 252357c1fd..ac497a4448 100644 --- a/Composer/packages/adaptive-form/src/utils/getFieldsets.ts +++ b/Composer/packages/adaptive-form/src/utils/getFieldsets.ts @@ -5,10 +5,11 @@ import { Fieldset, JSONSchema7, UIOptions } from '@bfc/extension-client'; import formatMessage from 'format-message'; import difference from 'lodash/difference'; import flatten from 'lodash/flatten'; -import keys from 'lodash/keys'; import pick from 'lodash/pick'; +import pickBy from 'lodash/pickBy'; -import { getHiddenProperties } from './getHiddenProperties'; +import { getSchemaWithAdditionalFields } from './getSchemaWithAdditionalFields'; +import { getOrderedProperties } from './getOrderedProperties'; interface FieldSetConfig extends Fieldset { schema: JSONSchema7; @@ -16,22 +17,18 @@ interface FieldSetConfig extends Fieldset { } export const getFieldsets = (baseSchema: JSONSchema7, baseUiOptions: UIOptions, value: any): FieldSetConfig[] => { - const { additionalFields = [], fieldsets: baseFieldsets = [] } = baseUiOptions; + const { fieldsets: baseFieldsets = [] } = baseUiOptions; const { properties } = baseSchema; - const hiddenProperties = getHiddenProperties(baseUiOptions, value); - const additionalFieldsNames = additionalFields.map(({ name }) => name); - const allFields: string[] = [ - ...keys(properties).filter((field) => !(field.startsWith('$') || hiddenProperties.has(field))), - ...additionalFieldsNames, - ]; + const schema = getSchemaWithAdditionalFields(baseSchema, baseUiOptions); + const orderedFields = flatten(getOrderedProperties(schema, baseUiOptions, value)); const fields: string[] = flatten(baseFieldsets.map(({ fields = ['*'] }) => fields)); - const restFields = difference(allFields, fields); + const restFields = difference(orderedFields, fields); if (fields.filter((field) => field === '*').length > 1) { throw new Error(formatMessage('multiple wildcards')); - } else if (!fields.includes('*') && allFields.some((field) => !fields.includes(field))) { + } else if (!fields.includes('*') && orderedFields.some((field) => !fields.includes(field))) { throw new Error(formatMessage('missing fields')); } else if (fields.length !== new Set(fields).size) { throw new Error(formatMessage('duplicate fields')); @@ -44,10 +41,7 @@ export const getFieldsets = (baseSchema: JSONSchema7, baseUiOptions: UIOptions, fields.splice(restIdx, 1, ...restFields); } - const uiOptions = { - ...baseUiOptions, - additionalFields: additionalFields.filter(({ name }) => fields.includes(name)), - }; + const uiOptions = pickBy({ ...baseUiOptions, order: fields, properties: pick(baseUiOptions.properties, fields) }); delete uiOptions.fieldsets; const schema = { ...baseSchema, properties: pick(properties, fields) } as JSONSchema7; diff --git a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts index 6f5379cf62..4e9c4ae31e 100644 --- a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts +++ b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { UIOptions, JSONSchema7, AdditionalField } from '@bfc/extension-client'; +import { UIOptions, JSONSchema7 } from '@bfc/extension-client'; import cloneDeep from 'lodash/cloneDeep'; import formatMessage from 'format-message'; import { getHiddenProperties } from './getHiddenProperties'; -type OrderConfig = (string | [string, string] | AdditionalField)[]; +type OrderConfig = (string | [string, string])[]; export function getOrderedProperties( schema: JSONSchema7, @@ -15,11 +15,9 @@ export function getOrderedProperties( data: any ): OrderConfig { const uiOptions = cloneDeep(baseUiOptions); - const { additionalFields = [], order = ['*'] } = uiOptions; + const { order = ['*'] } = uiOptions; const hiddenFieldSet = getHiddenProperties(uiOptions, data); - const additionalFieldsNames = additionalFields.map(({ name }) => name); - const additionalFieldsNamesSet = new Set(additionalFieldsNames); const uiOrder = typeof order === 'function' ? order(data) : order || []; const orderedFieldSet = new Set(); const orderedFields = uiOrder.reduce((allFields, field) => { @@ -44,7 +42,7 @@ export function getOrderedProperties( allFields.push(...fieldTuple); } } else { - if (!hiddenFieldSet.has(field) && (schema.properties?.[field] || additionalFieldsNamesSet.has(field))) { + if (!hiddenFieldSet.has(field) && schema.properties?.[field]) { orderedFieldSet.add(field); allFields.push(field); } @@ -65,8 +63,6 @@ export function getOrderedProperties( errorMsg = formatMessage('no wildcard'); } else if (restIdx !== orderedFields.lastIndexOf('*')) { errorMsg = formatMessage('multiple wildcards'); - } else if (allProperties.some((property) => additionalFieldsNamesSet.has(property))) { - errorMsg = formatMessage('additional field name already exists in schema'); } if (errorMsg) { @@ -80,7 +76,7 @@ export function getOrderedProperties( } } - const restFields = [...Object.keys(schema.properties || {}), ...additionalFieldsNames].filter((p) => { + const restFields = Object.keys(schema.properties || {}).filter((p) => { return !orderedFieldSet.has(p) && !p.startsWith('$'); }); @@ -90,10 +86,5 @@ export function getOrderedProperties( orderedFields.splice(restIdx, 1, ...restFields); } - return orderedFields.map((field) => { - if (!Array.isArray(field) && additionalFieldsNamesSet.has(field)) { - return additionalFields.find(({ name }) => name === field) || field; - } - return field; - }); + return orderedFields; } diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts new file mode 100644 index 0000000000..fdd584df81 --- /dev/null +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { JSONSchema7, UIOptions } from '@bfc/extension-client'; +import difference from 'lodash/difference'; +import fromPairs from 'lodash/fromPairs'; +import keys from 'lodash/keys'; + +const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOptions): JSONSchema7 => { + const additionalFields = difference(keys(uiOptions.properties), keys(baseSchema.properties)); + const additionalPropertySchema = fromPairs(additionalFields.map((field) => [field, { $role: 'additionalField' }])); + + const properties = { ...baseSchema.properties, ...additionalPropertySchema }; + return { ...baseSchema, properties }; +}; + +export { getSchemaWithAdditionalFields }; diff --git a/Composer/packages/adaptive-form/src/utils/index.ts b/Composer/packages/adaptive-form/src/utils/index.ts index 645e1f3a16..81c3a29ca6 100644 --- a/Composer/packages/adaptive-form/src/utils/index.ts +++ b/Composer/packages/adaptive-form/src/utils/index.ts @@ -3,6 +3,7 @@ export * from './arrayUtils'; export * from './getFieldsets'; export * from './getHiddenProperties'; +export * from './getSchemaWithAdditionalFields'; export * from './getOrderedProperties'; export * from './getUIOptions'; export * from './getValueType'; diff --git a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts index 73e86e2a38..442b24b02a 100644 --- a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts +++ b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts @@ -17,6 +17,10 @@ export function resolveFieldWidget( ): FieldWidget { const FieldOverride = uiOptions?.field; + if (schema?.$role === 'additionalField') { + return DefaultFields.AdditionalField; + } + if (typeof FieldOverride === 'function') { return FieldOverride; } diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index befca71d9e..34596541ac 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -9,15 +9,6 @@ type UIOptionValue = R | UIOptionFunc; // eslint-disable-next-line @typescript-eslint/no-explicit-any type UIOptionFunc = (data: D) => R; -export interface AdditionalField { - name: string; - field: FieldWidget; - label?: string | false; - description?: string; - helpLink?: string; - required?: boolean; -} - export interface Fieldset { title: string; fields?: string[]; @@ -25,8 +16,6 @@ export interface Fieldset { } export interface UIOptions { - /** Array of custom fields that are added to the form */ - additionalFields?: AdditionalField[]; /** Description override. */ description?: UIOptionValue; /** Field widget override. */ diff --git a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts index cfd5959aa9..5f8cd00065 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts +++ b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts @@ -9,7 +9,11 @@ import { SchemaEditorField } from './Fields/SchemaEditorField'; const uiSchema: UISchema = { [SDKKinds.AdaptiveDialog]: { form: { - additionalFields: [{ field: SchemaEditorField, name: 'schemaEditor' }], + properties: { + schemaEditor: { + field: SchemaEditorField, + }, + }, }, }, }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx index 0890ac78d4..d55b4ab370 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx @@ -26,7 +26,7 @@ export const SelectSkillDialogField: React.FC = ({ value, onChange } const { addSkillDialog, displayManifestModal } = shellApi; const [comboboxTitle, setComboboxTitle] = useState(null); - const skillId = getSkillNameFromSetting(value.skillEndpoint); + const skillId = getSkillNameFromSetting(value?.skillEndpoint); const { content, manifestUrl, name } = skills.find(({ id }) => id === skillId) || ({} as Skill); const options: IComboBoxOption[] = skills.map(({ id, name }) => ({ diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx index 9f7d1bb3d4..3b9f9f2e60 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SkillEndpointField.tsx @@ -12,7 +12,7 @@ export const SkillEndpointField: React.FC = ({ value }) => { const { shellApi, skillsSettings, skills = [] } = useShellApi(); const { updateSkillSetting } = shellApi; - const id = getSkillNameFromSetting(value.skillEndpoint); + const id = getSkillNameFromSetting(value?.skillEndpoint); const skill = skills.find(({ id: skillId }) => skillId === id) || ({} as Skill); const { endpointUrl, msAppId } = skillsSettings[id] || {}; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts index 80cb84d4a2..d854234917 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts @@ -12,22 +12,20 @@ const config: PluginConfig = { uiSchema: { [SDKKinds.BeginSkill]: { form: { - additionalFields: [ - { + order: ['selectSkillDialog', 'selectSkillEndpoint', '*', 'resultProperty', 'activityProcessed'], + hidden: ['skillEndpoint', 'botId', 'skillAppId', 'skillHostEndpoint'], + properties: { + selectSkillDialog: { field: SelectSkillDialogField, - name: 'selectSkillDialog', label: formatMessage('Skill Dialog Name'), description: formatMessage('Name of skill dialog to call'), }, - { + selectSkillEndpoint: { field: SkillEndpointField, - name: 'selectSkillEndpoint', label: formatMessage('Skill Endpoint'), description: formatMessage('The /api/messages endpoint for the skill.'), }, - ], - order: ['selectSkillDialog', 'selectSkillEndpoint', '*', 'resultProperty', 'activityProcessed'], - hidden: ['skillEndpoint', 'botId', 'skillAppId', 'skillHostEndpoint'], + }, }, }, }, From 61e6bccfd4ffe199c8d4c8703ff9f2063c4fb5d3 Mon Sep 17 00:00:00 2001 From: TJ Date: Thu, 24 Sep 2020 09:42:56 -1000 Subject: [PATCH 06/15] removed additional fields ui option --- .../src/components/fields/AdditionalField.tsx | 20 ---------- .../fields/__tests__/FieldSets.test.tsx | 3 +- .../src/components/fields/index.ts | 1 - .../src/utils/getOrderedProperties.ts | 2 +- .../utils/getSchemaWithAdditionalFields.ts | 2 +- .../src/utils/resolveFieldWidget.ts | 4 -- .../src/Fields/SchemaEditorField.tsx | 14 ++++++- .../schema-editor/src/formSchema.ts | 3 +- .../select-skill-dialog/src/ComboBoxField.tsx | 40 +++++++++++++------ .../src/SelectSkillDialogField.tsx | 5 ++- .../src/SkillEndpointField.tsx | 13 ++++-- .../src/__tests__/SelectSkillDialog.test.tsx | 5 +++ 12 files changed, 64 insertions(+), 48 deletions(-) delete mode 100644 Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx diff --git a/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx b/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx deleted file mode 100644 index 3fdb0cff1e..0000000000 --- a/Composer/packages/adaptive-form/src/components/fields/AdditionalField.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React from 'react'; -import { FieldProps } from '@bfc/extension-client'; - -import { FieldLabel } from '../FieldLabel'; - -export const AdditionalField: React.FC = (props) => { - const { id, description, label, required, uiOptions } = props; - const { field: Field, helpLink } = uiOptions; - - return Field ? ( - - - - - ) : null; -}; diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx index d3a7dbf85f..5622019617 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx @@ -60,7 +60,7 @@ describe('', () => { it('renders additional fields', async () => { const uiOptions = { properties: { - additionalField: { field: () =>
Additional Field
, label: 'Additional Field Label' }, + additionalField: { field: () =>
Additional Field
}, }, fieldsets: [ { @@ -76,6 +76,5 @@ describe('', () => { const { findByText } = renderSubject({ schema, uiOptions, value: {} }); await findByText('Additional Field'); - await findByText('Additional Field Label'); }); }); diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index 177273c055..24246f3a97 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export * from './AdditionalField'; export * from './ArrayField'; export * from './BooleanField'; export * from './CustomRecognizerField'; diff --git a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts index 4e9c4ae31e..9f330ae78f 100644 --- a/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts +++ b/Composer/packages/adaptive-form/src/utils/getOrderedProperties.ts @@ -49,7 +49,7 @@ export function getOrderedProperties( } return allFields; - }, [] as (string | [string, string])[]); + }, [] as OrderConfig); const allProperties = Object.keys(schema.properties ?? {}).filter( (p) => !p.startsWith('$') && !hiddenFieldSet.has(p) diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts index fdd584df81..b693e8389e 100644 --- a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -10,7 +10,7 @@ const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOpt const additionalFields = difference(keys(uiOptions.properties), keys(baseSchema.properties)); const additionalPropertySchema = fromPairs(additionalFields.map((field) => [field, { $role: 'additionalField' }])); - const properties = { ...baseSchema.properties, ...additionalPropertySchema }; + const properties = { ...additionalPropertySchema, ...baseSchema.properties }; return { ...baseSchema, properties }; }; diff --git a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts index 442b24b02a..73e86e2a38 100644 --- a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts +++ b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts @@ -17,10 +17,6 @@ export function resolveFieldWidget( ): FieldWidget { const FieldOverride = uiOptions?.field; - if (schema?.$role === 'additionalField') { - return DefaultFields.AdditionalField; - } - if (typeof FieldOverride === 'function') { return FieldOverride; } diff --git a/Composer/packages/ui-plugins/schema-editor/src/Fields/SchemaEditorField.tsx b/Composer/packages/ui-plugins/schema-editor/src/Fields/SchemaEditorField.tsx index 6730f1b4dc..5f09b2d2b8 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/Fields/SchemaEditorField.tsx +++ b/Composer/packages/ui-plugins/schema-editor/src/Fields/SchemaEditorField.tsx @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; import React, { useMemo } from 'react'; import AdaptiveForm from '@bfc/adaptive-form'; import { useShellApi, JSONSchema7 } from '@bfc/extension-client'; @@ -10,6 +12,12 @@ import { getDefinitions } from '../utils/getDefinitions'; import { uiOptions } from '../uiOptions'; import { schema, valueTypeDefinitions } from '../schema'; +const styles = { + container: css` + margin: 0 -18px; + `, +}; + const schemaUrl = 'https://raw.githubusercontent.com/microsoft/botframework-sdk/master/schemas/component/component.schema'; @@ -47,5 +55,9 @@ export const SchemaEditorField: React.FC = () => { updateDialogSchema({ content, id: dialogId }); }; - return ; + return ( +
+ +
+ ); }; diff --git a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts index 5f8cd00065..c8ab2bcde3 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts +++ b/Composer/packages/ui-plugins/schema-editor/src/formSchema.ts @@ -9,8 +9,9 @@ import { SchemaEditorField } from './Fields/SchemaEditorField'; const uiSchema: UISchema = { [SDKKinds.AdaptiveDialog]: { form: { + order: ['recognizer', '*', 'schema'], properties: { - schemaEditor: { + schema: { field: SchemaEditorField, }, }, diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx index 01b71f58ae..ee526c4dab 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/ComboBoxField.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { ComboBox, IComboBoxOption } from 'office-ui-fabric-react/lib/ComboBox'; +import { FieldLabel } from '@bfc/adaptive-form'; import { Icon } from 'office-ui-fabric-react/lib/Icon'; import { ISelectableOption } from 'office-ui-fabric-react/lib/utilities/selectableOption'; import { IRenderFunction } from 'office-ui-fabric-react/lib/Utilities'; @@ -11,13 +12,25 @@ export const ADD_DIALOG = 'ADD_DIALOG'; interface ComboBoxFieldProps { comboboxTitle: string | null; - id: string; options: IComboBoxOption[]; - value: string; onChange: any; + required?: boolean; + description: string; + id: string; + label: string; + value: string; } -export const ComboBoxField: React.FC = ({ comboboxTitle, id, options, value = '', onChange }) => { +export const ComboBoxField: React.FC = ({ + comboboxTitle, + description, + id, + label, + options, + value = '', + required, + onChange, +}) => { const onRenderOption: IRenderFunction = (option) => option ? (
@@ -31,15 +44,18 @@ export const ComboBoxField: React.FC = ({ comboboxTitle, id, ) : null; return ( - + + + + ); }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx index d55b4ab370..b553df3325 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/SelectSkillDialogField.tsx @@ -21,7 +21,8 @@ export const settingReferences = (skillName: string) => ({ skillAppId: referBySettings(skillName, 'msAppId'), }); -export const SelectSkillDialogField: React.FC = ({ value, onChange }) => { +export const SelectSkillDialogField: React.FC = (props) => { + const { value, onChange } = props; const { shellApi, skills = [] } = useShellApi(); const { addSkillDialog, displayManifestModal } = shellApi; const [comboboxTitle, setComboboxTitle] = useState(null); @@ -69,7 +70,9 @@ export const SelectSkillDialogField: React.FC = ({ value, onChange } = ({ value }) => { +export const SkillEndpointField: React.FC = (props) => { + const { description, label, required, uiOptions, value } = props; const { shellApi, skillsSettings, skills = [] } = useShellApi(); const { updateSkillSetting } = shellApi; @@ -39,5 +39,10 @@ export const SkillEndpointField: React.FC = ({ value }) => { } }; - return ; + return ( + + + + + ); }; diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx index c811dad7f4..1d6cc2c7ce 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/__tests__/SelectSkillDialog.test.tsx @@ -60,4 +60,9 @@ describe('Select Skill Dialog', () => { expect(addSkillDialog).toHaveBeenCalled(); expect(onChange).toHaveBeenCalledWith({ ...settingReferences('test-skill') }); }); + + it('should display label', async () => { + const { findByText } = renderSelectSkillDialog(); + await findByText('Skill Dialog Name'); + }); }); From d5319aaec48168bc71f4f994aea1d98d07a110a2 Mon Sep 17 00:00:00 2001 From: TJ Date: Thu, 24 Sep 2020 10:42:20 -1000 Subject: [PATCH 07/15] added additional field --- .../adaptive-form/src/components/FormRow.tsx | 4 +- .../fields/__tests__/FieldSets.test.tsx | 5 ++- .../src/utils/__tests__/getFieldSets.test.ts | 3 +- .../getSchemaWithAdditionalFields.test.ts | 41 +++++++++++++++++++ .../utils/getSchemaWithAdditionalFields.ts | 7 ++-- .../extension-client/src/types/formSchema.ts | 2 + .../packages/ui-plugins/composer/src/index.ts | 2 +- .../select-skill-dialog/src/index.ts | 2 + 8 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 Composer/packages/adaptive-form/src/utils/__tests__/getSchemaWithAdditionalFields.test.ts diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index a3d2e9cc5c..244193c5cc 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -64,8 +64,8 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { rawErrors: rawErrors?.[field], required: required.includes(field), uiOptions: newUiOptions, - value: fieldSchema?.$role === 'additionalField' ? value : value && value[field], - onChange: fieldSchema?.$role === 'additionalField' ? onChange : handleChange, + value: newUiOptions?.additionalField ? value : value && value[field], + onChange: newUiOptions?.additionalField ? onChange : handleChange, depth, definitions, transparentBorder, diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx index 5622019617..ec11b16f44 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/FieldSets.test.tsx @@ -60,7 +60,10 @@ describe('', () => { it('renders additional fields', async () => { const uiOptions = { properties: { - additionalField: { field: () =>
Additional Field
}, + additionalField: { + additionalField: true, + field: () =>
Additional Field
, + }, }, fieldsets: [ { diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts index b1fe477d21..caf1590a17 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getFieldSets.test.ts @@ -102,6 +102,7 @@ describe('getFieldsets', () => { ], properties: { additionalField: { + additionalField: true, field: 'field', }, }, @@ -138,7 +139,7 @@ describe('getFieldsets', () => { }, uiOptions: { order: ['one', 'three', 'five', 'seven', 'additionalField'], - properties: { additionalField: { field: 'field' } }, + properties: { additionalField: { additionalField: true, field: 'field' } }, }, }), ]); diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/getSchemaWithAdditionalFields.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/getSchemaWithAdditionalFields.test.ts new file mode 100644 index 0000000000..0b19b6260e --- /dev/null +++ b/Composer/packages/adaptive-form/src/utils/__tests__/getSchemaWithAdditionalFields.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { JSONSchema7 } from 'json-schema'; + +import { getSchemaWithAdditionalFields } from '../getSchemaWithAdditionalFields'; + +const schema = { + properties: { + one: { type: 'string' }, + two: { type: 'string' }, + three: { type: 'number' }, + four: { type: 'object' }, + five: { type: 'object' }, + six: { type: 'object' }, + seven: { type: 'boolean' }, + $kind: { type: 'string' }, + }, +} as JSONSchema7; + +describe('getSchemaWithAdditionalFields', () => { + it('should add additional fields to schema', () => { + const uiOptions: any = { + properties: { + additionalField: { + additionalField: true, + field: 'field', + }, + }, + }; + + const result = getSchemaWithAdditionalFields(schema, uiOptions); + expect(result).toEqual( + expect.objectContaining({ + properties: expect.objectContaining({ + additionalField: {}, + }), + }) + ); + }); +}); diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts index b693e8389e..1fa5dcf92d 100644 --- a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -2,13 +2,12 @@ // Licensed under the MIT License. import { JSONSchema7, UIOptions } from '@bfc/extension-client'; -import difference from 'lodash/difference'; +import entries from 'lodash/entries'; import fromPairs from 'lodash/fromPairs'; -import keys from 'lodash/keys'; const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOptions): JSONSchema7 => { - const additionalFields = difference(keys(uiOptions.properties), keys(baseSchema.properties)); - const additionalPropertySchema = fromPairs(additionalFields.map((field) => [field, { $role: 'additionalField' }])); + const additionalFields = entries(uiOptions.properties).filter(([, { additionalField }]) => additionalField); + const additionalPropertySchema = fromPairs(additionalFields.map(([field]) => [field, {}])); const properties = { ...additionalPropertySchema, ...baseSchema.properties }; return { ...baseSchema, properties }; diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index 34596541ac..29ccd633c9 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -16,6 +16,8 @@ export interface Fieldset { } export interface UIOptions { + /** Specifies if the property is an additional field */ + additionalField?: true; /** Description override. */ description?: UIOptionValue; /** Field widget override. */ diff --git a/Composer/packages/ui-plugins/composer/src/index.ts b/Composer/packages/ui-plugins/composer/src/index.ts index 9fa439562e..0c5c6d1345 100644 --- a/Composer/packages/ui-plugins/composer/src/index.ts +++ b/Composer/packages/ui-plugins/composer/src/index.ts @@ -16,7 +16,7 @@ const DefaultFormSchema: FormUISchema = { description: () => formatMessage('This configures a data driven dialog via a collection of events and actions.'), helpLink: 'https://aka.ms/bf-composer-docs-dialog', order: ['recognizer', '*'], - hidden: ['triggers', 'generator', 'selector', 'schema'], + hidden: ['triggers', 'generator', 'selector'], properties: { recognizer: { label: () => formatMessage('Language Understanding'), diff --git a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts index d854234917..12e316f4aa 100644 --- a/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts +++ b/Composer/packages/ui-plugins/select-skill-dialog/src/index.ts @@ -16,11 +16,13 @@ const config: PluginConfig = { hidden: ['skillEndpoint', 'botId', 'skillAppId', 'skillHostEndpoint'], properties: { selectSkillDialog: { + additionalField: true, field: SelectSkillDialogField, label: formatMessage('Skill Dialog Name'), description: formatMessage('Name of skill dialog to call'), }, selectSkillEndpoint: { + additionalField: true, field: SkillEndpointField, label: formatMessage('Skill Endpoint'), description: formatMessage('The /api/messages endpoint for the skill.'), From 448691ec1699580d2030daac47a98e955253a29d Mon Sep 17 00:00:00 2001 From: TJ Date: Thu, 24 Sep 2020 12:10:26 -1000 Subject: [PATCH 08/15] updated form row --- .../adaptive-form/src/components/fields/ObjectField.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx index 9ceb696ea4..460f621238 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx @@ -7,7 +7,7 @@ import { getOrderedProperties, getSchemaWithAdditionalFields } from '../../utils import { FormRow } from '../FormRow'; const ObjectField: React.FC> = function ObjectField(props) { - const { definitions, schema: baseSchema, uiOptions, depth, value, label, onChange, ...rest } = props; + const { schema: baseSchema, uiOptions, depth, value, label, ...rest } = props; if (!baseSchema) { return null; @@ -22,13 +22,11 @@ const ObjectField: React.FC> = function ObjectField(props) { ))}
From 775c81a1bcbce3a3faba52c4fcd56350d42c0e4b Mon Sep 17 00:00:00 2001 From: TJ Date: Fri, 25 Sep 2020 06:01:33 -1000 Subject: [PATCH 09/15] lint --- .../adaptive-form/src/utils/getSchemaWithAdditionalFields.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts index 1fa5dcf92d..004e4234db 100644 --- a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -2,11 +2,11 @@ // Licensed under the MIT License. import { JSONSchema7, UIOptions } from '@bfc/extension-client'; -import entries from 'lodash/entries'; import fromPairs from 'lodash/fromPairs'; +import toPairs from 'lodash/toPairs'; const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOptions): JSONSchema7 => { - const additionalFields = entries(uiOptions.properties).filter(([, { additionalField }]) => additionalField); + const additionalFields = toPairs(uiOptions.properties).filter(([, { additionalField }]) => additionalField); const additionalPropertySchema = fromPairs(additionalFields.map(([field]) => [field, {}])); const properties = { ...additionalPropertySchema, ...baseSchema.properties }; From 32025126bbbbbb28416061a6abf8f8110ce35397 Mon Sep 17 00:00:00 2001 From: TJ Date: Fri, 25 Sep 2020 06:42:00 -1000 Subject: [PATCH 10/15] changed properties to union type --- .../extension-client/src/types/formSchema.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index 29ccd633c9..aafedf7f86 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -15,9 +15,22 @@ export interface Fieldset { defaultExpanded?: boolean; } +export interface AdditionalField { + /** Specifies if the property is an additional field. */ + additionalField: true; + /** Field widget. */ + field: FieldWidget; + /** Description. */ + description?: UIOptionValue; + /** Url to docs. Rendered below field description. */ + helpLink?: string; + /** Label. */ + label?: UIOptionValue; + /** Placeholder. */ + placeholder?: UIOptionValue; +} + export interface UIOptions { - /** Specifies if the property is an additional field */ - additionalField?: true; /** Description override. */ description?: UIOptionValue; /** Field widget override. */ @@ -46,7 +59,7 @@ export interface UIOptions { placeholder?: UIOptionValue; /** Define ui options on fields that are children of this field. */ properties?: { - [key: string]: UIOptions; + [key: string]: UIOptions | AdditionalField; }; /** Use the serializer to apply additional data processing. * `get` is called to retrieve the value that is passed to SchemaField From 13357461fb654256ed4680650b465710f4eb009e Mon Sep 17 00:00:00 2001 From: TJ Date: Fri, 25 Sep 2020 12:58:08 -1000 Subject: [PATCH 11/15] fixed build errors --- .../adaptive-form/src/components/FormRow.tsx | 13 ++++++++----- .../src/components/fields/ObjectArrayField.tsx | 12 ++++++++---- .../src/utils/getSchemaWithAdditionalFields.ts | 4 ++-- .../extension-client/src/types/formSchema.ts | 4 ++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index 244193c5cc..ed59abd4b7 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { FieldProps, UIOptions } from '@bfc/extension-client'; +import { AdditionalField, isAdditionalField, FieldProps, UIOptions } from '@bfc/extension-client'; import { css, jsx } from '@emotion/core'; import React from 'react'; @@ -40,8 +40,11 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { intellisenseScopes.push('variable-scopes'); } - const newUiOptions = (uiOptions.properties?.[field] as UIOptions) ?? {}; - newUiOptions.intellisenseScopes = intellisenseScopes; + const newUiOptions: UIOptions | AdditionalField = uiOptions.properties?.[field] ?? {}; + + if (!isAdditionalField(newUiOptions)) { + newUiOptions.intellisenseScopes = intellisenseScopes; + } const handleChange = (data: any) => { const newData = { ...value }; @@ -64,8 +67,8 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { rawErrors: rawErrors?.[field], required: required.includes(field), uiOptions: newUiOptions, - value: newUiOptions?.additionalField ? value : value && value[field], - onChange: newUiOptions?.additionalField ? onChange : handleChange, + value: isAdditionalField(newUiOptions) ? value : value && value[field], + onChange: isAdditionalField(newUiOptions) ? onChange : handleChange, depth, definitions, transparentBorder, diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx index 169c0a7ef9..86ce388c07 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -4,7 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React, { useState, useMemo, useRef } from 'react'; -import { FieldProps, useShellApi } from '@bfc/extension-client'; +import { isAdditionalField, FieldProps, useShellApi } from '@bfc/extension-client'; import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { JSONSchema7 } from 'json-schema'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; @@ -58,10 +58,14 @@ const ObjectArrayField: React.FC> = (props) => { if (event.key.toLowerCase() === 'enter') { event.preventDefault(); - if (Object.keys(newObject).length > 0) { + if (Object.keys(newObject).length > 0 && !isAdditionalField(uiOptions)) { const formattedData = Object.entries(newObject).reduce((obj, [key, value]) => { - const serializeValue = uiOptions?.properties?.[key]?.serializer?.set; - return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; + const propertyUIOptions = uiOptions?.properties?.[key]; + if (!isAdditionalField(propertyUIOptions)) { + const serializeValue = propertyUIOptions?.serializer?.set; + return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; + } + return obj; }, {}); announce(INSIDE_ROW_LABEL); diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts index 004e4234db..74ea17fb11 100644 --- a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { JSONSchema7, UIOptions } from '@bfc/extension-client'; +import { isAdditionalField, JSONSchema7, UIOptions } from '@bfc/extension-client'; import fromPairs from 'lodash/fromPairs'; import toPairs from 'lodash/toPairs'; const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOptions): JSONSchema7 => { - const additionalFields = toPairs(uiOptions.properties).filter(([, { additionalField }]) => additionalField); + const additionalFields = toPairs(uiOptions.properties).filter(([, property]) => isAdditionalField(property)); const additionalPropertySchema = fromPairs(additionalFields.map(([field]) => [field, {}])); const properties = { ...additionalPropertySchema, ...baseSchema.properties }; diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index aafedf7f86..9eb8ab417a 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -102,3 +102,7 @@ export type RecognizerSchema = { shellApi: ShellApi ) => Promise | void; }; + +export const isAdditionalField = (uiOptions?: UIOptions | AdditionalField): uiOptions is AdditionalField => { + return typeof uiOptions !== 'undefined' && 'additionalField' in uiOptions && uiOptions?.additionalField; +}; From e13299681db0211f0cf188195de0b031ddb63fa1 Mon Sep 17 00:00:00 2001 From: TJ Date: Mon, 28 Sep 2020 08:38:02 -1000 Subject: [PATCH 12/15] resolved test issues --- .../src/components/fields/ObjectArrayField.tsx | 12 +++++++----- .../fields/__tests__/ObjectArrayField.test.tsx | 1 + .../extension-client/src/types/formSchema.ts | 4 ---- .../packages/extension-client/src/utils/index.ts | 1 + .../extension-client/src/utils/isAdditionalField.ts | 8 ++++++++ 5 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 Composer/packages/extension-client/src/utils/isAdditionalField.ts diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx index 86ce388c07..ab4383d0dd 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -58,12 +58,14 @@ const ObjectArrayField: React.FC> = (props) => { if (event.key.toLowerCase() === 'enter') { event.preventDefault(); - if (Object.keys(newObject).length > 0 && !isAdditionalField(uiOptions)) { + if (Object.keys(newObject).length > 0) { const formattedData = Object.entries(newObject).reduce((obj, [key, value]) => { - const propertyUIOptions = uiOptions?.properties?.[key]; - if (!isAdditionalField(propertyUIOptions)) { - const serializeValue = propertyUIOptions?.serializer?.set; - return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; + if (!isAdditionalField(uiOptions)) { + const propertyUIOptions = uiOptions?.properties?.[key]; + if (!isAdditionalField(propertyUIOptions)) { + const serializeValue = propertyUIOptions?.serializer?.set; + return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; + } } return obj; }, {}); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx index 804ead2226..e48b8894d4 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx @@ -27,6 +27,7 @@ jest.mock('../ArrayFieldItem', () => ({ })); jest.mock('@bfc/extension-client', () => ({ useShellApi: jest.fn(), + isAdditionalField: jest.fn().mockReturnValue(false), })); function renderSubject(overrides = {}) { diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index 9eb8ab417a..aafedf7f86 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -102,7 +102,3 @@ export type RecognizerSchema = { shellApi: ShellApi ) => Promise | void; }; - -export const isAdditionalField = (uiOptions?: UIOptions | AdditionalField): uiOptions is AdditionalField => { - return typeof uiOptions !== 'undefined' && 'additionalField' in uiOptions && uiOptions?.additionalField; -}; diff --git a/Composer/packages/extension-client/src/utils/index.ts b/Composer/packages/extension-client/src/utils/index.ts index 15ec8cdee2..d191b6aa74 100644 --- a/Composer/packages/extension-client/src/utils/index.ts +++ b/Composer/packages/extension-client/src/utils/index.ts @@ -1,4 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +export * from './isAdditionalField'; export * from './mergePluginConfigs'; diff --git a/Composer/packages/extension-client/src/utils/isAdditionalField.ts b/Composer/packages/extension-client/src/utils/isAdditionalField.ts new file mode 100644 index 0000000000..84cdc9adcf --- /dev/null +++ b/Composer/packages/extension-client/src/utils/isAdditionalField.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { AdditionalField, UIOptions } from '../types/formSchema'; + +export const isAdditionalField = (uiOptions?: UIOptions | AdditionalField): uiOptions is AdditionalField => { + return typeof uiOptions !== 'undefined' && 'additionalField' in uiOptions && uiOptions?.additionalField; +}; From c23d317b3df7a7f055c2c636f01494f6c707ba83 Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 29 Sep 2020 07:27:10 -1000 Subject: [PATCH 13/15] lint --- Composer/packages/adaptive-form/src/components/FormRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index bcaed93313..b50122e265 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { AdditionalField, isAdditionalField, FieldProps, UIOptions } from '@bfc/extension-client'; +import { isAdditionalField, FieldProps, UIOptions } from '@bfc/extension-client'; import { css, jsx } from '@emotion/core'; import React from 'react'; From eeade886dc95c13d154edf9ac73282f8f334c4ac Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 29 Sep 2020 08:27:26 -1000 Subject: [PATCH 14/15] removed AdditionalField type --- .../adaptive-form/src/components/FormRow.tsx | 6 +++--- .../components/fields/ObjectArrayField.tsx | 12 +++--------- .../__tests__/ObjectArrayField.test.tsx | 1 - .../utils/getSchemaWithAdditionalFields.ts | 4 ++-- .../extension-client/src/types/formSchema.ts | 19 +++---------------- .../extension-client/src/utils/index.ts | 1 - .../src/utils/isAdditionalField.ts | 8 -------- 7 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 Composer/packages/extension-client/src/utils/isAdditionalField.ts diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index b50122e265..7556800b87 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { isAdditionalField, FieldProps, UIOptions } from '@bfc/extension-client'; +import { FieldProps, UIOptions } from '@bfc/extension-client'; import { css, jsx } from '@emotion/core'; import React from 'react'; @@ -58,8 +58,8 @@ export const getRowProps = (rowProps: FormRowProps, field: string) => { rawErrors: rawErrors?.[field], required: required.includes(field), uiOptions: newUiOptions, - value: isAdditionalField(newUiOptions) ? value : value && value[field], - onChange: isAdditionalField(newUiOptions) ? onChange : handleChange, + value: !newUiOptions.additionalField && value ? value[field] : value, + onChange: !newUiOptions.additionalField ? handleChange : onChange, depth, definitions, transparentBorder, diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx index ab4383d0dd..169c0a7ef9 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx @@ -4,7 +4,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; import React, { useState, useMemo, useRef } from 'react'; -import { isAdditionalField, FieldProps, useShellApi } from '@bfc/extension-client'; +import { FieldProps, useShellApi } from '@bfc/extension-client'; import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; import { JSONSchema7 } from 'json-schema'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; @@ -60,14 +60,8 @@ const ObjectArrayField: React.FC> = (props) => { if (Object.keys(newObject).length > 0) { const formattedData = Object.entries(newObject).reduce((obj, [key, value]) => { - if (!isAdditionalField(uiOptions)) { - const propertyUIOptions = uiOptions?.properties?.[key]; - if (!isAdditionalField(propertyUIOptions)) { - const serializeValue = propertyUIOptions?.serializer?.set; - return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; - } - } - return obj; + const serializeValue = uiOptions?.properties?.[key]?.serializer?.set; + return { ...obj, [key]: typeof serializeValue === 'function' ? serializeValue(value) : value }; }, {}); announce(INSIDE_ROW_LABEL); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx index e48b8894d4..804ead2226 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx @@ -27,7 +27,6 @@ jest.mock('../ArrayFieldItem', () => ({ })); jest.mock('@bfc/extension-client', () => ({ useShellApi: jest.fn(), - isAdditionalField: jest.fn().mockReturnValue(false), })); function renderSubject(overrides = {}) { diff --git a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts index 74ea17fb11..004e4234db 100644 --- a/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts +++ b/Composer/packages/adaptive-form/src/utils/getSchemaWithAdditionalFields.ts @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { isAdditionalField, JSONSchema7, UIOptions } from '@bfc/extension-client'; +import { JSONSchema7, UIOptions } from '@bfc/extension-client'; import fromPairs from 'lodash/fromPairs'; import toPairs from 'lodash/toPairs'; const getSchemaWithAdditionalFields = (baseSchema: JSONSchema7, uiOptions: UIOptions): JSONSchema7 => { - const additionalFields = toPairs(uiOptions.properties).filter(([, property]) => isAdditionalField(property)); + const additionalFields = toPairs(uiOptions.properties).filter(([, { additionalField }]) => additionalField); const additionalPropertySchema = fromPairs(additionalFields.map(([field]) => [field, {}])); const properties = { ...additionalPropertySchema, ...baseSchema.properties }; diff --git a/Composer/packages/extension-client/src/types/formSchema.ts b/Composer/packages/extension-client/src/types/formSchema.ts index aafedf7f86..29ccd633c9 100644 --- a/Composer/packages/extension-client/src/types/formSchema.ts +++ b/Composer/packages/extension-client/src/types/formSchema.ts @@ -15,22 +15,9 @@ export interface Fieldset { defaultExpanded?: boolean; } -export interface AdditionalField { - /** Specifies if the property is an additional field. */ - additionalField: true; - /** Field widget. */ - field: FieldWidget; - /** Description. */ - description?: UIOptionValue; - /** Url to docs. Rendered below field description. */ - helpLink?: string; - /** Label. */ - label?: UIOptionValue; - /** Placeholder. */ - placeholder?: UIOptionValue; -} - export interface UIOptions { + /** Specifies if the property is an additional field */ + additionalField?: true; /** Description override. */ description?: UIOptionValue; /** Field widget override. */ @@ -59,7 +46,7 @@ export interface UIOptions { placeholder?: UIOptionValue; /** Define ui options on fields that are children of this field. */ properties?: { - [key: string]: UIOptions | AdditionalField; + [key: string]: UIOptions; }; /** Use the serializer to apply additional data processing. * `get` is called to retrieve the value that is passed to SchemaField diff --git a/Composer/packages/extension-client/src/utils/index.ts b/Composer/packages/extension-client/src/utils/index.ts index d191b6aa74..15ec8cdee2 100644 --- a/Composer/packages/extension-client/src/utils/index.ts +++ b/Composer/packages/extension-client/src/utils/index.ts @@ -1,5 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -export * from './isAdditionalField'; export * from './mergePluginConfigs'; diff --git a/Composer/packages/extension-client/src/utils/isAdditionalField.ts b/Composer/packages/extension-client/src/utils/isAdditionalField.ts deleted file mode 100644 index 84cdc9adcf..0000000000 --- a/Composer/packages/extension-client/src/utils/isAdditionalField.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { AdditionalField, UIOptions } from '../types/formSchema'; - -export const isAdditionalField = (uiOptions?: UIOptions | AdditionalField): uiOptions is AdditionalField => { - return typeof uiOptions !== 'undefined' && 'additionalField' in uiOptions && uiOptions?.additionalField; -}; From 8c30b7213889f5bd6e587758523b4e6cd2e0cc18 Mon Sep 17 00:00:00 2001 From: TJ Date: Tue, 29 Sep 2020 09:21:30 -1000 Subject: [PATCH 15/15] fix test --- .../src/controllers/__tests__/project.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Composer/packages/server/src/controllers/__tests__/project.test.ts b/Composer/packages/server/src/controllers/__tests__/project.test.ts index 78d7fc3675..d7f52366d2 100644 --- a/Composer/packages/server/src/controllers/__tests__/project.test.ts +++ b/Composer/packages/server/src/controllers/__tests__/project.test.ts @@ -4,6 +4,7 @@ import { Request, Response } from 'express'; import rimraf from 'rimraf'; import { ExtensionContext } from '@bfc/extension'; +import * as msRest from '@azure/ms-rest-js'; import { BotProjectService } from '../../services/project'; import { ProjectController } from '../../controllers/project'; @@ -22,6 +23,9 @@ jest.mock('@bfc/extension', () => { }; }); +const mockSendRequest = jest.fn(); +msRest.ServiceClient.prototype.sendRequest = mockSendRequest; + const mockSampleBotPath = Path.join(__dirname, '../../__mocks__/asset/projects/SampleBot'); let mockRes: Response; @@ -330,15 +334,22 @@ describe('skill operation', () => { }); it('should retrieve skill manifest', async () => { + mockSendRequest.mockResolvedValue({}); + const url = 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json'; + const mockReq = { params: { projectId }, query: { - url: 'https://yuesuemailskill0207-gjvga67.azurewebsites.net/manifest/manifest-1.0.json', + url, }, body: {}, } as Request; await ProjectController.getSkill(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockSendRequest).toHaveBeenCalledWith({ + url, + method: 'GET', + }); }, 10000); });