replace polls table with datagrid
This commit is contained in:
parent
468c79209d
commit
564774ebf4
@ -10,6 +10,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/x-data-grid": "^5.17.10",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
|
@ -1,20 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Popper from '@mui/material/Popper';
|
||||
import UserAvatar from './UserAvatar';
|
||||
|
||||
class PollsTable extends React.Component {
|
||||
render() {
|
||||
const { allUsers, polls } = this.props;
|
||||
const { intl } = this.props;
|
||||
|
||||
function getUserAnswer(user, poll) {
|
||||
if (typeof user.answers[poll.pollId] !== 'undefined') {
|
||||
return Array.isArray(user.answers[poll.pollId])
|
||||
? user.answers[poll.pollId]
|
||||
: [user.answers[poll.pollId]];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
const PollsTable = (props) => {
|
||||
const {
|
||||
allUsers, polls, intl,
|
||||
} = props;
|
||||
|
||||
if (typeof polls === 'object' && Object.values(polls).length === 0) {
|
||||
return (
|
||||
@ -51,149 +47,20 @@ class PollsTable extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
// Here we count each poll vote in order to find out the most common answer.
|
||||
const pollVotesCount = Object.keys(polls || {}).reduce((prevPollVotesCount, pollId) => {
|
||||
const currPollVotesCount = { ...prevPollVotesCount };
|
||||
currPollVotesCount[pollId] = {};
|
||||
const commonFieldProps = {
|
||||
field: 'User',
|
||||
headerName: 'User',
|
||||
flex: 1,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
if (polls[pollId].anonymous) {
|
||||
polls[pollId].anonymousAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}
|
||||
|
||||
Object.values(allUsers).forEach((currUser) => {
|
||||
if (currUser.answers[pollId] !== undefined) {
|
||||
const userAnswers = Array.isArray(currUser.answers[pollId])
|
||||
? currUser.answers[pollId]
|
||||
: [currUser.answers[pollId]];
|
||||
|
||||
userAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return currPollVotesCount;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-xs font-semibold tracking-wide col-text-left text-gray-500 uppercase border-b bg-gray-100">
|
||||
<th className="px-3.5 2xl:px-4 py-3">
|
||||
<FormattedMessage id="app.learningDashboard.user" defaultMessage="User" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 13l-5 5m0 0l-5-5m5 5V6" />
|
||||
</svg>
|
||||
</th>
|
||||
{typeof polls === 'object' && Object.values(polls || {}).length > 0 ? (
|
||||
Object.values(polls || {})
|
||||
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
||||
.map((poll, index) => <th className="px-3.5 2xl:px-4 py-3 text-center">{poll.question || `Poll ${index + 1}`}</th>)
|
||||
) : null }
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y whitespace-nowrap">
|
||||
{ typeof allUsers === 'object' && Object.values(allUsers || {}).length > 0 ? (
|
||||
Object.values(allUsers || {})
|
||||
.filter((user) => Object.values(user.answers).length > 0)
|
||||
.sort((a, b) => {
|
||||
if (a.isModerator === false && b.isModerator === true) return 1;
|
||||
if (a.isModerator === true && b.isModerator === false) return -1;
|
||||
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
|
||||
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((user) => (
|
||||
<tr className="text-gray-700">
|
||||
<td className="px-3.5 2xl:px-4 py-3">
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="relative hidden w-8 h-8 rounded-full md:block">
|
||||
<UserAvatar user={user} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-semibold truncate xl:max-w-sm max-w-xs">{user.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{typeof polls === 'object' && Object.values(polls || {}).length > 0 ? (
|
||||
Object.values(polls || {})
|
||||
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
||||
.map((poll) => (
|
||||
<td className="px-4 py-3 text-sm text-center">
|
||||
{ getUserAnswer(user, poll).map((answer) => {
|
||||
const answersSorted = Object
|
||||
.entries(pollVotesCount[poll?.pollId])
|
||||
.sort(([, countA], [, countB]) => countB - countA);
|
||||
const isMostCommonAnswer = (
|
||||
answersSorted[0]?.[0]?.toLowerCase() === answer?.toLowerCase()
|
||||
&& answersSorted[0]?.[1] > 1
|
||||
);
|
||||
return <p className={isMostCommonAnswer ? 'font-bold' : ''}>{answer}</p>;
|
||||
}) }
|
||||
{ poll.anonymous
|
||||
? (
|
||||
<span title={intl.formatMessage({
|
||||
id: 'app.learningDashboard.pollsTable.anonymousAnswer',
|
||||
defaultMessage: 'Anonymous Poll (answers in the last row)',
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
)
|
||||
: null }
|
||||
</td>
|
||||
))
|
||||
) : null }
|
||||
</tr>
|
||||
))) : null }
|
||||
{typeof polls === 'object'
|
||||
&& Object.values(polls || {}).length > 0
|
||||
&& Object.values(polls).reduce((prev, poll) => ([
|
||||
...prev,
|
||||
...poll.anonymousAnswers,
|
||||
]), []).length > 0 ? (
|
||||
<tr className="text-gray-700">
|
||||
<td className="px-3.5 2xl:px-4 py-3">
|
||||
const anonGridCols = [
|
||||
{
|
||||
...commonFieldProps,
|
||||
valueGetter: (params) => params?.row?.User?.name,
|
||||
renderCell: () => (
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
{/* <img className="object-cover w-full h-full rounded-full" */}
|
||||
{/* src="" */}
|
||||
{/* alt="" loading="lazy" /> */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="relative hidden w-8 h-8 mr-3 rounded-full md:block"
|
||||
@ -219,20 +86,291 @@ class PollsTable extends React.Component {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{Object.values(polls || {})
|
||||
.sort((a, b) => ((a.createdOn > b.createdOn) ? 1 : -1))
|
||||
.map((poll) => (
|
||||
<td className="px-3.5 2xl:px-4 py-3 text-sm text-center">
|
||||
{ poll.anonymousAnswers.map((answer) => <p>{answer}</p>) }
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const gridCols = [
|
||||
{
|
||||
...commonFieldProps,
|
||||
valueGetter: (params) => params?.row?.User?.name,
|
||||
renderCell: (params) => (
|
||||
<>
|
||||
<div className="relative hidden w-8 h-8 rounded-full md:block">
|
||||
<UserAvatar user={params?.row?.User} />
|
||||
</div>
|
||||
<div className="mx-2 font-semibold text-gray-700">{params?.value}</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const isOverflown = (element) => (
|
||||
element.scrollHeight > element.clientHeight
|
||||
|| element.scrollWidth > element.clientWidth
|
||||
);
|
||||
|
||||
const GridCellExpand = React.memo((cellProps) => {
|
||||
const {
|
||||
width, value, isMostCommonAnswer, anonymous,
|
||||
} = cellProps;
|
||||
const wrapper = React.useRef(null);
|
||||
const cellDiv = React.useRef(null);
|
||||
const cellValue = React.useRef(null);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const [showFullCell, setShowFullCell] = React.useState(false);
|
||||
const [showPopper, setShowPopper] = React.useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
const isCurrentlyOverflown = isOverflown(cellValue.current);
|
||||
setShowPopper(isCurrentlyOverflown);
|
||||
setAnchorEl(cellDiv.current);
|
||||
setShowFullCell(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setShowFullCell(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!showFullCell) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleKeyDown(nativeEvent) {
|
||||
if (nativeEvent.key === 'Escape' || nativeEvent.key === 'Esc') {
|
||||
setShowFullCell(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [setShowFullCell, showFullCell]);
|
||||
|
||||
if (anonymous) {
|
||||
return (
|
||||
<span title={intl.formatMessage({
|
||||
id: 'app.learningDashboard.pollsTable.anonymousAnswer',
|
||||
defaultMessage: 'Anonymous Poll (answers in the last row)',
|
||||
})}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4 inline"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let val = value;
|
||||
if (typeof value === 'object') {
|
||||
val = Object.values(value)?.join(', ');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={wrapper}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
lineHeight: '24px',
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={cellDiv}
|
||||
sx={{
|
||||
height: 1,
|
||||
width,
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
ref={cellValue}
|
||||
sx={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
||||
className={isMostCommonAnswer ? 'font-bold' : ''}
|
||||
>
|
||||
{ val }
|
||||
</Box>
|
||||
{showPopper && (
|
||||
<Popper
|
||||
open={showFullCell && anchorEl !== null}
|
||||
anchorEl={anchorEl}
|
||||
style={{ width, marginLeft: -17 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={1}
|
||||
style={{ minHeight: wrapper.current.offsetHeight - 3 }}
|
||||
>
|
||||
<Typography className={isMostCommonAnswer ? 'font-bold' : ''} variant="body2" style={{ padding: 8 }}>
|
||||
{ val }
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Popper>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
let hasAnonymousPoll = false;
|
||||
const initPollData = {};
|
||||
const anonymousPollData = {};
|
||||
const anonGridRow = [];
|
||||
const gridRows = [];
|
||||
|
||||
Object.values(polls).map((v, i) => {
|
||||
initPollData[`${v?.pollId}`] = '';
|
||||
const headerName = v?.question?.length > 0 ? v?.question : `Poll ${i + 1}`;
|
||||
if (v?.anonymous) {
|
||||
hasAnonymousPoll = true;
|
||||
anonymousPollData[`${v?.pollId}`] = v?.anonymousAnswers;
|
||||
}
|
||||
|
||||
const commonColProps = {
|
||||
field: v?.pollId,
|
||||
headerName,
|
||||
flex: 1,
|
||||
sortable: false,
|
||||
};
|
||||
|
||||
anonGridCols.push({
|
||||
...commonColProps,
|
||||
renderCell: (params) => <GridCellExpand value={params?.value || ''} width={params?.colDef?.computedWidth} />,
|
||||
});
|
||||
|
||||
gridCols.push({
|
||||
...commonColProps,
|
||||
renderCell: (params) => {
|
||||
// Here we count each poll vote in order to find out the most common answer.
|
||||
const pollVotesCount = Object.keys(polls || {}).reduce((prevPollVotesCount, pollId) => {
|
||||
const currPollVotesCount = { ...prevPollVotesCount };
|
||||
currPollVotesCount[pollId] = {};
|
||||
|
||||
if (polls[pollId].anonymous) {
|
||||
polls[pollId].anonymousAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
return currPollVotesCount;
|
||||
}
|
||||
|
||||
Object.values(allUsers).forEach((currUser) => {
|
||||
if (currUser.answers[pollId] !== undefined) {
|
||||
const userAnswers = Array.isArray(currUser.answers[pollId])
|
||||
? currUser.answers[pollId]
|
||||
: [currUser.answers[pollId]];
|
||||
|
||||
userAnswers.forEach((answer) => {
|
||||
const answerLowerCase = answer.toLowerCase();
|
||||
if (currPollVotesCount[pollId][answerLowerCase] === undefined) {
|
||||
currPollVotesCount[pollId][answerLowerCase] = 1;
|
||||
} else {
|
||||
currPollVotesCount[pollId][answerLowerCase] += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return currPollVotesCount;
|
||||
}, {});
|
||||
const answersSorted = Object.entries(pollVotesCount[v?.pollId])
|
||||
.sort(([, countA], [, countB]) => countB - countA);
|
||||
const isMostCommonAnswer = (
|
||||
answersSorted[0]?.[0]?.toLowerCase() === params?.value[0]?.toLowerCase()
|
||||
&& answersSorted[0]?.[1] > 1
|
||||
);
|
||||
return <GridCellExpand anonymous={v?.anonymous} isMostCommonAnswer={isMostCommonAnswer} value={params?.value || ''} width={params?.colDef?.computedWidth} />;
|
||||
},
|
||||
});
|
||||
return v;
|
||||
});
|
||||
|
||||
Object.values(allUsers).map((u, i) => {
|
||||
if (u?.isModerator && Object.keys(u?.answers)?.length === 0) return u;
|
||||
gridRows.push({ id: i + 1, User: u, ...{ ...initPollData, ...u?.answers } });
|
||||
return u;
|
||||
});
|
||||
|
||||
if (hasAnonymousPoll) {
|
||||
anonGridRow.push({ id: 1, User: { name: 'Anonymous' }, ...{ ...initPollData, ...anonymousPollData } });
|
||||
}
|
||||
|
||||
const commonGridProps = {
|
||||
autoHeight: true,
|
||||
hideFooter: true,
|
||||
disableColumnMenu: true,
|
||||
disableColumnSelector: true,
|
||||
disableSelectionOnClick: true,
|
||||
rowHeight: 45,
|
||||
};
|
||||
|
||||
const anonymousDataGrid = (
|
||||
<DataGrid
|
||||
{...commonGridProps}
|
||||
rows={anonGridRow}
|
||||
columns={anonGridCols}
|
||||
sx={{
|
||||
'& .MuiDataGrid-columnHeaders': {
|
||||
display: 'none',
|
||||
},
|
||||
'& .MuiDataGrid-virtualScroller': {
|
||||
marginTop: '0!important',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-white" style={{ width: '100%' }}>
|
||||
<DataGrid
|
||||
{...commonGridProps}
|
||||
rows={gridRows}
|
||||
columns={gridCols}
|
||||
sortingOrder={['asc', 'desc']}
|
||||
sx={{
|
||||
'& .MuiDataGrid-columnHeaders': {
|
||||
backgroundColor: 'rgb(243 244 246/var(--tw-bg-opacity))',
|
||||
color: 'rgb(107 114 128/1)',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '.025em',
|
||||
minHeight: '40.5px !important',
|
||||
maxHeight: '40.5px !important',
|
||||
height: '40.5px !important',
|
||||
},
|
||||
'& .MuiDataGrid-virtualScroller': {
|
||||
marginTop: '40.5px !important',
|
||||
},
|
||||
'& .MuiDataGrid-columnHeaderTitle': {
|
||||
fontWeight: '600',
|
||||
fontSize: 'smaller !important',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{hasAnonymousPoll && anonymousDataGrid}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(PollsTable);
|
||||
|
Loading…
Reference in New Issue
Block a user