diff --git a/Composer/packages/adaptive-form/src/components/AdaptiveForm.tsx b/Composer/packages/adaptive-form/src/components/AdaptiveForm.tsx index 46bb92cc10..e67baaec36 100644 --- a/Composer/packages/adaptive-form/src/components/AdaptiveForm.tsx +++ b/Composer/packages/adaptive-form/src/components/AdaptiveForm.tsx @@ -64,8 +64,8 @@ export const AdaptiveForm: React.FC = function AdaptiveForm(p onChange={(newData) => onChange({ ...formData, ...newData })} /> void; +}; + +export const AddButton = ({ children, onClick }: React.PropsWithChildren) => { + return ( + + + {children ?? formatMessage('Add new')} + + + ); +}; diff --git a/Composer/packages/adaptive-form/src/components/CollapseField.tsx b/Composer/packages/adaptive-form/src/components/CollapseField.tsx index 419ff2ba8e..2bf35af615 100644 --- a/Composer/packages/adaptive-form/src/components/CollapseField.tsx +++ b/Composer/packages/adaptive-form/src/components/CollapseField.tsx @@ -21,7 +21,7 @@ const styles = { header: css` background-color: #eff6fc; display: flex; - margin: 4px 0px; + margin: 4px -18px; align-items: center; `, }; diff --git a/Composer/packages/adaptive-form/src/components/FormRow.tsx b/Composer/packages/adaptive-form/src/components/FormRow.tsx index 62618999c3..a58bd35d56 100644 --- a/Composer/packages/adaptive-form/src/components/FormRow.tsx +++ b/Composer/packages/adaptive-form/src/components/FormRow.tsx @@ -18,12 +18,10 @@ export interface FormRowProps extends Omit { export function getRowProps(rowProps: FormRowProps, field: string): FieldProps { const { id, - depth, schema, definitions, value, uiOptions, - transparentBorder, className, label, rawErrors, @@ -60,9 +58,7 @@ export function getRowProps(rowProps: FormRowProps, field: string): FieldProps { uiOptions: newUiOptions, value: !newUiOptions.additionalField && value ? value[field] : value, onChange: !newUiOptions.additionalField ? handleChange : onChange, - depth, definitions, - transparentBorder, className, onBlur, onFocus, diff --git a/Composer/packages/adaptive-form/src/components/FormTitle.tsx b/Composer/packages/adaptive-form/src/components/FormTitle.tsx index 67ca083b58..94c36c442e 100644 --- a/Composer/packages/adaptive-form/src/components/FormTitle.tsx +++ b/Composer/packages/adaptive-form/src/components/FormTitle.tsx @@ -131,7 +131,6 @@ const FormTitle: React.FC = (props) => {
css` + container: (isRoot?: boolean) => css` display: flex; flex-direction: column; - margin: 10px ${depth === 0 ? 18 : 0}px; + margin: 10px ${isRoot ? 18 : 0}px; label: SchemaFieldContainer; `, @@ -34,6 +34,7 @@ export const SchemaField: React.FC = (props) => { expression, onBlur, id, + isRoot, ...rest } = props; const formUIOptions = useFormConfig(); @@ -108,7 +109,7 @@ export const SchemaField: React.FC = (props) => { }; return ( -
+
{!hideError && !uiOptions.hideError && error}
diff --git a/Composer/packages/adaptive-form/src/components/WithTypeIcons.tsx b/Composer/packages/adaptive-form/src/components/WithTypeIcons.tsx index 8d748fcca5..80868375e9 100644 --- a/Composer/packages/adaptive-form/src/components/WithTypeIcons.tsx +++ b/Composer/packages/adaptive-form/src/components/WithTypeIcons.tsx @@ -39,7 +39,7 @@ export function WithTypeIcons(WrappedComponent: FieldWidget): FieldWidget {
{iconText &&
{iconText}
}
- +
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 676d17c175..7e3b0871f2 100644 --- a/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx +++ b/Composer/packages/adaptive-form/src/components/__tests__/FormRow.test.tsx @@ -12,7 +12,6 @@ const field: FormRowProps = { onChange: jest.fn(), row: '', definitions: {}, - depth: 0, id: 'test', name: 'row', schema: { diff --git a/Composer/packages/adaptive-form/src/components/__tests__/SchemaField.test.tsx b/Composer/packages/adaptive-form/src/components/__tests__/SchemaField.test.tsx index e9f8a7efe5..407df57b7c 100644 --- a/Composer/packages/adaptive-form/src/components/__tests__/SchemaField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/__tests__/SchemaField.test.tsx @@ -24,7 +24,6 @@ jest.mock('@bfc/extension-client', () => ({ const defaultProps: FieldProps = { onChange: jest.fn(), - depth: 0, id: 'test-id', name: 'test-name', definitions: { foo: { type: 'string' } }, diff --git a/Composer/packages/adaptive-form/src/components/fields/ArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ArrayField.tsx index ce6db87c22..df75d1eb19 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ArrayField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ArrayField.tsx @@ -2,24 +2,19 @@ // Licensed under the MIT License. /** @jsx jsx */ import { jsx } from '@emotion/core'; -import React, { useState } from 'react'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import { IconButton } from 'office-ui-fabric-react/lib/Button'; -import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { SharedColors, NeutralColors, FontSizes } from '@uifabric/fluent-theme'; +import React from 'react'; import { FieldProps } from '@bfc/extension-client'; -import formatMessage from 'format-message'; import { getArrayItemProps, useArrayItems } from '../../utils'; import { FieldLabel } from '../FieldLabel'; +import { AddButton } from '../AddButton'; -import { arrayField } from './styles'; import { ArrayFieldItem } from './ArrayFieldItem'; import { UnsupportedField } from './UnsupportedField'; const ArrayField: React.FC> = (props) => { const { - value = [], + value, onChange, schema, label, @@ -29,30 +24,16 @@ const ArrayField: React.FC> = (props) => { uiOptions, className, required, - placeholder, ...rest } = props; - const [newValue, setNewValue] = useState(); const { arrayItems, handleChange, addItem } = useArrayItems(value, onChange); - const moreLabel = formatMessage('Item actions'); - - const handleNewChange = (_e: React.FormEvent, newValue?: string) => - setNewValue(newValue || ''); - - const handleKeyDown = (event) => { - if (event.key.toLowerCase() === 'enter') { - event.preventDefault(); - - if (newValue) { - addItem(newValue); - setNewValue(''); - } - } - }; - const itemSchema = Array.isArray(schema.items) ? schema.items[0] : schema.items; + const onClick = React.useCallback(() => { + addItem(undefined); + }, [addItem]); + if (!itemSchema || itemSchema === true) { return ; } @@ -60,58 +41,21 @@ const ArrayField: React.FC> = (props) => { return (
-
- {arrayItems.map((element, idx) => ( - - ))} -
-
-
- - - - -
-
+ {arrayItems.map((element, idx) => ( + + ))} +
); }; diff --git a/Composer/packages/adaptive-form/src/components/fields/ArrayFieldItem.tsx b/Composer/packages/adaptive-form/src/components/fields/ArrayFieldItem.tsx index 1567520e9f..e5df8f70e0 100644 --- a/Composer/packages/adaptive-form/src/components/fields/ArrayFieldItem.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/ArrayFieldItem.tsx @@ -33,10 +33,7 @@ const ArrayFieldItem: React.FC = (props) => { onRemove, index, label, - depth, onBlur, - stackArrayItems, - transparentBorder, uiOptions, value, className, @@ -89,12 +86,10 @@ const ArrayFieldItem: React.FC = (props) => {
{ fontSize?: string; styles?: Partial; - transparentBorder?: boolean; ariaLabel?: string; } const EditableField: React.FC = (props) => { - const { - depth, - styles = {}, - placeholder, - fontSize, - onChange, - onBlur, - value, - id, - error, - className, - transparentBorder, - ariaLabel, - } = props; + const { styles = {}, placeholder, fontSize, onChange, onBlur, value, id, error, className, ariaLabel } = props; const [editing, setEditing] = useState(false); const [hasFocus, setHasFocus] = useState(false); const [localValue, setLocalValue] = useState(value); @@ -55,7 +41,7 @@ const EditableField: React.FC = (props) => { let borderColor: string | undefined = undefined; if (!editing && !error) { - borderColor = localValue || transparentBorder || depth > 1 ? 'transparent' : NeutralColors.gray30; + borderColor = localValue ? 'transparent' : NeutralColors.gray30; } return ( diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx deleted file mode 100644 index 51cd4ebedc..0000000000 --- a/Composer/packages/adaptive-form/src/components/fields/ObjectArrayField.tsx +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React, { useState, useMemo, useRef } from 'react'; -import { FieldProps, useShellApi } from '@bfc/extension-client'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; -import { IconButton } from 'office-ui-fabric-react/lib/Button'; -import { TextField, ITextField } from 'office-ui-fabric-react/lib/TextField'; -import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { FontSizes, NeutralColors, SharedColors } from '@uifabric/fluent-theme'; -import formatMessage from 'format-message'; -import map from 'lodash/map'; - -import { getArrayItemProps, getOrderedProperties, useArrayItems, resolveRef, isPropertyHidden } from '../../utils'; -import { FieldLabel } from '../FieldLabel'; - -import { objectArrayField } from './styles'; -import { ArrayFieldItem } from './ArrayFieldItem'; -import { UnsupportedField } from './UnsupportedField'; - -const getNewPlaceholder = (props: FieldProps, propertyName: string): string | undefined => { - const { uiOptions } = props; - const placeholderOverride = uiOptions.properties?.[propertyName]?.placeholder; - - if (placeholderOverride) { - return typeof placeholderOverride === 'function' ? placeholderOverride(undefined) : placeholderOverride; - } - - return formatMessage('Add new {propertyName}', { propertyName }); -}; - -const ObjectArrayField: React.FC> = (props) => { - const { value = [], schema, id, onChange, className, uiOptions, label, description, required } = props; - const { items } = schema; - const itemSchema = Array.isArray(items) ? items[0] : items; - const properties = (itemSchema && itemSchema !== true && itemSchema.properties) || {}; - const [newObject, setNewObject] = useState({}); - const { arrayItems, handleChange, addItem } = useArrayItems(value, onChange); - const firstNewFieldRef: React.RefObject = useRef(null); - const { announce } = useShellApi().shellApi; - - const moreLabel = formatMessage('Item actions'); - - const END_OF_ROW_LABEL = formatMessage('press Enter to add this item or Tab to move to the next interactive element'); - - const INSIDE_ROW_LABEL = formatMessage( - 'press Enter to add this name and advance to the next row, or press Tab to advance to the value field' - ); - - const handleNewObjectChange = (property: string) => (_e: React.FormEvent, newValue?: string) => { - setNewObject({ ...newObject, [property]: newValue }); - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key.toLowerCase() === 'enter') { - event.preventDefault(); - - if (Object.keys(newObject).length > 0) { - 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 }; - }, {}); - - announce(INSIDE_ROW_LABEL); - addItem(formattedData); - setNewObject({}); - firstNewFieldRef.current?.focus(); - } - } - }; - - const handleAdd = () => { - addItem({}); - }; - - const orderedProperties = getOrderedProperties( - itemSchema && typeof itemSchema !== 'boolean' ? itemSchema : {}, - uiOptions, - value - ).filter((property) => Array.isArray(property) || !isPropertyHidden(uiOptions, value, property)); - - const stackArrayItems = useMemo(() => { - const allOrderProps = orderedProperties.reduce((all: string[], prop: string | string[]) => { - return [...all, ...(Array.isArray(prop) ? prop : [prop])]; - }, []); - - return ( - allOrderProps.length > 2 || - orderedProperties.some((property) => Array.isArray(property)) || - Object.entries(properties).some(([key, propSchema]) => { - const resolved = resolveRef(propSchema, props.definitions); - return allOrderProps.includes(key) && resolved.$role === 'expression'; - }) - ); - }, [itemSchema, orderedProperties]); - - if (!itemSchema || itemSchema === true) { - return ; - } - - return ( -
- -
- {orderedProperties.length > 1 && !stackArrayItems && ( -
- {orderedProperties.map((key, index) => { - if (typeof key === 'string') { - const propSchema = properties[key]; - - if (propSchema && propSchema !== true) { - return ( -
- -
- ); - } - } - })} -
-
- )} - {map(arrayItems, (item, idx) => ( - - ))} -
-
- {!stackArrayItems ? ( - -
- {orderedProperties - .filter((p) => !Array.isArray(p)) - .map((property, index, allProperties) => { - const lastField = index === allProperties.length - 1; - if (typeof property === 'string') { - return ( -
- -
- ); - } - })} -
- - - -
- ) : ( - - {formatMessage('Add')} - - )} -
-
- ); -}; - -export { ObjectArrayField }; diff --git a/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx b/Composer/packages/adaptive-form/src/components/fields/ObjectField.tsx index 460f621238..375a1d6066 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 { schema: baseSchema, uiOptions, depth, value, label, ...rest } = props; + const { schema: baseSchema, uiOptions, value = {}, label, ...rest } = props; if (!baseSchema) { return null; @@ -22,7 +22,6 @@ const ObjectField: React.FC> = function ObjectField(props) { = (props) => { label={selectedSchema.type !== 'object' ? false : undefined} placeholder={placeholder} schema={selectedSchema} - transparentBorder={false} uiOptions={props.uiOptions} /> ); diff --git a/Composer/packages/adaptive-form/src/components/fields/OneOfField/__tests__/OneOfField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/OneOfField/__tests__/OneOfField.test.tsx index 75ba371028..4f5dbc855f 100644 --- a/Composer/packages/adaptive-form/src/components/fields/OneOfField/__tests__/OneOfField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/OneOfField/__tests__/OneOfField.test.tsx @@ -15,7 +15,6 @@ jest.mock('../../../../utils/resolveFieldWidget', () => ({ const defaultProps = { onChange: jest.fn(), - depth: 0, schema: {}, definitions: {}, id: 'test', diff --git a/Composer/packages/adaptive-form/src/components/fields/OneOfField/utils.ts b/Composer/packages/adaptive-form/src/components/fields/OneOfField/utils.ts index 4a6b6cc37f..43961c5150 100644 --- a/Composer/packages/adaptive-form/src/components/fields/OneOfField/utils.ts +++ b/Composer/packages/adaptive-form/src/components/fields/OneOfField/utils.ts @@ -155,10 +155,8 @@ export function getFieldProps(props: FieldProps, schema?: JSONSchema7): FieldPro ...props, enumOptions, schema: schema || {}, - transparentBorder: false, // allows object fields to render their labels label: schema?.type === 'object' ? undefined : false, - depth: props.depth - 1, placeholder: getUiPlaceholder({ ...props }), description: getUiDescription({ ...props, description: undefined }), }; diff --git a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/ObjectItem.tsx b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/ObjectItem.tsx index 9e10a1e39d..a68622dece 100644 --- a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/ObjectItem.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/ObjectItem.tsx @@ -13,13 +13,14 @@ import formatMessage from 'format-message'; import { StringField } from '../StringField'; import { SchemaField } from '../../SchemaField'; +import { WithTypeIcons } from '../../WithTypeIcons'; -import { container, item, itemContainer } from './styles'; +import { container, item } from './styles'; +const StringFieldWithIcon = WithTypeIcons(StringField); interface ObjectItemProps extends FieldProps { name: string; formData: object; - stackedLayout?: boolean; onNameChange: (name: string) => void; onDelete: () => void; } @@ -29,7 +30,6 @@ const ObjectItem: React.FC = ({ name: originalName, formData, value, - stackedLayout, schema, onChange, onNameChange, @@ -68,38 +68,31 @@ const ObjectItem: React.FC = ({ return (
-
-
- setName(newValue || '')} - /> -
-
- -
+
+ setName(newValue || '')} + /> +
> = (props) => { const { definitions, - depth, description, id, label, @@ -33,120 +26,31 @@ const OpenObjectField: React.FC(''); - const [newValue, setNewValue] = useState(''); - const fieldRef = useRef(null); - - const moreLabel = formatMessage('Edit Property'); - const { addProperty, objectEntries, onChange: handleChange } = useObjectItems(value, onChange); - const handleKeyDown = (event) => { - if (event.key.toLowerCase() === 'enter') { - event.preventDefault(); - - if (name && !Object.keys(value).includes(name)) { - addProperty(name, newValue); - - if (fieldRef.current) { - fieldRef.current.focus(); - } - } - } - }; - - const keyLabel = formatMessage('Key'); - const valueLabel = formatMessage('Value'); - - const stackedLayout = typeof additionalProperties === 'object'; + const onClick = React.useCallback(() => { + addProperty(); + }, [addProperty]); return (
- {!stackedLayout && ( -
-
- -
-
- -
-
-
- )} {objectEntries.map(({ id, propertyName, propertyValue }, index) => { return ( ); })} - {additionalProperties && - (!stackedLayout ? ( -
-
- setName(newValue || '')} - onKeyDown={handleKeyDown} - /> -
-
- setNewValue(newValue || '')} - onKeyDown={handleKeyDown} - /> -
- - - -
- ) : ( -
- addProperty()}> - {formatMessage('Add')} - -
- ))} + {additionalProperties && }
); }; diff --git a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/__tests__/OpenObjectField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/__tests__/OpenObjectField.test.tsx index 0e7d2a785c..59f4200e2e 100644 --- a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/__tests__/OpenObjectField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/__tests__/OpenObjectField.test.tsx @@ -10,7 +10,6 @@ import { OpenObjectField } from '../OpenObjectField'; const defaultProps = { onChange: jest.fn(), value: {}, - depth: 0, definitions: {}, schema: { additionalProperties: false, @@ -72,21 +71,10 @@ describe('', () => { }); describe('adding more items', () => { - it('allows adding more items if the schema allows it', () => { + it('allows adding more items if the schema allows it', async () => { const onChange = jest.fn(); - const { getByPlaceholderText } = renderSubject({ schema: { additionalProperties: true }, onChange }); - - fireEvent.change(getByPlaceholderText('Add a new key'), { target: { value: 'newKey' } }); - fireEvent.change(getByPlaceholderText('Add a new value'), { target: { value: 'new value' } }); - - expect(onChange).not.toHaveBeenCalled(); - - fireEvent.keyDown(getByPlaceholderText('Add a new value'), { key: 'Enter' }); - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ - newKey: 'new value', - }) - ); + const { findByText } = renderSubject({ schema: { additionalProperties: true }, onChange }); + await findByText('Add new'); }); }); }); diff --git a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/styles.ts b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/styles.ts index 047e810ae1..3b9e908fe0 100644 --- a/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/styles.ts +++ b/Composer/packages/adaptive-form/src/components/fields/OpenObjectField/styles.ts @@ -11,50 +11,8 @@ export const container = css` label: OpenObjectFieldContainer; `; -export const itemContainer = (stackedLayout?: boolean) => css` - display: flex; +export const item = css` flex: 1; - flex-direction: ${stackedLayout ? 'column' : 'row'}; - - label: OpenObjectFieldItemContainer; -`; - -export const addButtonContainer = css` - border-top: 1px solid ${NeutralColors.gray30}; - padding: 8px 0; -`; - -export const filler = css` - width: 32px; - - label: OpenObjectFieldFiller; -`; - -export const item = (stackedLayout?: boolean) => css` - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - & + & { - margin-left: ${!stackedLayout ? '16px' : '0'}; - } label: OpenObjectFieldItem; `; - -export const label = css` - flex: 1; - padding-left: 8px; - - & + & { - margin-left: 16px; - } - - label: OpenObjectFieldLabel; -`; - -export const labelContainer = css` - display: flex; - - label: OpenObjectFieldLabelContainer; -`; diff --git a/Composer/packages/adaptive-form/src/components/fields/StringField.tsx b/Composer/packages/adaptive-form/src/components/fields/StringField.tsx index b6f12ec421..8d8df6de8d 100644 --- a/Composer/packages/adaptive-form/src/components/fields/StringField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/StringField.tsx @@ -3,32 +3,11 @@ import React, { useEffect } from 'react'; import { FieldProps } from '@bfc/extension-client'; -import { NeutralColors } from '@uifabric/fluent-theme'; import { ITextField, TextField } from 'office-ui-fabric-react/lib/TextField'; import formatMessage from 'format-message'; import { FieldLabel } from '../FieldLabel'; -export const borderStyles = (transparentBorder: boolean, error: boolean, hasIcon: boolean) => - transparentBorder - ? { - fieldGroup: { - borderColor: error ? undefined : 'transparent', - borderRadius: hasIcon ? '0 2px 2px 0' : undefined, - transition: 'border-color 0.1s linear', - selectors: { - ':hover': { - borderColor: error ? undefined : NeutralColors.gray30, - }, - }, - }, - } - : { - fieldGroup: { - borderRadius: hasIcon ? '0 2px 2px 0' : undefined, - }, - }; - export const StringField: React.FC> = function StringField(props) { const { id, @@ -39,7 +18,6 @@ export const StringField: React.FC> = function StringField(pr description, placeholder, readonly, - transparentBorder, onFocus, onBlur, error, @@ -89,14 +67,17 @@ export const StringField: React.FC> = function StringField(pr ariaLabel={label || formatMessage('string field')} autoComplete="off" componentRef={textFieldRef} + data-testid="string-field" disabled={disabled} errorMessage={error} id={id} placeholder={placeholder} readOnly={readonly} styles={{ - ...borderStyles(Boolean(transparentBorder), Boolean(error), !!hasIcon), root: { width: '100%' }, + fieldGroup: { + borderRadius: hasIcon ? '0 2px 2px 0' : undefined, + }, errorMessage: { display: 'none' }, }} value={value} diff --git a/Composer/packages/adaptive-form/src/components/fields/UnsupportedField.tsx b/Composer/packages/adaptive-form/src/components/fields/UnsupportedField.tsx index b41f0c21c7..26dc17edef 100644 --- a/Composer/packages/adaptive-form/src/components/fields/UnsupportedField.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/UnsupportedField.tsx @@ -6,6 +6,7 @@ import React, { useState } from 'react'; import { Link } from 'office-ui-fabric-react/lib/Link'; import { FieldProps } from '@bfc/extension-client'; import omit from 'lodash/omit'; +import formatMessage from 'format-message'; import { unsupportedField } from './styles'; @@ -17,7 +18,7 @@ export const UnsupportedField: React.FC = function UnsupportedField(
{props.label} (Unsupported Field) setShowDetails((prev) => !prev)}> - Toggle Details + {formatMessage('Toggle Details')}
diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayField.test.tsx
index 18ce6771ff..35bb859e20 100644
--- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayField.test.tsx
+++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayField.test.tsx
@@ -27,14 +27,11 @@ describe('', () => {
     expect(getAllByTestId('ArrayFieldItem')).toHaveLength(3);
   });
 
-  it('can add new items', () => {
-    const onChange = jest.fn();
-    const { getByLabelText } = renderSubject({ onChange });
+  it('can add new items', async () => {
+    const { getByText, findByTestId } = renderSubject();
 
-    const input = getByLabelText('New value');
-    fireEvent.change(input, { target: { value: 'new value' } });
-    fireEvent.keyDown(input, { key: 'Enter' });
-
-    expect(onChange).toHaveBeenCalledWith(['new value']);
+    const button = getByText('Add new');
+    fireEvent.click(button);
+    await findByTestId('string-field');
   });
 });
diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayFieldItem.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayFieldItem.test.tsx
index cf84bcb9be..6ccb0e6523 100644
--- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayFieldItem.test.tsx
+++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/ArrayFieldItem.test.tsx
@@ -110,6 +110,6 @@ describe('', () => {
       index: 1,
     });
 
-    expect(await findByText('error 2')).toBeInTheDocument();
+    expect(await findByText('Test Name error 2')).toBeInTheDocument();
   });
 });
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
deleted file mode 100644
index af54442bcd..0000000000
--- a/Composer/packages/adaptive-form/src/components/fields/__tests__/ObjectArrayField.test.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-import React from 'react';
-import { render, fireEvent } from '@botframework-composer/test-utils';
-import assign from 'lodash/assign';
-import { useShellApi } from '@bfc/extension-client';
-
-import { ObjectArrayField } from '../ObjectArrayField';
-
-import { fieldProps } from './testUtils';
-
-const testSchema = (moreItems: object = {}) => ({
-  type: 'array',
-  items: {
-    type: 'object',
-    properties: {
-      name: { type: 'string', title: 'Name Label' },
-      age: { type: 'integer', title: 'Age Label' },
-      ...moreItems,
-    },
-  },
-});
-
-jest.mock('../ArrayFieldItem', () => ({
-  ArrayFieldItem: ({ stackArrayItems }) => 
{stackArrayItems ? 'stacked' : 'not stacked'}
, -})); -jest.mock('@bfc/extension-client', () => ({ - useShellApi: jest.fn(), -})); - -function renderSubject(overrides = {}) { - const props = assign({}, fieldProps({ schema: { items: [{ type: 'object' }] } }), overrides); - return render(); -} - -describe('', () => { - let announce = jest.fn(); - const value = [ - { - name: 'foo', - age: 12, - }, - { - name: 'bar', - age: 23, - }, - ]; - - beforeEach(() => { - announce = jest.fn(); - (useShellApi as jest.Mock).mockReturnValue({ - shellApi: { announce }, - }); - }); - - describe('invalid item schema', () => { - it.each([undefined, [], true])('renders unsupported field for item schema %p', (itemSchema) => { - const { getByTestId } = renderSubject({ schema: { items: itemSchema } }); - expect(getByTestId('UnsupportedField')).toBeInTheDocument(); - }); - }); - - describe('when fields are not stacked', () => { - it('it adds new items by inline inputs', () => { - const onChange = jest.fn(); - const { getAllByText, getByPlaceholderText, queryAllByText } = renderSubject({ - schema: testSchema(), - value, - onChange, - }); - expect(getAllByText('not stacked')).toHaveLength(2); - expect(queryAllByText('stacked')).toHaveLength(0); - - const name = getByPlaceholderText('Add new name'); - fireEvent.change(name, { target: { value: 'new name' } }); - fireEvent.keyDown(name, { key: 'Enter' }); - expect(onChange).toHaveBeenCalledWith([...value, { name: 'new name' }]); - expect(announce).toHaveBeenCalledWith(expect.stringContaining('press Enter to add')); - expect(name).toHaveFocus(); - }); - - it('serializes the value when adding a new item', () => { - const onChange = jest.fn(); - const uiOptions = { - properties: { - name: { - serializer: { - set: (val) => `serialized ${val}`, - }, - }, - }, - }; - const { getByPlaceholderText } = renderSubject({ - schema: testSchema(), - value, - uiOptions, - onChange, - }); - - const name = getByPlaceholderText('Add new name'); - fireEvent.change(name, { target: { value: 'new name' } }); - fireEvent.keyDown(name, { key: 'Enter' }); - expect(onChange).toHaveBeenCalledWith([...value, { name: 'serialized new name' }]); - }); - - it('can override the new input placeholder', () => { - const uiOptions = { - properties: { - name: { - placeholder: 'Name Custom Placeholder', - }, - age: { - placeholder: () => 'Age Custom Placeholder', - }, - }, - }; - const { getByPlaceholderText } = renderSubject({ schema: testSchema(), value, uiOptions }); - - expect(getByPlaceholderText('Name Custom Placeholder')).toBeInTheDocument(); - expect(getByPlaceholderText('Age Custom Placeholder')).toBeInTheDocument(); - }); - }); - - describe('when fields are stacked', () => { - it('stacks if there are more than 2 properties', () => { - const onChange = jest.fn(); - const { getAllByText, getByText, queryAllByText } = renderSubject({ - schema: testSchema({ height: { type: 'number' } }), - value, - onChange, - }); - expect(getAllByText('stacked')).toHaveLength(2); - expect(queryAllByText('not stacked')).toHaveLength(0); - - const add = getByText('Add'); - fireEvent.click(add); - expect(onChange).toHaveBeenCalledWith([...value, {}]); - }); - - it('stacks if at least one property is an expression', () => { - const { getAllByText, queryAllByText } = renderSubject({ - schema: testSchema({ age: { $role: 'expression' } }), - value, - }); - expect(getAllByText('stacked')).toHaveLength(2); - expect(queryAllByText('not stacked')).toHaveLength(0); - }); - - it('stacks if there are more than 2 ordered properties', () => { - const { getAllByText, queryAllByText } = renderSubject({ - schema: testSchema({ height: { type: 'number' } }), - uiOptions: { order: ['name', ['age', 'height']] }, - value, - }); - expect(getAllByText('stacked')).toHaveLength(2); - expect(queryAllByText('not stacked')).toHaveLength(0); - }); - }); -}); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/StringField.test.tsx b/Composer/packages/adaptive-form/src/components/fields/__tests__/StringField.test.tsx index e8b514085c..60f3672e13 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/StringField.test.tsx +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/StringField.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { render, fireEvent } from '@botframework-composer/test-utils'; -import { StringField, borderStyles } from '../StringField'; +import { StringField } from '../StringField'; import { fieldProps } from './testUtils'; @@ -45,47 +45,3 @@ describe('', () => { expect(container.querySelector('[aria-label="string field"]')).toBeInTheDocument(); }); }); - -describe('borderStyles', () => { - it('does not apply border styles when transparentBorder is false', () => { - expect(borderStyles(false, false, false)).toEqual({ - fieldGroup: { - borderRadius: undefined, - }, - }); - }); - - it('applies a transparent border when there is no error', () => { - expect(borderStyles(true, false, false)).toMatchInlineSnapshot(` - Object { - "fieldGroup": Object { - "borderColor": "transparent", - "borderRadius": undefined, - "selectors": Object { - ":hover": Object { - "borderColor": "#edebe9", - }, - }, - "transition": "border-color 0.1s linear", - }, - } - `); - }); - - it('does not apply a transparent border when there is an error', () => { - expect(borderStyles(true, true, false)).toMatchInlineSnapshot(` - Object { - "fieldGroup": Object { - "borderColor": undefined, - "borderRadius": undefined, - "selectors": Object { - ":hover": Object { - "borderColor": undefined, - }, - }, - "transition": "border-color 0.1s linear", - }, - } - `); - }); -}); diff --git a/Composer/packages/adaptive-form/src/components/fields/__tests__/testUtils.ts b/Composer/packages/adaptive-form/src/components/fields/__tests__/testUtils.ts index 31ce801608..86b52f8a2e 100644 --- a/Composer/packages/adaptive-form/src/components/fields/__tests__/testUtils.ts +++ b/Composer/packages/adaptive-form/src/components/fields/__tests__/testUtils.ts @@ -5,7 +5,6 @@ import assign from 'lodash/assign'; import { FieldProps } from '@bfc/extension-client'; const defaults: FieldProps = { - depth: 0, schema: {}, definitions: {}, uiOptions: {}, diff --git a/Composer/packages/adaptive-form/src/components/fields/index.ts b/Composer/packages/adaptive-form/src/components/fields/index.ts index 212aa8bb6b..5c5efb089a 100644 --- a/Composer/packages/adaptive-form/src/components/fields/index.ts +++ b/Composer/packages/adaptive-form/src/components/fields/index.ts @@ -9,7 +9,6 @@ export * from './FieldSets'; export * from './IntentField'; export * from './JsonField'; export * from './NumberField'; -export * from './ObjectArrayField'; export * from './ObjectField'; export * from './OneOfField'; export * from './OpenObjectField'; diff --git a/Composer/packages/adaptive-form/src/components/fields/styles.ts b/Composer/packages/adaptive-form/src/components/fields/styles.ts index 75d373b75e..e78c2b4d47 100644 --- a/Composer/packages/adaptive-form/src/components/fields/styles.ts +++ b/Composer/packages/adaptive-form/src/components/fields/styles.ts @@ -5,25 +5,6 @@ import { css } from '@emotion/core'; import { NeutralColors } from '@uifabric/fluent-theme'; import { FontSizes } from '@uifabric/styling'; -export const arrayField = { - field: css` - flex: 1; - margin-top: 0; - margin-bottom: 0; - display: flex; - - label: ArrayFieldField; - `, - - inputFieldContainer: css` - border-top: 1px solid ${NeutralColors.gray30}; - display: flex; - padding: 7px 0px; - - label: ArrayFieldInputFieldContainer; - `, -}; - export const arrayItem = { container: css` border-top: 1px solid ${NeutralColors.gray30}; @@ -42,18 +23,12 @@ export const arrayItem = { label: ArrayFieldItemField; `, - schemaFieldOverride: (stacked: boolean) => css` - display: flex; - flex-direction: ${stacked ? 'column' : 'row'}; + schemaFieldOverride: css` flex: 1; margin: 0; /* prevents field from overflowing when error present */ min-width: 0px; - & + & { - margin-left: ${stacked ? 0 : '16px'}; - } - label: ArrayItemSchemaFieldOverride; `, }; @@ -82,48 +57,3 @@ export const unsupportedField = { label: UnsupportedFieldDetails; `, }; - -export const objectArrayField = { - objectItemLabel: css` - display: flex; - - label: ObjectItemLabel; - `, - - objectItemValueLabel: css` - color: ${NeutralColors.gray130}; - flex: 1; - font-size: 14px; - margin-left: 7px; - & + & { - margin-left: 20px; - } - - label: ObjectItemValueLabel; - `, - - objectItemInputField: css` - flex: 1; - & + & { - margin-left: 20px; - } - - label: ObjectItemInputField; - `, - - arrayItemField: css` - flex: 1; - display: flex; - min-width: 0; - - label: ArrayItemField; - `, - - inputFieldContainer: css` - border-top: 1px solid ${NeutralColors.gray30}; - display: flex; - padding: 7px 0; - - label: InputFieldContainer; - `, -}; diff --git a/Composer/packages/adaptive-form/src/utils/__tests__/resolveFieldWidget.test.ts b/Composer/packages/adaptive-form/src/utils/__tests__/resolveFieldWidget.test.ts index 7c45c0b2fd..5831634c25 100644 --- a/Composer/packages/adaptive-form/src/utils/__tests__/resolveFieldWidget.test.ts +++ b/Composer/packages/adaptive-form/src/utils/__tests__/resolveFieldWidget.test.ts @@ -162,7 +162,7 @@ describe('resolveFieldWidget', () => { expect(ReturnedField).toEqual(DefaultFields.JsonField); }); - it('returns ObjectArrayField when item type is object', () => { + it('returns ArrayField when item type is object', () => { const schema = { type: 'array' as const, items: { @@ -171,7 +171,7 @@ describe('resolveFieldWidget', () => { }; const { field: ReturnedField } = resolveFieldWidget({ schema }); - expect(ReturnedField).toEqual(DefaultFields.ObjectArrayField); + expect(ReturnedField).toEqual(DefaultFields.ArrayField); }); it('can handle different items schemas', () => { @@ -197,7 +197,7 @@ describe('resolveFieldWidget', () => { }; const { field: ReturnedField2 } = resolveFieldWidget({ schema: objectArray }); - expect(ReturnedField2).toEqual(DefaultFields.ObjectArrayField); + expect(ReturnedField2).toEqual(DefaultFields.ArrayField); }); }); diff --git a/Composer/packages/adaptive-form/src/utils/arrayUtils.ts b/Composer/packages/adaptive-form/src/utils/arrayUtils.ts index 1c10778fe3..14924c9949 100644 --- a/Composer/packages/adaptive-form/src/utils/arrayUtils.ts +++ b/Composer/packages/adaptive-form/src/utils/arrayUtils.ts @@ -3,7 +3,7 @@ import { generateUniqueId } from '@bfc/shared'; import { ChangeHandler } from '@bfc/extension-client'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; type ArrayChangeHandler = (items: ArrayItem[]) => void; @@ -18,7 +18,7 @@ interface ArrayItemState { addItem: (newItem: ItemType) => void; } -const generateArrayItems = (value: ItemType[]): ArrayItem[] => { +const generateArrayItems = (value: ItemType[] = []): ArrayItem[] => { return value.map((i) => ({ id: generateUniqueId(), value: i, @@ -73,14 +73,24 @@ export const getArrayItemProps = ( }; export function useArrayItems( - items: ItemType[], + items: ItemType[] | undefined, onChange: ChangeHandler ): ArrayItemState { const [cache, setCache] = useState(generateArrayItems(items)); + const didMount = useRef(false); + useEffect(() => { + // If the user switches between types and the value + // gets reset to undefined, then reset the cache + if (didMount.current && items === undefined) { + setCache([]); + } + didMount.current = true; + }, [items]); + const handleChange = (newItems: ArrayItem[]) => { setCache(newItems); - onChange(newItems.map(({ value }) => value)); + onChange(newItems.map(({ value }) => value).filter(Boolean)); }; const addItem = (newItem: ItemType) => { diff --git a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts index 7a7df04afb..157601079d 100644 --- a/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts +++ b/Composer/packages/adaptive-form/src/utils/resolveFieldWidget.ts @@ -115,13 +115,7 @@ export function resolveFieldWidget(params: { case 'boolean': return { field: isOneOf ? DefaultFields.BooleanField : BooleanFieldWithIcon }; case 'array': { - const { items } = schema; - - if (Array.isArray(items) && typeof items[0] === 'object' && items[0].type === 'object') { - return { field: DefaultFields.ObjectArrayField }; - } else if (!Array.isArray(items) && typeof items === 'object' && items.type === 'object') { - return { field: DefaultFields.ObjectArrayField }; - } else if (!schema.items && !schema.oneOf) { + if (!schema.items && !schema.oneOf) { if (showIntellisense && isOneOf) { return { field: DefaultFields.IntellisenseJSONField, customProps: { style: { height: 100 } } }; } else if (showIntellisense && !isOneOf) { diff --git a/Composer/packages/client/src/pages/botProject/adapters/ExternalAdapterModal.tsx b/Composer/packages/client/src/pages/botProject/adapters/ExternalAdapterModal.tsx index c89904735c..52d64f905a 100644 --- a/Composer/packages/client/src/pages/botProject/adapters/ExternalAdapterModal.tsx +++ b/Composer/packages/client/src/pages/botProject/adapters/ExternalAdapterModal.tsx @@ -70,7 +70,6 @@ const AdapterModal = (props: Props) => {
= (newValue?: T) => void; export interface FieldProps { className?: string; definitions: SchemaDefinitions | undefined; - depth: number; description?: string; disabled?: boolean; enumOptions?: string[]; @@ -31,12 +30,12 @@ export interface FieldProps { readonly?: boolean; schema: JSONSchema7; required?: boolean; - transparentBorder?: boolean; uiOptions: UIOptions; value?: T; focused?: boolean; style?: React.CSSProperties; cursorPosition?: number; + isRoot?: boolean; hasIcon?: boolean; onChange: ChangeHandler; diff --git a/Composer/packages/ui-plugins/schema-editor/src/__tests__/SchemaEditorField.test.tsx b/Composer/packages/ui-plugins/schema-editor/src/__tests__/SchemaEditorField.test.tsx index cba3ae8d5e..b633b13fac 100644 --- a/Composer/packages/ui-plugins/schema-editor/src/__tests__/SchemaEditorField.test.tsx +++ b/Composer/packages/ui-plugins/schema-editor/src/__tests__/SchemaEditorField.test.tsx @@ -37,7 +37,7 @@ describe('Schema Editor', () => { const { baseElement, findAllByText, findByLabelText, getByPlaceholderText } = renderSchemaEditor({ updateDialogSchema, }); - const [add] = await findAllByText('Add'); + const [add] = await findAllByText('Add new'); add.click(); @@ -77,7 +77,7 @@ describe('Schema Editor', () => { const { baseElement, findAllByText, findByLabelText, getByPlaceholderText } = renderSchemaEditor({ updateDialogSchema, }); - const [, add] = await findAllByText('Add'); + const [, add] = await findAllByText('Add new'); add.click(); diff --git a/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx b/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx index 5eea067d24..3af829c313 100644 --- a/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx +++ b/Composer/packages/ui-plugins/select-dialog/src/DialogOptionsField.tsx @@ -190,7 +190,6 @@ const DialogOptionsField: React.FC = ({