- Convert 17 server spec files from vitest to Jest (vi -> jest globals) - Add jest.mock stubs for ESM-only prosemirror/html and collaboration modules - Fix Zod v4 strict UUID validation failures in test fixtures (version byte [1-8] required) - Add JwtAuthGuard.overrideGuard in all controller specs that lacked it - Fix jest.Mock type inference (ReturnType<typeof jest.fn> -> jest.Mock) to prevent 'never' arg errors - Delete vitest.config.ts (CJS), keep vitest.config.mts (ESM-compatible) on client - Add global mocks for @excalidraw/excalidraw and @/main.tsx in client test-setup - Result: client 38/38 suites 313/313 tests, server acadenice 21/21 suites 210/210 tests, 0 TS errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import { Modal, Stack, Text, Group, Badge, Divider, Tabs } from "@mantine/core";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useAtom } from "jotai";
|
|
import { currentUserAtom } from "@/features/user/atoms/current-user-atom";
|
|
import type { BridgeRow, BridgeField } from "../types/database-view.types";
|
|
import { RowCommentsPanel } from "@/features/acadenice/comments/components/row-comments-panel";
|
|
|
|
interface RowDetailModalProps {
|
|
row: BridgeRow | null;
|
|
fields: BridgeField[];
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* Simple row detail modal — opened when the user clicks on a calendar event.
|
|
*
|
|
* Why simple in R3.1.d:
|
|
* Full inline editing inside the modal is a larger UX investment (field-level
|
|
* save, validation, optimistic feedback). The priority here is to make the
|
|
* calendar renderer clickable and show meaningful data. Inline edit from the
|
|
* modal is slated for R3.1.e / R3.2.
|
|
*/
|
|
export function RowDetailModal({ row, fields, opened, onClose }: RowDetailModalProps) {
|
|
const { t } = useTranslation();
|
|
const [currentUser] = useAtom(currentUserAtom);
|
|
|
|
if (!row) return null;
|
|
|
|
return (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={t("database_view.row_detail.title")}
|
|
size="lg"
|
|
centered
|
|
>
|
|
<Tabs defaultValue="fields">
|
|
<Tabs.List>
|
|
<Tabs.Tab value="fields">
|
|
{t("database_view.row_detail.tab_fields")}
|
|
</Tabs.Tab>
|
|
<Tabs.Tab value="comments">
|
|
{t("database_view.row_detail.tab_comments")}
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="fields" pt="xs">
|
|
<Stack gap="xs">
|
|
{fields.map((field) => {
|
|
const rawValue = row.fields[field.name] ?? row.fields[field.id];
|
|
return (
|
|
<div key={field.id}>
|
|
<Group gap="xs" mb={2}>
|
|
<Text size="xs" fw={600} c="dimmed">
|
|
{field.name}
|
|
</Text>
|
|
{field.primary && (
|
|
<Badge size="xs" variant="light" color="blue">
|
|
{t("database_view.row_detail.primary_badge")}
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
<Text size="sm">{formatValue(rawValue)}</Text>
|
|
<Divider mt="xs" />
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{fields.length === 0 && (
|
|
<Text size="sm" c="dimmed">
|
|
{t("database_view.row_detail.no_fields")}
|
|
</Text>
|
|
)}
|
|
</Stack>
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="comments" pt="xs">
|
|
{currentUser && (
|
|
<RowCommentsPanel
|
|
tableId={String(row.tableId ?? "")}
|
|
rowId={String(row.id)}
|
|
currentUserId={currentUser.user.id}
|
|
/>
|
|
)}
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function formatValue(value: unknown): string {
|
|
if (value === null || value === undefined) return "—";
|
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
if (Array.isArray(value)) {
|
|
return value
|
|
.map((v) =>
|
|
typeof v === "object" && v !== null
|
|
? (v as { value?: string }).value ?? JSON.stringify(v)
|
|
: String(v),
|
|
)
|
|
.join(", ");
|
|
}
|
|
if (typeof value === "object") {
|
|
return (value as { value?: string }).value ?? JSON.stringify(value);
|
|
}
|
|
return String(value);
|
|
}
|