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

feat: add global notification component#996

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
kylecarbs merged 6 commits intomainfrombq/992/notifications
Apr 13, 2022
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionssite/package.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@
"@material-ui/core": "4.9.4",
"@material-ui/icons": "4.5.1",
"@material-ui/lab": "4.0.0-alpha.42",
"@testing-library/react-hooks": "8.0.0",
"@xstate/inspect": "0.6.5",
"@xstate/react": "3.0.0",
"axios": "0.26.1",
Expand Down
2 changes: 2 additions & 0 deletionssite/src/app.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import React from "react"
import { BrowserRouter as Router } from "react-router-dom"
import { SWRConfig } from "swr"
import { AppRouter } from "./AppRouter"
import { GlobalSnackbar } from "./components/Snackbar/GlobalSnackbar"
import { light } from "./theme"
import "./theme/global-fonts"
import { XServiceProvider } from "./xServices/StateContext"
Expand DownExpand Up@@ -33,6 +34,7 @@ export const App: React.FC = () => {
<ThemeProvider theme={light}>
<CssBaseline />
<AppRouter />
<GlobalSnackbar />
</ThemeProvider>
</XServiceProvider>
</SWRConfig>
Expand Down
13 changes: 13 additions & 0 deletionssite/src/components/Icons/ErrorIcon.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
import React from "react"

export const ErrorIcon = (props: SvgIconProps): JSX.Element => (
<SvgIcon {...props} viewBox="0 0 24 24">
<path
fillRule="evenodd"
clipRule="evenodd"
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"
fill="currentColor"
/>
</SvgIcon>
)
24 changes: 24 additions & 0 deletionssite/src/components/Snackbar/EnterpriseSnackbar.stories.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
import { Story } from "@storybook/react"
import React from "react"
import { EnterpriseSnackbar, EnterpriseSnackbarProps } from "./EnterpriseSnackbar"

export default {
title: "Snackbar/EnterpriseSnackbar",
component: EnterpriseSnackbar,
}

const Template: Story<EnterpriseSnackbarProps> = (args: EnterpriseSnackbarProps) => <EnterpriseSnackbar {...args} />

export const Error = Template.bind({})
Error.args = {
variant: "error",
open: true,
message: "Oops, something wrong happened.",
}

export const Info = Template.bind({})
Info.args = {
variant: "info",
open: true,
message: "Hey, something happened.",
}
101 changes: 101 additions & 0 deletionssite/src/components/Snackbar/EnterpriseSnackbar.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
importIconButtonfrom"@material-ui/core/IconButton"
importSnackbar,{SnackbarPropsasMuiSnackbarProps}from"@material-ui/core/Snackbar"
import{makeStyles}from"@material-ui/core/styles"
importCloseIconfrom"@material-ui/icons/Close"
importReactfrom"react"
import{combineClasses}from"../../util/combineClasses"

typeEnterpriseSnackbarVariant="error"|"info"

exportinterfaceEnterpriseSnackbarPropsextendsMuiSnackbarProps{
/** Called when the snackbar should close, either from timeout or clicking close */
onClose:()=>void
/** Variant of snackbar, for theming */
variant?:EnterpriseSnackbarVariant
}

/**
* Wrapper around Material UI's Snackbar component, provides pre-configured
* themes and convenience props. Coder UI's Snackbars require a close handler,
* since they always render a close button.
*
* Snackbars do _not_ automatically appear in the top-level position when
* rendered, you'll need to use ReactDom portals or the Material UI Portal
* component for that.
*
* See original component's Material UI documentation here: https://material-ui.com/components/snackbars/
*/
exportconstEnterpriseSnackbar:React.FC<EnterpriseSnackbarProps>=({
onClose,
variant="info",
ContentProps={},
action,
...rest
})=>{
conststyles=useStyles()

return(
<Snackbar
anchorOrigin={{
vertical:"bottom",
horizontal:"right",
}}
{...rest}
action={
<divclassName={styles.actionWrapper}>
{action}
<IconButtononClick={onClose}className={styles.iconButton}>
<CloseIconclassName={variant==="info" ?styles.closeIcon :styles.closeIconError}/>
</IconButton>
</div>
}
ContentProps={{
...ContentProps,
className:combineClasses({
[styles.snackbarContent]:true,
[styles.snackbarContentInfo]:variant==="info",
[styles.snackbarContentError]:variant==="error",
}),
}}
onClose={onClose}
/>
)
}

constuseStyles=makeStyles((theme)=>({
actionWrapper:{
display:"flex",
alignItems:"center",
},
iconButton:{
padding:0,
},
closeIcon:{
width:25,
height:25,
color:theme.palette.info.contrastText,
},
closeIconError:{
width:25,
height:25,
color:theme.palette.error.contrastText,
},
snackbarContent:{
borderLeft:`4px solid${theme.palette.primary.main}`,
borderRadius:0,
padding:`${theme.spacing(1)}px${theme.spacing(3)}px${theme.spacing(1)}px${theme.spacing(2)}px`,
boxShadow:theme.shadows[6],
alignItems:"inherit",
},
snackbarContentInfo:{
backgroundColor:theme.palette.info.main,
// Use primary color as a highlight
borderLeftColor:theme.palette.primary.main,
color:theme.palette.info.contrastText,
},
snackbarContentError:{
backgroundColor:theme.palette.error.dark,
borderLeftColor:theme.palette.error.main,
color:theme.palette.error.contrastText,
},
}))
109 changes: 109 additions & 0 deletionssite/src/components/Snackbar/GlobalSnackbar.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
import { makeStyles } from "@material-ui/core/styles"
import React, { useCallback, useState } from "react"
import {
AdditionalMessage,
isNotificationList,
isNotificationText,
isNotificationTextPrefixed,
MsgType,
NotificationMsg,
SnackbarEventType,
} from "."
import { useCustomEvent } from "../../hooks/events"
import { CustomEventListener } from "../../util/events"
import { ErrorIcon } from "../Icons/ErrorIcon"
import { Typography } from "../Typography/Typography"
import { EnterpriseSnackbar } from "./EnterpriseSnackbar"

export const GlobalSnackbar: React.FC = () => {
const styles = useStyles()
const [open, setOpen] = useState<boolean>(false)
const [notification, setNotification] = useState<NotificationMsg>()

const handleNotification = useCallback<CustomEventListener<NotificationMsg>>((event) => {
setNotification(event.detail)
setOpen(true)
}, [])

useCustomEvent(SnackbarEventType, handleNotification)

const renderAdditionalMessage = (msg: AdditionalMessage, idx: number) => {
if (isNotificationText(msg)) {
return (
<Typography key={idx} gutterBottom variant="body2" className={styles.messageSubtitle}>
{msg}
</Typography>
)
} else if (isNotificationTextPrefixed(msg)) {
return (
<Typography key={idx} gutterBottom variant="body2" className={styles.messageSubtitle}>
<strong>{msg.prefix}:</strong> {msg.text}
</Typography>
)
} else if (isNotificationList(msg)) {
return (
<ul className={styles.list} key={idx}>
{msg.map((item, idx) => (
<li key={idx}>
<Typography variant="body2" className={styles.messageSubtitle}>
{item}
</Typography>
</li>
))}
</ul>
)
}
return null
}

if (!notification) {
return null
}

return (
<EnterpriseSnackbar
open={open}
variant={notification.msgType === MsgType.Error ? "error" : "info"}
message={
<div className={styles.messageWrapper}>
{notification.msgType === MsgType.Error && <ErrorIcon className={styles.errorIcon} />}
<div className={styles.message}>
<Typography variant="body1" className={styles.messageTitle}>
{notification.msg}
</Typography>
{notification.additionalMsgs && notification.additionalMsgs.map(renderAdditionalMessage)}
</div>
</div>
}
onClose={() => setOpen(false)}
autoHideDuration={notification.msgType === MsgType.Error ? 22000 : 6000}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
/>
)
}

const useStyles = makeStyles((theme) => ({
list: {
paddingLeft: 0,
},
messageWrapper: {
display: "flex",
},
message: {
maxWidth: 670,
},
messageTitle: {
fontSize: 14,
fontWeight: 600,
},
messageSubtitle: {
marginTop: theme.spacing(1.5),
},
errorIcon: {
color: theme.palette.error.contrastText,
marginRight: theme.spacing(2),
},
}))
79 changes: 79 additions & 0 deletionssite/src/components/Snackbar/index.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
import { displaySuccess, isNotificationTextPrefixed, MsgType, NotificationMsg } from "./index"

describe("Snackbar", () => {
describe("isNotificationTextPrefixed", () => {
// Regression test for case found in #10436
it("does not crash on null values", () => {
// Given
const msg = null

// When
const isTextPrefixed = isNotificationTextPrefixed(msg)

// Then
expect(isTextPrefixed).toBe(false)
})
})

describe("displaySuccess", () => {
const originalWindowDispatchEvent = window.dispatchEvent
let dispatchEventMock: jest.Mock

// Helper function to extract the notification event
// that was sent to `dispatchEvent`. This lets us validate
// the contents of the notification event are what we expect.
const extractNotificationEvent = (dispatchEventMock: jest.Mock): NotificationMsg => {
// The jest mock API isn't typesafe - but we know in our usage that
// this will always be a `NotificationMsg`.

// calls[0] is the first call made to the mock (this is reset in `beforeEach`)
// calls[0][0] is the first argument of the first call
// calls[0][0].detail is the 'detail' argument passed to the `CustomEvent` -
// this is the `NotificationMsg` object that gets sent to `dispatchEvent`

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return dispatchEventMock.mock.calls[0][0].detail as NotificationMsg
}

beforeEach(() => {
dispatchEventMock = jest.fn()
window.dispatchEvent = dispatchEventMock
})

afterEach(() => {
window.dispatchEvent = originalWindowDispatchEvent
})

it("can be called with only a title", () => {
// Given
const expected: NotificationMsg = {
msgType: MsgType.Success,
msg: "Test",
additionalMsgs: undefined,
}

// When
displaySuccess("Test")

// Then
expect(dispatchEventMock).toBeCalledTimes(1)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
})

it("can be called with a title and additional message", () => {
// Given
const expected: NotificationMsg = {
msgType: MsgType.Success,
msg: "Test",
additionalMsgs: ["additional message"],
}

// When
displaySuccess("Test", "additional message")

// Then
expect(dispatchEventMock).toBeCalledTimes(1)
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
})
})
})
Loading

[8]ページ先頭

©2009-2025 Movatter.jp