mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
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:
@@ -1,5 +1,5 @@
|
|||||||
import { Badge, Button, Menu, Text } from '@mantine/core';
|
import { ActionIcon, Badge, Menu, Text } from '@mantine/core';
|
||||||
import { IconChevronDown, IconRefresh, IconUserCheck } from '@tabler/icons-react';
|
import { IconDotsVertical, 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';
|
||||||
@@ -88,14 +88,14 @@ export function examCandidateColumns(
|
|||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={180} position="bottom-end">
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Button
|
<ActionIcon
|
||||||
size="compact-xs"
|
variant="subtle"
|
||||||
variant="light"
|
color="gray"
|
||||||
rightSection={<IconChevronDown size={12} />}
|
size="sm"
|
||||||
loading={handlers.regrading === row.original.attempt?.id}
|
loading={handlers.regrading === row.original.attempt?.id}
|
||||||
>
|
>
|
||||||
{t('exam.candidates.record', 'Actions')}
|
<IconDotsVertical size={16} />
|
||||||
</Button>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<RequirePermission
|
<RequirePermission
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
import { Button, Menu } from "@mantine/core";
|
import { ActionIcon, Menu } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
|
||||||
IconDetails,
|
IconDetails,
|
||||||
|
IconDotsVertical,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
|
IconToggleRight,
|
||||||
} from "@tabler/icons-react";
|
} 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";
|
||||||
import type { Exam, ExamStatus } from "../../types/exam";
|
import type { Exam } from "../../types/exam";
|
||||||
|
|
||||||
const STATUSES: ExamStatus[] = [
|
|
||||||
"PENDING",
|
|
||||||
"ACTIVE",
|
|
||||||
"COMPLETED",
|
|
||||||
"CANCELLED",
|
|
||||||
"POSTPONED",
|
|
||||||
"PUBLISHED",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function examActionsColumn(
|
export function examActionsColumn(
|
||||||
t: TFunction,
|
t: TFunction,
|
||||||
@@ -25,7 +17,7 @@ export function examActionsColumn(
|
|||||||
onEdit: (exam: Exam) => void;
|
onEdit: (exam: Exam) => void;
|
||||||
onDelete: (exam: Exam) => void;
|
onDelete: (exam: Exam) => void;
|
||||||
onDetails: (exam: Exam) => void;
|
onDetails: (exam: Exam) => void;
|
||||||
onChangeStatus: (exam: Exam, status: ExamStatus) => void;
|
onOpenStatusChange: (exam: Exam) => void;
|
||||||
changingStatusId?: string | null;
|
changingStatusId?: string | null;
|
||||||
},
|
},
|
||||||
): AdvancedColumn<Exam> {
|
): AdvancedColumn<Exam> {
|
||||||
@@ -33,16 +25,16 @@ export function examActionsColumn(
|
|||||||
header: t("exam.columns.actions", "Actions"),
|
header: t("exam.columns.actions", "Actions"),
|
||||||
align: "right",
|
align: "right",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Menu shadow="md" width={200} position="bottom-end">
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Button
|
<ActionIcon
|
||||||
size="compact-xs"
|
variant="subtle"
|
||||||
variant="light"
|
color="gray"
|
||||||
rightSection={<IconChevronDown size={12} />}
|
size="sm"
|
||||||
loading={handlers.changingStatusId === row.original.id}
|
loading={handlers.changingStatusId === row.original.id}
|
||||||
>
|
>
|
||||||
{t("exam.columns.actions", "Actions")}
|
<IconDotsVertical size={16} />
|
||||||
</Button>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -58,6 +50,12 @@ export function examActionsColumn(
|
|||||||
>
|
>
|
||||||
{t("exam.action.edit", "Edit")}
|
{t("exam.action.edit", "Edit")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
leftSection={<IconToggleRight size={14} />}
|
||||||
|
onClick={() => handlers.onOpenStatusChange(row.original)}
|
||||||
|
>
|
||||||
|
{t("exam.form.status")}
|
||||||
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
color="red"
|
color="red"
|
||||||
leftSection={<IconTrash size={14} />}
|
leftSection={<IconTrash size={14} />}
|
||||||
@@ -65,17 +63,6 @@ export function examActionsColumn(
|
|||||||
>
|
>
|
||||||
{t("exam.action.delete", "Delete")}
|
{t("exam.action.delete", "Delete")}
|
||||||
</Menu.Item>
|
</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>
|
</RequirePermission>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@@ -369,6 +369,10 @@ export function ExamPage() {
|
|||||||
const [deleteOpened, { open: openDelete, close: closeDelete }] =
|
const [deleteOpened, { open: openDelete, close: closeDelete }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
const [changingStatusId, setChangingStatusId] = useState<string | null>(null);
|
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
|
const certOptions = certifications
|
||||||
.filter((c) => c.isActive)
|
.filter((c) => c.isActive)
|
||||||
@@ -419,11 +423,14 @@ export function ExamPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChangeStatus = async (exam: Exam, status: Exam["status"]) => {
|
const handleChangeStatus = async () => {
|
||||||
setChangingStatusId(exam.id);
|
if (!statusTarget || !pendingStatus) return;
|
||||||
|
setChangingStatusId(statusTarget.id);
|
||||||
try {
|
try {
|
||||||
await updateExam({ id: exam.id, status }).unwrap();
|
await updateExam({ id: statusTarget.id, status: pendingStatus }).unwrap();
|
||||||
notify.success(t("exam.updated"));
|
notify.success(t("exam.updated"));
|
||||||
|
closeStatus();
|
||||||
|
setStatusTarget(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(e);
|
handleError(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -464,7 +471,11 @@ export function ExamPage() {
|
|||||||
openDelete();
|
openDelete();
|
||||||
},
|
},
|
||||||
onDetails: (exam) => navigate(`/exams/${exam.id}`),
|
onDetails: (exam) => navigate(`/exams/${exam.id}`),
|
||||||
onChangeStatus: handleChangeStatus,
|
onOpenStatusChange: (exam) => {
|
||||||
|
setStatusTarget(exam);
|
||||||
|
setPendingStatus(exam.status);
|
||||||
|
openStatus();
|
||||||
|
},
|
||||||
changingStatusId,
|
changingStatusId,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -541,6 +552,47 @@ export function ExamPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</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>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button, Menu } from '@mantine/core';
|
import { ActionIcon, Menu } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IconDotsVertical,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconGavel,
|
IconGavel,
|
||||||
IconSend,
|
IconSend,
|
||||||
@@ -29,14 +29,14 @@ export function questionActionsColumn(
|
|||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={180} position="bottom-end">
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Button
|
<ActionIcon
|
||||||
size="compact-xs"
|
variant="subtle"
|
||||||
variant="light"
|
color="gray"
|
||||||
rightSection={<IconChevronDown size={12} />}
|
size="sm"
|
||||||
loading={handlers.isSubmittingReview}
|
loading={handlers.isSubmittingReview}
|
||||||
>
|
>
|
||||||
{t('question.columns.actions', 'Actions')}
|
<IconDotsVertical size={16} />
|
||||||
</Button>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (
|
{(q.status === 'DRAFT' || q.status === 'REJECTED') && (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button, Menu } from '@mantine/core';
|
import { ActionIcon, Menu } from '@mantine/core';
|
||||||
import { IconChevronDown, IconEye, IconSend, IconTrash } from '@tabler/icons-react';
|
import { IconDotsVertical, 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';
|
||||||
@@ -24,9 +24,9 @@ export function resultActionsColumn(
|
|||||||
return (
|
return (
|
||||||
<Menu shadow="md" width={180} position="bottom-end">
|
<Menu shadow="md" width={180} position="bottom-end">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<Button size="compact-xs" variant="light" rightSection={<IconChevronDown size={12} />}>
|
<ActionIcon variant="subtle" color="gray" size="sm">
|
||||||
{t('result.columns.actions', 'Actions')}
|
<IconDotsVertical size={16} />
|
||||||
</Button>
|
</ActionIcon>
|
||||||
</Menu.Target>
|
</Menu.Target>
|
||||||
<Menu.Dropdown>
|
<Menu.Dropdown>
|
||||||
<Menu.Item leftSection={<IconEye size={14} />} onClick={() => handlers.onViewDetail(r)}>
|
<Menu.Item leftSection={<IconEye size={14} />} onClick={() => handlers.onViewDetail(r)}>
|
||||||
|
|||||||
Reference in New Issue
Block a user