This repository was archived by the owner on Jul 9, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 375
feat: add notification center for composer (QnA url import) #4080
Merged
Chris Whitten (cwhitten)
merged 25 commits into
microsoft:main
from
lei9444:notification
Sep 25, 2020
Merged
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
ca78825
feat: add notification center for composer(qna import)
lei9444 b2c4453
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
lei9444 dc54276
update some style
lei9444 de3bc8f
update the style
lei9444 597a541
Merge branch 'main' into notification
lei9444 7a3f4eb
update the icon color
lei9444 ee5f30c
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
lei9444 9d28e88
fix conflict
lei9444 2ca8e8d
Merge branch 'main' into notification
lei9444 603cffd
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
lei9444 ff8cfeb
remove the timer when error
lei9444 4dc4301
Merge branch 'main' into notification
boydc2014 2035974
Merge branch 'main' into notification
boydc2014 5f5e06a
update the notification create flow
lei9444 41f22a1
remove the return in dispatcher
lei9444 da0af55
Merge branch 'main' into notification
lei9444 7d14373
Merge branch 'main' into notification
cwhitten c964ad3
use typs instead of interface
lei9444 1ed60e0
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
lei9444 1503e39
use atomFamily to avoid over rendering
lei9444 1afc95e
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
lei9444 b6451cb
update the set
lei9444 52632c8
Merge branch 'main' into notification
cwhitten 2c26f7e
Merge branch 'main' into notification
cwhitten f777c90
Merge branch 'main' into notification
cwhitten File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
259 changes: 259 additions & 0 deletions
259
Composer/packages/client/src/components/NotificationCard.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,259 @@ | ||
| // 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 errorType = css` | ||
| margin-top: 4px; | ||
| color: #a80000; | ||
| `; | ||
|
|
||
| const successType = css` | ||
| margin-top: 4px; | ||
| color: #27ae60; | ||
| `; | ||
|
|
||
| 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; | ||
| margin-right: 8px; | ||
| `; | ||
|
|
||
| const getShimmerStyles = { | ||
| root: { | ||
| marginTop: '12px', | ||
| marginBottom: '8px', | ||
| }, | ||
| shimmerWrapper: [ | ||
| { | ||
| backgroundColor: '#EDEBE9', | ||
| }, | ||
| ], | ||
| shimmerGradient: [ | ||
| { | ||
| backgroundImage: 'radial-gradient(at 50% 50%, #0078D4 0%, #EDEBE9 100%);', | ||
| }, | ||
| ], | ||
| }; | ||
| // -------------------- NotificationCard -------------------- // | ||
|
|
||
| export enum NotificationType { | ||
| info = 1, | ||
| warning, | ||
| error, | ||
| loading, | ||
| success, | ||
| } | ||
|
|
||
| export interface ILink { | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| label: string; | ||
| onClick: () => void; | ||
| } | ||
|
|
||
| export interface ICardProps { | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| title: string; | ||
| description: string; | ||
| type: NotificationType; | ||
| retentionTime?: number; | ||
| link?: ILink; | ||
| onRenderCardContent?: (props: ICardProps) => JSX.Element; | ||
| } | ||
|
|
||
| export interface INotificationProps { | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| id: string; | ||
| cardProps: ICardProps; | ||
| onDismiss: (id: string) => void; | ||
| } | ||
|
|
||
| export class Timer { | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| 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 ( | ||
| <div css={cardContent}> | ||
| {type === NotificationType.error && <Icon css={errorType} iconName="ErrorBadge" />} | ||
| {type === NotificationType.success && <Icon css={successType} iconName="Completed" />} | ||
| <div css={cardDetail}> | ||
| <div css={cardTitle}>{title}</div> | ||
| <div css={cardDescription}>{description}</div> | ||
| {link && ( | ||
| <ActionButton css={linkButton} onClick={link.onClick}> | ||
| {link.label} | ||
| </ActionButton> | ||
| )} | ||
| {type === NotificationType.loading && ( | ||
| <Shimmer shimmerElements={[{ type: ShimmerElementType.line, height: 2 }]} styles={getShimmerStyles} /> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const NotificationCard = (props: INotificationProps) => { | ||
| const { cardProps, id, onDismiss } = props; | ||
| const [show, setShow] = useState(true); | ||
| const containerRef = useRef(null); | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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 mouse over stop the time and record the remaining time | ||
| 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; | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
|
|
||
| return ( | ||
| <div | ||
| ref={containerRef} | ||
| css={cardContainer(show, containerRef.current)} | ||
| role="presentation" | ||
| onAnimationEnd={handleAnimationEnd} | ||
| onFocus={() => {}} | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| onMouseLeave={handleMouseLeave} | ||
| onMouseOver={handleMouseOver} | ||
| > | ||
| <IconButton | ||
| ariaLabel={formatMessage('Close')} | ||
| css={cancelButton} | ||
| iconProps={{ iconName: 'Cancel', styles: { root: { fontSize: '12px' } } }} | ||
| onClick={removeNotification} | ||
| /> | ||
| {renderCard(cardProps)} | ||
| </div> | ||
| ); | ||
| }; | ||
34 changes: 34 additions & 0 deletions
34
Composer/packages/client/src/components/NotificationContainer.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div css={container} role="presentation"> | ||
| {notifications.map((item) => { | ||
| return <NotificationCard key={item.id} {...item} onDismiss={deleteNotification} />; | ||
| })} | ||
| </div> | ||
| ); | ||
| }; |
69 changes: 69 additions & 0 deletions
69
Composer/packages/client/src/components/NotificationIcon.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| export const NotificationIcon = (props: INotificationIconProps) => { | ||
| const { number } = props; | ||
| return ( | ||
| <div css={container} role="presentation"> | ||
| <Icon css={ringer} iconName="Ringer" /> | ||
| {number > 0 && <Icon css={circleMask} iconName="FullCircleMask" />} | ||
| {number > 0 && ( | ||
| <div css={numberContainer}> | ||
| <span css={numberText}>{number > 99 ? '...' : number}</span> | ||
|
lei9444 marked this conversation as resolved.
Outdated
|
||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.