mirror of
https://github.com/cheveguerra/whaticket-community.git
synced 2026-04-19 20:29:17 +00:00
improvement: better user feedback on clicking buttons
This commit is contained in:
35
frontend/src/components/ButtonWithSpinner/index.js
Normal file
35
frontend/src/components/ButtonWithSpinner/index.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
import { green } from "@material-ui/core/colors";
|
||||||
|
import { CircularProgress, Button } from "@material-ui/core";
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
button: {
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonProgress: {
|
||||||
|
color: green[500],
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
marginTop: -12,
|
||||||
|
marginLeft: -12,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ButtonWithSpinner = ({ loading, children, ...rest }) => {
|
||||||
|
const classes = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button className={classes.button} disabled={loading} {...rest}>
|
||||||
|
{children}
|
||||||
|
{loading && (
|
||||||
|
<CircularProgress size={24} className={classes.buttonProgress} />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ButtonWithSpinner;
|
||||||
@@ -195,55 +195,54 @@ const MessageInput = ({ ticketStatus }) => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStartRecording = () => {
|
const handleStartRecording = async () => {
|
||||||
navigator.getUserMedia(
|
|
||||||
{ audio: true },
|
|
||||||
() => {
|
|
||||||
Mp3Recorder.start()
|
|
||||||
.then(() => {
|
|
||||||
setRecording(true);
|
|
||||||
})
|
|
||||||
.catch(e => console.error(e));
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
console.log("Permission Denied");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUploadAudio = () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
Mp3Recorder.stop()
|
try {
|
||||||
.getMp3()
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
.then(async ([buffer, blob]) => {
|
await Mp3Recorder.start();
|
||||||
if (blob.size < 10000) {
|
setRecording(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRecording(false);
|
} catch (err) {
|
||||||
return;
|
console.log(err);
|
||||||
}
|
setLoading(false);
|
||||||
const formData = new FormData();
|
}
|
||||||
const filename = `${new Date().getTime()}.mp3`;
|
|
||||||
formData.append("media", blob, filename);
|
|
||||||
formData.append("body", filename);
|
|
||||||
formData.append("fromMe", true);
|
|
||||||
try {
|
|
||||||
await api.post(`/messages/${ticketId}`, formData);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
if (err.response && err.response.data && err.response.data.error) {
|
|
||||||
toast.error(err.response.data.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setRecording(false);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelAudio = () => {
|
const handleUploadAudio = async () => {
|
||||||
Mp3Recorder.stop()
|
setLoading(true);
|
||||||
.getMp3()
|
try {
|
||||||
.then(() => setRecording(false));
|
const [, blob] = await Mp3Recorder.stop().getMp3();
|
||||||
|
if (blob.size < 10000) {
|
||||||
|
setLoading(false);
|
||||||
|
setRecording(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
const filename = `${new Date().getTime()}.mp3`;
|
||||||
|
formData.append("media", blob, filename);
|
||||||
|
formData.append("body", filename);
|
||||||
|
formData.append("fromMe", true);
|
||||||
|
|
||||||
|
await api.post(`/messages/${ticketId}`, formData);
|
||||||
|
|
||||||
|
setRecording(false);
|
||||||
|
setLoading(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
if (err.response && err.response.data && err.response.data.error) {
|
||||||
|
toast.error(err.response.data.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelAudio = async () => {
|
||||||
|
try {
|
||||||
|
await Mp3Recorder.stop().getMp3();
|
||||||
|
setRecording(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (media.preview)
|
if (media.preview)
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import Autocomplete, {
|
|||||||
createFilterOptions,
|
createFilterOptions,
|
||||||
} from "@material-ui/lab/Autocomplete";
|
} from "@material-ui/lab/Autocomplete";
|
||||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||||
import { green } from "@material-ui/core/colors";
|
|
||||||
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
|
|
||||||
import { i18n } from "../../translate/i18n";
|
import { i18n } from "../../translate/i18n";
|
||||||
import api from "../../services/api";
|
import api from "../../services/api";
|
||||||
|
import ButtonWithSpinner from "../ButtonWithSpinner";
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
root: {
|
root: {
|
||||||
@@ -26,19 +26,11 @@ const useStyles = makeStyles(theme => ({
|
|||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
},
|
},
|
||||||
|
|
||||||
btnWrapper: {
|
// btnWrapper: {
|
||||||
// margin: theme.spacing(1),
|
// // margin: theme.spacing(1),
|
||||||
position: "relative",
|
// // position: "relative",
|
||||||
},
|
// display: "flex",
|
||||||
|
// },
|
||||||
buttonProgress: {
|
|
||||||
color: green[500],
|
|
||||||
position: "absolute",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
marginTop: -12,
|
|
||||||
marginLeft: -12,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const filterOptions = createFilterOptions({
|
const filterOptions = createFilterOptions({
|
||||||
@@ -163,21 +155,14 @@ const NewTicketModal = ({ modalOpen, onClose }) => {
|
|||||||
>
|
>
|
||||||
{i18n.t("newTicketModal.buttons.cancel")}
|
{i18n.t("newTicketModal.buttons.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
|
variant="contained"
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={loading}
|
loading={loading}
|
||||||
variant="contained"
|
|
||||||
className={classes.btnWrapper}
|
|
||||||
>
|
>
|
||||||
{i18n.t("newTicketModal.buttons.ok")}
|
{i18n.t("newTicketModal.buttons.ok")}
|
||||||
{loading && (
|
</ButtonWithSpinner>
|
||||||
<CircularProgress
|
|
||||||
size={24}
|
|
||||||
className={classes.buttonProgress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</form>
|
</form>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import { useHistory } from "react-router-dom";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
import { makeStyles } from "@material-ui/core/styles";
|
||||||
import { Button, IconButton } from "@material-ui/core";
|
import { IconButton } from "@material-ui/core";
|
||||||
import { MoreVert, Replay } from "@material-ui/icons";
|
import { MoreVert, Replay } from "@material-ui/icons";
|
||||||
|
|
||||||
import { i18n } from "../../translate/i18n";
|
import { i18n } from "../../translate/i18n";
|
||||||
import api from "../../services/api";
|
import api from "../../services/api";
|
||||||
import TicketOptionsMenu from "../TicketOptionsMenu";
|
import TicketOptionsMenu from "../TicketOptionsMenu";
|
||||||
|
import ButtonWithSpinner from "../ButtonWithSpinner";
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
actionButtons: {
|
actionButtons: {
|
||||||
@@ -27,6 +28,7 @@ const TicketActionButtons = ({ ticket }) => {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const userId = +localStorage.getItem("userId");
|
const userId = +localStorage.getItem("userId");
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const ticketOptionsMenuOpen = Boolean(anchorEl);
|
const ticketOptionsMenuOpen = Boolean(anchorEl);
|
||||||
|
|
||||||
const handleOpenTicketOptionsMenu = e => {
|
const handleOpenTicketOptionsMenu = e => {
|
||||||
@@ -38,18 +40,21 @@ const TicketActionButtons = ({ ticket }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateTicketStatus = async (e, status, userId) => {
|
const handleUpdateTicketStatus = async (e, status, userId) => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.put(`/tickets/${ticket.id}`, {
|
await api.put(`/tickets/${ticket.id}`, {
|
||||||
status: status,
|
status: status,
|
||||||
userId: userId || null,
|
userId: userId || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
if (status === "open") {
|
if (status === "open") {
|
||||||
history.push(`/tickets/${ticket.id}`);
|
history.push(`/tickets/${ticket.id}`);
|
||||||
} else {
|
} else {
|
||||||
history.push("/tickets");
|
history.push("/tickets");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setLoading(false);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
if (err.response && err.response.data && err.response.data.error) {
|
if (err.response && err.response.data && err.response.data.error) {
|
||||||
toast.error(err.response.data.error);
|
toast.error(err.response.data.error);
|
||||||
@@ -60,31 +65,34 @@ const TicketActionButtons = ({ ticket }) => {
|
|||||||
return (
|
return (
|
||||||
<div className={classes.actionButtons}>
|
<div className={classes.actionButtons}>
|
||||||
{ticket.status === "closed" && (
|
{ticket.status === "closed" && (
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
|
loading={loading}
|
||||||
startIcon={<Replay />}
|
startIcon={<Replay />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={e => handleUpdateTicketStatus(e, "open", userId)}
|
onClick={e => handleUpdateTicketStatus(e, "open", userId)}
|
||||||
>
|
>
|
||||||
{i18n.t("messagesList.header.buttons.reopen")}
|
{i18n.t("messagesList.header.buttons.reopen")}
|
||||||
</Button>
|
</ButtonWithSpinner>
|
||||||
)}
|
)}
|
||||||
{ticket.status === "open" && (
|
{ticket.status === "open" && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
|
loading={loading}
|
||||||
startIcon={<Replay />}
|
startIcon={<Replay />}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={e => handleUpdateTicketStatus(e, "pending", null)}
|
onClick={e => handleUpdateTicketStatus(e, "pending", null)}
|
||||||
>
|
>
|
||||||
{i18n.t("messagesList.header.buttons.return")}
|
{i18n.t("messagesList.header.buttons.return")}
|
||||||
</Button>
|
</ButtonWithSpinner>
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
|
loading={loading}
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={e => handleUpdateTicketStatus(e, "closed", userId)}
|
onClick={e => handleUpdateTicketStatus(e, "closed", userId)}
|
||||||
>
|
>
|
||||||
{i18n.t("messagesList.header.buttons.resolve")}
|
{i18n.t("messagesList.header.buttons.resolve")}
|
||||||
</Button>
|
</ButtonWithSpinner>
|
||||||
<IconButton onClick={handleOpenTicketOptionsMenu}>
|
<IconButton onClick={handleOpenTicketOptionsMenu}>
|
||||||
<MoreVert />
|
<MoreVert />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -97,14 +105,15 @@ const TicketActionButtons = ({ ticket }) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{ticket.status === "pending" && (
|
{ticket.status === "pending" && (
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
|
loading={loading}
|
||||||
size="small"
|
size="small"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={e => handleUpdateTicketStatus(e, "open", userId)}
|
onClick={e => handleUpdateTicketStatus(e, "open", userId)}
|
||||||
>
|
>
|
||||||
{i18n.t("messagesList.header.buttons.accept")}
|
{i18n.t("messagesList.header.buttons.accept")}
|
||||||
</Button>
|
</ButtonWithSpinner>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ const useStyles = makeStyles(theme => ({
|
|||||||
const TicketHeader = ({ loading, children }) => {
|
const TicketHeader = ({ loading, children }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
if (loading) return <TicketHeaderSkeleton />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card square className={classes.ticketHeader}>
|
<>
|
||||||
{children}
|
{loading ? (
|
||||||
</Card>
|
<TicketHeaderSkeleton />
|
||||||
|
) : (
|
||||||
|
<Card square className={classes.ticketHeader}>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { parseISO, format, isSameDay } from "date-fns";
|
import { parseISO, format, isSameDay } from "date-fns";
|
||||||
@@ -12,23 +12,15 @@ import Typography from "@material-ui/core/Typography";
|
|||||||
import Avatar from "@material-ui/core/Avatar";
|
import Avatar from "@material-ui/core/Avatar";
|
||||||
import Divider from "@material-ui/core/Divider";
|
import Divider from "@material-ui/core/Divider";
|
||||||
import Badge from "@material-ui/core/Badge";
|
import Badge from "@material-ui/core/Badge";
|
||||||
import Button from "@material-ui/core/Button";
|
|
||||||
|
|
||||||
import { i18n } from "../../translate/i18n";
|
import { i18n } from "../../translate/i18n";
|
||||||
|
|
||||||
import api from "../../services/api";
|
import api from "../../services/api";
|
||||||
|
import ButtonWithSpinner from "../ButtonWithSpinner";
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
ticket: {
|
ticket: {
|
||||||
position: "relative",
|
position: "relative",
|
||||||
"& .hidden-button": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
"&:hover .hidden-button": {
|
|
||||||
display: "flex",
|
|
||||||
position: "absolute",
|
|
||||||
left: "50%",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
noTicketsDiv: {
|
noTicketsDiv: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -83,23 +75,41 @@ const useStyles = makeStyles(theme => ({
|
|||||||
color: "white",
|
color: "white",
|
||||||
backgroundColor: green[500],
|
backgroundColor: green[500],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
acceptButton: {
|
||||||
|
position: "absolute",
|
||||||
|
left: "50%",
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const TicketListItem = ({ ticket }) => {
|
const TicketListItem = ({ ticket }) => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const userId = +localStorage.getItem("userId");
|
const userId = +localStorage.getItem("userId");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const { ticketId } = useParams();
|
const { ticketId } = useParams();
|
||||||
|
const isMounted = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleAcepptTicket = async ticketId => {
|
const handleAcepptTicket = async ticketId => {
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await api.put(`/tickets/${ticketId}`, {
|
await api.put(`/tickets/${ticketId}`, {
|
||||||
status: "open",
|
status: "open",
|
||||||
userId: userId,
|
userId: userId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setLoading(false);
|
||||||
alert(err);
|
alert(err);
|
||||||
}
|
}
|
||||||
|
if (isMounted.current) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
history.push(`/tickets/${ticketId}`);
|
history.push(`/tickets/${ticketId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,17 +190,18 @@ const TicketListItem = ({ ticket }) => {
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{ticket.status === "pending" ? (
|
{ticket.status === "pending" && (
|
||||||
<Button
|
<ButtonWithSpinner
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
color="primary"
|
color="primary"
|
||||||
className="hidden-button"
|
variant="contained"
|
||||||
|
className={classes.acceptButton}
|
||||||
|
size="small"
|
||||||
|
loading={loading}
|
||||||
onClick={e => handleAcepptTicket(ticket.id)}
|
onClick={e => handleAcepptTicket(ticket.id)}
|
||||||
>
|
>
|
||||||
{i18n.t("ticketsList.buttons.accept")}
|
{i18n.t("ticketsList.buttons.accept")}
|
||||||
</Button>
|
</ButtonWithSpinner>
|
||||||
) : null}
|
)}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider variant="inset" component="li" />
|
<Divider variant="inset" component="li" />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
Reference in New Issue
Block a user