Skip to content

Commit 1321271

Browse files
committed
BA-2205 copy messages to clipboard
1 parent 7c11c03 commit 1321271

File tree

16 files changed

+390
-12
lines changed

16 files changed

+390
-12
lines changed

packages/components/.storybook/main.ts

+5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ const config: StorybookConfig = {
1414
resolve(__dirname, './*.mdx'),
1515
resolve(__dirname, '../modules/**/__storybook__/*.mdx'),
1616
resolve(__dirname, '../../design-system/components/**/__storybook__/*.mdx'),
17+
resolve(__dirname, '../../design-system/providers/**/__storybook__/*.mdx'),
1718
resolve(__dirname, '../modules/**/__storybook__/stories.@(js|jsx|mjs|ts|tsx)'),
1819
resolve(
1920
__dirname,
2021
'../../design-system/components/**/__storybook__/stories.@(js|jsx|mjs|ts|tsx)',
2122
),
23+
resolve(
24+
__dirname,
25+
'../../design-system/providers/**/__storybook__/stories.@(js|jsx|mjs|ts|tsx)',
26+
),
2227
],
2328
framework: {
2429
name: getAbsolutePath('@storybook/react-webpack5'),

packages/components/modules/messages/MessagesList/MessagesGroup/UserMessage/MessageItem/index.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FC, useRef, useState } from 'react'
22

33
import { useCurrentProfile } from '@baseapp-frontend/authentication'
44
import { CopyIcon, DownloadIcon, PenEditIcon } from '@baseapp-frontend/design-system'
5+
import { useNotification } from '@baseapp-frontend/utils'
56

