Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow the Emulator user to specify a User ID #1456

Merged
merged 12 commits into from Apr 26, 2019
Expand Up @@ -94,6 +94,9 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A

public render(): JSX.Element {
const { state } = this;
const inputProps = {
disabled: !state.useCustomId,
};

return (
<GenericDocument className={styles.appSettingsEditor}>
Expand Down Expand Up @@ -167,7 +170,7 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
</Row>
</Column>
<Column className={[styles.rightColumn, styles.spacing].join(' ')}>
<SmallHeader>Auth</SmallHeader>
<SmallHeader>User settings</SmallHeader>
<Checkbox
className={styles.checkboxOverrides}
checked={state.use10Tokens}
Expand All @@ -176,7 +179,6 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
label="Use version 1.0 authentication tokens"
name="use10Tokens"
/>
<SmallHeader>Sign-in</SmallHeader>
<Checkbox
className={styles.checkboxOverrides}
checked={state.useCodeValidation}
Expand All @@ -185,6 +187,31 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
label="Use a sign-in verification code for OAuthCards"
name="useCodeValidation"
/>
<Checkbox
className={styles.checkboxOverrides}
checked={state.useCustomId}
onChange={this.onChangeCheckBox}
id="use-custom-id"
label="Use your own user ID to communicate with the bot"
name="useCustomId"
/>

<Row align={RowAlignment.Top}>
<TextField
{...inputProps}
label="User ID"
placeholder={state.useCustomId ? '' : 'There is no ID configured'}
className={styles.appSettingsInput}
inputContainerClassName={styles.inputContainer}
readOnly={false}
value={state.userGUID}
name="userGUID"
onChange={this.onInputChange}
required={state.useCustomId}
errorMessage={state.userGUID ? '' : 'Enter a User ID'}
/>
</Row>

<SmallHeader>Application Updates</SmallHeader>
<Checkbox
className={styles.checkboxOverrides}
Expand Down Expand Up @@ -216,17 +243,29 @@ export class AppSettingsEditor extends React.Component<AppSettingsEditorProps, A
</Row>
<Row className={styles.buttonRow} justify={RowJustification.Right}>
<PrimaryButton text="Cancel" onClick={this.props.discardChanges} className={styles.cancelButton} />
<PrimaryButton text="Save" onClick={this.onSaveClick} className={styles.saveButton} disabled={!state.dirty} />
<PrimaryButton
text="Save"
onClick={this.onSaveClick}
className={styles.saveButton}
disabled={this.disableSaveButton()}
/>
</Row>
</GenericDocument>
);
}

private disableSaveButton(): boolean {
return this.state.useCustomId ? !this.state.userGUID : !this.state.dirty;
}

private onChangeCheckBox = (event: ChangeEvent<HTMLInputElement>) => {
const { name, checked } = event.target;
const change = { [name]: checked };
this.setState(change);
this.updateDirtyFlag(change);
if (name === 'useCustomId' && checked === false) {
this.setState({ userGUID: null });
}
};

private onClickBrowse = async (): Promise<void> => {
Expand Down
1 change: 1 addition & 0 deletions packages/app/client/src/ui/editor/emulator/emulator.scss
Expand Up @@ -82,6 +82,7 @@
}

.restart-icon {
margin-left: 20px;
&::before { -webkit-mask: url(../../media/ic_refresh.svg); }
}
.save-icon {
Expand Down
44 changes: 22 additions & 22 deletions packages/app/client/src/ui/editor/emulator/emulator.spec.tsx
Expand Up @@ -288,8 +288,8 @@ describe('<EmulatorContainer/>', () => {
it('should export a transcript', () => {
instance.onExportTranscriptClick();

expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade[2].commandName).toBe(SharedConstants.Commands.Emulator.SaveTranscriptToFile);
expect(mockRemoteCallsMade[0].args).toEqual([16, 'convo1']);
});

Expand All @@ -312,7 +312,7 @@ describe('<EmulatorContainer/>', () => {
};
await instance.startNewConversation(undefined, true, true);

expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade).toHaveLength(4);
expect(initConversationSpy).toHaveBeenCalledWith(instance.props, options);
});

