Skip to content

Commit

Permalink
feat: added feedback dialog - wip
Browse files Browse the repository at this point in the history
Ticket: MEN-7355
Changelog: Title
Signed-off-by: Manuel Zedel <[email protected]>
  • Loading branch information
mzedel committed Sep 9, 2024
1 parent 0a9f54a commit e54efff
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 0 deletions.
16 changes: 16 additions & 0 deletions frontend/src/js/actions/userActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { duplicateFilter, extractErrorMessage, isEmpty, preformatWithRequestID }
import { getCurrentUser, getOnboardingState, getOrganization, getTooltipsState, getUserSettings as getUserSettingsSelector } from '../selectors';
import { clearAllRetryTimers } from '../utils/retrytimer';
import { commonErrorFallback, commonErrorHandler, initializeAppData, setOfflineThreshold, setSnackbar } from './appActions';
import { tenantadmApiUrlv2 } from './organizationActions';

const cookies = new Cookies();
const {
Expand Down Expand Up @@ -797,3 +798,18 @@ export const setAllTooltipsReadState =
const updatedTips = Object.keys(HELPTOOLTIPS).reduce((accu, id) => ({ ...accu, [id]: { readState } }), {});
return Promise.resolve(dispatch({ type: UserConstants.SET_TOOLTIPS_STATE, value: updatedTips })).then(() => dispatch(saveUserSettings()));
};

