fix(exam,result,question): kebab-icon action menus, status change as a picker

Two follow-ups on the dropdown-actions pass:
- All four action menus (exams, results, questions, candidates panel) now
  trigger off a plain three-dot icon button instead of a text 'Actions'
  button with a chevron.
- Exam status change no longer lists all six statuses flat in the dropdown
  (bad UX, looked like six always-visible options). 'Status' is now one
  menu item that opens a small picker modal (current status + a Select +
  Update) rather than the full edit form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mihretue
2026-08-18 11:46:16 +00:00
parent 80db201676
commit ae3af7777e
5 changed files with 95 additions and 56 deletions

View File

@@ -1,5 +1,5 @@
import { Badge, Button, Menu, Text } from '@mantine/core';
import { IconChevronDown, IconRefresh, IconUserCheck } from '@tabler/icons-react';
import { ActionIcon, Badge, Menu, Text } from '@mantine/core';
import { IconDotsVertical, IconRefresh, IconUserCheck } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
@@ -88,14 +88,14 @@ export function examCandidateColumns(
return (
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<Button
size="compact-xs"
variant="light"
rightSection={<IconChevronDown size={12} />}
<ActionIcon
variant="subtle"
color="gray"
size="sm"
loading={handlers.regrading === row.original.attempt?.id}
>
{t('exam.candidates.record', 'Actions')}
</Button>
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<RequirePermission

View File

@@ -1,23 +1,15 @@
import { Button, Menu } from "@mantine/core";
import { ActionIcon, Menu } from "@mantine/core";
import {
IconChevronDown,
IconDetails,
IconDotsVertical,
IconEdit,
IconTrash,
IconToggleRight,
} from "@tabler/icons-react";
import type { TFunction } from "i18next";
import type { AdvancedColumn } from "@ema-platform/ui";
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
import type { Exam, ExamStatus } from "../../types/exam";
const STATUSES: ExamStatus[] = [
"PENDING",
"ACTIVE",
"COMPLETED",
"CANCELLED",
"POSTPONED",
"PUBLISHED",
];
import type { Exam } from "../../types/exam";
export function examActionsColumn(
t: TFunction,
@@ -25,7 +17,7 @@ export function examActionsColumn(
onEdit: (exam: Exam) => void;
onDelete: (exam: Exam) => void;
onDetails: (exam: Exam) => void;
onChangeStatus: (exam: Exam, status: ExamStatus) => void;
onOpenStatusChange: (exam: Exam) => void;
changingStatusId?: string | null;
},
): AdvancedColumn<Exam> {
@@ -33,16 +25,16 @@ export function examActionsColumn(
header: t("exam.columns.actions", "Actions"),
align: "right",
cell: ({ row }) => (
<Menu shadow="md" width={200} position="bottom-end">
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<Button
size="compact-xs"
variant="light"
rightSection={<IconChevronDown size={12} />}
<ActionIcon
variant="subtle"
color="gray"
size="sm"
loading={handlers.changingStatusId === row.original.id}
>
{t("exam.columns.actions", "Actions")}
</Button>
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
@@ -58,6 +50,12 @@ export function examActionsColumn(
>
{t("exam.action.edit", "Edit")}
</Menu.Item>
<Menu.Item
leftSection={<IconToggleRight size={14} />}
onClick={() => handlers.onOpenStatusChange(row.original)}
>
{t("exam.form.status")}
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
@@ -65,17 +63,6 @@ export function examActionsColumn(
>
{t("exam.action.delete", "Delete")}
</Menu.Item>
<Menu.Divider />
<Menu.Label>{t("exam.form.status")}</Menu.Label>
{STATUSES.map((status) => (
<Menu.Item
key={status}
disabled={status === row.original.status}
onClick={() => handlers.onChangeStatus(row.original, status)}
>
{t(`exam.form.${status.toLowerCase()}`)}
</Menu.Item>
))}
</RequirePermission>
</Menu.Dropdown>
</Menu>

View File

@@ -369,6 +369,10 @@ export function ExamPage() {
const [deleteOpened, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [changingStatusId, setChangingStatusId] = useState<string | null>(null);
const [statusTarget, setStatusTarget] = useState<Exam | null>(null);
const [pendingStatus, setPendingStatus] = useState<Exam["status"] | null>(null);
const [statusOpened, { open: openStatus, close: closeStatus }] =
useDisclosure(false);
const certOptions = certifications
.filter((c) => c.isActive)
@@ -419,11 +423,14 @@ export function ExamPage() {
}
};
const handleChangeStatus = async (exam: Exam, status: Exam["status"]) => {
setChangingStatusId(exam.id);
const handleChangeStatus = async () => {
if (!statusTarget || !pendingStatus) return;
setChangingStatusId(statusTarget.id);
try {
await updateExam({ id: exam.id, status }).unwrap();
await updateExam({ id: statusTarget.id, status: pendingStatus }).unwrap();
notify.success(t("exam.updated"));
closeStatus();
setStatusTarget(null);
} catch (e) {
handleError(e);
} finally {
@@ -464,7 +471,11 @@ export function ExamPage() {
openDelete();
},
onDetails: (exam) => navigate(`/exams/${exam.id}`),
onChangeStatus: handleChangeStatus,
onOpenStatusChange: (exam) => {
setStatusTarget(exam);
setPendingStatus(exam.status);
openStatus();
},
changingStatusId,
}),
];
@@ -541,6 +552,47 @@ export function ExamPage() {
</Button>
</ModalFooter>
</Modal>
{/* Quick status change — not the full edit form */}
<Modal
opened={statusOpened}
onClose={closeStatus}
title={t("exam.form.status")}
size="sm"
>
<Stack gap="md">
<Text fz="sm" c="dimmed">
{statusTarget?.title?.[locale]}
</Text>
<Select
label={t("exam.form.status")}
data={[
{ value: "PENDING", label: t("exam.form.pending") },
{ value: "ACTIVE", label: t("exam.form.active") },
{ value: "COMPLETED", label: t("exam.form.completed") },
{ value: "CANCELLED", label: t("exam.form.cancelled") },
{ value: "POSTPONED", label: t("exam.form.postponed") },
{ value: "PUBLISHED", label: t("exam.form.published") },
]}
value={pendingStatus}
onChange={(value) => setPendingStatus(value as Exam["status"])}
size="sm"
/>
<ModalFooter>
<Button variant="default" onClick={closeStatus} size="sm">
{t("exam.cancel")}
</Button>
<Button
onClick={handleChangeStatus}
size="sm"
loading={changingStatusId === statusTarget?.id}
disabled={pendingStatus === statusTarget?.status}
>
{t("exam.update")}
</Button>
</ModalFooter>
</Stack>
</Modal>
</Stack>
);
}

View File

@@ -1,6 +1,6 @@
import { Button, Menu } from '@mantine/core';
import { ActionIcon, Menu } from '@mantine/core';
import {
IconChevronDown,
IconDotsVertical,
IconEdit,
IconGavel,
IconSend,
@@ -29,14 +29,14 @@ export function questionActionsColumn(
return (
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<Button
size="compact-xs"
variant="light"
rightSection={<IconChevronDown size={12} />}
<ActionIcon
variant="subtle"
color="gray"
size="sm"
loading={handlers.isSubmittingReview}
>
{t('question.columns.actions', 'Actions')}
</Button>
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (

View File

@@ -1,5 +1,5 @@
import { Button, Menu } from '@mantine/core';
import { IconChevronDown, IconEye, IconSend, IconTrash } from '@tabler/icons-react';
import { ActionIcon, Menu } from '@mantine/core';
import { IconDotsVertical, IconEye, IconSend, IconTrash } from '@tabler/icons-react';
import type { TFunction } from 'i18next';
import type { AdvancedColumn } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
@@ -24,9 +24,9 @@ export function resultActionsColumn(
return (
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<Button size="compact-xs" variant="light" rightSection={<IconChevronDown size={12} />}>
{t('result.columns.actions', 'Actions')}
</Button>
<ActionIcon variant="subtle" color="gray" size="sm">
<IconDotsVertical size={16} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEye size={14} />} onClick={() => handlers.onViewDetail(r)}>