mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-09 05:58:19 +00:00
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:
@@ -1,5 +1,5 @@
|
|||||||
import { Badge, Button, Text } from '@mantine/core';
|
import { Badge, Button, Menu, Text } from '@mantine/core';
|
||||||
import { IconRefresh, IconUserCheck } from '@tabler/icons-react';
|
import { IconChevronDown, IconRefresh, IconUserCheck } from '@tabler/icons-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||||
@@ -86,39 +86,45 @@ export function examCandidateColumns(
|
|||||||
const attemptStatus = row.original.attempt?.status;
|
const attemptStatus = row.original.attempt?.status;
|
||||||
const canRegrade = attemptStatus === 'SUBMITTED' || attemptStatus === 'EXPIRED';
|
const canRegrade = attemptStatus === 'SUBMITTED' || attemptStatus === 'EXPIRED';
|
||||||
return (
|
return (
|
||||||
<>
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
<RequirePermission
|
<Menu.Target>
|
||||||
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
|
|
||||||
hideOnly
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
variant="light"
|
variant="light"
|
||||||
leftSection={<IconUserCheck size={12} />}
|
rightSection={<IconChevronDown size={12} />}
|
||||||
onClick={() => handlers.onRecord(row.original)}
|
loading={handlers.regrading === row.original.attempt?.id}
|
||||||
mr={canRegrade ? 6 : 0}
|
|
||||||
>
|
>
|
||||||
{t('exam.candidates.record')}
|
{t('exam.candidates.record', 'Actions')}
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</Menu.Target>
|
||||||
{canRegrade && (
|
<Menu.Dropdown>
|
||||||
<RequirePermission
|
<RequirePermission
|
||||||
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_RESULT]}
|
anyOf={[LICENSE_PERMISSIONS.RECORD_EXAM_ATTENDANCE]}
|
||||||
hideOnly
|
hideOnly
|
||||||
>
|
>
|
||||||
<Button
|
<Menu.Item
|
||||||
size="compact-xs"
|
leftSection={<IconUserCheck size={14} />}
|
||||||
variant="light"
|
onClick={() => handlers.onRecord(row.original)}
|
||||||
color="grape"
|
|
||||||
leftSection={<IconRefresh size={12} />}
|
|
||||||
loading={handlers.regrading === row.original.attempt?.id}
|
|
||||||
onClick={() => handlers.onRegrade(row.original)}
|
|
||||||
>
|
>
|
||||||
{t('exam.candidates.regrade')}
|
{t('exam.candidates.record')}
|
||||||
</Button>
|
</Menu.Item>
|
||||||
</RequirePermission>
|
</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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { ActionIcon, Group, Menu } from "@mantine/core";
|
import { Button, Menu } from "@mantine/core";
|
||||||
import { IconEdit, IconTrash, IconDetails, IconToggleRight } from "@tabler/icons-react";
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconDetails,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import type { TFunction } from "i18next";
|
import type { TFunction } from "i18next";
|
||||||
import type { AdvancedColumn } from "@ema-platform/ui";
|
import type { AdvancedColumn } from "@ema-platform/ui";
|
||||||
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
|
import { LICENSE_PERMISSIONS, RequirePermission } from "@ema-platform/auth";
|
||||||
@@ -25,61 +30,55 @@ export function examActionsColumn(
|
|||||||
},
|
},
|
||||||
): AdvancedColumn<Exam> {
|
): AdvancedColumn<Exam> {
|
||||||
return {
|
return {
|
||||||
header: t("exam.columns.actions"),
|
header: t("exam.columns.actions", "Actions"),
|
||||||
align: "right",
|
align: "right",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Group gap="xs">
|
<Menu shadow="md" width={200} position="bottom-end">
|
||||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
|
<Menu.Target>
|
||||||
<Menu shadow="md" width={160} position="bottom-end">
|
<Button
|
||||||
<Menu.Target>
|
size="compact-xs"
|
||||||
<ActionIcon
|
variant="light"
|
||||||
variant="subtle"
|
rightSection={<IconChevronDown size={12} />}
|
||||||
color="teal"
|
loading={handlers.changingStatusId === row.original.id}
|
||||||
size="sm"
|
>
|
||||||
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} />
|
{t(`exam.form.${status.toLowerCase()}`)}
|
||||||
</ActionIcon>
|
</Menu.Item>
|
||||||
</Menu.Target>
|
))}
|
||||||
<Menu.Dropdown>
|
</RequirePermission>
|
||||||
<Menu.Label>{t("exam.form.status")}</Menu.Label>
|
</Menu.Dropdown>
|
||||||
{STATUSES.map((status) => (
|
</Menu>
|
||||||
<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>
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Group } from '@mantine/core';
|
import { Button, Menu } from '@mantine/core';
|
||||||
import { IconEye, IconSend, IconTrash } from '@tabler/icons-react';
|
import { IconChevronDown, IconEye, IconSend, IconTrash } from '@tabler/icons-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||||
@@ -22,62 +22,66 @@ export function resultActionsColumn(
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const r = row.original;
|
||||||
return (
|
return (
|
||||||
<Group gap="xs">
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
{(r.reviewStatus === 'MARKED' || r.reviewStatus === 'MODERATED') && (
|
<Menu.Target>
|
||||||
<>
|
<Button size="compact-xs" variant="light" rightSection={<IconChevronDown size={12} />}>
|
||||||
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MODERATE_EXAM_RESULT]} hideOnly>
|
{t('result.columns.actions', 'Actions')}
|
||||||
<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')}
|
|
||||||
</Button>
|
</Button>
|
||||||
</RequirePermission>
|
</Menu.Target>
|
||||||
</Group>
|
<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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user