export const submitFeedback =
({ satisfaction, feedback, ...additionalInfo }) =>
(dispatch, getState) => {
const meta = {
...additionalInfo,
tenant: getOrganization(getState())
};
return GeneralApi.post(`${tenantadmApiUrlv2}/contact/support`, { feedback, satisfaction, meta }).then(() => {
const today = new Date();
dispatch(saveUserSettings({ feedbackCollected: today.toISOString().substring(0, 7) }));
setTimeout(() => dispatch(dismissFeedbackDialog()), AppConstants.TIMEOUTS.threeSeconds);
});
};
export const dismissFeedbackDialog = () => dispatch => Promise.resolve(dispatch({ type: UserConstants.SET_SHOW_FEEDBACK_DIALOG, value: false }));
3 changes: 3 additions & 0 deletions frontend/src/js/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { dark as darkTheme, light as lightTheme } from '../themes/Mender';
import Tracking from '../tracking';
import ConfirmDismissHelptips from './common/dialogs/confirmdismisshelptips';
import DeviceConnectionDialog from './common/dialogs/deviceconnectiondialog';
import FeedbackDialog from './common/dialogs/feedback';
import StartupNotificationDialog from './common/dialogs/startupnotification';
import Footer from './footer';
import Header from './header/header';
Expand Down Expand Up @@ -103,6 +104,7 @@ export const AppRoot = () => {
const showDismissHelptipsDialog = useSelector(state => !state.onboarding.complete && state.onboarding.showTipsDialog);
const showDeviceConnectionDialog = useSelector(state => state.users.showConnectDeviceDialog);
const showStartupNotification = useSelector(state => state.users.showStartupNotification);
const showFeedbackDialog = useSelector(state => state.users.showFeedbackDialog);
const snackbar = useSelector(state => state.app.snackbar);
const trackingCode = useSelector(state => state.app.trackerCode);
const { mode } = useSelector(getUserSettings);
Expand Down Expand Up @@ -203,6 +205,7 @@ export const AppRoot = () => {
{showDismissHelptipsDialog && <ConfirmDismissHelptips />}
{showDeviceConnectionDialog && <DeviceConnectionDialog onCancel={() => dispatch(setShowConnectingDialog(false))} />}
{showStartupNotification && <StartupNotificationDialog />}
{showFeedbackDialog && <FeedbackDialog />}
</div>
) : (
<div className={classes.public}>
Expand Down
165 changes: 165 additions & 0 deletions frontend/src/js/components/common/dialogs/feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2024 Northern.tech AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';

import {
Close as CloseIcon,
SentimentVeryDissatisfied as DissatisfiedIcon,
SentimentNeutral as NeutralIcon,
SentimentSatisfiedAlt as SatisfiedIcon,
SentimentVeryDissatisfiedOutlined as VeryDissatisfiedIcon,
SentimentVerySatisfiedOutlined as VerySatisfiedIcon
} from '@mui/icons-material';
import {
Button,
Dialog,
DialogContent,
DialogTitle,
IconButton,
TextField,
dialogClasses,
dialogTitleClasses,
iconButtonClasses,
lighten,
textFieldClasses
} from '@mui/material';
import { makeStyles } from 'tss-react/mui';

import { dismissFeedbackDialog, submitFeedback } from '../../../actions/userActions';
import { TIMEOUTS } from '../../../constants/appConstants';

const useStyles = makeStyles()(theme => ({
root: {
[`.${dialogClasses.paper}`]: { width: 350, bottom: 0, right: 0, position: 'absolute' },
[`.${dialogTitleClasses.root}`]: {
alignSelf: 'flex-end',
padding: 0,
[`.${iconButtonClasses.root}`]: { paddingBottom: 0 }
}
},
columns: { gap: theme.spacing(2) },
rating: {
[`.${iconButtonClasses.root}`]: {
borderRadius: theme.shape.borderRadius,
height: theme.spacing(6),
width: theme.spacing(6),
backgroundColor: lighten(theme.palette.primary.main, 0.85),
color: theme.palette.primary.main,
'&:hover': {
backgroundColor: theme.palette.primary.main,
color: lighten(theme.palette.primary.main, 0.85)
}
}
},
text: { [`.${textFieldClasses.root}`]: { marginTop: 0 }, '.submitButton': { alignSelf: 'start' } }
}));

const satisfactionLevels = [
{ Icon: VeryDissatisfiedIcon, title: 'Very Dissatisfied' },
{ Icon: DissatisfiedIcon, title: 'Dissatisfied' },
{ Icon: NeutralIcon, title: 'Neutral' },
{ Icon: SatisfiedIcon, title: 'Satisfied' },
{ Icon: VerySatisfiedIcon, title: 'Very Satisfied' }
];
const explanations = ['Very unsatisfied', 'Very satisfied'];

const SatisfactionGauge = ({ classes, setSatisfaction }) => {
return (
<div className={`flexbox column ${classes.columns}`}>
<div>How satisfied are you with Mender?</div>
<div className={`flexbox space-between ${classes.rating}`}>
{satisfactionLevels.map(({ Icon, title }, index) => (
<IconButton key={`satisfaction-${index}`} onClick={() => setSatisfaction(index)} title={title}>
<Icon />
</IconButton>
))}
</div>
<div className="flexbox space-between muted">
{explanations.map((explanation, index) => (
<div className="slightly-smaller" key={`explanation-${index}`}>
{explanation}
</div>
))}
</div>
</div>
);
};

const TextEntry = ({ classes, feedback, onChangeFeedback, onSubmit }) => (
<div className={`flexbox column ${classes.columns} ${classes.text}`}>
<div>What do you think is the most important thing to improve in Mender? (optional)</div>
<TextField hint="Your feedback" multiline minRows={4} onChange={({ target: { value } }) => onChangeFeedback(value)} value={feedback} variant="outlined" />
<Button className="submitButton" variant="contained" onClick={onSubmit}>
Submit Feedback
</Button>
</div>
);

const AppreciationNote = () => <p className="margin-top-none align-center">Thank you for taking the time to share your thoughts!</p>;

const progressionLevels = [SatisfactionGauge, TextEntry, AppreciationNote];

export const FeedbackDialog = () => {
const [progress, setProgress] = useState(0);
const [satisfaction, setSatisfaction] = useState(2);
const [feedback, setFeedback] = useState('');
const dispatch = useDispatch();
const isInitialized = useRef(false);

const { classes } = useStyles();

useEffect(() => {
if (!isInitialized.current) {
return;
}
setProgress(current => current + 1);
}, [satisfaction]);

useEffect(() => {
setTimeout(() => (isInitialized.current = true), TIMEOUTS.threeSeconds);
}, []);

const onCloseClick = () => dispatch(dismissFeedbackDialog());

const onAdvance = () => setProgress(current => current + 1);

const onSubmit = () => {
onAdvance();
dispatch(submitFeedback({ satisfaction: satisfactionLevels[satisfaction].title, feedback }));
};

const Component = progressionLevels[progress];
return (
<Dialog className={classes.root} open>
<DialogTitle>
<IconButton onClick={onCloseClick} aria-label="close" size="small">
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
<Component
classes={classes}
feedback={feedback}
setSatisfaction={setSatisfaction}
onChangeFeedback={setFeedback}
onSubmit={onSubmit}
onAdvance={onAdvance}
/>
</DialogContent>
</Dialog>
);
};

export default FeedbackDialog;
1 change: 1 addition & 0 deletions frontend/src/js/constants/userConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export const SET_SHOW_CONNECT_DEVICE = 'SET_SHOW_CONNECT_DEVICE';
export const SET_TOOLTIP_STATE = 'SET_TOOLTIP_STATE';
export const SET_TOOLTIPS_STATE = 'SET_TOOLTIPS_STATE';
export const SET_SHOW_STARTUP_NOTIFICATION = 'SET_SHOW_STARTUP_NOTIFICATION';
export const SET_SHOW_FEEDBACK_DIALOG = 'SET_SHOW_FEEDBACK_DIALOG';

export const OWN_USER_ID = 'me';

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/js/reducers/userReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const initialState = {
...UserConstants.rolesById
},
showConnectDeviceDialog: false,
showFeedbackDialog: true,
showStartupNotification: false,
tooltips: {
byId: {
Expand Down Expand Up @@ -183,6 +184,11 @@ const userReducer = (state = initialState, action) => {
byId: action.value
}
};
case UserConstants.SET_SHOW_FEEDBACK_DIALOG:
return {
...state,
showFeedbackDialog: action.value
};
default:
return state;
}
Expand Down

0 comments on commit e54efff

Please sign in to comment.