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>
),
};
}