Skip to content

Commit

Permalink
feature: implement new ProjectTree design with popout menus (microsof…
Browse files Browse the repository at this point in the history
…t#4361)

* Update en-US.json

* bring in stuff from the draft branch

* make deletion work

* add error/warning icons

* read notification map for state

* fix type-checking and start on unit tests

* add sampleDialog and fix more tests

* add showAll

* rename to onAllSelected because it's a callback

* update unit tests

* fix onSelect handling in ProjectTree

* Update qna.test.tsx

* Update design.test.tsx

* add unit test

* fixes from PR comments

Co-authored-by: Chris Whitten <christopher.whitten@microsoft.com>
  • Loading branch information
beyackle and cwhitten authored Oct 20, 2020
1 parent 8ab8cdd commit 408060d
Show file tree
Hide file tree
Showing 23 changed files with 753 additions and 270 deletions.
31 changes: 16 additions & 15 deletions Composer/packages/client/__tests__/components/design.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,40 @@

import * as React from 'react';
import { fireEvent } from '@botframework-composer/test-utils';
import { DialogInfo } from '@bfc/shared';

import { renderWithRecoil } from '../testUtils';
import { dialogs } from '../constants.json';
import { SAMPLE_DIALOG } from '../mocks/sampleDialog';
import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree';
import { TriggerCreationModal } from '../../src/components/ProjectTree/TriggerCreationModal';
import { CreateDialogModal } from '../../src/pages/design/createDialogModal';
import { dialogsState, currentProjectIdState, botProjectIdsState, schemasState } from '../../src/recoilModel';

jest.mock('@bfc/code-editor', () => {
return {
LuEditor: () => <div></div>,
};
});
const projectId = '1234a-324234';
const projectId = '12345.6789';
const dialogs = [SAMPLE_DIALOG];

const initRecoilState = ({ set }) => {
set(currentProjectIdState, projectId);
set(botProjectIdsState, [projectId]);
set(dialogsState(projectId), dialogs);
set(schemasState(projectId), { sdk: { content: {} } });
};

describe('<ProjectTree/>', () => {
it('should render the ProjectTree', async () => {
const dialogId = 'todobot';
const selected = 'triggers[0]';
const handleSelect = jest.fn(() => {});
const handleDeleteDialog = jest.fn(() => {});
const handleDeleteTrigger = jest.fn(() => {});
const { findByText } = renderWithRecoil(
<ProjectTree
dialogId={dialogId}
dialogs={(dialogs as unknown) as DialogInfo[]}
selected={selected}
onDeleteDialog={handleDeleteDialog}
onDeleteTrigger={handleDeleteTrigger}
onSelect={handleSelect}
/>

const { findByTestId } = renderWithRecoil(
<ProjectTree onDeleteDialog={handleDeleteDialog} onDeleteTrigger={handleDeleteTrigger} onSelect={handleSelect} />,
initRecoilState
);
const node = await findByText('addtodo');
const node = await findByTestId('EchoBot-1_Greeting');
fireEvent.click(node);
expect(handleSelect).toHaveBeenCalledTimes(1);
});
Expand Down
50 changes: 33 additions & 17 deletions Composer/packages/client/__tests__/components/projecttree.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,57 @@
import * as React from 'react';
import { fireEvent } from '@botframework-composer/test-utils';

import { dialogs } from '../constants.json';
import { ProjectTree } from '../../src/components/ProjectTree/ProjectTree';
import { renderWithRecoil } from '../testUtils';
import { SAMPLE_DIALOG } from '../mocks/sampleDialog';
import { dialogsState, currentProjectIdState, botProjectIdsState, schemasState } from '../../src/recoilModel';

const projectId = '12345.6789';
const dialogs = [SAMPLE_DIALOG];

const initRecoilState = ({ set }) => {
set(currentProjectIdState, projectId);
set(botProjectIdsState, [projectId]);
set(dialogsState(projectId), dialogs);
set(schemasState(projectId), { sdk: { content: {} } });
};

describe('<ProjectTree/>', () => {
it('should render the projecttree', async () => {
const { findByText } = renderWithRecoil(
<ProjectTree
dialogId="ToDoBot"
dialogs={dialogs as any}
selected=""
onDeleteDialog={() => {}}
onDeleteTrigger={() => {}}
onSelect={() => {}}
/>
<ProjectTree onDeleteDialog={() => {}} onDeleteTrigger={() => {}} onSelect={() => {}} />,
initRecoilState
);

await findByText('ToDoBot');
await findByText('EchoBot-1');
});

it('should handle project tree item click', async () => {
const mockFileSelect = jest.fn(() => null);
const component = renderWithRecoil(
<ProjectTree onDeleteDialog={() => {}} onDeleteTrigger={() => {}} onSelect={mockFileSelect} />,
initRecoilState
);

const node = await component.findByTestId('EchoBot-1_Greeting');
fireEvent.click(node);
expect(mockFileSelect).toHaveBeenCalledTimes(1);
});

it('fires the onSelectAll event', async () => {
const mockOnSelected = jest.fn();
const { findByText } = renderWithRecoil(
<ProjectTree
dialogId="ToDoBot"
dialogs={dialogs as any}
selected=""
onDeleteDialog={() => {}}
onDeleteTrigger={() => {}}
onSelect={mockFileSelect}
/>
onSelect={() => {}}
onSelectAllLink={mockOnSelected}
/>,
initRecoilState
);

const node = await findByText('addtodo');
const node = await findByText('All');
fireEvent.click(node);
expect(mockFileSelect).toHaveBeenCalledTimes(1);
expect(mockOnSelected).toHaveBeenCalledTimes(1);
});
});
93 changes: 93 additions & 0 deletions Composer/packages/client/__tests__/mocks/sampleDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

// This is a copy of the JSON that defines the EchoBot sample plus some
// additional dialogs and triggers, including a trigger with a syntax
// error for use with testing error messages.

export const SAMPLE_DIALOG = {
isRoot: true,
displayName: 'EchoBot-1',
id: 'echobot-1',
content: {
$kind: 'Microsoft.AdaptiveDialog',
$designer: { id: '433224', description: '', name: 'EchoBot-1' },
autoEndDialog: true,
defaultResultProperty: 'dialog.result',
triggers: [
{
$kind: 'Microsoft.OnUnknownIntent',
$designer: { id: '821845' },
actions: [
{ $kind: 'Microsoft.SendActivity', $designer: { id: '003038' }, activity: '${SendActivity_003038()}' },
],
},
{
$kind: 'Microsoft.OnConversationUpdateActivity',
$designer: { id: '376720' },
actions: [
{
$kind: 'Microsoft.Foreach',
$designer: { id: '518944', name: 'Loop: for each item' },
itemsProperty: 'turn.Activity.membersAdded',
actions: [
{
$kind: 'Microsoft.IfCondition',
$designer: { id: '641773', name: 'Branch: if/else' },
condition: 'string(dialog.foreach.value.id) != string(turn.Activity.Recipient.id)',
actions: [
{
$kind: 'Microsoft.SendActivity',
$designer: { id: '859266', name: 'Send a response' },
activity: '${SendActivity_Welcome()}',
},
],
},
],
},
],
},
{ $kind: 'Microsoft.OnError', $designer: { id: 'XVSGCI' } },
{
$kind: 'Microsoft.OnIntent',
$designer: { id: 'QIgTMy', name: 'more errors' },
intent: 'test',
actions: [{ $kind: 'Microsoft.SetProperty', $designer: { id: 'VyWC7G' }, value: '=[' }],
},
],
generator: 'echobot-1.lg',
$schema:
'https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema',
id: 'EchoBot-1',
recognizer: 'echobot-1.lu.qna',
},
diagnostics: [
{
message:
"must be an expression: syntax error at line 1:1 mismatched input '<EOF>' expecting {STRING_INTERPOLATION_START, '+', '-', '!', '(', '[', ']', '{', NUMBER, IDENTIFIER, STRING}",
source: 'echobot-1',
severity: 0,
path: 'echobot-1.triggers[3].actions[0]#Microsoft.SetProperty#value',
},
],
referredDialogs: [],
lgTemplates: [
{ name: 'SendActivity_003038', path: 'echobot-1.triggers[0].actions[0]' },
{ name: 'SendActivity_Welcome', path: 'echobot-1.triggers[1].actions[0].actions[0].actions[0]' },
],
referredLuIntents: [{ name: 'test', path: 'echobot-1.triggers[3]#Microsoft.OnIntent' }],
luFile: 'echobot-1',
qnaFile: 'echobot-1',
lgFile: 'echobot-1',
triggers: [
{ id: 'triggers[0]', displayName: '', type: 'Microsoft.OnUnknownIntent', isIntent: false },
{ id: 'triggers[1]', displayName: '', type: 'Microsoft.OnConversationUpdateActivity', isIntent: false },
{ id: 'triggers[2]', displayName: '', type: 'Microsoft.OnError', isIntent: false },
{ id: 'triggers[3]', displayName: 'more errors', type: 'Microsoft.OnIntent', isIntent: true },
],
intentTriggers: [
{ intent: 'test', dialogs: [] },
{ intent: 'test', dialogs: [] },
],
skills: [],
};
22 changes: 16 additions & 6 deletions Composer/packages/client/__tests__/utils/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './../../src/utils/navigation';

const projectId = '123a-sdf123';
const skillId = '98765.4321';

describe('getFocusPath', () => {
it('return focus path', () => {
Expand Down Expand Up @@ -94,13 +95,22 @@ describe('composer url util', () => {
});

it('convert path to url', () => {
const result1 = convertPathToUrl(projectId, 'main');
expect(result1).toEqual(`/bot/${projectId}/dialogs/main`);
const result2 = convertPathToUrl(projectId, 'main', 'main.triggers[0].actions[0]');
expect(result2).toEqual(`/bot/${projectId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]`);
const result3 = convertPathToUrl(projectId, 'main', 'main.triggers[0].actions[0]#Microsoft.TextInput#prompt');
const result1 = convertPathToUrl(projectId, skillId, 'main');
expect(result1).toEqual(`/bot/${projectId}/skill/${skillId}/dialogs/main`);
const result2 = convertPathToUrl(projectId, skillId, 'main', 'main.triggers[0].actions[0]');
expect(result2).toEqual(
`/bot/${projectId}/skill/${skillId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]`
);
const result3 = convertPathToUrl(
projectId,
skillId,
'main',
'main.triggers[0].actions[0]#Microsoft.TextInput#prompt'
);
expect(result3).toEqual(
`/bot/${projectId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks`
`/bot/${projectId}/skill/${skillId}/dialogs/main?selected=triggers[0]&focused=triggers[0].actions[0]#botAsks`
);
const result4 = convertPathToUrl(projectId, null, 'main');
expect(result4).toEqual(`/bot/${projectId}/dialogs/main`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,21 @@
import { jsx, css } from '@emotion/core';
import { useRecoilValue } from 'recoil';
import { forwardRef } from 'react';
// import formatMessage from 'format-message';

import { RequireAuth } from '../RequireAuth';
import { ErrorBoundary } from '../ErrorBoundary';
import { Conversation } from '../Conversation';
//import { ProjectTree } from '../ProjectTree/ProjectTree';
//import { LeftRightSplit } from '../Split/LeftRightSplit';

import Routes from './../../router';
import { applicationErrorState, dispatcherState, currentProjectIdState } from './../../recoilModel';
import {
applicationErrorState,
dispatcherState,
currentProjectIdState,
// currentModeState,
} from './../../recoilModel';

// -------------------- Styles -------------------- //

Expand All @@ -33,10 +42,20 @@ const content = css`

const Content = forwardRef<HTMLDivElement>((props, ref) => <div css={content} {...props} ref={ref} />);

// const SHOW_TREE = ['design'];

export const RightPanel = () => {
const applicationError = useRecoilValue(applicationErrorState);
const { setApplicationLevelError, fetchProjectById } = useRecoilValue(dispatcherState);
const projectId = useRecoilValue(currentProjectIdState);
//const currentMode = useRecoilValue(currentModeState);

const conversation = (
<Conversation>
<Routes component={Content} />
</Conversation>
);

return (
<div css={rightPanel}>
<ErrorBoundary
Expand All @@ -45,7 +64,17 @@ export const RightPanel = () => {
setApplicationLevelError={setApplicationLevelError}
>
<RequireAuth>
<Routes component={Content} />
<div css={{ display: 'flex', flexDirection: 'row', label: 'MainPage' }}>
{/*
{SHOW_TREE.includes(currentMode) ? (
<LeftRightSplit initialLeftGridWidth="200px" minLeftPixels={200} minRightPixels={800}>
<ProjectTree regionName={formatMessage('Project tree')} showTriggers={currentMode === 'design'} />
{conversation}
</LeftRightSplit>
) : ( */}
{conversation}
{/* })} */}
</div>
</RequireAuth>
</ErrorBoundary>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { useState, MouseEvent, KeyboardEvent } from 'react';

type Props = {
children: React.ReactNode;
summary: React.ReactNode;
depth?: number;
detailsRef?: (el: HTMLElement | null) => void;
};

const summaryStyle = css`
label: summary;
display: flex;
padding-left: 12px;
padding-top: 6px;
`;

const nodeStyle = (depth: number) => css`
margin-left: ${depth * 16}px;
`;

export const ExpandableNode = ({ children, summary, detailsRef, depth = 0 }: Props) => {
const [isExpanded, setExpanded] = useState(true);

function handleClick(ev: MouseEvent) {
if ((ev.target as Element)?.tagName.toLowerCase() === 'summary') {
setExpanded(!isExpanded);
}
ev.preventDefault();
}

function handleKey(ev: KeyboardEvent) {
if (ev.key === 'Enter' || ev.key === 'Space') setExpanded(!isExpanded);
}

return (
<div css={nodeStyle(depth)} data-testid="dialog">
<details ref={detailsRef} open={isExpanded}>
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/no-noninteractive-tabindex */}
<summary css={summaryStyle} role="button" tabIndex={0} onClick={handleClick} onKeyUp={handleKey}>
{summary}
</summary>
{children}
</details>
</div>
);
};
Loading

0 comments on commit 408060d

Please sign in to comment.