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
1 change: 1 addition & 0 deletions backend/server/src/handler/question.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl QuestionHandler {
data.common,
data.roles,
data.required,
data.short_answer_word_limit,
data.question_data,
&mut transaction.tx,
&mut state.snowflake_generator,
Expand Down
22 changes: 18 additions & 4 deletions backend/server/src/models/question.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub struct Question {
pub roles: Vec<i64>, // (Possibly empty) list of roles the question is for
pub required: bool,

pub short_answer_word_limit: Option<i32>,

#[serde(flatten)]
pub question_data: QuestionData,

Expand All @@ -84,7 +86,7 @@ pub struct QuestionRawData {
common: bool, // Common question are shown at the start
roles: Vec<i64>,
required: bool,

short_answer_word_limit: Option<i32>,
question_type: QuestionType,
multi_option_data: Option<sqlx::types::Json<Vec<MultiOptionQuestionOption>>>,

Expand Down Expand Up @@ -174,6 +176,7 @@ impl Question {
q.common,
COALESCE(array_remove(array_agg(DISTINCT qr.role_id), NULL), '{}') AS "roles!: Vec<i64>",
q.required,
q.short_answer_word_limit,
q.question_type AS "question_type: QuestionType",
q.created_at,
q.updated_at,
Expand Down Expand Up @@ -215,6 +218,7 @@ impl Question {
common: question_raw_data.common,
roles: question_raw_data.roles,
required: question_raw_data.required,
short_answer_word_limit: question_raw_data.short_answer_word_limit,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
Expand All @@ -235,6 +239,7 @@ impl Question {
q.common,
COALESCE(array_remove(array_agg(DISTINCT qr.role_id), NULL), '{}') AS "roles!: Vec<i64>",
q.required,
q.short_answer_word_limit,
q.question_type AS "question_type: QuestionType",
q.created_at,
q.updated_at,
Expand Down Expand Up @@ -278,6 +283,7 @@ impl Question {
common: question_raw_data.common,
roles: question_raw_data.roles,
required: question_raw_data.required,
short_answer_word_limit: question_raw_data.short_answer_word_limit,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
Expand Down Expand Up @@ -306,6 +312,7 @@ impl Question {
'{}'
) AS "roles!: Vec<i64>",
q.required,
q.short_answer_word_limit,
q.question_type AS "question_type: QuestionType",
q.created_at,
q.updated_at,
Expand Down Expand Up @@ -352,6 +359,7 @@ impl Question {
common: question_raw_data.common,
roles: question_raw_data.roles,
required: question_raw_data.required,
short_answer_word_limit: question_raw_data.short_answer_word_limit,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
Expand All @@ -376,6 +384,7 @@ impl Question {
q.common,
COALESCE(array_remove(array_agg(DISTINCT qr.role_id), NULL), '{}') AS "roles!: Vec<i64>",
q.required,
q.short_answer_word_limit,
q.question_type AS "question_type: QuestionType",
q.created_at,
q.updated_at,
Expand Down Expand Up @@ -419,6 +428,7 @@ impl Question {
common: question_raw_data.common,
roles: question_raw_data.roles,
required: question_raw_data.required,
short_answer_word_limit: question_raw_data.short_answer_word_limit,
question_data,
created_at: question_raw_data.created_at,
updated_at: question_raw_data.updated_at,
Expand All @@ -436,6 +446,7 @@ impl Question {
common: bool,
roles: Vec<i64>,
required: bool,
short_answer_word_limit: Option<i32>,
question_data: QuestionData,
transaction: &mut Transaction<'_, Postgres>,
snowflake_generator: &mut SnowflakeIdGenerator,
Expand All @@ -447,7 +458,9 @@ impl Question {
r#"
UPDATE questions SET
title = $2, description = $3, common = $4,
required = $5, question_type = $6, updated_at = $7
required = $5, question_type = $6, updated_at = $7,
short_answer_word_limit = $8

WHERE id = $1
RETURNING question_type AS "question_type: QuestionType"
"#,
Expand All @@ -457,7 +470,8 @@ impl Question {
common,
required,
QuestionType::from_question_data(&question_data) as QuestionType,
Utc::now()
Utc::now(),
short_answer_word_limit
)
.fetch_one(transaction.deref_mut())
.await?;
Expand Down Expand Up @@ -669,4 +683,4 @@ impl QuestionData {
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function CampaignQuestions({ campaignId, orgId, dict }: { campaig
const addNewQuestion = async (type: QuestionType, roleId: string) => {
const common = roleId === "common";

let newQuestion: Question = { id: snowflakeGenerator.generate().toString(), title: "", description: "", roles: common ? [] : [roleId], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), question_type: type, data: { options: [{ id: snowflakeGenerator.generate().toString(), display_order: 1, text: "Default Option" }] }, common, required: false };
let newQuestion: Question = { id: snowflakeGenerator.generate().toString(), title: "", description: "", roles: common ? [] : [roleId], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), question_type: type, data: { options: [{ id: snowflakeGenerator.generate().toString(), display_order: 1, text: "Default Option" }] }, common, required: false, short_answer_word_limit: null };
if (type === 'ShortAnswer') {
delete (newQuestion as any).data;
}
Expand Down Expand Up @@ -458,7 +458,7 @@ function MultiOptionQuestionCard({ question, currentRole, possibleRole, handleQu
(e.target as HTMLInputElement).value = '';
}
}} />
<p className="text-xs text-gray-500">{dict.dashboard.campaigns.questions.option_help}</p>
<p className="text-xs text-foreground">{dict.dashboard.campaigns.questions.option_help}</p>
</div>
</div>
</div>
Expand All @@ -482,6 +482,13 @@ function OptionDecorator({ questionType, index }: { questionType: string, index:
function ShortAnswerQuestionCard({ question, currentRole, possibleRole, handleQuestionUpdate, dict }: { question?: Question, currentRole: string, possibleRole?: RoleDetails, handleQuestionUpdate: (action: "update" | "delete", question: Question) => Promise<void>, dict: any }) {
const [title, setTitle] = useState(question?.title ?? "");
const [required, setRequired] = useState(question?.required ?? false);
const [wordLimit, setWordLimit] = useState<number | null>(question?.short_answer_word_limit ?? null);

const updateWordLimit = async (value: string) => {
const parsed = value === "" ? null : parseInt(value, 10);
setWordLimit(parsed);
await handleQuestionUpdate('update', { ...question!, short_answer_word_limit: parsed });
};

const updateTitle = async (title: string) => {
setTitle(title);
Expand Down Expand Up @@ -535,9 +542,20 @@ function ShortAnswerQuestionCard({ question, currentRole, possibleRole, handleQu
<Button variant="destructive" onClick={handleDeleteQuestion}><Trash className="w-4 h-4" /></Button>
</div>
</div>
<div className="flex items-center gap-2 p-2">
<label className="text-sm text-foreground whitespace-nowrap">Word limit</label>
<Input
type="text"
inputMode="numeric"
className="max-w-[120px]"
placeholder="None"
value={wordLimit ?? ""}
onChange={(e) => updateWordLimit(e.target.value.replace(/[^0-9]/g, ""))}
/>
</div>
<div className="flex flex-col gap-1 p-2">
<div className="border-b-2 border-dotted border-gray-500 max-w-[300px]">
<p className="text-sm text-gray-500">{dict.dashboard.campaigns.questions.answer_text}</p>
<p className="text-sm text-foreground">{dict.dashboard.campaigns.questions.answer_text}</p>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export default function ShortAnswer({
}) {
const [value, setValue] = useState(question.answer);
const [previousAnswer, setPreviousAnswer] = useState(question.answer === "No Answer" ? null : question.answer)
const [wordCount, setWordCount] = useState(() => {
if (!question.answer || question.answer === "No Answer") return 0;
return question.answer.split(/\s+/).filter((word: string) => word.length !== 0).length;
});

useEffect(() => {
if (question.answer === 'No Answer') {
Expand All @@ -30,7 +34,18 @@ export default function ShortAnswer({
}, [question.answer, question.answer_id]);

const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setValue(e.target.value);
const currentValue = e.target.value;
const words = currentValue.split(/\s+/).filter((word) => word.length !== 0);

if (question.short_answer_word_limit && words.length > question.short_answer_word_limit) {
const trimmed = words.slice(0, question.short_answer_word_limit).join(" ");
setValue(trimmed);
setWordCount(question.short_answer_word_limit);
return;
}

setValue(currentValue);
setWordCount(words.length);
}

const handleBlur = async () => {
Expand Down Expand Up @@ -65,6 +80,11 @@ export default function ShortAnswer({
placeholder={question ? dict.applicationpage.youranswer : "Your answer"}
className={`w-full resize-y transition-all duration-200 hover:border-primary focus:border-primary max-w-4xl`}
/>
{question.short_answer_word_limit && (
<p className={`text-sm mt-1 ${wordCount >= question.short_answer_word_limit ? "text-destructive font-bold" : "text-muted-foreground"}`}>
{wordCount} / {question.short_answer_word_limit} words
</p>
)}
</div>
)
}
3 changes: 3 additions & 0 deletions frontend-nextjs/src/models/question.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface Question {
common: boolean,
roles: string[],
required: boolean,
short_answer_word_limit: number | null,
question_type: QuestionType,
data: QuestionData,
created_at: string,
Expand Down Expand Up @@ -36,6 +37,7 @@ export interface QuestionAndAnswer {
answer: AnswerValue,
question_type: QuestionType,
required: boolean,
short_answer_word_limit: number | null,
options: MultiOptionQuestionOption[],
description: string | null,
}
Expand Down Expand Up @@ -134,6 +136,7 @@ export function linkQuestionsAndAnswers(questions: Question[], answers: Answer[]
answer: processedAnswer,
question_type: question.question_type,
required: question.required,
short_answer_word_limit: question.short_answer_word_limit,
options: question.data?.options,
description: question?.description,
};
Expand Down
Loading