Skip to content

Commit e09794e

Browse files
committed
fix: option flickers while adding
1 parent acaca60 commit e09794e

File tree

3 files changed

+73
-47
lines changed

3 files changed

+73
-47
lines changed

src/features/form/components/AdminFormDetailPages/SectionEditPage.tsx

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -138,48 +138,41 @@ export const AdminSectionEditPage = () => {
138138
const syncQuestionFromApi = (apiQuestion: FormsQuestionResponse) => {
139139
setQuestions(prev => {
140140
if (!prev[index]) return prev;
141+
const old = prev[index];
142+
143+
const nextOptions = old.isFromAnswer ? [] : old.options;
144+
const nextDetailOptions = old.detailOptions;
145+
146+
const updated: Question = {
147+
...old,
148+
title: apiQuestion.title ?? old.title,
149+
description: apiQuestion.description ?? old.description,
150+
required: apiQuestion.required ?? old.required,
151+
...(apiQuestion.sourceId !== undefined && {
152+
sourceQuestionId: apiQuestion.sourceId ?? undefined,
153+
isFromAnswer: Boolean(apiQuestion.sourceId)
154+
}),
155+
icon: (apiQuestion.scale?.icon ?? old.icon) as Question["icon"],
156+
start: apiQuestion.scale?.minVal ?? old.start,
157+
end: apiQuestion.scale?.maxVal ?? old.end,
158+
startLabel: apiQuestion.scale?.minValueLabel ?? old.startLabel,
159+
endLabel: apiQuestion.scale?.maxValueLabel ?? old.endLabel,
160+
uploadAllowedFileTypes: apiQuestion.uploadFile?.allowedFileTypes ? [...apiQuestion.uploadFile.allowedFileTypes] : old.uploadAllowedFileTypes,
161+
uploadMaxFileAmount: apiQuestion.uploadFile?.maxFileAmount ?? old.uploadMaxFileAmount,
162+
uploadMaxFileSizeLimit: apiQuestion.uploadFile?.maxFileSizeLimit ?? old.uploadMaxFileSizeLimit,
163+
dateHasYear: apiQuestion.date?.hasYear ?? old.dateHasYear,
164+
dateHasMonth: apiQuestion.date?.hasMonth ?? old.dateHasMonth,
165+
dateHasDay: apiQuestion.date?.hasDay ?? old.dateHasDay,
166+
dateHasMinDate: Boolean(apiQuestion.date?.minDate),
167+
dateHasMaxDate: Boolean(apiQuestion.date?.maxDate),
168+
dateMinDate: apiQuestion.date?.minDate ? apiQuestion.date.minDate.slice(0, 10) : "",
169+
dateMaxDate: apiQuestion.date?.maxDate ? apiQuestion.date.maxDate.slice(0, 10) : "",
170+
options: nextOptions,
171+
detailOptions: nextDetailOptions
172+
};
173+
141174
const next = [...prev];
142-
const target = next[index];
143-
target.title = apiQuestion.title ?? target.title;
144-
target.description = apiQuestion.description ?? target.description;
145-
target.required = apiQuestion.required ?? target.required;
146-
if (apiQuestion.sourceId !== undefined) {
147-
target.sourceQuestionId = apiQuestion.sourceId ?? undefined;
148-
target.isFromAnswer = Boolean(apiQuestion.sourceId);
149-
}
150-
target.icon = apiQuestion.scale?.icon ?? target.icon;
151-
target.start = apiQuestion.scale?.minVal ?? target.start;
152-
target.end = apiQuestion.scale?.maxVal ?? target.end;
153-
target.startLabel = apiQuestion.scale?.minValueLabel ?? target.startLabel;
154-
target.endLabel = apiQuestion.scale?.maxValueLabel ?? target.endLabel;
155-
target.uploadAllowedFileTypes = apiQuestion.uploadFile?.allowedFileTypes ? [...apiQuestion.uploadFile.allowedFileTypes] : target.uploadAllowedFileTypes;
156-
target.uploadMaxFileAmount = apiQuestion.uploadFile?.maxFileAmount ?? target.uploadMaxFileAmount;
157-
target.uploadMaxFileSizeLimit = apiQuestion.uploadFile?.maxFileSizeLimit ?? target.uploadMaxFileSizeLimit;
158-
target.dateHasYear = apiQuestion.date?.hasYear ?? target.dateHasYear;
159-
target.dateHasMonth = apiQuestion.date?.hasMonth ?? target.dateHasMonth;
160-
target.dateHasDay = apiQuestion.date?.hasDay ?? target.dateHasDay;
161-
target.dateHasMinDate = Boolean(apiQuestion.date?.minDate);
162-
target.dateHasMaxDate = Boolean(apiQuestion.date?.maxDate);
163-
target.dateMinDate = apiQuestion.date?.minDate ? apiQuestion.date.minDate.slice(0, 10) : "";
164-
target.dateMaxDate = apiQuestion.date?.maxDate ? apiQuestion.date.maxDate.slice(0, 10) : "";
165-
166-
if (target.type === "DETAILED_MULTIPLE_CHOICE" && apiQuestion.choices) {
167-
target.detailOptions = apiQuestion.choices.map(choice => ({
168-
id: choice.id,
169-
label: choice.name ?? "",
170-
description: choice.description ?? ""
171-
}));
172-
} else if (apiQuestion.choices) {
173-
target.options = apiQuestion.choices.map(choice => ({
174-
id: choice.id,
175-
label: choice.name ?? "",
176-
isOther: choice.isOther ?? false
177-
}));
178-
} else if (target.isFromAnswer) {
179-
target.options = [];
180-
}
181-
182-
questionsRef.current = next;
175+
next[index] = updated;
183176
return next;
184177
});
185178
};

src/features/form/components/AdminFormDetailPages/components/OptionsQuestion.tsx

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,36 @@ const OptionRow = ({ option, index, type, canRemove, onCommit, onRemove }: Optio
4949
);
5050
};
5151

