Begin plugins system

This commit is contained in:
2025-08-07 01:12:58 -04:00
parent 544207e9a2
commit 3a443d1727
53 changed files with 5873 additions and 2547 deletions

View File

@@ -50,9 +50,9 @@ interface TrialFormProps {
export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
const router = useRouter();
const { selectedStudyId } = useStudyContext();
const contextStudyId = studyId || selectedStudyId;
const contextStudyId = studyId ?? selectedStudyId;
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isDeleting] = useState(false);
const [error, setError] = useState<string | null>(null);
const form = useForm<TrialFormData>({
@@ -93,16 +93,36 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
// Set breadcrumbs
const breadcrumbs = [
{ label: "Dashboard", href: "/dashboard" },
{ label: "Trials", href: "/trials" },
...(mode === "edit" && trial
{ label: "Studies", href: "/studies" },
...(contextStudyId
? [
{
label: `Trial ${trial.sessionNumber || trial.id.slice(-8)}`,
href: `/trials/${trial.id}`,
label: "Study",
href: `/studies/${contextStudyId}`,
},
{ label: "Edit" },
{ label: "Trials", href: `/studies/${contextStudyId}/trials` },
...(mode === "edit" && trial
? [
{
label: `Trial ${trial.sessionNumber || trial.id.slice(-8)}`,
href: `/trials/${trial.id}`,
},
{ label: "Edit" },
]
: [{ label: "New Trial" }]),
]
: [{ label: "New Trial" }]),
: [
{ label: "Trials", href: "/trials" },
...(mode === "edit" && trial
? [
{
label: `Trial ${trial.sessionNumber || trial.id.slice(-8)}`,
href: `/trials/${trial.id}`,
},
{ label: "Edit" },
]
: [{ label: "New Trial" }]),
]),
];
useBreadcrumbsEffect(breadcrumbs);
@@ -112,13 +132,13 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
if (mode === "edit" && trial) {
form.reset({
experimentId: trial.experimentId,
participantId: trial.participantId || "",
participantId: trial?.participantId ?? "",
scheduledAt: trial.scheduledAt
? new Date(trial.scheduledAt).toISOString().slice(0, 16)
: "",
wizardId: trial.wizardId || undefined,
notes: trial.notes || "",
sessionNumber: trial.sessionNumber || 1,
wizardId: trial.wizardId ?? undefined,
notes: trial.notes ?? "",
sessionNumber: trial.sessionNumber ?? 1,
});
}
}, [trial, mode, form]);
@@ -138,8 +158,8 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
participantId: data.participantId,
scheduledAt: new Date(data.scheduledAt),
wizardId: data.wizardId,
sessionNumber: data.sessionNumber || 1,
notes: data.notes || undefined,
sessionNumber: data.sessionNumber ?? 1,
notes: data.notes ?? undefined,
});
router.push(`/trials/${newTrial!.id}`);
} else {
@@ -147,8 +167,8 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
id: trialId!,
scheduledAt: new Date(data.scheduledAt),
wizardId: data.wizardId,
sessionNumber: data.sessionNumber || 1,
notes: data.notes || undefined,
sessionNumber: data.sessionNumber ?? 1,
notes: data.notes ?? undefined,
});
router.push(`/trials/${updatedTrial!.id}`);
}
@@ -244,7 +264,7 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
<SelectContent>
{participantsData?.participants?.map((participant) => (
<SelectItem key={participant.id} value={participant.id}>
{participant.name || participant.participantCode} (
{participant.name ?? participant.participantCode} (
{participant.participantCode})
</SelectItem>
))}
@@ -312,7 +332,7 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
<FormField>
<Label htmlFor="wizardId">Assigned Wizard</Label>
<Select
value={form.watch("wizardId") || "none"}
value={form.watch("wizardId") ?? "none"}
onValueChange={(value) =>
form.setValue("wizardId", value === "none" ? undefined : value)
}
@@ -329,11 +349,13 @@ export function TrialForm({ mode, trialId, studyId }: TrialFormProps) {
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No wizard assigned</SelectItem>
{usersData?.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.name} ({user.email})
</SelectItem>
))}
{usersData?.map(
(user: { id: string; name: string; email: string }) => (
<SelectItem key={user.id} value={user.id}>
{user.name} ({user.email})
</SelectItem>
),
)}
</SelectContent>
</Select>
<p className="text-muted-foreground text-xs">

View File

