Skip to content

Commit

Permalink
feat: Breaking change upgrade UX from 1.4 to 2.0 (#7388)
Browse files Browse the repository at this point in the history
* Added mechanism to show custom UX on breaking updates

* Minor updates / l10n

* Fixed some minor bugs and added l10n

* Cleaned up types

* Fixed "Not now" behavior

* Reenabled nightly feed to composer

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

* Revert "Reenabled nightly feed to composer"

This reverts commit f26216fb76065b760fbfecffb0e1f0f8036d3f78.

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

* Updated changelog and set correct feed URL

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

* revert updates

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

Revert import order

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

Removed logs

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

* Handle unit test upgrades

Signed-off-by: Srinaath Ravichandran <srravich@microsoft.com>

Co-authored-by: Tony <tonyanziano5@gmail.com>
Co-authored-by: Srinaath Ravichandran <srravich@microsoft.com>
  • Loading branch information
3 people committed May 4, 2021
1 parent 113e420 commit e2e35e6
Show file tree
Hide file tree
Showing 18 changed files with 367 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import { useRecoilValue } from 'recoil';
import { SharedColors, NeutralColors } from '@uifabric/fluent-theme';
import { IpcRendererEvent } from 'electron';

import { AppUpdaterStatus } from '../constants';
import { appUpdateState, dispatcherState } from '../recoilModel';
import { AppUpdaterStatus } from '../../constants';
import { appUpdateState, dispatcherState } from '../../recoilModel';

import { breakingUpdatesMap } from './breakingUpdates/breakingUpdatesMap';

const updateAvailableDismissBtn: Partial<IButtonStyles> = {
root: {
Expand Down Expand Up @@ -85,6 +87,12 @@ const downloadOptions = {
installAndUpdate: 'installAndUpdate',
};

// TODO: factor this out into shared or types
type BreakingUpdateMetaData = {
explicitCheck: boolean;
uxId: string;
};

// -------------------- AppUpdater -------------------- //

export const AppUpdater: React.FC<{}> = () => {
Expand All @@ -93,9 +101,11 @@ export const AppUpdater: React.FC<{}> = () => {
);
const { downloadSizeInBytes, error, progressPercent, showing, status, version } = useRecoilValue(appUpdateState);
const [downloadOption, setDownloadOption] = useState(downloadOptions.installAndUpdate);
const [breakingMetaData, setBreakingMetaData] = useState<BreakingUpdateMetaData | undefined>(undefined);

const handleDismiss = useCallback(() => {
setAppUpdateShowing(false);
setBreakingMetaData(undefined);
if (status === AppUpdaterStatus.UPDATE_UNAVAILABLE || status === AppUpdaterStatus.UPDATE_FAILED) {
setAppUpdateStatus(AppUpdaterStatus.IDLE, undefined);
}
Expand All @@ -118,53 +128,69 @@ export const AppUpdater: React.FC<{}> = () => {
setDownloadOption(option);
}, []);

// necessary?
const handleContinueFromBreakingUx = useCallback(() => {
handlePreDownloadOkay();
}, [handlePreDownloadOkay]);

// listen for app updater events from main process
useEffect(() => {
ipcRenderer.on('app-update', (_event: IpcRendererEvent, name: string, payload) => {
switch (name) {
case 'update-available':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_AVAILABLE, payload.version);
setAppUpdateShowing(true);
break;

case 'progress': {
const progress = +(payload.percent as number).toFixed(2);
setAppUpdateProgress(progress, payload.total);
break;
}
ipcRenderer.on(
'app-update',
(_event: IpcRendererEvent, name: string, payload, breakingMetaData?: BreakingUpdateMetaData) => {
switch (name) {
case 'update-available':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_AVAILABLE, payload.version);
setAppUpdateShowing(true);
break;

case 'update-in-progress': {
setAppUpdateStatus(AppUpdaterStatus.UPDATE_AVAILABLE, payload.version);
setAppUpdateShowing(true);
break;
}
case 'progress': {
const progress = +(payload.percent as number).toFixed(2);
setAppUpdateProgress(progress, payload.total);
break;
}

case 'update-not-available': {
const explicit = payload;
if (explicit) {
// the user has explicitly checked for an update via the Help menu;
// we should display some UI feedback if there are no updates available
setAppUpdateStatus(AppUpdaterStatus.UPDATE_UNAVAILABLE, undefined);
case 'update-in-progress': {
setAppUpdateStatus(AppUpdaterStatus.UPDATE_AVAILABLE, payload.version);
setAppUpdateShowing(true);
break;
}
break;
}

case 'update-downloaded':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_SUCCEEDED, undefined);
setAppUpdateShowing(true);
break;
case 'update-not-available': {
const explicit = payload;
if (explicit) {
// the user has explicitly checked for an update via the Help menu;
// we should display some UI feedback if there are no updates available
setAppUpdateStatus(AppUpdaterStatus.UPDATE_UNAVAILABLE, undefined);
setAppUpdateShowing(true);
}
break;
}

case 'error':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_FAILED, undefined);
setAppUpdateError(payload);
setAppUpdateShowing(true);
break;
case 'update-downloaded':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_SUCCEEDED, undefined);
setAppUpdateShowing(true);
break;

default:
break;
case 'error':
setAppUpdateStatus(AppUpdaterStatus.UPDATE_FAILED, undefined);
setAppUpdateError(payload);
setAppUpdateShowing(true);
break;

case 'breaking-update-available':
if (breakingMetaData) {
setBreakingMetaData(breakingMetaData);
setAppUpdateStatus(AppUpdaterStatus.BREAKING_UPDATE_AVAILABLE, payload.version);
setAppUpdateShowing(true);
}
break;

default:
break;
}
}
});
);
}, []);

const title = useMemo(() => {
Expand Down Expand Up @@ -292,6 +318,19 @@ export const AppUpdater: React.FC<{}> = () => {
const subText =
status === AppUpdaterStatus.UPDATE_AVAILABLE ? `${formatMessage('Bot Framework Composer')} v${version}` : '';

if (status === AppUpdaterStatus.BREAKING_UPDATE_AVAILABLE && showing && breakingMetaData) {
const BreakingUpdateUx = breakingUpdatesMap[breakingMetaData.uxId];
// TODO: check if breaking update ux component is defined and handle undefined case
return (
<BreakingUpdateUx
explicitCheck={breakingMetaData.explicitCheck}
version={version}
onCancel={handleDismiss}
onContinue={handleContinueFromBreakingUx}
/>
);
}

return showing ? (
<Dialog
dialogContentProps={{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React from 'react';
import { BreakingUpdateId } from '@botframework-composer/types';

import { BreakingUpdateProps } from './types';
import { Version1To2Content } from './version1To2';

/**
* A map of breaking update identifiers to the React components responsible
* for showing each breaking update's special UX before proceeding to the
* standard update flow.
*
* Ex. 'Version2.5.xTo3.x.x': DialogWithDisclaimerAndDocsAboutNewChanges
*/

export const breakingUpdatesMap: Record<BreakingUpdateId, React.FC<BreakingUpdateProps>> = {
'Version1.x.xTo2.x.x': Version1To2Content,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export type BreakingUpdateProps = {
explicitCheck: boolean;
/** Called when the user dismisses the breaking changes UX; stops the update flow completely. */
onCancel: () => void;
/** Called when the breaking changes UX is ready to continue into the normal update flow. */
onContinue: () => void;
version?: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import React, { useCallback, useState } from 'react';
import { DefaultButton, PrimaryButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
import { Dialog, DialogType, IDialogContentStyles } from 'office-ui-fabric-react/lib/Dialog';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { NeutralColors } from '@uifabric/fluent-theme';
import formatMessage from 'format-message';
import { useRecoilValue } from 'recoil';

import { dispatcherState, userSettingsState } from '../../../recoilModel';

import { BreakingUpdateProps } from './types';

const dismissButton: Partial<IButtonStyles> = {
root: {
marginRight: '6px;',
marginLeft: 'auto',
},
};

const dialogContent: Partial<IDialogContentStyles> = {
content: { color: NeutralColors.black },
};

const dialogContentWithoutHeader: Partial<IDialogContentStyles> = {
...dialogContent,
header: {
display: 'none',
},
inner: {
padding: '36px 24px 24px 24px',
},
};

const buttonRow = css`
display: flex;
flex-flow: row nowrap;
justify-items: flex-end;
`;

const gotItButton = css`
margin-left: auto;
`;

const updateCancelledCopy = css`
margin-top: 0;
margin-bottom: 27px;
`;

type ModalState = 'Default' | 'PressedNotNow';

export const Version1To2Content: React.FC<BreakingUpdateProps> = (props) => {
const { explicitCheck, onCancel, onContinue } = props;
const [currentState, setCurrentState] = useState<ModalState>('Default');
const userSettings = useRecoilValue(userSettingsState);
const { updateUserSettings } = useRecoilValue(dispatcherState);
const onNotNow = useCallback(() => {
if (userSettings.appUpdater.autoDownload) {
// disable auto update and notify the user
updateUserSettings({
appUpdater: {
autoDownload: false,
},
});
setCurrentState('PressedNotNow');
} else {
onCancel();
}
}, []);

return currentState === 'Default' ? (
<Dialog
dialogContentProps={{
styles: dialogContent,
title: formatMessage('Composer 2.0 is now available!'),
type: DialogType.largeHeader,
}}
hidden={false}
maxWidth={427}
minWidth={427}
modalProps={{
isBlocking: false,
}}
>
<p>
{formatMessage(
'Bot Framework Composer 2.0 provides more built-in capabilities so you can build complex bots quickly. Update to Composer 2.0 for advanced bot templates, prebuilt components, and a runtime that is fully extensible through packages.'
)}
</p>

<p>
{formatMessage.rich(
'Note: If your bot is using custom actions, they will not be supported in Composer 2.0. <a>Learn more about updating to Composer 2.0.</a>',
{
// TODO: needs real link
a: ({ children }) => (
<Link key="v2-breaking-changes-docs" href="https://aka.ms/bot-framework-composer-2.0">
{children}
</Link>
),
}
)}
</p>
<div css={buttonRow}>
{explicitCheck ? (
<DefaultButton styles={dismissButton} text={formatMessage('Cancel')} onClick={onCancel} />
) : (
<DefaultButton styles={dismissButton} text={formatMessage('Not now')} onClick={onNotNow} />
)}
<PrimaryButton text={formatMessage('Update and restart')} onClick={onContinue} />
</div>
</Dialog>
) : (
<Dialog
dialogContentProps={{
styles: dialogContentWithoutHeader,
title: undefined,
type: DialogType.normal,
}}
hidden={false}
maxWidth={427}
minWidth={427}
modalProps={{
isBlocking: false,
}}
>
<p css={updateCancelledCopy}>
{formatMessage.rich(
'Update cancelled. Auto-update has been turned off for this release. You can update at any time by selecting <b>Help > Check for updates.</b>',
{ b: ({ children }) => <b key="v2-breaking-changes-re-enable-auto-updates">{children}</b> }
)}
</p>
<div css={buttonRow}>
<PrimaryButton css={gotItButton} text={formatMessage('Got it!')} onClick={onCancel} />
</div>
</Dialog>
);
};
4 changes: 4 additions & 0 deletions Composer/packages/client/src/components/AppUpdater/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export { AppUpdater } from './AppUpdater';
1 change: 1 addition & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export const SupportedFileTypes = [
export const USER_TOKEN_STORAGE_KEY = 'composer.userToken';

export enum AppUpdaterStatus {
BREAKING_UPDATE_AVAILABLE,
IDLE,
UPDATE_AVAILABLE,
UPDATE_UNAVAILABLE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const applicationDispatcher = () => {
const newAppUpdateState = {
...currentAppUpdate,
};
if (status === AppUpdaterStatus.UPDATE_AVAILABLE) {
if (status === AppUpdaterStatus.UPDATE_AVAILABLE || status === AppUpdaterStatus.BREAKING_UPDATE_AVAILABLE) {
newAppUpdateState.version = version;
}
if (status === AppUpdaterStatus.IDLE) {
Expand Down
Loading

0 comments on commit e2e35e6

Please sign in to comment.