wrapper */
+.permission-column {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+
+ .permission-column > img {
+ display: block;
+ margin: 0;
+ }
+
+.assignment-participants-table tr:nth-child(even) {
+ background-color: #f9fafb;
+}
+
+.assignment-participants-table tr:hover {
+ background-color: #f3f4f6;
+}
+
+/* Status-based colors */
+.status-yes {
+ color: #10b981;
+ font-weight: bold;
+}
+
+.status-no {
+ color: #ef4444;
+ font-weight: bold;
+}
+
+.no-results-message {
+ text-align: center;
+ font-style: italic;
+ color: #999;
+ padding: 1rem;
+}
+
+
+/* Role-based colors */
+.role-student {
+ color: #2563eb;
+ font-weight: 500;
+}
+
+.role-instructor {
+ color: #10b981;
+ font-weight: 500;
+}
+
+.role-admin {
+ color: #f59e0b;
+ font-weight: 500;
+}
+
+.role-unknown {
+ color: #6b7280;
+ font-weight: 500;
+}
+
+.remove-user-button {
+ color: #ef4444;
+ font-weight: 600;
+ cursor: pointer;
+ background-color: transparent;
+ border: 1px solid transparent;
+ /* padding: 0.4em 0.8em; */
+ border-radius: 4px;
+ transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
+}
+
+.remove-user-button:hover {
+ color: #dc2626;
+ /* Darker shade for hover effect */
+ background-color: rgba(239, 68, 68, 0.1);
+ border-color: #ef4444;
+ text-decoration: none;
+}
+
+.remove-user-button:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.5);
+}
+
+
+.edit-user-button {
+ color: #1d4ed8;
+ font-weight: 600;
+ cursor: pointer;
+ background-color: transparent;
+ border: 1px solid transparent;
+ /* padding: 0.4em 0.8em; */
+ border-radius: 4px;
+ transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
+}
+
+.edit-user-button:hover {
+ color: #1e40af;
+ /* Darker shade for hover effect */
+ background-color: rgba(29, 78, 216, 0.1);
+ border-color: #1d4ed8;
+ text-decoration: none;
+}
+
+.edit-user-button:focus {
+ outline: none;
+ /* Focus ring for accessibility */
+ box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.5);
+}
+
+
+/* Checkbox Styling */
+.assignment-participants-table input[type="checkbox"] {
+ width: 1.2rem;
+ height: 1.2rem;
+ cursor: pointer;
+ accent-color: #2563eb;
+ transition: transform 0.2s ease;
+}
+
+.assignment-participants-table input[type="checkbox"]:hover {
+ transform: scale(1.1);
+}
+
+.actions-column {
+ display: flex;
+ justify-content: space-around;
+}
+
+
+@media (max-width: 768px) {
+ .assignment-participants-header {
+ font-size: 1.25rem;
+ }
+
+ .add-user-button {
+ width: 100%;
+ }
+
+ .assignment-participants-table th::before {
+ content: attr(data-label);
+ float: left;
+ font-weight: bold;
+ }
+}
diff --git a/src/pages/AssignmentParticipants/ParticipantsTable.tsx b/src/pages/AssignmentParticipants/ParticipantsTable.tsx
new file mode 100644
index 00000000..dea0adda
--- /dev/null
+++ b/src/pages/AssignmentParticipants/ParticipantsTable.tsx
@@ -0,0 +1,136 @@
+import { permissionIcon } from "./AssignmentParticipants";
+import { AssignmentProperties, Participant } from "./AssignmentParticipantsTypes";
+import { classForRole, classForStatus } from "./AssignmentParticipantsUtil";
+import "./ParticipantsTable.css";
+import { OverlayTrigger, Tooltip, Button } from "react-bootstrap";
+import Table from "components/Table/Table";
+import { publicUrl } from "utils/publicUrl";
+
+interface ParticipantTableProps {
+ participants: Participant[];
+ assignmentProps: AssignmentProperties;
+ openEditModal: (participant: Participant) => void;
+ openRemoveModal: (participant: Participant) => void;
+}
+
+/** Renders the assignment participants table with permission and action columns. */
+function ParticipantTable({
+ participants,
+ assignmentProps,
+ openEditModal,
+ openRemoveModal,
+}: ParticipantTableProps) {
+ const columns = [
+ { accessorKey: "id", header: "ID", enableSorting: true },
+ { accessorKey: "name", header: "Name", enableSorting: true },
+ { accessorKey: "email", header: "Email address", enableSorting: true },
+ {
+ accessorKey: "role",
+ header: "Role",
+ enableSorting: true,
+ cell: ({ row }: { row: any }) => (
+
+ {row.original.role}
+
+ ),
+ },
+ { accessorKey: "parent", header: "Parent", enableSorting: true },
+ {
+ accessorKey: "permissions.review",
+ id: "perm_review",
+ header: "Review",
+ enableSorting: true,
+ cell: ({ row }: { row: any }) => (
+
+ {permissionIcon(row.original.permissions.review, "Review permission")}
+
+ ),
+ },
+ {
+ accessorKey: "permissions.submit",
+ id: "perm_submit",
+ header: "Submit",
+ enableSorting: true,
+ cell: ({ row }: { row: any }) => (
+
+ {permissionIcon(row.original.permissions.submit, "Submit permission")}
+
+ ),
+ },
+ assignmentProps.hasQuiz && {
+ accessorKey: "permissions.takeQuiz",
+ id: "perm_quiz",
+ header: "Take quiz",
+ enableSorting: true,
+ cell: ({ row }: { row: any }) => (
+
+ {permissionIcon(row.original.permissions.takeQuiz)}
+
+ ),
+ },
+ assignmentProps.hasMentor && {
+ accessorKey: "permissions.mentor",
+ id: "perm_mentor",
+ header: "Mentor",
+ enableSorting: true,
+ cell: ({ row }: { row: any }) => (
+
+ {permissionIcon(row.original.permissions.mentor, "Mentor permission")}
+
+ ),
+ },
+ {
+ id: "actions",
+ header: "Actions",
+ enableSorting: false,
+ enableColumnFilter: false,
+ cell: ({ row }: { row: any }) => (
+
+
Edit participant} placement="top">
+
+
+
+
Delete participant} placement="top">
+
+
+
+ ),
+ },
+ ].filter(Boolean) as any;
+
+ return (
+
+
+
+
= 10}
+ />
+
+
+
+ );
+}
+
+export default ParticipantTable;
diff --git a/src/pages/Assignments/CreateTeams.tsx b/src/pages/Assignments/CreateTeams.tsx
index eee84265..95ed1479 100644
--- a/src/pages/Assignments/CreateTeams.tsx
+++ b/src/pages/Assignments/CreateTeams.tsx
@@ -957,6 +957,7 @@ import { useLoaderData, useNavigate } from 'react-router-dom';
import ImportModal from "../../components/Modals/ImportModal";
import ExportModal from "../../components/Modals/ExportModal";
+import { publicUrl } from "../../utils/publicUrl";
/* =============================================================================
Types
@@ -989,25 +990,7 @@ interface LoaderPayload {
Assets (icons used only where required)
============================================================================= */
-// Safe base URL (no import.meta)
-const getBaseUrl = (): string => {
- // 1) if present
- if (typeof document !== 'undefined') {
- const base = document.querySelector('base[href]') as HTMLBaseElement | null;
- if (base?.href) return base.href.replace(/\/$/, '');
- }
- // 2) Optional global you can set from Rails/layout, etc.
- const fromGlobal = (globalThis as any)?.__BASE_URL__;
- if (typeof fromGlobal === 'string' && fromGlobal) return fromGlobal.replace(/\/$/, '');
-
- // 3) CRA-style env if available in tests/builds
- const fromProcess =
- (typeof process !== 'undefined' && (process as any)?.env?.PUBLIC_URL) || '';
- return String(fromProcess).replace(/\/$/, '');
-};
-
-const assetUrl = (rel: string) =>
- `${getBaseUrl()}/${rel.replace(/^\//, '')}`;
+const assetUrl = (rel: string) => publicUrl(rel.replace(/^\//, ""));
const ICONS = {
add: 'assets/icons/add-participant-24.png',
diff --git a/src/pages/TA/TA.tsx b/src/pages/TA/TA.tsx
index d38ce406..15991e8f 100644
--- a/src/pages/TA/TA.tsx
+++ b/src/pages/TA/TA.tsx
@@ -14,6 +14,7 @@ import { ITAResponse, ROLE } from "../../utils/interfaces";
import { TAColumns as TA_COLUMNS } from "./TAColumns";
import ColumnButton from "../../components/ColumnButton";
import DeleteTA from "./TADelete";
+import { publicUrl } from "../../utils/publicUrl";
/**
* @author Atharva Thorve, on December, 2023
@@ -95,7 +96,7 @@ const TAs = () => {
onClick={() => navigate("new")}
tooltip="Add TA to this course"
icon={
}
diff --git a/src/pages/TA/TAColumns.tsx b/src/pages/TA/TAColumns.tsx
index fd7510cd..aabad7ea 100644
--- a/src/pages/TA/TAColumns.tsx
+++ b/src/pages/TA/TAColumns.tsx
@@ -4,6 +4,7 @@ import { Button } from "react-bootstrap";
import { BsPersonXFill } from "react-icons/bs";
import { ITAResponse as ITA } from "../../utils/interfaces";
import ColumnButton from "../../components/ColumnButton";
+import { publicUrl } from "../../utils/publicUrl";
/**
* @author Atharva Thorve, on December, 2023
@@ -47,7 +48,7 @@ export const TAColumns = (handleDelete: Fn) => [
onClick={() => handleDelete(row)}
tooltip="Delete TA"
icon={
}
diff --git a/src/pages/Users/userColumns.tsx b/src/pages/Users/userColumns.tsx
index b2b8dfba..04597047 100644
--- a/src/pages/Users/userColumns.tsx
+++ b/src/pages/Users/userColumns.tsx
@@ -2,6 +2,7 @@ import {createColumnHelper, Row} from "@tanstack/react-table";
import {Button, Tooltip, OverlayTrigger } from "react-bootstrap";
import {BsPencilFill, BsPersonXFill} from "react-icons/bs";
import {IUserResponse as IUser} from "../../utils/interfaces";
+import { publicUrl } from "../../utils/publicUrl";
/**
* @author Ankur Mundra on April, 2023
*/
@@ -59,7 +60,7 @@ export const userColumns = (handleEdit: Fn, handleDelete: Fn) => [
cell: (info) =>
info.getValue() ? (
@@ -81,7 +82,7 @@ export const userColumns = (handleEdit: Fn, handleDelete: Fn) => [
cell: (info) =>
info.getValue() ? (
diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts
index 5454e972..165c88dc 100644
--- a/src/utils/interfaces.ts
+++ b/src/utils/interfaces.ts
@@ -58,6 +58,16 @@ export interface IParticipantResponse {
institution: { id: number | null; name: string | null };
}
+export interface IAssignmentParticipantResponse {
+ id: number;
+ user_id: number;
+ can_mentor: boolean | null;
+ can_review: boolean | null;
+ can_submit: boolean | null;
+ can_take_quiz: boolean | null;
+ authorization: string | null;
+}
+
export interface IUserRequest {
name: string;
email: string;
@@ -249,6 +259,10 @@ export interface IAssignmentResponse {
private:boolean;
show_template_review: boolean;
require_quiz:boolean;
+ /** When true, assignment uses mentors; shows Mentor permission column on assignment participants. */
+ has_mentors?: boolean;
+ /** When true with require_quiz, supports quiz workflows; used with require_quiz for Take quiz column. */
+ has_quizzes?: boolean;
has_badge:boolean;
staggered_deadline:boolean;
is_calibrated:boolean;
diff --git a/src/utils/publicUrl.ts b/src/utils/publicUrl.ts
new file mode 100644
index 00000000..07a0c3a3
--- /dev/null
+++ b/src/utils/publicUrl.ts
@@ -0,0 +1,9 @@
+/**
+ * Absolute URL for a file under `public/`.
+ * Use instead of Create React App's `process.env.PUBLIC_URL`, which is undefined in Vite's browser bundle.
+ */
+export function publicUrl(path: string): string {
+ const base = import.meta.env.BASE_URL;
+ const suffix = path.replace(/^\//, "");
+ return `${base}${suffix}`;
+}