From 182eaf31c5eba4d5a0ac722431ce030d6cadb3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Castillo?= Date: Fri, 4 Sep 2026 11:12:54 -0300 Subject: [PATCH 1/3] fix: restore edit-company-page and deep linking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Castillo --- src/components/forms/company-form.js | 767 +++++++++--------- .../formik-inputs/mui-formik-async-select.js | 2 +- src/i18n/en.json | 1 + src/pages/companies/company-list-page.js | 87 +- .../__tests__/company-dialog.test.js | 343 -------- .../companies/components/company-dialog.js | 551 ------------- src/pages/companies/edit-company-page.js | 243 +++--- 7 files changed, 529 insertions(+), 1465 deletions(-) delete mode 100644 src/pages/companies/components/__tests__/company-dialog.test.js delete mode 100644 src/pages/companies/components/company-dialog.js diff --git a/src/components/forms/company-form.js b/src/components/forms/company-form.js index 5292aeb2d..9eea02525 100644 --- a/src/components/forms/company-form.js +++ b/src/components/forms/company-form.js @@ -1,5 +1,5 @@ -/* - * Copyright 2017 OpenStack Foundation +/** + * Copyright 2024 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -11,416 +11,411 @@ * limitations under the License. * */ -import React from "react"; +import React, { useState } from "react"; +import PropTypes from "prop-types"; import T from "i18n-react/dist/i18n-react"; -import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input" -import Input from "openstack-uicore-foundation/lib/components/inputs/text-input" -import CountryDropdown from "openstack-uicore-foundation/lib/components/inputs/country-dropdown" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import Table from "openstack-uicore-foundation/lib/components/table"; -import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; -import { isEmpty, scrollToError, shallowEqual } from "../../utils/methods"; +import { useFormikContext } from "formik"; +import { + Button, + FormControl, + Grid2, + InputLabel, + MenuItem, + Select +} from "@mui/material"; +import UploadInputV3 from "openstack-uicore-foundation/lib/components/inputs/upload-input-v3"; +import { getCountryList } from "openstack-uicore-foundation/lib/utils/query-actions"; +import Table from "openstack-uicore-foundation/lib/components/mui/table"; +import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; +import MuiFormikSelect from "openstack-uicore-foundation/lib/components/mui/formik-inputs/select"; +import useScrollToError from "../../hooks/useScrollToError"; +import FormikTextEditor from "../inputs/formik-text-editor"; +import MuiFormikAsyncAutocomplete from "../mui/formik-inputs/mui-formik-async-select"; +import MuiFormikColorField from "../mui/formik-inputs/mui-formik-color-field"; +import showConfirmDialog from "../mui/showConfirmDialog"; -class CompanyForm extends React.Component { - constructor(props) { - super(props); +const MEMBER_LEVELS = [ + { label: "Platinum", value: "Platinum" }, + { label: "Gold", value: "Gold" }, + { label: "StartUp", value: "StartUp" }, + { label: "Corporate", value: "Corporate" }, + { label: "Mention", value: "Mention" }, + { label: "None", value: "None" } +]; - this.state = { - entity: { ...props.entity }, - errors: props.errors, - selectedSponsoredProject: null, - selectedSponsorShipType: null, - sponsorShipTypes: [] - }; - - this.handleChange = this.handleChange.bind(this); - this.handleUploadLogo = this.handleUploadLogo.bind(this); - this.handleUploadBigLogo = this.handleUploadBigLogo.bind(this); - this.handleRemoveFile = this.handleRemoveFile.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleSelectedSponsoredProject = - this.handleSelectedSponsoredProject.bind(this); - this.handleSelectedSponsorshipType = - this.handleSelectedSponsorshipType.bind(this); - this.onAddSponsorshipType = this.onAddSponsorshipType.bind(this); - } +const getLogoValue = (value) => { + if (!value) return []; + if (typeof value === "string") return [{ filename: value, file_url: value }]; + return [{ filename: value.filename, file_url: value.filepath }]; +}; - componentDidUpdate(prevProps) { - const state = {}; - scrollToError(this.props.errors); - - if (!shallowEqual(prevProps.entity, this.props.entity)) { - state.entity = { ...this.props.entity }; - state.errors = {}; - } +const CompanyForm = ({ + initialEntity, + sponsoredProjects = [], + onAttach, + onRemove, + onAddSponsorship, + onDeleteSponsorship, + isSaving, + setIsSaving +}) => { + const formik = useFormikContext(); + const [selectedSponsoredProject, setSelectedSponsoredProject] = + useState(null); + const [selectedSponsorShipType, setSelectedSponsorShipType] = useState(null); + const [sponsorShipTypes, setSponsorShipTypes] = useState([]); - if (!shallowEqual(prevProps.errors, this.props.errors)) { - state.errors = { ...this.props.errors }; - } - - if (!isEmpty(state)) { - this.setState({ ...this.state, ...state }); - } - } + useScrollToError(formik, true); - handleChange(ev) { - const entity = { ...this.state.entity }; - const errors = { ...this.state.errors }; - let { value, id } = ev.target; - - if (ev.target.type === "checkbox") { - value = ev.target.checked; + const handleLogoUploadComplete = (field) => (response) => { + const path = + response.path && response.name + ? `${response.path}${response.name}` + : response.file_url ?? response.path ?? ""; + const uploadLogo = { + ...response, + filepath: path, + filename: response.name + }; + delete uploadLogo.path; + delete uploadLogo.name; + const prevValue = formik.values[field]; + formik.setFieldValue(field, uploadLogo); + if (initialEntity?.id) { + setIsSaving(true); + onAttach(initialEntity, uploadLogo, field) + .catch(() => formik.setFieldValue(field, prevValue)) + .finally(() => setIsSaving(false)); + } else { + setIsSaving(false); } + }; - if (ev.target.type === "memberinput") { - entity.email = ""; + const handleLogoRemove = (field) => () => { + formik.setFieldValue(field, ""); + if (initialEntity?.id) { + setIsSaving(true); + const prevValue = formik.values[field]; + onRemove(initialEntity, field) + .catch(() => formik.setFieldValue(field, prevValue)) + .finally(() => setIsSaving(false)); } + }; - errors[id] = ""; - entity[id] = value; - this.setState({ entity, errors }); - } - - handleUploadLogo(file) { - const formData = new FormData(); - formData.append("file", file); - this.props.onAttach(this.state.entity, formData, "logo"); - } - - handleUploadBigLogo(file) { - const formData = new FormData(); - formData.append("file", file); - this.props.onAttach(this.state.entity, formData, "big"); - } - - handleRemoveFile(picAttr) { - const entity = { ...this.state.entity }; - entity[picAttr] = ""; - this.setState({ entity }); - } - - handleSubmit(publish, ev) { - ev.preventDefault(); - this.props.onSubmit(this.state.entity); - } - - handleSelectedSponsoredProject(ev) { - const { sponsoredProjects } = this.props; + const handleSelectedSponsoredProject = (ev) => { const { value } = ev.target; + const project = sponsoredProjects.find((p) => p.id == value); + setSelectedSponsoredProject(value); + setSponsorShipTypes( + project + ? project.sponsorship_types.map((s) => ({ label: s.name, value: s.id })) + : [] + ); + setSelectedSponsorShipType(null); + }; - const project = sponsoredProjects.find((e) => e.id == value); + const handleAddSponsorshipType = () => { + if (!selectedSponsoredProject || !selectedSponsorShipType || isSaving) + return; + setIsSaving(true); + onAddSponsorship(selectedSponsoredProject, selectedSponsorShipType, { + id: 0, + company: { id: formik.values.id } + }).finally(() => setIsSaving(false)); + }; - this.setState({ - ...this.state, - selectedSponsoredProject: value, - sponsorShipTypes: project - ? project.sponsorship_types.map((s) => ({ label: s.name, value: s.id })) - : [], - selectedSponsorShipType: null + const handleDeleteSponsorship = async (sponsorshipId) => { + const sponsorship = initialEntity?.project_sponsorships?.find( + (ps) => ps.id === sponsorshipId + ); + if (!sponsorship) return; + const supportingCompany = sponsorship.supporting_companies?.find( + (sc) => sc.company_id === formik.values.id + ); + if (!supportingCompany) return; + + const confirmed = await showConfirmDialog({ + title: T.translate("general.are_you_sure"), + text: T.translate("edit_company.delete_supporting_company_warning") }); - } - handleSelectedSponsorshipType(ev) { - const { value } = ev.target; - this.setState({ ...this.state, selectedSponsorShipType: value }); - } + if (confirmed) { + if (isSaving) return; + setIsSaving(true); + onDeleteSponsorship( + sponsorship.sponsored_project.id, + sponsorshipId, + supportingCompany.id + ).finally(() => setIsSaving(false)); + } + }; - onAddSponsorshipType(ev) { - ev.preventDefault(); - const { selectedSponsoredProject, selectedSponsorShipType, entity } = - this.state; - if (!selectedSponsoredProject) return; - if (!selectedSponsorShipType) return; - this.props.addSponsoreProjectSponsorship( - entity.id, - selectedSponsoredProject, - selectedSponsorShipType - ); - } + const sponsored_project_columns = [ + { + columnKey: "project_name", + header: T.translate("edit_company.project_name") + }, + { columnKey: "name", header: T.translate("edit_company.sponsorship_type") } + ]; - render() { - const { entity } = this.state; - const { sponsoredProjects } = this.props; + const sponsored_projects_ddl = sponsoredProjects.map((sp) => ({ + label: sp.name, + value: sp.id + })); - const member_levels_ddl = [ - { label: "Platinum", value: "Platinum" }, - { label: "Gold", value: "Gold" }, - { label: "StartUp", value: "StartUp" }, - { label: "Corporate", value: "Corporate" }, - { label: "Mention", value: "Mention" }, - { label: "None", value: "None" } - ]; + const showOpenStackSection = + formik.values.id > 0 && window.APP_CLIENT_NAME === "openstack"; - const sponsored_projects_ddl = - sponsoredProjects && Array.isArray(sponsoredProjects) - ? sponsoredProjects.map((sp) => ({ - label: sp.name, - value: sp.id - })) - : []; + return ( + + + + {T.translate("edit_company.name")} * + + + + + {T.translate("edit_company.url")} + + + + + {T.translate("edit_company.contact_email")} + + + - const columns = [ - { - columnKey: "project_name", - value: T.translate("edit_company.project_name") - }, - { columnKey: "name", value: T.translate("edit_company.sponsorship_type") } - ]; + + + {T.translate("edit_company.member_level")} + + + {MEMBER_LEVELS.map((lvl) => ( + + {lvl.label} + + ))} + + + + + {T.translate("edit_company.color")} + + + + + + {T.translate("edit_company.admin_email")} + + + - const table_options = { - actions: { - delete: { onClick: this.props.onDeleteSponsorship } - } - }; + + + {T.translate("edit_company.city")} + + + + + + {T.translate("edit_company.state")} + + + + + + {T.translate("edit_company.country")} + + getCountryList(callback)} + formatOption={(country) => ({ + value: country.iso_code, + label: country.name + })} + defaultOptions + /> + + + + {T.translate("edit_company.industry")} + + + + + + {T.translate("edit_company.products")} + + + + + + {T.translate("edit_company.contributions")} + + + - return ( -
- -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
- {entity.id > 0 && window.APP_CLIENT_NAME == "openstack" && ( -
-
- + + {T.translate("edit_company.description")} + + + + + + {T.translate("edit_company.overview")} + + + + + + {T.translate("edit_company.commitment")} + + + + + {showOpenStackSection && ( + + + + {T.translate("edit_company.project_name")} + + + + + + + + + {T.translate("edit_company.sponsorship_type")} + + + + + + +   + + + + )} -
-
- 0 && + window.APP_CLIENT_NAME == "openstack" && ( + + ({ + ...sp, + project_name: sp.sponsored_project.name + }))} + columns={sponsored_project_columns} + onDelete={handleDeleteSponsorship} /> - - - - ); - } -} + + )} + + + + {T.translate("edit_company.logo")} + + + {T.translate("edit_company.big_logo")} + + + + ); +}; + +CompanyForm.propTypes = { + initialEntity: PropTypes.object.isRequired, + sponsoredProjects: PropTypes.array, + onAttach: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + onAddSponsorship: PropTypes.func, + onDeleteSponsorship: PropTypes.func, + isSaving: PropTypes.bool, + setIsSaving: PropTypes.func.isRequired +}; export default CompanyForm; diff --git a/src/components/mui/formik-inputs/mui-formik-async-select.js b/src/components/mui/formik-inputs/mui-formik-async-select.js index b574b198f..e7302479c 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -9,7 +9,7 @@ import { useField } from "formik"; import { DEBOUNCE_WAIT_250 } from "../../../utils/constants"; // Stays local: uicore's async-select.js lacks defaultOptions, filterOptions, -// and the plain-value-sync effect this component has. company-dialog.js +// and the plain-value-sync effect this component has. company-form.js // depends on defaultOptions specifically. Deferred as a follow-up rather // than migrated blindly in this pass. const MuiFormikAsyncAutocomplete = ({ diff --git a/src/i18n/en.json b/src/i18n/en.json index c01abeafd..4c3b68989 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -93,6 +93,7 @@ "n_a": "N/A", "to": "To", "from": "From", + "add_new": "Add new", "placeholders": { "search": "Search...", "search_speakers": "Search Speakers by Name, Email, Speaker Id or Member Id", diff --git a/src/pages/companies/company-list-page.js b/src/pages/companies/company-list-page.js index 0812d95b3..6e7215661 100644 --- a/src/pages/companies/company-list-page.js +++ b/src/pages/companies/company-list-page.js @@ -11,58 +11,28 @@ * limitations under the License. * */ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import { Box, Button } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import GridToolbar from "../../components/mui/grid-toolbar"; -import { - getCompanies, - getCompany, - deleteCompany, - saveCompany, - resetCompanyForm, - attachLogo, - removeLogo -} from "../../actions/company-actions"; -import { - getSponsoredProjects, - saveSupportingCompany, - deleteSupportingCompany -} from "../../actions/sponsored-project-actions"; -import { DEFAULT_CURRENT_PAGE, MAX_PER_PAGE } from "../../utils/constants"; -import CompanyDialog from "./components/company-dialog"; +import { getCompanies, deleteCompany } from "../../actions/company-actions"; +import { DEFAULT_CURRENT_PAGE } from "../../utils/constants"; const CompanyListPage = ({ companies, - currentCompany, term, order, orderDir, currentPage, perPage, totalCompanies, + history, getCompanies, - getCompany, - deleteCompany, - saveCompany, - resetCompanyForm, - attachLogo, - removeLogo, - getSponsoredProjects, - saveSupportingCompany, - deleteSupportingCompany, - sponsoredProjects + deleteCompany }) => { - const [companyPopup, setCompanyPopup] = useState(false); - - useEffect(() => { - if (window.APP_CLIENT_NAME === "openstack") - getSponsoredProjects("", 1, MAX_PER_PAGE); - }, []); - const columns = [ { columnKey: "id", header: "Id", sortable: true }, { columnKey: "name", header: T.translate("general.name"), sortable: true }, @@ -83,7 +53,7 @@ const CompanyListPage = ({ }, []); const handleEdit = (company) => { - getCompany(company.id).then(() => setCompanyPopup(true)); + history.push(`/app/companies/${company.id}`); }; const handleDelete = (companyId) => { @@ -109,17 +79,7 @@ const CompanyListPage = ({ }; const handleNewCompany = () => { - resetCompanyForm(); - setCompanyPopup(true); - }; - - const handleSave = (entity) => - saveCompany(entity).then(() => - getCompanies(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir) - ); - - const handleClose = () => { - setCompanyPopup(false); + history.push("/app/companies/new"); }; return ( @@ -166,42 +126,15 @@ const CompanyListPage = ({ {companies.length === 0 && (
{T.translate("company_list.no_results")}
)} - - {companyPopup && ( - - )} ); }; -const mapStateToProps = ({ - currentCompanyListState, - sponsoredProjectListState, - currentCompanyState -}) => ({ - ...currentCompanyListState, - currentCompany: currentCompanyState.entity, - sponsoredProjects: sponsoredProjectListState.sponsoredProjects +const mapStateToProps = ({ currentCompanyListState }) => ({ + ...currentCompanyListState }); export default connect(mapStateToProps, { getCompanies, - getCompany, - deleteCompany, - saveCompany, - resetCompanyForm, - getSponsoredProjects, - saveSupportingCompany, - deleteSupportingCompany, - attachLogo, - removeLogo + deleteCompany })(CompanyListPage); diff --git a/src/pages/companies/components/__tests__/company-dialog.test.js b/src/pages/companies/components/__tests__/company-dialog.test.js deleted file mode 100644 index bb0077fed..000000000 --- a/src/pages/companies/components/__tests__/company-dialog.test.js +++ /dev/null @@ -1,343 +0,0 @@ -import React from "react"; -import { - render, - screen, - waitFor, - act, - fireEvent -} from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; -import CompanyDialog from "../company-dialog"; - -jest.mock("i18n-react/dist/i18n-react", () => ({ - translate: jest.fn((key) => key) -})); - -jest.mock("openstack-uicore-foundation/lib/utils/query-actions", () => ({ - getCountryList: jest.fn((callback) => { - callback([ - { iso_code: "AR", name: "Argentina" }, - { iso_code: "US", name: "United States" } - ]); - return Promise.resolve(); - }) -})); - -jest.mock( - "openstack-uicore-foundation/lib/components/inputs/upload-input-v3", - () => ({ - __esModule: true, - default: ({ id, onUploadComplete, onUploadStart, value }) => ( -
- -
- ) - }) -); - -jest.mock( - "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield", - () => - function MockTextField({ name }) { - return ; - } -); - -jest.mock( - "openstack-uicore-foundation/lib/components/mui/formik-inputs/select", - () => - function MockSelect({ name, children }) { - return
{children}
; - } -); - -jest.mock( - "openstack-uicore-foundation/lib/components/mui/table", - () => - function MockTable() { - return
; - } -); - -jest.mock( - "../../../../components/inputs/formik-text-editor", - () => - function MockTextEditor({ name }) { - return