feat: add validation for minimum length on various fields and update type definitions

This commit is contained in:
Abhimanyu Saharan
2026-02-06 16:12:04 +05:30
parent ca614328ac
commit d86fe0a7a6
157 changed files with 12340 additions and 2977 deletions

View File

@@ -2,8 +2,6 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useAuth } from "@clerk/nextjs";
import {
DialogFooter,
DialogHeader,
@@ -11,9 +9,19 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { getApiBaseUrl } from "@/lib/api-base";
const apiBase = getApiBaseUrl();
import {
answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost,
confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost,
getOnboardingApiV1BoardsBoardIdOnboardingGet,
startOnboardingApiV1BoardsBoardIdOnboardingStartPost,
} from "@/api/generated/board-onboarding/board-onboarding";
import type {
BoardOnboardingRead,
BoardOnboardingReadDraftGoal,
BoardOnboardingReadMessages,
BoardRead,
} from "@/api/generated/model";
type BoardDraft = {
board_type?: string;
@@ -22,24 +30,55 @@ type BoardDraft = {
target_date?: string | null;
};
type BoardSummary = {
id: string;
name: string;
slug: string;
board_type?: string;
objective?: string | null;
success_metrics?: Record<string, unknown> | null;
target_date?: string | null;
goal_confirmed?: boolean;
type NormalizedMessage = {
role: string;
content: string;
};
type OnboardingSession = {
id: string;
board_id: string;
session_key: string;
status: string;
messages?: Array<{ role: string; content: string }> | null;
draft_goal?: BoardDraft | null;
const normalizeMessages = (
value?: BoardOnboardingReadMessages,
): NormalizedMessage[] | null => {
if (!value) return null;
if (!Array.isArray(value)) return null;
const items: NormalizedMessage[] = [];
for (const entry of value) {
if (!entry || typeof entry !== "object") continue;
const raw = entry as Record<string, unknown>;
const role = typeof raw.role === "string" ? raw.role : null;
const content = typeof raw.content === "string" ? raw.content : null;
if (!role || !content) continue;
items.push({ role, content });
}
return items.length ? items : null;
};
const normalizeDraftGoal = (value?: BoardOnboardingReadDraftGoal): BoardDraft | null => {
if (!value || typeof value !== "object") return null;
const raw = value as Record<string, unknown>;
const board_type = typeof raw.board_type === "string" ? raw.board_type : undefined;
const objective =
typeof raw.objective === "string" ? raw.objective : raw.objective === null ? null : undefined;
const target_date =
typeof raw.target_date === "string"
? raw.target_date
: raw.target_date === null
? null
: undefined;
let success_metrics: Record<string, unknown> | null = null;
if (raw.success_metrics === null || raw.success_metrics === undefined) {
success_metrics = null;
} else if (typeof raw.success_metrics === "object") {
success_metrics = raw.success_metrics as Record<string, unknown>;
}
return {
board_type,
objective: objective ?? null,
success_metrics,
target_date: target_date ?? null,
};
};
type QuestionOption = { id: string; label: string };
@@ -75,7 +114,7 @@ const normalizeQuestion = (value: unknown): Question | null => {
return { question: data.question, options };
};
const parseQuestion = (messages?: Array<{ role: string; content: string }> | null) => {
const parseQuestion = (messages?: NormalizedMessage[] | null) => {
if (!messages?.length) return null;
const lastAssistant = [...messages].reverse().find((msg) => msg.role === "assistant");
if (!lastAssistant?.content) return null;
@@ -99,66 +138,52 @@ export function BoardOnboardingChat({
onConfirmed,
}: {
boardId: string;
onConfirmed: (board: BoardSummary) => void;
onConfirmed: (board: BoardRead) => void;
}) {
const { getToken } = useAuth();
const [session, setSession] = useState<OnboardingSession | null>(null);
const [session, setSession] = useState<BoardOnboardingRead | null>(null);
const [loading, setLoading] = useState(false);
const [otherText, setOtherText] = useState("");
const [error, setError] = useState<string | null>(null);
const [selectedOptions, setSelectedOptions] = useState<string[]>([]);
const question = useMemo(() => parseQuestion(session?.messages), [session]);
const draft = session?.draft_goal ?? null;
const normalizedMessages = useMemo(
() => normalizeMessages(session?.messages),
[session?.messages],
);
const question = useMemo(() => parseQuestion(normalizedMessages), [normalizedMessages]);
const draft = useMemo(() => normalizeDraftGoal(session?.draft_goal), [session?.draft_goal]);
useEffect(() => {
setSelectedOptions([]);
setOtherText("");
}, [question?.question]);
const authFetch = useCallback(
async (url: string, options: RequestInit = {}) => {
const token = await getToken();
return fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.headers ?? {}),
},
});
},
[getToken]
);
const startSession = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await authFetch(`${apiBase}/api/v1/boards/${boardId}/onboarding/start`, {
method: "POST",
body: JSON.stringify({}),
});
if (!res.ok) throw new Error("Unable to start onboarding.");
const data = (await res.json()) as OnboardingSession;
setSession(data);
const result = await startOnboardingApiV1BoardsBoardIdOnboardingStartPost(
boardId,
{},
);
if (result.status !== 200) throw new Error("Unable to start onboarding.");
setSession(result.data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to start onboarding.");
} finally {
setLoading(false);
}
}, [authFetch, boardId]);
}, [boardId]);
const refreshSession = useCallback(async () => {
try {
const res = await authFetch(`${apiBase}/api/v1/boards/${boardId}/onboarding`);
if (!res.ok) return;
const data = (await res.json()) as OnboardingSession;
setSession(data);
const result = await getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId);
if (result.status !== 200) return;
setSession(result.data);
} catch {
// ignore
}
}, [authFetch, boardId]);
}, [boardId]);
useEffect(() => {
startSession();
@@ -171,19 +196,15 @@ export function BoardOnboardingChat({
setLoading(true);
setError(null);
try {
const res = await authFetch(
`${apiBase}/api/v1/boards/${boardId}/onboarding/answer`,
const result = await answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(
boardId,
{
method: "POST",
body: JSON.stringify({
answer: value,
other_text: freeText ?? null,
}),
}
answer: value,
other_text: freeText ?? null,
},
);
if (!res.ok) throw new Error("Unable to submit answer.");
const data = (await res.json()) as OnboardingSession;
setSession(data);
if (result.status !== 200) throw new Error("Unable to submit answer.");
setSession(result.data);
setOtherText("");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to submit answer.");
@@ -191,7 +212,7 @@ export function BoardOnboardingChat({
setLoading(false);
}
},
[authFetch, boardId]
[boardId],
);
const toggleOption = useCallback((label: string) => {
@@ -213,21 +234,17 @@ export function BoardOnboardingChat({
setLoading(true);
setError(null);
try {
const res = await authFetch(
`${apiBase}/api/v1/boards/${boardId}/onboarding/confirm`,
const result = await confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost(
boardId,
{
method: "POST",
body: JSON.stringify({
board_type: draft.board_type ?? "goal",
objective: draft.objective ?? null,
success_metrics: draft.success_metrics ?? null,
target_date: draft.target_date ?? null,
}),
}
board_type: draft.board_type ?? "goal",
objective: draft.objective ?? null,
success_metrics: draft.success_metrics ?? null,
target_date: draft.target_date ?? null,
},
);
if (!res.ok) throw new Error("Unable to confirm board goal.");
const updated = await res.json();
onConfirmed(updated);
if (result.status !== 200) throw new Error("Unable to confirm board goal.");
onConfirmed(result.data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to confirm board goal.");
} finally {