From ca78825323e477e46d99401d14a51bcc8300966a Mon Sep 17 00:00:00 2001 From: leilzh Date: Thu, 10 Sep 2020 19:32:56 +0800 Subject: [PATCH 01/11] feat: add notification center for composer(qna import) --- .../AppComponents/MainContainer.tsx | 3 + .../src/components/NotificationCard.tsx | 249 ++++++++++++++++++ .../src/components/NotificationContainer.tsx | 34 +++ .../src/components/NotificationIcon.tsx | 69 +++++ .../__tests__/NotificationCard.test.tsx | 57 ++++ .../client/src/pages/design/DesignPage.tsx | 3 +- .../src/recoilModel/DispatcherWrapper.tsx | 2 +- .../client/src/recoilModel/atoms/appState.ts | 6 + .../src/recoilModel/dispatchers/index.ts | 2 + .../recoilModel/dispatchers/notification.ts | 43 +++ .../client/src/recoilModel/dispatchers/qna.ts | 23 +- .../packages/client/src/recoilModel/types.ts | 7 + .../client/src/utils/notifications.ts | 35 +++ 13 files changed, 522 insertions(+), 11 deletions(-) create mode 100644 Composer/packages/client/src/components/NotificationCard.tsx create mode 100644 Composer/packages/client/src/components/NotificationContainer.tsx create mode 100644 Composer/packages/client/src/components/NotificationIcon.tsx create mode 100644 Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx create mode 100644 Composer/packages/client/src/recoilModel/dispatchers/notification.ts create mode 100644 Composer/packages/client/src/utils/notifications.ts diff --git a/Composer/packages/client/src/components/AppComponents/MainContainer.tsx b/Composer/packages/client/src/components/AppComponents/MainContainer.tsx index 5b37ffc206..f21dc0c4f2 100644 --- a/Composer/packages/client/src/components/AppComponents/MainContainer.tsx +++ b/Composer/packages/client/src/components/AppComponents/MainContainer.tsx @@ -3,6 +3,8 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; +import { NotificationContainer } from '../NotificationContainer'; + import { SideBar } from './SideBar'; import { RightPanel } from './RightPanel'; import { Assistant } from './Assistant'; @@ -18,6 +20,7 @@ export const MainContainer = () => { + ); }; diff --git a/Composer/packages/client/src/components/NotificationCard.tsx b/Composer/packages/client/src/components/NotificationCard.tsx new file mode 100644 index 0000000000..c60edfbddc --- /dev/null +++ b/Composer/packages/client/src/components/NotificationCard.tsx @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css, keyframes } from '@emotion/core'; +import { IconButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; +import { useEffect, useRef, useState } from 'react'; +import { FontSizes } from '@uifabric/fluent-theme'; +import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import formatMessage from 'format-message'; + +// -------------------- Styles -------------------- // + +const fadeIn = keyframes` + from { opacity: 0; transform: translate3d(40px,0,0) } + to { opacity: 1; translate3d(0,0,0) } +`; + +const fadeOut = (height: number) => keyframes` + from { opacity: 1; height: ${height}px} + to { opacity: 0; height:0} +`; + +const cardContainer = (show: boolean, ref?: HTMLDivElement | null) => () => { + let height = 100; + if (ref) { + height = ref.clientHeight; + } + + return css` + border-left: 4px solid #0078d4; + background: white; + box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108); + width: 340px; + border-radius: 2px; + display: flex; + flex-direction: column; + margin-bottom: 8px; + animation-duration: ${show ? '0.467' : '0.2'}s; + animation-timing-function: ${show ? 'cubic-bezier(0.1, 0.9, 0.2, 1)' : 'linear'}; + animation-fill-mode: both; + animation-name: ${show ? fadeIn : fadeOut(height)}; + `; +}; + +const cancelButton = css` + float: right; + color: #605e5c; + margin-left: auto; + width: 24px; + height: 24px; +`; + +const cardContent = css` + display: flex; + padding: 0 8px 16px 12px; + min-height: 64px; +`; + +const cardDetail = css` + margin-left: 8px; + flex-grow: 1; +`; + +const cardType = css` + margin-top: 4px; + color: #a80000; +`; + +const cardTitle = css` + font-size: ${FontSizes.size16}; + lint-height: 22px; + margin-right: 16px; +`; + +const cardDescription = css` + text-size-adjust: none; + font-size: ${FontSizes.size10}; + margin-top: 8px; + margin-right: 16px; + word-break: break-word; +`; + +const linkButton = css` + color: #0078d4; + float: right; + font-size: 12px; + height: auto; +`; + +const getShimmerStyles = { + root: { + marginTop: '12px', + }, + shimmerWrapper: [ + { + backgroundColor: '#EDEBE9', + }, + ], + shimmerGradient: [ + { + backgroundImage: 'radial-gradient(at 50% 50%, #0078D4 0%, #EDEBE9 100%);', + }, + ], +}; +// -------------------- NotificationCard -------------------- // + +export enum NotificationType { + info = 1, + warning, + error, + loading, +} + +export interface ILink { + label: string; + onClick: () => void; +} + +export interface ICardProps { + title: string; + description: string; + type: NotificationType; + retentionTime?: number; + link?: ILink; + onRenderCardContent?: (props: ICardProps) => JSX.Element; +} + +export interface INotificationProps { + id: string; + cardProps: ICardProps; + onDismiss: (id: string) => void; +} + +export class Timer { + timerId: NodeJS.Timeout; + start: number; + remaining: number; + pausing = false; + callback: () => void; + + constructor(callback: () => void, delay: number) { + this.remaining = delay; + this.callback = callback; + this.start = Date.now(); + this.timerId = setTimeout(callback, this.remaining); + } + + pause() { + if (!this.pausing) { + clearTimeout(this.timerId); + this.remaining -= Date.now() - this.start; + this.pausing = true; + } + } + + resume() { + this.pausing = false; + this.start = Date.now(); + clearTimeout(this.timerId); + this.timerId = setTimeout(this.callback, this.remaining); + } + + clear() { + clearTimeout(this.timerId); + } +} + +const renderCardContent = (props: ICardProps) => { + const { title, description, type, link } = props; + return ( +
+ {type === NotificationType.error && } +
+
{title}
+
{description}
+ {link && ( + + {link.label} + + )} + {type === NotificationType.loading && ( + + )} +
+
+ ); +}; + +export const NotificationCard = (props: INotificationProps) => { + const { cardProps, id, onDismiss } = props; + const [show, setShow] = useState(true); + const containerRef = useRef(null); + + const removeNotification = () => { + setShow(false); + }; + + // notification will disappear in 5 secs + const timer = useRef(cardProps.retentionTime ? new Timer(removeNotification, cardProps.retentionTime) : null).current; + + useEffect(() => { + return () => { + if (timer) { + timer.clear(); + } + }; + }, []); + + const handleMouseOver = () => { + if (timer) { + timer.pause(); + } + }; + + const handleMouseLeave = () => { + if (timer) { + timer.resume(); + } + }; + + const handleAnimationEnd = () => { + if (!show) onDismiss(id); + }; + + let renderCard = renderCardContent; + if (cardProps.onRenderCardContent) renderCard = cardProps.onRenderCardContent; + + return ( +
{}} + onMouseLeave={handleMouseLeave} + onMouseOver={handleMouseOver} + > + + {renderCard(cardProps)} +
+ ); +}; diff --git a/Composer/packages/client/src/components/NotificationContainer.tsx b/Composer/packages/client/src/components/NotificationContainer.tsx new file mode 100644 index 0000000000..ec89a89d55 --- /dev/null +++ b/Composer/packages/client/src/components/NotificationContainer.tsx @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { useRecoilValue } from 'recoil'; + +import { notificationsState, dispatcherState } from '../recoilModel'; + +import { NotificationCard } from './NotificationCard'; + +// -------------------- Styles -------------------- // + +const container = css` + cursor: default; + position: absolute; + right: 0px; + padding: 6px; +`; + +// -------------------- NotificationContainer -------------------- // + +export const NotificationContainer = () => { + const notifications = useRecoilValue(notificationsState); + const { deleteNotification } = useRecoilValue(dispatcherState); + + return ( +
+ {notifications.map((item) => { + return ; + })} +
+ ); +}; diff --git a/Composer/packages/client/src/components/NotificationIcon.tsx b/Composer/packages/client/src/components/NotificationIcon.tsx new file mode 100644 index 0000000000..ed5e925991 --- /dev/null +++ b/Composer/packages/client/src/components/NotificationIcon.tsx @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; + +// -------------------- Styles -------------------- // + +const container = css` + cursor: pointer; + position: absolute; + display: flex; + right: 0px; + width: 48px; + height: 100%; + align-items: center; + justify-content: space-around; +`; + +const ringer = css` + color: white; +`; + +const circleMask = css` + position: absolute; + top: 8px; + right: 8px; + color: #005a9e; +`; + +const numberContainer = css` + height: 16px; + width: 16px; + display: flex; + align-items: center; + justify-content: space-around; + text-align: center; + position: absolute; + right: 8px; + top: 8px; +`; + +const numberText = css` + transform: scale(0.5); + font-size: 20px; + color: white; +`; + +// -------------------- NotificationIcon -------------------- // + +interface INotificationIconProps { + number: number; +} + +export const NotificationIcon = (props: INotificationIconProps) => { + const { number } = props; + return ( +
+ + {number > 0 && } + {number > 0 && ( +
+ {number > 99 ? '...' : number} +
+ )} +
+ ); +}; diff --git a/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx b/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx new file mode 100644 index 0000000000..a6a84333da --- /dev/null +++ b/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as React from 'react'; + +import { renderWithRecoil } from '../../../__tests__/testUtils/renderWithRecoil'; +import { NotificationCard, NotificationType, Timer } from '../NotificationCard'; + +jest.useFakeTimers(); + +describe('', () => { + it('should render the NotificationCard', () => { + const cardProps = { + title: 'There was error creating your KB', + description: 'error', + retentionTime: 1, + type: NotificationType.error, + }; + const onDismiss = jest.fn(); + const { container } = renderWithRecoil(); + + expect(container).toHaveTextContent('There was error creating your KB'); + }); + + it('should render the customized card', () => { + const cardProps = { + title: 'There was error creating your KB', + description: 'error', + retentionTime: 5000, + type: NotificationType.error, + onRenderCardContent: () =>
customized
, + }; + const onDismiss = jest.fn(); + const { container } = renderWithRecoil(); + + expect(container).toHaveTextContent('customized'); + }); +}); + +describe('Notification Time Management', () => { + it('should invoke callback', () => { + const callback = jest.fn(); + new Timer(callback, 0); + expect(callback).not.toBeCalled(); + jest.runAllTimers(); + expect(callback).toHaveBeenCalled(); + }); + + it('should pause and resume', () => { + const callback = jest.fn(); + const timer = new Timer(callback, 1); + timer.pause(); + expect(timer.pausing).toBeTruthy(); + timer.resume(); + expect(timer.pausing).toBeFalsy(); + }); +}); diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 7d784a6141..e683171a76 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -584,8 +584,7 @@ const DesignPage: React.FC 0) { await importQnAFromUrls({ id: `${dialogId}.${locale}`, urls }); diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index d99fb0ec5d..4d3454737a 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -55,7 +55,7 @@ const wrapDispatcher = (dispatchers, forceUpdate) => { return Object.keys(dispatchers).reduce((boundDispatchers, dispatcherName) => { const dispatcher = async (...args) => { forceUpdate([]); //gurarantee the snapshot get the latset state - await dispatchers[dispatcherName](...args); + return await dispatchers[dispatcherName](...args); }; boundDispatchers[dispatcherName] = dispatcher; return boundDispatchers; diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 1caacbaf22..d8c4593562 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -10,6 +10,7 @@ import { RuntimeTemplate, AppUpdateState, BoilerplateVersion, + Notification, } from '../../recoilModel/types'; import { getUserSettings } from '../utils'; import onboardingStorage from '../../utils/onboardingStorage'; @@ -150,3 +151,8 @@ export const boilerplateVersionState = atom({ updateRequired: false, }, }); + +export const notificationsState = atom({ + key: getFullyQualifiedKey('boilerplateVersion'), + default: [], +}); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/index.ts b/Composer/packages/client/src/recoilModel/dispatchers/index.ts index 2b79a13807..f33bf86dbe 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/index.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/index.ts @@ -18,6 +18,7 @@ import { settingsDispatcher } from './setting'; import { skillDispatcher } from './skill'; import { userDispatcher } from './user'; import { multilangDispatcher } from './multilang'; +import { notificationDispatcher } from './notification'; const createDispatchers = () => { return { @@ -38,6 +39,7 @@ const createDispatchers = () => { ...skillDispatcher(), ...userDispatcher(), ...multilangDispatcher(), + ...notificationDispatcher(), }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts new file mode 100644 index 0000000000..949fd5631d --- /dev/null +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -0,0 +1,43 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CallbackInterface, useRecoilCallback } from 'recoil'; +import { v4 as uuid } from 'uuid'; + +import { notificationsState } from '../atoms/appState'; +import { ICardProps } from '../../components/NotificationCard'; + +export const addNotificationInternal = ({ set }: CallbackInterface, notification: ICardProps) => { + const id = uuid(6); + set(notificationsState, (notifications) => [...notifications, { id, cardProps: notification }]); + return id; +}; + +export const deleteNotificationInternal = ({ set }: CallbackInterface, id: string) => { + set(notificationsState, (items) => { + const notifications = [...items]; + const index = notifications.findIndex((item) => item.id === id); + if (index > -1) { + notifications.splice(index, 1); + } + return notifications; + }); +}; + +export const notificationDispatcher = () => { + const addNotification = useRecoilCallback( + (callbackHelper: CallbackInterface) => (notification: ICardProps): string => { + return addNotificationInternal(callbackHelper, notification); + } + ); + + const deleteNotification = useRecoilCallback((callbackHelper: CallbackInterface) => (id: string) => { + deleteNotificationInternal(callbackHelper, id); + }); + + return { + addNotification, + deleteNotification, + }; +}; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 62dc9bbfc5..1e5663af27 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -5,13 +5,14 @@ import { QnAFile } from '@bfc/shared'; import { useRecoilCallback, CallbackInterface } from 'recoil'; import qnaWorker from '../parsers/qnaWorker'; -import { qnaFilesState, qnaAllUpViewStatusState, projectIdState, localeState, settingsState } from '../atoms/botState'; -import { QnAAllUpViewStatus } from '../types'; +import { qnaFilesState, projectIdState, localeState, settingsState } from '../atoms/botState'; import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { getBaseName } from '../../utils/fileUtil'; +import { navigateTo } from '../../utils/navigation'; +import { getQnAFailed, getQnASuccess, getQnAPending } from './../../utils/notifications'; import httpClient from './../../utils/httpUtil'; -import { setError } from './shared'; +import { addNotificationInternal, deleteNotificationInternal } from './notification'; export const updateQnAFileState = async ( callbackHelpers: CallbackInterface, @@ -89,10 +90,13 @@ export const qnaDispatcher = () => { const importQnAFromUrls = useRecoilCallback( (callbackHelpers: CallbackInterface) => async ({ id, urls }: { id: string; urls: string[] }) => { - const { set, snapshot } = callbackHelpers; + const { snapshot } = callbackHelpers; const qnaFiles = await snapshot.getPromise(qnaFilesState); + const projectId = await snapshot.getPromise(projectIdState); const qnaFile = qnaFiles.find((f) => f.id === id); - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Loading); + + const notificationId = await addNotificationInternal(callbackHelpers, getQnAPending(urls)); + try { const response = await httpClient.get(`/utilities/qna/parse`, { params: { urls: encodeURIComponent(urls.join(',')) }, @@ -100,11 +104,14 @@ export const qnaDispatcher = () => { const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; await updateQnAFileState(callbackHelpers, { id, content }); - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); + addNotificationInternal( + callbackHelpers, + getQnASuccess(() => navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`)) + ); } catch (err) { - setError(callbackHelpers, err); + addNotificationInternal(callbackHelpers, getQnAFailed(err.response?.data?.message)); } finally { - set(qnaAllUpViewStatusState, QnAAllUpViewStatus.Success); + deleteNotificationInternal(callbackHelpers, notificationId); } } ); diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts index c9f8d189f8..6873b42535 100644 --- a/Composer/packages/client/src/recoilModel/types.ts +++ b/Composer/packages/client/src/recoilModel/types.ts @@ -5,6 +5,8 @@ import { AppUpdaterSettings, CodeEditorSettings, PromptTab } from '@bfc/shared'; import { AppUpdaterStatus } from '../constants'; +import { ICardProps } from './../components/NotificationCard'; + export interface StateError { status?: number; summary: string; @@ -112,3 +114,8 @@ export enum QnAAllUpViewStatus { Success, Failed, } + +export type Notification = { + cardProps: ICardProps; + id: string; +}; diff --git a/Composer/packages/client/src/utils/notifications.ts b/Composer/packages/client/src/utils/notifications.ts new file mode 100644 index 0000000000..3dd9a0da4b --- /dev/null +++ b/Composer/packages/client/src/utils/notifications.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import formatMessage from 'format-message'; + +import { NotificationType } from './../components/NotificationCard'; +import { ICardProps } from './../components/NotificationCard'; +export const getQnAPending = (urls: string[]) => { + return { + title: formatMessage('Creating your knowledge base'), + description: formatMessage('Extacting QnA pairs from ') + urls.join(' '), + type: NotificationType.loading, + }; +}; + +export const getQnASuccess = (callback: () => void): ICardProps => { + return { + title: formatMessage('Your knowledge base Surface go FAQ is ready!'), + description: '', + type: NotificationType.info, + retentionTime: 5000, + link: { + label: formatMessage('View KB'), + onClick: callback, + }, + }; +}; + +export const getQnAFailed = (error: string) => { + return { + title: formatMessage('There was error creating your KB'), + description: error, + retentionTime: 5000, + type: NotificationType.error, + }; +}; From dc54276b4306f03420db419a86f48b0e7613d064 Mon Sep 17 00:00:00 2001 From: leilzh Date: Thu, 10 Sep 2020 21:25:34 +0800 Subject: [PATCH 02/11] update some style --- .../client/src/components/NotificationCard.tsx | 12 ++++++++++-- .../client/src/recoilModel/dispatchers/qna.ts | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Composer/packages/client/src/components/NotificationCard.tsx b/Composer/packages/client/src/components/NotificationCard.tsx index c60edfbddc..372f6827e4 100644 --- a/Composer/packages/client/src/components/NotificationCard.tsx +++ b/Composer/packages/client/src/components/NotificationCard.tsx @@ -63,11 +63,16 @@ const cardDetail = css` flex-grow: 1; `; -const cardType = css` +const errorType = css` margin-top: 4px; color: #a80000; `; +const successType = css` + margin-top: 4px; + color: greenyellow; +`; + const cardTitle = css` font-size: ${FontSizes.size16}; lint-height: 22px; @@ -92,6 +97,7 @@ const linkButton = css` const getShimmerStyles = { root: { marginTop: '12px', + marginBottom: '8px', }, shimmerWrapper: [ { @@ -111,6 +117,7 @@ export enum NotificationType { warning, error, loading, + success, } export interface ILink { @@ -171,7 +178,8 @@ const renderCardContent = (props: ICardProps) => { const { title, description, type, link } = props; return (
- {type === NotificationType.error && } + {type === NotificationType.error && } + {type === NotificationType.success && }
{title}
{description}
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 1e5663af27..f56f94ab08 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -104,9 +104,12 @@ export const qnaDispatcher = () => { const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; await updateQnAFileState(callbackHelpers, { id, content }); - addNotificationInternal( + const notificationId = addNotificationInternal( callbackHelpers, - getQnASuccess(() => navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`)) + getQnASuccess(() => { + navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`); + deleteNotificationInternal(callbackHelpers, notificationId); + }) ); } catch (err) { addNotificationInternal(callbackHelpers, getQnAFailed(err.response?.data?.message)); From de3bc8fedbdb6f7359c1fef75d57855ebc95dd5a Mon Sep 17 00:00:00 2001 From: leilzh Date: Thu, 10 Sep 2020 21:59:25 +0800 Subject: [PATCH 03/11] update the style --- Composer/packages/client/src/components/NotificationCard.tsx | 2 ++ Composer/packages/client/src/utils/notifications.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Composer/packages/client/src/components/NotificationCard.tsx b/Composer/packages/client/src/components/NotificationCard.tsx index 372f6827e4..6065cc9fb7 100644 --- a/Composer/packages/client/src/components/NotificationCard.tsx +++ b/Composer/packages/client/src/components/NotificationCard.tsx @@ -92,6 +92,7 @@ const linkButton = css` float: right; font-size: 12px; height: auto; + margin-right: 8px; `; const getShimmerStyles = { @@ -217,6 +218,7 @@ export const NotificationCard = (props: INotificationProps) => { }, []); const handleMouseOver = () => { + // if mouse over stop the time and record the remaining time if (timer) { timer.pause(); } diff --git a/Composer/packages/client/src/utils/notifications.ts b/Composer/packages/client/src/utils/notifications.ts index 3dd9a0da4b..f5b2187d37 100644 --- a/Composer/packages/client/src/utils/notifications.ts +++ b/Composer/packages/client/src/utils/notifications.ts @@ -16,7 +16,7 @@ export const getQnASuccess = (callback: () => void): ICardProps => { return { title: formatMessage('Your knowledge base Surface go FAQ is ready!'), description: '', - type: NotificationType.info, + type: NotificationType.success, retentionTime: 5000, link: { label: formatMessage('View KB'), From 7a3f4eb563fa8bd62003ecb00f7ee7a1682c0b7c Mon Sep 17 00:00:00 2001 From: leilzh Date: Fri, 11 Sep 2020 08:56:49 +0800 Subject: [PATCH 04/11] update the icon color --- Composer/packages/client/src/components/NotificationCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/components/NotificationCard.tsx b/Composer/packages/client/src/components/NotificationCard.tsx index 6065cc9fb7..bc64d2916a 100644 --- a/Composer/packages/client/src/components/NotificationCard.tsx +++ b/Composer/packages/client/src/components/NotificationCard.tsx @@ -70,7 +70,7 @@ const errorType = css` const successType = css` margin-top: 4px; - color: greenyellow; + color: #27ae60; `; const cardTitle = css` From 9d28e88a90f3d0d6cbadcb31384141dcdaf082c3 Mon Sep 17 00:00:00 2001 From: leilzh Date: Fri, 11 Sep 2020 09:47:36 +0800 Subject: [PATCH 05/11] fix conflict --- Composer/packages/client/src/recoilModel/atoms/appState.ts | 6 ------ .../packages/client/src/recoilModel/dispatchers/index.ts | 2 -- 2 files changed, 8 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index e331f5ac6e..9b098a1c29 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -11,7 +11,6 @@ import { AppUpdateState, BoilerplateVersion, Notification, - PluginConfig, ExtensionConfig, } from '../../recoilModel/types'; import { getUserSettings } from '../utils'; @@ -159,11 +158,6 @@ export const notificationsState = atom({ default: [], }); -export const pluginsState = atom({ - key: getFullyQualifiedKey('plugins'), - default: [], -}); - export const extensionsState = atom({ key: getFullyQualifiedKey('extensions'), default: [], diff --git a/Composer/packages/client/src/recoilModel/dispatchers/index.ts b/Composer/packages/client/src/recoilModel/dispatchers/index.ts index da5e71c2e0..edbae77387 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/index.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/index.ts @@ -19,7 +19,6 @@ import { skillDispatcher } from './skill'; import { userDispatcher } from './user'; import { multilangDispatcher } from './multilang'; import { notificationDispatcher } from './notification'; -import { pluginsDispatcher } from './plugins'; import { extensionsDispatcher } from './extensions'; const createDispatchers = () => { @@ -42,7 +41,6 @@ const createDispatchers = () => { ...userDispatcher(), ...multilangDispatcher(), ...notificationDispatcher(), - ...pluginsDispatcher(), ...extensionsDispatcher(), }; }; From ff8cfeb5339b66e95ff247b739640d71043bb7f3 Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 14 Sep 2020 17:49:58 +0800 Subject: [PATCH 06/11] remove the timer when error --- Composer/packages/client/src/utils/notifications.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/Composer/packages/client/src/utils/notifications.ts b/Composer/packages/client/src/utils/notifications.ts index f5b2187d37..cc0a722f0a 100644 --- a/Composer/packages/client/src/utils/notifications.ts +++ b/Composer/packages/client/src/utils/notifications.ts @@ -29,7 +29,6 @@ export const getQnAFailed = (error: string) => { return { title: formatMessage('There was error creating your KB'), description: error, - retentionTime: 5000, type: NotificationType.error, }; }; From 5f5e06a38a5a0c31d829fb7a695357dc9d725571 Mon Sep 17 00:00:00 2001 From: leilzh Date: Thu, 17 Sep 2020 10:26:38 +0800 Subject: [PATCH 07/11] update the notification create flow --- .../recoilModel/dispatchers/notification.ts | 18 ++++++++++-------- .../client/src/recoilModel/dispatchers/qna.ts | 15 ++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts index 949fd5631d..eb2b223137 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -7,11 +7,15 @@ import { v4 as uuid } from 'uuid'; import { notificationsState } from '../atoms/appState'; import { ICardProps } from '../../components/NotificationCard'; +import { Notification } from '../../recoilModel/types'; -export const addNotificationInternal = ({ set }: CallbackInterface, notification: ICardProps) => { +export const createNotifiction = (notificationCard: ICardProps) => { const id = uuid(6); - set(notificationsState, (notifications) => [...notifications, { id, cardProps: notification }]); - return id; + return { id, cardProps: notificationCard }; +}; + +export const addNotificationInternal = ({ set }: CallbackInterface, notification: Notification) => { + set(notificationsState, (notifications) => [...notifications, notification]); }; export const deleteNotificationInternal = ({ set }: CallbackInterface, id: string) => { @@ -26,11 +30,9 @@ export const deleteNotificationInternal = ({ set }: CallbackInterface, id: strin }; export const notificationDispatcher = () => { - const addNotification = useRecoilCallback( - (callbackHelper: CallbackInterface) => (notification: ICardProps): string => { - return addNotificationInternal(callbackHelper, notification); - } - ); + const addNotification = useRecoilCallback((callbackHelper: CallbackInterface) => (notification: Notification) => { + return addNotificationInternal(callbackHelper, notification); + }); const deleteNotification = useRecoilCallback((callbackHelper: CallbackInterface) => (id: string) => { deleteNotificationInternal(callbackHelper, id); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index f56f94ab08..93602ae518 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -12,7 +12,7 @@ import { navigateTo } from '../../utils/navigation'; import { getQnAFailed, getQnASuccess, getQnAPending } from './../../utils/notifications'; import httpClient from './../../utils/httpUtil'; -import { addNotificationInternal, deleteNotificationInternal } from './notification'; +import { addNotificationInternal, deleteNotificationInternal, createNotifiction } from './notification'; export const updateQnAFileState = async ( callbackHelpers: CallbackInterface, @@ -95,7 +95,8 @@ export const qnaDispatcher = () => { const projectId = await snapshot.getPromise(projectIdState); const qnaFile = qnaFiles.find((f) => f.id === id); - const notificationId = await addNotificationInternal(callbackHelpers, getQnAPending(urls)); + const notification = createNotifiction(getQnAPending(urls)); + addNotificationInternal(callbackHelpers, notification); try { const response = await httpClient.get(`/utilities/qna/parse`, { @@ -104,17 +105,17 @@ export const qnaDispatcher = () => { const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; await updateQnAFileState(callbackHelpers, { id, content }); - const notificationId = addNotificationInternal( - callbackHelpers, + const notification = createNotifiction( getQnASuccess(() => { navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`); - deleteNotificationInternal(callbackHelpers, notificationId); + deleteNotificationInternal(callbackHelpers, notification.id); }) ); + addNotificationInternal(callbackHelpers, notification); } catch (err) { - addNotificationInternal(callbackHelpers, getQnAFailed(err.response?.data?.message)); + addNotificationInternal(callbackHelpers, createNotifiction(getQnAFailed(err.response?.data?.message))); } finally { - deleteNotificationInternal(callbackHelpers, notificationId); + deleteNotificationInternal(callbackHelpers, notification.id); } } ); From 41f22a14b235a5964bfbf4ba6cb0ed2975facc23 Mon Sep 17 00:00:00 2001 From: leilzh Date: Thu, 17 Sep 2020 10:28:10 +0800 Subject: [PATCH 08/11] remove the return in dispatcher --- Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx index 4d3454737a..d99fb0ec5d 100644 --- a/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx +++ b/Composer/packages/client/src/recoilModel/DispatcherWrapper.tsx @@ -55,7 +55,7 @@ const wrapDispatcher = (dispatchers, forceUpdate) => { return Object.keys(dispatchers).reduce((boundDispatchers, dispatcherName) => { const dispatcher = async (...args) => { forceUpdate([]); //gurarantee the snapshot get the latset state - return await dispatchers[dispatcherName](...args); + await dispatchers[dispatcherName](...args); }; boundDispatchers[dispatcherName] = dispatcher; return boundDispatchers; From c964ad3686cc351f66b6ee63321337a2c1940ff7 Mon Sep 17 00:00:00 2001 From: leilzh Date: Fri, 18 Sep 2020 10:44:05 +0800 Subject: [PATCH 09/11] use typs instead of interface --- .../src/components/NotificationCard.tsx | 88 ++++++------------- .../src/components/NotificationContainer.tsx | 3 +- .../src/components/NotificationIcon.tsx | 69 --------------- .../__tests__/NotificationCard.test.tsx | 11 +-- .../recoilModel/dispatchers/notification.ts | 8 +- .../client/src/recoilModel/dispatchers/qna.ts | 51 ++++++----- .../packages/client/src/recoilModel/types.ts | 7 +- .../client/src/utils/notifications.ts | 19 ++-- Composer/packages/client/src/utils/timer.ts | 36 ++++++++ 9 files changed, 113 insertions(+), 179 deletions(-) delete mode 100644 Composer/packages/client/src/components/NotificationIcon.tsx create mode 100644 Composer/packages/client/src/utils/timer.ts diff --git a/Composer/packages/client/src/components/NotificationCard.tsx b/Composer/packages/client/src/components/NotificationCard.tsx index bc64d2916a..d865f96301 100644 --- a/Composer/packages/client/src/components/NotificationCard.tsx +++ b/Composer/packages/client/src/components/NotificationCard.tsx @@ -3,6 +3,7 @@ /** @jsx jsx */ import { jsx, css, keyframes } from '@emotion/core'; +import React from 'react'; import { IconButton, ActionButton } from 'office-ui-fabric-react/lib/Button'; import { useEffect, useRef, useState } from 'react'; import { FontSizes } from '@uifabric/fluent-theme'; @@ -10,6 +11,8 @@ import { Shimmer, ShimmerElementType } from 'office-ui-fabric-react/lib/Shimmer' import { Icon } from 'office-ui-fabric-react/lib/Icon'; import formatMessage from 'format-message'; +import Timer from '../utils/timer'; + // -------------------- Styles -------------------- // const fadeIn = keyframes` @@ -113,83 +116,43 @@ const getShimmerStyles = { }; // -------------------- NotificationCard -------------------- // -export enum NotificationType { - info = 1, - warning, - error, - loading, - success, -} +export type NotificationType = 'info' | 'warning' | 'error' | 'pending' | 'success'; -export interface ILink { +export type Link = { label: string; onClick: () => void; -} +}; -export interface ICardProps { - title: string; - description: string; +export type CardProps = { type: NotificationType; + title: string; + description?: string; retentionTime?: number; - link?: ILink; - onRenderCardContent?: (props: ICardProps) => JSX.Element; -} + link?: Link; + onRenderCardContent?: (props: CardProps) => JSX.Element; +}; -export interface INotificationProps { +export type NotificationProps = { id: string; - cardProps: ICardProps; + cardProps: CardProps; onDismiss: (id: string) => void; -} - -export class Timer { - timerId: NodeJS.Timeout; - start: number; - remaining: number; - pausing = false; - callback: () => void; - - constructor(callback: () => void, delay: number) { - this.remaining = delay; - this.callback = callback; - this.start = Date.now(); - this.timerId = setTimeout(callback, this.remaining); - } - - pause() { - if (!this.pausing) { - clearTimeout(this.timerId); - this.remaining -= Date.now() - this.start; - this.pausing = true; - } - } - - resume() { - this.pausing = false; - this.start = Date.now(); - clearTimeout(this.timerId); - this.timerId = setTimeout(this.callback, this.remaining); - } - - clear() { - clearTimeout(this.timerId); - } -} +}; -const renderCardContent = (props: ICardProps) => { +const defaultCardContentRenderer = (props: CardProps) => { const { title, description, type, link } = props; return (
- {type === NotificationType.error && } - {type === NotificationType.success && } + {type === 'error' && } + {type === 'success' && }
{title}
-
{description}
+ {description &&
{description}
} {link && ( {link.label} )} - {type === NotificationType.loading && ( + {type === 'pending' && ( )}
@@ -197,10 +160,10 @@ const renderCardContent = (props: ICardProps) => { ); }; -export const NotificationCard = (props: INotificationProps) => { +export const NotificationCard = React.memo((props: NotificationProps) => { const { cardProps, id, onDismiss } = props; const [show, setShow] = useState(true); - const containerRef = useRef(null); + const containerRef = useRef(null); const removeNotification = () => { setShow(false); @@ -234,8 +197,7 @@ export const NotificationCard = (props: INotificationProps) => { if (!show) onDismiss(id); }; - let renderCard = renderCardContent; - if (cardProps.onRenderCardContent) renderCard = cardProps.onRenderCardContent; + const renderCard = cardProps.onRenderCardContent || defaultCardContentRenderer; return (
{ css={cardContainer(show, containerRef.current)} role="presentation" onAnimationEnd={handleAnimationEnd} - onFocus={() => {}} + onFocus={() => void 0} onMouseLeave={handleMouseLeave} onMouseOver={handleMouseOver} > @@ -256,4 +218,4 @@ export const NotificationCard = (props: INotificationProps) => { {renderCard(cardProps)}
); -}; +}); diff --git a/Composer/packages/client/src/components/NotificationContainer.tsx b/Composer/packages/client/src/components/NotificationContainer.tsx index ec89a89d55..bfa85147ba 100644 --- a/Composer/packages/client/src/components/NotificationContainer.tsx +++ b/Composer/packages/client/src/components/NotificationContainer.tsx @@ -27,7 +27,8 @@ export const NotificationContainer = () => { return (
{notifications.map((item) => { - return ; + const { id, ...rest } = item; + return ; })}
); diff --git a/Composer/packages/client/src/components/NotificationIcon.tsx b/Composer/packages/client/src/components/NotificationIcon.tsx deleted file mode 100644 index ed5e925991..0000000000 --- a/Composer/packages/client/src/components/NotificationIcon.tsx +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { jsx, css } from '@emotion/core'; -import { Icon } from 'office-ui-fabric-react/lib/Icon'; - -// -------------------- Styles -------------------- // - -const container = css` - cursor: pointer; - position: absolute; - display: flex; - right: 0px; - width: 48px; - height: 100%; - align-items: center; - justify-content: space-around; -`; - -const ringer = css` - color: white; -`; - -const circleMask = css` - position: absolute; - top: 8px; - right: 8px; - color: #005a9e; -`; - -const numberContainer = css` - height: 16px; - width: 16px; - display: flex; - align-items: center; - justify-content: space-around; - text-align: center; - position: absolute; - right: 8px; - top: 8px; -`; - -const numberText = css` - transform: scale(0.5); - font-size: 20px; - color: white; -`; - -// -------------------- NotificationIcon -------------------- // - -interface INotificationIconProps { - number: number; -} - -export const NotificationIcon = (props: INotificationIconProps) => { - const { number } = props; - return ( -
- - {number > 0 && } - {number > 0 && ( -
- {number > 99 ? '...' : number} -
- )} -
- ); -}; diff --git a/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx b/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx index a6a84333da..fdff6b95e2 100644 --- a/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx +++ b/Composer/packages/client/src/components/__tests__/NotificationCard.test.tsx @@ -4,17 +4,18 @@ import * as React from 'react'; import { renderWithRecoil } from '../../../__tests__/testUtils/renderWithRecoil'; -import { NotificationCard, NotificationType, Timer } from '../NotificationCard'; +import { NotificationCard, CardProps } from '../NotificationCard'; +import Timer from '../../utils/timer'; jest.useFakeTimers(); describe('', () => { it('should render the NotificationCard', () => { - const cardProps = { + const cardProps: CardProps = { title: 'There was error creating your KB', description: 'error', retentionTime: 1, - type: NotificationType.error, + type: 'error', }; const onDismiss = jest.fn(); const { container } = renderWithRecoil(); @@ -23,11 +24,11 @@ describe('', () => { }); it('should render the customized card', () => { - const cardProps = { + const cardProps: CardProps = { title: 'There was error creating your KB', description: 'error', retentionTime: 5000, - type: NotificationType.error, + type: 'error', onRenderCardContent: () =>
customized
, }; const onDismiss = jest.fn(); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts index eb2b223137..9a3c010ae7 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -6,12 +6,12 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { v4 as uuid } from 'uuid'; import { notificationsState } from '../atoms/appState'; -import { ICardProps } from '../../components/NotificationCard'; +import { CardProps } from '../../components/NotificationCard'; import { Notification } from '../../recoilModel/types'; -export const createNotifiction = (notificationCard: ICardProps) => { - const id = uuid(6); - return { id, cardProps: notificationCard }; +export const createNotifiction = (notificationCard: CardProps): Notification => { + const id = uuid(6) + ''; + return { id, ...notificationCard }; }; export const addNotificationInternal = ({ set }: CallbackInterface, notification: Notification) => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts index 93602ae518..711961f65b 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/qna.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/qna.ts @@ -9,9 +9,13 @@ import { qnaFilesState, projectIdState, localeState, settingsState } from '../at import qnaFileStatusStorage from '../../utils/qnaFileStatusStorage'; import { getBaseName } from '../../utils/fileUtil'; import { navigateTo } from '../../utils/navigation'; +import { + getQnaFailedNotification, + getQnaSuccessNotification, + getQnaPendingNotification, +} from '../../utils/notifications'; +import httpClient from '../../utils/httpUtil'; -import { getQnAFailed, getQnASuccess, getQnAPending } from './../../utils/notifications'; -import httpClient from './../../utils/httpUtil'; import { addNotificationInternal, deleteNotificationInternal, createNotifiction } from './notification'; export const updateQnAFileState = async ( @@ -95,28 +99,31 @@ export const qnaDispatcher = () => { const projectId = await snapshot.getPromise(projectIdState); const qnaFile = qnaFiles.find((f) => f.id === id); - const notification = createNotifiction(getQnAPending(urls)); + const notification = createNotifiction(getQnaPendingNotification(urls)); addNotificationInternal(callbackHelpers, notification); - try { - const response = await httpClient.get(`/utilities/qna/parse`, { - params: { urls: encodeURIComponent(urls.join(',')) }, - }); - const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; - - await updateQnAFileState(callbackHelpers, { id, content }); - const notification = createNotifiction( - getQnASuccess(() => { - navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`); - deleteNotificationInternal(callbackHelpers, notification.id); - }) - ); - addNotificationInternal(callbackHelpers, notification); - } catch (err) { - addNotificationInternal(callbackHelpers, createNotifiction(getQnAFailed(err.response?.data?.message))); - } finally { - deleteNotificationInternal(callbackHelpers, notification.id); - } + // try { + // const response = await httpClient.get(`/utilities/qna/parse`, { + // params: { urls: encodeURIComponent(urls.join(',')) }, + // }); + // const content = qnaFile ? qnaFile.content + '\n' + response.data : response.data; + + // await updateQnAFileState(callbackHelpers, { id, content }); + // const notification = createNotifiction( + // getQnaSuccessNotification(() => { + // navigateTo(`/bot/${projectId}/knowledge-base/${getBaseName(id)}`); + // deleteNotificationInternal(callbackHelpers, notification.id); + // }) + // ); + // addNotificationInternal(callbackHelpers, notification); + // } catch (err) { + // addNotificationInternal( + // callbackHelpers, + // createNotifiction(getQnaFailedNotification(err.response?.data?.message)) + // ); + // } finally { + // deleteNotificationInternal(callbackHelpers, notification.id); + // } } ); diff --git a/Composer/packages/client/src/recoilModel/types.ts b/Composer/packages/client/src/recoilModel/types.ts index be26c3d5e9..20f7f4a592 100644 --- a/Composer/packages/client/src/recoilModel/types.ts +++ b/Composer/packages/client/src/recoilModel/types.ts @@ -5,7 +5,7 @@ import { AppUpdaterSettings, CodeEditorSettings, PromptTab } from '@bfc/shared'; import { AppUpdaterStatus } from '../constants'; -import { ICardProps } from './../components/NotificationCard'; +import { CardProps } from './../components/NotificationCard'; export interface StateError { status?: number; @@ -131,7 +131,4 @@ export enum QnAAllUpViewStatus { Failed, } -export type Notification = { - cardProps: ICardProps; - id: string; -}; +export type Notification = CardProps & { id: string }; diff --git a/Composer/packages/client/src/utils/notifications.ts b/Composer/packages/client/src/utils/notifications.ts index cc0a722f0a..5ea1b4dc64 100644 --- a/Composer/packages/client/src/utils/notifications.ts +++ b/Composer/packages/client/src/utils/notifications.ts @@ -2,21 +2,20 @@ // Licensed under the MIT License. import formatMessage from 'format-message'; -import { NotificationType } from './../components/NotificationCard'; -import { ICardProps } from './../components/NotificationCard'; -export const getQnAPending = (urls: string[]) => { +import { CardProps } from './../components/NotificationCard'; + +export const getQnaPendingNotification = (urls: string[]): CardProps => { return { title: formatMessage('Creating your knowledge base'), - description: formatMessage('Extacting QnA pairs from ') + urls.join(' '), - type: NotificationType.loading, + description: formatMessage('Extracting QNA pairs from {urls}', { urls: urls.join(' ') }), + type: 'pending', }; }; -export const getQnASuccess = (callback: () => void): ICardProps => { +export const getQnaSuccessNotification = (callback: () => void): CardProps => { return { title: formatMessage('Your knowledge base Surface go FAQ is ready!'), - description: '', - type: NotificationType.success, + type: 'success', retentionTime: 5000, link: { label: formatMessage('View KB'), @@ -25,10 +24,10 @@ export const getQnASuccess = (callback: () => void): ICardProps => { }; }; -export const getQnAFailed = (error: string) => { +export const getQnaFailedNotification = (error: string): CardProps => { return { title: formatMessage('There was error creating your KB'), description: error, - type: NotificationType.error, + type: 'error', }; }; diff --git a/Composer/packages/client/src/utils/timer.ts b/Composer/packages/client/src/utils/timer.ts new file mode 100644 index 0000000000..b0077b3344 --- /dev/null +++ b/Composer/packages/client/src/utils/timer.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export default class Timer { + timerId: NodeJS.Timeout; + start: number; + remaining: number; + pausing = false; + callback: () => void; + + constructor(callback: () => void, delay: number) { + this.remaining = delay; + this.callback = callback; + this.start = Date.now(); + this.timerId = setTimeout(callback, this.remaining); + } + + pause() { + if (!this.pausing) { + clearTimeout(this.timerId); + this.remaining -= Date.now() - this.start; + this.pausing = true; + } + } + + resume() { + this.pausing = false; + this.start = Date.now(); + clearTimeout(this.timerId); + this.timerId = setTimeout(this.callback, this.remaining); + } + + clear() { + clearTimeout(this.timerId); + } +} From 1503e39a81f242c52fccc99cc542dece43bf1205 Mon Sep 17 00:00:00 2001 From: leilzh Date: Fri, 18 Sep 2020 11:52:15 +0800 Subject: [PATCH 10/11] use atomFamily to avoid over rendering --- .../src/components/NotificationContainer.tsx | 13 +++++++------ .../client/src/recoilModel/atoms/appState.ts | 13 ++++++++++--- .../src/recoilModel/dispatchers/notification.ts | 17 +++++++---------- .../selectors/notificationsSelector.ts | 15 +++++++++++++++ 4 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts diff --git a/Composer/packages/client/src/components/NotificationContainer.tsx b/Composer/packages/client/src/components/NotificationContainer.tsx index bfa85147ba..afdc87b5a2 100644 --- a/Composer/packages/client/src/components/NotificationContainer.tsx +++ b/Composer/packages/client/src/components/NotificationContainer.tsx @@ -4,8 +4,10 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; import { useRecoilValue } from 'recoil'; +import React from 'react'; -import { notificationsState, dispatcherState } from '../recoilModel'; +import { dispatcherState } from '../recoilModel'; +import { notificationsSelector } from '../recoilModel/selectors/notificationsSelector'; import { NotificationCard } from './NotificationCard'; @@ -20,16 +22,15 @@ const container = css` // -------------------- NotificationContainer -------------------- // -export const NotificationContainer = () => { - const notifications = useRecoilValue(notificationsState); +export const NotificationContainer = React.memo(() => { + const notifications = useRecoilValue(notificationsSelector); const { deleteNotification } = useRecoilValue(dispatcherState); return (
{notifications.map((item) => { - const { id, ...rest } = item; - return ; + return ; })}
); -}; +}); diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts index 0280aae7f0..ffa97d2f58 100644 --- a/Composer/packages/client/src/recoilModel/atoms/appState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { atom } from 'recoil'; +import { atom, atomFamily } from 'recoil'; import { ProjectTemplate, UserSettings } from '@bfc/shared'; import { @@ -153,11 +153,18 @@ export const boilerplateVersionState = atom({ }, }); -export const notificationsState = atom({ - key: getFullyQualifiedKey('notification'), +export const notificationIdsState = atom({ + key: getFullyQualifiedKey('notificationIds'), default: [], }); +export const notificationsState = atomFamily({ + key: getFullyQualifiedKey('notification'), + default: (id: string): Notification => { + return { id, type: 'info', title: '' }; + }, +}); + export const extensionsState = atom({ key: getFullyQualifiedKey('extensions'), default: [], diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts index 9a3c010ae7..f30fc7a838 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -5,7 +5,7 @@ import { CallbackInterface, useRecoilCallback } from 'recoil'; import { v4 as uuid } from 'uuid'; -import { notificationsState } from '../atoms/appState'; +import { notificationsState, notificationIdsState } from '../atoms/appState'; import { CardProps } from '../../components/NotificationCard'; import { Notification } from '../../recoilModel/types'; @@ -15,17 +15,14 @@ export const createNotifiction = (notificationCard: CardProps): Notification => }; export const addNotificationInternal = ({ set }: CallbackInterface, notification: Notification) => { - set(notificationsState, (notifications) => [...notifications, notification]); + set(notificationsState(notification.id), notification); + set(notificationIdsState, (ids) => [...ids, notification.id]); }; -export const deleteNotificationInternal = ({ set }: CallbackInterface, id: string) => { - set(notificationsState, (items) => { - const notifications = [...items]; - const index = notifications.findIndex((item) => item.id === id); - if (index > -1) { - notifications.splice(index, 1); - } - return notifications; +export const deleteNotificationInternal = ({ reset, set }: CallbackInterface, id: string) => { + reset(notificationsState(id)); + set(notificationIdsState, (items) => { + return [...items].filter((item) => item !== id); }); }; diff --git a/Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts b/Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts new file mode 100644 index 0000000000..a9a902e5ee --- /dev/null +++ b/Composer/packages/client/src/recoilModel/selectors/notificationsSelector.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { selector } from 'recoil'; + +import { notificationIdsState, notificationsState } from '../atoms/appState'; + +export const notificationsSelector = selector({ + key: 'notificationsSelector', + get: ({ get }) => { + const ids = get(notificationIdsState); + const notifications = ids.map((id) => get(notificationsState(id))); + return notifications; + }, +}); From b6451cbb5197b82190fc01ea1a476c3a52b92a53 Mon Sep 17 00:00:00 2001 From: leilzh Date: Mon, 21 Sep 2020 10:10:52 +0800 Subject: [PATCH 11/11] update the set --- .../client/src/recoilModel/dispatchers/notification.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts index f30fc7a838..7579d19c77 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -21,8 +21,8 @@ export const addNotificationInternal = ({ set }: CallbackInterface, notification export const deleteNotificationInternal = ({ reset, set }: CallbackInterface, id: string) => { reset(notificationsState(id)); - set(notificationIdsState, (items) => { - return [...items].filter((item) => item !== id); + set(notificationIdsState, (notifications) => { + return notifications.filter((notification) => notification !== id); }); };