From 4180fed1576e0525f2f5d5d3741071646ee17e94 Mon Sep 17 00:00:00 2001 From: TJ Durnford Date: Mon, 9 Dec 2019 19:03:09 -0800 Subject: [PATCH] style: Updated Array UI (#1617) * Update validation and choice array ui * Updated string array * Updated regex pattern * Updated array styles * Style cleanup * Fixed string array test * Fixed ObjectArray tests * Updated request header object * Small change to custom object field * Fixed styling * Updated dialogs * Fix margin issue * Updated callback dependencies * Added error message * Updated callback dependencies * Fixed margin issues * Fixed styles * Fixed transparent border issue * Fixed border color --- .../ArrayFieldTemplate/StringArray.test.tsx | 7 +- .../src/Form/ArrayFieldTemplate/ArrayItem.tsx | 26 +-- .../Form/ArrayFieldTemplate/ObjectArray.tsx | 138 ++++++++++-- .../Form/ArrayFieldTemplate/StringArray.tsx | 72 ++++++- .../src/Form/ArrayFieldTemplate/index.tsx | 8 +- .../src/Form/ArrayFieldTemplate/styles.ts | 48 +++++ .../Form/ObjectFieldTemplate/ObjectItem.tsx | 9 +- .../src/Form/ObjectFieldTemplate/index.tsx | 8 +- .../src/Form/ObjectFieldTemplate/styles.css | 17 +- .../src/Form/fields/BaseField.tsx | 51 ++--- .../src/Form/fields/CasesField.tsx | 181 ---------------- .../src/Form/fields/CustomObjectField.tsx | 201 ++++++++++++++++++ .../src/Form/fields/EditableField.tsx | 8 +- .../PromptField/ChoiceInput/Choices.tsx | 109 ++++++---- .../Form/fields/PromptField/Validations.tsx | 58 +++-- .../src/Form/fields/PromptField/styles.ts | 39 +++- .../src/Form/fields/RootField.tsx | 2 +- .../obiformeditor/src/Form/fields/index.tsx | 2 +- .../obiformeditor/src/Form/fields/styles.css | 3 + .../obiformeditor/src/Form/fields/styles.ts | 24 +++ .../obiformeditor/src/Form/styles.css | 4 + .../obiformeditor/src/Form/types.ts | 2 + .../src/Form/widgets/ExpressionWidget.tsx | 22 +- .../src/Form/widgets/TextWidget.tsx | 42 +++- .../obiformeditor/src/FormEditor.tsx | 4 + .../obiformeditor/src/schema/uischema.ts | 60 +++++- .../extensions/obiformeditor/src/styles.ts | 6 + 27 files changed, 819 insertions(+), 332 deletions(-) create mode 100644 Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/styles.ts delete mode 100644 Composer/packages/extensions/obiformeditor/src/Form/fields/CasesField.tsx create mode 100644 Composer/packages/extensions/obiformeditor/src/Form/fields/CustomObjectField.tsx diff --git a/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/StringArray.test.tsx b/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/StringArray.test.tsx index 7def88eba5..0a4396fe80 100644 --- a/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/StringArray.test.tsx +++ b/Composer/packages/extensions/obiformeditor/__tests__/Form/ArrayFieldTemplate/StringArray.test.tsx @@ -53,9 +53,10 @@ describe('', () => { it('can add an item', async () => { const onAddClick = jest.fn(); - const { findByText } = renderDefault({ canAdd: true, onAddClick }); - const addBtn = await findByText('Add'); - fireEvent.click(addBtn); + const { findByTestId } = renderDefault({ canAdd: true, onAddClick }); + const input = await findByTestId('string-array-text-input'); + fireEvent.change(input, { target: { value: 'test' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 13 }); expect(onAddClick).toHaveBeenCalled(); }); }); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ArrayItem.tsx b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ArrayItem.tsx index 94a2dedbab..ff6f919b49 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ArrayItem.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ArrayItem.tsx @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +/** @jsx jsx */ +import { jsx } from '@emotion/core'; import React from 'react'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; @@ -8,8 +10,10 @@ import { ArrayFieldItem } from '@bfcomposer/react-jsonschema-form'; import formatMessage from 'format-message'; import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; +import { arrayItem, arrayItemField } from './styles'; + const ArrayItem: React.FC = props => { - const { hasMoveUp, hasMoveDown, hasRemove, onReorderClick, onDropIndexClick, index } = props; + const { children, hasMoveUp, hasMoveDown, hasRemove, onReorderClick, onDropIndexClick, index } = props; // This needs to return true to dismiss the menu after a click. const fabricMenuItemClickHandler = fn => e => { @@ -42,17 +46,15 @@ const ArrayItem: React.FC = props => { ]; return ( -
-
{props.children}
-
- -
+
+
{children}
+
); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ObjectArray.tsx b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ObjectArray.tsx index 2147497540..28c683182b 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ObjectArray.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/ObjectArray.tsx @@ -1,34 +1,146 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React from 'react'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React, { useCallback, useState } from 'react'; import { ArrayFieldTemplateProps } from '@bfcomposer/react-jsonschema-form'; +import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +import { FontSizes, NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { JSONSchema6 } from 'json-schema'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; import formatMessage from 'format-message'; import { BaseField } from '../fields/BaseField'; +import { WidgetLabel } from '../widgets/WidgetLabel'; import ArrayItem from './ArrayItem'; +import { + arrayItemInputFieldContainer, + arrayItemField, + objectItemLabel, + objectItemInputField, + objectItemValueLabel, +} from './styles'; const ObjectArray: React.FunctionComponent = props => { - const { items, canAdd, onAddClick } = props; + const { canAdd, idSchema, items, onAddClick, schema = {}, uiSchema = {} } = props; + const { object } = (uiSchema['ui:options'] || {}) as any; + const { items: itemSchema = {} } = schema; + const { properties = {} } = itemSchema as JSONSchema6; + + const [value, setValue] = useState({}); + + const handleChange = useCallback( + property => (_, newValue?: string) => { + setValue(currentValue => ({ ...currentValue, [property]: newValue || '' })); + }, + [setValue] + ); + + const handleKeyDown = useCallback( + event => { + if (event.key.toLowerCase() === 'enter') { + event.preventDefault(); + + if (Object.keys(value).length) { + onAddClick(event, value); + setValue({}); + } + } + }, + [onAddClick, setValue, value] + ); + + const isVisible = useCallback( + (property: string) => { + const { items: itemsSchema } = uiSchema; + return !( + itemsSchema['ui:hidden'] && + Array.isArray(itemsSchema['ui:hidden']) && + itemsSchema['ui:hidden'].includes(property) + ); + }, + [uiSchema] + ); return ( + {object && ( +
+ {Object.keys(properties) + .filter(isVisible) + .map((key, index) => { + const { description, title } = properties[key] as JSONSchema6; + const { __id = '' } = idSchema[key] || {}; + + return ( +
+ +
+ ); + })} +
+
+ )}
{items.map((element, idx) => ( ))} - {canAdd && ( - - {formatMessage('Add')} - - )} + {canAdd && + (!object ? ( + + {formatMessage('Add')} + + ) : ( +
+
+ {Object.keys(properties) + .filter(isVisible) + .map((property, index, items) => ( +
+ +
+ ))} +
+ +
+ ))}
); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/StringArray.tsx b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/StringArray.tsx index 5f9161dd1c..636fde1437 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/StringArray.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/StringArray.tsx @@ -1,27 +1,77 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React from 'react'; -import { DefaultButton } from 'office-ui-fabric-react/lib/Button'; +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React, { useCallback, useState } from 'react'; import { ArrayFieldTemplateProps } from '@bfcomposer/react-jsonschema-form'; +import { FontSizes, NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; import formatMessage from 'format-message'; import { BaseField } from '../fields/BaseField'; +import { arrayItemField, arrayItemInputFieldContainer } from './styles'; import ArrayItem from './ArrayItem'; -import './styles.css'; - const StringArray: React.FunctionComponent = props => { + const { canAdd, items, onAddClick } = props; + const [value, setValue] = useState(''); + + const handleChange = useCallback((_, newValue?: string) => setValue(newValue || ''), [setValue]); + + const handleKeyDown = useCallback( + event => { + if (event.key.toLowerCase() === 'enter') { + event.preventDefault(); + + if (value) { + onAddClick(event, value); + setValue(''); + } + } + }, + [onAddClick, setValue, value] + ); + return ( - {props.items.map((element, idx) => ( - - ))} - {props.canAdd && ( - props.onAddClick(e)} styles={{ root: { marginTop: '10px' } }}> - {formatMessage('Add')} - +
+ {items.map((element, idx) => ( + + ))} +
+ {canAdd && ( +
+
+ +
+ +
)}
); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/index.tsx index 24dfac4a9e..8e8e7ade81 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/index.tsx @@ -10,15 +10,17 @@ import ObjectArray from './ObjectArray'; import IDialogArray from './IDialogArray'; const ArrayFieldTemplate: React.FunctionComponent = props => { - if (!props.schema.items) { + const { registry, schema } = props; + + if (!schema.items) { return null; } - let itemSchema = props.schema.items as any; + let itemSchema = schema.items as any; const $ref = itemSchema.$ref; if (!itemSchema.type && $ref) { - itemSchema = findSchemaDefinition($ref, props.registry.definitions); + itemSchema = findSchemaDefinition($ref, registry.definitions); } switch (itemSchema.type) { diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/styles.ts b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/styles.ts new file mode 100644 index 0000000000..3ed8aa9faa --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/ArrayFieldTemplate/styles.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { css } from '@emotion/core'; +import { NeutralColors } from '@uifabric/fluent-theme'; + +export const arrayItem = css` + display: flex; + padding: 7px 0; + + border-bottom: 1px solid ${NeutralColors.gray30}; + + &:first-type-of { + border-top: 1px solid ${NeutralColors.gray30}; + } +`; + +export const arrayItemField = css` + flex: 1; + display: flex; +`; + +export const arrayItemInputFieldContainer = css` + display: flex; + padding: 7px 0; +`; + +export const objectItemLabel = css` + border-bottom: 1px solid ${NeutralColors.gray30}; + display: flex; +`; + +export const objectItemValueLabel = css` + color: ${NeutralColors.gray130}; + flex: 1; + font-size: 14px; + margin-left: 7px; + & + & { + margin-left: 20px; + } +`; + +export const objectItemInputField = css` + flex: 1; + & + & { + margin-left: 20px; + } +`; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/ObjectItem.tsx b/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/ObjectItem.tsx index 52c1492894..18a5b98588 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/ObjectItem.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/ObjectItem.tsx @@ -8,6 +8,7 @@ import formatMessage from 'format-message'; import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; import classnames from 'classnames'; import { FIELDS_TO_HIDE, OBISchema } from '@bfc/shared'; +import { UiSchema } from '@bfcomposer/react-jsonschema-form'; import './styles.css'; @@ -18,10 +19,12 @@ interface ObjectItemProps { onEdit: (e) => void; onAdd: (e) => void; schema: OBISchema; + uiSchema: UiSchema; } export default function ObjectItem(props: ObjectItemProps) { - const { content, schema, onAdd, onEdit, onDropPropertyClick, name } = props; + const { content, schema, onAdd, onEdit, onDropPropertyClick, name, uiSchema } = props; + const { inline } = uiSchema['ui:options'] || ({} as any); if (name && FIELDS_TO_HIDE.includes(name)) { return null; @@ -59,8 +62,8 @@ export default function ObjectItem(props: ObjectItemProps) { const compoundType = schema.type && typeof schema.type === 'string' && ['array', 'object'].includes(schema.type); return ( -
-
{content}
+
+
{content}
{contextItems.length > 0 && (
= p {props.properties .filter(p => !isHidden(p.name)) .map(p => ( - onEditProperty(p.name)} onAdd={() => setShowModal(true)} /> + onEditProperty(p.name)} + onAdd={() => setShowModal(true)} + uiSchema={uiSchema} + /> ))} {canExpand(props) && ( <> diff --git a/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/styles.css b/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/styles.css index a5febcc0e0..d4baa27aed 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/styles.css +++ b/Composer/packages/extensions/obiformeditor/src/Form/ObjectFieldTemplate/styles.css @@ -1,5 +1,9 @@ -.ObjectItem { - display: flex; +.ObjectItemInline { + flex: 1; +} + +.ObjectItemInline + .ObjectItemInline { + margin-left: 20px; } .ObjectItem .ObjectItemField { @@ -9,6 +13,15 @@ margin: 10px 0; } +.ObjectItem .ObjectItemField { + padding: 0px 18px; + margin: 10px 0; +} + +.ObjectItemFieldInline { + flex: 1; +} + .ObjectItem .ObjectItemContext { flex-basis: auto; margin-left: 15px; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/BaseField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/BaseField.tsx index f1748a4e91..bd5b9aa7de 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/BaseField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/BaseField.tsx @@ -29,13 +29,14 @@ interface BaseFieldProps { } export function BaseField(props: BaseFieldProps): JSX.Element { - const { children, title, name, description, schema, uiSchema, idSchema, formContext, className } = props; + const { children, className, description, formContext, idSchema, name, schema, title, uiSchema } = props; const isRootBaseField = idSchema.__id === formContext.rootId; const fieldOverrides = get(formContext.editorSchema, `content.SDKOverrides`); - let titleOverride = undefined; - let descriptionOverride = undefined; - let helpLink = undefined; - let helpLinkText = undefined; + const { inline: displayInline, hideDescription } = (uiSchema['ui:options'] || {}) as any; + let titleOverride; + let descriptionOverride; + let helpLink; + let helpLinkText; let key = idSchema.__id; if (schema.title) { @@ -72,25 +73,27 @@ export function BaseField(props: BaseFieldProps): JSX.Element { {children} ) : ( -
-
-

{getTitle()}

- {descriptionOverride !== false && (descriptionOverride || description || schema.description) && ( -

- {getDescription()} - {helpLink && helpLinkText && ( - <> -
-
- - {helpLinkText} - - - )} -

- )} -
- {children} +
+ {!hideDescription && ( +
+

{getTitle()}

+ {descriptionOverride !== false && (descriptionOverride || description || schema.description) && ( +

+ {getDescription()} + {helpLink && helpLinkText && ( + <> +
+
+ + {helpLinkText} + + + )} +

+ )} +
+ )} +
{children}
); } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/CasesField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/CasesField.tsx deleted file mode 100644 index 59eaf1ac92..0000000000 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/CasesField.tsx +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import React, { useState } from 'react'; -import formatMessage from 'format-message'; -import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { IconButton } from 'office-ui-fabric-react/lib/Button'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; -import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; -import { CaseCondition } from '@bfc/shared'; -import cloneDeep from 'lodash/cloneDeep'; - -import { swap, remove } from '../utils'; -import { BFDFieldProps } from '../types'; -import { ExpressionWidget } from '../widgets/ExpressionWidget'; - -import { arrayItem, arrayItemValue, field } from './styles'; -import { EditableField } from './EditableField'; -interface CaseItemProps { - index: number; - value: string; - hasMoveUp: boolean; - hasMoveDown: boolean; - onReorder: (a: number, b: number) => void; - onDelete: (idx: number) => void; - onEdit: (idx: number, value?: string) => void; -} - -const CaseItem: React.FC = props => { - const { value, hasMoveDown, hasMoveUp, onReorder, onDelete, index, onEdit } = props; - const [key, setKey] = useState(value); - - // This needs to return true to dismiss the menu after a click. - const fabricMenuItemClickHandler = fn => e => { - fn(e); - return true; - }; - - const contextItems: IContextualMenuItem[] = [ - { - key: 'moveUp', - text: 'Move Up', - iconProps: { iconName: 'CaretSolidUp' }, - disabled: !hasMoveUp, - onClick: fabricMenuItemClickHandler(() => onReorder(index, index - 1)), - }, - { - key: 'moveDown', - text: 'Move Down', - iconProps: { iconName: 'CaretSolidDown' }, - disabled: !hasMoveDown, - onClick: fabricMenuItemClickHandler(() => onReorder(index, index + 1)), - }, - { - key: 'remove', - text: 'Remove', - iconProps: { iconName: 'Cancel' }, - onClick: fabricMenuItemClickHandler(() => onDelete(index)), - }, - ]; - - const handleEdit = (_e: any, newVal?: string) => { - onEdit(index, newVal); - }; - - const handleBlur = () => { - setKey(value); - if (!value) { - onDelete(index); - } - }; - - return ( -
-
- -
- -
- ); -}; - -export const CasesField: React.FC> = props => { - const { id, formData, schema, formContext } = props; - const [newBranch, setNewBranch] = useState(''); - - const handleChange = (_e: any, newValue?: string) => { - setNewBranch(newValue || ''); - }; - - const submitNewBranch = (e: React.KeyboardEvent) => { - if (e.key.toLowerCase() === 'enter') { - e.preventDefault(); - - if (newBranch) { - props.onChange([...props.formData, { value: newBranch }]); - setNewBranch(''); - } - } - }; - - const handleReorder = (aIdx: number, bIdx: number) => { - props.onChange(swap(props.formData, aIdx, bIdx)); - }; - - const handleDelete = (idx: number) => { - props.onChange(remove(props.formData, idx)); - }; - - const handleEdit = (idx, val) => { - const casesCopy = cloneDeep(props.formData) || []; - casesCopy[idx].value = val; - props.onChange(casesCopy); - }; - - return ( -
-
- -
-
- {formData.map((v, i) => ( - - ))} -
-
- {}} - autoComplete="off" - readOnly - /> -
-
-
-
- ); -}; - -CasesField.defaultProps = { - formData: [], -}; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/CustomObjectField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/CustomObjectField.tsx new file mode 100644 index 0000000000..6598e6bfbd --- /dev/null +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/CustomObjectField.tsx @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import React, { useCallback, useState } from 'react'; +import { FieldProps } from '@bfcomposer/react-jsonschema-form'; +import { FontSizes, NeutralColors, SharedColors } from '@uifabric/fluent-theme'; +import { IconButton } from 'office-ui-fabric-react/lib/Button'; +import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import formatMessage from 'format-message'; + +import { BaseField } from './BaseField'; +import { customObjectFieldContainer, customObjectFieldItem, customObjectFieldLabel } from './styles'; +import { EditableField } from './EditableField'; + +const ObjectItem = ({ + name: originalName, + formData, + value, + handleNameChange, + handleValueChange, + handleDropPropertyClick, +}) => { + const [name, setName] = useState(originalName); + const [errorMessage, setErrorMessage] = useState(''); + + const contextItems: IContextualMenuItem[] = [ + { + iconProps: { iconName: 'Cancel' }, + key: 'remove', + onClick: handleDropPropertyClick, + text: formatMessage('Remove'), + }, + ]; + + const handleBlur = useCallback(() => { + if (name !== originalName && Object.keys(formData).includes(name)) { + setErrorMessage(formatMessage('Keys must be unique')); + } else { + handleNameChange(name); + setErrorMessage(''); + } + }, [formData, handleNameChange, name, originalName, setErrorMessage]); + + return ( +
+
+ setName(newValue || '')} + options={{ transparentBorder: true }} + placeholder={formatMessage('Add a new key')} + value={name} + styles={{ + errorMessage: { display: 'block', paddingTop: 0 }, + root: { margin: '7px 0 7px 0' }, + }} + errorMessage={errorMessage} + /> +
+
+ +
+ +
+ ); +}; + +export const CustomObjectField: React.FC = props => { + const { + formData = {}, + schema: { additionalProperties }, + onChange, + } = props; + + const [name, setName] = useState(''); + const [value, setValue] = useState(''); + + const handleKeyDown = useCallback( + event => { + if (event.key.toLowerCase() === 'enter') { + event.preventDefault(); + + if (name && !Object.keys(formData).includes(name)) { + onChange({ ...formData, [name]: value }); + setName(''); + setValue(''); + } + } + }, + [formData, onChange, name, setName, setValue, value] + ); + + const handleNameChange = useCallback( + name => newName => { + const { [name]: value, ...rest } = formData; + const newFormData = !(newName || value) ? rest : { ...rest, [newName]: value }; + onChange(newFormData); + }, + [formData, onChange] + ); + const handleValueChange = useCallback( + name => (_, newValue) => { + onChange({ ...formData, [name]: newValue || '' }); + }, + [formData, onChange] + ); + const handleDropPropertyClick = useCallback( + name => () => { + const { [name]: _, ...newFormData } = formData; + onChange(newFormData); + }, + [formData, onChange] + ); + + return ( + +
+
{formatMessage('Key')}
+
{formatMessage('Value')}
+
+ {Object.entries(formData).map(([name, value], index) => { + return ( + + ); + })} + {additionalProperties && ( +
+
+ setName(newValue || '')} + onKeyDown={handleKeyDown} + placeholder={formatMessage('Add a new key')} + value={name} + styles={{ + root: { margin: '7px 0 7px 0' }, + }} + /> +
+
+ setValue(newValue || '')} + onKeyDown={handleKeyDown} + placeholder={formatMessage('Add a new value')} + value={value} + iconProps={{ + iconName: 'ReturnKey', + style: { color: SharedColors.cyanBlue10, opacity: 0.6 }, + }} + styles={{ + root: { margin: '7px 0 7px 0' }, + }} + /> +
+ +
+ )} +
+ ); +}; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/EditableField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/EditableField.tsx index 11979288fe..5bf5262e7a 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/EditableField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/EditableField.tsx @@ -10,10 +10,12 @@ interface EditableFieldProps extends ITextFieldProps { onChange: (e: any, newTitle?: string) => void; placeholder?: string; fontSize?: string; + options?: any; } export const EditableField: React.FC = props => { - const { styles = {}, placeholder, fontSize, onChange, onBlur, value, ...rest } = props; + const { styles = {}, placeholder, fontSize, onChange, onBlur, value, options = {}, ...rest } = props; + const { transparentBorder } = options; const [editing, setEditing] = useState(false); const [hasFocus, setHasFocus] = useState(false); const [localValue, setLocalValue] = useState(value); @@ -40,7 +42,7 @@ export const EditableField: React.FC = props => { let borderColor: string | undefined = undefined; if (!editing) { - borderColor = localValue ? 'transparent' : NeutralColors.gray30; + borderColor = localValue || transparentBorder ? 'transparent' : NeutralColors.gray30; } return ( @@ -50,7 +52,7 @@ export const EditableField: React.FC = props => { value={localValue} styles={mergeStyleSets( { - root: { margin: '5px 0 7px -9px' }, + root: { margin: '5px 0 7px 0' }, field: { fontSize: fontSize, selectors: { diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/ChoiceInput/Choices.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/ChoiceInput/Choices.tsx index d08eedb96b..e94bc9e93b 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/ChoiceInput/Choices.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/ChoiceInput/Choices.tsx @@ -9,12 +9,19 @@ import formatMessage from 'format-message'; import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; +import { NeutralColors, FontSizes, SharedColors } from '@uifabric/fluent-theme'; import { IChoice } from '@bfc/shared'; -import { field, choiceItemContainer, choiceItemValue, choiceItemSynonyms } from '../styles'; +import { + field, + choiceField, + choiceItem, + choiceItemContainer, + choiceItemLabel, + choiceItemValue, + choiceItemValueLabel, +} from '../styles'; import { swap, remove } from '../../../utils'; -// import { FormContext } from '../../../types'; import { WidgetLabel } from '../../../widgets/WidgetLabel'; import { EditableField } from '../../EditableField'; @@ -79,25 +86,26 @@ const ChoiceItem: React.FC = props => { }; return ( -
+
-
+
@@ -158,39 +166,18 @@ export const Choices: React.FC = props => { return ( -
-
- -
-
- -
-
- -
-
-
+ +
+
{formatMessage('Choice Name')}
+
{formatMessage('Synonyms (Optional)')}
-
+
{formData && formData.map((c, i) => ( = props => { /> ))}
+
+
+
+ +
+
+ +
+
+ +
+
+
); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Validations.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Validations.tsx index 376e93097f..343dbd65e9 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Validations.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/Validations.tsx @@ -8,13 +8,14 @@ import formatMessage from 'format-message'; import { JSONSchema6 } from 'json-schema'; import { IconButton } from 'office-ui-fabric-react/lib/Button'; import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { NeutralColors, FontSizes } from '@uifabric/fluent-theme'; +import { NeutralColors, FontSizes, SharedColors } from '@uifabric/fluent-theme'; import { swap, remove } from '../../utils'; import { ExpressionWidget } from '../../widgets/ExpressionWidget'; import { FormContext } from '../../types'; +import { WidgetLabel } from '../../widgets/WidgetLabel'; -import { validationItem, validationItemValue, field } from './styles'; +import { validationItem, validationItemInput, validationItemValue, field } from './styles'; interface ValidationItemProps { index: number; @@ -73,7 +74,7 @@ const ValidationItem: React.FC = props => { }; return ( -
+
= props => { schema={schema} onChange={handleEdit} onBlur={handleBlur} + styles={{ + root: { margin: '7px 0 7px 0' }, + }} />
= props => { const { schema, id, formData, formContext } = props; + const { description } = schema; const [newValidation, setNewValidation] = useState(''); const handleChange = (_e: any, newValue?: string) => { @@ -138,19 +143,7 @@ export const Validations: React.FC = props => { return (
-
- -
+
{formData.map((v, i) => ( = props => { /> ))}
+
+
+ +
+ +
); }; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts index 4cc3765d0a..894331a0c0 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/PromptField/styles.ts @@ -3,6 +3,7 @@ import { IPivotStyles } from 'office-ui-fabric-react/lib/Pivot'; import { css } from '@emotion/core'; +import { NeutralColors } from '@uifabric/fluent-theme'; export const tabs: Partial = { root: { @@ -24,13 +25,19 @@ export const tabsContainer = css` border-bottom: 1px solid #c8c6c4; `; -export const validationItem = css` +export const validationItemInput = css` display: flex; align-items: center; padding-left: 10px; +`; - & + & { - margin-top: 10px; +export const validationItem = css` + ${validationItemInput} + + border-bottom: 1px solid ${NeutralColors.gray30}; + + &:first-of-type { + border-top: 1px solid ${NeutralColors.gray30}; } `; @@ -79,11 +86,29 @@ export const choiceItemContainer = (align = 'center') => css` align-items: ${align}; `; -export const choiceItemValue = css` - width: 180px; +export const choiceField = css` + margin-bottom: 7px; +`; + +export const choiceItem = css` + border-bottom: 1px solid ${NeutralColors.gray30}; `; -export const choiceItemSynonyms = css` +export const choiceItemValue = css` flex: 1; - margin-left: 20px; + + & + & { + margin-left: 20px; + } +`; + +export const choiceItemLabel = css` + border-bottom: 1px solid ${NeutralColors.gray30}; + padding-bottom: 7px; +`; + +export const choiceItemValueLabel = css` + color: ${NeutralColors.gray130}; + font-size: 12px; + margin-left: 7px; `; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/RootField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/RootField.tsx index 19be5d0f1c..83ede66ccf 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/RootField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/RootField.tsx @@ -66,7 +66,7 @@ export const RootField: React.FC = props => {

{getSubTitle()}

diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/index.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/index.tsx index ab91a6f61c..406874c571 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/index.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/index.tsx @@ -3,8 +3,8 @@ import './styles.css'; -export * from './CasesField'; export * from './CodeField'; +export * from './CustomObjectField'; export * from './JsonField'; export * from './PromptField'; export * from './RecognizerField'; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.css b/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.css index 67b5509d34..bcaedcbaa5 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.css +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.css @@ -10,6 +10,9 @@ margin-bottom: 20px; white-space: pre-line; } +.BaseFieldInline { + display: flex; +} .RootFieldTitle { border-bottom: 1px solid #c8c6c4; padding: 0 18px; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.ts b/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.ts index 9e687e36e4..d9e7563ed8 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.ts +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/styles.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { css } from '@emotion/core'; +import { NeutralColors } from '@uifabric/fluent-theme'; export const arrayItem = css` display: flex; @@ -24,3 +25,26 @@ export const arrayItemDefault = css` export const field = css` margin: 10px 0; `; + +export const customObjectFieldContainer = css` + display: flex; + + &:not(:last-child) { + border-bottom: 1px solid ${NeutralColors.gray30}; + } +`; + +export const customObjectFieldItem = css` + flex: 1; + + & + & { + margin-left: 20px; + } +`; + +export const customObjectFieldLabel = css` + color: ${NeutralColors.gray130}; + font-size: 12px; + margin-left: 7px; + padding-bottom: 5px; +`; diff --git a/Composer/packages/extensions/obiformeditor/src/Form/styles.css b/Composer/packages/extensions/obiformeditor/src/Form/styles.css index 0b4f5fdfc4..80054882ad 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/styles.css +++ b/Composer/packages/extensions/obiformeditor/src/Form/styles.css @@ -1,3 +1,7 @@ +.FieldTemplate { + flex: 1; +} + .FormContainer { flex: 1; /** diff --git a/Composer/packages/extensions/obiformeditor/src/Form/types.ts b/Composer/packages/extensions/obiformeditor/src/Form/types.ts index eddcd78cb6..657ef86324 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/types.ts +++ b/Composer/packages/extensions/obiformeditor/src/Form/types.ts @@ -36,6 +36,8 @@ export interface BFDWidgetProps extends Partial { options?: { label?: string | false; enumOptions?: EnumOption[]; + hideLabel?: boolean; + transparentBorder?: boolean; }; } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx index 77d794370a..fcc04efe77 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/ExpressionWidget.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. import React from 'react'; -import { TextField, ITextFieldProps } from 'office-ui-fabric-react/lib/TextField'; +import { TextField, ITextFieldProps, ITextFieldStyles } from 'office-ui-fabric-react/lib/TextField'; import { MessageBar, MessageBarType } from 'office-ui-fabric-react/lib/MessageBar'; import { JSONSchema6 } from 'json-schema'; import formatMessage from 'format-message'; @@ -21,6 +21,8 @@ interface ExpresionWidgetProps extends ITextFieldProps { onChange: (event: React.FormEvent, newValue?: string) => void; /** Set to true to display as inline text that is editable on hover */ editable?: boolean; + styles?: Partial; + options?: any; } const getDefaultErrorMessage = () => { @@ -39,9 +41,21 @@ const getDefaultErrorMessage = () => { }; export const ExpressionWidget: React.FC = props => { - const { rawErrors, formContext, schema, id, label, editable, hiddenErrMessage, onValidate, ...rest } = props; + const { + rawErrors, + formContext, + schema, + id, + label, + editable, + hiddenErrMessage, + onValidate, + options = {}, + ...rest + } = props; const { shellApi } = formContext; const { description } = schema; + const { hideLabel } = options; const onGetErrorMessage = async (value: string): Promise => { if (!value) { @@ -88,18 +102,20 @@ export const ExpressionWidget: React.FC = props => { return ( <> - + {!hideLabel && !!label && } ); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/TextWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/TextWidget.tsx index 1723cf2fed..564e374dce 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/TextWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/TextWidget.tsx @@ -1,9 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React from 'react'; -import { TextField } from 'office-ui-fabric-react/lib/TextField'; +import React, { useState } from 'react'; +import { NeutralColors } from '@uifabric/fluent-theme'; import { SpinButton } from 'office-ui-fabric-react/lib/SpinButton'; +import { TextField } from 'office-ui-fabric-react/lib/TextField'; import { BFDWidgetProps } from '../types'; @@ -39,11 +40,14 @@ export function TextWidget(props: ITextWidgetProps) { rawErrors, hiddenErrMessage, onValidate, + options = {}, } = props; const { description, examples = [], type, $role } = schema; - + const { hideLabel, transparentBorder } = options; let placeholderText = placeholder; + const [hasFocus, setHasFocus] = useState(false); + if (!placeholderText && examples.length > 0) { placeholderText = `ex. ${examples.join(', ')}`; } @@ -64,7 +68,7 @@ export function TextWidget(props: ITextWidgetProps) { return ( <> - + {!hideLabel && } onBlur && onBlur(id, value), + onBlur: () => { + onBlur && onBlur(id, value); + setHasFocus(false); + }, onChange: (_, newValue?: string) => onChange(newValue), - onFocus: () => onFocus && onFocus(id, value), + onFocus: () => { + onFocus && onFocus(id, value); + setHasFocus(true); + }, placeholder: placeholderText, readOnly: Boolean(schema.const) || readonly, }; @@ -96,20 +106,36 @@ export function TextWidget(props: ITextWidgetProps) { return ( ); } return ( <> - - + {!hideLabel && } + ); } diff --git a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx index 30c1a2642b..4c39f3eaf8 100644 --- a/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx +++ b/Composer/packages/extensions/obiformeditor/src/FormEditor.tsx @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +/** @jsx jsx */ +import { Global, jsx } from '@emotion/core'; import React, { useState } from 'react'; import { Dropdown } from 'office-ui-fabric-react/lib/Dropdown'; import { JSONSchema6Definition, JSONSchema6 } from 'json-schema'; @@ -13,6 +15,7 @@ import Form from './Form'; import { uiSchema } from './schema/uischema'; import { getMemoryOptions } from './Form/utils'; import { FormMemory, FormData } from './types'; +import { root } from './styles'; const getType = (data: FormData): string | undefined => { return data.$type; @@ -77,6 +80,7 @@ export const FormEditor: React.FunctionComponent = props => { return (
+ {memoryOptions.length > 0 && (