mirror of
https://github.com/strapi/strapi.git
synced 2025-12-25 22:23:10 +00:00
Merge pull request #10249 from strapi/core/notification-api
Notfications API
This commit is contained in:
commit
856e59e99e
@ -1,23 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import configureStore from './core/store/configureStore';
|
||||
import reducers from './reducers';
|
||||
import basename from './utils/basename';
|
||||
import LanguageProvider from './containers/LanguageProvider';
|
||||
// TODO remove
|
||||
import App from './containers/App';
|
||||
import LanguageProvider from './containers/LanguageProvider';
|
||||
import Fonts from './components/Fonts';
|
||||
import GlobalStyle from './components/GlobalStyle';
|
||||
import Notifications from './components/Notifications';
|
||||
import themes from './themes';
|
||||
|
||||
import reducers from './reducers';
|
||||
|
||||
// TODO
|
||||
import translationMessages from './translations';
|
||||
|
||||
// const App = () => 'todo';
|
||||
|
||||
window.strapi = {
|
||||
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL,
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
class StrapiApp {
|
||||
plugins = {};
|
||||
|
||||
@ -39,14 +50,22 @@ class StrapiApp {
|
||||
const store = configureStore(this);
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Fonts />
|
||||
<LanguageProvider messages={translationMessages}>
|
||||
<BrowserRouter basename={basename}>
|
||||
<App store={store} />
|
||||
</BrowserRouter>
|
||||
</LanguageProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider theme={themes}>
|
||||
<GlobalStyle />
|
||||
<Fonts />
|
||||
<Provider store={store}>
|
||||
<LanguageProvider messages={translationMessages}>
|
||||
<>
|
||||
<Notifications />
|
||||
<BrowserRouter basename={basename}>
|
||||
<App store={store} />
|
||||
</BrowserRouter>
|
||||
</>
|
||||
</LanguageProvider>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import 'sanitize.css/sanitize.css';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import '@fortawesome/fontawesome-free/js/all.min.js';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
||||
@ -1,168 +0,0 @@
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { themePropTypes } from '@strapi/helper-plugin';
|
||||
|
||||
const GlobalNotification = createGlobalStyle`
|
||||
.notificationIcon {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
> div {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 10px;
|
||||
left: 5px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid ${props => props.theme.main.colors.green};
|
||||
display: flex;
|
||||
svg {
|
||||
margin: auto;
|
||||
color: ${props => props.theme.main.colors.green};
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 220px;
|
||||
margin: 0;
|
||||
padding-right: 10px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.notificationTitle {
|
||||
margin-bottom: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.8rem;
|
||||
}
|
||||
|
||||
.notificationClose {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 20px;
|
||||
margin-right: 15px;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
font-size: 1.4rem;
|
||||
color: #BBC2BF;
|
||||
transition: opacity 0.1s ease;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin: auto;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 100!important;
|
||||
}
|
||||
}
|
||||
|
||||
.notificationSuccess{
|
||||
.notificationIcon {
|
||||
div {
|
||||
border-color: ${props => props.theme.main.colors.green};
|
||||
}
|
||||
svg {
|
||||
color: ${props => props.theme.main.colors.green};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationWarning {
|
||||
.notificationIcon {
|
||||
div {
|
||||
border-color: ${props => props.theme.main.colors.orange};
|
||||
}
|
||||
svg {
|
||||
color: ${props => props.theme.main.colors.orange};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationError {
|
||||
.notificationIcon {
|
||||
div {
|
||||
border-color: ${props => props.theme.main.colors.red};
|
||||
}
|
||||
svg {
|
||||
color: ${props => props.theme.main.colors.red};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationInfo {
|
||||
.notificationIcon {
|
||||
div {
|
||||
border-color: ${props => props.theme.main.colors.blue};
|
||||
}
|
||||
svg {
|
||||
color: ${props => props.theme.main.colors.blue};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Li = styled.li`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 300px;
|
||||
min-height: 60px;
|
||||
margin-bottom: 14px;
|
||||
background: ${props => props.theme.main.colors.white};
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.15);
|
||||
color: #333740;
|
||||
transition: all 0.15s ease;
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
padding: 1rem;
|
||||
border-left: 2px solid ${props => props.theme.main.colors.green};
|
||||
&.notificationError {
|
||||
border-color: ${props => props.theme.main.colors.red};
|
||||
}
|
||||
&.notificationWarning {
|
||||
border-color: ${props => props.theme.main.colors.orange};
|
||||
}
|
||||
&.notificationInfo {
|
||||
border-color: ${props => props.theme.main.colors.blue};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
Li.defaultProps = {
|
||||
theme: {
|
||||
main: {
|
||||
colors: {
|
||||
leftMenu: {},
|
||||
},
|
||||
sizes: {
|
||||
header: {},
|
||||
leftMenu: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Li.propTypes = {
|
||||
...themePropTypes,
|
||||
};
|
||||
|
||||
export default Li;
|
||||
|
||||
export { GlobalNotification };
|
||||
@ -1,104 +1,178 @@
|
||||
/**
|
||||
*
|
||||
* Notification
|
||||
*
|
||||
*/
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { isObject } from 'lodash';
|
||||
import { Padded, Text, Flex } from '@buffetjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Remove } from '@buffetjs/icons';
|
||||
import Li, { GlobalNotification } from './Li';
|
||||
import { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper } from './styledComponents';
|
||||
|
||||
class Notification extends React.Component {
|
||||
// eslint-disable-line react/prefer-stateless-function
|
||||
handleCloseClicked = () => {
|
||||
this.props.onHideNotification(this.props.notification.id);
|
||||
};
|
||||
const types = {
|
||||
success: {
|
||||
icon: 'check',
|
||||
color: 'green',
|
||||
},
|
||||
warning: {
|
||||
icon: 'exclamation',
|
||||
color: 'orange',
|
||||
},
|
||||
info: {
|
||||
icon: 'info',
|
||||
color: 'blue',
|
||||
},
|
||||
};
|
||||
|
||||
options = {
|
||||
success: {
|
||||
icon: 'check',
|
||||
title: 'Success',
|
||||
class: 'notificationSuccess',
|
||||
},
|
||||
warning: {
|
||||
icon: 'exclamation',
|
||||
title: 'Warning',
|
||||
class: 'notificationWarning',
|
||||
},
|
||||
error: {
|
||||
icon: 'exclamation',
|
||||
title: 'Error',
|
||||
class: 'notificationError',
|
||||
},
|
||||
info: {
|
||||
icon: 'info',
|
||||
title: 'Info',
|
||||
class: 'notificationInfo',
|
||||
},
|
||||
};
|
||||
const Notification = ({ dispatch, notification }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
render() {
|
||||
const options = this.options[this.props.notification.status] || this.options.info;
|
||||
const {
|
||||
notification: { message },
|
||||
} = this.props;
|
||||
const content =
|
||||
isObject(message) && message.id ? (
|
||||
<FormattedMessage id={message.id} defaultMessage={message.id} values={message.values} />
|
||||
) : (
|
||||
<FormattedMessage id={message} defaultMessage={message} />
|
||||
);
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
link,
|
||||
type,
|
||||
id,
|
||||
onClose,
|
||||
timeout,
|
||||
blockTransition,
|
||||
centered,
|
||||
} = notification;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalNotification />
|
||||
<Li
|
||||
key={this.props.notification.id}
|
||||
className={`${options.class}`}
|
||||
onClick={this.handleCloseClicked}
|
||||
>
|
||||
<div className={`notificationIcon`}>
|
||||
<div>
|
||||
<FontAwesomeIcon icon={options.icon} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="notificationContent">
|
||||
<p className="notificationTitle">{content}</p>
|
||||
</div>
|
||||
<div className={`notificationClose`}>
|
||||
<Remove onClick={this.handleCloseClicked} />
|
||||
</div>
|
||||
</Li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
const formattedMessage = msg => (typeof msg === 'string' ? msg : formatMessage(msg, msg.values));
|
||||
const handleClose = useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'HIDE_NOTIFICATION',
|
||||
id,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutToClear;
|
||||
|
||||
if (!blockTransition) {
|
||||
timeoutToClear = setTimeout(() => {
|
||||
handleClose();
|
||||
}, timeout || 2500);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutToClear);
|
||||
}, [blockTransition, handleClose, timeout]);
|
||||
|
||||
return (
|
||||
<NotificationWrapper centered={centered} color={types[type].color}>
|
||||
<Padded top left right bottom size="smd">
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<IconWrapper>
|
||||
<FontAwesomeIcon icon={types[type].icon} />
|
||||
</IconWrapper>
|
||||
<Padded left size="sm" style={{ width: '80%', flex: 1 }}>
|
||||
{title && (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
textTransform="uppercase"
|
||||
color="grey"
|
||||
title={formattedMessage(title)}
|
||||
>
|
||||
{formattedMessage(title)}
|
||||
</Text>
|
||||
)}
|
||||
<Flex justifyContent="space-between">
|
||||
{message && (
|
||||
<Text title={formattedMessage(message)}>{formattedMessage(message)}</Text>
|
||||
)}
|
||||
{link && (
|
||||
<a
|
||||
href={link.url}
|
||||
target={link.target || '_blank'}
|
||||
rel={!link.target || link.target === '_blank' ? 'noopener noreferrer' : ''}
|
||||
>
|
||||
<Padded right left size="xs">
|
||||
<Flex alignItems="center">
|
||||
<Text
|
||||
style={{ maxWidth: '100px' }}
|
||||
ellipsis
|
||||
fontWeight="bold"
|
||||
color="blue"
|
||||
title={formattedMessage(link.label)}
|
||||
>
|
||||
{formattedMessage(link.label)}
|
||||
</Text>
|
||||
{link.target === '_blank' && (
|
||||
<Padded left size="xs">
|
||||
<LinkArrow />
|
||||
</Padded>
|
||||
)}
|
||||
</Flex>
|
||||
</Padded>
|
||||
</a>
|
||||
)}
|
||||
</Flex>
|
||||
</Padded>
|
||||
<RemoveWrapper>
|
||||
<Remove onClick={handleClose} />
|
||||
</RemoveWrapper>
|
||||
</Flex>
|
||||
</Padded>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Notification.defaultProps = {
|
||||
notification: {
|
||||
id: 1,
|
||||
message: 'app.utils.defaultMessage',
|
||||
status: 'success',
|
||||
type: 'success',
|
||||
message: {
|
||||
id: 'notification.success.saved',
|
||||
defaultMessage: 'Saved',
|
||||
},
|
||||
onClose: () => null,
|
||||
timeout: 2500,
|
||||
blockTransition: false,
|
||||
centered: false,
|
||||
},
|
||||
};
|
||||
|
||||
Notification.propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
notification: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
message: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
status: PropTypes.string,
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
link: PropTypes.shape({
|
||||
target: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]).isRequired,
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
blockTransition: PropTypes.bool,
|
||||
centered: PropTypes.bool,
|
||||
}),
|
||||
onHideNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { TransitionGroup } from 'react-transition-group';
|
||||
|
||||
const Wrapper = styled(TransitionGroup)`
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
left: 240px;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
list-style: none;
|
||||
width: 300px;
|
||||
margin: 0 auto;
|
||||
overflow-y: hidden;
|
||||
pointer-events: none;
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@ -1,56 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* NotificationsContainer
|
||||
*
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import Notification from '../Notification';
|
||||
import Wrapper from './Wrapper';
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
const NotificationsContainer = ({ notifications, onHideNotification }) => {
|
||||
if (notifications.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notifs = notifications.map((notification, i) => (
|
||||
<CSSTransition
|
||||
key={i}
|
||||
classNames="notification"
|
||||
timeout={{
|
||||
enter: 500,
|
||||
exit: 300,
|
||||
}}
|
||||
>
|
||||
<Notification
|
||||
key={notification.id}
|
||||
onHideNotification={onHideNotification}
|
||||
notification={notification}
|
||||
/>
|
||||
</CSSTransition>
|
||||
));
|
||||
|
||||
return <Wrapper>{notifs}</Wrapper>;
|
||||
};
|
||||
|
||||
NotificationsContainer.defaultProps = {
|
||||
notifications: [
|
||||
{
|
||||
id: 1,
|
||||
message: 'app.utils.defaultMessage',
|
||||
status: 'success',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
NotificationsContainer.propTypes = {
|
||||
notifications: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
onHideNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NotificationsContainer;
|
||||
@ -1,2 +1,46 @@
|
||||
export { default as Notification } from './Notification';
|
||||
export { default as NotificationsContainer } from './NotificationsContainer';
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import Notification from './Notification';
|
||||
import reducer, { initialState } from './reducer';
|
||||
import NotificationsWrapper from './Wrapper';
|
||||
|
||||
const Notifications = () => {
|
||||
const [{ notifications }, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const displayNotification = config => {
|
||||
dispatch({
|
||||
type: 'SHOW_NOTIFICATION',
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.strapi = Object.assign(window.strapi || {}, {
|
||||
notification: {
|
||||
toggle: config => {
|
||||
displayNotification(config);
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<NotificationsWrapper>
|
||||
{notifications.map(notification => (
|
||||
<CSSTransition
|
||||
key={notification.id}
|
||||
classNames="notification"
|
||||
timeout={{
|
||||
enter: 500,
|
||||
exit: 300,
|
||||
}}
|
||||
>
|
||||
<Notification dispatch={dispatch} notification={notification} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</NotificationsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import produce from 'immer';
|
||||
import { get } from 'lodash';
|
||||
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from './constants';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const initialState = {
|
||||
notifId: 0,
|
||||
@ -11,7 +10,7 @@ const notificationReducer = (state = initialState, action) =>
|
||||
// eslint-disable-next-line consistent-return
|
||||
produce(state, draftState => {
|
||||
switch (action.type) {
|
||||
case SHOW_NEW_NOTIFICATION: {
|
||||
case 'SHOW_NOTIFICATION': {
|
||||
draftState.notifications.push({
|
||||
// No action.config spread to limit the notification API and avoid customization
|
||||
id: state.notifId,
|
||||
@ -31,7 +30,7 @@ const notificationReducer = (state = initialState, action) =>
|
||||
draftState.notifId = state.notifId + 1;
|
||||
break;
|
||||
}
|
||||
case HIDE_NEW_NOTIFICATION: {
|
||||
case 'HIDE_NOTIFICATION': {
|
||||
const indexToRemove = state.notifications.findIndex(notif => notif.id === action.id);
|
||||
|
||||
if (indexToRemove !== -1) {
|
||||
@ -47,3 +46,4 @@ const notificationReducer = (state = initialState, action) =>
|
||||
});
|
||||
|
||||
export default notificationReducer;
|
||||
export { initialState };
|
||||
@ -1,7 +1,6 @@
|
||||
import reducer from '../reducer';
|
||||
import { SHOW_NEW_NOTIFICATION, HIDE_NEW_NOTIFICATION } from '../constants';
|
||||
|
||||
describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
|
||||
describe('ADMIN | COMPONENTS | NOTIFICATIONS | reducer', () => {
|
||||
describe('DEFAULT_ACTION', () => {
|
||||
it('should return the initialState', () => {
|
||||
const state = {
|
||||
@ -12,10 +11,10 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHOW_NEW_NOTIFICATION', () => {
|
||||
describe('SHOW_NOTIFICATION', () => {
|
||||
it('should add a notification', () => {
|
||||
const action = {
|
||||
type: SHOW_NEW_NOTIFICATION,
|
||||
type: 'SHOW_NOTIFICATION',
|
||||
config: {
|
||||
type: 'success',
|
||||
message: {
|
||||
@ -49,10 +48,10 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('HIDE_NEW_NOTIFICATION', () => {
|
||||
describe('HIDE_NOTIFICATION', () => {
|
||||
it('should remove a notification if the notification exist', () => {
|
||||
const action = {
|
||||
type: HIDE_NEW_NOTIFICATION,
|
||||
type: 'HIDE_NOTIFICATION',
|
||||
id: 1,
|
||||
};
|
||||
const initialState = {
|
||||
@ -67,7 +66,7 @@ describe('AMDIN | CONTAINERS | NEWNOTIFICATION | reducer', () => {
|
||||
|
||||
it('should not remove the notification if the notification does not exist', () => {
|
||||
const action = {
|
||||
type: HIDE_NEW_NOTIFICATION,
|
||||
type: 'HIDE_NOTIFICATION',
|
||||
id: 3,
|
||||
};
|
||||
const initialState = {
|
||||
@ -2,13 +2,6 @@
|
||||
*
|
||||
* App.js
|
||||
*
|
||||
* This component is the skeleton around the actual pages, and should only
|
||||
* contain code that should be seen on all pages. (e.g. navigation bar)
|
||||
*
|
||||
* NOTE: while this component should technically be a stateless functional
|
||||
* component (SFC), hot reloading does not currently support SFCs. If hot
|
||||
* reloading is not a neccessity for you then you can refactor it and remove
|
||||
* the linting exception.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
@ -17,50 +10,24 @@ import { Switch, Route } from 'react-router-dom';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { LoadingIndicatorPage, auth, request } from '@strapi/helper-plugin';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
|
||||
// FIXME
|
||||
import 'sanitize.css/sanitize.css';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import 'font-awesome/css/font-awesome.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.css';
|
||||
// eslint-disable-next-line import/extensions
|
||||
import '@fortawesome/fontawesome-free/js/all.min.js';
|
||||
|
||||
import GlobalStyle from '../../components/GlobalStyle';
|
||||
import Admin from '../Admin';
|
||||
import AuthPage from '../AuthPage';
|
||||
import NotFoundPage from '../NotFoundPage';
|
||||
// FIXME
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import NotificationProvider from '../NotificationProvider';
|
||||
import Theme from '../Theme';
|
||||
|
||||
import { getUID } from './utils';
|
||||
import { Content, Wrapper } from './components';
|
||||
import { getDataSucceeded } from './actions';
|
||||
import NewNotification from '../NewNotification';
|
||||
import PrivateRoute from '../PrivateRoute';
|
||||
import routes from './utils/routes';
|
||||
import { makeUniqueRoutes, createRoute } from '../SettingsPage/utils';
|
||||
|
||||
window.strapi = Object.assign(window.strapi || {}, {
|
||||
notification: {
|
||||
toggle: () => {},
|
||||
},
|
||||
|
||||
lockApp: () => console.log('todo lockApp'),
|
||||
unlockApp: () => console.log('todo unlockApp'),
|
||||
lockAppWithOverlay: () => console.log('todo unlockAppWithOverlay'),
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App(props) {
|
||||
const getDataRef = useRef();
|
||||
const [{ isLoading, hasAdmin }, setState] = useState({ isLoading: true, hasAdmin: false });
|
||||
@ -143,29 +110,22 @@ function App(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Theme>
|
||||
<Wrapper>
|
||||
<GlobalStyle />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NotificationProvider />
|
||||
<NewNotification />
|
||||
<Content>
|
||||
<Switch>
|
||||
{authRoutes}
|
||||
<Route
|
||||
path="/auth/:authType"
|
||||
render={routerProps => (
|
||||
<AuthPage {...routerProps} setHasAdmin={setHasAdmin} hasAdmin={hasAdmin} />
|
||||
)}
|
||||
exact
|
||||
/>
|
||||
<PrivateRoute path="/" component={Admin} />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Content>
|
||||
</QueryClientProvider>
|
||||
</Wrapper>
|
||||
</Theme>
|
||||
<Wrapper>
|
||||
<Content>
|
||||
<Switch>
|
||||
{authRoutes}
|
||||
<Route
|
||||
path="/auth/:authType"
|
||||
render={routerProps => (
|
||||
<AuthPage {...routerProps} setHasAdmin={setHasAdmin} hasAdmin={hasAdmin} />
|
||||
)}
|
||||
exact
|
||||
/>
|
||||
<PrivateRoute path="/" component={Admin} />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Content>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,181 +0,0 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Padded, Text, Flex } from '@buffetjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Remove } from '@buffetjs/icons';
|
||||
|
||||
import { HIDE_NEW_NOTIFICATION } from '../constants';
|
||||
import { NotificationWrapper, IconWrapper, LinkArrow, RemoveWrapper } from './styledComponents';
|
||||
|
||||
const types = {
|
||||
success: {
|
||||
icon: 'check',
|
||||
color: 'green',
|
||||
},
|
||||
warning: {
|
||||
icon: 'exclamation',
|
||||
color: 'orange',
|
||||
},
|
||||
info: {
|
||||
icon: 'info',
|
||||
color: 'blue',
|
||||
},
|
||||
};
|
||||
|
||||
const Notification = ({ notification }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
message,
|
||||
link,
|
||||
type,
|
||||
id,
|
||||
onClose,
|
||||
timeout,
|
||||
blockTransition,
|
||||
centered,
|
||||
} = notification;
|
||||
|
||||
const formattedMessage = msg => (typeof msg === 'string' ? msg : formatMessage(msg, msg.values));
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: HIDE_NEW_NOTIFICATION,
|
||||
id,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutToClear;
|
||||
|
||||
if (!blockTransition) {
|
||||
timeoutToClear = setTimeout(() => {
|
||||
handleClose();
|
||||
}, timeout || 2500);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeoutToClear);
|
||||
}, [blockTransition, handleClose, timeout]);
|
||||
|
||||
return (
|
||||
<NotificationWrapper centered={centered} color={types[type].color}>
|
||||
<Padded top left right bottom size="smd">
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<IconWrapper>
|
||||
<FontAwesomeIcon icon={types[type].icon} />
|
||||
</IconWrapper>
|
||||
<Padded left size="sm" style={{ width: '80%', flex: 1 }}>
|
||||
{title && (
|
||||
<Text
|
||||
fontSize="xs"
|
||||
textTransform="uppercase"
|
||||
color="grey"
|
||||
title={formattedMessage(title)}
|
||||
>
|
||||
{formattedMessage(title)}
|
||||
</Text>
|
||||
)}
|
||||
<Flex justifyContent="space-between">
|
||||
{message && (
|
||||
<Text title={formattedMessage(message)}>{formattedMessage(message)}</Text>
|
||||
)}
|
||||
{link && (
|
||||
<a
|
||||
href={link.url}
|
||||
target={link.target || '_blank'}
|
||||
rel={!link.target || link.target === '_blank' ? 'noopener noreferrer' : ''}
|
||||
>
|
||||
<Padded right left size="xs">
|
||||
<Flex alignItems="center">
|
||||
<Text
|
||||
style={{ maxWidth: '100px' }}
|
||||
ellipsis
|
||||
fontWeight="bold"
|
||||
color="blue"
|
||||
title={formattedMessage(link.label)}
|
||||
>
|
||||
{formattedMessage(link.label)}
|
||||
</Text>
|
||||
{link.target === '_blank' && (
|
||||
<Padded left size="xs">
|
||||
<LinkArrow />
|
||||
</Padded>
|
||||
)}
|
||||
</Flex>
|
||||
</Padded>
|
||||
</a>
|
||||
)}
|
||||
</Flex>
|
||||
</Padded>
|
||||
<RemoveWrapper>
|
||||
<Remove onClick={handleClose} />
|
||||
</RemoveWrapper>
|
||||
</Flex>
|
||||
</Padded>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
Notification.defaultProps = {
|
||||
notification: {
|
||||
id: 1,
|
||||
type: 'success',
|
||||
message: {
|
||||
id: 'notification.success.saved',
|
||||
defaultMessage: 'Saved',
|
||||
},
|
||||
onClose: () => null,
|
||||
timeout: 2500,
|
||||
blockTransition: false,
|
||||
centered: false,
|
||||
},
|
||||
};
|
||||
|
||||
Notification.propTypes = {
|
||||
notification: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
message: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
title: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]),
|
||||
link: PropTypes.shape({
|
||||
target: PropTypes.string,
|
||||
url: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
defaultMessage: PropTypes.string,
|
||||
values: PropTypes.object,
|
||||
}),
|
||||
]).isRequired,
|
||||
}),
|
||||
type: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
timeout: PropTypes.number,
|
||||
blockTransition: PropTypes.bool,
|
||||
centered: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@ -1,9 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { SHOW_NEW_NOTIFICATION } from './constants';
|
||||
|
||||
export function showNotification(config) {
|
||||
return {
|
||||
type: SHOW_NEW_NOTIFICATION,
|
||||
config,
|
||||
};
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export const SHOW_NEW_NOTIFICATION = 'SHOW_NEW_NOTIFICATION';
|
||||
export const HIDE_NEW_NOTIFICATION = 'HIDE_NEW_NOTIFICATION';
|
||||
@ -1,35 +0,0 @@
|
||||
/**
|
||||
*
|
||||
* NotificationsContainer
|
||||
*
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
import Notification from './Notification';
|
||||
import NotificationsWrapper from './Wrapper';
|
||||
|
||||
const NotificationsContainer = () => {
|
||||
const notifications = useSelector(state => state.newNotification.notifications);
|
||||
|
||||
return (
|
||||
<NotificationsWrapper>
|
||||
{notifications.map(notification => (
|
||||
<CSSTransition
|
||||
key={notification.id}
|
||||
classNames="notification"
|
||||
timeout={{
|
||||
enter: 500,
|
||||
exit: 300,
|
||||
}}
|
||||
>
|
||||
<Notification notification={notification} />
|
||||
</CSSTransition>
|
||||
))}
|
||||
</NotificationsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(NotificationsContainer);
|
||||
@ -1,40 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* NotificationProvider actions
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable import/no-cycle */
|
||||
// import { dispatch } from '../../app';
|
||||
|
||||
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
|
||||
|
||||
// TODO
|
||||
const dispatch = () => {};
|
||||
|
||||
let nextNotificationId = 0;
|
||||
|
||||
export function showNotification(message, status) {
|
||||
nextNotificationId++; // eslint-disable-line no-plusplus
|
||||
|
||||
// Start timeout to hide the notification
|
||||
(id => {
|
||||
setTimeout(() => {
|
||||
dispatch(hideNotification(id));
|
||||
}, 2500);
|
||||
})(nextNotificationId);
|
||||
|
||||
return {
|
||||
type: SHOW_NOTIFICATION,
|
||||
message,
|
||||
status,
|
||||
id: nextNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
export function hideNotification(id) {
|
||||
return {
|
||||
type: HIDE_NOTIFICATION,
|
||||
id,
|
||||
};
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* NotificationProvider constants
|
||||
*
|
||||
*/
|
||||
|
||||
export const SHOW_NOTIFICATION = 'app/NotificationProvider/SHOW_NOTIFICATION';
|
||||
export const HIDE_NOTIFICATION = 'app/NotificationProvider/HIDE_NOTIFICATION';
|
||||
@ -1,45 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* NotificationProvider
|
||||
*
|
||||
*/
|
||||
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { NotificationsContainer } from '../../components/Notifications';
|
||||
import { selectNotifications } from './selectors';
|
||||
import { hideNotification } from './actions';
|
||||
|
||||
export class NotificationProvider extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<NotificationsContainer
|
||||
onHideNotification={this.props.onHideNotification}
|
||||
notifications={this.props.notifications}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationProvider.propTypes = {
|
||||
notifications: PropTypes.object.isRequired,
|
||||
onHideNotification: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
notifications: selectNotifications(),
|
||||
});
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
onHideNotification: id => {
|
||||
dispatch(hideNotification(id));
|
||||
},
|
||||
dispatch,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NotificationProvider);
|
||||
@ -1,48 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* NotificationProvider reducer
|
||||
*
|
||||
*/
|
||||
|
||||
import { fromJS } from 'immutable';
|
||||
import { SHOW_NOTIFICATION, HIDE_NOTIFICATION } from './constants';
|
||||
|
||||
const initialState = fromJS({
|
||||
notifications: [],
|
||||
});
|
||||
|
||||
function notificationProviderReducer(state = initialState, action) {
|
||||
// Init variable
|
||||
let index;
|
||||
|
||||
switch (action.type) {
|
||||
case SHOW_NOTIFICATION:
|
||||
return state.set(
|
||||
'notifications',
|
||||
state.get('notifications').push({
|
||||
message: action.message || 'app.utils.defaultMessage',
|
||||
status: action.status || 'success',
|
||||
id: action.id,
|
||||
})
|
||||
);
|
||||
case HIDE_NOTIFICATION:
|
||||
// Check that the index exists
|
||||
state.get('notifications').forEach((notification, i) => {
|
||||
if (notification.id === action.id) {
|
||||
index = i;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof index !== 'undefined') {
|
||||
// Remove the notification
|
||||
return state.set('notifications', state.get('notifications').splice(index, 1));
|
||||
}
|
||||
|
||||
// Notification not found, return the current state
|
||||
return state;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default notificationProviderReducer;
|
||||
@ -1,27 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
/**
|
||||
* Direct selector to the notificationProvider state domain
|
||||
*/
|
||||
const selectNotificationProviderDomain = () => state => state.notification;
|
||||
|
||||
/**
|
||||
* Other specific selectors
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default selector used by NotificationProvider
|
||||
*/
|
||||
|
||||
const selectNotificationProvider = () =>
|
||||
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
|
||||
notificationProviderState.toJS()
|
||||
);
|
||||
|
||||
const selectNotifications = () =>
|
||||
createSelector(selectNotificationProviderDomain(), notificationProviderState =>
|
||||
notificationProviderState.get('notifications')
|
||||
);
|
||||
|
||||
export default selectNotificationProvider;
|
||||
export { selectNotificationProviderDomain, selectNotifications };
|
||||
@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import themes from '../../themes';
|
||||
|
||||
const Theme = ({ children }) => {
|
||||
return <ThemeProvider theme={themes}>{children}</ThemeProvider>;
|
||||
};
|
||||
|
||||
Theme.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default Theme;
|
||||
@ -1,8 +1,6 @@
|
||||
import globalReducer from '../containers/App/reducer';
|
||||
import adminReducer from '../containers/Admin/reducer';
|
||||
import languageProviderReducer from '../containers/LanguageProvider/reducer';
|
||||
import notificationProviderReducer from '../containers/NotificationProvider/reducer';
|
||||
import newNotificationReducer from '../containers/NewNotification/reducer';
|
||||
import permissionsManagerReducer from '../containers/PermissionsManager/reducer';
|
||||
import menuReducer from '../containers/LeftMenu/reducer';
|
||||
|
||||
@ -12,8 +10,6 @@ const reducers = {
|
||||
app: globalReducer,
|
||||
admin: adminReducer,
|
||||
language: languageProviderReducer,
|
||||
notification: notificationProviderReducer,
|
||||
newNotification: newNotificationReducer,
|
||||
permissionsManager: permissionsManagerReducer,
|
||||
menu: menuReducer,
|
||||
};
|
||||
|
||||
@ -5,6 +5,133 @@ describe('ADMIN | StrapiApp', () => {
|
||||
it('should render the app without plugins', () => {
|
||||
const app = StrapiApp({});
|
||||
|
||||
render(app.render());
|
||||
expect(render(app.render())).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": .c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: space-around;
|
||||
-webkit-justify-content: space-around;
|
||||
-ms-flex-pack: space-around;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.c1 > div {
|
||||
margin: auto;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 6px solid #f3f3f3;
|
||||
border-top: 6px solid #1c91e7;
|
||||
border-radius: 50%;
|
||||
-webkit-animation: fEWCgj 2s linear infinite;
|
||||
animation: fEWCgj 2s linear infinite;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 72px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
list-style: none;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
/>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</body>,
|
||||
"container": <div>
|
||||
<div
|
||||
class="sc-fTxPal krAKpM"
|
||||
/>
|
||||
<div
|
||||
class="Loader-sc-1xt0x01-0 JXMaR"
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
</div>,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user