diff --git a/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx b/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx deleted file mode 100644 index c7f44f0458..0000000000 --- a/Composer/packages/extensions/visual-designer/__tests__/widgets/ChoiceInput.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import React from 'react'; -import { render, cleanup } from 'react-testing-library'; - -import { ChoiceInput } from '../../src/widgets/ChoiceInput'; -import { ObiTypes } from '../../src/constants/ObiTypes'; - -describe('', () => { - let renderResult, id, data, onEvent, onResize; - - beforeEach(() => { - onEvent = jest.fn(); - onResize = jest.fn(); - id = 'choiceInput'; - }); - afterEach(cleanup); - - it("should have no choice dom when data's choice is null", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: null, - }; - - renderResult = render(); - - const { queryByTestId } = renderResult; - expect(queryByTestId('ChoiceInput')).toBeNull(); - }); - - it("should show as many choice items as there are when data's choices length is less than 4", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: [ - { - value: '1', - }, - { - value: '2', - }, - { - value: '3', - }, - ], - }; - - renderResult = render(); - - const { getAllByRole } = renderResult; - const choices = getAllByRole('choice'); - - expect(choices).toHaveLength(3); - }); - - it("should show three choice items and 'more' dom when data's choices length is more than 3", () => { - data = { - $type: ObiTypes.ChoiceInput, - choices: [ - { - value: '1', - }, - { - value: '2', - }, - { - value: '3', - }, - { - value: '4', - }, - ], - }; - - renderResult = render(); - - const { getAllByRole, getByTestId } = renderResult; - const choices = getAllByRole('choice'); - const hasMore = getByTestId('hasMore'); - - expect(choices).toHaveLength(3); - expect(hasMore).toBeTruthy(); - }); -}); diff --git a/Composer/packages/extensions/visual-designer/package.json b/Composer/packages/extensions/visual-designer/package.json index acb0e94a99..736dba2a2a 100644 --- a/Composer/packages/extensions/visual-designer/package.json +++ b/Composer/packages/extensions/visual-designer/package.json @@ -25,7 +25,8 @@ }, "dependencies": { "@bfc/shared": "*", - "@emotion/core": "^10.0.7", + "@emotion/core": "^10.0.27", + "@emotion/styled": "^10.0.27", "@types/react": "16.9.0", "classnames": "^2.2.6", "create-react-class": "^15.6.3", diff --git a/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx new file mode 100644 index 0000000000..ef90a437fe --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/common/ListOverview.tsx @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React, { FC } from 'react'; + +import { SingleLineDiv } from '../elements/styledComponents'; + +export interface ListOverviewProps { + items: string[]; + renderItem: (item: object | string) => JSX.Element; + maxCount: number; +} +export const ListOverview: FC = ({ items, renderItem, maxCount }) => { + if (!Array.isArray(items)) { + return null; + } + return ( +
+ {items.slice(0, maxCount).map((item, index) => { + return ( +
+ {renderItem(item)} +
+ ); + })} + {items.length > maxCount ? ( + + {`${items.length - maxCount} more`} + + ) : null} +
+ ); +}; diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx new file mode 100644 index 0000000000..f4afc5467f --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.tsx @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { css } from '@emotion/core'; +import styled from '@emotion/styled'; +import { Link } from 'office-ui-fabric-react/lib/Link'; + +import { ObiColors } from '../../constants/ElementColors'; + +import { MultiLineDivProps, DivProps } from './styledComponents.types'; + +const dynamicStyle = props => + css` + color: ${props.color || ObiColors.Black}; + `; + +export const LinkBtn = styled(Link)(props => ({ + color: props.color || ObiColors.AzureBlue, +})); + +export const Span = styled.span` + ${dynamicStyle} +`; + +export const BorderedDiv = styled.div(props => ({ + color: props.color || ObiColors.Black, + width: props.width, + height: props.height, + padding: '2px 0 0 8px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + fontFamily: 'Segoe UI', + fontSize: '12px', + lineHeight: '14px', + border: '1px solid #C4C4C4', + boxSizing: 'border-box', +})); + +export const MultiLineDiv = styled.div(props => ({ + color: props.color || ObiColors.Black, + fontSize: '12px', + height: `${(props.lineNum || 1) * 19}px`, + lineHeight: '19px', + fontFamily: 'Segoe UI', + overflow: 'hidden', + textOverflow: 'ellipsis', + wordBreak: 'break-word', + display: '-webkit-box', + '-webkit-line-clamp': `${props.lineNum || 1}`, + '-webkit-box-orient': 'vertical', +})); + +export const SingleLineDiv = styled.div(props => ({ + width: props.width || 150, + height: props.height || '19px', + color: props.color || ObiColors.Black, + fontSize: '12px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + overflow: 'hidden', + lineHeight: '19px', + fontFamily: 'Segoe UI', +})); diff --git a/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts new file mode 100644 index 0000000000..dd6d1680e8 --- /dev/null +++ b/Composer/packages/extensions/visual-designer/src/components/elements/styledComponents.types.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { DetailedHTMLProps, HTMLAttributes, ComponentClass, FC } from 'react'; + +export type ElementProps = DetailedHTMLProps, HTMLDivElement>; +export interface DivProps extends ElementProps { + width?: number; + height?: number; +} + +export interface MultiLineDivProps extends DivProps { + lineNum?: number; +} +export type ElementComponent = FC | ComponentClass; diff --git a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts index 1ec57683df..4800cf9a5c 100644 --- a/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts +++ b/Composer/packages/extensions/visual-designer/src/constants/ElementSizes.ts @@ -39,6 +39,14 @@ export const ChoiceInputSize = { export const ChoiceInputMarginTop = 8; export const ChoiceInputMarginBottom = 10; +export const PropertyAssignmentSize = { + width: 155, + height: 16, +}; + +export const AssignmentMarginTop = 8; +export const AssignmentMarginBottom = 8; + export const EventNodeSize = { width: 240, height: 125, diff --git a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts index cbea2e38e3..17c9f34b51 100644 --- a/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts +++ b/Composer/packages/extensions/visual-designer/src/layouters/measureJsonBoundary.ts @@ -13,6 +13,9 @@ import { ChoiceInputMarginTop, ChoiceInputMarginBottom, IconBrickSize, + AssignmentMarginTop, + PropertyAssignmentSize, + AssignmentMarginBottom, } from '../constants/ElementSizes'; import { transformIfCondtion } from '../transformers/transformIfCondition'; import { transformSwitchCondition } from '../transformers/transformSwitchCondition'; @@ -75,6 +78,20 @@ export function measureChoiceInputDetailBoundary(data): Boundary { return new Boundary(width, height); } +export function measurePropertyAssignmentBoundary(data): Boundary { + const width = InitNodeSize.width; + const height = Math.max( + InitNodeSize.height / 2 + + (data.assignments && Array.isArray(data.assignments) + ? (data.assignments.length < 4 + ? data.assignments.length * (PropertyAssignmentSize.height + AssignmentMarginTop) + : 4 * (PropertyAssignmentSize.height + AssignmentMarginTop)) + AssignmentMarginBottom + : 0), + InitNodeSize.height + ); + return new Boundary(width, height); +} + function measureBaseInputBoundary(data): Boundary { const { botAsks, userAnswers } = transformBaseInput(data, ''); return calculateBaseInputBoundary(measureJsonBoundary(botAsks.json), measureJsonBoundary(userAnswers.json)); @@ -126,6 +143,9 @@ export function measureJsonBoundary(json): Boundary { case ObiTypes.InvalidPromptBrick: boundary = new Boundary(IconBrickSize.width, IconBrickSize.height); break; + case ObiTypes.SetProperties: + boundary = measurePropertyAssignmentBoundary(json); + break; case ObiTypes.EndDialog: case ObiTypes.EndTurn: case ObiTypes.RepeatDialog: diff --git a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx index e0ad27eb69..34b36837df 100644 --- a/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx +++ b/Composer/packages/extensions/visual-designer/src/schema/uischema.tsx @@ -13,10 +13,12 @@ import { PromptWidget } from '../widgets/PromptWidget'; import { IfConditionWidget } from '../widgets/IfConditionWidget'; import { SwitchConditionWidget } from '../widgets/SwitchConditionWidget'; import { ForeachWidget } from '../widgets/ForeachWidget'; -import { ChoiceInputChoices } from '../widgets/ChoiceInput'; import { ActionHeader } from '../widgets/ActionHeader'; import { ElementIcon } from '../utils/obiPropertyResolver'; import { ObiColors } from '../constants/ElementColors'; +import { SingleLineDiv, BorderedDiv } from '../components/elements/styledComponents'; +import { PropertyAssignmentSize, ChoiceInputSize } from '../constants/ElementSizes'; +import { ListOverview } from '../components/common/ListOverview'; import { UISchema, UIWidget } from './uischema.types'; @@ -40,7 +42,6 @@ const BaseInputSchema: UIWidget = { icon: ElementIcon.User, menu: 'none', content: data => data.property || '', - children: data => (data.$type === SDKTypes.ChoiceInput ? : null), colors: { theme: ObiColors.LightBlue, icon: ObiColors.AzureBlue, @@ -48,6 +49,31 @@ const BaseInputSchema: UIWidget = { }, }; +const ChoiceInputSchema: UIWidget = Object.assign({}, BaseInputSchema, { + userInput: { + 'ui:widget': ActionCard, + title: data => `User Input (${getInputType(data.$type)})`, + disableSDKTitle: true, + icon: ElementIcon.User, + menu: 'none', + content: data => {data.property || ''}, + children: data => { + const renderItem = item => { + const content = item.value; + return ( + + {content} + + ); + }; + return ; + }, + colors: { + theme: ObiColors.LightBlue, + icon: ObiColors.AzureBlue, + }, + }, +}); export const uiSchema: UISchema = { default: { 'ui:widget': ActionCard, @@ -102,7 +128,7 @@ export const uiSchema: UISchema = { [SDKTypes.DateTimeInput]: BaseInputSchema, [SDKTypes.NumberInput]: BaseInputSchema, [SDKTypes.TextInput]: BaseInputSchema, - [SDKTypes.ChoiceInput]: BaseInputSchema, + [SDKTypes.ChoiceInput]: ChoiceInputSchema, [SDKTypes.BeginDialog]: { 'ui:widget': DialogRefCard, dialog: data => data.dialog, @@ -123,6 +149,22 @@ export const uiSchema: UISchema = { [SDKTypes.SetProperties]: { 'ui:widget': ActionCard, content: data => `Set ${Array.isArray(data.assignments) ? data.assignments.length : 0} property values`, + children: data => { + const renderItem = item => { + const content = `${item.property} = ${item.value}`; + return ( + + {content} + + ); + }; + return ; + }, }, [SDKTypes.DeleteProperty]: { 'ui:widget': ActionCard, diff --git a/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx b/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx deleted file mode 100644 index b225f17ebd..0000000000 --- a/Composer/packages/extensions/visual-designer/src/widgets/ChoiceInput.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx } from '@emotion/core'; -import { FC } from 'react'; -import { DialogGroup, PromptTab } from '@bfc/shared'; - -import { ChoiceInputSize, ChoiceInputMarginTop } from '../constants/ElementSizes'; -import { NodeEventTypes } from '../constants/NodeEventTypes'; -import { NodeColors } from '../constants/ElementColors'; -import { measureJsonBoundary } from '../layouters/measureJsonBoundary'; -import { ElementIcon } from '../utils/obiPropertyResolver'; -import { FormCard } from '../components/nodes/templates/FormCard'; -import { NodeProps } from '../components/nodes/nodeProps'; -import { getUserAnswersTitle } from '../components/nodes/utils'; - -export const ChoiceInputChoices = ({ choices }) => { - if (!Array.isArray(choices)) { - return null; - } - - return ( -
- {choices.map(({ value }, index) => { - if (index < 3) { - return ( -
- {value} -
- ); - } - })} - {Array.isArray(choices) && choices.length > 3 ? ( -
- {`${choices.length - 3} more`} -
- ) : null} -
- ); -}; - -export const ChoiceInput: FC = ({ id, data, onEvent }): JSX.Element => { - const boundary = measureJsonBoundary(data); - const { choices } = data; - const children = ; - - return ( - '} - onClick={() => { - onEvent(NodeEventTypes.Focus, { id, tab: PromptTab.USER_INPUT }); - }} - styles={{ width: boundary.width, height: boundary.height }} - > - {children} - - ); -}; diff --git a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx b/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx index 9b90ed157e..75a7a97a3d 100644 --- a/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx +++ b/Composer/packages/extensions/visual-designer/src/widgets/DialogRefCard.tsx @@ -5,8 +5,8 @@ import { jsx } from '@emotion/core'; import { generateSDKTitle } from '@bfc/shared'; import get from 'lodash/get'; -import { Link } from 'office-ui-fabric-react/lib/Link'; +import { LinkBtn } from '../components/elements/styledComponents'; import { FormCard } from '../components/nodes/templates/FormCard'; import { WidgetContainerProps, WidgetComponent } from '../schema/uischema.types'; import { ObiColors } from '../constants/ElementColors'; @@ -39,14 +39,14 @@ export const DialogRefCard: WidgetComponent = ({ const nodeColors = { themeColor: colors.theme, iconColor: colors.icon }; const calleeDialog = typeof dialog === 'object' ? get(dialog, '$ref') : dialog; const dialogRef = ( - { e.stopPropagation(); onEvent(NodeEventTypes.OpenDialog, { caller: id, callee: calleeDialog }); }} > {calleeDialog} - + ); return (