From e0733be39a8ed12e9c74e0dc4c29c6958bf9d16d Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 11 Sep 2026 17:15:42 -0300 Subject: [PATCH] chore: migrate summit-docs to mui --- src/actions/summitdoc-actions.js | 26 +- .../forms/__tests__/summitdoc-form.test.js | 142 ++++++ src/components/forms/summitdoc-form.js | 439 ++++++++++-------- .../__tests__/summitdoc-list-page.test.js | 221 +++++++++ src/pages/summitdocs/summitdoc-list-page.js | 281 +++++------ 5 files changed, 742 insertions(+), 367 deletions(-) create mode 100644 src/components/forms/__tests__/summitdoc-form.test.js create mode 100644 src/pages/summitdocs/__tests__/summitdoc-list-page.test.js diff --git a/src/actions/summitdoc-actions.js b/src/actions/summitdoc-actions.js index 478811d18..455f01492 100644 --- a/src/actions/summitdoc-actions.js +++ b/src/actions/summitdoc-actions.js @@ -17,8 +17,7 @@ import { createAction, stopLoading, startLoading, - showMessage, - showSuccessMessage, + snackbarSuccessHandler, authErrorHandler, postRequest, putRequest, @@ -179,15 +178,14 @@ export const saveSummitDoc = (entity, file) => async (dispatch, getState) => { authErrorHandler, entity )(params)(dispatch).then(() => { - dispatch(showSuccessMessage(T.translate("summitdoc.saved"))); + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.done"), + html: T.translate("summitdoc.saved") + }) + ); }); } else { - const successMessage = { - title: T.translate("general.done"), - html: T.translate("summitdoc.created"), - type: "success" - }; - postFile( createAction(UPDATE_SUMMITDOC), createAction(SUMMITDOC_ADDED), @@ -198,12 +196,14 @@ export const saveSummitDoc = (entity, file) => async (dispatch, getState) => { entity )(params)(dispatch).then((payload) => { dispatch( - showMessage(successMessage, () => { - history.push( - `/app/summits/${currentSummit.id}/summitdocs/${payload.response.id}` - ); + snackbarSuccessHandler({ + title: T.translate("general.done"), + html: T.translate("summitdoc.created") }) ); + history.push( + `/app/summits/${currentSummit.id}/summitdocs/${payload.response.id}` + ); }); } }; diff --git a/src/components/forms/__tests__/summitdoc-form.test.js b/src/components/forms/__tests__/summitdoc-form.test.js new file mode 100644 index 000000000..f47e6a481 --- /dev/null +++ b/src/components/forms/__tests__/summitdoc-form.test.js @@ -0,0 +1,142 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import SummitDocForm from "../summitdoc-form"; + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock( + "openstack-uicore-foundation/lib/components/inputs/upload-input", + () => ({ + __esModule: true, + default: ({ value, handleUpload, handleRemove, disabled }) => ( +
+ + {value && ( + + )} +
+ ) + }) +); + +const currentSummit = { + event_types: [ + { id: 1, name: "Keynote" }, + { id: 2, name: "Panel" } + ], + selection_plans: [{ id: 10, name: "Plan A" }] +}; + +const baseEntity = { + id: 0, + name: "", + label: "", + description: "", + event_types: [], + file_preview: "", + file: null, + selection_plan_id: null, + show_always: false, + web_link: "" +}; + +const renderForm = (entityOverrides = {}, props = {}) => + render( + + ); + +describe("SummitDocForm", () => { + it("clears and disables event types when show_always is checked", async () => { + renderForm({ event_types: [1] }); + + const eventTypesSelect = screen.getByTestId("event-types-select"); + expect(eventTypesSelect).toHaveTextContent("Keynote"); + + await userEvent.click( + screen.getByRole("checkbox", { name: "summitdoc.show_always" }) + ); + + expect(eventTypesSelect).not.toHaveTextContent("Keynote"); + expect(eventTypesSelect.querySelector("[role='combobox']")).toHaveClass( + "Mui-disabled" + ); + }); + + it("disables the file upload when a web link is entered", async () => { + renderForm(); + + await userEvent.type( + screen.getByLabelText("summitdoc.web_link *"), + "http://example.com" + ); + + expect(screen.getByRole("button", { name: "upload-file" })).toBeDisabled(); + }); + + it("disables the web link field once a file is present", () => { + renderForm({ file_preview: "blob:existing-file" }); + + expect(screen.getByLabelText("summitdoc.web_link *")).toBeDisabled(); + }); + + it("holds the file in local state for a new doc and submits it together with the entity on save", async () => { + const onSubmit = jest.fn(); + const addFileToDoc = jest.fn(); + renderForm({}, { onSubmit, addFileToDoc }); + + await userEvent.click(screen.getByRole("button", { name: "upload-file" })); + + expect(addFileToDoc).not.toHaveBeenCalled(); + expect( + screen.getByRole("button", { name: "remove-file" }) + ).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("button", { name: "general.save" })); + + expect(onSubmit).toHaveBeenCalledWith( + expect.objectContaining({ file_preview: "blob:new-file" }), + { preview: "blob:new-file" } + ); + }); + + it("uploads/removes the file directly against the API for an existing doc", async () => { + const addFileToDoc = jest.fn(); + const removeFileFromDoc = jest.fn(); + renderForm( + { id: 5, file_preview: "blob:existing-file" }, + { addFileToDoc, removeFileFromDoc } + ); + + await userEvent.click(screen.getByRole("button", { name: "upload-file" })); + expect(addFileToDoc).toHaveBeenCalledWith( + expect.objectContaining({ id: 5 }), + { preview: "blob:new-file" } + ); + + await userEvent.click(screen.getByRole("button", { name: "remove-file" })); + expect(removeFileFromDoc).toHaveBeenCalledWith( + expect.objectContaining({ id: 5 }) + ); + }); +}); diff --git a/src/components/forms/summitdoc-form.js b/src/components/forms/summitdoc-form.js index 3af66746b..f39da9052 100644 --- a/src/components/forms/summitdoc-form.js +++ b/src/components/forms/summitdoc-form.js @@ -9,249 +9,292 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - **/ + * */ -import React from "react"; +import React, { useEffect, useState } from "react"; import T from "i18n-react/dist/i18n-react"; -import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import TextArea from "openstack-uicore-foundation/lib/components/inputs/textarea-input" +import Box from "@mui/material/Box"; +import { Grid2 } from "@mui/material"; +import TextField from "@mui/material/TextField"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import Checkbox from "@mui/material/Checkbox"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Button from "@mui/material/Button"; +import Tooltip from "@mui/material/Tooltip"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input"; -import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods"; +import { scrollToError, shallowEqual } from "../../utils/methods"; -class SummitDocForm extends React.Component { - constructor(props) { - super(props); +const SummitDocForm = ({ + currentSummit, + entity: entityProp, + errors: errorsProp, + onSubmit, + addFileToDoc, + removeFileFromDoc +}) => { + const [entity, setEntity] = useState({ ...entityProp }); + const [errors, setErrors] = useState(errorsProp); + const [file, setFile] = useState(null); - this.state = { - entity: { ...props.entity }, - errors: props.errors - }; + useEffect(() => { + scrollToError(errorsProp); + }, [errorsProp]); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleUploadFile = this.handleUploadFile.bind(this); - this.handleRemoveFile = this.handleRemoveFile.bind(this); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - const state = {}; - scrollToError(this.props.errors); - - if (!shallowEqual(prevProps.entity, this.props.entity)) { - state.entity = { ...this.props.entity }; - state.errors = {}; + useEffect(() => { + if (!shallowEqual(entity, entityProp)) { + setEntity({ ...entityProp }); + setErrors({}); } + }, [entityProp]); - if (!shallowEqual(prevProps.errors, this.props.errors)) { - state.errors = { ...this.props.errors }; + useEffect(() => { + if (!shallowEqual(errors, errorsProp)) { + setErrors({ ...errorsProp }); } + }, [errorsProp]); - if (!isEmpty(state)) { - this.setState({ ...this.state, ...state }); - } - } - - handleChange(ev) { - let entity = { ...this.state.entity }; - let errors = { ...this.state.errors }; - let { value, id } = ev.target; + const handleChange = (ev) => { + const newEntity = { ...entity }; + const newErrors = { ...errors }; + const { id } = ev.target; + let { value } = ev.target; if (ev.target.type === "checkbox") { value = ev.target.checked; } if (ev.target.type === "number") { - value = parseInt(ev.target.value); + value = parseInt(value, 10); } - errors[id] = ""; - entity[id] = value; + newErrors[id] = ""; + newEntity[id] = value; if (id === "show_always" && value) { - entity.event_types = []; + newEntity.event_types = []; } - this.setState({ entity: entity, errors: errors }); - } + setEntity(newEntity); + setErrors(newErrors); + }; - handleSubmit(ev) { - const { entity, file } = this.state; + const handleSubmit = (ev) => { ev.preventDefault(); + onSubmit(entity, file); + }; - this.props.onSubmit(entity, file); - } - - hasErrors(field) { - let { errors } = this.state; + const hasErrors = (field) => { if (field in errors) { return errors[field]; } return ""; - } + }; - handleUploadFile(file) { - let entity = { ...this.state.entity }; + const handleUploadFile = (uploadedFile) => { + const newEntity = { ...entity }; - if (entity.id) { - this.props.addFileToDoc(entity, file); + if (newEntity.id) { + addFileToDoc(newEntity, uploadedFile); } else { - entity.file_preview = file.preview; - this.setState({ file: file, entity: entity }); + newEntity.file_preview = uploadedFile.preview; + setFile(uploadedFile); + setEntity(newEntity); } - } + }; - handleRemoveFile(ev) { - let entity = { ...this.state.entity }; + const handleRemoveFile = () => { + const newEntity = { ...entity }; - if (entity.id) { - this.props.removeFileFromDoc(entity); + if (newEntity.id) { + removeFileFromDoc(newEntity); } else { - entity.file_preview = ""; - this.setState({ file: null, entity: entity }); + newEntity.file_preview = ""; + setFile(null); + setEntity(newEntity); } - } + }; - render() { - const { entity } = this.state; - const { currentSummit } = this.props; + const eventTypesDDL = currentSummit.event_types.map((et) => ({ + value: et.id, + label: et.name + })); - let event_types_ddl = currentSummit.event_types.map((et) => ({ - value: et.id, - label: et.name - })); + const selectionPlansDDL = currentSummit.selection_plans.map((sp) => ({ + value: sp.id, + label: sp.name + })); - let selection_plans_ddl = currentSummit.selection_plans.map((et) => ({ - value: et.id, - label: et.name - })); + return ( + + + + + + + + + + + + + + + - return ( -
- -
-
- - -
-
- - -
-
-