Skip to content
Open
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
2 changes: 2 additions & 0 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,8 @@
"groupVariableDescription": "Co představují štítky podotázek (např. rok, země atd.).",
"categoryPickerDescription": "Název parametru, který se liší mezi podotázkami, jako je \"Možnost\", \"Rok\" nebo \"Země\"",
"subquestionLabel": "Štítek podotázky",
"optionColor": "Barva",
"defaultColor": "Výchozí barva",
"subquestionLabelDescription": "Štítek nebo parametr, který identifikuje tuto podotázku, jako je \"Možnost 1\", \"2033\" nebo \"Francie\"",
"conditionalPairDescription": "Podmíněný pár je speciální typ skupiny otázek, který vyvolává podmíněné pravděpodobnosti. Každý podmíněný pár je mezi nadřazenou otázkou a podřazenou otázkou. Nadřazená otázka musí být binární, zatímco podřazená může být buď binární, číselná nebo datová otázka.",
"isDoneError": "Nelze upravit uzavřené, vyřešené nebo odmítnuté otázky",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,8 @@
"groupVariableDescription": "What the subquestion labels represent (e.g. year, country, etc).",
"categoryPickerDescription": "A name for the parameter which varies between subquestions, like \"Option\", \"Year\" or \"Country\"",
"subquestionLabel": "Subquestion label",
"optionColor": "Color",
"defaultColor": "Default color",
"subquestionLabelDescription": "The label or parameter which identifies this subquestion, like \"Option 1\", \"2033\" or \"France\"",
"conditionalPairDescription": "A Conditional Pair is a special type of Question Group that elicits conditional probabilities. Each Conditional Pair sits between a Parent Question and a Child Question. The Parent must be a binary question, while the child may be a single binary, numeric, or date question.",
"parentQuestion": "Parent Question",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,8 @@
"groupVariableDescription": "Lo que representan las etiquetas de subpreguntas (por ejemplo, año, país, etc.).",
"categoryPickerDescription": "Un nombre para el parámetro que varía entre subpreguntas, como \"Opción\", \"Año\" o \"País\"",
"subquestionLabel": "Etiqueta de subpregunta",
"optionColor": "Color",
"defaultColor": "Color predeterminado",
"subquestionLabelDescription": "La etiqueta o parámetro que identifica esta subpregunta, como \"Opción 1\", \"2033\" o \"Francia\"",
"conditionalPairDescription": "Un Par Condicional es un tipo especial de Grupo de Preguntas que solicita probabilidades condicionales. Cada Par Condicional se encuentra entre una Pregunta Principal y una Pregunta Secundaria. La Principal debe ser una pregunta binaria, mientras que la secundaria puede ser una pregunta binaria, numérica o de fecha.",
"isDoneError": "No se pueden editar preguntas cerradas, resueltas o rechazadas",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,8 @@
"groupVariableDescription": "O que os rótulos das subperguntas representam (por exemplo, ano, país, etc.).",
"categoryPickerDescription": "Um nome para o parâmetro que varia entre as subperguntas, como \"Opção\", \"Ano\" ou \"País\"",
"subquestionLabel": "Rótulo da Subpergunta",
"optionColor": "Cor",
"defaultColor": "Cor padrão",
"subquestionLabelDescription": "O rótulo ou parâmetro que identifica esta subpergunta, como \"Opção 1\", \"2033\" ou \"França\"",
"conditionalPairDescription": "Um Par Condicional é um tipo especial de Grupo de Perguntas que elicia probabilidades condicionais. Cada Par Condicional está entre uma Pergunta Pai e uma Pergunta Filha. O Pai deve ser uma pergunta binária, enquanto a filha pode ser uma única pergunta binária, numérica ou de data.",
"isDoneError": "Não é possível editar perguntas fechadas, resolvidas ou rejeitadas",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,8 @@
"groupVariableDescription": "子問題標籤代表什麼 (例如年份、國家等)。",
"categoryPickerDescription": "在子問題之間變化的參數名稱,例如“選項”、“年份”或“國家”",
"subquestionLabel": "子問題標籤",
"optionColor": "顏色",
"defaultColor": "預設顏色",
"subquestionLabelDescription": "標籤或參數,用於識別此子問題,例如“選項 1”、“2033”或“法國”",
"conditionalPairDescription": "條件對是一種特殊類型的問題組,用於引出條件概率。每個條件對位於父問題與子問題之間。父問題必須是二元問題,而子問題可以是單個二元、數字或日期問題。",
"parentQuestion": "父問題",
Expand Down
2 changes: 2 additions & 0 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,8 @@
"groupVariableDescription": "子問題標籤所代表的內容(例如年份、國家等)。",
"categoryPickerDescription": "子問題之間變化的參數名稱,如「選項」、「年份」或「國家」",
"subquestionLabel": "子問題標籤",
"optionColor": "颜色",
"defaultColor": "默认颜色",
"subquestionLabelDescription": "識別此子問題的���籤或參數,如「選項1」、「2033」或「法國」",
"conditionalPairDescription": "條件對是一種特殊類型的問題組,用於獲取條件概率。每個條件對位於父問題和子問題之間。父問題必須是二元問題,而子問題可以是單一二元、數值或日期問題。",
"isDoneError": "無法編輯已關閉、已解決或被拒絕的問題",
Expand Down
87 changes: 87 additions & 0 deletions front_end/src/app/(main)/questions/components/color_picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use client";

import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { useTranslations } from "next-intl";
import { FC } from "react";

import {
getMcOptionColor,
MC_OPTION_PALETTE_KEYS,
METAC_COLORS,
} from "@/constants/colors";
import useAppTheme from "@/hooks/use_app_theme";
import { ThemeColor } from "@/types/theme";
import cn from "@/utils/core/cn";

type Props = {
value: string | null | undefined;
onChange: (key: string | null) => void;
fallback?: ThemeColor;
};

const ColorPicker: FC<Props> = ({ value, onChange, fallback }) => {
const t = useTranslations();
const { getThemeColor } = useAppTheme();

const currentColor =
getMcOptionColor(value) ?? fallback ?? METAC_COLORS.gray["400"];

return (
<Popover className="relative">
<PopoverButton
type="button"
title={t("optionColor")}
aria-label={t("optionColor")}
className="block size-6 shrink-0 rounded border border-gray-500 dark:border-gray-500-dark"
style={{ backgroundColor: getThemeColor(currentColor) }}
/>
<PopoverPanel
portal
anchor="bottom start"
className="z-[100] grid grid-cols-6 gap-1 rounded border border-gray-300 bg-gray-0 p-2 shadow-lg dark:border-gray-300-dark dark:bg-gray-0-dark"
>
{({ close }) => (
<>
{MC_OPTION_PALETTE_KEYS.map((key) => {
const color = getMcOptionColor(key);
if (!color) return null;

return (
<button
key={key}
type="button"
className={cn(
"size-6 rounded border border-transparent",
key === value &&
"ring-2 ring-blue-700 ring-offset-1 dark:ring-blue-700-dark"
)}
style={{ backgroundColor: getThemeColor(color) }}
onClick={() => {
onChange(key);
close();
}}
/>
);
Comment thread
aseckin marked this conversation as resolved.
})}
<button
type="button"
title={t("defaultColor")}
aria-label={t("defaultColor")}
className="flex size-6 items-center justify-center rounded border border-gray-500 text-gray-600 dark:border-gray-500-dark dark:text-gray-600-dark"
onClick={() => {
onChange(null);
close();
}}
>
<FontAwesomeIcon icon={faXmark} size="sm" />
</button>
</>
)}
</PopoverPanel>
</Popover>
);
};