Expand Down Expand Up @@ -349,9 +349,9 @@ describe('<EmulatorContainer/>', () => {
};
await instance.startNewConversation(undefined, false, true);

expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.SetCurrentUser);
expect(mockRemoteCallsMade[0].args).toEqual([options.userId]);
expect(mockRemoteCallsMade).toHaveLength(4);
expect(mockRemoteCallsMade[3].commandName).toBe(SharedConstants.Commands.Emulator.SetCurrentUser);
expect(mockRemoteCallsMade[3].args).toEqual([options.userId]);
expect(mockInitConversation).toHaveBeenCalledWith(instance.props, options);
});

Expand All @@ -362,9 +362,9 @@ describe('<EmulatorContainer/>', () => {

expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['conversation_restart', { userId: 'new' }]);
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade[2].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[2].args).toEqual(['conversation_restart', { userId: 'new' }]);
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, true);
});

Expand All @@ -375,9 +375,9 @@ describe('<EmulatorContainer/>', () => {

expect(mockDispatch).toHaveBeenCalledWith(clearLog('doc1'));
expect(mockDispatch).toHaveBeenCalledWith(setInspectorObjects('doc1', []));
expect(mockRemoteCallsMade).toHaveLength(1);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[0].args).toEqual(['conversation_restart', { userId: 'same' }]);
expect(mockRemoteCallsMade).toHaveLength(3);
expect(mockRemoteCallsMade[2].commandName).toBe(SharedConstants.Commands.Telemetry.TrackEvent);
expect(mockRemoteCallsMade[2].args).toEqual(['conversation_restart', { userId: 'same' }]);
expect(mockStartNewConversation).toHaveBeenCalledWith(undefined, true, false);
});

Expand Down Expand Up @@ -417,11 +417,11 @@ describe('<EmulatorContainer/>', () => {

await instance.startNewConversation(mockProps);

expect(mockRemoteCallsMade).toHaveLength(2);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
expect(mockRemoteCallsMade[0].args).toEqual(['someUniqueId|transcript']);
expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromMemory);
expect(mockRemoteCallsMade[1].args).toEqual(['someConvoId', 'someBotId', 'someUserId', []]);
expect(mockRemoteCallsMade).toHaveLength(6);
expect(mockRemoteCallsMade[4].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
expect(mockRemoteCallsMade[4].args).toEqual(['someUniqueId|transcript']);
expect(mockRemoteCallsMade[5].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromMemory);
expect(mockRemoteCallsMade[5].args).toEqual(['someConvoId', 'someBotId', 'someUserId', []]);
});

