169 lines · 5.4 KB · TypeScript
RawDownload
import type { UserInputQuestion } from "@t3tools/contracts"; export interface PendingUserInputDraftAnswer {  selectedOptionValues?: string[];  customAnswer?: string;  attachmentCount?: number;  attachmentsBlocked?: boolean;} export interface PendingUserInputProgress {  questionIndex: number;  activeQuestion: UserInputQuestion | null;  activeDraft: PendingUserInputDraftAnswer | undefined;  selectedOptionValues: string[];  customAnswer: string;  resolvedAnswer: string | string[] | null;  usingCustomAnswer: boolean;  answeredQuestionCount: number;  isLastQuestion: boolean;  isComplete: boolean;  canAdvance: boolean;} function normalizeDraftAnswer(value: string | undefined): string | null {  if (typeof value !== "string") {    return null;  }   const trimmed = value.trim();  return trimmed.length > 0 ? trimmed : null;} function normalizeSelectedOptionValues(value: string[] | undefined): string[] {  if (!Array.isArray(value)) {    return [];  }   // Provider option IDs must stay unchanged, including whitespace.  return Array.from(new Set(value.filter((entry) => typeof entry === "string")));} export function resolvePendingUserInputAnswer(  question: UserInputQuestion,  draft: PendingUserInputDraftAnswer | undefined,): string | string[] | null {  if (draft?.attachmentsBlocked) return null;  const customAnswer =    question.allowCustomAnswer === false ? null : normalizeDraftAnswer(draft?.customAnswer);  if (customAnswer) {    return customAnswer;  }   const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues).filter(    (value) => question.options.some((option) => (option.value ?? option.label) === value),  );  if (question.multiSelect) {    return selectedOptionValues.length > 0      ? selectedOptionValues      : question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0        ? ""        : null;  }   return (    selectedOptionValues[0] ??    (question.allowCustomAnswer !== false && (draft?.attachmentCount ?? 0) > 0 ? "" : null)  );} export function setPendingUserInputCustomAnswer(  draft: PendingUserInputDraftAnswer | undefined,  customAnswer: string,): PendingUserInputDraftAnswer {  const selectedOptionValues =    customAnswer.trim().length > 0      ? undefined      : normalizeSelectedOptionValues(draft?.selectedOptionValues);   return {    customAnswer,    ...(selectedOptionValues && selectedOptionValues.length > 0 ? { selectedOptionValues } : {}),  };} export function togglePendingUserInputOptionSelection(  question: UserInputQuestion,  draft: PendingUserInputDraftAnswer | undefined,  optionValue: string,): PendingUserInputDraftAnswer {  if (question.multiSelect) {    const selectedOptionValues = normalizeSelectedOptionValues(draft?.selectedOptionValues);    const nextSelectedOptionValues = selectedOptionValues.includes(optionValue)      ? selectedOptionValues.filter((value) => value !== optionValue)      : [...selectedOptionValues, optionValue];     return {      customAnswer: "",      ...(nextSelectedOptionValues.length > 0        ? { selectedOptionValues: nextSelectedOptionValues }        : {}),    };  }   return {    customAnswer: "",    selectedOptionValues: [optionValue],  };} export function buildPendingUserInputAnswers(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): Record<string, string | string[]> | null {  const answers: Record<string, string | string[]> = {};   for (const question of questions) {    const answer = resolvePendingUserInputAnswer(question, draftAnswers[question.id]);    if (answer === null) {      return null;    }    answers[question.id] = answer;  }   return answers;} export function countAnsweredPendingUserInputQuestions(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,): number {  return questions.reduce((count, question) => {    return resolvePendingUserInputAnswer(question, draftAnswers[question.id]) !== null      ? count + 1      : count;  }, 0);} export function derivePendingUserInputProgress(  questions: ReadonlyArray<UserInputQuestion>,  draftAnswers: Record<string, PendingUserInputDraftAnswer>,  questionIndex: number,): PendingUserInputProgress {  const normalizedQuestionIndex =    questions.length === 0 ? 0 : Math.max(0, Math.min(questionIndex, questions.length - 1));  const activeQuestion = questions[normalizedQuestionIndex] ?? null;  const activeDraft = activeQuestion ? draftAnswers[activeQuestion.id] : undefined;  const resolvedAnswer = activeQuestion    ? resolvePendingUserInputAnswer(activeQuestion, activeDraft)    : null;  const customAnswer =    activeQuestion?.allowCustomAnswer === false ? "" : (activeDraft?.customAnswer ?? "");  const answeredQuestionCount = countAnsweredPendingUserInputQuestions(questions, draftAnswers);  const isLastQuestion =    questions.length === 0 ? true : normalizedQuestionIndex >= questions.length - 1;   return {    questionIndex: normalizedQuestionIndex,    activeQuestion,    activeDraft,    selectedOptionValues: normalizeSelectedOptionValues(activeDraft?.selectedOptionValues),    customAnswer,    resolvedAnswer,    usingCustomAnswer: customAnswer.trim().length > 0,    answeredQuestionCount,    isLastQuestion,    isComplete: buildPendingUserInputAnswers(questions, draftAnswers) !== null,    canAdvance: resolvedAnswer !== null,  };}