1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-10 01:16:39 +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:
olav 2022-03-31 09:23:46 +02:00 committed by GitHub
parent 2377def561
commit dbb62631a6
22 changed files with 844 additions and 75 deletions

View File

@ -1,5 +1,5 @@
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 Loader from 'component/common/Loader/Loader';
import NotFound from 'component/common/NotFound/NotFound';
@ -72,7 +72,7 @@ export const App = () => {
<Route path="/404" component={NotFound} />
<Redirect to="/404" />
</Switch>
<Feedback openUrl="http://feedback.unleash.run" />
<FeedbackNPS openUrl="http://feedback.unleash.run" />
<SplashPageRedirect />
</LayoutPicker>
</div>

View File

@ -1,5 +1,4 @@
import { weightTypes } from '../feature/FeatureView/FeatureVariants/FeatureVariantsList/AddFeatureVariant/enums';
import differenceInDays from 'date-fns/differenceInDays';
export const filterByFlags = flags => r => {
if (r.flag && !flags[r.flag]) {
@ -80,30 +79,3 @@ export const modalStyles = {
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';

View File

@ -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],
},
}));

View 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>
);
};

View File

@ -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',
},
}));

View File

@ -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();
});

View File

@ -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>
);
};

View File

@ -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,
},
},
}));

View File

@ -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>
);
};

View File

@ -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": "&#8203;",
}
}
/>
</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>
`;

View File

@ -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),
}
);
};

View File

@ -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,
});

View File

@ -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>
);
};

View File

@ -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')
);
};

View File

@ -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}`;
};

View File

@ -4,44 +4,37 @@ import classnames from 'classnames';
import CloseIcon from '@material-ui/icons/Close';
import { ReactComponent as Logo } from 'assets/icons/logoPlain.svg';
import { useCommonStyles } from 'common.styles';
import { useStyles } from './Feedback.styles';
import AnimateOnMount from '../AnimateOnMount/AnimateOnMount';
import ConditionallyRender from '../ConditionallyRender';
import { formatApiPath } from 'utils/formatPath';
import { useStyles } from 'component/feedback/FeedbackNPS/FeedbackNPS.styles';
import AnimateOnMount from 'component/common/AnimateOnMount/AnimateOnMount';
import ConditionallyRender from 'component/common/ConditionallyRender';
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 { useAuthFeedbackApi } from 'hooks/api/actions/useAuthFeedbackApi/useAuthFeedbackApi';
interface IFeedbackProps {
interface IFeedbackNPSProps {
openUrl: string;
}
const Feedback = ({ openUrl }: IFeedbackProps) => {
export const FeedbackNPS = ({ openUrl }: IFeedbackNPSProps) => {
const { showFeedback, setShowFeedback } = useContext(UIContext);
const { feedback, refetchFeedback } = useAuthFeedback();
const { createFeedback, updateFeedback } = useAuthFeedbackApi();
const { feedback } = useAuthFeedback();
const [answeredNotNow, setAnsweredNotNow] = useState(false);
const styles = useStyles();
const commonStyles = useCommonStyles();
const feedbackId = PNPS_FEEDBACK_ID;
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 refetchFeedback();
await createFeedback({ feedbackId });
} catch (err) {
console.warn(err);
setShowFeedback(false);
}
// Await api call to register confirmation
window.open(openUrl, '_blank');
setTimeout(() => {
@ -50,32 +43,18 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
};
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 refetchFeedback();
await updateFeedback({ feedbackId, neverShow: true });
} catch (err) {
console.warn(err);
setShowFeedback(false);
}
setTimeout(() => {
setShowFeedback(false);
}, 100);
};
if (!showPnpsFeedback(feedback)) {
if (!showNPSFeedback(feedback)) {
return null;
}
@ -152,5 +131,3 @@ const Feedback = ({ openUrl }: IFeedbackProps) => {
</AnimateOnMount>
);
};
export default Feedback;

View File

@ -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;
};

View File

@ -6,15 +6,17 @@ import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValida
import { useSegments } from 'hooks/api/getters/useSegments/useSegments';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import useToast from 'hooks/useToast';
import React from 'react';
import React, { useContext } from 'react';
import { useHistory } from 'react-router-dom';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useSegmentForm } from '../hooks/useSegmentForm';
import { SegmentForm } from '../SegmentForm/SegmentForm';
import { feedbackCESContext } from 'component/feedback/FeedbackCESContext/FeedbackCESContext';
export const CreateSegment = () => {
const { uiConfig } = useUiConfig();
const { setToastData, setToastApiError } = useToast();
const { showFeedbackCES } = useContext(feedbackCESContext);
const history = useHistory();
const { createSegment, loading } = useSegmentsApi();
const { refetchSegments } = useSegments();
@ -54,6 +56,11 @@ export const CreateSegment = () => {
confetti: true,
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) {
setToastApiError(formatUnknownError(error));
}

View File

@ -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),
});
};

View File

@ -32,10 +32,10 @@ export interface IAuthOptions {
}
export interface IAuthFeedback {
neverShow: boolean;
feedbackId: string;
neverShow?: boolean;
given?: string;
userId: number;
userId?: number;
}
export interface IAuthSplash {

View File

@ -13,6 +13,7 @@ import ScrollToTop from 'component/scrollToTop';
import AccessProvider from './component/providers/AccessProvider/AccessProvider';
import { getBasePath } from 'utils/formatPath';
import UIProvider from './component/providers/UIProvider/UIProvider';
import { FeedbackCESProvider } from 'component/feedback/FeedbackCESContext/FeedbackCESProvider';
ReactDOM.render(
<DndProvider backend={HTML5Backend}>
@ -21,10 +22,12 @@ ReactDOM.render(
<Router basename={`${getBasePath()}`}>
<ThemeProvider theme={mainTheme}>
<StylesProvider injectFirst>
<CssBaseline />
<ScrollToTop>
<Route path="/" component={App} />
</ScrollToTop>
<FeedbackCESProvider>
<CssBaseline />
<ScrollToTop>
<Route path="/" component={App} />
</ScrollToTop>
</FeedbackCESProvider>
</StylesProvider>
</ThemeProvider>
</Router>