diff --git a/src/components/Componentes/multi-select-e2e.integration.test.tsx b/src/components/Componentes/multi-select-e2e.integration.test.tsx index fbbc41c..004cce3 100644 --- a/src/components/Componentes/multi-select-e2e.integration.test.tsx +++ b/src/components/Componentes/multi-select-e2e.integration.test.tsx @@ -291,5 +291,53 @@ describe("Multi-Select End-to-End & Integration Tests", () => { expect(storedField.value).toEqual(["hookah_rarely", "cigarettes_rarely"]); expect(storedField.option_id).toEqual(["hookah_rarely", "cigarettes_rarely"]); }); + + it("E2E Scenario 5: QuestionCheckbox multi-select enforces max_select limit", async () => { + const { QuestionCheckbox } = await import("./question-checkbox"); + let currentAnswers: any = null; + + function TestCheckboxContainer({ question }: { question: QuestionField }) { + const { answers } = useQuestionAnswers(); + currentAnswers = answers; + return ; + } + + const question: QuestionField = { + id: "beliefs_lifestyle.what_are_your_prominent_personality_traits", + title: "ویژگیهای شخصیتی شما چیست؟", + type: "checkbox", + order: 1, + required: true, + isVisible: true, + description: "", + tooltip: "", + extras: { placeHolder: "" }, + options: [ + { id: "t1", value: "t1", label: "Trait 1", order: 1 }, + { id: "t2", value: "t2", label: "Trait 2", order: 2 }, + { id: "t3", value: "t3", label: "Trait 3", order: 3 }, + ], + ui_config: { max_select: 2 }, + }; + + render( + + + + + + ); + + // Select trait 1 and trait 2 + fireEvent.click(screen.getByText("Trait 1")); + fireEvent.click(screen.getByText("Trait 2")); + + expect(currentAnswers["beliefs_lifestyle.what_are_your_prominent_personality_traits"].value).toEqual(["t1", "t2"]); + + // Attempt to select trait 3 (should be ignored due to max_select: 2) + fireEvent.click(screen.getByText("Trait 3")); + expect(currentAnswers["beliefs_lifestyle.what_are_your_prominent_personality_traits"].value).toEqual(["t1", "t2"]); + }); }); + diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index d12e710..a199cb1 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -171,24 +171,37 @@ function createQuestionField( question.type === "dropdown" || question.type === "radio" || question.type === "checkbox" || - question.type === "scale"; + question.type === "scale" || + question.type === "select" || + question.type === "multi_select" || + question.type === "multiselect" || + question.type === "choice"; + + const maxSelect = + question.ui_config?.max_select || + (question.validation?.max ? Number(question.validation.max) : undefined); + + let boundedValue = value; + if (Array.isArray(boundedValue) && maxSelect && boundedValue.length > maxSelect) { + boundedValue = boundedValue.slice(0, maxSelect); + } if (isChoiceType && question.options && Array.isArray(question.options) && question.options.length > 0) { - if (Array.isArray(value)) { - option_id = value; - } else if (typeof value === "string" && value) { - const strVal = value.trim().toLowerCase(); + if (Array.isArray(boundedValue)) { + option_id = boundedValue; + } else if (typeof boundedValue === "string" && boundedValue) { + const strVal = boundedValue.trim().toLowerCase(); const selectedOpt = question.options.find( (opt) => - opt.id === value || - opt.value === value || - opt.label === value || + opt.id === boundedValue || + opt.value === boundedValue || + opt.label === boundedValue || opt.id.toLowerCase() === strVal || (typeof opt.value === "string" && opt.value.toLowerCase() === strVal) || (typeof opt.label === "string" && opt.label.toLowerCase() === strVal) || opt.id.toLowerCase().endsWith(`.${strVal}`) ); - option_id = selectedOpt ? selectedOpt.id : value; + option_id = selectedOpt ? selectedOpt.id : boundedValue; } } @@ -198,7 +211,7 @@ function createQuestionField( key, label: question.title, type: question.type, - value, + value: boundedValue, private: question.private, option_id: isChoiceType ? option_id : undefined, } as MarriageField; diff --git a/src/components/Componentes/question-checkbox.tsx b/src/components/Componentes/question-checkbox.tsx index a9f8cb7..d2b7508 100644 --- a/src/components/Componentes/question-checkbox.tsx +++ b/src/components/Componentes/question-checkbox.tsx @@ -100,11 +100,18 @@ export function QuestionCheckbox({ ); } + const maxSelect = + question.ui_config?.max_select || + (question.validation?.max ? Number(question.validation.max) : undefined); + const toggleOption = (optionId: string) => { let nextValue: string[]; if (value.includes(optionId)) { nextValue = value.filter((v) => v !== optionId); } else { + if (maxSelect && value.length >= maxSelect) { + return; + } nextValue = [...value, optionId]; } @@ -137,27 +144,37 @@ export function QuestionCheckbox({ {options.map((option) => { const optionId = `checkbox-${question.id}-${option.id}`; const isSelected = value.includes(option.id); + const isOptionDisabled = + !isSelected && Boolean(maxSelect && value.length >= maxSelect); return ( toggleOption(option.id)} + disabled={disabled || isOptionDisabled} + onChange={() => { + if (isOptionDisabled) return; + toggleOption(option.id); + }} className="sr-only" /> {!isShortOptions && ( diff --git a/src/components/Componentes/question-dropdown.tsx b/src/components/Componentes/question-dropdown.tsx index a2f55e2..b7d30ab 100644 --- a/src/components/Componentes/question-dropdown.tsx +++ b/src/components/Componentes/question-dropdown.tsx @@ -248,11 +248,18 @@ export function QuestionDropdown({ ? selectedList.includes(option.id) : singleValue === option.id; + const isOptionDisabled = + isMulti && + !isSelected && + Boolean(maxSelect && selectedList.length >= maxSelect); + return ( { + if (isOptionDisabled) return; if (isMulti) { toggleMultiOption(option.id); } else { @@ -260,7 +267,12 @@ export function QuestionDropdown({ setIsOpen(false); } }} - className="flex w-full items-start gap-3 text-start cursor-pointer group/opt py-0.5" + className={[ + "flex w-full items-start gap-3 text-start py-0.5", + isOptionDisabled + ? "opacity-35 cursor-not-allowed pointer-events-none" + : "cursor-pointer group/opt", + ].join(" ")} > {/* Option Indicator Icon */} {isMulti ? ( @@ -268,9 +280,11 @@ export function QuestionDropdown({ ) : ( diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index ccd6558..35e7540 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -402,11 +402,21 @@ export function QuestionFile({ pendingDeferredRef.current = { resolve: resolveFn, reject: rejectFn }; registerPendingUpload?.(question.id, uploadPromise); + const pickerSource = + question.ui_config?.source || + question.ui_config?.picker_type || + "file_system"; + const pickerType = + question.ui_config?.picker_type || + question.ui_config?.source || + "file_system"; + uploadFile({ requestId: String(question.id), mediaType, multiple: true, - source: (question.ui_config?.source as any) || "file_system", + source: pickerSource as any, + picker_type: pickerType as any, returnAs: "upload", uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`, fieldName: "file", diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 3810d39..582452b 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -155,10 +155,20 @@ export function QuestionPhoto({ pendingDeferredRef.current = { resolve: resolveFn, reject: rejectFn }; registerPendingUpload?.(question.id, uploadPromise); + const pickerSource = + question.ui_config?.source || + question.ui_config?.picker_type || + "gallery"; + const pickerType = + question.ui_config?.picker_type || + question.ui_config?.source || + "gallery"; + uploadFile({ requestId: String(question.id), mediaType: "image", - source: (question.ui_config?.source as any) || "gallery", + source: pickerSource as any, + picker_type: pickerType as any, returnAs: "upload", uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`, fieldName: "file", diff --git a/src/components/Componentes/question-renderer.tsx b/src/components/Componentes/question-renderer.tsx index d0f7c27..a7a83d7 100644 --- a/src/components/Componentes/question-renderer.tsx +++ b/src/components/Componentes/question-renderer.tsx @@ -63,6 +63,10 @@ export function QuestionRenderer({ /> ); case "dropdown": + case "select": + case "multi_select": + case "multiselect": + case "choice": return ( 0) { - const fieldNames: string[] = []; - for (const [key] of errorEntries) { - const matchingQuestion = questions?.find( - (q) => - q.id === key || - q.id.endsWith(`.${key}`) || - key.endsWith(`.${q.id}`) || - q.id.split(".").pop() === key.split(".").pop(), - ); - if (matchingQuestion?.title) { - fieldNames.push(`«${matchingQuestion.title}»`); - } else { - fieldNames.push(`«${key}»`); - } - } + // 1. Direct message / detail / error string from backend + const directMessage = data?.detail || data?.message || data?.error; + if (typeof directMessage === "string" && directMessage.trim()) { + msg = directMessage; + } - if (fieldNames.length > 0) { - const uniqueFieldNames = Array.from(new Set(fieldNames)); - const template = - t["Please complete the required fields: {fields}"] || - "Please complete the required fields: {fields}"; - msg = template.replace("{fields}", uniqueFieldNames.join("، ")); - } - } + // 2. Non-field errors array + if (!msg && Array.isArray(data?.non_field_errors) && data.non_field_errors.length > 0) { + msg = data.non_field_errors.join(" - "); + } + + // 3. Array of error messages + if (!msg && Array.isArray(data?.errors) && data.errors.length > 0) { + msg = data.errors + .map((e: any) => (typeof e === "string" ? e : e?.message || e?.detail || JSON.stringify(e))) + .filter(Boolean) + .join(" - "); } - // 2. Direct message / detail / error from backend + // 4. Detailed field-level validation errors from backend (data.errors, data.answers, or data directly) if (!msg) { - const directMessage = data?.detail || data?.message || data?.error; - if (typeof directMessage === "string" && directMessage.trim()) { - msg = directMessage; + const rawFieldErrors = + (data?.errors && typeof data.errors === "object" && !Array.isArray(data.errors) ? data.errors : null) || + (data?.answers && typeof data.answers === "object" && !Array.isArray(data.answers) ? data.answers : null) || + (data && typeof data === "object" && !Array.isArray(data) ? data : null); + + if (rawFieldErrors) { + const errorEntries = Object.entries(rawFieldErrors).filter( + ([k, v]) => + k !== "status" && + k !== "code" && + k !== "status_code" && + k !== "version" && + k !== "success" && + v !== null && + v !== undefined, + ); + + if (errorEntries.length > 0) { + const messagesWithFields: string[] = []; + const fieldNamesOnly: string[] = []; + + for (const [key, val] of errorEntries) { + const matchingQuestion = questions?.find( + (q) => + q.id === key || + q.id.endsWith(`.${key}`) || + key.endsWith(`.${q.id}`) || + q.id.split(".").pop() === key.split(".").pop(), + ); + const fieldTitle = matchingQuestion?.title + ? `«${matchingQuestion.title}»` + : `«${key}»`; + + let fieldErrMsg = ""; + if (typeof val === "string" && val.trim()) { + fieldErrMsg = val; + } else if (Array.isArray(val) && val.length > 0) { + fieldErrMsg = val + .map((item) => + typeof item === "string" + ? item + : item?.message || JSON.stringify(item), + ) + .join("، "); + } else if (typeof val === "object" && val !== null) { + const innerMsg = + (val as any).message || + (val as any).detail || + (val as any).error; + if (typeof innerMsg === "string") { + fieldErrMsg = innerMsg; + } + } + + if (fieldErrMsg) { + messagesWithFields.push(`${fieldTitle}: ${fieldErrMsg}`); + } else { + fieldNamesOnly.push(fieldTitle); + } + } + + if (messagesWithFields.length > 0) { + msg = messagesWithFields.join(" | "); + } else if (fieldNamesOnly.length > 0) { + const uniqueFieldNames = Array.from(new Set(fieldNamesOnly)); + const template = + t["Please complete the required fields: {fields}"] || + "Please complete the required fields: {fields}"; + msg = template.replace("{fields}", uniqueFieldNames.join("، ")); + } + } } } - // 3. Fallback for 400 Bad Request + // 5. Fallback for 400 Bad Request if (!msg && statusCode === 400) { msg = t["Some required fields are missing or invalid. Please check your answers."] || "Some required fields are missing or invalid. Please check your answers."; } - // 4. Fallback for 401 Unauthorized + // 6. Fallback for 401 Unauthorized if (!msg && statusCode === 401) { msg = t["Session expired. Please log in again."] || "Session expired. Please log in again."; } - // 5. Fallback for Network / Server Error + // 7. Fallback for Network / Server Error if (!msg) { msg = t["Internet connection error. Answers could not be synced with server. Please check your connection and tap Continue again."] || diff --git a/src/components/Componentes/question-sheet.test.tsx b/src/components/Componentes/question-sheet.test.tsx index 00ab1b9..e8ddb38 100644 --- a/src/components/Componentes/question-sheet.test.tsx +++ b/src/components/Componentes/question-sheet.test.tsx @@ -598,5 +598,77 @@ describe("QuestionSheet component", () => { expect(screen.getByText("صفت ۱, صفت ۲, صفت ۳, صفت ۴, صفت ۵")).toBeDefined(); expect(screen.queryByText(/صفت ۶/)).toBeNull(); }); + + it("disables unselected options when max 3 limit is reached in QuestionSheet", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + const multiQuestion = { + id: "personality_traits", + title: "انتخاب خصوصیات اخلاقی", + type: "dropdown", + order: 1, + required: true, + isVisible: true, + description: "", + tooltip: "", + extras: { placeHolder: "انتخاب حداکثر ۳ مورد" }, + options: [ + { id: "opt1", value: "opt1", label: "گزینه ۱", order: 1 }, + { id: "opt2", value: "opt2", label: "گزینه ۲", order: 2 }, + { id: "opt3", value: "opt3", label: "گزینه ۳", order: 3 }, + { id: "opt4", value: "opt4", label: "گزینه ۴", order: 4 }, + { id: "opt5", value: "opt5", label: "گزینه ۵", order: 5 }, + ], + ui_config: { is_multi: true, max_select: 3 }, + validation: { max: 3 }, + } as QuestionField; + + render( + + + + + , + ); + + fireEvent.click(screen.getByRole("button", { name: "انتخاب حداکثر ۳ مورد" })); + + const btn1 = screen.getByRole("button", { name: "گزینه ۱" }); + const btn2 = screen.getByRole("button", { name: "گزینه ۲" }); + const btn3 = screen.getByRole("button", { name: "گزینه ۳" }); + const btn4 = screen.getByRole("button", { name: "گزینه ۴" }); + const btn5 = screen.getByRole("button", { name: "گزینه ۵" }); + + // Initially none are disabled + expect(btn1).not.toBeDisabled(); + expect(btn4).not.toBeDisabled(); + + // Select 1 and 2 + fireEvent.click(btn1); + fireEvent.click(btn2); + expect(btn4).not.toBeDisabled(); + + // Select 3rd option (reaches limit of 3) + fireEvent.click(btn3); + + // Selected buttons remain enabled (so user can deselect) + expect(btn1).not.toBeDisabled(); + expect(btn2).not.toBeDisabled(); + expect(btn3).not.toBeDisabled(); + + // Remaining unselected buttons are disabled + expect(btn4).toBeDisabled(); + expect(btn5).toBeDisabled(); + + // Deselect option 2 + fireEvent.click(btn2); + + // Now buttons 4 and 5 become active again + expect(btn4).not.toBeDisabled(); + expect(btn5).not.toBeDisabled(); + }); }); + diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx index 6b8e65b..155c199 100644 --- a/src/components/Componentes/question-sheet.tsx +++ b/src/components/Componentes/question-sheet.tsx @@ -70,9 +70,13 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { setIsClosing(true); if (isMulti) { const currentList = localSelectedListRef.current; + const trimmedList = + maxSelect && currentList.length > maxSelect + ? currentList.slice(0, maxSelect) + : currentList; setAnswerValue( question, - currentList.length > 0 ? currentList : null, + trimmedList.length > 0 ? trimmedList : null, ); } window.setTimeout(() => { @@ -80,7 +84,7 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { setIsClosing(false); setSearchQuery(""); }, EXIT_ANIMATION_MS); - }, [isMulti, question, setAnswerValue]); + }, [isMulti, maxSelect, question, setAnswerValue]); const openSheet = useCallback(() => { if (disabled) return; @@ -341,9 +345,13 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { const handleConfirmMulti = () => { const currentList = localSelectedListRef.current; + const trimmedList = + maxSelect && currentList.length > maxSelect + ? currentList.slice(0, maxSelect) + : currentList; setAnswerValue( question, - currentList.length > 0 ? currentList : null, + trimmedList.length > 0 ? trimmedList : null, ); closeSheet(); }; @@ -449,9 +457,26 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { ].join(" ")} > - - {question.title} - + + + {question.title} + + {isMulti && Boolean(maxSelect) && ( + + {localSelectedList.length >= (maxSelect as number) + ? (locale === "fa" + ? `سقف ${maxSelect} انتخاب تکمیل شد` + : locale === "ar" + ? `تم اختيار الحد الأقصى (${maxSelect})` + : `Maximum of ${maxSelect} selected`) + : (locale === "fa" + ? `انتخاب حداکثر ${maxSelect} مورد (${localSelectedList.length}/${maxSelect})` + : locale === "ar" + ? `اختر حتى ${maxSelect} عناصر (${localSelectedList.length}/${maxSelect})` + : `Select up to ${maxSelect} (${localSelectedList.length}/${maxSelect})`)} + + )} + = maxSelect); + return ( { + if (isOptionDisabled) return; if (isMulti) { toggleMultiOption(option.id); } else { @@ -558,10 +590,12 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { } }} className={[ - "flex min-h-[52px] w-full items-start gap-3 rounded-[12px] border px-3.5 py-3 text-start transition-colors cursor-pointer", - isSelected - ? "bg-[#FFF4F5] border-[#F0445B]/40 text-[#181818]" - : "bg-white hover:bg-[#F2F4F7] border-[#EAECF0] text-[#181818]", + "flex min-h-[52px] w-full items-start gap-3 rounded-[12px] border px-3.5 py-3 text-start transition-colors", + isOptionDisabled + ? "opacity-35 cursor-not-allowed bg-[#F9FAFB] border-[#F2F4F7] text-[#98A2B3] pointer-events-none" + : isSelected + ? "bg-[#FFF4F5] border-[#F0445B]/40 text-[#181818] cursor-pointer" + : "bg-white hover:bg-[#F2F4F7] border-[#EAECF0] text-[#181818] cursor-pointer", ].join(" ")} > {/* Indicator Icon */} @@ -569,9 +603,11 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { {isSelected && ( @@ -615,10 +651,23 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { const description = parts.slice(1).join(" - "); return ( - + {title} - + {description} @@ -627,9 +676,11 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { ) : ( {option.label} diff --git a/src/components/Componentes/ui-config.test.tsx b/src/components/Componentes/ui-config.test.tsx index befe4bf..9312c90 100644 --- a/src/components/Componentes/ui-config.test.tsx +++ b/src/components/Componentes/ui-config.test.tsx @@ -516,7 +516,55 @@ describe("UI Config based behavior", () => { expect((reenteredCityInput as HTMLInputElement).value).toBe("یزد"); }); }); + + it("should normalize ui_config for photo and file questions with source and picker_type", async () => { + const { mapBackendQuestionToFrontend } = await import("@/lib/schema-adapter"); + + const photoBackendQuestion: any = { + id: "verification.face_photo", + type: "photo", + title: "عکس پرسنلی یا واضح", + required: true, + is_visible: true, + ui_config: {}, + }; + + const adaptedPhoto = mapBackendQuestionToFrontend(photoBackendQuestion, 0); + expect(adaptedPhoto.ui_config?.source).toBe("gallery"); + expect(adaptedPhoto.ui_config?.picker_type).toBe("gallery"); + + const fileBackendQuestion: any = { + id: "verification.identity_document", + type: "file", + title: "بارگذاری مدرک هویتی", + required: true, + is_visible: true, + ui_config: {}, + }; + + const adaptedFile = mapBackendQuestionToFrontend(fileBackendQuestion, 1); + expect(adaptedFile.ui_config?.source).toBe("file_system"); + expect(adaptedFile.ui_config?.picker_type).toBe("file_system"); + }); + + it("should preserve custom source and picker_type from backend ui_config", async () => { + const { mapBackendQuestionToFrontend } = await import("@/lib/schema-adapter"); + + const customPhoto: any = { + id: "verification.face_photo", + type: "photo", + title: "عکس سلفی دوربین", + required: true, + is_visible: true, + ui_config: { source: "camera", picker_type: "camera" }, + }; + + const adaptedCustomPhoto = mapBackendQuestionToFrontend(customPhoto, 0); + expect(adaptedCustomPhoto.ui_config?.source).toBe("camera"); + expect(adaptedCustomPhoto.ui_config?.picker_type).toBe("camera"); + }); }); + diff --git a/src/hooks/marriage/use-form-schema.ts b/src/hooks/marriage/use-form-schema.ts index a1eabd4..6a83e85 100644 --- a/src/hooks/marriage/use-form-schema.ts +++ b/src/hooks/marriage/use-form-schema.ts @@ -167,18 +167,22 @@ export function useFormSchemaQuery( } export interface SaveAnswersPayload { - answers: Array<{ - question_id: string; - value: any; - option_id?: string | string[]; - }>; + section_slug?: string; + answers: + | Array<{ + question_id: string; + value: any; + option_id?: string | string[]; + }> + | Record; + version?: number; } export async function saveFormAnswers( formId: string, payload: SaveAnswersPayload, ): Promise { - const { data } = await http.put( + const { data } = await http.post( `/api/marriage/forms/${formId}/answers/`, payload, ); diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts index 15397c7..5ae2056 100644 --- a/src/lib/schema-adapter.ts +++ b/src/lib/schema-adapter.ts @@ -215,6 +215,15 @@ export function mapBackendQuestionToFrontend( range = [Number(uiRange[0]), Number(uiRange[1])]; } + const uiConfig = { ...(bq.ui_config || {}) }; + if (bq.type === "photo") { + if (!uiConfig.source) uiConfig.source = "gallery"; + if (!uiConfig.picker_type) uiConfig.picker_type = uiConfig.source || "gallery"; + } else if (bq.type === "file") { + if (!uiConfig.source) uiConfig.source = "file_system"; + if (!uiConfig.picker_type) uiConfig.picker_type = uiConfig.source || "file_system"; + } + return { id: bq.id, title: bq.title || "Untitled", @@ -230,7 +239,7 @@ export function mapBackendQuestionToFrontend( ? bq.is_private : bq.ui_config?.private, validation: bq.validation, - ui_config: bq.ui_config, + ui_config: uiConfig, description: bq.description || "", tooltip: bq.tooltip || "", extras: { diff --git a/src/lib/webview-actions.ts b/src/lib/webview-actions.ts index b2f2738..8382443 100644 --- a/src/lib/webview-actions.ts +++ b/src/lib/webview-actions.ts @@ -118,6 +118,7 @@ export interface UploadFileOptions { requestId?: string | number; mediaType: "image" | "video" | "image+video" | "audio" | "file"; source?: "gallery" | "camera" | "any" | "file_system"; + picker_type?: "gallery" | "camera" | "any" | "file_system" | string; multiple?: boolean; returnAs?: "upload" | "base64"; uploadUrl?: string; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index e5f6cad..e815e3d 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -66,6 +66,7 @@ declare global { // upload_file mediaType?: string; source?: string; + picker_type?: string; files?: Array<{ url?: string; path?: string;