Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit300c6d0

Browse files
feat: add global notification component (#996)
* feat: add global notification component* fix: update yarn.lock* fix: pin @testing-library/react-hooks* fix: update yarn.lock* refactor: remove displayError
1 parent5ecc823 commit300c6d0

File tree

13 files changed

+515
-0
lines changed

13 files changed

+515
-0
lines changed

‎site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@material-ui/core":"4.9.4",
3030
"@material-ui/icons":"4.5.1",
3131
"@material-ui/lab":"4.0.0-alpha.42",
32+
"@testing-library/react-hooks":"8.0.0",
3233
"@xstate/inspect":"0.6.5",
3334
"@xstate/react":"3.0.0",
3435
"axios":"0.26.1",

‎site/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from "react"
44
import{BrowserRouterasRouter}from"react-router-dom"
55
import{SWRConfig}from"swr"
66
import{AppRouter}from"./AppRouter"
7+
import{GlobalSnackbar}from"./components/Snackbar/GlobalSnackbar"
78
import{light}from"./theme"
89
import"./theme/global-fonts"
910
import{XServiceProvider}from"./xServices/StateContext"
@@ -33,6 +34,7 @@ export const App: React.FC = () => {
3334
<ThemeProvidertheme={light}>
3435
<CssBaseline/>
3536
<AppRouter/>
37+
<GlobalSnackbar/>
3638
</ThemeProvider>
3739
</XServiceProvider>
3840
</SWRConfig>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
importSvgIcon,{SvgIconProps}from"@material-ui/core/SvgIcon"
2+
importReactfrom"react"
3+
4+
exportconstErrorIcon=(props:SvgIconProps):JSX.Element=>(
5+
<SvgIcon{...props}viewBox="0 0 24 24">
6+
<path
7+
fillRule="evenodd"
8+
clipRule="evenodd"
9+
d="M7.59354 2.26627C7.76403 2.09578 7.99526 2 8.23637 2H15.7636C16.0047 2 16.236 2.09578 16.4065 2.26627L21.7337 7.59354C21.9042 7.76403 22 7.99526 22 8.23636V15.7636C22 16.0047 21.9042 16.236 21.7337 16.4065L16.4065 21.7337C16.236 21.9042 16.0047 22 15.7636 22H8.23637C7.99526 22 7.76403 21.9042 7.59354 21.7337L2.26627 16.4065C2.09578 16.236 2 16.0047 2 15.7636V8.23636C2 7.99526 2.09578 7.76403 2.26627 7.59354L7.59354 2.26627ZM8.61293 3.81818L3.81819 8.61292V15.3871L8.61293 20.1818H15.3871L20.1818 15.3871V8.61292L15.3871 3.81818H8.61293ZM12 7.45455C12.5021 7.45455 12.9091 7.86156 12.9091 8.36364V12C12.9091 12.5021 12.5021 12.9091 12 12.9091C11.4979 12.9091 11.0909 12.5021 11.0909 12V8.36364C11.0909 7.86156 11.4979 7.45455 12 7.45455ZM12 14.7273C11.4979 14.7273 11.0909 15.1343 11.0909 15.6364C11.0909 16.1384 11.4979 16.5455 12 16.5455H12.0091C12.5112 16.5455 12.9182 16.1384 12.9182 15.6364C12.9182 15.1343 12.5112 14.7273 12.0091 14.7273H12Z"
10+
fill="currentColor"
11+
/>
12+
</SvgIcon>
13+
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import{Story}from"@storybook/react"
2+
importReactfrom"react"
3+
import{EnterpriseSnackbar,EnterpriseSnackbarProps}from"./EnterpriseSnackbar"
4+
5+
exportdefault{
6+
title:"Snackbar/EnterpriseSnackbar",
7+
component:EnterpriseSnackbar,
8+
}
9+
10+
constTemplate:Story<EnterpriseSnackbarProps>=(args:EnterpriseSnackbarProps)=><EnterpriseSnackbar{...args}/>
11+
12+
exportconstError=Template.bind({})
13+
Error.args={
14+
variant:"error",
15+
open:true,
16+
message:"Oops, something wrong happened.",
17+
}
18+
19+
exportconstInfo=Template.bind({})
20+
Info.args={
21+
variant:"info",
22+
open:true,
23+
message:"Hey, something happened.",
24+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
importIconButtonfrom"@material-ui/core/IconButton"
2+
importSnackbar,{SnackbarPropsasMuiSnackbarProps}from"@material-ui/core/Snackbar"
3+
import{makeStyles}from"@material-ui/core/styles"
4+
importCloseIconfrom"@material-ui/icons/Close"
5+
importReactfrom"react"
6+
import{combineClasses}from"../../util/combineClasses"
7+
8+
typeEnterpriseSnackbarVariant="error"|"info"
9+
10+
exportinterfaceEnterpriseSnackbarPropsextendsMuiSnackbarProps{
11+
/** Called when the snackbar should close, either from timeout or clicking close */
12+
onClose:()=>void
13+
/** Variant of snackbar, for theming */
14+
variant?:EnterpriseSnackbarVariant
15+
}
16+
17+
/**
18+
* Wrapper around Material UI's Snackbar component, provides pre-configured
19+
* themes and convenience props. Coder UI's Snackbars require a close handler,
20+
* since they always render a close button.
21+
*
22+
* Snackbars do _not_ automatically appear in the top-level position when
23+
* rendered, you'll need to use ReactDom portals or the Material UI Portal
24+
* component for that.
25+
*
26+
* See original component's Material UI documentation here: https://material-ui.com/components/snackbars/
27+
*/
28+
exportconstEnterpriseSnackbar:React.FC<EnterpriseSnackbarProps>=({
29+
onClose,
30+
variant="info",
31+
ContentProps={},
32+
action,
33+
...rest
34+
})=>{
35+
conststyles=useStyles()
36+
37+
return(
38+
<Snackbar
39+
anchorOrigin={{
40+
vertical:"bottom",
41+
horizontal:"right",
42+
}}
43+
{...rest}
44+
action={
45+
<divclassName={styles.actionWrapper}>
46+
{action}
47+
<IconButtononClick={onClose}className={styles.iconButton}>
48+
<CloseIconclassName={variant==="info" ?styles.closeIcon :styles.closeIconError}/>
49+
</IconButton>
50+
</div>
51+
}
52+
ContentProps={{
53+
...ContentProps,
54+
className:combineClasses({
55+
[styles.snackbarContent]:true,
56+
[styles.snackbarContentInfo]:variant==="info",
57+
[styles.snackbarContentError]:variant==="error",
58+
}),
59+
}}
60+
onClose={onClose}
61+
/>
62+
)
63+
}
64+
65+
constuseStyles=makeStyles((theme)=>({
66+
actionWrapper:{
67+
display:"flex",
68+
alignItems:"center",
69+
},
70+
iconButton:{
71+
padding:0,
72+
},
73+
closeIcon:{
74+
width:25,
75+
height:25,
76+
color:theme.palette.info.contrastText,
77+
},
78+
closeIconError:{
79+
width:25,
80+
height:25,
81+
color:theme.palette.error.contrastText,
82+
},
83+
snackbarContent:{
84+
borderLeft:`4px solid${theme.palette.primary.main}`,
85+
borderRadius:0,
86+
padding:`${theme.spacing(1)}px${theme.spacing(3)}px${theme.spacing(1)}px${theme.spacing(2)}px`,
87+
boxShadow:theme.shadows[6],
88+
alignItems:"inherit",
89+
},
90+
snackbarContentInfo:{
91+
backgroundColor:theme.palette.info.main,
92+
// Use primary color as a highlight
93+
borderLeftColor:theme.palette.primary.main,
94+
color:theme.palette.info.contrastText,
95+
},
96+
snackbarContentError:{
97+
backgroundColor:theme.palette.error.dark,
98+
borderLeftColor:theme.palette.error.main,
99+
color:theme.palette.error.contrastText,
100+
},
101+
}))
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import{makeStyles}from"@material-ui/core/styles"
2+
importReact,{useCallback,useState}from"react"
3+
import{
4+
AdditionalMessage,
5+
isNotificationList,
6+
isNotificationText,
7+
isNotificationTextPrefixed,
8+
MsgType,
9+
NotificationMsg,
10+
SnackbarEventType,
11+
}from"."
12+
import{useCustomEvent}from"../../hooks/events"
13+
import{CustomEventListener}from"../../util/events"
14+
import{ErrorIcon}from"../Icons/ErrorIcon"
15+
import{Typography}from"../Typography/Typography"
16+
import{EnterpriseSnackbar}from"./EnterpriseSnackbar"
17+
18+
exportconstGlobalSnackbar:React.FC=()=>{
19+
conststyles=useStyles()
20+
const[open,setOpen]=useState<boolean>(false)
21+
const[notification,setNotification]=useState<NotificationMsg>()
22+
23+
consthandleNotification=useCallback<CustomEventListener<NotificationMsg>>((event)=>{
24+
setNotification(event.detail)
25+
setOpen(true)
26+
},[])
27+
28+
useCustomEvent(SnackbarEventType,handleNotification)
29+
30+
constrenderAdditionalMessage=(msg:AdditionalMessage,idx:number)=>{
31+
if(isNotificationText(msg)){
32+
return(
33+
<Typographykey={idx}gutterBottomvariant="body2"className={styles.messageSubtitle}>
34+
{msg}
35+
</Typography>
36+
)
37+
}elseif(isNotificationTextPrefixed(msg)){
38+
return(
39+
<Typographykey={idx}gutterBottomvariant="body2"className={styles.messageSubtitle}>
40+
<strong>{msg.prefix}:</strong>{msg.text}
41+
</Typography>
42+
)
43+
}elseif(isNotificationList(msg)){
44+
return(
45+
<ulclassName={styles.list}key={idx}>
46+
{msg.map((item,idx)=>(
47+
<likey={idx}>
48+
<Typographyvariant="body2"className={styles.messageSubtitle}>
49+
{item}
50+
</Typography>
51+
</li>
52+
))}
53+
</ul>
54+
)
55+
}
56+
returnnull
57+
}
58+
59+
if(!notification){
60+
returnnull
61+
}
62+
63+
return(
64+
<EnterpriseSnackbar
65+
open={open}
66+
variant={notification.msgType===MsgType.Error ?"error" :"info"}
67+
message={
68+
<divclassName={styles.messageWrapper}>
69+
{notification.msgType===MsgType.Error&&<ErrorIconclassName={styles.errorIcon}/>}
70+
<divclassName={styles.message}>
71+
<Typographyvariant="body1"className={styles.messageTitle}>
72+
{notification.msg}
73+
</Typography>
74+
{notification.additionalMsgs&&notification.additionalMsgs.map(renderAdditionalMessage)}
75+
</div>
76+
</div>
77+
}
78+
onClose={()=>setOpen(false)}
79+
autoHideDuration={notification.msgType===MsgType.Error ?22000 :6000}
80+
anchorOrigin={{
81+
vertical:"bottom",
82+
horizontal:"right",
83+
}}
84+
/>
85+
)
86+
}
87+
88+
constuseStyles=makeStyles((theme)=>({
89+
list:{
90+
paddingLeft:0,
91+
},
92+
messageWrapper:{
93+
display:"flex",
94+
},
95+
message:{
96+
maxWidth:670,
97+
},
98+
messageTitle:{
99+
fontSize:14,
100+
fontWeight:600,
101+
},
102+
messageSubtitle:{
103+
marginTop:theme.spacing(1.5),
104+
},
105+
errorIcon:{
106+
color:theme.palette.error.contrastText,
107+
marginRight:theme.spacing(2),
108+
},
109+
}))
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import{displaySuccess,isNotificationTextPrefixed,MsgType,NotificationMsg}from"./index"
2+
3+
describe("Snackbar",()=>{
4+
describe("isNotificationTextPrefixed",()=>{
5+
// Regression test for case found in #10436
6+
it("does not crash on null values",()=>{
7+
// Given
8+
constmsg=null
9+
10+
// When
11+
constisTextPrefixed=isNotificationTextPrefixed(msg)
12+
13+
// Then
14+
expect(isTextPrefixed).toBe(false)
15+
})
16+
})
17+
18+
describe("displaySuccess",()=>{
19+
constoriginalWindowDispatchEvent=window.dispatchEvent
20+
letdispatchEventMock:jest.Mock
21+
22+
// Helper function to extract the notification event
23+
// that was sent to `dispatchEvent`. This lets us validate
24+
// the contents of the notification event are what we expect.
25+
constextractNotificationEvent=(dispatchEventMock:jest.Mock):NotificationMsg=>{
26+
// The jest mock API isn't typesafe - but we know in our usage that
27+
// this will always be a `NotificationMsg`.
28+
29+
// calls[0] is the first call made to the mock (this is reset in `beforeEach`)
30+
// calls[0][0] is the first argument of the first call
31+
// calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` -
32+
// this is the `NotificationMsg` object that gets sent to `dispatchEvent`
33+
34+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
35+
returndispatchEventMock.mock.calls[0][0].detailasNotificationMsg
36+
}
37+
38+
beforeEach(()=>{
39+
dispatchEventMock=jest.fn()
40+
window.dispatchEvent=dispatchEventMock
41+
})
42+
43+
afterEach(()=>{
44+
window.dispatchEvent=originalWindowDispatchEvent
45+
})
46+
47+
it("can be called with only a title",()=>{
48+
// Given
49+
constexpected:NotificationMsg={
50+
msgType:MsgType.Success,
51+
msg:"Test",
52+
additionalMsgs:undefined,
53+
}
54+
55+
// When
56+
displaySuccess("Test")
57+
58+
// Then
59+
expect(dispatchEventMock).toBeCalledTimes(1)
60+
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
61+
})
62+
63+
it("can be called with a title and additional message",()=>{
64+
// Given
65+
constexpected:NotificationMsg={
66+
msgType:MsgType.Success,
67+
msg:"Test",
68+
additionalMsgs:["additional message"],
69+
}
70+
71+
// When
72+
displaySuccess("Test","additional message")
73+
74+
// Then
75+
expect(dispatchEventMock).toBeCalledTimes(1)
76+
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
77+
})
78+
})
79+
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp