Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 34 additions & 23 deletions __tests__/components/ui/coaching-session-selector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ vi.mock('next/navigation', () => ({
}))

vi.mock('@/lib/api/coaching-sessions', () => ({
useCoachingSessionList: vi.fn(),
useEnrichedCoachingSessionsForUser: vi.fn(),
CoachingSessionInclude: {
Relationship: 'relationship',
Organization: 'organization',
Goal: 'goal',
Agreements: 'agreements',
},
}))

vi.mock('@/lib/hooks/use-current-coaching-session', () => ({
Expand All @@ -31,7 +37,7 @@ vi.mock('@/lib/api/overarching-goals', () => ({

vi.mock('@/lib/providers/auth-store-provider', () => ({
useAuthStore: vi.fn(() => ({
userSession: { timezone: 'America/Chicago' },
userSession: { id: 'user-123', timezone: 'America/Chicago' },
})),
AuthStoreProvider: ({ children }: { children: React.ReactNode }) => children,
}))
Expand All @@ -45,7 +51,7 @@ vi.mock('@/types/general', () => ({
getDateTimeFromString: (dateStr: string) => DateTime.fromISO(dateStr),
}))

import { useCoachingSessionList } from '@/lib/api/coaching-sessions'
import { useEnrichedCoachingSessionsForUser } from '@/lib/api/coaching-sessions'

describe('CoachingSessionSelector - Sorting & Grouping', () => {
const relationshipId = 'rel-123'
Expand All @@ -55,29 +61,31 @@ describe('CoachingSessionSelector - Sorting & Grouping', () => {
})

it('should call API with correct sorting parameters (date desc)', () => {
vi.mocked(useCoachingSessionList).mockReturnValue({
coachingSessions: [],
vi.mocked(useEnrichedCoachingSessionsForUser).mockReturnValue({
enrichedSessions: [],
isLoading: false,
isError: false,
refresh: vi.fn(),
})

render(
<TestProviders>
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
/>
</TestProviders>
)

// Verify backend sorting is requested: date desc (newest first)
expect(useCoachingSessionList).toHaveBeenCalledWith(
relationshipId,
expect(useEnrichedCoachingSessionsForUser).toHaveBeenCalledWith(
'user-123', // userId
expect.any(Object), // fromDate
expect.any(Object), // toDate
expect.any(Object), // toDate
expect.any(Array), // include
'date', // sortBy
'desc' // sortOrder - newest first
'desc', // sortOrder - newest first
relationshipId // relationshipId
)
})

Expand All @@ -88,26 +96,28 @@ describe('CoachingSessionSelector - Sorting & Grouping', () => {
id: 'upcoming-1',
date: now.plus({ days: 1 }).toISO(),
coaching_relationship_id: relationshipId,
overarching_goal: { title: 'Upcoming Goal' },
},
{
id: 'previous-1',
date: now.minus({ days: 1 }).toISO(),
coaching_relationship_id: relationshipId,
overarching_goal: { title: 'Previous Goal' },
},
]

vi.mocked(useCoachingSessionList).mockReturnValue({
coachingSessions: sessions,
vi.mocked(useEnrichedCoachingSessionsForUser).mockReturnValue({
enrichedSessions: sessions,
isLoading: false,
isError: false,
refresh: vi.fn(),
})

render(
<TestProviders>
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
/>
</TestProviders>
)
Expand All @@ -128,21 +138,22 @@ describe('CoachingSessionSelector - Sorting & Grouping', () => {
id: 'upcoming-1',
date: now.plus({ days: 1 }).toISO(),
coaching_relationship_id: relationshipId,
overarching_goal: { title: 'Test Goal' },
},
]

vi.mocked(useCoachingSessionList).mockReturnValue({
coachingSessions: sessions,
vi.mocked(useEnrichedCoachingSessionsForUser).mockReturnValue({
enrichedSessions: sessions,
isLoading: false,
isError: false,
refresh: vi.fn(),
})

render(
<TestProviders>
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
<CoachingSessionSelector
relationshipId={relationshipId}
disabled={false}
/>
</TestProviders>
)
Expand All @@ -156,4 +167,4 @@ describe('CoachingSessionSelector - Sorting & Grouping', () => {
expect(option).toHaveClass('pl-8') // Indentation class
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ describe('EditorCacheProvider', () => {
it('should create extensions only once even if synced event fires multiple times', async () => {
const { Extensions } = await import('@/components/ui/coaching-sessions/coaching-notes/extensions')

// Clear any previous calls to Extensions from other tests
vi.mocked(Extensions).mockClear()

// Create a mock provider that we can control
const { TiptapCollabProvider } = await import('@hocuspocus/provider')
let syncedCallback: (() => void) | undefined
Expand Down
135 changes: 93 additions & 42 deletions src/components/ui/coaching-session-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { getDateTimeFromString, Id } from "@/types/general";
import { useCoachingSessionList } from "@/lib/api/coaching-sessions";
import {
useEnrichedCoachingSessionsForUser,
CoachingSessionInclude,
} from "@/lib/api/coaching-sessions";
import { useOverarchingGoalBySession } from "@/lib/api/overarching-goals";
import { useCurrentCoachingSession } from "@/lib/hooks/use-current-coaching-session";
import { DateTime } from "ts-luxon";
import { CoachingSession } from "@/types/coaching-session";
import {
useOverarchingGoalBySession,
useOverarchingGoalList,
} from "@/lib/api/overarching-goals";
import type { EnrichedCoachingSession } from "@/types/coaching-session";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/lib/providers/auth-store-provider";
import {
Expand All @@ -37,23 +39,62 @@ function CoachingSessionsSelectItems({
}: {
relationshipId: Id | null;
}) {
const { userSession } = useAuthStore((state) => ({
userSession: state.userSession,
}));
const userId = userSession?.id;

const fromDate = DateTime.now().minus({ month: 1 });
const toDate = DateTime.now().plus({ month: 1 });

const {
coachingSessions,
isLoading: isLoadingSessions,
isError: isErrorSessions,
} = useCoachingSessionList(relationshipId, fromDate, toDate, 'date', 'desc');
const { enrichedSessions, isLoading, isError } =
useEnrichedCoachingSessionsForUser(
userId ?? null,
fromDate,
toDate,
[CoachingSessionInclude.Goal],
"date",
"desc",
relationshipId ?? undefined
);

// Early return if no relationship - component will be disabled anyway
if (!relationshipId) {
return <div>Select a coaching relationship</div>;
return (
<div className="p-2 text-sm text-muted-foreground">
Select a coaching relationship
</div>
);
}

if (isLoadingSessions) return <div>Loading...</div>;
if (isErrorSessions) return <div>Error loading coaching sessions</div>;
if (!coachingSessions?.length) return <div>No coaching sessions found</div>;
if (isLoading || !userId) {
return (
<div className="space-y-3 p-2">
{[1, 2, 3].map((i) => (
<div key={i} className="flex flex-col space-y-1.5 pl-8">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-20" />
</div>
))}
</div>
);
}

if (isError) {
return (
<div className="p-2 text-sm text-destructive">
Error loading coaching sessions
</div>
);
}

if (!enrichedSessions.length) {
return (
<div className="p-2 text-sm text-muted-foreground">
No coaching sessions found
</div>
);
}

return (
<>
Expand All @@ -70,13 +111,17 @@ function CoachingSessionsSelectItems({
},
].map(
({ label, condition }) =>
coachingSessions.some((session) => condition(session.date)) && (
enrichedSessions.some((session) => condition(session.date)) && (
<SelectGroup key={label}>
<SelectLabel>{label}</SelectLabel>
{coachingSessions
{enrichedSessions
.filter((session) => condition(session.date))
.map((session) => (
<SessionItemWithGoal key={session.id} session={session} />
<SessionItem
key={session.id}
session={session}
timezone={userSession?.timezone}
/>
))}
</SelectGroup>
)
Expand All @@ -85,27 +130,25 @@ function CoachingSessionsSelectItems({
);
}

// Separate component to handle individual session goal fetching
function SessionItemWithGoal({ session }: { session: CoachingSession }) {
const { overarchingGoal, isLoading, isError } = useOverarchingGoalBySession(
session.id
);
const { userSession } = useAuthStore((state) => state);

if (isLoading) return <div>Loading goal...</div>;
if (isError) return <div>Error loading goal</div>;

// Session item that displays goal from enriched session data (no separate API call)
function SessionItem({
session,
timezone,
}: {
session: EnrichedCoachingSession;
timezone?: string;
}) {
return (
<SelectItem value={session.id} className="pl-8">
<div className="flex min-w-0 ml-4">
<div className="min-w-0 w-full">
<p className="truncate text-sm font-medium">
{overarchingGoal.title || "No goal set"}
{session.overarching_goal?.title || "No goal set"}
</p>
<p className="truncate text-sm text-gray-400">
{formatDateInUserTimezone(
session.date,
userSession.timezone || getBrowserTimezone()
timezone || getBrowserTimezone()
)}
</p>
</div>
Expand All @@ -118,7 +161,7 @@ export default function CoachingSessionSelector({
relationshipId,
disabled,
onSelect,
...props
..._props
}: CoachingSessionsSelectorProps) {
const router = useRouter();

Expand All @@ -128,11 +171,8 @@ export default function CoachingSessionSelector({

const { userSession } = useAuthStore((state) => state);

const {
overarchingGoal,
isLoading: isLoadingGoal,
isError: isErrorGoal,
} = useOverarchingGoalBySession(currentCoachingSessionId || "");
const { overarchingGoal, isLoading: isLoadingGoal } =
useOverarchingGoalBySession(currentCoachingSessionId || "");

const handleSetCoachingSession = (coachingSessionId: Id) => {
// Navigate to the coaching session page
Expand All @@ -145,18 +185,29 @@ export default function CoachingSessionSelector({

const displayValue = isLoadingSession ? (
<div className="flex flex-col w-full">
<span className="truncate text-left">Loading session...</span>
<span className="flex items-center gap-2 text-left">
<Spinner className="size-3" />
<span className="truncate">Loading session...</span>
</span>
</div>
) : currentCoachingSession ? (
<div className="flex flex-col w-full">
<span className="truncate text-left">
{isLoadingGoal ? "Loading..." : overarchingGoal?.title || "No goal set"}
{isLoadingGoal ? (
<span className="flex items-center gap-2">
<Spinner className="size-3" />
<span>Loading goal...</span>
</span>
) : (
overarchingGoal?.title || "No goal set"
)}
</span>
<span className="text-sm text-gray-500 text-left truncate">
{currentCoachingSession.date && formatDateInUserTimezone(
currentCoachingSession.date,
userSession.timezone || getBrowserTimezone()
)}
{currentCoachingSession.date &&
formatDateInUserTimezone(
currentCoachingSession.date,
userSession.timezone || getBrowserTimezone()
)}
</span>
</div>
) : undefined;
Expand Down
Loading