From 7b9e09ffece031c309f5ad385b9d33a6dcdaedeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 13 Feb 2026 01:16:59 -0300 Subject: [PATCH 1/3] feat: add new formik input for file size, add it on page modules for media MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui-formik-file-size-field.test.js | 116 ++++++++++++++++++ .../mui-formik-file-size-field.js | 70 +++++++++++ .../page-template-media-request-module.js | 4 +- 3 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 src/components/mui/__tests__/mui-formik-file-size-field.test.js create mode 100644 src/components/mui/formik-inputs/mui-formik-file-size-field.js diff --git a/src/components/mui/__tests__/mui-formik-file-size-field.test.js b/src/components/mui/__tests__/mui-formik-file-size-field.test.js new file mode 100644 index 000000000..debdc090e --- /dev/null +++ b/src/components/mui/__tests__/mui-formik-file-size-field.test.js @@ -0,0 +1,116 @@ +import React from "react"; +import { render, screen, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import MuiFormikFilesizeField from "../formik-inputs/mui-formik-file-size-field"; + +const BYTES_PER_MB = 1_000_000; + +const renderWithFormik = (props, initialValues = { max_file_size: 0 }) => + render( + +
+ + + +
+ ); + +describe("MuiFormikFilesizeField", () => { + describe("display and store", () => { + it("converts MB input to bytes", async () => { + const onSubmit = jest.fn(); + renderWithFormik({ + label: "Max File Size", + onSubmit + }); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.type(field, "10"); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + max_file_size: 10 * BYTES_PER_MB + }), + expect.anything() + ); + }); + + it("displays bytes as MB", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { + label: "Max File Size", + onSubmit + }, + { max_file_size: 15_000_000 } + ); + + const field = screen.getByLabelText("Max File Size"); + expect(field).toHaveValue(15); + }); + }); + + describe("Empty Value Handling", () => { + it("auto-detects null emptyValue from initialValues", async () => { + const onSubmit = jest.fn(); + renderWithFormik( + { + label: "Max File Size", + onSubmit + }, + { max_file_size: null } + ); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.type(field, "5"); + await userEvent.clear(field); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + max_file_size: null + }), + expect.anything() + ); + }); + + it("auto-detects string emptyValue from initialValues", async () => { + const onSubmit = jest.fn(); + + renderWithFormik( + { + label: "Max File Size", + onSubmit + }, + { max_file_size: "" } + ); + + const field = screen.getByLabelText("Max File Size"); + const submitButton = screen.getByText("submit"); + + await act(async () => { + await userEvent.type(field, "5"); + await userEvent.clear(field); + await userEvent.click(submitButton); + }); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ + max_file_size: "" + }), + expect.anything() + ); + }); + }); +}); diff --git a/src/components/mui/formik-inputs/mui-formik-file-size-field.js b/src/components/mui/formik-inputs/mui-formik-file-size-field.js new file mode 100644 index 000000000..bedb92ccd --- /dev/null +++ b/src/components/mui/formik-inputs/mui-formik-file-size-field.js @@ -0,0 +1,70 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { InputAdornment } from "@mui/material"; +import { useField, useFormikContext } from "formik"; +import MuiFormikTextField from "./mui-formik-textfield"; + +const BLOCKED_KEYS = ["e", "E", "+", "-", ".", ","]; +const BYTES_PER_MB = 1_000_000; + +const MuiFormikFilesizeField = ({ name, label, ...props }) => { + const [field, meta, helpers] = useField(name); + const { initialValues } = useFormikContext(); + + const displayValue = + field.value !== null && field.value !== "" + ? Math.floor(field.value / BYTES_PER_MB) + : ""; + + const emptyValue = initialValues[name] === null ? null : ""; + + const handleChange = (e) => { + const mbValue = e.target.value; + + if (mbValue === "" || mbValue === null || mbValue === undefined) { + helpers.setValue(emptyValue); + return; + } + + const bytes = Number(mbValue) * BYTES_PER_MB; + helpers.setValue(bytes); + }; + + return ( + helpers.setTouched(true)} + error={meta.touched && Boolean(meta.error)} + helperText={meta.touched && meta.error} + slotProps={{ + input: { + endAdornment: MB + } + }} + onKeyDown={(e) => { + if (BLOCKED_KEYS.includes(e.key)) { + e.nativeEvent.preventDefault(); + e.nativeEvent.stopImmediatePropagation(); + } + }} + inputProps={{ + min: 0, + inputMode: "numeric", + step: 1 + }} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); +}; + +MuiFormikFilesizeField.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string +}; + +export default MuiFormikFilesizeField; diff --git a/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js index 24269887a..60a2261f4 100644 --- a/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js +++ b/src/pages/sponsors-global/page-templates/page-template-popup/modules/page-template-media-request-module.js @@ -6,6 +6,7 @@ import { Grid2, Divider, InputLabel } from "@mui/material"; import MuiFormikTextField from "../../../../../components/mui/formik-inputs/mui-formik-textfield"; import MuiFormikDatepicker from "../../../../../components/mui/formik-inputs/mui-formik-datepicker"; import MuiFormikRadioGroup from "../../../../../components/mui/formik-inputs/mui-formik-radio-group"; +import MuiFormikFilesizeField from "../../../../../components/mui/formik-inputs/mui-formik-file-size-field"; import { PAGE_MODULES_MEDIA_TYPES } from "../../../../../utils/constants"; import MuiFormikAsyncAutocomplete from "../../../../../components/mui/formik-inputs/mui-formik-async-select"; import { queryMediaFileTypes } from "../../../../../actions/media-file-type-actions"; @@ -67,9 +68,8 @@ const MediaRequestModule = ({ baseName, index }) => { {T.translate("page_template_list.page_crud.max_file_size")} - From ca45888b354e8c984ea16f5b0d46d37e78ec78c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 13 Feb 2026 09:18:11 -0300 Subject: [PATCH 2/3] fix: add formik getIn to obtain initialValues, avoid undefined values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui/formik-inputs/mui-formik-file-size-field.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/mui/formik-inputs/mui-formik-file-size-field.js b/src/components/mui/formik-inputs/mui-formik-file-size-field.js index bedb92ccd..c6c5cfb03 100644 --- a/src/components/mui/formik-inputs/mui-formik-file-size-field.js +++ b/src/components/mui/formik-inputs/mui-formik-file-size-field.js @@ -1,7 +1,7 @@ import React from "react"; import PropTypes from "prop-types"; import { InputAdornment } from "@mui/material"; -import { useField, useFormikContext } from "formik"; +import { useField, useFormikContext, getIn } from "formik"; import MuiFormikTextField from "./mui-formik-textfield"; const BLOCKED_KEYS = ["e", "E", "+", "-", ".", ","]; @@ -12,11 +12,11 @@ const MuiFormikFilesizeField = ({ name, label, ...props }) => { const { initialValues } = useFormikContext(); const displayValue = - field.value !== null && field.value !== "" + field.value !== null && field.value !== "" && field.value !== undefined ? Math.floor(field.value / BYTES_PER_MB) : ""; - const emptyValue = initialValues[name] === null ? null : ""; + const emptyValue = getIn(initialValues, name) === null ? null : ""; const handleChange = (e) => { const mbValue = e.target.value; From 03dc271d73dfc4ada7696a7bb1cb31bed92e926a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Sat, 14 Feb 2026 01:11:32 -0300 Subject: [PATCH 3/3] fix: simplify component validations, default value 0, adjust mb size constant and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../mui-formik-file-size-field.test.js | 63 +------------------ .../mui-formik-file-size-field.js | 16 ++--- src/utils/constants.js | 4 +- 3 files changed, 11 insertions(+), 72 deletions(-) diff --git a/src/components/mui/__tests__/mui-formik-file-size-field.test.js b/src/components/mui/__tests__/mui-formik-file-size-field.test.js index debdc090e..fc00b79fd 100644 --- a/src/components/mui/__tests__/mui-formik-file-size-field.test.js +++ b/src/components/mui/__tests__/mui-formik-file-size-field.test.js @@ -4,8 +4,7 @@ import userEvent from "@testing-library/user-event"; import { Formik, Form } from "formik"; import "@testing-library/jest-dom"; import MuiFormikFilesizeField from "../formik-inputs/mui-formik-file-size-field"; - -const BYTES_PER_MB = 1_000_000; +import { BYTES_PER_MB } from "../../../utils/constants"; const renderWithFormik = (props, initialValues = { max_file_size: 0 }) => render( @@ -30,6 +29,7 @@ describe("MuiFormikFilesizeField", () => { const submitButton = screen.getByText("submit"); await act(async () => { + await userEvent.clear(field); // field initializes with 0 await userEvent.type(field, "10"); await userEvent.click(submitButton); }); @@ -49,68 +49,11 @@ describe("MuiFormikFilesizeField", () => { label: "Max File Size", onSubmit }, - { max_file_size: 15_000_000 } + { max_file_size: 15_728_640 } // 15 * 1_048_576 ); const field = screen.getByLabelText("Max File Size"); expect(field).toHaveValue(15); }); }); - - describe("Empty Value Handling", () => { - it("auto-detects null emptyValue from initialValues", async () => { - const onSubmit = jest.fn(); - renderWithFormik( - { - label: "Max File Size", - onSubmit - }, - { max_file_size: null } - ); - - const field = screen.getByLabelText("Max File Size"); - const submitButton = screen.getByText("submit"); - - await act(async () => { - await userEvent.type(field, "5"); - await userEvent.clear(field); - await userEvent.click(submitButton); - }); - - expect(onSubmit).toHaveBeenCalledWith( - expect.objectContaining({ - max_file_size: null - }), - expect.anything() - ); - }); - - it("auto-detects string emptyValue from initialValues", async () => { - const onSubmit = jest.fn(); - - renderWithFormik( - { - label: "Max File Size", - onSubmit - }, - { max_file_size: "" } - ); - - const field = screen.getByLabelText("Max File Size"); - const submitButton = screen.getByText("submit"); - - await act(async () => { - await userEvent.type(field, "5"); - await userEvent.clear(field); - await userEvent.click(submitButton); - }); - - expect(onSubmit).toHaveBeenCalledWith( - expect.objectContaining({ - max_file_size: "" - }), - expect.anything() - ); - }); - }); }); diff --git a/src/components/mui/formik-inputs/mui-formik-file-size-field.js b/src/components/mui/formik-inputs/mui-formik-file-size-field.js index c6c5cfb03..e6025bb6c 100644 --- a/src/components/mui/formik-inputs/mui-formik-file-size-field.js +++ b/src/components/mui/formik-inputs/mui-formik-file-size-field.js @@ -1,27 +1,24 @@ import React from "react"; import PropTypes from "prop-types"; import { InputAdornment } from "@mui/material"; -import { useField, useFormikContext, getIn } from "formik"; +import { useField } from "formik"; import MuiFormikTextField from "./mui-formik-textfield"; +import { BYTES_PER_MB } from "../../../utils/constants"; const BLOCKED_KEYS = ["e", "E", "+", "-", ".", ","]; -const BYTES_PER_MB = 1_000_000; const MuiFormikFilesizeField = ({ name, label, ...props }) => { const [field, meta, helpers] = useField(name); - const { initialValues } = useFormikContext(); const displayValue = - field.value !== null && field.value !== "" && field.value !== undefined - ? Math.floor(field.value / BYTES_PER_MB) - : ""; + field.value != null ? Math.floor(field.value / BYTES_PER_MB) : 0; - const emptyValue = getIn(initialValues, name) === null ? null : ""; + const emptyValue = meta.initialValue === null ? null : 0; const handleChange = (e) => { const mbValue = e.target.value; - if (mbValue === "" || mbValue === null || mbValue === undefined) { + if (mbValue === "") { helpers.setValue(emptyValue); return; } @@ -37,9 +34,6 @@ const MuiFormikFilesizeField = ({ name, label, ...props }) => { type="number" value={displayValue} onChange={handleChange} - onBlur={() => helpers.setTouched(true)} - error={meta.touched && Boolean(meta.error)} - helperText={meta.touched && meta.error} slotProps={{ input: { endAdornment: MB diff --git a/src/utils/constants.js b/src/utils/constants.js index 738fefa11..3983fbeb5 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -163,6 +163,8 @@ export const LANGUAGE_CODE_LENGTH = 2; export const SLICE_TICKET_NUMBER = -15; +export const BYTES_PER_MB = 1_048_576; // 1024 * 1024 + export const MARKETING_SETTING_TYPE_TEXT = "TEXT"; export const MARKETING_SETTING_TYPE_TEXTAREA = "TEXTAREA"; export const MARKETING_SETTING_TYPE_FILE = "FILE"; @@ -256,7 +258,7 @@ export const PURCHASE_STATUS = { PENDING: "Pending", PAID: "Paid", CANCELLED: "Cancelled" -} +}; export const SPONSOR_USER_ASSIGNMENT_TYPE = { EXISTING: "existing",