Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New user comment table on event page #271

Merged
merged 8 commits into from
Jan 7, 2025
355 changes: 338 additions & 17 deletions components/EventCommentThreads.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,357 @@
import React from "react"
import React, { useState, useEffect, useMemo } from "react"
import { Material, sectionSplit } from "lib/material"
import { EventFull } from "lib/types"
import { Avatar } from "flowbite-react"
alasdairwilson marked this conversation as resolved.
Show resolved Hide resolved
import Link from "next/link"
import useCommentThreads from "lib/hooks/useCommentThreads"
import { useTheme } from "next-themes"
import useUsersList from "lib/hooks/useUsersList"
import {
Accordion,
AccordionSummary,
AccordionDetails,
Typography,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Tooltip,
} from "@mui/material"
import LinkIcon from "@mui/icons-material/Link"
import Stack from "./ui/Stack"
import Thread from "./content/Thread"
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"
import ExpandLessIcon from "@mui/icons-material/ExpandLess"
import { CommentThread } from "pages/api/commentThread/[commentThreadId]"

const noSectionKey = "No Section found (material changed?)"

// group threads by section
const groupThreadsBySection = (threads: CommentThread[], material: Material) => {
const grouped: { [key: string]: CommentThread[] } = {}
threads.forEach((thread: CommentThread) => {
const { section } = sectionSplit(thread.section, material)
if (section && "name" in section) {
if (!grouped[section.name]) {
grouped[section.name] = []
}
grouped[section.name].push(thread)
} else {
if (!grouped[noSectionKey]) {
grouped[noSectionKey] = []
}
grouped[noSectionKey].push(thread)
}
})
return grouped
}

type Props = {
event: EventFull
material: Material
}

