mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
feat: add FeedbackCES component (#826)
* refactor: add screen-reader-only util class * refactor: move FeedbackNPS component * feat: add FeedbackCES component * refactor: improve hidden checkbox styles * refactor: fix IFeedbackEndpointRequestBody source type * refactor: remove unnecessary event.persist() calls * refactor: remove disableEscapeKeyDown from FeedbackCES modal * refactor: make textarea label customizable * refactor: store feedback state on the backend * refactor: add FeedbackCESForm snapshot test * refactor: use extant IAuthFeedback type * refactor: fix showNPSFeedback logic for multiple feedback types
This commit is contained in:
parent
2377def561
commit
dbb62631a6
@ -1,5 +1,5 @@
|
|||||||
import ConditionallyRender from 'component/common/ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import Feedback from 'component/common/Feedback/Feedback';
|
import { FeedbackNPS } from 'component/feedback/FeedbackNPS/FeedbackNPS';
|
||||||
import LayoutPicker from 'component/layout/LayoutPicker/LayoutPicker';
|
import LayoutPicker from 'component/layout/LayoutPicker/LayoutPicker';
|
||||||
import Loader from 'component/common/Loader/Loader';
|
import Loader from 'component/common/Loader/Loader';
|
||||||
import NotFound from 'component/common/NotFound/NotFound';
|
import NotFound from 'component/common/NotFound/NotFound';
|
||||||
@ -72,7 +72,7 @@ export const App = () => {
|
|||||||
<Route path="/404" component={NotFound} />
|
<Route path="/404" component={NotFound} />
|
||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
<Feedback openUrl="http://feedback.unleash.run" />
|
<FeedbackNPS openUrl="http://feedback.unleash.run" />
|
||||||
<SplashPageRedirect />
|
<SplashPageRedirect />
|
||||||
</LayoutPicker>
|
</LayoutPicker>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums';
|
import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums';
|
||||||
import differenceInDays from 'date-fns/differenceInDays';
|
|
||||||
|
|
||||||
export const filterByFlags = flags => r => {
|
export const filterByFlags = flags => r => {
|
||||||
if (r.flag && !flags[r.flag]) {
|
if (r.flag && !flags[r.flag]) {
|
||||||
@ -80,30 +79,3 @@ export const modalStyles = {
|
|||||||
transform: 'translate(-50%, -50%)',
|
transform: 'translate(-50%, -50%)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showPnpsFeedback = feedbackList => {
|
|
||||||
if (!feedbackList) return;
|
|
||||||
if (feedbackList.length > 0) {
|
|
||||||
const feedback = feedbackList.find(
|
|
||||||
feedback => feedback.feedbackId === PNPS_FEEDBACK_ID
|
|
||||||
);
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PNPS_FEEDBACK_ID = 'pnps';
|
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
overlay: {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
display: 'grid',
|
||||||
|
padding: '1rem',
|
||||||
|
overflowY: 'auto',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
pointerEvents: 'auto',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '4rem',
|
||||||
|
background: 'white',
|
||||||
|
boxShadow: '0 0 1rem rgba(0, 0, 0, 0.25)',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
padding: '2rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
all: 'unset',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
padding: '1rem',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
closeIcon: {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
}));
|
43
frontend/src/component/feedback/FeedbackCES/FeedbackCES.tsx
Normal file
43
frontend/src/component/feedback/FeedbackCES/FeedbackCES.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Modal } from '@material-ui/core';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import {
|
||||||
|
feedbackCESContext,
|
||||||
|
IFeedbackCESState,
|
||||||
|
} from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
|
||||||
|
import { FeedbackCESForm } from 'component/feedback/FeedbackCES/FeedbackCESForm';
|
||||||
|
import { useStyles } from 'component/feedback/FeedbackCES/FeedbackCES.styles';
|
||||||
|
import { CloseOutlined } from '@material-ui/icons';
|
||||||
|
|
||||||
|
export interface IFeedbackCESProps {
|
||||||
|
state?: IFeedbackCESState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedbackCES = ({ state }: IFeedbackCESProps) => {
|
||||||
|
const { hideFeedbackCES } = useContext(feedbackCESContext);
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const closeButton = (
|
||||||
|
<button className={styles.close} onClick={hideFeedbackCES}>
|
||||||
|
<CloseOutlined titleAccess="Close" className={styles.closeIcon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const modalContent = state && (
|
||||||
|
<FeedbackCESForm state={state} onClose={hideFeedbackCES} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={Boolean(state)}
|
||||||
|
onClose={hideFeedbackCES}
|
||||||
|
aria-label={state?.title}
|
||||||
|
>
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.modal}>
|
||||||
|
{closeButton}
|
||||||
|
{modalContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
fontWeight: theme.fontWeight.thin,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: 'grid',
|
||||||
|
gap: '3rem',
|
||||||
|
gridTemplateColumns: 'minmax(auto, 40rem)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
all: 'unset',
|
||||||
|
display: 'block',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
all: 'unset',
|
||||||
|
display: 'block',
|
||||||
|
marginTop: '2.5rem',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
textLabel: {
|
||||||
|
display: 'block',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
minWidth: '15rem',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,21 @@
|
|||||||
|
import { ThemeProvider } from '@material-ui/core';
|
||||||
|
import renderer from 'react-test-renderer';
|
||||||
|
import { FeedbackCESForm } from './FeedbackCESForm';
|
||||||
|
import mainTheme from 'themes/mainTheme';
|
||||||
|
|
||||||
|
test('FeedbackCESForm', () => {
|
||||||
|
const onClose = () => {
|
||||||
|
throw new Error('Unexpected onClose call.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = renderer.create(
|
||||||
|
<ThemeProvider theme={mainTheme}>
|
||||||
|
<FeedbackCESForm
|
||||||
|
onClose={onClose}
|
||||||
|
state={{ title: 'a', text: 'b', path: '/c' }}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
@ -0,0 +1,96 @@
|
|||||||
|
import { useStyles } from 'component/feedback/FeedbackCES/FeedbackCESForm.styles';
|
||||||
|
import { Button, TextField } from '@material-ui/core';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import produce from 'immer';
|
||||||
|
import useToast from 'hooks/useToast';
|
||||||
|
import { IFeedbackCESState } from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
|
||||||
|
import { FeedbackCESScore } from 'component/feedback/FeedbackCES/FeedbackCESScore';
|
||||||
|
import { sendFeedbackInput } from 'component/feedback/FeedbackCES/sendFeedbackInput';
|
||||||
|
|
||||||
|
export interface IFeedbackCESFormProps {
|
||||||
|
state: IFeedbackCESState;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFeedbackCESForm {
|
||||||
|
score: number;
|
||||||
|
comment: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedbackCESForm = ({ state, onClose }: IFeedbackCESFormProps) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { setToastData } = useToast();
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const [form, setForm] = useState<Partial<IFeedbackCESForm>>({
|
||||||
|
path: state.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setForm(
|
||||||
|
produce(draft => {
|
||||||
|
draft.comment = event.target.value;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await sendFeedbackInput(form);
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Feedback sent. Thank you!',
|
||||||
|
confetti: true,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<h1 className={styles.title}>Please help us improve</h1>
|
||||||
|
<form
|
||||||
|
className={styles.form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<p className={styles.subtitle}>{state.title}</p>
|
||||||
|
<FeedbackCESScore form={form} setForm={setForm} />
|
||||||
|
<div hidden={!form.score}>
|
||||||
|
<label htmlFor="comment" className={styles.textLabel}>
|
||||||
|
{state.text}
|
||||||
|
</label>
|
||||||
|
<TextField
|
||||||
|
value={form.comment ?? ''}
|
||||||
|
onChange={onCommentChange}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttons} hidden={!form.score}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
className={styles.button}
|
||||||
|
disabled={!form.score || loading}
|
||||||
|
>
|
||||||
|
Send feedback
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,55 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
scoreInput: {
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
},
|
||||||
|
scoreHelp: {
|
||||||
|
width: '8rem',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
color: theme.palette.grey[600],
|
||||||
|
'&:first-child': {
|
||||||
|
textAlign: 'right',
|
||||||
|
},
|
||||||
|
[theme.breakpoints.down('xs')]: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scoreValue: {
|
||||||
|
'& input': {
|
||||||
|
clip: 'rect(0 0 0 0)',
|
||||||
|
clipPath: 'inset(50%)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'absolute',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
'& span': {
|
||||||
|
display: 'grid',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: theme.palette.grey[300],
|
||||||
|
width: '3rem',
|
||||||
|
height: '3rem',
|
||||||
|
borderRadius: '10rem',
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
paddingBottom: 2,
|
||||||
|
userSelect: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
'& input:checked + span': {
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
background: theme.palette.primary.main,
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
'& input:focus-visible + span': {
|
||||||
|
outline: '2px solid',
|
||||||
|
outlineOffset: 2,
|
||||||
|
outlineColor: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import produce from 'immer';
|
||||||
|
import { useStyles } from 'component/feedback/FeedbackCES/FeedbackCESScore.styles';
|
||||||
|
import { IFeedbackCESForm } from 'component/feedback/FeedbackCES/FeedbackCESForm';
|
||||||
|
|
||||||
|
interface IFeedbackCESScoreProps {
|
||||||
|
form: Partial<IFeedbackCESForm>;
|
||||||
|
setForm: React.Dispatch<React.SetStateAction<Partial<IFeedbackCESForm>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedbackCESScore = ({ form, setForm }: IFeedbackCESScoreProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const onScoreChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setForm(
|
||||||
|
produce(draft => {
|
||||||
|
draft.score = Number(event.target.value);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.scoreInput}>
|
||||||
|
<span className={styles.scoreHelp}>Very difficult</span>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7].map(score => (
|
||||||
|
<label key={score} className={styles.scoreValue}>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="score"
|
||||||
|
value={score}
|
||||||
|
checked={form.score === score}
|
||||||
|
onChange={onScoreChange}
|
||||||
|
/>
|
||||||
|
<span>{score}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<span className={styles.scoreHelp}>Very easy</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,222 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`FeedbackCESForm 1`] = `
|
||||||
|
<div
|
||||||
|
className="makeStyles-container-1"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className="makeStyles-title-3"
|
||||||
|
>
|
||||||
|
Please help us improve
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
aria-live="polite"
|
||||||
|
className="makeStyles-form-2"
|
||||||
|
onSubmit={[Function]}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="makeStyles-subtitle-4"
|
||||||
|
>
|
||||||
|
a
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="makeStyles-scoreInput-8"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="makeStyles-scoreHelp-9"
|
||||||
|
>
|
||||||
|
Very difficult
|
||||||
|
</span>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={1}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={2}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
2
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={3}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
3
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={4}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
4
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={5}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
5
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={6}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
6
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="makeStyles-scoreValue-10"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
checked={false}
|
||||||
|
name="score"
|
||||||
|
onChange={[Function]}
|
||||||
|
type="radio"
|
||||||
|
value={7}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
7
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span
|
||||||
|
className="makeStyles-scoreHelp-9"
|
||||||
|
>
|
||||||
|
Very easy
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
hidden={true}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="makeStyles-textLabel-5"
|
||||||
|
htmlFor="comment"
|
||||||
|
>
|
||||||
|
b
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className="MuiFormControl-root MuiTextField-root MuiFormControl-fullWidth"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-multiline MuiOutlinedInput-multiline"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
aria-invalid={false}
|
||||||
|
autoFocus={false}
|
||||||
|
className="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMultiline MuiOutlinedInput-inputMultiline"
|
||||||
|
disabled={false}
|
||||||
|
onAnimationStart={[Function]}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onChange={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
required={false}
|
||||||
|
rows={3}
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden={true}
|
||||||
|
className="PrivateNotchedOutline-root-11 MuiOutlinedInput-notchedOutline"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"paddingLeft": 8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
className="PrivateNotchedOutline-legend-12"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"width": 0.01,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "​",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="makeStyles-buttons-6"
|
||||||
|
hidden={true}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-button-7 MuiButton-containedPrimary Mui-disabled Mui-disabled"
|
||||||
|
disabled={true}
|
||||||
|
onBlur={[Function]}
|
||||||
|
onDragLeave={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
onKeyUp={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onMouseUp={[Function]}
|
||||||
|
onTouchEnd={[Function]}
|
||||||
|
onTouchMove={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="MuiButton-label"
|
||||||
|
>
|
||||||
|
Send feedback
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,40 @@
|
|||||||
|
import { IFeedbackCESForm } from 'component/feedback/FeedbackCES/FeedbackCESForm';
|
||||||
|
|
||||||
|
interface IFeedbackEndpointRequestBody {
|
||||||
|
source: 'app' | 'app:segments';
|
||||||
|
data: {
|
||||||
|
score: number;
|
||||||
|
comment?: string;
|
||||||
|
customerType?: 'open source' | 'paying';
|
||||||
|
openedManually?: boolean;
|
||||||
|
currentPage?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendFeedbackInput = async (
|
||||||
|
form: Partial<IFeedbackCESForm>
|
||||||
|
): Promise<void> => {
|
||||||
|
if (!form.score) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: IFeedbackEndpointRequestBody = {
|
||||||
|
source: 'app:segments',
|
||||||
|
data: {
|
||||||
|
score: form.score,
|
||||||
|
comment: form.comment,
|
||||||
|
currentPage: form.path,
|
||||||
|
openedManually: false,
|
||||||
|
customerType: 'paying',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await fetch(
|
||||||
|
'https://europe-west3-docs-feedback-v1.cloudfunctions.net/function-1',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import React, { createContext } from 'react';
|
||||||
|
|
||||||
|
export type ShowFeedbackCES = React.Dispatch<
|
||||||
|
React.SetStateAction<IFeedbackCESState | undefined>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface IFeedbackCESContext {
|
||||||
|
showFeedbackCES: ShowFeedbackCES;
|
||||||
|
hideFeedbackCES: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFeedbackCESState {
|
||||||
|
path: `/${string}`;
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showFeedbackCESPlaceholder = () => {
|
||||||
|
throw new Error('showFeedbackCES called outside feedbackCESContext');
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideFeedbackCESPlaceholder = () => {
|
||||||
|
throw new Error('hideFeedbackCES called outside feedbackCESContext');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const feedbackCESContext = createContext<IFeedbackCESContext>({
|
||||||
|
showFeedbackCES: showFeedbackCESPlaceholder,
|
||||||
|
hideFeedbackCES: hideFeedbackCESPlaceholder,
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import React, {
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useMemo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import { FeedbackCES } from 'component/feedback/FeedbackCES/FeedbackCES';
|
||||||
|
import {
|
||||||
|
feedbackCESContext,
|
||||||
|
ShowFeedbackCES,
|
||||||
|
IFeedbackCESContext,
|
||||||
|
IFeedbackCESState,
|
||||||
|
} from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
|
||||||
|
import { useFeedbackCESSeen } from 'component/feedback/FeedbackCESContext/useFeedbackCESSeen';
|
||||||
|
import { useFeedbackCESEnabled } from 'component/feedback/FeedbackCESContext/useFeedbackCESEnabled';
|
||||||
|
|
||||||
|
interface IFeedbackProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeedbackCESProvider = ({ children }: IFeedbackProviderProps) => {
|
||||||
|
const [state, setState] = useState<IFeedbackCESState>();
|
||||||
|
const { isSeen, setSeen } = useFeedbackCESSeen();
|
||||||
|
const enabled = useFeedbackCESEnabled();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
state && setSeen(state);
|
||||||
|
}, [state, setSeen]);
|
||||||
|
|
||||||
|
// Set a new feedback state iff the path is unseen and CES is enabled.
|
||||||
|
const showFeedbackCES: ShowFeedbackCES = useCallback(
|
||||||
|
value => {
|
||||||
|
setState(prev => {
|
||||||
|
const next = value instanceof Function ? value(prev) : value;
|
||||||
|
return !enabled || !next || isSeen(next) ? undefined : next;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[enabled, isSeen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideFeedbackCES = useCallback(() => {
|
||||||
|
setState(undefined);
|
||||||
|
}, [setState]);
|
||||||
|
|
||||||
|
const value: IFeedbackCESContext = useMemo(
|
||||||
|
() => ({
|
||||||
|
showFeedbackCES: showFeedbackCES,
|
||||||
|
hideFeedbackCES: hideFeedbackCES,
|
||||||
|
}),
|
||||||
|
[showFeedbackCES, hideFeedbackCES]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<feedbackCESContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
<FeedbackCES state={state} />
|
||||||
|
</feedbackCESContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,10 @@
|
|||||||
|
export const useFeedbackCESEnabled = (): boolean => {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
|
||||||
|
return (
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname.endsWith('.vercel.app') ||
|
||||||
|
hostname.endsWith('.getunleash.io') ||
|
||||||
|
hostname.endsWith('.unleash-hosted.com')
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import { useAuthFeedback } from 'hooks/api/getters/useAuth/useAuthFeedback';
|
||||||
|
import { useAuthFeedbackApi } from 'hooks/api/actions/useAuthFeedbackApi/useAuthFeedbackApi';
|
||||||
|
import { IFeedbackCESState } from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
interface IUseFeedbackCESSeen {
|
||||||
|
setSeen: (state: IFeedbackCESState) => void;
|
||||||
|
isSeen: (state: IFeedbackCESState) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFeedbackCESSeen = (): IUseFeedbackCESSeen => {
|
||||||
|
const { createFeedback } = useAuthFeedbackApi();
|
||||||
|
const { feedback } = useAuthFeedback();
|
||||||
|
|
||||||
|
const isSeen = useCallback(
|
||||||
|
(state: IFeedbackCESState) =>
|
||||||
|
!!feedback &&
|
||||||
|
feedback.some(f => f.feedbackId === formatFeedbackCESId(state)),
|
||||||
|
[feedback]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setSeen = useCallback(
|
||||||
|
(state: IFeedbackCESState) =>
|
||||||
|
createFeedback({ feedbackId: formatFeedbackCESId(state) }),
|
||||||
|
[createFeedback]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSeen,
|
||||||
|
setSeen,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFeedbackCESId = (state: IFeedbackCESState): string => {
|
||||||
|
return `ces${state.path}`;
|
||||||
|
};
|
@ -4,44 +4,37 @@ import classnames from 'classnames';
|
|||||||
import CloseIcon from '@material-ui/icons/Close';
|
import CloseIcon from '@material-ui/icons/Close';
|
||||||
import { ReactComponent as Logo } from 'assets/icons/logoPlain.svg';
|
import { ReactComponent as Logo } from 'assets/icons/logoPlain.svg';
|
||||||
import { useCommonStyles } from 'common.styles';
|
import { useCommonStyles } from 'common.styles';
|
||||||
import { useStyles } from './Feedback.styles';
|
import { useStyles } from 'component/feedback/FeedbackNPS/FeedbackNPS.styles';
|
||||||
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
|
import AnimateOnMount from 'component/common/AnimateOnMount/AnimateOnMount';
|
||||||
import ConditionallyRender from '../ConditionallyRender';
|
import ConditionallyRender from 'component/common/ConditionallyRender';
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
import UIContext from 'contexts/UIContext';
|
import UIContext from 'contexts/UIContext';
|
||||||
import { PNPS_FEEDBACK_ID, showPnpsFeedback } from '../util';
|
import {
|
||||||
|
PNPS_FEEDBACK_ID,
|
||||||
|
showNPSFeedback,
|
||||||
|
} from 'component/feedback/FeedbackNPS/showNPSFeedback';
|
||||||
import { useAuthFeedback } from 'hooks/api/getters/useAuth/useAuthFeedback';
|
import { useAuthFeedback } from 'hooks/api/getters/useAuth/useAuthFeedback';
|
||||||
|
import { useAuthFeedbackApi } from 'hooks/api/actions/useAuthFeedbackApi/useAuthFeedbackApi';
|
||||||
|
|
||||||
interface IFeedbackProps {
|
interface IFeedbackNPSProps {
|
||||||
openUrl: string;
|
openUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Feedback = ({ openUrl }: IFeedbackProps) => {
|
export const FeedbackNPS = ({ openUrl }: IFeedbackNPSProps) => {
|
||||||
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
const { showFeedback, setShowFeedback } = useContext(UIContext);
|
||||||
const { feedback, refetchFeedback } = useAuthFeedback();
|
const { createFeedback, updateFeedback } = useAuthFeedbackApi();
|
||||||
|
const { feedback } = useAuthFeedback();
|
||||||
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
const [answeredNotNow, setAnsweredNotNow] = useState(false);
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const commonStyles = useCommonStyles();
|
const commonStyles = useCommonStyles();
|
||||||
const feedbackId = PNPS_FEEDBACK_ID;
|
const feedbackId = PNPS_FEEDBACK_ID;
|
||||||
|
|
||||||
const onConfirm = async () => {
|
const onConfirm = async () => {
|
||||||
const url = formatApiPath('api/admin/feedback');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(url, {
|
await createFeedback({ feedbackId });
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ feedbackId }),
|
|
||||||
});
|
|
||||||
await refetchFeedback();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Await api call to register confirmation
|
// Await api call to register confirmation
|
||||||
window.open(openUrl, '_blank');
|
window.open(openUrl, '_blank');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -50,32 +43,18 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDontShowAgain = async () => {
|
const onDontShowAgain = async () => {
|
||||||
const feedbackId = 'pnps';
|
|
||||||
const url = formatApiPath(
|
|
||||||
`api/admin/feedback/${encodeURIComponent(feedbackId)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fetch(url, {
|
await updateFeedback({ feedbackId, neverShow: true });
|
||||||
method: 'PUT',
|
|
||||||
credentials: 'include',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ feedbackId, neverShow: true }),
|
|
||||||
});
|
|
||||||
await refetchFeedback();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setShowFeedback(false);
|
setShowFeedback(false);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!showPnpsFeedback(feedback)) {
|
if (!showNPSFeedback(feedback)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,5 +131,3 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
|
|||||||
</AnimateOnMount>
|
</AnimateOnMount>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Feedback;
|
|
@ -0,0 +1,37 @@
|
|||||||
|
import differenceInDays from 'date-fns/differenceInDays';
|
||||||
|
import { IAuthFeedback } from 'hooks/api/getters/useAuth/useAuthEndpoint';
|
||||||
|
|
||||||
|
export const PNPS_FEEDBACK_ID = 'pnps';
|
||||||
|
|
||||||
|
export const showNPSFeedback = (
|
||||||
|
feedbackList: IAuthFeedback[] | undefined
|
||||||
|
): boolean => {
|
||||||
|
if (!feedbackList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feedbackList.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedback = feedbackList.find(
|
||||||
|
feedback => feedback.feedbackId === PNPS_FEEDBACK_ID
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!feedback) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
@ -6,15 +6,17 @@ import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValida
|
|||||||
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useSegmentForm } from '../hooks/useSegmentForm';
|
import { useSegmentForm } from '../hooks/useSegmentForm';
|
||||||
import { SegmentForm } from '../SegmentForm/SegmentForm';
|
import { SegmentForm } from '../SegmentForm/SegmentForm';
|
||||||
|
import { feedbackCESContext } from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
|
||||||
|
|
||||||
export const CreateSegment = () => {
|
export const CreateSegment = () => {
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { showFeedbackCES } = useContext(feedbackCESContext);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { createSegment, loading } = useSegmentsApi();
|
const { createSegment, loading } = useSegmentsApi();
|
||||||
const { refetchSegments } = useSegments();
|
const { refetchSegments } = useSegments();
|
||||||
@ -54,6 +56,11 @@ export const CreateSegment = () => {
|
|||||||
confetti: true,
|
confetti: true,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
|
showFeedbackCES({
|
||||||
|
title: 'How easy was it to create a segment?',
|
||||||
|
text: 'Please help us understand how we can improve segments',
|
||||||
|
path: '/segments/create',
|
||||||
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useAuthFeedback } from 'hooks/api/getters/useAuth/useAuthFeedback';
|
||||||
|
import { IAuthFeedback } from 'hooks/api/getters/useAuth/useAuthEndpoint';
|
||||||
|
|
||||||
|
interface IUseAuthFeedbackApi {
|
||||||
|
createFeedback: (feedback: IAuthFeedback) => Promise<void>;
|
||||||
|
updateFeedback: (feedback: IAuthFeedback) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthFeedbackApi = (): IUseAuthFeedbackApi => {
|
||||||
|
const { refetchFeedback } = useAuthFeedback();
|
||||||
|
const path = formatApiPath('api/admin/feedback');
|
||||||
|
|
||||||
|
const createFeedback = useCallback(
|
||||||
|
async (feedback: IAuthFeedback): Promise<void> => {
|
||||||
|
await sendFeedback('POST', path, feedback);
|
||||||
|
await refetchFeedback();
|
||||||
|
},
|
||||||
|
[path, refetchFeedback]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateFeedback = useCallback(
|
||||||
|
async (feedback: IAuthFeedback): Promise<void> => {
|
||||||
|
const pathWithId = `${path}/${feedback.feedbackId}`;
|
||||||
|
await sendFeedback('PUT', pathWithId, feedback);
|
||||||
|
await refetchFeedback();
|
||||||
|
},
|
||||||
|
[path, refetchFeedback]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
createFeedback,
|
||||||
|
updateFeedback,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendFeedback = async (
|
||||||
|
method: 'PUT' | 'POST',
|
||||||
|
path: string,
|
||||||
|
feedback: IAuthFeedback
|
||||||
|
): Promise<void> => {
|
||||||
|
await fetch(path, {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(feedback),
|
||||||
|
});
|
||||||
|
};
|
@ -32,10 +32,10 @@ export interface IAuthOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthFeedback {
|
export interface IAuthFeedback {
|
||||||
neverShow: boolean;
|
|
||||||
feedbackId: string;
|
feedbackId: string;
|
||||||
|
neverShow?: boolean;
|
||||||
given?: string;
|
given?: string;
|
||||||
userId: number;
|
userId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthSplash {
|
export interface IAuthSplash {
|
||||||
|
@ -13,6 +13,7 @@ import ScrollToTop from 'component/scrollToTop';
|
|||||||
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
|
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
|
||||||
import { getBasePath } from 'utils/formatPath';
|
import { getBasePath } from 'utils/formatPath';
|
||||||
import UIProvider from './component/providers/UIProvider/UIProvider';
|
import UIProvider from './component/providers/UIProvider/UIProvider';
|
||||||
|
import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/FeedbackCESProvider';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
@ -21,10 +22,12 @@ ReactDOM.render(
|
|||||||
<Router basename={`${getBasePath()}`}>
|
<Router basename={`${getBasePath()}`}>
|
||||||
<ThemeProvider theme={mainTheme}>
|
<ThemeProvider theme={mainTheme}>
|
||||||
<StylesProvider injectFirst>
|
<StylesProvider injectFirst>
|
||||||
<CssBaseline />
|
<FeedbackCESProvider>
|
||||||
<ScrollToTop>
|
<CssBaseline />
|
||||||
<Route path="/" component={App} />
|
<ScrollToTop>
|
||||||
</ScrollToTop>
|
<Route path="/" component={App} />
|
||||||
|
</ScrollToTop>
|
||||||
|
</FeedbackCESProvider>
|
||||||
</StylesProvider>
|
</StylesProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</Router>
|
</Router>
|
||||||
|
Loading…
Reference in New Issue
Block a user