mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-18 01:18:23 +02:00
Feat/pnps (#306)
* feat: animation * feat: setup api calls * feat: try catch * feat: refetch user * fix: remove unused variables * fix: call hideFeedback * fix: return expression
This commit is contained in:
parent
a17e2f29b2
commit
7fddf04398
6
frontend/src/assets/icons/logo-plain.svg
Normal file
6
frontend/src/assets/icons/logo-plain.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg width="23" height="23" viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="0.288513" y="0.288574" width="7.13461" height="22.4231" fill="#1A4049"/>
|
||||||
|
<rect x="15.5769" y="0.288574" width="7.13461" height="22.4231" fill="#1A4049"/>
|
||||||
|
<path d="M22.7115 15.5771L22.7115 22.7118L0.28841 22.7118L0.28841 15.5771L22.7115 15.5771Z" fill="#1A4049"/>
|
||||||
|
<rect x="15.5769" y="15.5771" width="7.13461" height="7.13461" fill="#817AFE"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 455 B |
@ -12,12 +12,14 @@ import styles from './styles.module.scss';
|
|||||||
import IAuthStatus from '../interfaces/user';
|
import IAuthStatus from '../interfaces/user';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import NotFound from './common/NotFound/NotFound';
|
import NotFound from './common/NotFound/NotFound';
|
||||||
|
import Feedback from './common/Feedback';
|
||||||
interface IAppProps extends RouteComponentProps {
|
interface IAppProps extends RouteComponentProps {
|
||||||
user: IAuthStatus;
|
user: IAuthStatus;
|
||||||
fetchUiBootstrap: any;
|
fetchUiBootstrap: any;
|
||||||
|
feedback: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUiBootstrap();
|
fetchUiBootstrap();
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
@ -84,6 +86,10 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
<Route path="/404" component={NotFound} />
|
<Route path="/404" component={NotFound} />
|
||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
|
<Feedback
|
||||||
|
feedbackId="pnps"
|
||||||
|
openUrl="https://getunleash.ai/pnps"
|
||||||
|
/>
|
||||||
</LayoutPicker>
|
</LayoutPicker>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -91,6 +97,7 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
|||||||
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
|
// Set state to any for now, to avoid typing up entire state object while converting to tsx.
|
||||||
const mapStateToProps = (state: any) => ({
|
const mapStateToProps = (state: any) => ({
|
||||||
user: state.user.toJS(),
|
user: state.user.toJS(),
|
||||||
|
feedback: state.feedback,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(App);
|
export default connect(mapStateToProps)(App);
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { useEffect, useState, useRef, FC } from 'react';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
|
|
||||||
|
interface IAnimateOnMountProps {
|
||||||
|
mounted: boolean;
|
||||||
|
enter: string;
|
||||||
|
start: string;
|
||||||
|
leave: string;
|
||||||
|
container?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimateOnMount: FC<IAnimateOnMountProps> = ({
|
||||||
|
mounted,
|
||||||
|
enter,
|
||||||
|
start,
|
||||||
|
leave,
|
||||||
|
container,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [show, setShow] = useState(mounted);
|
||||||
|
const [styles, setStyles] = useState('');
|
||||||
|
const mountedRef = useRef<null | boolean>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mountedRef.current !== mounted || mountedRef === null) {
|
||||||
|
if (mounted) {
|
||||||
|
setShow(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setStyles(enter);
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
setStyles(leave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mounted, enter, leave]);
|
||||||
|
|
||||||
|
const onTransitionEnd = () => {
|
||||||
|
if (!mounted) {
|
||||||
|
setShow(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={show}
|
||||||
|
show={
|
||||||
|
<div
|
||||||
|
className={`${start} ${styles} ${
|
||||||
|
container ? container : ''
|
||||||
|
}`}
|
||||||
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnimateOnMount;
|
56
frontend/src/component/common/Feedback/Feedback.styles.ts
Normal file
56
frontend/src/component/common/Feedback/Feedback.styles.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
feedback: {
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
zIndex: '9999',
|
||||||
|
boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)',
|
||||||
|
padding: '1.5rem',
|
||||||
|
maxWidth: '400px',
|
||||||
|
width: '400px',
|
||||||
|
height: '200px',
|
||||||
|
},
|
||||||
|
animateContainer: {
|
||||||
|
zIndex: '9999',
|
||||||
|
},
|
||||||
|
feedbackStart: {
|
||||||
|
opacity: '0',
|
||||||
|
position: 'fixed',
|
||||||
|
right: '40px',
|
||||||
|
bottom: '40px',
|
||||||
|
transform: 'translateY(400px)',
|
||||||
|
},
|
||||||
|
feedbackEnter: {
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
opacity: '1',
|
||||||
|
transition: 'transform 0.6s ease, opacity 1s ease',
|
||||||
|
},
|
||||||
|
feedbackLeave: {
|
||||||
|
transform: 'translateY(400px)',
|
||||||
|
opacity: '0',
|
||||||
|
transition: 'transform 1.25s ease, opacity 1s ease',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '-38px',
|
||||||
|
top: '-47px',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
boxShadow: '2px 2px 4px 4px rgba(143,143,143, 0.25)',
|
||||||
|
['&:hover']: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
width: '25px',
|
||||||
|
height: '25px',
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
marginLeft: '1rem',
|
||||||
|
},
|
||||||
|
}));
|
156
frontend/src/component/common/Feedback/Feedback.tsx
Normal file
156
frontend/src/component/common/Feedback/Feedback.tsx
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button, IconButton } from '@material-ui/core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
|
|
||||||
|
import { ReactComponent as Logo } from '../../../assets/icons/logo-plain.svg';
|
||||||
|
import { useCommonStyles } from '../../../common.styles';
|
||||||
|
import { useStyles } from './Feedback.styles';
|
||||||
|
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
||||||
|
import ConditionallyRender from '../ConditionallyRender';
|
||||||
|
import { formatApiPath } from '../../../utils/format-path';
|
||||||
|
import { Action, Dispatch } from 'redux';
|
||||||
|
|
||||||
|
interface IFeedbackProps {
|
||||||
|
show?: boolean;
|
||||||
|
hideFeedback: () => Dispatch<Action>;
|
||||||
|
fetchUser: () => void;
|
||||||
|
feedbackId: string;
|
||||||
|
openUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Feedback = ({
|
||||||
|
show,
|
||||||
|
hideFeedback,
|
||||||
|
fetchUser,
|
||||||
|
feedbackId,
|
||||||
|
openUrl,
|
||||||
|
}: IFeedbackProps) => {
|
||||||
|
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
||||||
|
const styles = useStyles();
|
||||||
|
const commonStyles = useCommonStyles();
|
||||||
|
|
||||||
|
const onConfirm = async () => {
|
||||||
|
const url = formatApiPath('api/admin/feedback');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ feedbackId }),
|
||||||
|
});
|
||||||
|
await fetchUser();
|
||||||
|
} catch {
|
||||||
|
hideFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Await api call to register confirmation
|
||||||
|
window.open(openUrl, '_blank');
|
||||||
|
setTimeout(() => {
|
||||||
|
hideFeedback();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDontShowAgain = async () => {
|
||||||
|
const feedbackId = 'pnps';
|
||||||
|
const url = formatApiPath(
|
||||||
|
`api/admin/feedback/${encodeURIComponent(feedbackId)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'PUT',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ feedbackId, neverShow: true }),
|
||||||
|
});
|
||||||
|
await fetchUser();
|
||||||
|
} catch {
|
||||||
|
hideFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
hideFeedback();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimateOnMount
|
||||||
|
mounted={show}
|
||||||
|
enter={styles.feedbackEnter}
|
||||||
|
start={styles.feedbackStart}
|
||||||
|
leave={styles.feedbackLeave}
|
||||||
|
container={styles.animateContainer}
|
||||||
|
>
|
||||||
|
<div className={styles.feedback}>
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
styles.container,
|
||||||
|
commonStyles.contentSpacingY
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className={styles.close}
|
||||||
|
onClick={() => hideFeedback()}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Logo className={styles.logo} />
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={answeredNotNow}
|
||||||
|
show={
|
||||||
|
<p>
|
||||||
|
Alright, apologies for the disruption. Have a
|
||||||
|
nice day!
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<p>
|
||||||
|
Hi. Do you have 2 minutes to help us improve
|
||||||
|
Unleash?{' '}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={answeredNotNow}
|
||||||
|
show={
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={onDontShowAgain}
|
||||||
|
>
|
||||||
|
Don't show again
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
Yes, no problem
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={styles.cancel}
|
||||||
|
onClick={() => setAnsweredNotNow(true)}
|
||||||
|
>
|
||||||
|
Not now
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AnimateOnMount>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Feedback;
|
19
frontend/src/component/common/Feedback/index.tsx
Normal file
19
frontend/src/component/common/Feedback/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Dispatch } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { hideFeedback } from '../../../store/feedback/actions';
|
||||||
|
import { fetchUser } from '../../../store/user/actions';
|
||||||
|
import Feedback from './Feedback';
|
||||||
|
|
||||||
|
const mapStateToProps = (state: any) => ({
|
||||||
|
show: state.feedback.show,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch<Action>) => ({
|
||||||
|
hideFeedback: () => {
|
||||||
|
hideFeedback(dispatch)();
|
||||||
|
},
|
||||||
|
fetchUser: () => fetchUser()(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Feedback);
|
@ -1,4 +1,5 @@
|
|||||||
import { weightTypes } from '../feature/variant/enums';
|
import { weightTypes } from '../feature/variant/enums';
|
||||||
|
import differenceInDays from 'date-fns/differenceInDays';
|
||||||
|
|
||||||
const dateTimeOptions = {
|
const dateTimeOptions = {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@ -111,3 +112,29 @@ export const modalStyles = {
|
|||||||
|
|
||||||
export const updateIndexInArray = (array, index, newValue) =>
|
export const updateIndexInArray = (array, index, newValue) =>
|
||||||
array.map((v, i) => (i === index ? newValue : v));
|
array.map((v, i) => (i === index ? newValue : v));
|
||||||
|
|
||||||
|
export const showPnpsFeedback = user => {
|
||||||
|
if (!user) return;
|
||||||
|
if (!user.feedback) return;
|
||||||
|
if (user.feedback.length > 0) {
|
||||||
|
const feedback = user.feedback.find(
|
||||||
|
feedback => feedback.feedbackId === 'pnps'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!feedback) return false;
|
||||||
|
|
||||||
|
if (feedback.neverShow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedback.given) {
|
||||||
|
const SIX_MONTHS_IN_DAYS = 182;
|
||||||
|
const now = new Date();
|
||||||
|
const difference = differenceInDays(now, new Date(feedback.given));
|
||||||
|
|
||||||
|
return difference > SIX_MONTHS_IN_DAYS;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
validateName,
|
validateName,
|
||||||
} from '../../../../store/feature-toggle/actions';
|
} from '../../../../store/feature-toggle/actions';
|
||||||
import CreateFeature from './CreateFeature';
|
import CreateFeature from './CreateFeature';
|
||||||
import { loadNameFromHash } from '../../../common/util';
|
import { loadNameFromHash, showPnpsFeedback } from '../../../common/util';
|
||||||
|
import { showFeedback } from '../../../../store/feedback/actions';
|
||||||
|
|
||||||
const defaultStrategy = {
|
const defaultStrategy = {
|
||||||
name: 'default',
|
name: 'default',
|
||||||
@ -62,8 +63,9 @@ class WrapperComponent extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSubmit = async evt => {
|
onSubmit = async evt => {
|
||||||
|
const { user } = this.props;
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const { createFeatureToggles, history } = this.props;
|
const { createFeatureToggles, history, showFeedback } = this.props;
|
||||||
const { featureToggle } = this.state;
|
const { featureToggle } = this.state;
|
||||||
|
|
||||||
const errors = Object.values(this.state.errors).filter(i => i);
|
const errors = Object.values(this.state.errors).filter(i => i);
|
||||||
@ -80,6 +82,11 @@ class WrapperComponent extends Component {
|
|||||||
await createFeatureToggles(featureToggle).then(() =>
|
await createFeatureToggles(featureToggle).then(() =>
|
||||||
history.push(`/features/strategies/${featureToggle.name}`)
|
history.push(`/features/strategies/${featureToggle.name}`)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (showPnpsFeedback(user)) {
|
||||||
|
showFeedback();
|
||||||
|
}
|
||||||
|
// Trigger
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.toString().includes('not allowed to be empty')) {
|
if (e.toString().includes('not allowed to be empty')) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -119,6 +126,7 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
validateName: name => validateName(name)(dispatch),
|
validateName: name => validateName(name)(dispatch),
|
||||||
createFeatureToggles: featureToggle =>
|
createFeatureToggles: featureToggle =>
|
||||||
createFeatureToggles(featureToggle)(dispatch),
|
createFeatureToggles(featureToggle)(dispatch),
|
||||||
|
showFeedback: showFeedback(dispatch),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
8
frontend/src/store/feedback/actions.js
Normal file
8
frontend/src/store/feedback/actions.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const SHOW_FEEDBACK = 'SHOW_FEEDBACK';
|
||||||
|
export const HIDE_FEEDBACK = 'HIDE_FEEDBACK';
|
||||||
|
|
||||||
|
export const showFeedback = dispatch => () => dispatch({ type: SHOW_FEEDBACK });
|
||||||
|
|
||||||
|
export const hideFeedback = dispatch => () => {
|
||||||
|
dispatch({ type: HIDE_FEEDBACK });
|
||||||
|
};
|
15
frontend/src/store/feedback/index.js
Normal file
15
frontend/src/store/feedback/index.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { HIDE_FEEDBACK, SHOW_FEEDBACK } from './actions';
|
||||||
|
|
||||||
|
const feedback = (state = {}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SHOW_FEEDBACK:
|
||||||
|
return { ...state, show: true };
|
||||||
|
case HIDE_FEEDBACK: {
|
||||||
|
return { ...state, show: false };
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default feedback;
|
@ -20,6 +20,7 @@ import apiAdmin from './e-api-admin';
|
|||||||
import authAdmin from './e-admin-auth';
|
import authAdmin from './e-admin-auth';
|
||||||
import apiCalls from './api-calls';
|
import apiCalls from './api-calls';
|
||||||
import invoiceAdmin from './e-admin-invoice';
|
import invoiceAdmin from './e-admin-invoice';
|
||||||
|
import feedback from './feedback';
|
||||||
|
|
||||||
const unleashStore = combineReducers({
|
const unleashStore = combineReducers({
|
||||||
features,
|
features,
|
||||||
@ -43,6 +44,7 @@ const unleashStore = combineReducers({
|
|||||||
authAdmin,
|
authAdmin,
|
||||||
apiCalls,
|
apiCalls,
|
||||||
invoiceAdmin,
|
invoiceAdmin,
|
||||||
|
feedback,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default unleashStore;
|
export default unleashStore;
|
||||||
|
@ -2,12 +2,13 @@ import { Map as $Map } from 'immutable';
|
|||||||
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
||||||
import { AUTH_REQUIRED } from '../util';
|
import { AUTH_REQUIRED } from '../util';
|
||||||
|
|
||||||
const userStore = (state = new $Map({permissions: []}), action) => {
|
const userStore = (state = new $Map({ permissions: [] }), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case USER_CHANGE_CURRENT:
|
case USER_CHANGE_CURRENT:
|
||||||
state = state
|
state = state
|
||||||
.set('profile', action.value.user)
|
.set('profile', action.value.user)
|
||||||
.set('permissions', action.value.permissions || [])
|
.set('permissions', action.value.permissions || [])
|
||||||
|
.set('feedback', action.value.feedback || [])
|
||||||
.set('showDialog', false)
|
.set('showDialog', false)
|
||||||
.set('authDetails', undefined);
|
.set('authDetails', undefined);
|
||||||
return state;
|
return state;
|
||||||
@ -17,7 +18,7 @@ const userStore = (state = new $Map({permissions: []}), action) => {
|
|||||||
.set('showDialog', true);
|
.set('showDialog', true);
|
||||||
return state;
|
return state;
|
||||||
case USER_LOGOUT:
|
case USER_LOGOUT:
|
||||||
return new $Map({permissions: []});
|
return new $Map({ permissions: [] });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user