export default ColorPicker;
33 changes: 26 additions & 7 deletions front_end/src/app/(main)/questions/components/group_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ import { InputContainer } from "@/components/ui/input_container";
import LoadingIndicator from "@/components/ui/loading_indicator";
import { MarkdownText } from "@/components/ui/markdown_text";
import Select from "@/components/ui/select";
import { MULTIPLE_CHOICE_COLOR_SCALE } from "@/constants/colors";
import { ContinuousQuestionTypes } from "@/constants/questions";
import { useAuth } from "@/contexts/auth_context";
import { useDebouncedCallback } from "@/hooks/use_debounce";
import {
GroupOfQuestionsGraphType,
Expand Down Expand Up @@ -66,13 +68,15 @@ import { sortGroupPredictionOptions } from "@/utils/questions/groupOrdering";

import BacktoCreate from "./back_to_create";
import CategoryPicker from "./category_picker";
import ColorPicker from "./color_picker";
import NumericQuestionInput from "./numeric_question_input";
import { createQuestionPost, updatePost } from "../actions";

type SubQuestionDraftBase = {
id?: number;
clientId: string;
label: string;
label_color?: string | null;
scheduled_close_time?: string | undefined;
scheduled_resolve_time?: string | undefined;
open_time?: string | undefined;
Expand All @@ -96,6 +100,7 @@ type CreateSubQuestionPayload = {
type: SimpleQuestionType;
title: string;
label: string;
label_color?: string | null;
scheduled_close_time: string | undefined;
scheduled_resolve_time?: string | undefined;
open_time: string | undefined;
Expand Down Expand Up @@ -209,6 +214,7 @@ const GroupForm: React.FC<Props> = ({
}) => {
const router = useRouter();
const t = useTranslations();
const { user } = useAuth();
const isDuplicate = mode === "create" && !!post;
const isDraftMounted = useRef(false);
const draftKey = `group_${subtype}`;
Expand Down Expand Up @@ -259,6 +265,7 @@ const GroupForm: React.FC<Props> = ({
type: sqType,
title: `${data["title"]} (${x.label})`,
label: x.label,
label_color: x.label_color ?? null,
scheduled_close_time: x.scheduled_close_time,
scheduled_resolve_time: x.scheduled_resolve_time,
open_time: x.open_time,
Expand Down Expand Up @@ -395,6 +402,7 @@ const GroupForm: React.FC<Props> = ({
open_time: isDuplicate ? undefined : x.open_time,
cp_reveal_time: isDuplicate ? undefined : x.cp_reveal_time,
label: x.label,
label_color: x.label_color,
unit: x.unit,
scaling: x.scaling,
open_lower_bound: x.open_lower_bound,
Expand Down Expand Up @@ -800,13 +808,24 @@ const GroupForm: React.FC<Props> = ({
: undefined
}
>
<Input
onChange={(e) => {
updateSubQuestionAt(index, { label: e.target.value });
}}
className="rounded border border-gray-500 px-3 py-2 text-base dark:border-gray-500-dark dark:bg-blue-50-dark"
value={subQuestion?.label}
/>
<div className="flex items-center gap-2">
<Input
onChange={(e) => {
updateSubQuestionAt(index, { label: e.target.value });
}}
className="w-full rounded border border-gray-500 px-3 py-2 text-base dark:border-gray-500-dark dark:bg-blue-50-dark"
value={subQuestion?.label}
/>
{user?.is_superuser && (
<ColorPicker
value={subQuestion?.label_color}
fallback={MULTIPLE_CHOICE_COLOR_SCALE[index]}
onChange={(key) => {
updateSubQuestionAt(index, { label_color: key });
}}
/>
)}
</div>
</InputContainer>
{collapsedSubQuestions[index] && (
<div className="flex w-full flex-col gap-4">
Expand Down
64 changes: 62 additions & 2 deletions front_end/src/app/(main)/questions/components/question_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import LoadingIndicator from "@/components/ui/loading_indicator";
import { MarkdownText } from "@/components/ui/markdown_text";
import SectionToggle from "@/components/ui/section_toggle";
import Select from "@/components/ui/select";
import { MULTIPLE_CHOICE_COLOR_SCALE } from "@/constants/colors";
import { ContinuousQuestionTypes } from "@/constants/questions";
import { useAuth } from "@/contexts/auth_context";
import { useDebouncedCallback } from "@/hooks/use_debounce";
Expand Down Expand Up @@ -62,6 +63,7 @@ import { getQuestionStatus } from "@/utils/questions/helpers";
import { createQuestionPost, updatePost } from "../actions";
import BacktoCreate from "./back_to_create";
import CategoryPicker from "./category_picker";
import ColorPicker from "./color_picker";
import NumericQuestionInput from "./numeric_question_input";

// Extended interface to include additional fields being used
Expand Down Expand Up @@ -278,6 +280,7 @@ const createQuestionSchemas = (
})
),
options_order: z.nativeEnum(MultipleChoiceOptionsOrder).optional(),
options_colors: z.record(z.string()).optional(),
})
);

Expand Down Expand Up @@ -380,6 +383,11 @@ const QuestionForm: FC<Props> = ({
...(data as MultipleChoiceQuestionType),
type: QuestionType.MultipleChoice,
options: optionsList.map((o) => o.trim()),
options_colors: Object.fromEntries(
optionsList
.map((o, i) => [o.trim(), optionColors[i]] as const)
.filter(([, c]) => !!c)
),
} as MultipleChoiceQuestionType)
: ({
...data,
Expand Down Expand Up @@ -428,6 +436,13 @@ const QuestionForm: FC<Props> = ({
? post.question.options
: Array(MIN_OPTIONS_AMOUNT).fill("")
);
const [optionColors, setOptionColors] = useState<(string | null)[]>(
post?.question?.options
? post.question.options.map(
(o) => post?.question?.options_colors?.[o] ?? null
)
: Array(MIN_OPTIONS_AMOUNT).fill(null)
);

const [categoriesList, setCategoriesList] = useState<Category[]>(
post?.projects?.category ?? ([] as Category[])
Expand Down Expand Up @@ -509,11 +524,23 @@ const QuestionForm: FC<Props> = ({
saveQuestionDraft(questionType, {
...formData,
options: optionsList,
options_colors: Object.fromEntries(
optionsList
.map((o, i) => [o, optionColors[i]] as const)
.filter(([, c]) => !!c)
),
categories: categoriesList,
type: formData.type as unknown as QuestionType,
} as Partial<ExtendedQuestionDraft>);
}
}, [form, shouldUseDraftValue, questionType, optionsList, categoriesList]);
}, [
form,
shouldUseDraftValue,
questionType,
optionsList,
optionColors,
categoriesList,
]);

const debouncedHandleFormChange = useDebouncedCallback(
handleFormChange,
Expand Down Expand Up @@ -614,6 +641,11 @@ const QuestionForm: FC<Props> = ({
const draft = getQuestionDraft(questionType);
if (draft) {
setOptionsList(draft.options ?? Array(MIN_OPTIONS_AMOUNT).fill("")); // MC questions
setOptionColors(
draft.options
? draft.options.map((o) => draft.options_colors?.[o] ?? null)
: Array(MIN_OPTIONS_AMOUNT).fill(null)
);
setCategoriesList(draft.categories ?? []);
setCurrentProject(
!isNil(draft.default_project) &&
Expand Down Expand Up @@ -644,6 +676,13 @@ const QuestionForm: FC<Props> = ({
return () => subscription.unsubscribe();
}, [form, shouldUseDraftValue, debouncedHandleFormChange]);

// update draft when option colors change
useEffect(() => {
if (shouldUseDraftValue && isDraftMounted.current) {
debouncedHandleFormChange();
}
}, [shouldUseDraftValue, debouncedHandleFormChange, optionColors]);

return (
<main className="mb-4 mt-2 flex max-w-4xl flex-col justify-center self-center rounded-none bg-gray-0 px-4 pb-5 pt-4 dark:bg-gray-0-dark md:m-8 md:mx-auto md:rounded-md md:px-8 md:pb-8 lg:m-12 lg:mx-auto">
<BacktoCreate
Expand Down Expand Up @@ -808,6 +847,21 @@ const QuestionForm: FC<Props> = ({
}
/>
</div>
{user?.is_superuser && (
<div className="my-2 flex h-[42px] items-center px-2">
<ColorPicker
value={optionColors[opt_index]}
fallback={MULTIPLE_CHOICE_COLOR_SCALE[opt_index]}
onChange={(key) => {
setOptionColors((prev) =>
prev.map((c, index) =>
index === opt_index ? key : c
)
);
}}
/>
</div>
)}
{opt_index >= MIN_OPTIONS_AMOUNT && !optionsLocked && (
<Button
className="my-2 h-[42px] w-max self-start capitalize"
Expand All @@ -820,6 +874,9 @@ const QuestionForm: FC<Props> = ({
form.setValue("options", newOptionsArray);
return newOptionsArray;
});
setOptionColors((prev) =>
prev.filter((_, index) => index !== opt_index)
);
}}
>
<FontAwesomeIcon icon={faXmark} />
Expand All @@ -832,7 +889,10 @@ const QuestionForm: FC<Props> = ({
<Button
className="w-max capitalize"
disabled={optionsLocked}
onClick={() => setOptionsList([...optionsList, ""])}
onClick={() => {
setOptionsList([...optionsList, ""]);
setOptionColors([...optionColors, null]);
}}
>
+ {t("addOption")}
</Button>
Expand Down
9 changes: 9 additions & 0 deletions front_end/src/constants/colors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ThemeColor } from "@/types/theme";

export const METAC_COLORS = {
gray: {
0: { DEFAULT: "#ffffff", dark: "#262f38" },
Expand Down Expand Up @@ -202,3 +204,10 @@ export const METAC_COLORS = {
export const MULTIPLE_CHOICE_COLOR_SCALE = Object.values(
METAC_COLORS["mc-option"]
).map((value) => value);

export const MC_OPTION_PALETTE_KEYS = Object.keys(METAC_COLORS["mc-option"]);

export function getMcOptionColor(key?: string | null): ThemeColor | undefined {
if (!key) return undefined;
return (METAC_COLORS["mc-option"] as Record<string, ThemeColor>)[key];
}
Loading
Loading