mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +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 { useEffect } from 'react';
|
||||
import NotFound from './common/NotFound/NotFound';
|
||||
import Feedback from './common/Feedback';
|
||||
interface IAppProps extends RouteComponentProps {
|
||||
user: IAuthStatus;
|
||||
fetchUiBootstrap: any;
|
||||
feedback: any;
|
||||
}
|
||||
|
||||
const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => {
|
||||
useEffect(() => {
|
||||
fetchUiBootstrap();
|
||||
/* eslint-disable-next-line */
|
||||
@ -84,6 +86,10 @@ const App = ({ location, user, fetchUiBootstrap }: IAppProps) => {
|
||||
<Route path="/404" component={NotFound} />
|
||||
<Redirect to="/404" />
|
||||
</Switch>
|
||||
<Feedback
|
||||
feedbackId="pnps"
|
||||
openUrl="https://getunleash.ai/pnps"
|
||||
/>
|
||||
</LayoutPicker>
|
||||
</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.
|
||||
const mapStateToProps = (state: any) => ({
|
||||
user: state.user.toJS(),
|
||||
feedback: state.feedback,
|
||||
});
|
||||
|
||||
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 differenceInDays from 'date-fns/differenceInDays';
|
||||
|
||||
const dateTimeOptions = {
|
||||
day: '2-digit',
|
||||
@ -111,3 +112,29 @@ export const modalStyles = {
|
||||
|
||||
export const updateIndexInArray = (array, index, newValue) =>
|
||||
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,
|
||||
} from '../../../../store/feature-toggle/actions';
|
||||
import CreateFeature from './CreateFeature';
|
||||
import { loadNameFromHash } from '../../../common/util';
|
||||
import { loadNameFromHash, showPnpsFeedback } from '../../../common/util';
|
||||
import { showFeedback } from '../../../../store/feedback/actions';
|
||||
|
||||
const defaultStrategy = {
|
||||
name: 'default',
|
||||
@ -62,8 +63,9 @@ class WrapperComponent extends Component {
|
||||
};
|
||||
|
||||
onSubmit = async evt => {
|
||||
const { user } = this.props;
|
||||
evt.preventDefault();
|
||||
const { createFeatureToggles, history } = this.props;
|
||||
const { createFeatureToggles, history, showFeedback } = this.props;
|
||||
const { featureToggle } = this.state;
|
||||
|
||||
const errors = Object.values(this.state.errors).filter(i => i);
|
||||
@ -80,6 +82,11 @@ class WrapperComponent extends Component {
|
||||
await createFeatureToggles(featureToggle).then(() =>
|
||||
history.push(`/features/strategies/${featureToggle.name}`)
|
||||
);
|
||||
|
||||
if (showPnpsFeedback(user)) {
|
||||
showFeedback();
|
||||
}
|
||||
// Trigger
|
||||
} catch (e) {
|
||||
if (e.toString().includes('not allowed to be empty')) {
|
||||
this.setState({
|
||||
@ -119,6 +126,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
validateName: name => validateName(name)(dispatch),
|
||||
createFeatureToggles: featureToggle =>
|
||||
createFeatureToggles(featureToggle)(dispatch),
|
||||
showFeedback: showFeedback(dispatch),
|
||||
});
|
||||
|
||||
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 apiCalls from './api-calls';
|
||||
import invoiceAdmin from './e-admin-invoice';
|
||||
import feedback from './feedback';
|
||||
|
||||
const unleashStore = combineReducers({
|
||||
features,
|
||||
@ -43,6 +44,7 @@ const unleashStore = combineReducers({
|
||||
authAdmin,
|
||||
apiCalls,
|
||||
invoiceAdmin,
|
||||
feedback,
|
||||
});
|
||||
|
||||
export default unleashStore;
|
||||
|
@ -2,12 +2,13 @@ import { Map as $Map } from 'immutable';
|
||||
import { USER_CHANGE_CURRENT, USER_LOGOUT } from './actions';
|
||||
import { AUTH_REQUIRED } from '../util';
|
||||
|
||||
const userStore = (state = new $Map({permissions: []}), action) => {
|
||||
const userStore = (state = new $Map({ permissions: [] }), action) => {
|
||||
switch (action.type) {
|
||||
case USER_CHANGE_CURRENT:
|
||||
state = state
|
||||
.set('profile', action.value.user)
|
||||
.set('permissions', action.value.permissions || [])
|
||||
.set('feedback', action.value.feedback || [])
|
||||
.set('showDialog', false)
|
||||
.set('authDetails', undefined);
|
||||
return state;
|
||||
@ -17,7 +18,7 @@ const userStore = (state = new $Map({permissions: []}), action) => {
|
||||
.set('showDialog', true);
|
||||
return state;
|
||||
case USER_LOGOUT:
|
||||
return new $Map({permissions: []});
|
||||
return new $Map({ permissions: [] });
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user