@@ -58,6 +58,7 @@ export type Trial = {
id: string;
name: string;
email: string;
participantCode?: string;
};
wizard: {
id: string;
@@ -119,7 +120,7 @@ function TrialActionsCell({ trial }: { trial: Trial }) {
};
const handleCopyId = () => {
navigator.clipboard.writeText(trial.id);
void navigator.clipboard.writeText(trial.id);
toast.success("Trial ID copied to clipboard");
};
@@ -301,7 +302,7 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<Badge
variant="outline"
className="ml-auto shrink-0 border-amber-200 bg-amber-50 text-amber-700"
title={`Access restricted - You are an ${trial.userRole || "observer"} on this study`}
title={`Access restricted - You are an ${trial.userRole ?? "observer"} on this study`}
>
{trial.userRole === "observer" ? "View Only" : "Restricted"}
</Badge>
@@ -317,9 +318,9 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Status" />
),
cell: ({ row }) => {
const status = row.getValue("status") as Trial["status"];
const status = row.getValue("status");
const trial = row.original;
const config = statusConfig[status];
const config = statusConfig[status as keyof typeof statusConfig];
return (
<div className="flex flex-col gap-1">
@@ -343,7 +344,7 @@ export const trialsColumns: ColumnDef<Trial>[] = [
);
},
filterFn: (row, id, value: string[]) => {
const status = row.getValue(id) as string;
const status = row.getValue(id) as string; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
return value.includes(status);
},
},
@@ -353,16 +354,22 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Participant" />
),
cell: ({ row }) => {
const participant = row.getValue("participant") as Trial["participant"];
const participant = row.original.participant;
return (
<div className="max-w-[120px]">
<div className="flex items-center space-x-1">
<User className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<span
className="truncate text-sm font-medium"
title={participant.name || "Unnamed Participant"}
title={
participant?.name ??
participant?.participantCode ??
"Unnamed Participant"
}
>
{participant.name || "Unnamed Participant"}
{participant?.name ??
participant?.participantCode ??
"Unnamed Participant"}
</span>
</div>
</div>
@@ -376,16 +383,16 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Experiment" />
),
cell: ({ row }) => {
const experiment = row.getValue("experiment") as Trial["experiment"];
const experiment = row.original.experiment;
return (
<div className="flex max-w-[140px] items-center space-x-2">
<FlaskConical className="text-muted-foreground h-3 w-3 flex-shrink-0" />
<Link
href={`/experiments/${experiment.id}`}
href={`/experiments/${experiment?.id ?? ""}`}
className="truncate text-sm hover:underline"
title={experiment.name || "Unnamed Experiment"}
title={experiment?.name ?? "Unnamed Experiment"}
>
{experiment.name || "Unnamed Experiment"}
{experiment?.name ?? "Unnamed Experiment"}
</Link>
</div>
);
@@ -402,7 +409,7 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Wizard" />
),
cell: ({ row }) => {
const wizard = row.getValue("wizard") as Trial["wizard"];
const wizard = row.original.wizard;
if (!wizard) {
return (
<span className="text-muted-foreground text-sm">Not assigned</span>
@@ -418,9 +425,9 @@ export const trialsColumns: ColumnDef<Trial>[] = [
</div>
<div
className="text-muted-foreground truncate text-xs"
title={wizard.email}
title={wizard.email ?? ""}
>
{wizard.email}
{wizard.email ?? ""}
</div>
</div>
);
@@ -437,7 +444,7 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Scheduled" />
),
cell: ({ row }) => {
const date = row.getValue("scheduledAt") as Date | null;
const date = row.getValue("scheduledAt") as Date | null; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
if (!date) {
return (
<span className="text-muted-foreground text-sm">Not scheduled</span>
@@ -527,7 +534,7 @@ export const trialsColumns: ColumnDef<Trial>[] = [
<DataTableColumnHeader column={column} title="Created" />
),
cell: ({ row }) => {
const date = row.getValue("createdAt") as Date;
const date = row.getValue("createdAt") as Date; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
return (
<div className="text-sm whitespace-nowrap">
{formatDistanceToNow(date, { addSuffix: true })}

View File

@@ -59,10 +59,22 @@ export function TrialsDataTable() {
return () => clearInterval(interval);
}, [refetch]);
// Get study data for breadcrumbs
const { data: studyData } = api.studies.get.useQuery(
{ id: selectedStudyId! },
{ enabled: !!selectedStudyId },
);
// Set breadcrumbs
useBreadcrumbsEffect([
{ label: "Dashboard", href: "/dashboard" },
{ label: "Trials" },
{ label: "Studies", href: "/studies" },
...(selectedStudyId && studyData
? [
{ label: studyData.name, href: `/studies/${selectedStudyId}` },
{ label: "Trials" },
]
: [{ label: "Trials" }]),
]);
// Transform trials data to match the Trial type expected by columns
@@ -149,7 +161,7 @@ export function TrialsDataTable() {
const filters = (
<div className="flex items-center space-x-2">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[140px]">
<SelectTrigger className="h-8 w-[140px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
@@ -222,10 +234,10 @@ export function TrialsDataTable() {
Limited Trial Access
</h3>
<p className="mt-1 text-sm text-amber-700">
Some trials are marked as "View Only" or "Restricted" because
you have observer-level access to their studies. Only
researchers, wizards, and study owners can view detailed trial
information.
Some trials are marked as &ldquo;View Only&rdquo; or
&ldquo;Restricted&rdquo; because you have observer-level
access to their studies. Only researchers, wizards, and study
owners can view detailed trial information.
</p>
</div>
</div>