fix(exam,result): collapse row actions into a dropdown menu everywhere

Same treatment as the questions table, applied consistently: exams list,
exam results list, and the exam-detail candidates panel all now show a
single Actions button opening a dropdown instead of a row of inline
buttons/icons. Also fixes the exams list's action column header, which was
rendering the literal untranslated key ('exam.columns.actions') — missing
a fallback string, same pattern already used elsewhere in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
mihretue
2026-08-18 11:40:13 +00:00
parent 896a19bd5d
commit 80db201676
3 changed files with 145 additions and 136 deletions

View File

@@ -1,5 +1,5 @@
import { Badge, Button, Text } from '@mantine/core';
import { IconRefresh, IconUserCheck } from '@tabler/icons-react';
import { Badge, Button, Menu, Text } from '@mantine/core';
import { IconChevronDown, 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';
@@ -86,39 +86,45 @@ export function examCandidateColumns(
const attemptStatus = row.original.attempt?.status;
const canRegrade = attemptStatus === 'SUBMITTED' || attemptStatus === 'EXPIRED';
return (
<>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
hideOnly
>
<Menu shadow="md" width={180} position="bottom-end">
<Menu.Target>
<Button
size="compact-xs"
variant="light"
leftSection={<IconUserCheck size={12} />}
onClick={() => handlers.onRecord(row.original)}
mr={canRegrade ? 6 : 0}
rightSection={<IconChevronDown size={12} />}
loading={handlers.regrading === row.original.attempt?.id}
>
{t('exam.candidates.record')}
{t('exam.candidates.record', 'Actions')}
</Button>
</RequirePermission>
{canRegrade && (
</Menu.Target>
<Menu.Dropdown>
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]}
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
hideOnly
>
<Button
size="compact-xs"
variant="light"
color="grape"
leftSection={<IconRefresh size={12} />}
loading={handlers.regrading === row.original.attempt?.id}
onClick={() => handlers.onRegrade(row.original)}
<Menu.Item
leftSection={<IconUserCheck size={14} />}
onClick={() => handlers.onRecord(row.original)}
>
{t('exam.candidates.regrade')}
</Button>
{t('exam.candidates.record')}
</Menu.Item>
</RequirePermission>
)}
</>
{canRegrade && (
<RequirePermission
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]}
hideOnly
>
<Menu.Item
color="grape"
leftSection={<IconRefresh size={14} />}
onClick={() => handlers.onRegrade(row.original)}
>
{t('exam.candidates.regrade')}
</Menu.Item>
</RequirePermission>
)}
</Menu.Dropdown>
</Menu>
);
},
},

View File

@@ -1,5 +1,10 @@
import { ActionIcon, Group, Menu } from "@mantine/core";
import { IconEdit, IconTrash, IconDetails, IconToggleRight } from "@tabler/icons-react";
import { Button, Menu } from "@mantine/core";
import {
IconChevronDown,
IconDetails,
IconEdit,
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";
@@ -25,61 +30,55 @@ export function examActionsColumn(
},
): AdvancedColumn<Exam> {
return {
header: t("exam.columns.actions"),
header: t("exam.columns.actions", "Actions"),
align: "right",
cell: ({ row }) => (
<Group gap="xs">
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Menu shadow="md" width={160} position="bottom-end">
<Menu.Target>
<ActionIcon
variant="subtle"
color="teal"
size="sm"
loading={handlers.changingStatusId === row.original.id}
<Menu shadow="md" width={200} position="bottom-end">
<Menu.Target>
<Button
size="compact-xs"
variant="light"
rightSection={<IconChevronDown size={12} />}
loading={handlers.changingStatusId === row.original.id}
>
{t("exam.columns.actions", "Actions")}
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
leftSection={<IconDetails size={14} />}
onClick={() => handlers.onDetails(row.original)}
>
{t("exam.action.details", "Details")}
</Menu.Item>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Menu.Item
leftSection={<IconEdit size={14} />}
onClick={() => handlers.onEdit(row.original)}
>
{t("exam.action.edit", "Edit")}
</Menu.Item>
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
onClick={() => handlers.onDelete(row.original)}
>
{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)}
>
<IconToggleRight size={14} />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<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>
))}
</Menu.Dropdown>
</Menu>
<ActionIcon
variant="subtle"
color="blue"
size="sm"
onClick={() => handlers.onEdit(row.original)}
>
<IconEdit size={14} />
</ActionIcon>
<ActionIcon
variant="subtle"
color="red"
size="sm"
onClick={() => handlers.onDelete(row.original)}
>
<IconTrash size={14} />
</ActionIcon>
</RequirePermission>
<ActionIcon
variant="subtle"
color="red"
size="sm"
onClick={() => handlers.onDetails(row.original)}
>
<IconDetails size={14} />
</ActionIcon>
</Group>
{t(`exam.form.${status.toLowerCase()}`)}
</Menu.Item>
))}
</RequirePermission>
</Menu.Dropdown>
</Menu>
),
};
}

View File

@@ -1,5 +1,5 @@
import { Button, Group } from '@mantine/core';
import { IconEye, IconSend, IconTrash } from '@tabler/icons-react';
import { Button, Menu } from '@mantine/core';
import { IconChevronDown, 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';
@@ -22,62 +22,66 @@ export function resultActionsColumn(
cell: ({ row }) => {
const r = row.original;
return (
<Group gap="xs">
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Button>
</RequirePermission>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button size="compact-xs" variant="light" color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Button>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT,
]}
hideOnly
>
<Button size="compact-xs" variant="subtle" color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Button>
</RequirePermission>
)}
{r.reviewStatus === 'APPROVED' && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Button
size="compact-xs"
variant="light"
color="teal"
leftSection={<IconSend size={13} />}
onClick={() => handlers.onPublish(r)}
>
{t('result.review.publish')}
</Button>
</RequirePermission>
)}
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Button>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Button
size="xs"
variant="subtle"
color="red"
leftSection={<IconTrash size={13} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
<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>
</RequirePermission>
</Group>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<IconEye size={14} />} onClick={() => handlers.onViewDetail(r)}>
{t('result.action.viewEdit')}
</Menu.Item>
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
<Menu.Item color="yellow" onClick={() => handlers.onQc(r, 'moderate')}>
{t('result.review.moderate')}
</Menu.Item>
</RequirePermission>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Menu.Item color="blue" onClick={() => handlers.onQc(r, 'approve')}>
{t('result.review.approve')}
</Menu.Item>
</RequirePermission>
</>
)}
{(r.reviewStatus === 'APPROVED' || r.reviewStatus === 'MODERATED') && (
<RequirePermission
anyOf={[
LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT,
LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT,
]}
hideOnly
>
<Menu.Item color="orange" onClick={() => handlers.onQc(r, 'return')}>
{t('result.review.return')}
</Menu.Item>
</RequirePermission>
)}
{r.reviewStatus === 'APPROVED' && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Menu.Item
color="teal"
leftSection={<IconSend size={14} />}
onClick={() => handlers.onPublish(r)}
>
{t('result.review.publish')}
</Menu.Item>
</RequirePermission>
)}
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_EXAM_RESULT]} hideOnly>
<Menu.Divider />
<Menu.Item
color="red"
leftSection={<IconTrash size={14} />}
onClick={() => handlers.onDelete(r)}
>
{t('result.action.delete')}
</Menu.Item>
</RequirePermission>
</Menu.Dropdown>
</Menu>
);
},
};