const EventCommentThreads: React.FC<Props> = ({ material, event }) => {
const [activeThreadId, setActiveThreadId] = useState<number | undefined>(undefined)
const { commentThreads, error: threadsError, isLoading: threadsIsLoading } = useCommentThreads(event.id)
if (threadsIsLoading) return <div>loading...</div>
if (!commentThreads) return <div>failed to load</div>
const [expandedSections, setExpandedSections] = useState<{ [key: string]: boolean }>({})
const { theme: currentTheme } = useTheme()

// Memoize the emails array to prevent unnecessary re-renders
const emails = useMemo(() => {
return commentThreads?.map((thread) => thread.createdByEmail) || []
}, [commentThreads])

const { users, error: usersError } = useUsersList(emails)

useEffect(() => {
if (commentThreads) {
const unresolvedSectionNames = Object.keys(groupedUnresolvedThreads)
const resolvedSectionNames = Object.keys(groupedResolvedThreads)

const initialExpandedState: { [key: string]: boolean } = {}

unresolvedSectionNames.forEach((sectionName) => {
if (sectionName !== noSectionKey) {
initialExpandedState[sectionName] = true
}
})

resolvedSectionNames.forEach((sectionName) => {
initialExpandedState[sectionName] = true
})

// ensure noSectionKey is collapsed
initialExpandedState[noSectionKey] = false

setExpandedSections(initialExpandedState)
}
}, [commentThreads]) // Only run when commentThreads change

if (threadsIsLoading) return <div>Loading...</div>
if (!commentThreads) return <div>Failed to load</div>

const unresolvedThreads = commentThreads.filter((thread) => !thread.resolved)
const resolvedThreads = commentThreads.filter((thread) => thread.resolved)

const groupedUnresolvedThreads: { [key: string]: typeof unresolvedThreads } = groupThreadsBySection(
unresolvedThreads,
material
)
const groupedResolvedThreads: { [key: string]: typeof resolvedThreads } = groupThreadsBySection(
resolvedThreads,
material
)

const toggleSection = (sectionName: string) => {
setExpandedSections((prevState) => ({
...prevState,
[sectionName]: !prevState[sectionName],
}))
}

return (
<div>
<ul className="list-disc text-gray-800 dark:text-gray-300">
{unresolvedThreads.map((thread) => {
const { theme, course, section, url } = sectionSplit(thread.section, material)
const urlWithAnchor = url + `#comment-thread-${thread.id}`
return (
<li key={thread.id}>
<Link href={urlWithAnchor}>
<span className="font-bold">{thread.section}:</span> {thread.createdByEmail}:{" "}
{thread.Comment.length > 0 ? thread.Comment[0].markdown : ""}
</Link>
</li>
)
})}
</ul>
<Accordion defaultExpanded>
<AccordionSummary expandIcon={<ExpandMoreIcon />} data-cy="unresolved-threads-expand">
<Typography variant="h6">Unresolved Comment Threads</Typography>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper}>
<Table aria-label="unresolved threads table" size="small">
<TableHead>
<TableRow>
<TableCell colSpan={3}>
<Typography variant="body1" fontWeight="bold">
Section
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(groupedUnresolvedThreads).map((sectionName) => {
const isExpanded = expandedSections[sectionName] || false
const firstThread = groupedUnresolvedThreads[sectionName][0]
const { url } = sectionSplit(firstThread.section, material)

return (
<React.Fragment key={sectionName}>
<TableRow
onClick={() => toggleSection(sectionName)}
style={{ cursor: "pointer" }}
data-cy={`section:${sectionName}:unresolved`}
>
<TableCell
colSpan={3}
sx={{
backgroundColor: (theme) => (theme.palette.mode === "dark" ? "#000000" : "#e5e7eb"),
}}
>
<Typography variant="body1" fontWeight="bold">
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />} {sectionName}
<LinkIcon
onClick={(e) => {
e.stopPropagation() // Prevent row click event from firing
window.open(url, "_blank") // Open the link in a new tab
}}
style={{ marginLeft: 8, cursor: "pointer" }} // Adjust styling as needed
/>
</Typography>
</TableCell>
</TableRow>

{isExpanded && (
<>
<TableRow>
<TableCell sx={{ width: "25%" }}>
<strong>Created By</strong>
</TableCell>
<TableCell sx={{ width: "75%" }}>
<strong>Comment</strong>
</TableCell>
<TableCell sx={{ width: "10%" }}>
<strong>Expand</strong>
</TableCell>
</TableRow>
{groupedUnresolvedThreads[sectionName].map((thread) => {
const urlWithAnchor = url + `#comment-thread-${thread.id}`

const commentText =
thread.Comment &&
thread.Comment.length > 0 &&
typeof thread.Comment[0].markdown === "string"
? thread.Comment[0].markdown
: "[Invalid Content]"

return (
<TableRow key={thread.id}>
<TableCell sx={{ width: "25%" }}>
<Stack direction="row" spacing={1}>
<Avatar
className="pr-2"
size="xs"
rounded
img={users[thread.createdByEmail]?.image || undefined}
/>
<>{thread.createdByEmail}</>
</Stack>
</TableCell>
<TableCell sx={{ width: "75%" }}>
<Link href={urlWithAnchor}>
<Typography variant="body1">
<Tooltip title="Go to comment thread">
<span>{commentText}</span>
</Tooltip>
</Typography>
</Link>
</TableCell>
<TableCell sx={{ width: "25%" }}>
<Thread
key={thread.id}
thread={thread.id}
active={activeThreadId === thread.id}
setActive={(active: boolean) =>
active ? setActiveThreadId(thread.id) : setActiveThreadId(undefined)
}
finaliseThread={() => {}}
onDelete={() => {}}
/>
</TableCell>
</TableRow>
)
})}
</>
)}
</React.Fragment>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>

<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />} data-cy="resolved-threads-expand">
<Typography variant="h6">Resolved Comment Threads</Typography>
</AccordionSummary>
<AccordionDetails>
<TableContainer component={Paper}>
<Table aria-label="resolved threads table" size="small">
<TableHead>
<TableRow>
<TableCell colSpan={3}>
<Typography variant="body1" fontWeight="bold">
Section Name
</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(groupedResolvedThreads).map((sectionName) => {
const isExpanded = expandedSections[sectionName] || false
const firstThread = groupedResolvedThreads[sectionName][0]
const { url } = sectionSplit(firstThread.section, material)

return (
<React.Fragment key={sectionName}>
<TableRow
onClick={() => toggleSection(sectionName)}
style={{ cursor: "pointer" }}
data-cy={`section:${sectionName}:resolved`}
>
<TableCell
colSpan={3}
sx={{
backgroundColor: (theme) => (theme.palette.mode === "dark" ? "#000000" : "#e5e7eb"),
}}
>
<Typography variant="body1" fontWeight="bold">
{isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />} {sectionName}
<LinkIcon
onClick={(e) => {
e.stopPropagation() // Prevent row click event from firing
window.open(url, "_blank") // Open the link in a new tab
}}
style={{ marginLeft: 8, cursor: "pointer" }} // Adjust styling as needed
/>
</Typography>
</TableCell>
</TableRow>

{isExpanded && (
<>
<TableRow>
<TableCell sx={{ width: "25%" }}>
<strong>Created By</strong>
</TableCell>
<TableCell sx={{ width: "75%" }}>
<strong>Comment</strong>
</TableCell>
<TableCell sx={{ width: "10%" }}>
<strong>Expand</strong>
</TableCell>
</TableRow>
{groupedResolvedThreads[sectionName].map((thread) => {
const urlWithAnchor = url + `#comment-thread-${thread.id}`

const commentText =
thread.Comment &&
thread.Comment.length > 0 &&
typeof thread.Comment[0].markdown === "string"
? thread.Comment[0].markdown
: "[Invalid Content]"

return (
<TableRow key={thread.id}>
<TableCell sx={{ width: "25%" }}>
<Stack direction="row" spacing={1}>
<Avatar
className="pr-2"
size="xs"
rounded
img={users[thread.createdByEmail]?.image || undefined}
/>
<>{thread.createdByEmail}</>
</Stack>
</TableCell>
<TableCell sx={{ width: "75%" }}>
<Link href={urlWithAnchor}>
<Typography variant="body1">
<Tooltip title={`Go to comment thread: ${thread.textRef}`}>
<span>{commentText}</span>
</Tooltip>
</Typography>
</Link>
</TableCell>
<TableCell sx={{ width: "10%" }}>
<Thread
key={thread.id}
thread={thread.id}
active={activeThreadId === thread.id}
setActive={(active: boolean) =>
active ? setActiveThreadId(thread.id) : setActiveThreadId(undefined)
}
finaliseThread={() => {}}
onDelete={() => {}}
/>
</TableCell>
</TableRow>
)
})}
</>
)}
</React.Fragment>
)
})}
</TableBody>
</Table>
</TableContainer>
</AccordionDetails>
</Accordion>
</div>
)
}
Expand Down
Loading