mirror of
https://github.com/getpaseo/paseo.git
synced 2026-08-14 12:23:16 +00:00
Adds client-side i18n support, language settings, translated UI copy, and locale parity coverage.
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
export interface QuestionOption {
|
|
label: string;
|
|
description?: string;
|
|
}
|
|
|
|
export interface QuestionFormQuestion {
|
|
question: string;
|
|
header: string;
|
|
options: QuestionOption[];
|
|
multiSelect: boolean;
|
|
allowOther: boolean;
|
|
allowEmpty: boolean;
|
|
placeholder?: string;
|
|
dismissLabel?: string;
|
|
}
|
|
|
|
export type QuestionSelections = Record<number, ReadonlySet<number>>;
|
|
export type QuestionOtherTexts = Record<number, string>;
|
|
|
|
function readOptionalString(record: Record<string, unknown>, key: string): string | undefined {
|
|
const value = record[key];
|
|
return typeof value === "string" ? value : undefined;
|
|
}
|
|
|
|
export function parseQuestionFormQuestions(input: unknown): QuestionFormQuestion[] | null {
|
|
if (
|
|
typeof input !== "object" ||
|
|
input === null ||
|
|
!("questions" in input) ||
|
|
!Array.isArray((input as Record<string, unknown>).questions)
|
|
) {
|
|
return null;
|
|
}
|
|
const raw = (input as Record<string, unknown>).questions as unknown[];
|
|
const questions: QuestionFormQuestion[] = [];
|
|
for (const item of raw) {
|
|
if (typeof item !== "object" || item === null) return null;
|
|
const q = item as Record<string, unknown>;
|
|
if (typeof q.question !== "string" || typeof q.header !== "string") return null;
|
|
if (!Array.isArray(q.options)) return null;
|
|
const options: QuestionOption[] = [];
|
|
for (const opt of q.options as unknown[]) {
|
|
if (typeof opt !== "object" || opt === null) return null;
|
|
const o = opt as Record<string, unknown>;
|
|
if (typeof o.label !== "string") return null;
|
|
options.push({
|
|
label: o.label,
|
|
description: typeof o.description === "string" ? o.description : undefined,
|
|
});
|
|
}
|
|
questions.push({
|
|
question: q.question,
|
|
header: q.header,
|
|
options,
|
|
multiSelect: q.multiSelect === true,
|
|
allowOther: q.allowOther === true || q.isOther === true,
|
|
allowEmpty: q.allowEmpty === true,
|
|
placeholder: readOptionalString(q, "placeholder"),
|
|
dismissLabel: readOptionalString(q, "dismissLabel"),
|
|
});
|
|
}
|
|
return questions.length > 0 ? questions : null;
|
|
}
|
|
|
|
export function questionShowsTextInput(question: QuestionFormQuestion): boolean {
|
|
return question.options.length === 0 || question.allowOther;
|
|
}
|
|
|
|
export function isQuestionAnswered(
|
|
question: QuestionFormQuestion,
|
|
qIndex: number,
|
|
selections: QuestionSelections,
|
|
otherTexts: QuestionOtherTexts,
|
|
): boolean {
|
|
const selected = selections[qIndex];
|
|
if (selected && selected.size > 0) {
|
|
return true;
|
|
}
|
|
|
|
if (!questionShowsTextInput(question)) {
|
|
return false;
|
|
}
|
|
|
|
const otherText = otherTexts[qIndex]?.trim();
|
|
if (otherText && otherText.length > 0) {
|
|
return true;
|
|
}
|
|
|
|
return question.allowEmpty;
|
|
}
|
|
|
|
export function areQuestionsAnswered(
|
|
questions: QuestionFormQuestion[] | null,
|
|
selections: QuestionSelections,
|
|
otherTexts: QuestionOtherTexts,
|
|
): boolean {
|
|
return (
|
|
questions?.every((question, qIndex) =>
|
|
isQuestionAnswered(question, qIndex, selections, otherTexts),
|
|
) ?? false
|
|
);
|
|
}
|
|
|
|
export function buildQuestionFormAnswers(
|
|
questions: QuestionFormQuestion[],
|
|
selections: QuestionSelections,
|
|
otherTexts: QuestionOtherTexts,
|
|
): Record<string, string> {
|
|
const answers: Record<string, string> = {};
|
|
for (let i = 0; i < questions.length; i++) {
|
|
const q = questions[i];
|
|
const selected = selections[i];
|
|
const otherText = otherTexts[i]?.trim();
|
|
|
|
if (questionShowsTextInput(q)) {
|
|
if (otherText && otherText.length > 0) {
|
|
answers[q.header] = otherText;
|
|
continue;
|
|
}
|
|
if (q.allowEmpty && q.options.length === 0) {
|
|
answers[q.header] = "";
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (selected && selected.size > 0) {
|
|
const labels = Array.from(selected).map((idx) => q.options[idx].label);
|
|
answers[q.header] = labels.join(", ");
|
|
}
|
|
}
|
|
return answers;
|
|
}
|
|
|
|
export function shouldSubmitEmptyOnDismiss(questions: QuestionFormQuestion[]): boolean {
|
|
return (
|
|
questions.length > 0 &&
|
|
questions.every((question) => question.allowEmpty && question.options.length === 0)
|
|
);
|
|
}
|
|
|
|
export function resolveDismissLabel(
|
|
questions: QuestionFormQuestion[],
|
|
fallbackLabel = "Dismiss",
|
|
): string {
|
|
return questions.find((question) => question.dismissLabel)?.dismissLabel ?? fallbackLabel;
|
|
}
|