Skip to content

Commit

Permalink
Merge branch 'master' into jwilaby/#1140-postback
Browse files Browse the repository at this point in the history
  • Loading branch information
Justin Wilaby committed Dec 6, 2018
2 parents 22199be + 93bbe7d commit 6d0e910
Show file tree
Hide file tree
Showing 32 changed files with 1,197 additions and 126 deletions.
18 changes: 17 additions & 1 deletion packages/app/client/src/data/action/chatActions.ts
Expand Up @@ -45,7 +45,8 @@ export enum ChatActions {
setInspectorObjects = 'CHAT/INSPECTOR/OBJECTS/SET',
addTranscript = 'CHAT/TRANSCRIPT/ADD',
clearTranscripts = 'CHAT/TRANSCRIPT/CLEAR',
removeTranscript = 'CHAT/TRANSCRIPT/REMOVE'
removeTranscript = 'CHAT/TRANSCRIPT/REMOVE',
updateChat = 'CHAT/DOCUMENT/UPDATE'
}

export interface ActiveInspectorChangedPayload {
Expand Down Expand Up @@ -91,6 +92,11 @@ export interface RemoveTranscriptPayload {
filename: string;
}

export interface UpdateChatPayload {
documentId: string;
updatedValues: any;
}

export interface ChatAction<T = any> extends Action {
payload: T;
}
Expand Down Expand Up @@ -216,3 +222,13 @@ export function setInspectorObjects(documentId: string, objs: any): ChatAction<S
}
};
}

export function updateChat(documentId: string, updatedValues: any): ChatAction<UpdateChatPayload> {
return {
type: ChatActions.updateChat,
payload: {
documentId,
updatedValues
}
};
}
20 changes: 19 additions & 1 deletion packages/app/client/src/data/reducer/chat.spec.ts
Expand Up @@ -42,7 +42,8 @@ import {
newConversation,
newDocument,
removeTranscript,
setInspectorObjects
setInspectorObjects,
updateChat
} from '../action/chatActions';
import { closeNonGlobalTabs } from '../action/editorActions';
import { chat, ChatState } from './chat';
Expand Down Expand Up @@ -219,4 +220,21 @@ describe('Chat reducer tests', () => {
expect(state.transcripts.length).toBe(0);
expect(state.chats[tempChat]).toBeFalsy();
});

it('should update a chat', () => {
const startingState = {
...DEFAULT_STATE,
chats: {
...DEFAULT_STATE.chats,
chat1: {
id: 'chat',
userId: 'userId'
}
}
};
const action = updateChat('chat1', { id: 'updatedChatId', userId: 'updatedUserId' });
const state = chat(startingState, action);
expect(state.chats.chat1.id).toBe('updatedChatId');
expect(state.chats.chat1.userId).toBe('updatedUserId');
});
});
22 changes: 22 additions & 0 deletions packages/app/client/src/data/reducer/chat.ts
Expand Up @@ -189,6 +189,28 @@ export function chat(state: ChatState = DEFAULT_STATE, action: ChatAction | Edit
break;
}

case ChatActions.updateChat: {
const { payload } = action;
const { documentId = '', updatedValues = {} } = payload;
let document = state.chats[documentId];
if (document) {
document = {
...document,
...updatedValues
};
state = {
...state,
chats: {
...state.chats,
[payload.documentId]: {
...document
}
}
};
}
break;
}