67
import { Typography } from '@mui/material'
78
import { useFragment } from 'react-relay'
@@ -18,6 +19,7 @@ const MessageItem: FC<MessageItemProps> = ({ messageRef, isFirstGroupedMessage }
1819
const message = useFragment(MessageItemFragment, messageRef)
1920
const isOwnMessage = currentProfile?.id === message?.profile?.id
2021
const messageCardRef = useRef<HTMLDivElement>(null)
22+
const { sendSnack } = useNotification()
2123

2224
const [isEditMode, setIsEditMode] = useState(false)
2325

@@ -50,7 +52,10 @@ const MessageItem: FC<MessageItemProps> = ({ messageRef, isFirstGroupedMessage }
5052
disabled: false,
5153
icon: <CopyIcon />,
5254
label: 'Copy',
53-
onClick: () => {}, // TODO: Implement copy message
55+
onClick: () => {
56+
navigator.clipboard.writeText(message?.content || '')
57+
sendSnack('Copied to clipboard', { type: 'info' })
58+
},
5459
hasPermission: true,
5560
},
5661
{

packages/design-system/.storybook/main.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ const config: StorybookConfig = {
1313
stories: [
1414
'./*.mdx',
1515
'../components/**/__storybook__/*.mdx',
16+
'../providers/**/__storybook__/*.mdx',
1617
'../components/**/__storybook__/stories.@(js|jsx|mjs|ts|tsx)',
18+
'../providers/**/__storybook__/stories.@(js|jsx|mjs|ts|tsx)',
1719
],
1820
framework: {
1921
name: getAbsolutePath('@storybook/react-webpack5'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { FC } from 'react'
2+
3+
import { SvgIcon, SvgIconProps } from '@mui/material'
4+
5+
const ErrorIcon: FC<SvgIconProps> = ({ sx, ...props }) => (
6+
<SvgIcon sx={{ fontSize: 20, color: 'text.primary', ...sx }} {...props}>
7+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
8+
<g id="Group">
9+
<g id="Group_2">
10+
<path
11+
id="Path"
12+
d="M12 12V7.5"
13+
stroke="currentColor"
14+
strokeWidth="1.5"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
id="Path_2"
20+
d="M11.999 15.5C11.861 15.5 11.749 15.612 11.75 15.75C11.75 15.888 11.862 16 12 16C12.138 16 12.25 15.888 12.25 15.75C12.25 15.612 12.138 15.5 11.999 15.5"
21+
stroke="currentColor"
22+
strokeWidth="1.5"
23+
strokeLinecap="round"
24+
strokeLinejoin="round"
25+
/>
26+
<path
27+
id="Path_3"
28+
fillRule="evenodd"
29+
clipRule="evenodd"
30+
d="M3 14.3815V9.61948C3 8.01748 3.852 6.53648 5.236 5.73048L9.736 3.11148C11.135 2.29748 12.864 2.29748 14.263 3.11148L18.763 5.73048C20.148 6.53648 21 8.01748 21 9.61948V14.3815C21 15.9835 20.148 17.4645 18.764 18.2705L14.264 20.8895C12.865 21.7035 11.136 21.7035 9.737 20.8895L5.237 18.2705C3.852 17.4645 3 15.9835 3 14.3815Z"
31+
stroke="currentColor"
32+
strokeWidth="1.5"
33+
strokeLinecap="round"
34+
strokeLinejoin="round"
35+
/>
36+
</g>
37+
</g>
38+
</svg>
39+
</SvgIcon>
40+
)
41+
42+
export default ErrorIcon

packages/design-system/components/icons/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export { default as CloseIcon } from './CloseIcon'
99
export { default as CommentReplyIcon } from './CommentReplyIcon'
1010
export { default as CopyIcon } from './CopyIcon'
1111
export { default as DownloadIcon } from './DownloadIcon'
12+
export { default as ErrorIcon } from './ErrorIcon'
1213
export { default as FavoriteIcon } from './FavoriteIcon'
1314
export { default as FavoriteSelectedIcon } from './FavoriteSelectedIcon'
1415
export { default as LinkIcon } from './LinkIcon'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FC } from 'react'
2+
3+
import { ProgressBarContainer, ProgressContainer } from './styled'
4+
import { ProgressAnimationProps } from './types'
5+
6+
const ProgressAnimation: FC<ProgressAnimationProps> = ({ animationTime, severity }) => (
7+
<ProgressContainer severity={severity}>
8+
<ProgressBarContainer {...{ severity, animationTime }} />
9+
</ProgressContainer>
10+
)
11+
12+
export default ProgressAnimation
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Box, alpha } from '@mui/material'
2+
import { styled } from '@mui/material/styles'
3+
4+
import { AlertContainerProps, ProgressBarContainerProps } from './types'
5+
6+
export const ProgressContainer = styled(Box)<AlertContainerProps>(({ theme, severity }) => ({
7+
display: 'flex',
8+
flexDirection: 'column',
9+
alignItems: 'flex-start',
10+
width: '100%',
11+
height: '4px',
12+
backgroundColor:
13+
severity === 'info'
14+
? alpha(theme.palette.common.black, 0.24)
15+
: alpha(theme.palette[severity].main, 0.24),
16+
}))
17+
18+
export const ProgressBarContainer = styled(Box)<ProgressBarContainerProps>(
19+
({ theme, animationTime, severity }) => ({
20+
borderRadius: '50px',
21+
animation: `increase-width ${animationTime}ms linear forwards`,
22+
'@keyframes increase-width': {
23+
from: {
24+
width: 0,
25+
},
26+
to: {
27+
width: '100%',
28+
},
29+
},
30+
height: '100%',
31+
backgroundColor:
32+
severity === 'info' ? theme.palette.common.black : theme.palette[severity].main,
33+
}),
34+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { AlertColor, BoxProps } from '@mui/material'
2+
3+
type SeverityProp = { severity: AlertColor }
4+
type AnimationTimeProp = { animationTime: number }
5+
6+
export type ProgressAnimationProps = SeverityProp & AnimationTimeProp
7+
8+
export type AlertContainerProps = SeverityProp & BoxProps
9+
export type ProgressBarContainerProps = ProgressAnimationProps & BoxProps
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Meta } from '@storybook/addon-docs'
2+
3+
<Meta title="@baseapp-frontend | designSystem/SnackbarProvider/SnackbarProvider" />
4+
5+
# Component Documentation
6+
7+
## SnackbarProvider
8+
9+
- **Purpose**: A customized wrapper around MUI's snackbar displaying messages from the notification store. It provides consistent styling for icons and a uniform color scheme.
10+
- **Expected Behavior**: Renders messages set with 'useNotification'. The messages disappear automatically after a fixed amount of time, but can also be dismissed before by clicking on a close icon. The component can be used with or without a bar indicating the remaining time before the message is dismissed. If used without a bar, the time until automatic dismissal is restarted after the user interacts with the message, if used with bar the message is always dismissed after the timeout, no matter whether the user interacted with it or not.
11+
12+
## Props
13+
14+
- **children** (ReactNode): The content wrapped by the SnackbarProvider. Any components in this wrapped content can make use of 'useNotification' to display messages
15+
- **...other**: All other props are passed to the snackbar
16+
17+
## Example Usage
18+
19+
```javascript
20+
import { SnackbarProvider } from '@baseapp-frontend/design-system'
21+
import { useNotification } from '@baseapp-frontend/utils'
22+
23+
const MessageEmitter = () => {
24+
const { sendToast, sendSnack } = useNotification()
25+
26+
return (
27+
<>
28+
<Button onClick={() => sendToast("Button 1 was clicked", {type: "info"})}>
29+
Click this button to display a message without progress bar.
30+
</Button>
31+
<Button onClick={() => sendSnack("Button 2 was clicked", {type: "info"})}>
32+
Click this button to display a message with progress bar.
33+
</Button>
34+
< />
35+
)
36+
}
37+
38+
const MyComponent = () => {
39+
return (
40+
<SnackbarProvider>
41+
<MessageEmitter />
42+
</SnackbarProvider>
43+
)
44+
}
45+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { FC } from 'react'
2+
3+
import { useNotification } from '@baseapp-frontend/utils'
4+
import { NotificationState } from '@baseapp-frontend/utils/dist/hooks/useNotification/types'
5+
6+
import { Button } from '@mui/material'
7+
import { Meta, StoryObj } from '@storybook/react'
8+
9+
import SnackbarProvider from '..'
10+
11+
interface SnackbarProps {
12+
message: string
13+
shouldShowProgress: boolean
14+
type: NotificationState['type']
15+
}
16+
17+
const SnackbarWrapper: FC<SnackbarProps> = ({ message, shouldShowProgress, type }) => {
18+
const { sendToast, sendSnack } = useNotification()
19+
const send = shouldShowProgress ? sendSnack : sendToast
20+
return (
21+
<SnackbarProvider>
22+
<Button onClick={() => send(message, { type })}>Post message</Button>
23+
</SnackbarProvider>
24+
)
25+
}
26+
27+
const meta: Meta<SnackbarProps> = {
28+
title: '@baseapp-frontend | designSystem/SnackbarProvider/SnackbarProvider',
29+
component: SnackbarWrapper,
30+
argTypes: {
31+
type: {
32+
control: 'select',
33+
options: ['info', 'success', 'warning', 'error'],
34+
},
35+
},
36+
}
37+
38+
export default meta
39+
40+
type Story = StoryObj<SnackbarProps>
41+
42+
export const InfoWithProgress: Story = {
43+
render: (args) => <SnackbarWrapper {...args} />,
44+
args: {
45+
message: 'This is an info message with progress bar',
46+
shouldShowProgress: true,
47+
type: 'info',
48+
},
49+
}
50+
51+
export const SuccessWithProgress: Story = {
52+
render: (args) => <SnackbarWrapper {...args} />,
53+
args: {
54+
message: 'This is a success message with progress bar',
55+
shouldShowProgress: true,
56+
type: 'success',
57+
},
58+
}
59+
60+
export const WarningWithProgress: Story = {
61+
render: (args) => <SnackbarWrapper {...args} />,
62+
args: {
63+
message: 'This is a warning message with progress bar',
64+
shouldShowProgress: true,
65+
type: 'warning',
66+
},
67+
}
68+
69+
export const ErrorWithProgress: Story = {
70+
render: (args) => <SnackbarWrapper {...args} />,
71+
args: {
72+
message: 'This is an error message with progress bar',
73+
shouldShowProgress: true,
74+
type: 'error',
75+
},
76+
}
77+
78+
export const LongInfoWithProgress: Story = {
79+
render: (args) => <SnackbarWrapper {...args} />,
80+
args: {
81+
message:
82+
'Mr. Jones, of the Manor Farm, had locked the hen-houses for the night, but was too drunk to remember to shut the popholes. With the ring of light from his lantern dancing from side to side, he lurched across the yard, kicked off his boots at the back door, drew himself a last glass of beer from the barrel in the scullery, and made his way up to bed, where Mrs. Jones was already snoring. From Geoge Orwell, Animal Farm, Chapter 1',
83+
shouldShowProgress: true,
84+
type: 'info',
85+
},
86+
}
87+
88+
export const InfoWithoutProgress: Story = {
89+
render: (args) => <SnackbarWrapper {...args} />,
90+
args: {
91+
message: 'This is an info message without progress bar',
92+
shouldShowProgress: false,
93+
type: 'info',
94+
},
95+
}
96+
97+
export const SuccessWithoutProgress: Story = {
98+
render: (args) => <SnackbarWrapper {...args} />,
99+
args: {
100+
message: 'This is a success message without progress bar',
101+
shouldShowProgress: false,
102+
type: 'success',
103+
},
104+
}
105+
106+
export const WarningWithoutProgress: Story = {
107+
render: (args) => <SnackbarWrapper {...args} />,
108+
args: {
109+
message: 'This is a warning message without progress bar',
110+
shouldShowProgress: false,
111+
type: 'warning',
112+
},
113+
}
114+
115+
export const ErrorWithoutProgress: Story = {
116+
render: (args) => <SnackbarWrapper {...args} />,
117+
args: {
118+
message: 'This is an error message without progress bar',
119+
shouldShowProgress: false,
120+
type: 'error',
121+
},
122+
}
123+
124+
export const LongInfoWithoutProgress: Story = {
125+
render: (args) => <SnackbarWrapper {...args} />,
126+
args: {
127+
message:
128+
'Mr. Jones, of the Manor Farm, had locked the hen-houses for the night, but was too drunk to remember to shut the popholes. With the ring of light from his lantern dancing from side to side, he lurched across the yard, kicked off his boots at the back door, drew himself a last glass of beer from the barrel in the scullery, and made his way up to bed, where Mrs. Jones was already snoring. From Geoge Orwell, Animal Farm, Chapter 1',
129+
shouldShowProgress: false,
130+
type: 'info',
131+
},
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Iconify from '../../components/Iconify'
2+
import ErrorIcon from '../../components/icons/ErrorIcon'
3+
4+
export const HIDE_DURATION = 3000
5+
export const OUTLINED_ALERT_ICONS = {
6+
error: <ErrorIcon sx={{ color: 'error.main', width: '24px', height: '24px' }} />,
7+
info: <Iconify icon="eva:info-outline" width={24} sx={{ color: 'common.black' }} />,
8+
success: <Iconify icon="eva:checkmark-circle-2-outline" width={24} />,
9+
warning: <Iconify icon="eva:alert-triangle-outline" width={24} />,
10+
}

0 commit comments

Comments
 (0)