chore: clean diagnostics and prepare for designer structural refactor (stub legacy useActiveStudy)

This commit is contained in:
2025-08-11 16:38:29 -04:00
parent 524eff89fd
commit 779c639465
33 changed files with 5147 additions and 882 deletions

View File

@@ -1,146 +1,44 @@
"use client";
/**
* @file useActiveStudy.ts
*
* Legacy placeholder for the deprecated `useActiveStudy` hook.
*
* This file exists solely to satisfy lingering TypeScript project
* service references (e.g. editor cached import paths) after the
* migration to the unified `useSelectedStudyDetails` hook.
*
* Previous responsibilities:
* - Exposed the currently "active" study id via localStorage.
* - Partially overlapped with a separate study context implementation.
*
* Migration:
* - All consumers should now import `useSelectedStudyDetails` from:
* `~/hooks/useSelectedStudyDetails`
* - That hook centralizes selection, metadata, counts, and role info.
*
* Safe Removal:
* - Once you are certain no editors / build artifacts reference this
* path, you may delete this file. It is intentionally tiny and has
* zero runtime footprint unless mistakenly invoked.
*/
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { api } from "~/trpc/react";
const ACTIVE_STUDY_KEY = "hristudio-active-study";
// Helper function to validate UUID format
const isValidUUID = (id: string): boolean => {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(id);
};
export function useActiveStudy() {
const { data: session } = useSession();
const [activeStudyId, setActiveStudyId] = useState<string | null>(null);
const [isSettingActiveStudy, setIsSettingActiveStudy] = useState(false);
// Load active study from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(ACTIVE_STUDY_KEY);
if (stored && isValidUUID(stored)) {
setActiveStudyId(stored);
} else if (stored) {
// Clear invalid UUID from localStorage
localStorage.removeItem(ACTIVE_STUDY_KEY);
}
}, []);
// Get active study details
const { data: activeStudy, isLoading: isLoadingActiveStudy } =
api.studies.get.useQuery(
{ id: activeStudyId! },
{
enabled: !!activeStudyId && isValidUUID(activeStudyId),
staleTime: 5 * 60 * 1000, // 5 minutes
retry: false, // Don't retry if study doesn't exist
},
);
// Clear localStorage if study doesn't exist
useEffect(() => {
if (activeStudyId && !activeStudy && !isLoadingActiveStudy) {
localStorage.removeItem(ACTIVE_STUDY_KEY);
setActiveStudyId(null);
toast.error(
"Selected study no longer exists. Please select a new study.",
);
}
}, [activeStudy, activeStudyId, isLoadingActiveStudy]);
// Get user's studies for switching (always use memberOnly: true for security)
const { data: studiesData, isLoading: isLoadingStudies } =
api.studies.list.useQuery(
{ limit: 20, memberOnly: true },
{
staleTime: 2 * 60 * 1000, // 2 minutes
enabled: !!session?.user?.id,
},
);
const userStudies = studiesData?.studies ?? [];
const utils = api.useUtils();
const setActiveStudy = (studyId: string) => {
if (!isValidUUID(studyId)) {
toast.error("Invalid study ID format");
return;
}
setIsSettingActiveStudy(true);
setActiveStudyId(studyId);
localStorage.setItem(ACTIVE_STUDY_KEY, studyId);
// Invalidate all related queries when study changes
void utils.participants.invalidate();
void utils.trials.invalidate();
void utils.experiments.invalidate();
toast.success("Active study updated");
// Reset loading state after a brief delay to allow queries to refetch
setTimeout(() => {
setIsSettingActiveStudy(false);
}, 1000);
};
const clearActiveStudy = () => {
setIsSettingActiveStudy(true);
setActiveStudyId(null);
localStorage.removeItem(ACTIVE_STUDY_KEY);
// Invalidate all related queries when clearing study
void utils.participants.invalidate();
void utils.trials.invalidate();
void utils.experiments.invalidate();
toast.success("Active study cleared");
// Reset loading state after a brief delay
setTimeout(() => {
setIsSettingActiveStudy(false);
}, 500);
};
// Note: Auto-selection removed to require manual study selection
return {
// State
activeStudyId,
activeStudy:
activeStudy && typeof activeStudy === "object"
? {
id: activeStudy.id,
title: (activeStudy as { name?: string }).name ?? "",
description:
(activeStudy as { description?: string }).description ?? "",
}
: null,
userStudies: userStudies.map(
(study: { id: string; name: string; description?: string | null }) => ({
id: study.id,
title: study.name,
description: study.description ?? "",
}),
),
// Loading states
isLoadingActiveStudy,
isLoadingStudies,
isSettingActiveStudy,
isClearingActiveStudy: false,
// Actions
setActiveStudy,
clearActiveStudy,
// Utilities
hasActiveStudy: !!activeStudyId,
hasStudies: userStudies.length > 0,
};
/**
* @deprecated Use `useSelectedStudyDetails()` instead.
* Legacy no-op placeholder retained only to satisfy stale references.
* Returns a neutral object so accidental invocations are harmless.
*/
export function useActiveStudy(): DeprecatedActiveStudyHookReturn {
return { studyId: null };
}
/**
* Type alias maintained for backward compatibility with (now removed)
* code that might have referenced the old hook's return type.
* Kept minimal on purpose.
*/
export interface DeprecatedActiveStudyHookReturn {
/** Previously the active study id (now: studyId in useSelectedStudyDetails) */
studyId: string | null;
}
export default useActiveStudy;