case EditorActions.closeAll: {
// HACK. Need a better system.
return DEFAULT_STATE;
Expand Down
17 changes: 17 additions & 0 deletions packages/app/client/src/ui/editor/emulator/emulator.scss
Expand Up @@ -16,6 +16,23 @@
flex-shrink: 1;

.toolbar-icon {
cursor: pointer;
margin: 0;
border: 1px solid transparent;
height: 30px;
background-color: transparent;
white-space: nowrap;
color: var(--toolbar-text-color);

&:hover {
background-color: transparent;
border: var(--toolbar-button-hover-border);
}

&:active {
color: var(--toolbar-text-active);
}

&::before {
background-color: var(--toolbar-icon-color);
content: '';
Expand Down
96 changes: 60 additions & 36 deletions packages/app/client/src/ui/editor/emulator/emulator.tsx
Expand Up @@ -32,8 +32,8 @@
//

import * as BotChat from 'botframework-webchat';
import { uniqueId } from '@bfemulator/sdk-shared';
import { Splitter } from '@bfemulator/ui-react';
import { uniqueId, uniqueIdv4 } from '@bfemulator/sdk-shared';
import { Splitter, SplitButton } from '@bfemulator/ui-react';
import base64Url from 'base64url';
import { IEndpointService } from 'botframework-config/lib/schema';
import * as React from 'react';
Expand All @@ -45,7 +45,6 @@ import * as PresentationActions from '../../../data/action/presentationActions';
import { Document } from '../../../data/reducer/editor';
import { RootState } from '../../../data/store';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
import ToolBar, { Button as ToolBarButton } from '../toolbar/toolbar';
import ChatPanel from './chatPanel/chatPanel';
import LogPanel from './logPanel/logPanel';
import PlaybackBar from './playbackBar/playbackBar';
Expand All @@ -54,9 +53,15 @@ import { newNotification, Notification, SharedConstants } from '@bfemulator/app-
import * as styles from './emulator.scss';
import { beginAdd } from '../../../data/action/notificationActions';
import { InspectorContainer } from './parts';
import { ToolBar } from './toolbar/toolbar';

const { encode } = base64Url;

const RestartConversationOptions = {
NewUserId: 'Restart with new user ID',
SameUserId: 'Restart with same user ID'
};

export type EmulatorMode = 'transcript' | 'livechat';

interface EmulatorProps {
Expand All @@ -74,6 +79,7 @@ interface EmulatorProps {
newConversation?: (documentId: string, options: any) => void;
presentationModeEnabled?: boolean;
setInspectorObjects?: (documentId: string, objects: any) => void;
updateChat?: (documentId: string, updatedValues: any) => void;
updateDocument?: (documentId: string, updatedValues: Partial<Document>) => void;
url?: string;
}
Expand All @@ -97,8 +103,7 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
super(props);
}

shouldStartNewConversation(props?: any) {
props = props || this.props;
shouldStartNewConversation(props: EmulatorProps = this.props): boolean {
return !props.document.directLine ||
(props.document.conversationId !== props.document.directLine.conversationId);
}
Expand All @@ -114,10 +119,15 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
window.removeEventListener('keydown', this.keyboardEventListener);
}

componentWillReceiveProps(nextProps: any) {
componentWillReceiveProps(nextProps: EmulatorProps) {
const { props, keyboardEventListener, startNewConversation } = this;
const { document = {} } = props;
const { document: nextDocument = {} } = nextProps;

const documentOrUserIdChanged = (!nextDocument.directLine && document.documentId !== nextDocument.documentId)
|| (document.userId !== nextDocument.userId);

if (!nextProps.document.directLine && props.document.documentId !== nextProps.document.documentId) {
if (documentOrUserIdChanged) {
startNewConversation(nextProps).catch();
}

Expand All @@ -134,9 +144,7 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
}
}

async startNewConversation(props?: any) {
props = props || this.props;

startNewConversation = async (props: EmulatorProps = this.props): Promise<any> => {
if (props.document.subscription) {
props.document.subscription.unsubscribe();
}
Expand Down Expand Up @@ -206,7 +214,7 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
}
}

initConversation(props: any, options: any, selectedActivity$: any, subscription: any) {
initConversation(props: EmulatorProps, options: any, selectedActivity$: any, subscription: any): void {
const encodedOptions = encode(JSON.stringify(options));

// TODO: We need to use encoded token because we need to pass both endpoint ID and conversation ID
Expand Down Expand Up @@ -240,38 +248,37 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
<div className={ styles.presentation }>
<div className={ styles.presentationContent }>
<ChatPanel mode={ this.props.mode } document={ this.props.document }
onStartConversation={ this.handleStartOverClick }/>
onStartConversation={ this.onStartOverClick }/>
{ chatPanelChild }
</div>
<span
className={ styles.closePresentationIcon }
onClick={ () => this.handlePresentationClick(false) }
onClick={ () => this.onPresentationClick(false) }
/>
</div>
);
}

renderDefaultView(): JSX.Element {
const { NewUserId, SameUserId } = RestartConversationOptions;

return (
<div className={ styles.emulator }>
{
{
this.props.mode === 'livechat' &&
<div className={ styles.header }>
<ToolBar>
<ToolBarButton
iconClassName={ styles.toolbarIcon }
icon={ styles.restartIcon }
visible={ true }
title="Restart conversation"
onClick={ this.handleStartOverClick }
/>
<ToolBarButton
iconClassName={ styles.toolbarIcon }
icon={ styles.saveTranscriptIcon }
visible={ true }
title="Save transcript"
onClick={ this.handleExportClick }
/>
<SplitButton
defaultLabel="Restart conversation"
buttonClass={ styles.restartIcon }
options={ [NewUserId, SameUserId] }
onClick={ this.onStartOverClick }/>
<button
className={ `${ styles.saveTranscriptIcon } ${ styles.toolbarIcon || '' }` }
onClick={ this.onExportClick }
>
Save transcript
</button>
</ToolBar>
</div>
}
Expand All @@ -284,7 +291,7 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
<ChatPanel mode={ this.props.mode }
className={ styles.chatPanel }
document={ this.props.document }
onStartConversation={ this.handleStartOverClick }/>
onStartConversation={ this.onStartOverClick }/>
</div>
<div className={ styles.content }>
<Splitter orientation="horizontal" primaryPaneIndex={ 0 }
Expand All @@ -301,29 +308,45 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
);
}

private getVerticalSplitterSizes = () => {
private getVerticalSplitterSizes = (): { [0]: string } => {
return {
0: `${this.props.document.ui.verticalSplitter[0].percentage}`
};
}

private getHorizontalSplitterSizes = () => {
private getHorizontalSplitterSizes = (): { [0]: string } => {
return {
0: `${this.props.document.ui.horizontalSplitter[0].percentage}`
};
}

private handlePresentationClick = (enabled: boolean) => {
private onPresentationClick = (enabled: boolean): void => {
this.props.enablePresentationMode(enabled);
}

private handleStartOverClick = () => {
private onStartOverClick = async (option: string = RestartConversationOptions.NewUserId): Promise<void> => {
const { NewUserId, SameUserId } = RestartConversationOptions;
this.props.clearLog(this.props.document.documentId);
this.props.setInspectorObjects(this.props.document.documentId, []);
this.startNewConversation();

switch (option) {
case NewUserId:
const newUserId = uniqueIdv4();
// set new user as current on emulator facilities side
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, newUserId);
this.props.updateChat(this.props.documentId, { userId: newUserId });
break;

case SameUserId:
this.startNewConversation();
break;

default:
break;
}
}

private handleExportClick = () => {
private onExportClick = (): void => {
if (this.props.document.directLine) {
CommandServiceImpl.remoteCall(
SharedConstants.Commands.Emulator.SaveTranscriptToFile,
Expand All @@ -338,7 +361,7 @@ class EmulatorComponent extends React.Component<EmulatorProps, {}> {
const shiftPressed = ctrlOrCmdPressed && event.getModifierState('Shift');
const key = event.key.toLowerCase();
if (ctrlOrCmdPressed && shiftPressed && key === 'r') {
this.handleStartOverClick();
this.onStartOverClick();
}
}
}
Expand All @@ -357,6 +380,7 @@ const mapDispatchToProps = (dispatch): EmulatorProps => ({
setInspectorObjects: (documentId, objects) => dispatch(ChatActions.setInspectorObjects(documentId, objects)),
clearLog: documentId => dispatch(ChatActions.clearLog(documentId)),
newConversation: (documentId, options) => dispatch(ChatActions.newConversation(documentId, options)),
updateChat: (documentId: string, updatedValues: any) => dispatch(ChatActions.updateChat(documentId, updatedValues)),
updateDocument: (documentId, updatedValues: Partial<Document>) => dispatch(updateDocument(documentId, updatedValues)),
createErrorNotification: (notification: Notification) => dispatch(beginAdd(notification))
});
Expand Down
44 changes: 44 additions & 0 deletions packages/app/client/src/ui/editor/emulator/toolbar/toolbar.scss
@@ -0,0 +1,44 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license.
//
// Microsoft Bot Framework: http://botframework.com
//
// Bot Framework Emulator Github:
// https://github.com/Microsoft/BotFramwork-Emulator
//
// Copyright (c) Microsoft Corporation
// All rights reserved.
//
// MIT License:
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

.toolbar {
box-sizing: border-box;
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 29px;
padding-left: 12px;
background-color: var(--toolbar-bg);
border-top: var(--toolbar-border);
border-bottom: var(--toolbar-border-bottom);
}
@@ -1,4 +1,2 @@
// This is a generated file. Changes are likely to result in being overwritten
export const toolbar: string;
export const button: string;
export const separator: string;

0 comments on commit 6d0e910

Please sign in to comment.