diff --git a/package.json b/package.json index e04c54b36..bd8752fe5 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,8 @@ "moment": "^2.29.1", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.33", - "openstack-uicore-foundation": "5.0.35", + "mui-color-input": "^9.0.0", + "openstack-uicore-foundation": "5.0.36", "p-limit": "^6.1.0", "path-browserify": "^1.0.1", "postcss-loader": "^6.2.1", diff --git a/src/actions/company-actions.js b/src/actions/company-actions.js index e665fbb74..2c099accb 100644 --- a/src/actions/company-actions.js +++ b/src/actions/company-actions.js @@ -20,16 +20,13 @@ import { createAction, stopLoading, startLoading, - showMessage, - showSuccessMessage, - authErrorHandler, escapeFilterValue, fetchResponseHandler, fetchErrorHandler } from "openstack-uicore-foundation/lib/utils/actions"; -import debounce from "lodash/debounce" +import debounce from "lodash/debounce"; import URI from "urijs"; -import history from "../history"; +import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; import { getAccessTokenSafely } from "../utils/methods"; import { DEBOUNCE_WAIT, @@ -50,6 +47,8 @@ export const COMPANY_UPDATED = "COMPANY_UPDATED"; export const COMPANY_ADDED = "COMPANY_ADDED"; export const LOGO_ATTACHED = "LOGO_ATTACHED"; export const BIG_LOGO_ATTACHED = "BIG_LOGO_ATTACHED"; +export const LOGO_REMOVED = "LOGO_REMOVED"; +export const BIG_LOGO_REMOVED = "BIG_LOGO_REMOVED"; export const getCompanies = ( @@ -92,9 +91,9 @@ export const getCompanies = createAction(REQUEST_COMPANIES), createAction(RECEIVE_COMPANIES), `${window.API_BASE_URL}/api/v1/companies`, - authErrorHandler, - { order, orderDir, page, term } - )(params)(dispatch).then(() => { + snackbarErrorHandler, + { order, orderDir, page, perPage, term } + )(params)(dispatch).finally(() => { dispatch(stopLoading()); }); }; @@ -114,8 +113,8 @@ export const getCompany = (companyId) => async (dispatch) => { null, createAction(RECEIVE_COMPANY), `${window.API_BASE_URL}/api/v1/companies/${companyId}`, - authErrorHandler - )(params)(dispatch).then(() => { + snackbarErrorHandler + )(params)(dispatch).finally(() => { dispatch(stopLoading()); }); }; @@ -134,8 +133,8 @@ export const deleteCompany = (companyId) => async (dispatch) => { createAction(COMPANY_DELETED)({ companyId }), `${window.API_BASE_URL}/api/v1/companies/${companyId}`, null, - authErrorHandler - )(params)(dispatch).then(() => { + snackbarErrorHandler + )(params)(dispatch).finally(() => { dispatch(stopLoading()); }); }; @@ -155,38 +154,42 @@ export const saveCompany = (entity) => async (dispatch) => { const normalizedEntity = normalizeEntity(entity); if (entity.id) { - putRequest( + return putRequest( createAction(UPDATE_COMPANY), createAction(COMPANY_UPDATED), `${window.API_BASE_URL}/api/v1/companies/${entity.id}`, normalizedEntity, - authErrorHandler, + snackbarErrorHandler, entity - )(params)(dispatch).then(() => { - dispatch(showSuccessMessage(T.translate("edit_company.company_saved"))); - }); - } else { - const success_message = { - title: T.translate("general.done"), - html: T.translate("edit_company.company_created"), - type: "success" - }; + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_company.company_saved") + }) + ); + }) + .finally(() => dispatch(stopLoading())); + } - postRequest( - createAction(UPDATE_COMPANY), - createAction(COMPANY_ADDED), - `${window.API_BASE_URL}/api/v1/companies`, - normalizedEntity, - authErrorHandler, - entity - )(params)(dispatch).then(() => { + return postRequest( + createAction(UPDATE_COMPANY), + createAction(COMPANY_ADDED), + `${window.API_BASE_URL}/api/v1/companies`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch) + .then(() => { dispatch( - showMessage(success_message, () => { - history.push("/app/companies"); + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("edit_company.company_created") }) ); - }); - } + }) + .finally(() => dispatch(stopLoading())); }; export const attachLogo = (entity, file, picAttr) => async (dispatch) => { @@ -203,19 +206,19 @@ export const attachLogo = (entity, file, picAttr) => async (dispatch) => { const uploadFile = picAttr === "logo" ? uploadLogo : uploadBigLogo; if (entity.id) { - dispatch(uploadFile(entity, file)); - } else { - return postRequest( - createAction(UPDATE_COMPANY), - createAction(COMPANY_ADDED), - `${window.API_BASE_URL}/api/v1/companies`, - normalizedEntity, - authErrorHandler, - entity - )(params)(dispatch).then((payload) => { - dispatch(uploadFile(payload.response, file)); - }); + return dispatch(uploadFile(entity, file)); } + + return postRequest( + createAction(UPDATE_COMPANY), + createAction(COMPANY_ADDED), + `${window.API_BASE_URL}/api/v1/companies`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch).then((payload) => { + dispatch(uploadFile(payload.response, file)); + }); }; const uploadLogo = (entity, file) => async (dispatch) => { @@ -225,17 +228,14 @@ const uploadLogo = (entity, file) => async (dispatch) => { access_token: accessToken }; - postRequest( + return postRequest( null, createAction(LOGO_ATTACHED), `${window.API_BASE_URL}/api/v1/companies/${entity.id}/logo`, file, - authErrorHandler, + snackbarErrorHandler, { pic: entity.pic } - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - history.push(`/app/companies/${entity.id}`); - }); + )(params)(dispatch).finally(() => dispatch(stopLoading())); }; const uploadBigLogo = (entity, file) => async (dispatch) => { @@ -245,31 +245,56 @@ const uploadBigLogo = (entity, file) => async (dispatch) => { access_token: accessToken }; - postRequest( + return postRequest( null, createAction(BIG_LOGO_ATTACHED), `${window.API_BASE_URL}/api/v1/companies/${entity.id}/logo/big`, file, - authErrorHandler, + snackbarErrorHandler, { pic: entity.pic } - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - history.push(`/app/companies/${entity.id}`); - }); + )(params)(dispatch).finally(() => dispatch(stopLoading())); }; const normalizeEntity = (entity) => { const normalizedEntity = { ...entity }; // remove # from color hexa - normalizedEntity.color = normalizedEntity.color.substr(1); + if (normalizedEntity.color.startsWith("#")) + normalizedEntity.color = normalizedEntity.color.substr(1); - delete normalizedEntity.logo; - delete normalizedEntity.big_logo; + if (entity.id > 0) { + delete normalizedEntity.logo; + delete normalizedEntity.big_logo; + } return normalizedEntity; }; +export const removeLogo = (entity, picAttr) => async (dispatch) => { + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + const endpoint = + picAttr === "logo" + ? `${window.API_BASE_URL}/api/v1/companies/${entity.id}/logo` + : `${window.API_BASE_URL}/api/v1/companies/${entity.id}/logo/big`; + + const action = picAttr === "logo" ? LOGO_REMOVED : BIG_LOGO_REMOVED; + + return deleteRequest( + null, + createAction(action), + endpoint, + {}, + snackbarErrorHandler + )(params)(dispatch).finally(() => dispatch(stopLoading())); +}; + export const queryCompanies = debounce(async (input, callback) => { const accessToken = await getAccessTokenSafely(); const endpoint = URI(`${window.API_BASE_URL}/api/v1/companies`); 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 852417e1a..f06b65b18 100644 --- a/src/components/mui/formik-inputs/mui-formik-async-select.js +++ b/src/components/mui/formik-inputs/mui-formik-async-select.js @@ -18,7 +18,9 @@ const MuiFormikAsyncAutocomplete = ({ formatOption = (item) => ({ value: item.id.toString(), label: item.name }), formatSelectedValue = null, queryParams = [], - isMulti = false + isMulti = false, + defaultOptions, + ...rest }) => { const [field, meta, helpers] = useField(name); const [options, setOptions] = useState([]); @@ -26,6 +28,15 @@ const MuiFormikAsyncAutocomplete = ({ const [searchTerm, setSearchTerm] = useState(""); const value = field.value || (multiple ? [] : null); + + // Sync a plain stored value back to the full option object + useEffect(() => { + if (!field.value || typeof field.value === "object" || options.length === 0) + return; + const match = options.find((o) => o.value === String(field.value)); + if (match) helpers.setValue(match); + }, [options]); + const error = meta.touched && meta.error; const fetchOptions = async (input = "") => { @@ -45,7 +56,7 @@ const MuiFormikAsyncAutocomplete = ({ }; useEffect(() => { - if (searchTerm) { + if (!defaultOptions && searchTerm) { const delayDebounce = setTimeout(() => { fetchOptions(searchTerm); }, DEBOUNCE_WAIT_250); @@ -86,7 +97,17 @@ const MuiFormikAsyncAutocomplete = ({ fullWidth getOptionLabel={(option) => option.label || ""} isOptionEqualToValue={(option, value) => option.value === value.value} - onInputChange={(e, newInput) => setSearchTerm(newInput)} + onInputChange={ + !defaultOptions ? (e, newInput) => setSearchTerm(newInput) : undefined + } + filterOptions={ + defaultOptions + ? (options, { inputValue }) => + options.filter((opt) => + opt.label.toLowerCase().includes(inputValue.toLowerCase()) + ) + : undefined // MUI usa su default que no filtra (deja que la API filtre) + } renderInput={(params) => ( )} + {...rest} /> ); }; diff --git a/src/i18n/en.json b/src/i18n/en.json index eadc2eefd..87677687a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -799,7 +799,8 @@ "companies": "Companies", "add_company": "Add Company", "member_level": "Member Level", - "delete_company_warning": "Are you sure you want to delete company", + "delete_company_warning": "Are you sure you want to delete company {name}", + "no_results": "No items found for this search criteria.", "placeholders": { "search_companies": "Search by Company Name" } @@ -835,7 +836,9 @@ "placeholders": { "select_country": "Select Country", "sponsored_project": "Select Sponsored Project", - "sponsorship_type": "Select Tier" + "sponsorship_type": "Select Tier", + "select_project_first": "Select a project first", + "no_options": "No options" } }, "tag_list": { diff --git a/src/pages/companies/company-list-page.js b/src/pages/companies/company-list-page.js index bf965c017..fc50edb96 100644 --- a/src/pages/companies/company-list-page.js +++ b/src/pages/companies/company-list-page.js @@ -9,163 +9,229 @@ * 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 { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import Swal from "sweetalert2"; -import { Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import Table from "openstack-uicore-foundation/lib/components/table"; -import { getCompanies, deleteCompany } from "../../actions/company-actions"; - -class CompanyListPage extends React.Component { - constructor(props) { - super(props); - - props.getCompanies(); - - this.handleEdit = this.handleEdit.bind(this); - this.handleDelete = this.handleDelete.bind(this); - this.handlePageChange = this.handlePageChange.bind(this); - this.handleSort = this.handleSort.bind(this); - this.handleSearch = this.handleSearch.bind(this); - this.handleNewCompany = this.handleNewCompany.bind(this); - - this.state = {}; - } - - handleEdit(company_id) { - const { history } = this.props; - history.push(`/app/companies/${company_id}`); - } - - handleDelete(companyId) { - const { deleteCompany, companies } = this.props; - let company = companies.find((s) => s.id === companyId); - - Swal.fire({ - title: T.translate("general.are_you_sure"), - text: - T.translate("company_list.delete_company_warning") + " " + company.name, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }).then(function (result) { - if (result.value) { - deleteCompany(companyId); - } - }); - } - - handlePageChange(page) { - const { term, order, orderDir, perPage } = this.props; - this.props.getCompanies(term, page, perPage, order, orderDir); - } - - handleSort(index, key, dir, func) { - const { term, page, perPage } = this.props; - this.props.getCompanies(term, page, perPage, key, dir); - } - - handleSearch(term) { - const { order, orderDir, page, perPage } = this.props; - this.props.getCompanies(term, page, perPage, order, orderDir); - } - - handleNewCompany(ev) { - const { history } = this.props; - history.push(`/app/companies/new`); - } - - render() { - const { - companies, - lastPage, - currentPage, - term, - order, - orderDir, - totalCompanies - } = this.props; - - const columns = [ - { columnKey: "id", value: "Id", sortable: true }, - { columnKey: "name", value: T.translate("general.name"), sortable: true }, - { columnKey: "contact_email", value: T.translate("general.email") }, - { - columnKey: "member_level", - value: T.translate("company_list.member_level") - } - ]; - - const table_options = { - sortCol: order, - sortDir: orderDir, - actions: { - edit: { onClick: this.handleEdit }, - delete: { onClick: this.handleDelete } - } - }; - - return ( -
-

- {" "} - {T.translate("company_list.company_list")} ({totalCompanies}){" "} -

-
-
- { + 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 }, + { columnKey: "contact_email", header: T.translate("general.email") }, + { + columnKey: "member_level", + header: T.translate("company_list.member_level") + } + ]; + + const table_options = { + sortCol: order, + sortDir: orderDir + }; + + useEffect(() => { + getCompanies(); + }, []); + + const handleEdit = (company) => { + getCompany(company.id).then(() => setCompanyPopup(true)); + }; + + const handleDelete = (companyId) => { + deleteCompany(companyId).then(() => + getCompanies(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir) + ); + }; + + const handlePageChange = (page) => { + getCompanies(term, page, perPage, order, orderDir); + }; + + const handlePerPageChange = (newPerPage) => { + getCompanies(term, DEFAULT_CURRENT_PAGE, newPerPage, order, orderDir); + }; + + const handleSort = (key, dir) => { + getCompanies(term, currentPage, perPage, key, dir); + }; + + const handleSearch = (searchTerm) => { + getCompanies(searchTerm, DEFAULT_CURRENT_PAGE, perPage, order, orderDir); + }; + + const handleNewCompany = () => { + resetCompanyForm(); + setCompanyPopup(true); + }; + + const handleSave = (entity) => + saveCompany(entity).then(() => + getCompanies(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir) + ); + + const handleClose = () => { + setCompanyPopup(false); + }; + + return ( +
+

{T.translate("company_list.company_list")}

+ + + + {totalCompanies} {T.translate("company_list.companies")} + + + + + -
-
- -
-
- - {companies.length > 0 && ( -
- - - - )} - - ); - } -} - -const mapStateToProps = ({ currentCompanyListState }) => ({ - ...currentCompanyListState + + + + + + {companies.length > 0 && ( + + T.translate("company_list.delete_company_warning", { name }) + } + /> + )} + + {companies.length === 0 && ( +
{T.translate("company_list.no_results")}
+ )} + + {companyPopup && ( + + )} + + ); +}; + +const mapStateToProps = ({ + currentCompanyListState, + sponsoredProjectListState, + currentCompanyState +}) => ({ + ...currentCompanyListState, + currentCompany: currentCompanyState.entity, + sponsoredProjects: sponsoredProjectListState.sponsoredProjects }); export default connect(mapStateToProps, { getCompanies, - deleteCompany + getCompany, + deleteCompany, + saveCompany, + resetCompanyForm, + getSponsoredProjects, + saveSupportingCompany, + deleteSupportingCompany, + attachLogo, + removeLogo })(CompanyListPage); diff --git a/src/pages/companies/components/__tests__/company-dialog.test.js b/src/pages/companies/components/__tests__/company-dialog.test.js new file mode 100644 index 000000000..a7aaa4195 --- /dev/null +++ b/src/pages/companies/components/__tests__/company-dialog.test.js @@ -0,0 +1,193 @@ +import React from "react"; +import { render, screen, waitFor, act } 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", () => ({ + UploadInputV3: () =>
+})); + +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