View File

@@ -0,0 +1,123 @@
import { useCallback, useMemo } from "react";
import { api } from "~/trpc/react";
import { useStudyContext } from "~/lib/study-context";
/**
* useSelectedStudyDetails
*
* Strongly typed unified source of truth for the currently selected study.
*
* Provides a single hook to retrieve:
* - selected study id
* - lightweight summary counts
* - role + createdAt
* - loading / fetching flags
* - mutation helpers
*/
interface StudyRelatedEntity {
id: string;
}
interface StudyMember {
id: string;
userId?: string;
role?: string;
}
interface StudyDetails {
id: string;
name: string;
description: string | null;
status: string;
experiments?: StudyRelatedEntity[];
participants?: StudyRelatedEntity[];
members?: StudyMember[];
userRole?: string;
createdAt?: Date;
}
export interface StudySummary {
id: string;
name: string;
description: string;
status: string;
experimentCount: number;
participantCount: number;
memberCount: number;
userRole?: string;
createdAt?: Date;
}
export interface UseSelectedStudyDetailsReturn {
studyId: string | null;
study: StudySummary | null;
isLoading: boolean;
isFetching: boolean;
refetch: () => Promise<unknown>;
setStudyId: (id: string | null) => void;
clearStudy: () => void;
hasStudy: boolean;
}
export function useSelectedStudyDetails(): UseSelectedStudyDetailsReturn {
const { selectedStudyId, setSelectedStudyId } = useStudyContext();
const { data, isLoading, isFetching, refetch } = api.studies.get.useQuery(
{ id: selectedStudyId ?? "" },
{
enabled: !!selectedStudyId,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000,
},
);
const study: StudySummary | null = useMemo(() => {
if (!data || !selectedStudyId) return null;
// data is inferred from tRPC; we defensively narrow array fields
const typed = data as StudyDetails;
const experiments = Array.isArray(typed.experiments)
? typed.experiments
: [];
const participants = Array.isArray(typed.participants)
? typed.participants
: [];
const members = Array.isArray(typed.members) ? typed.members : [];
return {
id: typed.id,
name: typed.name ?? "Unnamed Study",
description: typed.description ?? "",
status: typed.status ?? "active",
experimentCount: experiments.length,
participantCount: participants.length,
memberCount: members.length,
userRole: typed.userRole,
createdAt: typed.createdAt,
};
}, [data, selectedStudyId]);
const setStudyId = useCallback(
(id: string | null) => {
void setSelectedStudyId(id);
},
[setSelectedStudyId],
);
const clearStudy = useCallback(() => {
void setSelectedStudyId(null);
}, [setSelectedStudyId]);
return {
studyId: selectedStudyId,
study,
isLoading,
isFetching,
refetch,
setStudyId,
clearStudy,
hasStudy: !!study,
};
}