Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -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,6 +542,17 @@ 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-gray-500 whitespace-nowrap">Word limit</label>
Comment thread
Plebbaroni marked this conversation as resolved.
Outdated
<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>
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