diff --git a/src/components/forms/form-template-form.js b/src/components/forms/form-template-form.js deleted file mode 100644 index 3123cdd09..000000000 --- a/src/components/forms/form-template-form.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * 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 - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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, { useState, useEffect, useRef } from "react"; -import T from "i18n-react/dist/i18n-react"; -import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import { - Input, - UploadInputV2 -} from "openstack-uicore-foundation/lib/components"; -import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; -import Swal from "sweetalert2"; -import FormRepeater from "../form-repeater"; -import FormTemplateMetaFieldForm from "./form-template-meta-field-form"; -import { scrollToError, shallowEqual, hasErrors } from "../../utils/methods"; -import { - MAX_FORM_TEMPLATE_MATERIALS_UPLOAD_SIZE, - MAX_FORM_TEMPLATE_MATERIALS_UPLOAD_QTY, - ALLOWED_FORM_TEMPLATE_MATERIAL_FORMATS -} from "../../utils/constants"; - -const FormTemplateForm = ({ - entity: initialEntity, - errors: initialErrors, - onMetaFieldTypeDeleted, - onMetaFieldTypeValueDeleted, - onMaterialDeleted, - onSubmit -}) => { - const repeaterRef = useRef(null); - const [entity, setEntity] = useState({ ...initialEntity }); - const [errors, setErrors] = useState(initialErrors); - - const mediaType = { - max_size: MAX_FORM_TEMPLATE_MATERIALS_UPLOAD_SIZE, - max_uploads_qty: MAX_FORM_TEMPLATE_MATERIALS_UPLOAD_QTY, - type: { - allowed_extensions: ALLOWED_FORM_TEMPLATE_MATERIAL_FORMATS - } - }; - - useEffect(() => { - scrollToError(initialErrors); - if (!shallowEqual(initialEntity, entity)) { - setEntity({ ...initialEntity }); - setErrors({}); - } - - if (!shallowEqual(initialErrors, errors)) { - setErrors({ ...initialErrors }); - } - }, [initialEntity, initialErrors]); - - const handleChange = (ev) => { - const { id, value, checked, type } = ev.target; - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - [id]: type === "checkbox" ? checked : value - })); - setErrors((prevErrors) => ({ ...prevErrors, [id]: "" })); - }; - - const handleMaterialUploadComplete = (response) => { - if (response) { - const material = { - file_path: `${response.path}${response.name}`, - filename: response.name - }; - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - materials: [...prevEntity.materials, material] - })); - } - }; - - const handleRemoveMaterial = (materialFile) => { - const materials = entity.materials.filter( - (material) => material.filename != materialFile.name - ); - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - materials - })); - - if (onMaterialDeleted && entity.id && materialFile.id) { - onMaterialDeleted(entity.id, materialFile.id); - } - }; - - const getMediaInputValue = () => - entity.materials.length > 0 - ? entity.materials.map((material) => ({ - ...material, - filename: material.filename ?? material.file_path ?? material.file_url - })) - : []; - - const handleSubmit = (ev) => { - ev.preventDefault(); - entity.meta_fields = getNormalizedMetaFields(); - onSubmit(entity); - }; - - const getNormalizedMetaFields = () => { - if (repeaterRef.current) { - const content = repeaterRef.current.getContent(); - - return content.map((item) => { - const idSuffix = item.id; - const newValue = Object.fromEntries( - Object.entries(item.value).map(([key, value]) => { - const newKey = key.replace(`_${idSuffix}`, ""); - return [newKey, value]; - }) - ); - return newValue; - }); - } - return []; - }; - - const initMetaFieldLines = (metaFields) => [ - ...metaFields - .filter((metaField) => metaField.id) - .sort((a, b) => a.id - b.id) - .map((metaField) => ({ - id: metaField.id, - value: { ...metaField } - })), - ...metaFields - .filter((metaField) => !metaField.id) - .map((metaField) => ({ - id: Date.now(), - value: { ...metaField } - })) - ]; - - const handleRemoveMetaFieldType = async (metaField) => { - if (!onMetaFieldTypeDeleted || !metaField.value.id) { - return true; - } - const result = await Swal.fire({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("edit_form_template.delete_meta_field_warning")} ${ - metaField.value.id - }`, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - if (result.value) { - onMetaFieldTypeDeleted(entity.id, metaField.value.id); - return true; - } - return false; - }; - - const handleRemoveMetaFieldTypeValue = (metaFieldId, metaFieldValueId) => { - if (onMetaFieldTypeDeleted) { - onMetaFieldTypeValueDeleted(entity.id, metaFieldId, metaFieldValueId); - } - }; - - const renderMetaFieldForm = (line, updateValue) => ( - - ); - - return ( -
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- -
-
-
- ); -}; - -export default FormTemplateForm; diff --git a/src/components/forms/inventory-item-form.js b/src/components/forms/inventory-item-form.js deleted file mode 100644 index 6b9b7ed4c..000000000 --- a/src/components/forms/inventory-item-form.js +++ /dev/null @@ -1,358 +0,0 @@ -/** - * 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 - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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, { useState, useEffect, useRef } from "react"; -import T from "i18n-react/dist/i18n-react"; -import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css"; -import { - Input, - UploadInputV2 -} from "openstack-uicore-foundation/lib/components"; -import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3"; -import Swal from "sweetalert2"; -import FormRepeater from "../form-repeater"; -import InventoryItemMetaFieldForm from "./inventory-item-meta-field-form"; -import { scrollToError, shallowEqual, hasErrors } from "../../utils/methods"; -import { - MAX_INVENTORY_IMAGE_UPLOAD_SIZE, - MAX_INVENTORY_IMAGES_UPLOAD_QTY, - ALLOWED_INVENTORY_IMAGE_FORMATS -} from "../../utils/constants"; - -const InventoryItemForm = ({ - entity: initialEntity, - errors: initialErrors, - onMetaFieldTypeDeleted, - onMetaFieldTypeValueDeleted, - onImageDeleted, - onSubmit -}) => { - const repeaterRef = useRef(null); - const [entity, setEntity] = useState({ ...initialEntity }); - const [errors, setErrors] = useState(initialErrors); - - const mediaType = { - max_size: MAX_INVENTORY_IMAGE_UPLOAD_SIZE, - max_uploads_qty: MAX_INVENTORY_IMAGES_UPLOAD_QTY, - type: { - allowed_extensions: ALLOWED_INVENTORY_IMAGE_FORMATS - } - }; - - useEffect(() => { - scrollToError(initialErrors); - if (!shallowEqual(initialEntity, entity)) { - setEntity({ ...initialEntity }); - setErrors({}); - } - - if (!shallowEqual(initialErrors, errors)) { - setErrors({ ...initialErrors }); - } - }, [initialEntity, initialErrors]); - - const handleChange = (ev) => { - const { id, value, checked, type } = ev.target; - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - [id]: type === "checkbox" ? checked : value - })); - setErrors((prevErrors) => ({ ...prevErrors, [id]: "" })); - }; - - const handleImageUploadComplete = (response) => { - if (response) { - const image = { - file_path: `${response.path}${response.name}`, - filename: response.name - }; - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - images: [...prevEntity.images, image] - })); - } - }; - - const handleRemoveImage = (imageFile) => { - const images = entity.images.filter( - (image) => image.filename != imageFile.name - ); - setEntity((prevEntity) => ({ - ...prevEntity, - meta_fields: getNormalizedMetaFields(), - images - })); - - if (onImageDeleted && entity.id && imageFile.id) { - onImageDeleted(entity.id, imageFile.id); - } - }; - - const getMediaInputValue = () => - entity.images.length > 0 - ? entity.images.map((img) => ({ - ...img, - filename: img.filename ?? img.file_path ?? img.file_url - })) - : []; - - const handleSubmit = (ev) => { - ev.preventDefault(); - entity.meta_fields = getNormalizedMetaFields(); - onSubmit(entity); - }; - - const getNormalizedMetaFields = () => { - if (repeaterRef.current) { - const content = repeaterRef.current.getContent(); - - return content.map((item) => { - const idSuffix = item.id; - const newValue = Object.fromEntries( - Object.entries(item.value).map(([key, value]) => { - const newKey = key.replace(`_${idSuffix}`, ""); - return [newKey, value]; - }) - ); - return newValue; - }); - } - return []; - }; - - const initMetaFieldLines = (metaFields) => [ - ...metaFields - .filter((metaField) => metaField.id) - .sort((a, b) => a.id - b.id) - .map((metaField) => ({ - id: metaField.id, - value: { ...metaField } - })), - ...metaFields - .filter((metaField) => !metaField.id) - .map((metaField) => ({ - id: Date.now(), - value: { ...metaField } - })) - ]; - - const handleRemoveMetaFieldType = async (metaField) => { - if (!onMetaFieldTypeDeleted || !metaField.value.id) { - return true; - } - const result = await Swal.fire({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("edit_inventory_item.delete_meta_field_warning")} ${ - metaField.value.id - }`, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }); - if (result.value) { - onMetaFieldTypeDeleted(entity.id, metaField.value.id); - return true; - } - return false; - }; - - const handleRemoveMetaFieldTypeValue = (metaFieldId, metaFieldValueId) => { - if (onMetaFieldTypeDeleted) { - onMetaFieldTypeValueDeleted(entity.id, metaFieldId, metaFieldValueId); - } - }; - - const renderMetaFieldForm = (line, updateValue) => ( - - ); - - return ( -
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- -
-
-
- ); -}; - -export default InventoryItemForm; diff --git a/src/layouts/page-template-layout.js b/src/layouts/page-template-layout.js index 445381daa..37eba71bb 100644 --- a/src/layouts/page-template-layout.js +++ b/src/layouts/page-template-layout.js @@ -17,7 +17,6 @@ import T from "i18n-react/dist/i18n-react"; import { Breadcrumb } from "react-breadcrumbs"; import Restrict from "../routes/restrict"; import NoMatchPage from "../pages/no-match-page"; -import EditPageTemplatePage from "../pages/sponsors-global/page-templates/edit-page-template-page"; import PageTemplateListPage from "../pages/sponsors-global/page-templates/page-template-list-page"; const PageTemplateLayout = ({ match }) => ( @@ -29,18 +28,6 @@ const PageTemplateLayout = ({ match }) => ( }} /> - - )} {showInventoryItemModal && ( - - initialEntity.images?.length > 0 - ? initialEntity.images.map((img) => { - const filename = img.filename ?? img.file_path ?? img.file_url; - return { - ...img, - filename: filename.concat("?t=", Date?.now()) - }; - }) - : []; - const handleClose = () => { formik.resetForm(); onClose(); @@ -283,7 +273,7 @@ const SponsorItemDialog = ({ id="image-upload" name="image" onUploadComplete={handleImageUploadComplete} - value={getMediaInputValue()} + value={getMediaInputValue(initialEntity)} mediaType={mediaType} onRemove={handleRemoveImage} postUrl={`${window.FILE_UPLOAD_API_BASE_URL}/api/v1/files/upload`} diff --git a/src/pages/sponsors-global/page-templates/edit-page-template-page.js b/src/pages/sponsors-global/page-templates/edit-page-template-page.js deleted file mode 100644 index cc3c1d25e..000000000 --- a/src/pages/sponsors-global/page-templates/edit-page-template-page.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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 - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * 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, { useEffect } from "react"; -import { connect } from "react-redux"; -import { Breadcrumb } from "react-breadcrumbs"; -import T from "i18n-react/dist/i18n-react"; -import FormTemplateForm from "../../../components/forms/form-template-form"; -import { - getFormTemplate, - resetFormTemplateForm, - saveFormTemplate, - deleteFormTemplateMetaFieldType, - deleteFormTemplateMetaFieldTypeValue, - deleteFormTemplateMaterial -} from "../../../actions/form-template-actions"; - -const EditPageTemplatePage = (props) => { - const { - match, - entity, - errors, - getFormTemplate, - resetFormTemplateForm, - saveFormTemplate, - deleteFormTemplateMetaFieldType, - deleteFormTemplateMetaFieldTypeValue, - deleteFormTemplateMaterial - } = props; - const pageTemplateId = match.params.page_template_id; - - useEffect(() => { - if (!pageTemplateId) { - resetFormTemplateForm(); - } else { - getFormTemplate(pageTemplateId); - } - }, [pageTemplateId, getFormTemplate, resetFormTemplateForm]); - - const title = entity.id - ? T.translate("general.edit") - : T.translate("general.add"); - const breadcrumb = entity.id ? entity.name : T.translate("general.new"); - - return ( -
- -

- {title} {T.translate("edit_form_template.form_template")} -

-
- -
- ); -}; - -const mapStateToProps = ({ currentFormTemplateState }) => ({ - ...currentFormTemplateState -}); - -export default connect(mapStateToProps, { - getFormTemplate, - resetFormTemplateForm, - saveFormTemplate, - deleteFormTemplateMetaFieldType, - deleteFormTemplateMetaFieldTypeValue, - deleteFormTemplateMaterial -})(EditPageTemplatePage); diff --git a/src/utils/__tests__/methods.test.js b/src/utils/__tests__/methods.test.js new file mode 100644 index 000000000..4f964d34f --- /dev/null +++ b/src/utils/__tests__/methods.test.js @@ -0,0 +1,58 @@ +import { getMediaInputValue } from "../methods"; + +const FIXED_NOW = 1_772_551_911_231; +beforeAll(() => jest.spyOn(Date, "now").mockReturnValue(FIXED_NOW)); +afterAll(() => jest.restoreAllMocks()); + +describe("getMediaInputValue", () => { + describe("fileUrl guard — all url fields undefined/null", () => { + it("should does NOT throw TypeError when all url fields are undefined", () => { + expect(() => getMediaInputValue({ images: [{ id: 1 }] })).not.toThrow(); + }); + + it("should returns filename: '' when all url fields are null", () => { + const [result] = getMediaInputValue({ + images: [{ filename: null, file_path: null, file_url: null }] + }); + expect(result.filename).toBe(""); + }); + + it("should preserves other props on the image object when filename is empty", () => { + const [result] = getMediaInputValue({ + images: [{ id: 42, alt: "broken" }] + }); + expect(result).toMatchObject({ id: 42, alt: "broken", filename: "" }); + }); + }); + + describe("path stripping", () => { + it("should strips the directory prefix and keeps only the basename", () => { + const [result] = getMediaInputValue({ + images: [{ filename: "uploads/2024/photo.jpg" }] + }); + expect(result.filename).toBe("photo.jpg"); + }); + + it("should keeps the filename unchanged when there is no slash", () => { + const [result] = getMediaInputValue({ + images: [{ filename: "photo.jpg" }] + }); + expect(result.filename).toBe("photo.jpg"); + }); + }); + + describe("files without extensions", () => { + it("should returns 'README' as filename", () => { + const [result] = getMediaInputValue({ images: [{ filename: "README" }] }); + expect(result.filename).toBe("README"); + expect(result.filename.startsWith(".")).toBe(false); + }); + + it("should strips the path for an extension-less file in a subdirectory", () => { + const [result] = getMediaInputValue({ + images: [{ filename: "some/path/README" }] + }); + expect(result.filename).toBe("README"); + }); + }); +}); diff --git a/src/utils/constants.js b/src/utils/constants.js index c6b095009..2e432af48 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -155,9 +155,13 @@ export const EVENT_TYPE_FISHBOWL = "Fishbowl"; export const EVENT_TYPE_GROUP_EVENTS = "Groups Events"; +export const FILENAME_TRUNCATE_SIDE_PERCENT = 0.4; + +export const TRIM_TEXT_LENGTH_20 = 20; + export const TRIM_TEXT_LENGTH_50 = 50; -export const TRIM_TEXT_LENGTH_40 = 50; +export const TRIM_TEXT_LENGTH_40 = 40; export const LANGUAGE_CODE_LENGTH = 2; diff --git a/src/utils/methods.js b/src/utils/methods.js index a73060818..a9ab9fce5 100644 --- a/src/utils/methods.js +++ b/src/utils/methods.js @@ -18,6 +18,7 @@ import { initLogOut } from "openstack-uicore-foundation/lib/security/methods"; import Swal from "sweetalert2"; +import URI from "urijs"; import * as Sentry from "@sentry/react"; import T from "i18n-react/dist/i18n-react"; import { @@ -532,5 +533,25 @@ export const formatBadgeQR = (code, summit) => { return null; }; +export const getMediaInputValue = (entity, fieldName = "images") => { + const mediaFiles = entity?.[fieldName]; + if (!mediaFiles?.length) return []; + + return mediaFiles.map((img) => { + const fileUrl = img.filename ?? img.file_path ?? img.file_url; + if (!fileUrl) return { ...img, filename: "" }; + + const fileName = new URI(fileUrl).filename(); + const publicURL = new URI(img?.public_url || fileUrl) + .setQuery("t", Date.now()) + .toString(); + + return { + ...img, + public_url: publicURL, + filename: fileName + }; + }); +}; // eslint-disable-next-line no-magic-numbers export const bytesToMb = (bytes) => (bytes / BYTES_IN_MEGABYTE).toFixed(2);