it('should start a new conversation from transcript on disk', async () => {
Expand All @@ -441,11 +441,11 @@ describe('<EmulatorContainer/>', () => {

await instance.startNewConversation(mockProps);

expect(mockRemoteCallsMade).toHaveLength(2);
expect(mockRemoteCallsMade[0].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
expect(mockRemoteCallsMade[0].args).toEqual(['someUniqueId|transcript']);
expect(mockRemoteCallsMade[1].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromDisk);
expect(mockRemoteCallsMade[1].args).toEqual(['someConvoId', 'someBotId', 'someUserId', 'someDocId']);
expect(mockRemoteCallsMade).toHaveLength(6);
expect(mockRemoteCallsMade[4].commandName).toBe(SharedConstants.Commands.Emulator.NewTranscript);
expect(mockRemoteCallsMade[4].args).toEqual(['someUniqueId|transcript']);
expect(mockRemoteCallsMade[5].commandName).toBe(SharedConstants.Commands.Emulator.FeedTranscriptFromDisk);
expect(mockRemoteCallsMade[5].args).toEqual(['someConvoId', 'someBotId', 'someUserId', 'someDocId']);
expect(mockDispatch).toHaveBeenCalledWith(updateDocument('someDocId', { meta: 'some file info' }));
});
});
33 changes: 21 additions & 12 deletions packages/app/client/src/ui/editor/emulator/emulator.tsx
Expand Up @@ -33,11 +33,18 @@

import { createDirectLine } from 'botframework-webchat';
import { uniqueId, uniqueIdv4 } from '@bfemulator/sdk-shared';
import { SplitButton, Splitter } from '@bfemulator/ui-react';
import { Splitter, SplitButton } from '@bfemulator/ui-react';
import base64Url from 'base64url';
import { IEndpointService } from 'botframework-config/lib/schema';
import * as React from 'react';
import { DebugMode, newNotification, Notification, SharedConstants, ValueTypesMask } from '@bfemulator/app-shared';
import {
DebugMode,
FrameworkSettings,
newNotification,
Notification,
SharedConstants,
ValueTypesMask,
} from '@bfemulator/app-shared';

import { Document } from '../../../data/reducer/editor';
import { CommandServiceImpl } from '../../../platform/commands/commandServiceImpl';
Expand Down Expand Up @@ -121,14 +128,11 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
const { document = {} } = props;
const { document: nextDocument = {} } = nextProps;

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

if (documentOrUserIdChanged) {
if (documentIdChanged) {
startNewConversation(nextProps).catch();
}

const switchedDocuments = props.activeDocumentId !== nextProps.activeDocumentId;
const switchedToThisDocument = nextProps.activeDocumentId === props.documentId;

Expand All @@ -152,10 +156,13 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
? `${uniqueId()}|${props.mode}`
: props.document.conversationId || `${uniqueId()}|${props.mode}`;

const userId = requireNewUserId ? uniqueIdv4() : props.document.userId;
if (requireNewUserId) {
await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);
}
const framework: FrameworkSettings = await CommandServiceImpl.remoteCall(
SharedConstants.Commands.Settings.LoadAppSettings
);
const stableId = framework.userGUID || props.document.userId;
const userId = requireNewUserId ? uniqueIdv4() : stableId;

await CommandServiceImpl.remoteCall(SharedConstants.Commands.Emulator.SetCurrentUser, userId);

const options = {
conversationId,
Expand Down Expand Up @@ -276,6 +283,7 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
onClick={this.onStartOverClick}
/>
)}

<button
className={`${styles.saveIcon} ${styles.toolbarIcon || ''}`}
onClick={this.onExportTranscriptClick}
Expand Down Expand Up @@ -368,13 +376,14 @@ export class Emulator extends React.Component<EmulatorProps, {}> {
break;
}

case SameUserId:
case SameUserId: {
this.props.trackEvent('conversation_restart', {
userId: 'same',
});
// start conversation with new convo id
await this.startNewConversation(undefined, true, false);
break;
}

default:
break;
Expand Down
Expand Up @@ -37,7 +37,6 @@
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);
Expand Down
6 changes: 6 additions & 0 deletions packages/app/shared/src/types/serverSettingsTypes.ts
Expand Up @@ -60,6 +60,10 @@ export interface FrameworkSettings {
collectUsageData?: boolean;
// Digest of k/v pairs for integrity
hash?: string;
// GUID set by the user
userGUID?: string;
// use custom user id
useCustomId?: boolean;
}

export interface WindowStateSettings {
Expand Down Expand Up @@ -138,6 +142,8 @@ export const frameworkDefault: FrameworkSettings = {
usePrereleases: false,
autoUpdate: true,
collectUsageData: false,
userGUID: '',
useCustomId: false,
};

export const windowStateDefault: WindowStateSettings = {
Expand Down
4 changes: 2 additions & 2 deletions packages/sdk/ui-react/src/widget/textField/textField.scss
Expand Up @@ -18,7 +18,7 @@
border: var(--input-border-error);
}

&[disabled], &[aria-disabled] {
&[disabled], &[aria-disabled="true"] {
border: var(--input-border-disabled);
}
}
Expand All @@ -29,7 +29,7 @@
color: var(--input-label-color);
padding: 5px 0;

&[disabled], &[aria-disabled] {
&[disabled], &[aria-disabled="true"] {
color: var(--input-label-color-disabled);
}
}
Expand Down