(Feature) - v3 Decoded Tx - Generic Modal (#2054)
Co-authored-by: Agustín Longoni <agustin.longoni@altoros.com> Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
parent
4e96152e21
commit
15ae933a18
|
@ -50,12 +50,6 @@ const notificationStyles = {
|
|||
info: {
|
||||
background: '#fff',
|
||||
},
|
||||
receiveModal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
minHeight: '544px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}
|
||||
|
||||
const Frame = styled.div`
|
||||
|
@ -149,7 +143,7 @@ const App: React.FC = ({ children }) => {
|
|||
description="Receive Tokens Form"
|
||||
handleClose={onReceiveHide}
|
||||
open={safeActionsState.showReceive}
|
||||
paperClassName={classes.receiveModal}
|
||||
paperClassName="receive-modal"
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<ReceiveModal onClose={onReceiveHide} safeAddress={safeAddress} safeName={safeName} />
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, useState } from 'react'
|
||||
|
||||
import TextField from 'src/components/forms/TextField'
|
||||
import GnoField from 'src/components/forms/Field'
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import { required } from 'src/components/forms/validator'
|
||||
|
||||
import { Modal } from '.'
|
||||
|
||||
export default {
|
||||
title: 'Modal',
|
||||
component: Modal,
|
||||
parameters: {
|
||||
children: 'The body of the modal or the whole modal being composed by `Modal.Header` and `Modal.Footer` components',
|
||||
title: 'The title, useful for screen readers',
|
||||
description: 'A description, useful for screen readers',
|
||||
handleClose:
|
||||
'A callback which will be called when an action to close the modal is triggered (Esc, clicking outside, etc)',
|
||||
open: 'If `true`, the modal will be displayed. Hidden otherwise.',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Header',
|
||||
component: <Modal.Header />,
|
||||
parameters: {
|
||||
title: 'The title that will be displayed in the modal',
|
||||
titleNote: 'An annotation for the title, like "1 of 2"',
|
||||
onClose: 'Callback to be called when attempt to close the modal',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Header.Title',
|
||||
component: <Modal.Header.Title size="xs">{}</Modal.Header.Title>,
|
||||
description: 'safe-react-component exposed with a few styles added to personalize the modal header',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Modal.Header',
|
||||
component: <Modal.Body>{}</Modal.Body>,
|
||||
parameters: {
|
||||
children: 'whatever is required to be rendered in the footer. Usually buttons.',
|
||||
noPadding: 'a flag that will set padding to 0 (zero) in case it is needed',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Modal.Footer',
|
||||
component: <Modal.Footer>{}</Modal.Footer>,
|
||||
parameters: {
|
||||
children: 'whatever is required to be rendered in the footer. Usually buttons.',
|
||||
},
|
||||
compositionElements: [
|
||||
{
|
||||
title: 'Modal.Footer.Buttons',
|
||||
component: <Modal.Footer.Buttons />,
|
||||
description: 'standard two buttons wrapped implementation. One "Cancel" and one "Submit" button.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const SimpleFormModal = ({ title, description, handleClose, handleSubmit, isOpen, children }) => (
|
||||
<Modal title={title} description={description} handleClose={handleClose} open={isOpen}>
|
||||
{/* header */}
|
||||
<Modal.Header onClose={handleClose}>
|
||||
<Modal.Header.Title>{title}</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<GnoForm onSubmit={handleSubmit}>
|
||||
{() => (
|
||||
<>
|
||||
{/* body */}
|
||||
<Modal.Body>{children}</Modal.Body>
|
||||
|
||||
{/* footer */}
|
||||
<Modal.Footer>
|
||||
<Modal.Footer.Buttons cancelButtonProps={{ text: 'Close', onClick: handleClose }} />
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)}
|
||||
</GnoForm>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
const Username = () => (
|
||||
<label htmlFor="username">
|
||||
<Text size="lg" strong>
|
||||
Username
|
||||
</Text>
|
||||
<GnoField
|
||||
autoComplete="off"
|
||||
component={TextField}
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="your username"
|
||||
validate={required}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
|
||||
export const FormModal = (): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
console.log('modal closed')
|
||||
}
|
||||
|
||||
const handleSubmit = (values) => {
|
||||
alert(JSON.stringify(values, null, 2))
|
||||
console.log('form submitted', values)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
{/* Modal with Form */}
|
||||
<SimpleFormModal
|
||||
title="My first modal"
|
||||
description="My first modal description"
|
||||
handleClose={handleClose}
|
||||
handleSubmit={handleSubmit}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{/* Form Fields */}
|
||||
<Username />
|
||||
</SimpleFormModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const RemoveSomething = (): ReactElement => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const title = 'Remove Something'
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false)
|
||||
console.log('modal closed')
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
alert('Something was removed')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
||||
{/* Modal */}
|
||||
<Modal handleClose={handleClose} title={title} description={title} open={isOpen}>
|
||||
{/* Header */}
|
||||
<Modal.Header onClose={handleClose}>
|
||||
<Modal.Header.Title>{title}</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
{/* Body */}
|
||||
<Modal.Body>
|
||||
<Text size="md">You are about to remove something</Text>
|
||||
</Modal.Body>
|
||||
|
||||
{/* Footer */}
|
||||
<Modal.Footer>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: handleClose }}
|
||||
confirmButtonProps={{ onClick: handleSubmit, color: 'error', text: 'Remove' }}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,69 +1,267 @@
|
|||
import Modal from '@material-ui/core/Modal'
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles'
|
||||
import { Button, Icon, theme, Title as TitleSRC } from '@gnosis.pm/safe-react-components'
|
||||
import { ButtonProps as ButtonPropsMUI, Modal as ModalMUI } from '@material-ui/core'
|
||||
import cn from 'classnames'
|
||||
import React, { ReactElement, ReactNode } from 'react'
|
||||
import React, { ReactElement, ReactNode, ReactNodeArray } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { sm } from 'src/theme/variables'
|
||||
type Theme = typeof theme
|
||||
|
||||
const useStyles = makeStyles(
|
||||
createStyles({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
paper: {
|
||||
position: 'relative',
|
||||
top: '68px',
|
||||
width: '500px',
|
||||
borderRadius: sm,
|
||||
backgroundColor: '#ffffff',
|
||||
boxShadow: '0 0 5px 0 rgba(74, 85, 121, 0.5)',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
}),
|
||||
)
|
||||
const ModalStyled = styled(ModalMUI)`
|
||||
& {
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background-color: rgba(232, 231, 230, 0.75) !important;
|
||||
}
|
||||
|
||||
.paper {
|
||||
position: relative;
|
||||
top: 68px;
|
||||
width: 500px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 1px 2px 10px 0 rgba(40, 54, 61, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// TODO: replace class-based styles by params
|
||||
&.receive-modal {
|
||||
height: auto;
|
||||
max-width: calc(100% - 130px);
|
||||
min-height: 544px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.bigger-modal-window {
|
||||
width: 775px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.smaller-modal-window {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.modal {
|
||||
height: auto;
|
||||
max-width: calc(100% - 130px);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface GnoModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
// type copied from Material-UI Modal's `close` prop
|
||||
handleClose?: {
|
||||
bivarianceHack(event: Record<string, unknown>, reason: 'backdropClick' | 'escapeKeyDown'): void
|
||||
}['bivarianceHack']
|
||||
modalClassName?: string
|
||||
handleClose?: (event: Record<string, unknown>, reason: 'backdropClick' | 'escapeKeyDown') => void
|
||||
open: boolean
|
||||
paperClassName?: string
|
||||
title: string
|
||||
}
|
||||
|
||||
const GnoModal = ({
|
||||
children,
|
||||
description,
|
||||
handleClose,
|
||||
modalClassName,
|
||||
open,
|
||||
paperClassName,
|
||||
title,
|
||||
}: GnoModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
const GnoModal = ({ children, description, handleClose, open, paperClassName, title }: GnoModalProps): ReactElement => {
|
||||
return (
|
||||
<Modal
|
||||
<ModalStyled
|
||||
BackdropProps={{ className: 'overlay' }}
|
||||
aria-describedby={description}
|
||||
aria-labelledby={title}
|
||||
className={cn(classes.root, modalClassName)}
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
>
|
||||
<div className={cn(classes.paper, paperClassName, 'classpep')}>{children}</div>
|
||||
</Modal>
|
||||
<div className={cn('paper', paperClassName)}>{children}</div>
|
||||
</ModalStyled>
|
||||
)
|
||||
}
|
||||
|
||||
export default GnoModal
|
||||
|
||||
/*****************/
|
||||
/* Generic Modal */
|
||||
/*****************/
|
||||
|
||||
/*** Header ***/
|
||||
const HeaderSection = styled.div`
|
||||
display: flex;
|
||||
padding: 24px 18px 24px 24px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
|
||||
h5 {
|
||||
color: ${({ theme }) => theme.colors.text};
|
||||
}
|
||||
|
||||
.close-button {
|
||||
align-self: flex-end;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
|
||||
span {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.colors.separator};
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TitleStyled = styled(TitleSRC)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-basis: 100%;
|
||||
|
||||
.image,
|
||||
img {
|
||||
width: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.note,
|
||||
span {
|
||||
margin-left: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
interface TitleProps {
|
||||
children: string | ReactNode
|
||||
size?: keyof Theme['title']['size']
|
||||
withoutMargin?: boolean
|
||||
strong?: boolean
|
||||
}
|
||||
|
||||
const Title = ({ children, ...props }: TitleProps): ReactElement => (
|
||||
<TitleStyled size="xs" withoutMargin {...props}>
|
||||
{children}
|
||||
</TitleStyled>
|
||||
)
|
||||
|
||||
interface HeaderProps {
|
||||
children?: ReactNode
|
||||
onClose?: (event: any) => void
|
||||
}
|
||||
|
||||
const Header = ({ children, onClose }: HeaderProps): ReactElement => {
|
||||
return (
|
||||
<HeaderSection className="modal-header">
|
||||
{children}
|
||||
|
||||
{onClose && (
|
||||
<button className="close-button" onClick={onClose}>
|
||||
<Icon size="sm" type="cross" />
|
||||
</button>
|
||||
)}
|
||||
</HeaderSection>
|
||||
)
|
||||
}
|
||||
|
||||
Header.Title = Title
|
||||
|
||||
/*** Body ***/
|
||||
const BodySection = styled.div<{ withoutPadding: BodyProps['withoutPadding'] }>`
|
||||
padding: ${({ withoutPadding }) => (withoutPadding ? 0 : '24px')};
|
||||
`
|
||||
|
||||
interface BodyProps {
|
||||
children: ReactNode | ReactNodeArray
|
||||
withoutPadding?: boolean
|
||||
}
|
||||
|
||||
const Body = ({ children, withoutPadding = false }: BodyProps): ReactElement => (
|
||||
<BodySection className="modal-body" withoutPadding={withoutPadding}>
|
||||
{children}
|
||||
</BodySection>
|
||||
)
|
||||
|
||||
/*** Footer ***/
|
||||
const FooterSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const ButtonStyled = styled(Button)`
|
||||
&.MuiButtonBase-root {
|
||||
margin: 0 10px;
|
||||
}
|
||||
`
|
||||
|
||||
type CustomButtonMUIProps = Omit<ButtonPropsMUI, 'size' | 'color' | 'variant'> & {
|
||||
to?: string
|
||||
component?: ReactNode
|
||||
}
|
||||
|
||||
interface ButtonProps extends CustomButtonMUIProps {
|
||||
text?: string
|
||||
size?: keyof Theme['buttons']['size']
|
||||
color?: 'primary' | 'secondary' | 'error'
|
||||
variant?: 'bordered' | 'contained' | 'outlined'
|
||||
}
|
||||
|
||||
interface ButtonsProps {
|
||||
cancelButtonProps?: ButtonProps
|
||||
confirmButtonProps?: ButtonProps
|
||||
}
|
||||
|
||||
const Buttons = ({ cancelButtonProps = {}, confirmButtonProps = {} }: ButtonsProps): ReactElement => {
|
||||
const { text: cancelText = 'Cancel' } = cancelButtonProps
|
||||
const { text: confirmText = 'Submit' } = confirmButtonProps
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonStyled
|
||||
size="md"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
type={cancelButtonProps?.onClick ? 'button' : 'submit'}
|
||||
{...cancelButtonProps}
|
||||
>
|
||||
{cancelText}
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="md" type={confirmButtonProps?.onClick ? 'button' : 'submit'} {...confirmButtonProps}>
|
||||
{confirmText}
|
||||
</ButtonStyled>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface FooterProps {
|
||||
children: ReactNode | ReactNodeArray
|
||||
}
|
||||
|
||||
const Footer = ({ children }: FooterProps): ReactElement => (
|
||||
<FooterSection className="modal-footer">{children}</FooterSection>
|
||||
)
|
||||
|
||||
Footer.Buttons = Buttons
|
||||
|
||||
interface ModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
handleClose: () => void
|
||||
open: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
export const Modal = ({ children, ...props }: ModalProps): ReactElement => {
|
||||
return (
|
||||
<GnoModal {...props} paperClassName="modal">
|
||||
{children}
|
||||
</GnoModal>
|
||||
)
|
||||
}
|
||||
|
||||
Modal.Header = Header
|
||||
Modal.Body = Body
|
||||
Modal.Footer = Footer
|
||||
|
|
|
@ -67,7 +67,7 @@ export const CreateEditEntryModal = ({
|
|||
description={isNew ? 'Create new addressBook entry' : 'Edit addressBook entry'}
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title={isNew ? 'Create new entry' : 'Edit entry'}
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
|
|
@ -23,8 +23,5 @@ export const useStyles = makeStyles(
|
|||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -25,7 +25,7 @@ const DeleteEntryModalComponent = ({ classes, deleteEntryModalHandler, entryToDe
|
|||
description="Delete entry"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Delete entry"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
|
|
@ -27,7 +27,4 @@ export const styles = () => ({
|
|||
buttonCancel: {
|
||||
color: '#008c73',
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import React, { Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import Modal from 'src/components/Modal'
|
||||
|
@ -32,12 +31,6 @@ const SendCustomTx = React.lazy(() => import('./screens/ContractInteraction/Send
|
|||
const ReviewCustomTx = React.lazy(() => import('./screens/ContractInteraction/ReviewCustomTx'))
|
||||
|
||||
const useStyles = makeStyles({
|
||||
scalableModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
scalableStaticModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
loaderStyle: {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
|
@ -86,8 +79,6 @@ const SendModal = ({
|
|||
setTx({})
|
||||
}, [activeScreenType, isOpen])
|
||||
|
||||
const scalableModalSize = activeScreen === 'chooseTxType'
|
||||
|
||||
const handleTxCreation = (txInfo: SendCollectibleTxInfo) => {
|
||||
setActiveScreen('sendFundsReviewTx')
|
||||
setTx(txInfo)
|
||||
|
@ -117,7 +108,7 @@ const SendModal = ({
|
|||
description="Send Tokens Form"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={cn(scalableModalSize ? classes.scalableStaticModalWindow : classes.scalableModalWindow)}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Send Tokens"
|
||||
>
|
||||
<Suspense
|
||||
|
|
|
@ -92,7 +92,7 @@ const Balances = (): React.ReactElement => {
|
|||
}))
|
||||
}
|
||||
|
||||
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, receiveModal, tokenControls } = classes
|
||||
const { assetDivider, assetTab, assetTabActive, assetTabs, controls, tokenControls } = classes
|
||||
const { erc721Enabled, sendFunds, showReceive } = state
|
||||
|
||||
return (
|
||||
|
@ -174,7 +174,7 @@ const Balances = (): React.ReactElement => {
|
|||
description="Receive Tokens Form"
|
||||
handleClose={() => onHide('Receive')}
|
||||
open={showReceive}
|
||||
paperClassName={receiveModal}
|
||||
paperClassName="receive-modal"
|
||||
title="Receive Tokens"
|
||||
>
|
||||
<ReceiveModal safeAddress={address} safeName={safeName} onClose={() => onHide('Receive')} />
|
||||
|
|
|
@ -58,12 +58,6 @@ export const styles = createStyles({
|
|||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
receiveModal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
minHeight: '544px',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
send: {
|
||||
width: '75px',
|
||||
minWidth: '75px',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// TODO: remove this file. It's no longer used
|
||||
import { screenSm, sm } from 'src/theme/variables'
|
||||
import { createStyles } from '@material-ui/core'
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@ export const RemoveModuleModal = ({ onClose, selectedModulePair }: RemoveModuleM
|
|||
<Modal
|
||||
description="Remove the selected Module"
|
||||
handleClose={onClose}
|
||||
paperClassName={classes.modal}
|
||||
paperClassName="modal"
|
||||
title="Remove Module"
|
||||
open
|
||||
>
|
||||
|
|
|
@ -101,11 +101,6 @@ export const styles = createStyles({
|
|||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
gasCostsContainer: {
|
||||
backgroundColor: background,
|
||||
padding: `0 ${lg}`,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
|
@ -18,15 +17,6 @@ import { OwnerForm } from './screens/OwnerForm'
|
|||
import { ReviewAddOwner } from './screens/Review'
|
||||
import { ThresholdForm } from './screens/ThresholdForm'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
export type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
|
@ -66,7 +56,6 @@ type Props = {
|
|||
}
|
||||
|
||||
export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('selectOwner')
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerName: '', ownerAddress: '', threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
|
@ -123,7 +112,7 @@ export const AddOwnerModal = ({ isOpen, onClose }: Props): React.ReactElement =>
|
|||
description="Add owner to Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Add owner to Safe"
|
||||
>
|
||||
<>
|
||||
|
|
|
@ -60,7 +60,7 @@ export const EditOwnerModal = ({ isOpen, onClose, ownerAddress, selectedOwnerNam
|
|||
description="Edit owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.smallerModalWindow}
|
||||
paperClassName="smaller-modal-window"
|
||||
title="Edit owner from Safe"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
|
|
@ -32,7 +32,4 @@ export const styles = createStyles({
|
|||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
smallerModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
|
@ -15,15 +14,6 @@ import { safeParamAddressFromStateSelector, safeThresholdSelector } from 'src/lo
|
|||
import { Dispatch } from 'src/logic/safe/store/actions/types.d'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
ownerAddress: string
|
||||
ownerName: string
|
||||
|
@ -78,7 +68,6 @@ export const RemoveOwnerModal = ({
|
|||
ownerAddress,
|
||||
ownerName,
|
||||
}: RemoveOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState<OwnerValues>({ ownerAddress, ownerName, threshold: '' })
|
||||
const dispatch = useDispatch()
|
||||
|
@ -120,7 +109,7 @@ export const RemoveOwnerModal = ({
|
|||
description="Remove owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Remove owner from Safe"
|
||||
>
|
||||
<>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
|
@ -18,15 +17,6 @@ import { OwnerForm } from 'src/routes/safe/components/Settings/ManageOwners/Repl
|
|||
import { ReviewReplaceOwnerModal } from 'src/routes/safe/components/Settings/ManageOwners/ReplaceOwnerModal/screens/Review'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
||||
const styles = createStyles({
|
||||
biggerModalWindow: {
|
||||
width: '775px',
|
||||
height: 'auto',
|
||||
},
|
||||
})
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
type OwnerValues = {
|
||||
newOwnerAddress: string
|
||||
newOwnerName: string
|
||||
|
@ -84,7 +74,6 @@ export const ReplaceOwnerModal = ({
|
|||
ownerAddress,
|
||||
ownerName,
|
||||
}: ReplaceOwnerProps): React.ReactElement => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState('checkOwner')
|
||||
const [values, setValues] = useState({
|
||||
newOwnerAddress: '',
|
||||
|
@ -137,7 +126,7 @@ export const ReplaceOwnerModal = ({
|
|||
description="Replace owner from Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.biggerModalWindow}
|
||||
paperClassName="bigger-modal-window"
|
||||
title="Replace owner from Safe"
|
||||
>
|
||||
<>
|
||||
|
|
|
@ -68,7 +68,7 @@ export const RemoveSafeModal = ({ isOpen, onClose }: RemoveSafeModalProps): Reac
|
|||
description="Remove the selected Safe"
|
||||
handleClose={onClose}
|
||||
open={isOpen}
|
||||
paperClassName={classes.modal}
|
||||
paperClassName="modal"
|
||||
title="Remove Safe"
|
||||
>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
|
|
|
@ -52,9 +52,4 @@ export const styles = createStyles({
|
|||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
import { Icon, Text, Title } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, ReactNode, ReactNodeArray } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GnoModal from 'src/components/Modal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
|
||||
const TitleSection = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
`
|
||||
|
||||
const StyledButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
|
||||
span {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
:hover {
|
||||
background: ${({ theme }) => theme.colors.separator};
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const FooterSection = styled.div`
|
||||
border-top: 2px solid ${({ theme }) => theme.colors.separator};
|
||||
padding: 16px 24px;
|
||||
`
|
||||
|
||||
const FooterWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`
|
||||
|
||||
export interface TopBarProps {
|
||||
title: string
|
||||
titleNote?: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const TopBar = ({ title, titleNote, onClose }: TopBarProps): ReactElement => (
|
||||
<TitleSection>
|
||||
<Title size="xs" withoutMargin>
|
||||
{title}
|
||||
{titleNote && (
|
||||
<>
|
||||
{' '}
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
{titleNote}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Title>
|
||||
|
||||
<StyledButton onClick={onClose}>
|
||||
<Icon size="sm" type="cross" />
|
||||
</StyledButton>
|
||||
</TitleSection>
|
||||
)
|
||||
|
||||
interface FooterProps {
|
||||
children: ReactNodeArray
|
||||
}
|
||||
|
||||
const Footer = ({ children }: FooterProps): ReactElement => (
|
||||
<FooterSection>
|
||||
<FooterWrapper>{children}</FooterWrapper>
|
||||
</FooterSection>
|
||||
)
|
||||
|
||||
export interface ModalProps {
|
||||
children: ReactNode
|
||||
description: string
|
||||
handleClose: () => void
|
||||
open: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
// TODO: this is a potential proposal for `safe-react-components` Modal
|
||||
// By being able to combine components for better flexibility, this way Buttons can be part of the form body
|
||||
const Modal = ({ children, ...props }: ModalProps): ReactElement => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<GnoModal {...props} paperClassName={classes.modal}>
|
||||
{children}
|
||||
</GnoModal>
|
||||
)
|
||||
}
|
||||
|
||||
Modal.TopBar = TopBar
|
||||
Modal.Footer = Footer
|
||||
|
||||
export default Modal
|
|
@ -1,12 +1,11 @@
|
|||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import { FormState, Mutator } from 'final-form'
|
||||
import React, { ReactElement } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GnoForm from 'src/components/forms/GnoForm'
|
||||
import GnoButton from 'src/components/layout/Button'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { Amount, Beneficiary, ResetTime, Token } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
|
||||
const FormContainer = styled.div`
|
||||
padding: 24px 8px 24px 24px;
|
||||
|
@ -24,14 +23,6 @@ const FormContainer = styled.div`
|
|||
'resetTimeOption resetTimeOption';
|
||||
`
|
||||
|
||||
const YetAnotherButton = styled(GnoButton)`
|
||||
&.Mui-disabled {
|
||||
background-color: ${({ theme }) => theme.colors.primary};
|
||||
color: ${({ theme }) => theme.colors.white};
|
||||
opacity: 0.5;
|
||||
}
|
||||
`
|
||||
|
||||
const formMutators: Record<string, Mutator<{ beneficiary: { name: string } }>> = {
|
||||
setBeneficiary: (args, state, utils) => {
|
||||
utils.changeValue(state, 'beneficiary', () => args[0])
|
||||
|
@ -55,7 +46,16 @@ const canReview = ({
|
|||
const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="1 of 2" onClose={onCancel} />
|
||||
<Modal.Header onClose={onCancel}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
<>
|
||||
New Spending Limit
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
1 of 2
|
||||
</Text>
|
||||
</>
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<GnoForm formMutators={formMutators} onSubmit={onReview} initialValues={initialValues}>
|
||||
{(...args) => {
|
||||
|
@ -69,21 +69,10 @@ const Create = ({ initialValues, onCancel, onReview }: NewSpendingLimitProps): R
|
|||
</FormContainer>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button color="primary" size="md" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{/* TODO: replace this with safe-react-components button. */}
|
||||
{/* This is used as "submit" SRC Button does not triggers submission up until the 2nd click */}
|
||||
<YetAnotherButton
|
||||
color="primary"
|
||||
size="medium"
|
||||
variant="contained"
|
||||
type="submit"
|
||||
disabled={!canReview(args[2])}
|
||||
>
|
||||
Review
|
||||
</YetAnotherButton>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: onCancel }}
|
||||
confirmButtonProps={{ disabled: !canReview(args[2]), text: 'Review' }}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Button, Text } from '@gnosis.pm/safe-react-components'
|
||||
import { Text } from '@gnosis.pm/safe-react-components'
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { createTransaction, CreateTransactionArgs } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { SafeRecordProps, SpendingLimit } from 'src/logic/safe/store/models/safe'
|
||||
import {
|
||||
|
@ -22,7 +22,6 @@ import { fromTokenUnit, toTokenUnit } from 'src/logic/tokens/utils/humanReadable
|
|||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { RESET_TIME_OPTIONS } from 'src/routes/safe/components/Settings/SpendingLimit/FormFields/ResetTime'
|
||||
import { AddressInfo, ResetTimeInfo, TokenInfo } from 'src/routes/safe/components/Settings/SpendingLimit/InfoDisplay'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
import { useStyles } from 'src/routes/safe/components/Settings/SpendingLimit/style'
|
||||
import { safeParamAddressFromStateSelector, safeSpendingLimitsSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
|
@ -242,9 +241,16 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
|||
>
|
||||
{(txParameters, toggleEditMode) => (
|
||||
<>
|
||||
<Modal.TopBar title="New Spending Limit" titleNote="2 of 2" onClose={onClose} />
|
||||
<Modal.Header onClose={onClose}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
New Spending Limit
|
||||
<Text size="lg" color="secondaryLight" as="span">
|
||||
2 of 2
|
||||
</Text>
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Modal.Body>
|
||||
<Col margin="lg">
|
||||
<AddressInfo address={values.beneficiary} title="Beneficiary" />
|
||||
</Col>
|
||||
|
@ -286,7 +292,7 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
|||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
</Block>
|
||||
</Modal.Body>
|
||||
<div className={classes.gasCostsContainer}>
|
||||
<TransactionFees
|
||||
gasCostFormatted={gasCostFormatted}
|
||||
|
@ -298,23 +304,17 @@ export const ReviewSpendingLimits = ({ onBack, onClose, txToken, values }: Revie
|
|||
</div>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
onClick={() => onBack({ values: {}, txToken: makeToken(), step: CREATE })}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => handleSubmit(txParameters)}
|
||||
disabled={existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{
|
||||
onClick: () => onBack({ values: {}, txToken: makeToken(), step: CREATE }),
|
||||
text: 'Back',
|
||||
}}
|
||||
confirmButtonProps={{
|
||||
onClick: () => handleSubmit(txParameters),
|
||||
disabled:
|
||||
existentSpendingLimit === undefined || txEstimationExecutionStatus === EstimationStatus.LOADING,
|
||||
}}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -2,10 +2,10 @@ import { List } from 'immutable'
|
|||
import React, { ReactElement, Reducer, useCallback, useReducer } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { makeToken, Token } from 'src/logic/tokens/store/model/token'
|
||||
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||
import { extendedSafeTokensSelector } from 'src/routes/safe/container/selector'
|
||||
import Modal from 'src/routes/safe/components/Settings/SpendingLimit/Modal'
|
||||
|
||||
import Create from './Create'
|
||||
import { ReviewSpendingLimits } from './Review'
|
||||
|
|
|
@ -1,29 +1,27 @@
|
|||
import { Button } from '@gnosis.pm/safe-react-components'
|
||||
import cn from 'classnames'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import Block from 'src/components/layout/Block'
|
||||
import Col from 'src/components/layout/Col'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { Modal } from 'src/components/Modal'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import useTokenInfo from 'src/logic/safe/hooks/useTokenInfo'
|
||||
import { createTransaction } from 'src/logic/safe/store/actions/createTransaction'
|
||||
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from 'src/logic/safe/transactions'
|
||||
import { getDeleteAllowanceTxData } from 'src/logic/safe/utils/spendingLimits'
|
||||
import { fromTokenUnit } from 'src/logic/tokens/utils/humanReadableValue'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { SPENDING_LIMIT_MODULE_ADDRESS } from 'src/utils/constants'
|
||||
|
||||
import { RESET_TIME_OPTIONS } from './FormFields/ResetTime'
|
||||
import { AddressInfo, ResetTimeInfo, TokenInfo } from './InfoDisplay'
|
||||
import { SpendingLimitTable } from './LimitsTable/dataFetcher'
|
||||
import Modal from './Modal'
|
||||
import { useStyles } from './style'
|
||||
import { EstimationStatus, useEstimateTransactionGas } from 'src/logic/hooks/useEstimateTransactionGas'
|
||||
import { EditableTxParameters } from 'src/routes/safe/components/Transactions/helpers/EditableTxParameters'
|
||||
import { TxParameters } from 'src/routes/safe/container/hooks/useTransactionParameters'
|
||||
import { TxParametersDetail } from 'src/routes/safe/components/Transactions/helpers/TxParametersDetail'
|
||||
import Row from 'src/components/layout/Row'
|
||||
import { TransactionFees } from 'src/components/TransactionsFees'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface RemoveSpendingLimitModalProps {
|
||||
onClose: () => void
|
||||
|
@ -126,9 +124,13 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
|||
{(txParameters, toggleEditMode) => {
|
||||
return (
|
||||
<>
|
||||
<Modal.TopBar title="Remove Spending Limit" onClose={onClose} />
|
||||
<Modal.Header onClose={onClose}>
|
||||
<Modal.Header.Title size="xs" withoutMargin>
|
||||
Remove Spending Limit
|
||||
</Modal.Header.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Block className={classes.container}>
|
||||
<Modal.Body>
|
||||
<Col margin="lg">
|
||||
<AddressInfo title="Beneficiary" address={spendingLimit.beneficiary} />
|
||||
</Col>
|
||||
|
@ -152,7 +154,7 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
|||
isTransactionExecution={isExecution}
|
||||
isOffChainSignature={isOffChainSignature}
|
||||
/>
|
||||
</Block>
|
||||
</Modal.Body>
|
||||
|
||||
<Row className={cn(classes.modalDescription, classes.gasCostsContainer)}>
|
||||
<TransactionFees
|
||||
|
@ -165,18 +167,15 @@ export const RemoveLimitModal = ({ onClose, spendingLimit, open }: RemoveSpendin
|
|||
</Row>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button size="md" color="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="error"
|
||||
size="md"
|
||||
variant="contained"
|
||||
onClick={() => removeSelectedSpendingLimit(txParameters)}
|
||||
disabled={txEstimationExecutionStatus === EstimationStatus.LOADING}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
<Modal.Footer.Buttons
|
||||
cancelButtonProps={{ onClick: onClose }}
|
||||
confirmButtonProps={{
|
||||
color: 'error',
|
||||
onClick: () => removeSelectedSpendingLimit(txParameters),
|
||||
disabled: txEstimationExecutionStatus === EstimationStatus.LOADING,
|
||||
text: 'Remove',
|
||||
}}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -120,10 +120,6 @@ export const useStyles = makeStyles(
|
|||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
height: 'auto',
|
||||
maxWidth: 'calc(100% - 30px)',
|
||||
},
|
||||
amountInput: {
|
||||
width: '100% !important',
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue