diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 8d1da6907f..69b0c159be 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -13,7 +13,7 @@ jobs:
ci:
name: Unit Tests
runs-on: ubuntu-latest
- timeout-minutes: 20
+ timeout-minutes: 30
defaults:
run:
working-directory: Composer
diff --git a/.vscode/launch.json b/.vscode/launch.json
index e1e676abe3..9f416d1b4d 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -88,7 +88,12 @@
"DEBUG": "composer*",
"COMPOSER_ENABLE_ONEAUTH": "false"
},
- "outputCapture": "std"
+ "outputCapture": "std",
+ "outFiles": [
+ "${workspaceRoot}/Composer/packages/electron-server/build/**/*.js",
+ "${workspaceRoot}/Composer/packages/server/build/**/*.js",
+ "${workspaceRoot}/extensions/**/*.js"
+ ]
},
{
"name": "Debug current jest test",
diff --git a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
index e42d04f5b8..38a25ad4e2 100644
--- a/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
+++ b/Composer/packages/client/__tests__/pages/publish/Publish.test.tsx
@@ -46,6 +46,7 @@ const state = {
publish: true,
status: true,
rollback: true,
+ pull: true,
},
},
],
diff --git a/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx b/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx
index 608369b8f6..777c65735b 100644
--- a/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx
+++ b/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx
@@ -3,7 +3,7 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DefaultButton, IconButton } from 'office-ui-fabric-react/lib/Button';
import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu';
import { useRecoilValue } from 'recoil';
@@ -34,6 +34,13 @@ const iconSectionContainer = css`
}
`;
+const disabledStyle = css`
+ &:before {
+ opacity: 0.4;
+ }
+ pointer-events: none;
+`;
+
const startPanelsContainer = css`
display: flex;
flex-direction: 'row';
@@ -47,6 +54,7 @@ const BotController: React.FC = () => {
const [isControllerHidden, setControllerVisibility] = useState(true);
const { onboardingAddCoachMarkRef } = useRecoilValue(dispatcherState);
const onboardRef = useCallback((startBot) => onboardingAddCoachMarkRef({ startBot }), []);
+ const [disableStartBots, setDisableOnStartBotsWidget] = useState(false);
const target = useRef(null);
const botControllerMenuTarget = useRef(null);
@@ -55,6 +63,14 @@ const BotController: React.FC = () => {
setControllerVisibility(true);
});
+ useEffect(() => {
+ if (projectCollection.length === 0) {
+ setDisableOnStartBotsWidget(true);
+ return;
+ }
+ setDisableOnStartBotsWidget(false);
+ }, [projectCollection]);
+
const running = useMemo(() => !projectCollection.every(({ status }) => status === BotStatus.unConnected), [
projectCollection,
]);
@@ -93,11 +109,19 @@ const BotController: React.FC = () => {
{projectCollection.map(({ projectId }) => {
return ;
})}
-
+
null}
styles={{
root: {
@@ -115,9 +139,10 @@ const BotController: React.FC = () => {
>
{buttonText}
-
+
= () => {
schemaUrl: formData.schemaUrl,
appLocale,
qnaKbUrls,
+ templateDir: formData.templateDir,
+ eTag: formData.eTag,
+ urlSuffix: formData.urlSuffix,
+ alias: formData.alias,
+ preserveRoot: formData.preserveRoot,
};
createNewBot(newBotData);
};
@@ -199,6 +205,7 @@ const CreationFlow: React.FC = () => {
path="create/vaCore/*"
onDismiss={handleDismiss}
/>
+
diff --git a/Composer/packages/client/src/components/CreationFlow/DefineConversation.tsx b/Composer/packages/client/src/components/CreationFlow/DefineConversation.tsx
index 4197a28de0..a284a557bf 100644
--- a/Composer/packages/client/src/components/CreationFlow/DefineConversation.tsx
+++ b/Composer/packages/client/src/components/CreationFlow/DefineConversation.tsx
@@ -8,16 +8,20 @@ import { DialogFooter } from 'office-ui-fabric-react/lib/Dialog';
import formatMessage from 'format-message';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { Stack, StackItem } from 'office-ui-fabric-react/lib/Stack';
-import React, { Fragment, useEffect, useCallback, useMemo } from 'react';
+import React, { Fragment, useEffect, useCallback, useMemo, useState } from 'react';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { RouteComponentProps } from '@reach/router';
import querystring from 'query-string';
import { FontWeights } from '@uifabric/styling';
import { DialogWrapper, DialogTypes } from '@bfc/ui-shared';
+import { useRecoilValue } from 'recoil';
import { DialogCreationCopy, QnABotTemplateId, nameRegex } from '../../constants';
import { FieldConfig, useForm } from '../../hooks/useForm';
import { StorageFolder } from '../../recoilModel/types';
+import { createNotification } from '../../recoilModel/dispatchers/notification';
+import { ImportSuccessNotificationWrapper } from '../ImportModal/ImportSuccessNotification';
+import { dispatcherState } from '../../recoilModel';
import { LocationSelectContent } from './LocationSelectContent';
@@ -66,6 +70,12 @@ interface DefineConversationFormData {
description: string;
schemaUrl: string;
location?: string;
+
+ templateDir?: string; // location of the imported template
+ eTag?: string; // e tag used for content sync between composer and imported bot content
+ urlSuffix?: string; // url to deep link to after creation
+ alias?: string; // identifier that is used to track bots between imports
+ preserveRoot?: boolean; // identifier that is used to determine ay project file renames upon creation
}
interface DefineConversationProps
@@ -109,18 +119,19 @@ const DefineConversation: React.FC = (props) => {
);
return defaultName;
};
+ const { addNotification } = useRecoilValue(dispatcherState);
const formConfig: FieldConfig = {
name: {
required: true,
validate: (value) => {
- if (!value || !nameRegex.test(value)) {
+ if (!value || !nameRegex.test(`${value}`)) {
return formatMessage('Spaces and special characters are not allowed. Use letters, numbers, -, or _.');
}
const newBotPath =
focusedStorageFolder !== null && Object.keys(focusedStorageFolder as Record).length
- ? Path.join(focusedStorageFolder.parent, focusedStorageFolder.name, value)
+ ? Path.join(focusedStorageFolder.parent, focusedStorageFolder.name, `${value}`)
: '';
if (
files.some((bot) => {
@@ -146,6 +157,14 @@ const DefineConversation: React.FC = (props) => {
},
};
const { formData, formErrors, hasErrors, updateField, updateForm } = useForm(formConfig);
+ const [isImported, setIsImported] = useState(false);
+
+ useEffect(() => {
+ if (props.location?.state) {
+ const { imported } = props.location.state;
+ setIsImported(imported);
+ }
+ }, [props.location?.state]);
useEffect(() => {
const formData: DefineConversationFormData = {
@@ -189,9 +208,35 @@ const DefineConversation: React.FC = (props) => {
return;
}
+ // handle extra properties in the case of an imported bot project
+ const dataToSubmit = {
+ ...formData,
+ };
+ if (props.location?.state) {
+ const { alias, eTag, imported, templateDir, urlSuffix } = props.location.state;
+
+ if (imported) {
+ dataToSubmit.templateDir = templateDir;
+ dataToSubmit.eTag = eTag;
+ dataToSubmit.urlSuffix = urlSuffix;
+ dataToSubmit.alias = alias;
+ dataToSubmit.preserveRoot = true;
+
+ // create a notification to indicate import success
+ const notification = createNotification({
+ type: 'success',
+ title: '',
+ onRenderCardContent: ImportSuccessNotificationWrapper({
+ importedToExisting: false,
+ }),
+ });
+ addNotification(notification);
+ }
+ }
+
onSubmit(
{
- ...formData,
+ ...dataToSubmit,
},
templateId || ''
);
@@ -223,15 +268,11 @@ const DefineConversation: React.FC = (props) => {
/>
);
}, [focusedStorageFolder]);
+ const dialogCopy = isImported ? DialogCreationCopy.IMPORT_BOT_PROJECT : DialogCreationCopy.DEFINE_BOT_PROJECT;
return (
-
+