52+
interface OtherOptionRowProps {
53+
option: Option;
54+
index: number;
55+
type: "radio" | "checkbox" | "list";
56+
canRemove: boolean;
57+
onCommit: (index: number, value: string) => void;
58+
onRemoveOther: () => void;
59+
}
60+
61+
const OtherOptionRow = ({ option, index, type, canRemove, onCommit, onRemoveOther }: OtherOptionRowProps) => {
62+
const [localLabel, setLocalLabel] = useState(option.label);
63+
return (
64+
<div className={styles.optionWrapper}>
65+
<OptionsInput
66+
value={localLabel}
67+
type={type}
68+
themeColor="--comment"
69+
variant="flushed"
70+
listLabel={`${index + 1}.`}
71+
className={styles.optionInput}
72+
placeholder="其他(使用者填寫)"
73+
onFocus={e => e.target.select()}
74+
onChange={e => setLocalLabel(e.target.value)}
75+
onBlur={() => onCommit(index, localLabel)}
76+
/>
77+
{canRemove && <X onClick={onRemoveOther} />}
78+
</div>
79+
);
80+
};
81+
5282
export const OptionsQuestion = (props: OptionsQuestionProps) => {
5383
return (
5484
<div className={styles.container}>
@@ -60,10 +90,15 @@ export const OptionsQuestion = (props: OptionsQuestionProps) => {
6090
}
6191
if (option.isOther) {
6292
return (
63-
<div className={styles.optionWrapper}>
64-
<OptionsInput key="other" value="其他(使用者填寫)" type={props.type} variant="none" readOnly />
65-
{props.options.length > 1 && <X onClick={props.onRemoveOther} />}
66-
</div>
93+
<OtherOptionRow
94+
key={option.id ?? index}
95+
option={option}
96+
index={index}
97+
type={props.type}
98+
canRemove={props.options.length > 1}
99+
onCommit={props.onChange}
100+
onRemoveOther={props.onRemoveOther}
101+
/>
67102
);
68103
}
69104
})}

src/features/form/components/AdminFormDetailPages/types/question.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,12 @@ export type QuestionTemplate = {
6464
};
6565

6666
export type Option = {
67-
/** Stable identity for React key – use API choice ID or a client-side UUID */
6867
id?: string;
6968
label: string;
7069
isOther?: boolean;
7170
};
7271

7372
export type DetailOption = {
74-
/** Stable identity for React key – use API choice ID or a client-side UUID */
7573
id?: string;
7674
label: string;
7775
description: string;

0 commit comments

Comments
 (0)