From 0a25df65c34301d91388af39f8f4352835e0fe62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Mon, 5 Jan 2026 10:27:36 -0300 Subject: [PATCH 1/2] feat: use formik on additional input list, unify use, add unit tests, adjust actions urls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/actions/form-template-actions.js | 2 +- src/actions/form-template-item-actions.js | 2 +- src/actions/inventory-item-actions.js | 2 +- src/actions/inventory-shared-actions.js | 2 +- .../__tests__/additional-input-list.test.js | 348 ++++++++++++++++++ .../mui/__tests__/additional-input.test.js | 253 +++++++++++++ .../mui/__tests__/meta-field-values.test.js | 319 ++++++++++++++++ .../additional-input/additional-input-list.js | 98 +++++ .../additional-input/additional-input.js | 185 ++++++++++ .../additional-input/meta-field-values.js | 187 ++++++++++ .../mui/formik-inputs/mui-formik-select.js | 14 +- src/i18n/en.json | 8 +- .../components/additional-input-list.js | 130 ------- .../sponsors/components/additional-input.js | 144 -------- .../components/item-form.js | 2 +- .../form-template/form-template-form.js | 2 +- .../customized-form/customized-form.js | 2 +- .../sponsors_inventory/inventory-list-page.js | 21 +- .../popup/form-template-popup.js | 249 +------------ .../popup/meta-field-values.js | 211 ----------- .../popup/sponsor-inventory-popup.js | 263 +------------ 21 files changed, 1445 insertions(+), 999 deletions(-) create mode 100644 src/components/mui/__tests__/additional-input-list.test.js create mode 100644 src/components/mui/__tests__/additional-input.test.js create mode 100644 src/components/mui/__tests__/meta-field-values.test.js create mode 100644 src/components/mui/formik-inputs/additional-input/additional-input-list.js create mode 100644 src/components/mui/formik-inputs/additional-input/additional-input.js create mode 100644 src/components/mui/formik-inputs/additional-input/meta-field-values.js delete mode 100644 src/pages/sponsors/components/additional-input-list.js delete mode 100644 src/pages/sponsors/components/additional-input.js delete mode 100644 src/pages/sponsors_inventory/popup/meta-field-values.js diff --git a/src/actions/form-template-actions.js b/src/actions/form-template-actions.js index 08ad83393..9122f1430 100644 --- a/src/actions/form-template-actions.js +++ b/src/actions/form-template-actions.js @@ -315,7 +315,7 @@ export const deleteFormTemplateMetaFieldTypeValue = ( valueId ) => { const settings = { - url: `${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${templateId}/meta-field-types/${metaFieldId}/values`, + url: `${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${templateId}/meta-field-types/${metaFieldId}/values/`, deletedActionName: FORM_TEMPLATE_META_FIELD_VALUE_DELETED }; return deleteMetaFieldTypeValue(metaFieldId, valueId, settings); diff --git a/src/actions/form-template-item-actions.js b/src/actions/form-template-item-actions.js index 7d961f51b..8053f7957 100644 --- a/src/actions/form-template-item-actions.js +++ b/src/actions/form-template-item-actions.js @@ -375,7 +375,7 @@ export const deleteItemMetaFieldTypeValue = ( valueId ) => { const settings = { - url: `${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${formTemplateId}/items/${formTemplateItemId}/meta-field-types/${metaFieldId}/values`, + url: `${window.INVENTORY_API_BASE_URL}/api/v1/form-templates/${formTemplateId}/items/${formTemplateItemId}/meta-field-types/${metaFieldId}/values/`, deletedActionName: FORM_TEMPLATE_ITEM_META_FIELD_VALUE_DELETED }; return deleteMetaFieldTypeValue(metaFieldId, valueId, settings); diff --git a/src/actions/inventory-item-actions.js b/src/actions/inventory-item-actions.js index 29e9e047f..0baa61e39 100644 --- a/src/actions/inventory-item-actions.js +++ b/src/actions/inventory-item-actions.js @@ -352,7 +352,7 @@ export const deleteInventoryItemMetaFieldTypeValue = ( valueId ) => { const settings = { - url: `${window.INVENTORY_API_BASE_URL}/api/v1/inventory-items/${inventoryItemId}/meta-field-types/${metaFieldId}/values`, + url: `${window.INVENTORY_API_BASE_URL}/api/v1/inventory-items/${inventoryItemId}/meta-field-types/${metaFieldId}/values/`, deletedActionName: INVENTORY_ITEM_META_FIELD_VALUE_DELETED }; return deleteMetaFieldTypeValue(metaFieldId, valueId, settings); diff --git a/src/actions/inventory-shared-actions.js b/src/actions/inventory-shared-actions.js index 34bdc4af0..64de746a7 100644 --- a/src/actions/inventory-shared-actions.js +++ b/src/actions/inventory-shared-actions.js @@ -207,7 +207,7 @@ export const deleteMetaFieldTypeValue = metaFieldId, valueId }), - `${settings.url}${valueId}/`, + `${settings.url}${valueId}`, null, authErrorHandler )(params)(dispatch).then(() => { diff --git a/src/components/mui/__tests__/additional-input-list.test.js b/src/components/mui/__tests__/additional-input-list.test.js new file mode 100644 index 000000000..923a7bd37 --- /dev/null +++ b/src/components/mui/__tests__/additional-input-list.test.js @@ -0,0 +1,348 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form, useFormikContext } from "formik"; +import "@testing-library/jest-dom"; +import AdditionalInputList from "../formik-inputs/additional-input/additional-input-list"; +import showConfirmDialog from "../showConfirmDialog"; + +// Mocks +jest.mock("../showConfirmDialog", () => jest.fn()); + +jest.mock( + "../formik-inputs/additional-input/additional-input", + () => + function MockAdditionalInput({ + item, + itemIdx, + onAdd, + onDelete, + isAddDisabled + }) { + return ( +
+ {item.name} + {item.type} + + +
+ ); + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("AdditionalInputList", () => { + const defaultMetaField = { + id: 1, + name: "Field 1", + type: "Text", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] + }; + + const defaultProps = { + name: "meta_fields", + onDelete: jest.fn(), + onDeleteValue: jest.fn(), + entityId: 1 + }; + + const defaultInitialValues = { + meta_fields: [defaultMetaField] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders an AdditionalInput for each meta field", () => { + const multipleFields = { + meta_fields: [ + { ...defaultMetaField, id: 1, name: "Field 1" }, + { ...defaultMetaField, id: 2, name: "Field 2" }, + { ...defaultMetaField, id: 3, name: "Field 3" } + ] + }; + + renderWithFormik(defaultProps, multipleFields); + + expect(screen.getByTestId("additional-input-0")).toBeInTheDocument(); + expect(screen.getByTestId("additional-input-1")).toBeInTheDocument(); + expect(screen.getByTestId("additional-input-2")).toBeInTheDocument(); + expect(screen.getByTestId("item-name-0")).toHaveTextContent("Field 1"); + expect(screen.getByTestId("item-name-1")).toHaveTextContent("Field 2"); + expect(screen.getByTestId("item-name-2")).toHaveTextContent("Field 3"); + }); + + test("renders nothing when meta_fields is empty", () => { + renderWithFormik(defaultProps, { meta_fields: [] }); + + expect( + screen.queryByTestId("additional-input-0") + ).not.toBeInTheDocument(); + }); + }); + + describe("handleAddItem", () => { + test("adds a new empty meta field when onAdd is called", async () => { + const TestWrapper = () => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + + const addButton = screen.getByTestId("add-btn-0"); + await userEvent.click(addButton); + + await waitFor(() => { + expect(screen.getByTestId("field-count")).toHaveTextContent("2"); + }); + }); + }); + + describe("handleRemove", () => { + test("shows confirmation dialog when delete is clicked", async () => { + showConfirmDialog.mockResolvedValue(false); + + renderWithFormik(defaultProps, defaultInitialValues); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + expect(showConfirmDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + type: "warning" + }) + ); + }); + + test("calls API and removes from UI when item has id", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(mockOnDelete).toHaveBeenCalledWith(1, 1); // entityId, item.id + }); + }); + + test("removes from UI without API call when item has no id", async () => { + const mockOnDelete = jest.fn(); + showConfirmDialog.mockResolvedValue(true); + + const fieldWithoutId = { + name: "New Field", + type: "Text", + is_required: false, + values: [] + }; + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+ + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getByTestId("field-count")).toHaveTextContent("2"); + + // remove the second field (without id) + const deleteButton = screen.getByTestId("delete-btn-1"); + await userEvent.click(deleteButton); + + await waitFor(() => { + expect(mockOnDelete).not.toHaveBeenCalled(); + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + }); + }); + + test("resets to default meta field when last item is deleted", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + return ( + <> + +
{values.meta_fields.length}
+
+ {values.meta_fields[0]?.name || "empty"} +
+ + ); + }; + + render( + +
+ + +
+ ); + + const deleteButton = screen.getByTestId("delete-btn-0"); + await userEvent.click(deleteButton); + + await waitFor(() => { + // should still have 1 field (the default empty one) + expect(screen.getByTestId("field-count")).toHaveTextContent("1"); + // field should be reset to empty + expect(screen.getByTestId("first-field-name")).toHaveTextContent( + "empty" + ); + }); + }); + }); + + describe("areMetafieldsIncomplete", () => { + test("disables add button when name is empty", () => { + const fieldWithEmptyName = { ...defaultMetaField, name: "" }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithEmptyName] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when type is empty", () => { + const fieldWithEmptyType = { + ...defaultMetaField, + name: "Field", + type: "" + }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithEmptyType] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when type with options has no values", () => { + const fieldWithNoValues = { + ...defaultMetaField, + name: "Field", + type: "CheckBoxList", + values: [] + }; + + renderWithFormik(defaultProps, { meta_fields: [fieldWithNoValues] }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("disables add button when values are incomplete", () => { + const fieldWithIncompleteValues = { + ...defaultMetaField, + name: "Field", + type: "ComboBox", + values: [{ name: "Option", value: "" }] // value is empty + }; + + renderWithFormik(defaultProps, { + meta_fields: [fieldWithIncompleteValues] + }); + + expect(screen.getByTestId("add-btn-0")).toBeDisabled(); + }); + + test("enables add button when all fields are complete", () => { + const completeField = { + ...defaultMetaField, + name: "Field", + type: "Text" + }; + + renderWithFormik(defaultProps, { meta_fields: [completeField] }); + + expect(screen.getByTestId("add-btn-0")).not.toBeDisabled(); + }); + + test("enables add button when type with options has complete values", () => { + const completeFieldWithValues = { + ...defaultMetaField, + name: "Field", + type: "CheckBoxList", + values: [{ name: "Option 1", value: "opt1" }] + }; + + renderWithFormik(defaultProps, { + meta_fields: [completeFieldWithValues] + }); + + expect(screen.getByTestId("add-btn-0")).not.toBeDisabled(); + }); + }); +}); diff --git a/src/components/mui/__tests__/additional-input.test.js b/src/components/mui/__tests__/additional-input.test.js new file mode 100644 index 000000000..de283043d --- /dev/null +++ b/src/components/mui/__tests__/additional-input.test.js @@ -0,0 +1,253 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form } from "formik"; +import "@testing-library/jest-dom"; +import AdditionalInput from "../formik-inputs/additional-input/additional-input"; + +// Mocks +jest.mock( + "../formik-inputs/additional-input/meta-field-values", + () => + function MockMetaFieldValues() { + return
MetaFieldValues
; + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("AdditionalInput", () => { + const defaultItem = { + id: 1, + name: "Test Field", + type: "", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] + }; + + const defaultProps = { + item: defaultItem, + itemIdx: 0, + baseName: "meta_fields", + onAdd: jest.fn(), + onDelete: jest.fn(), + onDeleteValue: jest.fn(), + entityId: 1, + isAddDisabled: false + }; + + const defaultInitialMetaFields = { + meta_fields: [defaultItem] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders name, type and is_required fields", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.meta_field_title" + ) + ).toBeInTheDocument(); + expect(screen.getByRole("combobox")).toBeInTheDocument(); + expect(screen.getByRole("checkbox")).toBeInTheDocument(); + }); + + test("renders add and delete buttons", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.getByRole("button", { name: /delete/i }) + ).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /add/i })).toBeInTheDocument(); + }); + }); + + describe("Conditional rendering based on type", () => { + test("shows MetaFieldValues when type is CheckBoxList", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("shows MetaFieldValues when type is ComboBox", () => { + const itemWithOptions = { ...defaultItem, type: "ComboBox" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("shows MetaFieldValues when type is RadioButtonList", () => { + const itemWithOptions = { ...defaultItem, type: "RadioButtonList" }; + + renderWithFormik( + { ...defaultProps, item: itemWithOptions }, + { meta_fields: [itemWithOptions] } + ); + + expect(screen.getByTestId("meta-field-values")).toBeInTheDocument(); + }); + + test("does not show MetaFieldValues when type is Text", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect(screen.queryByTestId("meta-field-values")).not.toBeInTheDocument(); + }); + + test("shows quantity fields when type is Quantity", () => { + const itemQuantity = { ...defaultItem, type: "Quantity" }; + + renderWithFormik( + { ...defaultProps, item: itemQuantity }, + { meta_fields: [itemQuantity] } + ); + + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.meta_field_minimum_quantity" + ) + ).toBeInTheDocument(); + expect( + screen.getByPlaceholderText( + "additional_inputs.placeholders.meta_field_maximum_quantity" + ) + ).toBeInTheDocument(); + }); + + test("does not show quantity fields when type is not Quantity", () => { + renderWithFormik(defaultProps, defaultInitialMetaFields); + + expect( + screen.queryByPlaceholderText( + "additional_inputs.placeholders.meta_field_minimum_quantity" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByPlaceholderText( + "additional_inputs.placeholders.meta_field_maximum_quantity" + ) + ).not.toBeInTheDocument(); + }); + }); + + describe("Button interactions", () => { + test("calls onDelete with item and index when delete button is clicked", async () => { + const mockOnDelete = jest.fn(); + + renderWithFormik( + { ...defaultProps, onDelete: mockOnDelete }, + defaultInitialMetaFields + ); + + const deleteButton = screen.getByRole("button", { name: /delete/i }); + await userEvent.click(deleteButton); + + expect(mockOnDelete).toHaveBeenCalledWith(defaultItem, 0); + }); + + test("calls onAdd when add button is clicked", async () => { + const mockOnAdd = jest.fn(); + + renderWithFormik( + { ...defaultProps, onAdd: mockOnAdd }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + await userEvent.click(addButton); + + expect(mockOnAdd).toHaveBeenCalled(); + }); + + test("disables add button when isAddDisabled is true", () => { + renderWithFormik( + { ...defaultProps, isAddDisabled: true }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("enables add button when isAddDisabled is false", () => { + renderWithFormik( + { ...defaultProps, isAddDisabled: false }, + defaultInitialMetaFields + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + }); + + describe("Error display", () => { + test("shows values error when touched and has error", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + render( + +
+ + +
+ ); + + expect( + screen.getByText("At least one option required") + ).toBeInTheDocument(); + }); + + test("does not show values error when not touched", () => { + const itemWithOptions = { ...defaultItem, type: "CheckBoxList" }; + + render( + +
+ + +
+ ); + + expect( + screen.queryByText("At least one option required") + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/components/mui/__tests__/meta-field-values.test.js b/src/components/mui/__tests__/meta-field-values.test.js new file mode 100644 index 000000000..36b8950ed --- /dev/null +++ b/src/components/mui/__tests__/meta-field-values.test.js @@ -0,0 +1,319 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Formik, Form, useFormikContext } from "formik"; +import "@testing-library/jest-dom"; +import MetaFieldValues from "../formik-inputs/additional-input/meta-field-values"; +import showConfirmDialog from "../showConfirmDialog"; + +// Mocks +jest.mock("../showConfirmDialog", () => jest.fn()); + +jest.mock( + "../dnd-list", + () => + function MockDragAndDropList({ items, renderItem }) { + return ( +
+ {items.map((item, index) => ( +
+ {renderItem(item, index, {}, { isDragging: false })} +
+ ))} +
+ ); + } +); + +// Helper function to render the component with Formik +const renderWithFormik = (props, initialValues = { meta_fields: [] }) => + render( + +
+ + +
+ ); + +describe("MetaFieldValues", () => { + const defaultField = { + id: 1, + name: "Test Field", + type: "CheckBoxList", + values: [ + { id: 101, name: "Option 1", value: "opt1", is_default: false, order: 1 }, + { id: 102, name: "Option 2", value: "opt2", is_default: true, order: 2 } + ] + }; + + const defaultProps = { + field: defaultField, + fieldIndex: 0, + baseName: "meta_fields", + onMetaFieldTypeValueDeleted: jest.fn(), + entityId: 1 + }; + + const defaultInitialValues = { + meta_fields: [defaultField] + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("Rendering", () => { + test("renders all field values sorted by order prop", () => { + const fieldWithUnorderedValues = { + ...defaultField, + values: [ + { id: 103, name: "Option 3", value: "opt3", order: 3 }, + { id: 101, name: "Option 1", value: "opt1", order: 1 }, + { id: 102, name: "Option 2", value: "opt2", order: 2 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithUnorderedValues }, + { meta_fields: [fieldWithUnorderedValues] } + ); + + // verify all values are rendered + const items = screen.getAllByPlaceholderText( + "edit_inventory_item.placeholders.meta_field_name" + ); + expect(items).toHaveLength(3); + + // verify the values are rendered using the order prop + expect(items[0]).toHaveValue("Option 1"); + expect(items[1]).toHaveValue("Option 2"); + expect(items[2]).toHaveValue("Option 3"); + }); + }); + + describe("handleAddValue", () => { + test("adds a new empty value when add button is clicked", async () => { + // Componente wrapper que sincroniza field con Formik + const TestWrapper = () => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + // Verificar cantidad inicial + const initialInputs = screen.getAllByPlaceholderText( + "edit_inventory_item.placeholders.meta_field_name" + ); + expect(initialInputs).toHaveLength(2); + + // Click en agregar + const addButton = screen.getByRole("button", { name: /add/i }); + await userEvent.click(addButton); + + // Esperar actualización + await waitFor(() => { + const updatedInputs = screen.getAllByPlaceholderText( + "edit_inventory_item.placeholders.meta_field_name" + ); + expect(updatedInputs).toHaveLength(3); + }); + }); + }); + + describe("isMetafieldValueIncomplete", () => { + test("disables add button when a value name is empty", () => { + const fieldWithIncomplete = { + ...defaultField, + values: [ + { id: 101, name: "", value: "opt1", is_default: false, order: 1 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithIncomplete }, + { meta_fields: [fieldWithIncomplete] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("disables add button when a value is empty", () => { + const fieldWithIncomplete = { + ...defaultField, + values: [ + { id: 101, name: "Option", value: "", is_default: false, order: 1 } + ] + }; + + renderWithFormik( + { ...defaultProps, field: fieldWithIncomplete }, + { meta_fields: [fieldWithIncomplete] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).toBeDisabled(); + }); + + test("enables add button when all values are complete", () => { + renderWithFormik(defaultProps, defaultInitialValues); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + + test("enables add button when there are no values", () => { + const fieldWithNoValues = { ...defaultField, values: [] }; + + renderWithFormik( + { ...defaultProps, field: fieldWithNoValues }, + { meta_fields: [fieldWithNoValues] } + ); + + const addButton = screen.getByRole("button", { name: /add/i }); + expect(addButton).not.toBeDisabled(); + }); + }); + + describe("handleDefaultChange", () => { + test("only one value can be default at a time", async () => { + renderWithFormik(defaultProps, defaultInitialValues); + + const checkboxes = screen.getAllByRole("checkbox"); + + // Option 2 is default + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[0]).not.toBeChecked(); + + // click on Option 1 + await userEvent.click(checkboxes[0]); + + // Option 1 should be checked and Option 2 unchecked + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + }); + }); + + describe("handleRemoveValue", () => { + test("shows confirmation dialog when remove is clicked", async () => { + showConfirmDialog.mockResolvedValue(false); + renderWithFormik(defaultProps, defaultInitialValues); + + const closeIcons = screen.getAllByTestId("CloseIcon"); + const closeButton = closeIcons[0].closest("button"); + await userEvent.click(closeButton); + + expect(showConfirmDialog).toHaveBeenCalledWith( + expect.objectContaining({ + title: expect.any(String), + type: "warning" + }) + ); + }); + + test("calls API and removes from UI when value has id", async () => { + const mockOnDelete = jest.fn().mockResolvedValue(); + showConfirmDialog.mockResolvedValue(true); + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(2); + + const closeButton = screen + .getAllByTestId("CloseIcon")[0] + .closest("button"); + await userEvent.click(closeButton); + + await waitFor(() => { + expect(mockOnDelete).toHaveBeenCalledWith(1, 1, 101); + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(1); + }); + }); + + test("removes from UI without API call when value has no id", async () => { + const mockOnDelete = jest.fn(); + showConfirmDialog.mockResolvedValue(true); + + const fieldWithoutIds = { + ...defaultField, + values: [ + { name: "Option 1", value: "opt1", is_default: false, order: 1 }, + { name: "Option 2", value: "opt2", is_default: false, order: 2 } + ] + }; + + const TestWrapper = ({ onDelete }) => { + const { values } = useFormikContext(); + const field = values.meta_fields[0]; + return ( + + ); + }; + + render( + +
+ + +
+ ); + + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(2); + + const closeButton = screen + .getAllByTestId("CloseIcon")[0] + .closest("button"); + await userEvent.click(closeButton); + + await waitFor(() => { + expect(mockOnDelete).not.toHaveBeenCalled(); + expect(screen.getAllByTestId("CloseIcon")).toHaveLength(1); + }); + }); + }); +}); diff --git a/src/components/mui/formik-inputs/additional-input/additional-input-list.js b/src/components/mui/formik-inputs/additional-input/additional-input-list.js new file mode 100644 index 000000000..541732502 --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/additional-input-list.js @@ -0,0 +1,98 @@ +import React from "react"; +import { useFormikContext, getIn } from "formik"; +import T from "i18n-react"; +import AdditionalInput from "./additional-input"; +import showConfirmDialog from "../../showConfirmDialog"; +import { METAFIELD_TYPES_WITH_OPTIONS } from "../../../../utils/constants"; + +const DEFAULT_META_FIELD = { + name: "", + type: "", + is_required: false, + minimum_quantity: 0, + maximum_quantity: 0, + values: [] +}; + +const AdditionalInputList = ({ name, onDelete, onDeleteValue, entityId }) => { + const { values, setFieldValue, errors } = useFormikContext(); + + const metaFields = values[name] || []; + + const handleAddItem = () => { + setFieldValue(name, [...metaFields, { ...DEFAULT_META_FIELD }]); + }; + + const handleRemove = async (item, index) => { + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: `${T.translate("additional_inputs.delete_meta_field_warning")} ${ + item.name + }`, + type: "warning", + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (!isConfirmed) return; + + const removeFromUI = () => { + const newValues = metaFields.filter((_, idx) => idx !== index); + if (newValues.length === 0) { + newValues.push({ ...DEFAULT_META_FIELD }); + } + setFieldValue(name, newValues); + }; + + if (item.id && onDelete) { + onDelete(entityId, item.id) + .then(() => removeFromUI()) + .catch((err) => console.error("Error deleting field from API", err)); + } else { + removeFromUI(); + } + }; + + const areMetafieldsIncomplete = () => { + const fieldErrors = getIn(errors, name); + if (fieldErrors && Array.isArray(fieldErrors)) { + const hasRealErrors = fieldErrors.some( + (err) => err && Object.keys(err).length > 0 + ); + if (hasRealErrors) return true; + } + + return metaFields.some((field) => { + if (!field.name?.trim() || !field.type) return true; + if (METAFIELD_TYPES_WITH_OPTIONS.includes(field.type)) { + if (!field.values || field.values.length === 0) return true; + const hasIncompleteValues = field.values.some( + (v) => !v.name?.trim() || !v.value?.trim() + ); + if (hasIncompleteValues) return true; + } + + return false; + }); + }; + + return ( + <> + {metaFields.map((item, itemIdx) => ( + + ))} + + ); +}; + +export default AdditionalInputList; diff --git a/src/components/mui/formik-inputs/additional-input/additional-input.js b/src/components/mui/formik-inputs/additional-input/additional-input.js new file mode 100644 index 000000000..f1ef1f1fc --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/additional-input.js @@ -0,0 +1,185 @@ +import React from "react"; +import { + Box, + Button, + Divider, + FormHelperText, + Grid2, + InputLabel, + MenuItem +} from "@mui/material"; +import { useFormikContext, getIn } from "formik"; +import T from "i18n-react/dist/i18n-react"; +import DeleteIcon from "@mui/icons-material/Delete"; +import AddIcon from "@mui/icons-material/Add"; +import MetaFieldValues from "./meta-field-values"; +import MuiFormikTextField from "../mui-formik-textfield"; +import MuiFormikSelect from "../mui-formik-select"; +import MuiFormikCheckbox from "../mui-formik-checkbox"; +import { + METAFIELD_TYPES, + METAFIELD_TYPES_WITH_OPTIONS +} from "../../../../utils/constants"; + +const AdditionalInput = ({ + item, + itemIdx, + baseName, + onAdd, + onDelete, + onDeleteValue, + entityId, + isAddDisabled +}) => { + const { errors, touched, values } = useFormikContext(); + + const buildFieldName = (fieldName) => `${baseName}[${itemIdx}].${fieldName}`; + const currentType = getIn(values, buildFieldName("type")); + + const fieldErrors = getIn(errors, `${baseName}[${itemIdx}]`); + const fieldTouched = getIn(touched, `${baseName}[${itemIdx}]`); + + const showValuesError = + fieldTouched?.values && + fieldErrors?.values && + typeof fieldErrors.values === "string"; + + return ( + + + + + + + {T.translate("additional_inputs.meta_field_title")} + + + + + + {T.translate("additional_inputs.meta_field_type")} + + + {METAFIELD_TYPES.map((fieldType) => ( + + {fieldType} + + ))} + + + + + + + {METAFIELD_TYPES_WITH_OPTIONS.includes(currentType) && ( + <> + + + {showValuesError && ( + + {fieldErrors.values} + + )} + + )} + {currentType === "Quantity" && ( + + + + + + + + + )} + + + + + + + + + + ); +}; + +export default AdditionalInput; diff --git a/src/components/mui/formik-inputs/additional-input/meta-field-values.js b/src/components/mui/formik-inputs/additional-input/meta-field-values.js new file mode 100644 index 000000000..f4af0984b --- /dev/null +++ b/src/components/mui/formik-inputs/additional-input/meta-field-values.js @@ -0,0 +1,187 @@ +import React from "react"; +import T from "i18n-react/dist/i18n-react"; +import { useFormikContext } from "formik"; +import { Box, Button, Grid2, Divider, IconButton } from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; +import DragAndDropList from "../../dnd-list"; +import showConfirmDialog from "../../showConfirmDialog"; +import MuiFormikTextField from "../mui-formik-textfield"; +import MuiFormikCheckbox from "../mui-formik-checkbox"; + +const MetaFieldValues = ({ + field, + fieldIndex, + baseName = "meta_fields", + onMetaFieldTypeValueDeleted, + entityId +}) => { + const { values, setFieldValue } = useFormikContext(); + + const metaFields = values[baseName] || []; + const sortedValues = [...field.values].sort((a, b) => a.order - b.order); + + const buildValueFieldName = (valueIndex, fieldName) => + `${baseName}[${fieldIndex}].values[${valueIndex}].${fieldName}`; + + const onReorder = (newValues) => { + const newMetaFields = [...metaFields]; + newMetaFields[fieldIndex].values = newValues; + setFieldValue(baseName, newMetaFields); + }; + + const handleAddValue = () => { + const newFields = [...metaFields]; + newFields[fieldIndex].values.push({ + value: "", + name: "", + is_default: false + }); + setFieldValue(baseName, newFields); + }; + + const handleDefaultChange = (valueIndex, checked) => { + const newFields = [...metaFields]; + if (checked) { + newFields[fieldIndex].values.forEach((v) => { + v.is_default = false; + }); + } + newFields[fieldIndex].values[valueIndex].is_default = checked; + setFieldValue(baseName, newFields); + }; + + const isMetafieldValueIncomplete = () => { + if (field.values.length > 0) { + return field.values.some((v) => !v.name?.trim() || !v.value?.trim()); + } + return false; + }; + + const handleRemoveValue = async (metaFieldValue, valueIndex) => { + const isConfirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: `${T.translate("meta_field_values_list.delete_value_warning")} ${ + metaFieldValue.name + }`, + type: "warning", + confirmButtonColor: "#DD6B55", + confirmButtonText: T.translate("general.yes_delete") + }); + + if (!isConfirmed) return; + + const removeValueFromFields = () => { + const newFields = [...metaFields]; + newFields[fieldIndex].values = newFields[fieldIndex].values.filter( + (_, index) => index !== valueIndex + ); + setFieldValue(baseName, newFields); + }; + + if (field.id && metaFieldValue.id && onMetaFieldTypeValueDeleted) { + console.log("Delete params:", { + entityId, + fieldId: field.id, + valueId: metaFieldValue.id + }); + onMetaFieldTypeValueDeleted(entityId, field.id, metaFieldValue.id).then( + () => removeValueFromFields() + ); + } else { + removeValueFromFields(); + } + }; + + const renderMetaFieldValue = (val, sortedIndex, provided, snapshot) => { + const originalIndex = field.values.findIndex( + (v) => (v.id && v.id === val.id) || v === val + ); + const valueIndex = originalIndex !== -1 ? originalIndex : sortedIndex; + + return ( + + + + + + + handleRemoveValue(val, valueIndex)} + aria-label="remove value" + > + + + ) + }} + /> + + + + handleDefaultChange(valueIndex, e.target.checked) + } + /> + + + + + ); + }; + + return ( + + + + + + + ); +}; + +export default MetaFieldValues; diff --git a/src/components/mui/formik-inputs/mui-formik-select.js b/src/components/mui/formik-inputs/mui-formik-select.js index 226737a18..15a2d4903 100644 --- a/src/components/mui/formik-inputs/mui-formik-select.js +++ b/src/components/mui/formik-inputs/mui-formik-select.js @@ -10,7 +10,13 @@ import { import ClearIcon from "@mui/icons-material/Clear"; import { useField } from "formik"; -const MuiFormikSelect = ({ name, children, isClearable, ...rest }) => { +const MuiFormikSelect = ({ + name, + placeholder, + children, + isClearable, + ...rest +}) => { const [field, meta, helpers] = useField(name); const handleClear = (ev) => { @@ -25,6 +31,12 @@ const MuiFormikSelect = ({ name, children, isClearable, ...rest }) => { // eslint-disable-next-line react/jsx-props-no-spreading {...field} displayEmpty + renderValue={(selected) => { + if (!selected || selected === "") { + return {placeholder}; + } + return selected; + }} endAdornment={ isClearable && field.value ? ( diff --git a/src/i18n/en.json b/src/i18n/en.json index dc388e52b..9a22aab39 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2467,7 +2467,13 @@ "meta_field_type": "Field Type", "meta_field_required": "Required", "delete_meta_field_warning": "Are you sure you want to delete meta field ", - "delete_value_warning": "Are you sure you want to delete meta field value " + "delete_value_warning": "Are you sure you want to delete meta field value ", + "placeholders": { + "meta_field_title": "Field Title", + "meta_field_type": "Select...", + "meta_field_maximum_quantity": "Maximum", + "meta_field_minimum_quantity": "Minimum" + } }, "sponsor_forms": { "forms": "Forms", diff --git a/src/pages/sponsors/components/additional-input-list.js b/src/pages/sponsors/components/additional-input-list.js deleted file mode 100644 index 5575e7e60..000000000 --- a/src/pages/sponsors/components/additional-input-list.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from "react"; -import _ from "lodash"; -import { useField } from "formik"; -import T from "i18n-react"; -import AdditionalInput from "./additional-input"; -import showConfirmDialog from "../../../components/mui/showConfirmDialog"; - -const AdditionalInputList = ({ name, onDelete, onDeleteValue }) => { - // eslint-disable-next-line no-unused-vars - const [field, meta, helpers] = useField(name); - - const handleChange = (itemIdx, fieldName, fieldValue) => { - const newValues = _.cloneDeep(field.value); - newValues[itemIdx][fieldName] = fieldValue; - helpers.setValue(newValues); - }; - - const handleRemove = async (item, index) => { - const isConfirmed = await showConfirmDialog({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("additional_inputs.delete_meta_field_warning")} ${ - item.name - }`, - type: "warning", - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - - if (isConfirmed) { - const removeFromUI = () => { - const newValues = field.value.filter((val, idx) => idx !== index); - - if (newValues.length === 0) - newValues.push({ - name: "", - type: "Text", - is_required: false, - values: [] - }); - - helpers.setValue(newValues); - }; - - if (item.id && onDelete) { - onDelete(item.id).then(() => { - removeFromUI(); - }); - } else { - removeFromUI(); - } - } - }; - - const handleRemoveValue = async (item, itemValue, valueIndex, itemIndex) => { - const isConfirmed = await showConfirmDialog({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("additional_inputs.delete_value_warning")} ${ - itemValue.name - }`, - type: "warning", - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - - if (isConfirmed) { - const removeFromUI = () => { - const newValues = _.cloneDeep(field.value); - newValues[itemIndex].values = newValues[itemIndex].values.filter( - (val, idx) => idx !== valueIndex - ); - helpers.setValue(newValues); - }; - - if (item.id && itemValue.id && onDeleteValue) { - onDeleteValue(item.id, itemValue.id).then(() => { - removeFromUI(); - }); - } else { - removeFromUI(); - } - } - }; - - const handleAddValue = (index) => { - const newValues = _.cloneDeep(field.value); - newValues[index].values.push({ value: "", is_default: false }); - helpers.setValue(newValues); - }; - - const handleValueChange = (itemIdx, valueIdx, key, value) => { - const newValues = _.cloneDeep(field.value); - newValues[itemIdx].values[valueIdx][key] = value; - helpers.setValue(newValues); - }; - - const handleAddItem = () => { - helpers.setValue([ - ...field.value, - { name: "", type: "Text", is_required: false, values: [] } - ]); - }; - - const handleReorderValues = (itemIdx, newItemValues) => { - const newValues = _.cloneDeep(field.value); - newValues[itemIdx].values = newItemValues; - helpers.setValue(newValues); - }; - - return ( - <> - {field.value.map((item, itemIdx) => ( - - ))} - - ); -}; - -export default AdditionalInputList; diff --git a/src/pages/sponsors/components/additional-input.js b/src/pages/sponsors/components/additional-input.js deleted file mode 100644 index 6386f6795..000000000 --- a/src/pages/sponsors/components/additional-input.js +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; -import { - Box, - Button, - Checkbox, - Divider, - FormControl, - FormControlLabel, - Grid2, - MenuItem, - Select, - TextField -} from "@mui/material"; -import T from "i18n-react"; -import DeleteIcon from "@mui/icons-material/Delete"; -import AddIcon from "@mui/icons-material/Add"; -import MetaFieldValues from "../../sponsors_inventory/popup/meta-field-values"; -import { - METAFIELD_TYPES, - METAFIELD_TYPES_WITH_OPTIONS -} from "../../../utils/constants"; - -const AdditionalInput = ({ - item, - itemIdx, - onChange, - onChangeValue, - onAdd, - onAddValue, - onDelete, - onDeleteValue, - onReorderValue -}) => ( - - - - - - onChange(itemIdx, "name", ev.target.value)} - fullWidth - /> - - - - - - - - onChange(itemIdx, "is_required", ev.target.checked) - } - /> - } - label={T.translate("additional_inputs.meta_field_required")} - /> - - - - {METAFIELD_TYPES_WITH_OPTIONS.includes(item.type) && ( - <> - - - - )} - - - - - - - - - -); - -export default AdditionalInput; diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js b/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js index b442bbbac..4c677afe9 100644 --- a/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js +++ b/src/pages/sponsors/sponsor-form-item-list-page/components/item-form.js @@ -15,7 +15,7 @@ import * as yup from "yup"; import { FormikProvider, useFormik } from "formik"; import { addIssAfterDateFieldValidator } from "../../../../utils/yup"; import MuiFormikTextField from "../../../../components/mui/formik-inputs/mui-formik-textfield"; -import AdditionalInputList from "../../components/additional-input-list"; +import AdditionalInputList from "../../../../components/mui/formik-inputs/additional-input/additional-input-list"; import useScrollToError from "../../../../hooks/useScrollToError"; import MuiFormikUpload from "../../../../components/mui/formik-inputs/mui-formik-upload"; import MuiFormikPriceField from "../../../../components/mui/formik-inputs/mui-formik-pricefield"; diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js index 22747a7e0..b72e0ef07 100644 --- a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js +++ b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js @@ -17,7 +17,7 @@ import { addIssAfterDateFieldValidator } from "../../../../../utils/yup"; import DropdownCheckbox from "../../../../../components/mui/dropdown-checkbox"; import MuiFormikTextField from "../../../../../components/mui/formik-inputs/mui-formik-textfield"; import MuiFormikDatepicker from "../../../../../components/mui/formik-inputs/mui-formik-datepicker"; -import AdditionalInputList from "../../../components/additional-input-list"; +import AdditionalInputList from "../../../../../components/mui/formik-inputs/additional-input/additional-input-list"; import useScrollToError from "../../../../../hooks/useScrollToError"; import FormikTextEditor from "../../../../../components/inputs/formik-text-editor"; diff --git a/src/pages/sponsors/sponsor-forms-tab/components/customized-form/customized-form.js b/src/pages/sponsors/sponsor-forms-tab/components/customized-form/customized-form.js index 87fdbcd90..f91575aa6 100644 --- a/src/pages/sponsors/sponsor-forms-tab/components/customized-form/customized-form.js +++ b/src/pages/sponsors/sponsor-forms-tab/components/customized-form/customized-form.js @@ -16,7 +16,7 @@ import { FormikProvider, useFormik } from "formik"; import { addIssAfterDateFieldValidator } from "../../../../../utils/yup"; import MuiFormikTextField from "../../../../../components/mui/formik-inputs/mui-formik-textfield"; import MuiFormikDatepicker from "../../../../../components/mui/formik-inputs/mui-formik-datepicker"; -import AdditionalInputList from "../../../components/additional-input-list"; +import AdditionalInputList from "../../../../../components/mui/formik-inputs/additional-input/additional-input-list"; import useScrollToError from "../../../../../hooks/useScrollToError"; import FormikTextEditor from "../../../../../components/inputs/formik-text-editor"; import { querySponsorAddons } from "../../../../../actions/sponsor-actions"; diff --git a/src/pages/sponsors_inventory/inventory-list-page.js b/src/pages/sponsors_inventory/inventory-list-page.js index 0a4f666bd..61a99d497 100644 --- a/src/pages/sponsors_inventory/inventory-list-page.js +++ b/src/pages/sponsors_inventory/inventory-list-page.js @@ -131,17 +131,18 @@ const InventoryListPage = ({ }; const handleInventorySave = (item) => { - saveInventoryItem(item).then(() => - getInventoryItems( - term, - currentPage, - perPage, - order, - orderDir, - hideArchived + saveInventoryItem(item) + .then(() => + getInventoryItems( + term, + currentPage, + perPage, + order, + orderDir, + hideArchived + ) ) - ); - setOpen(false); + .finally(() => setOpen(false)); }; const handleArchiveItem = (item) => diff --git a/src/pages/sponsors_inventory/popup/form-template-popup.js b/src/pages/sponsors_inventory/popup/form-template-popup.js index 88b97044d..8bbf3ecff 100644 --- a/src/pages/sponsors_inventory/popup/form-template-popup.js +++ b/src/pages/sponsors_inventory/popup/form-template-popup.js @@ -7,29 +7,20 @@ import { DialogContent, DialogTitle, Button, - MenuItem, InputLabel, Box, IconButton, Divider, Grid2 } from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; -import { useFormik, FormikProvider, FieldArray } from "formik"; +import { useFormik, FormikProvider } from "formik"; import * as yup from "yup"; -import showConfirmDialog from "../../../components/mui/showConfirmDialog"; -import MetaFieldValues from "./meta-field-values"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; import FormikTextEditor from "../../../components/inputs/formik-text-editor"; -import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; -import MuiFormikCheckbox from "../../../components/mui/formik-inputs/mui-formik-checkbox"; +import AdditionalInputList from "../../../components/mui/formik-inputs/additional-input/additional-input-list"; import useScrollToError from "../../../hooks/useScrollToError"; -import { - METAFIELD_TYPES, - METAFIELD_TYPES_WITH_OPTIONS -} from "../../../utils/constants"; +import { METAFIELD_TYPES } from "../../../utils/constants"; const FormTemplateDialog = ({ open, @@ -99,30 +90,6 @@ const FormTemplateDialog = ({ useScrollToError(formik); - const handleRemoveFieldType = async (fieldType, index, removeFormik) => { - const isConfirmed = await showConfirmDialog({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("edit_form_template.delete_meta_field_warning")} ${ - fieldType.name - }`, - type: "warning", - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - - if (!isConfirmed) return; - - if (fieldType.id) { - onMetaFieldTypeDeleted(initialEntity.id, fieldType.id).then(() => - removeFormik(index) - ); - } else { - removeFormik(index); - } - }; - - const buildFieldName = (base, index, field) => `${base}[${index}].${field}`; - const handleClose = () => { formik.resetForm(); onClose(); @@ -196,210 +163,12 @@ const FormTemplateDialog = ({ - - {({ push, remove }) => ( - <> - {formik.values.meta_fields.map((field, fieldIndex) => ( - - - - - - - {T.translate( - "edit_form_template.meta_field_title" - )} - - - - - - {T.translate( - "edit_form_template.meta_field_type" - )} - - - {METAFIELD_TYPES.map((field_type) => ( - - {field_type} - - ))} - - - - - - - {METAFIELD_TYPES_WITH_OPTIONS.includes( - field.type - ) && ( - <> - - - - )} - {field.type === "Quantity" && ( - - - - - - - - - - - - - )} - - - - - - - - - - ))} - - )} - + diff --git a/src/pages/sponsors_inventory/popup/meta-field-values.js b/src/pages/sponsors_inventory/popup/meta-field-values.js deleted file mode 100644 index b0e96f91a..000000000 --- a/src/pages/sponsors_inventory/popup/meta-field-values.js +++ /dev/null @@ -1,211 +0,0 @@ -import React from "react"; -import T from "i18n-react/dist/i18n-react"; -import { useFormikContext } from "formik"; -import { - Box, - Button, - Checkbox, - FormControlLabel, - FormGroup, - IconButton, - TextField, - Divider, - Grid2 -} from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; -import AddIcon from "@mui/icons-material/Add"; -import DragAndDropList from "../../../components/mui/dnd-list"; -import showConfirmDialog from "../../../components/mui/showConfirmDialog"; - -const MetaFieldValues = ({ - field, - fieldIndex, - onMetaFieldTypeValueDeleted, - initialEntity -}) => { - const { values, setFieldValue } = useFormikContext(); - - const sortedValues = [...field.values].sort((a, b) => a.order - b.order); - - const onReorder = (newValues) => { - const newMetaFields = [...values.meta_fields]; - newMetaFields[fieldIndex].values = newValues; - setFieldValue("meta_fields", newMetaFields); - }; - - const handleFieldValueChange = (fieldIndex, valueIndex, key, value) => { - const newFields = [...values.meta_fields]; - if (key === "is_default" && value === true) { - newFields[fieldIndex].values.forEach((v) => { - v.is_default = false; - }); - } - newFields[fieldIndex].values[valueIndex][key] = value; - setFieldValue("meta_fields", newFields); - }; - - const handleAddValue = (index) => { - const newFields = [...values.meta_fields]; - newFields[index].values.push({ value: "", name: "", is_default: false }); - setFieldValue("meta_fields", newFields); - }; - - const isMetafieldValueIncomplete = (index) => { - const metafield = values.meta_fields[index]; - if (metafield.values.length > 0) { - return metafield.values.some((f) => f.name === "" || f.value === ""); - } - return false; - }; - - const handleRemoveValue = async ( - metaField, - metaFieldValue, - valueIndex, - fieldIndex - ) => { - const isConfirmed = await showConfirmDialog({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("meta_field_values_list.delete_value_warning")} ${ - metaFieldValue.name - }`, - type: "warning", - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - - if (isConfirmed) { - const removeValueFromFields = () => { - const newFields = [...values.meta_fields]; - newFields[fieldIndex].values = newFields[fieldIndex].values.filter( - (_, index) => index !== valueIndex - ); - setFieldValue("meta_fields", newFields); - }; - - if (metaField.id && metaFieldValue.id && onMetaFieldTypeValueDeleted) { - onMetaFieldTypeValueDeleted( - initialEntity.id, - metaField.id, - metaFieldValue.id - ).then(() => { - removeValueFromFields(); - }); - } else { - removeValueFromFields(); - } - } - }; - - const renderMetaFieldValue = (val, valueIndex, provided, snapshot) => ( - <> - - - - handleFieldValueChange( - fieldIndex, - valueIndex, - "name", - e.target.value - ) - } - fullWidth - /> - - - - handleFieldValueChange( - fieldIndex, - valueIndex, - "value", - e.target.value - ) - } - fullWidth - slotProps={{ - input: { - endAdornment: ( - - handleRemoveValue(field, val, valueIndex, fieldIndex) - } - > - - - ) - } - }} - /> - - - - - handleFieldValueChange( - fieldIndex, - valueIndex, - "is_default", - e.target.checked - ) - } - /> - } - label={T.translate("edit_inventory_item.meta_field_is_default")} - /> - - - - - - ); - - return ( - - - - - - - ); -}; - -export default MetaFieldValues; diff --git a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js b/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js index 700746357..ae0733fca 100644 --- a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js +++ b/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js @@ -1,7 +1,7 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; import PropTypes from "prop-types"; -import { FieldArray, FormikProvider, useFormik } from "formik"; +import { FormikProvider, useFormik } from "formik"; import * as yup from "yup"; import { Dialog, @@ -9,7 +9,6 @@ import { DialogContent, DialogTitle, Button, - MenuItem, InputLabel, Box, IconButton, @@ -17,8 +16,6 @@ import { Grid2, FormHelperText } from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; import { UploadInputV2 } from "openstack-uicore-foundation/lib/components"; import { @@ -27,13 +24,10 @@ import { MAX_INVENTORY_IMAGES_UPLOAD_QTY, METAFIELD_TYPES } from "../../../utils/constants"; -import showConfirmDialog from "../../../components/mui/showConfirmDialog"; -import MetaFieldValues from "./meta-field-values"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; import useScrollToError from "../../../hooks/useScrollToError"; -import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; -import MuiFormikCheckbox from "../../../components/mui/formik-inputs/mui-formik-checkbox"; import FormikTextEditor from "../../../components/inputs/formik-text-editor"; +import AdditionalInputList from "../../../components/mui/formik-inputs/additional-input/additional-input-list"; const SponsorItemDialog = ({ open, @@ -163,47 +157,6 @@ const SponsorItemDialog = ({ useScrollToError(formik); - const handleRemoveFieldType = async (fieldType, index, removeFormik) => { - const isConfirmed = await showConfirmDialog({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("edit_inventory_item.delete_meta_field_warning")} ${ - fieldType.name - }`, - type: "warning", - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - - if (!isConfirmed) return; - - const removeOrResetField = () => { - if (formik.values.meta_fields.length === 1) { - formik.setFieldValue("meta_fields", [ - { - name: "", - type: "Text", - is_required: false, - minimum_quantity: 0, - maximum_quantity: 0, - values: [] - } - ]); - } else { - removeFormik(index); - } - }; - - if (fieldType.id) { - onMetaFieldTypeDeleted(initialEntity.id, fieldType.id) - .then(() => removeOrResetField()) - .catch((err) => console.log("Error at delete field from API", err)); - } else { - removeOrResetField(); - } - }; - - const buildFieldName = (base, index, field) => `${base}[${index}].${field}`; - const handleImageUploadComplete = (response) => { if (response) { const image = { @@ -238,11 +191,6 @@ const SponsorItemDialog = ({ onClose(); }; - const areMetafieldsIncomplete = () => { - if (formik.errors.meta_fields) return true; - return formik.values.meta_fields.some((f) => f.name?.trim() === ""); - }; - return ( - - {({ push, remove }) => ( - <> - {formik.values.meta_fields.map((field, fieldIndex) => ( - - - - - - - {T.translate( - "edit_inventory_item.meta_field_title" - )} - - - - - - {T.translate( - "edit_inventory_item.meta_field_type" - )} - - - {METAFIELD_TYPES.map((field_type) => ( - - {field_type} - - ))} - - - - - - - {fieldTypesWithOptions.includes(field.type) && ( - <> - - - {formik.touched.meta_fields?.[fieldIndex] - ?.values && - formik.errors.meta_fields?.[fieldIndex] - ?.values && ( - - { - formik.errors.meta_fields[fieldIndex] - .values - } - - )} - - )} - {field.type === "Quantity" && ( - - - - - - - - - )} - - - - - - - - - - ))} - - )} - + Date: Tue, 6 Jan 2026 15:54:25 -0300 Subject: [PATCH 2/2] fix: address PR comments, remove console logs, add missing propTypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- .../formik-inputs/additional-input/meta-field-values.js | 7 +------ src/components/mui/formik-inputs/mui-formik-select.js | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/mui/formik-inputs/additional-input/meta-field-values.js b/src/components/mui/formik-inputs/additional-input/meta-field-values.js index f4af0984b..32da68b4e 100644 --- a/src/components/mui/formik-inputs/additional-input/meta-field-values.js +++ b/src/components/mui/formik-inputs/additional-input/meta-field-values.js @@ -80,11 +80,6 @@ const MetaFieldValues = ({ }; if (field.id && metaFieldValue.id && onMetaFieldTypeValueDeleted) { - console.log("Delete params:", { - entityId, - fieldId: field.id, - valueId: metaFieldValue.id - }); onMetaFieldTypeValueDeleted(entityId, field.id, metaFieldValue.id).then( () => removeValueFromFields() ); @@ -167,7 +162,7 @@ const MetaFieldValues = ({ onReorder={onReorder} renderItem={renderMetaFieldValue} idKey="id" - updateOrder="order" + updateOrderKey="order" droppableId={`droppable-values-${fieldIndex}`} /> diff --git a/src/components/mui/formik-inputs/mui-formik-select.js b/src/components/mui/formik-inputs/mui-formik-select.js index 15a2d4903..f8c2aa75e 100644 --- a/src/components/mui/formik-inputs/mui-formik-select.js +++ b/src/components/mui/formik-inputs/mui-formik-select.js @@ -61,6 +61,7 @@ const MuiFormikSelect = ({ MuiFormikSelect.propTypes = { name: PropTypes.string.isRequired, children: PropTypes.node.isRequired, + placeholder: PropTypes.string, isClearable: PropTypes.bool };