diff --git a/src/actions/form-template-item-actions.js b/src/actions/form-template-item-actions.js
index 710fdd911..53086a433 100644
--- a/src/actions/form-template-item-actions.js
+++ b/src/actions/form-template-item-actions.js
@@ -91,7 +91,7 @@ export const getFormTemplateItems =
const params = {
page,
- fields: "id,code,name,is_archived,images,images.file_url",
+ fields: "id,code,name,is_archived,images,images.file_url,images.created",
expand: "images",
per_page: perPage,
access_token: accessToken
diff --git a/src/actions/inventory-item-actions.js b/src/actions/inventory-item-actions.js
index 45814f39e..1b6277390 100644
--- a/src/actions/inventory-item-actions.js
+++ b/src/actions/inventory-item-actions.js
@@ -119,7 +119,7 @@ export const getInventoryItems =
const params = {
page,
fields:
- "id,code,name,images,images.file_url,is_archived,early_bird_rate,standard_rate,onsite_rate,default_quantity",
+ "id,code,name,images,images.file_url,images.created,is_archived,early_bird_rate,standard_rate,onsite_rate,default_quantity",
expand: "images",
per_page: perPage,
access_token: accessToken
diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js
index 2df0b36a6..c2ae2fb61 100644
--- a/src/actions/sponsor-forms-actions.js
+++ b/src/actions/sponsor-forms-actions.js
@@ -1056,8 +1056,8 @@ export const getSponsorFormItems =
const params = {
page,
fields:
- "id,code,name,early_bird_rate,standard_rate,onsite_rate,default_quantity,images,is_archived",
- relations: "images",
+ "id,code,name,early_bird_rate,standard_rate,onsite_rate,default_quantity,images,images.file_url,images.created,is_archived",
+ expand: "images",
per_page: perPage,
access_token: accessToken
};
@@ -1095,7 +1095,7 @@ export const getSponsorFormItem =
const params = {
access_token: accessToken,
- expands: "meta_fields,meta_fields.values,images"
+ expand: "meta_fields,meta_fields.values,images"
};
return getRequest(
diff --git a/src/app.js b/src/app.js
index 3f459ed2c..b649b2985 100644
--- a/src/app.js
+++ b/src/app.js
@@ -107,7 +107,8 @@ window.CFP_APP_BASE_URL = process.env.CFP_APP_BASE_URL;
window.DROPBOX_MATERIALIZER_API_BASE_URL =
process.env.DROPBOX_MATERIALIZER_API_BASE_URL;
window.FILE_UPLOAD_ALLOWED_EXTENSIONS =
- process.env.FILE_UPLOAD_ALLOWED_EXTENSIONS || ALLOWED_INVENTORY_IMAGE_FORMATS;
+ process.env.FILE_UPLOAD_ALLOWED_EXTENSIONS ||
+ ALLOWED_INVENTORY_IMAGE_FORMATS.join(",");
if (exclusiveSections.hasOwnProperty(process.env.APP_CLIENT_NAME)) {
window.EXCLUSIVE_SECTIONS = exclusiveSections[process.env.APP_CLIENT_NAME];
diff --git a/src/components/__tests__/image-preview-cell.test.js b/src/components/__tests__/image-preview-cell.test.js
new file mode 100644
index 000000000..8f16dc042
--- /dev/null
+++ b/src/components/__tests__/image-preview-cell.test.js
@@ -0,0 +1,68 @@
+import React from "react";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import T from "i18n-react/dist/i18n-react";
+import { ImagePreviewCell } from "../image-preview-cell";
+
+describe("ImagePreviewCell", () => {
+ const imageUrl = "https://example.com/path/sponsor_banner.png";
+ const title = T.translate("preview_modal.title");
+
+ test("does not render when imageUrl is null or empty", () => {
+ const { rerender } = render();
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+
+ rerender();
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+
+ test("renders a trigger button", () => {
+ render();
+ expect(screen.getByRole("button", { name: title })).toBeInTheDocument();
+ });
+
+ test("opens PreviewModal on click", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: title }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ });
+
+ test("passes itemName as dialog title", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: title }));
+ expect(screen.getByText("Sponsor Banner")).toBeInTheDocument();
+ });
+
+ test("extracts filename from URL when no fileName prop", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: title }));
+ expect(screen.getByText("sponsor_banner.png")).toBeInTheDocument();
+ });
+
+ test("uses fileName prop over URL extraction", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: title }));
+ expect(screen.getByText("custom.png")).toBeInTheDocument();
+ });
+
+ test("closes dialog when X is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: title }));
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "close" }));
+ await waitFor(() =>
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument()
+ );
+ });
+});
diff --git a/src/components/image-preview-cell.js b/src/components/image-preview-cell.js
new file mode 100644
index 000000000..82b1bb082
--- /dev/null
+++ b/src/components/image-preview-cell.js
@@ -0,0 +1,61 @@
+/**
+ * Copyright 2026 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 } from "react";
+import IconButton from "@mui/material/IconButton";
+import ImageIcon from "@mui/icons-material/Image";
+import T from "i18n-react/dist/i18n-react";
+import PreviewModal from "./mui/PreviewModal";
+import { isImageUrl } from "../utils/methods";
+
+export const ImagePreviewCell = React.memo(
+ ({ imageUrl, itemName, fileName, uploadDate }) => {
+ const [open, setOpen] = useState(false);
+
+ if (!imageUrl || !isImageUrl(imageUrl)) return null;
+
+ const rawSegment = imageUrl.split("/").pop().split("?")[0];
+ let decoded = rawSegment;
+ try {
+ decoded = decodeURIComponent(rawSegment);
+ } catch {
+ /* malformed encoding — keep raw */
+ }
+ const resolvedFileName = fileName || decoded;
+
+ return (
+ <>
+ setOpen(true)}
+ >
+
+
+
+ {open && (
+ setOpen(false)}
+ url={imageUrl}
+ filename={resolvedFileName}
+ uploadDate={uploadDate}
+ />
+ )}
+ >
+ );
+ }
+);
+
+ImagePreviewCell.displayName = "ImagePreviewCell";
diff --git a/src/components/mui/PreviewModal/index.jsx b/src/components/mui/PreviewModal/index.jsx
index b0cbd50a4..23b038794 100644
--- a/src/components/mui/PreviewModal/index.jsx
+++ b/src/components/mui/PreviewModal/index.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
import {
@@ -13,90 +13,111 @@ import CloseIcon from "@mui/icons-material/Close";
import BrokenImageOutlinedIcon from "@mui/icons-material/BrokenImageOutlined";
import { formatDate } from "../../../utils/methods";
-const PreviewModal = ({ title, open, onClose, url, filename, uploadDate }) => (
-
-);
+ {title}
+
+ ({
+ position: "absolute",
+ right: 12,
+ top: 12,
+ color: theme.palette.grey[500]
+ })}
+ >
+
+
+
+
+ {!url || imageError ? (
+
+ ) : (
+ setImageError(true)}
+ sx={{
+ maxWidth: "100%",
+ maxHeight: 400,
+ display: "block",
+ objectFit: "contain"
+ }}
+ />
+ )}
+
+
+ {filename && (
+
+
+ {T.translate("preview_modal.file_name")}
+
+
+ {filename}
+
+
+ )}
+ {!!uploadDate && (
+
+
+ {T.translate("preview_modal.uploaded")}
+
+ {formatDate(uploadDate)}
+
+ )}
+
+
+
+ );
+};
PreviewModal.propTypes = {
title: PropTypes.string.isRequired,
diff --git a/src/components/mui/__tests__/preview-modal.test.js b/src/components/mui/__tests__/preview-modal.test.js
new file mode 100644
index 000000000..1b0dd1a99
--- /dev/null
+++ b/src/components/mui/__tests__/preview-modal.test.js
@@ -0,0 +1,132 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import T from "i18n-react/dist/i18n-react";
+import PreviewModal from "../PreviewModal";
+
+describe("PreviewModal", () => {
+ const baseProps = {
+ title: "Test Image",
+ open: true,
+ onClose: jest.fn()
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("renders title", () => {
+ render();
+ expect(screen.getByText("Test Image")).toBeInTheDocument();
+ });
+
+ test("shows image when url is provided", () => {
+ render(
+
+ );
+ expect(screen.getByRole("img")).toHaveAttribute(
+ "src",
+ "https://example.com/img.png"
+ );
+ });
+
+ test("shows broken image icon when url is not provided", () => {
+ render();
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
+ });
+
+ test("shows filename when provided", () => {
+ render();
+ expect(
+ screen.getByText(T.translate("preview_modal.file_name"))
+ ).toBeInTheDocument();
+ expect(screen.getByText("sponsor.png")).toBeInTheDocument();
+ });
+
+ test("does not show filename row when filename is empty", () => {
+ render();
+ expect(
+ screen.queryByText(T.translate("preview_modal.file_name"))
+ ).not.toBeInTheDocument();
+ });
+
+ test("shows formatted uploadDate when provided", () => {
+ render();
+ expect(
+ screen.getByText(T.translate("preview_modal.uploaded"))
+ ).toBeInTheDocument();
+ });
+
+ test("does not show uploaded row when uploadDate is 0", () => {
+ render();
+ expect(
+ screen.queryByText(T.translate("preview_modal.uploaded"))
+ ).not.toBeInTheDocument();
+ });
+
+ test("calls onClose when X button is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: "close" }));
+ expect(baseProps.onClose).toHaveBeenCalledTimes(1);
+ });
+
+ test("calls onClose when clicking outside the dialog", async () => {
+ render();
+ fireEvent.keyDown(screen.getByRole("dialog"), { key: "Escape" });
+ expect(baseProps.onClose).toHaveBeenCalledTimes(1);
+ });
+
+ test("does not render dialog when open is false", () => {
+ render();
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+
+ test("shows broken image icon when image fails to load", () => {
+ render(
+
+ );
+ fireEvent.error(screen.getByRole("img"));
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
+ });
+
+ test("resets image error when modal is reopened", () => {
+ const { rerender } = render(
+
+ );
+
+ fireEvent.error(screen.getByRole("img"));
+ expect(screen.queryByRole("img")).not.toBeInTheDocument();
+
+ rerender(
+
+ );
+ rerender(
+
+ );
+ expect(screen.getByRole("img")).toBeInTheDocument();
+ });
+});
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 1ea40d141..1b707dac5 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -4229,6 +4229,7 @@
"cancel": "Cancel"
},
"preview_modal": {
+ "title": "Image Preview",
"file_name": "File name",
"uploaded": "Uploaded"
}
diff --git a/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js b/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
index 2a6f6cb06..86496b0ad 100644
--- a/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
+++ b/src/pages/sponsors-global/form-templates/add-form-template-item-popup.js
@@ -18,9 +18,9 @@ import {
} from "@mui/material";
import SwapVertIcon from "@mui/icons-material/SwapVert";
import CloseIcon from "@mui/icons-material/Close";
-import ImageIcon from "@mui/icons-material/Image";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
+import { ImagePreviewCell } from "../../../components/image-preview-cell";
import MenuButton from "../../../components/mui/menu-button";
import {
clearAllSelectedInventoryItems,
@@ -159,21 +159,18 @@ const AddFormTemplateItemDialog = ({
header: "",
width: 40,
align: "center",
- render: (row) =>
- row.images.length > 0 ? (
-
-
- window.open(
- row.images[0].file_url,
- "_blank",
- "noopener,noreferrer"
- )
- }
- />
-
- ) : null
+ render: (row) => {
+ const img = row.images?.[0];
+ const url = img?.file_url ?? img?.file_path;
+ if (!url) return null;
+ return (
+
+ );
+ }
}
];
diff --git a/src/pages/sponsors-global/form-templates/form-template-item-list-page.js b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
index 95e6bcd47..90263fa87 100644
--- a/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
+++ b/src/pages/sponsors-global/form-templates/form-template-item-list-page.js
@@ -24,13 +24,10 @@ import {
Grid2
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
-import IconButton from "@mui/material/IconButton";
-import Tooltip from "@mui/material/Tooltip";
-import ImageIcon from "@mui/icons-material/Image";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
+import { ImagePreviewCell } from "../../../components/image-preview-cell";
import {
cloneFromInventoryItem,
- deleteFormTemplateItem,
getFormTemplateItem,
getFormTemplateItems,
saveFormTemplateItem,
@@ -208,23 +205,18 @@ const FormTemplateItemListPage = ({
header: "",
width: 40,
align: "center",
- render: (row) =>
- row.images?.length > 0 ? (
-
-
-
- window.open(
- row.images[0].file_url,
- "_blank",
- "noopener,noreferrer"
- )
- }
- />
-
-
- ) : null
+ render: (row) => {
+ const img = row.images?.[0];
+ const url = img?.file_url ?? img?.file_path;
+ if (!url) return null;
+ return (
+
+ );
+ }
}
];
@@ -349,7 +341,6 @@ const mapStateToProps = ({
export default connect(mapStateToProps, {
cloneFromInventoryItem,
- deleteFormTemplateItem,
getFormTemplateItems,
getFormTemplate,
getInventoryItems,
diff --git a/src/pages/sponsors-global/inventory/__tests__/inventory-list-page.test.js b/src/pages/sponsors-global/inventory/__tests__/inventory-list-page.test.js
deleted file mode 100644
index ad788a1c6..000000000
--- a/src/pages/sponsors-global/inventory/__tests__/inventory-list-page.test.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import React from "react";
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import T from "i18n-react/dist/i18n-react";
-import { InventoryImagePreviewCell } from "../inventory-list-page";
-
-describe("InventoryListPage", () => {
- describe("InventoryImagePreviewCell", () => {
- const imageUrl = "https://example.com/image.png";
- const previewAlt = T.translate("inventory_item_list.image_preview_alt");
- const previewActionAlt = T.translate(
- "inventory_item_list.image_preview_action_alt"
- );
- const previewUnavailable = T.translate(
- "inventory_item_list.image_preview_unavailable"
- );
-
- beforeEach(() => {
- jest.restoreAllMocks();
- });
-
- test("opens preview on hover/focus and closes on leave/blur", async () => {
- const user = userEvent.setup();
-
- render();
-
- const button = screen.getByRole("button");
-
- await user.hover(button);
- await waitFor(() => {
- expect(screen.getByAltText(previewAlt)).toBeInTheDocument();
- });
-
- await user.unhover(button);
- await waitFor(() => {
- expect(screen.queryByRole("img")).not.toBeInTheDocument();
- });
-
- button.focus();
- await waitFor(() => {
- expect(screen.getByAltText(previewAlt)).toBeInTheDocument();
- });
-
- button.blur();
- await waitFor(() => {
- expect(screen.queryByRole("img")).not.toBeInTheDocument();
- });
- });
-
- test("does not render when imageUrl is null or empty", () => {
- const { rerender } = render(
-
- );
-
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
-
- rerender();
-
- expect(screen.queryByRole("button")).not.toBeInTheDocument();
- });
-
- test("sets explicit aria-label for accessibility", () => {
- render();
-
- expect(
- screen.getByRole("button", { name: previewActionAlt })
- ).toBeInTheDocument();
- });
-
- test("shows fallback text if image fails to load", async () => {
- const user = userEvent.setup();
-
- render();
-
- const button = screen.getByRole("button");
- await user.hover(button);
-
- const image = screen.getByAltText(previewAlt);
- fireEvent.error(image);
-
- expect(screen.getByText(previewUnavailable)).toBeInTheDocument();
- });
-
- test("keeps click action while preview is visible", async () => {
- const openSpy = jest.spyOn(window, "open").mockImplementation(() => null);
- const user = userEvent.setup();
-
- render();
-
- const button = screen.getByRole("button");
- await user.hover(button);
-
- await waitFor(() => {
- expect(screen.getByAltText(previewAlt)).toBeInTheDocument();
- });
-
- await user.click(button);
-
- expect(openSpy).toHaveBeenCalledWith(
- imageUrl,
- "_blank",
- "noopener,noreferrer"
- );
- });
- });
-});
diff --git a/src/pages/sponsors-global/inventory/inventory-list-page.js b/src/pages/sponsors-global/inventory/inventory-list-page.js
index b6e0d072f..2dfb4964a 100644
--- a/src/pages/sponsors-global/inventory/inventory-list-page.js
+++ b/src/pages/sponsors-global/inventory/inventory-list-page.js
@@ -18,21 +18,16 @@ import {
Checkbox,
FormControlLabel,
FormGroup,
- Grid2,
- Popover,
- Typography
+ Grid2
} from "@mui/material";
import Box from "@mui/material/Box";
import AddIcon from "@mui/icons-material/Add";
-import IconButton from "@mui/material/IconButton";
-import ImageIcon from "@mui/icons-material/Image";
import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
import { connect } from "react-redux";
import T from "i18n-react/dist/i18n-react";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
import {
archiveInventoryItem,
- deleteInventoryItem,
deleteInventoryItemImage,
deleteInventoryItemMetaFieldType,
deleteInventoryItemMetaFieldTypeValue,
@@ -43,118 +38,9 @@ import {
unarchiveInventoryItem
} from "../../../actions/inventory-item-actions";
import SponsorInventoryDialog from "../form-templates/sponsor-inventory-popup";
+import { ImagePreviewCell } from "../../../components/image-preview-cell";
import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants";
-const PREVIEW_BOX_SIZE = 220;
-const PREVIEW_MARGIN_BOTTOM = 1;
-const PREVIEW_TRANSITION_DURATION = { enter: 120, exit: 90 };
-const PREVIEW_POINTER_EVENTS = "none";
-
-export const InventoryImagePreviewCell = React.memo(
- ({ imageUrl, itemName }) => {
- const [anchorEl, setAnchorEl] = useState(null);
- const [imageLoadError, setImageLoadError] = useState(false);
-
- const previewActionLabel = itemName
- ? T.translate("inventory_item_list.image_preview_action_alt", {
- itemName
- })
- : T.translate("inventory_item_list.image_preview_action_alt");
-
- const previewImgAlt = itemName
- ? T.translate("inventory_item_list.image_preview_alt", { itemName })
- : T.translate("inventory_item_list.image_preview_alt");
-
- if (!imageUrl) return null;
-
- const isOpen = Boolean(anchorEl);
-
- const openPreview = (target) => {
- setAnchorEl(target);
- setImageLoadError(false);
- };
-
- const closePreview = () => setAnchorEl(null);
-
- return (
- <>
- openPreview(event.currentTarget)}
- onMouseLeave={closePreview}
- onFocus={(event) => openPreview(event.currentTarget)}
- onBlur={closePreview}
- onClick={() => window.open(imageUrl, "_blank", "noopener,noreferrer")}
- >
-
-
-
-
-
- {!imageLoadError ? (
- setImageLoadError(true)}
- sx={{
- width: "100%",
- height: "100%",
- display: "block",
- objectFit: "contain"
- }}
- />
- ) : (
-
- {T.translate("inventory_item_list.image_preview_unavailable")}
-
- )}
-
-
- >
- );
- }
-);
-
-InventoryImagePreviewCell.displayName = "InventoryImagePreviewCell";
-
const InventoryListPage = ({
inventoryItems,
currentInventoryItem,
@@ -284,14 +170,17 @@ const InventoryListPage = ({
width: 40,
align: "center",
render: (row) => {
- const hasImages = Array.isArray(row.images) && row.images.length > 0;
- const imageUrl = row.images?.[0]?.file_url;
- const itemName = row.name;
+ const img = row.images?.[0];
+ const imageUrl = img?.file_url ?? img?.file_path;
- if (!hasImages || !imageUrl) return null;
+ if (!imageUrl) return null;
return (
-
+
);
}
}
@@ -425,7 +314,7 @@ export default connect(mapStateToProps, {
getInventoryItem,
resetInventoryItemForm,
saveInventoryItem,
- deleteInventoryItem,
+
deleteInventoryItemImage,
deleteInventoryItemMetaFieldType,
deleteInventoryItemMetaFieldTypeValue,
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
index 0b3d89101..e2b40d06e 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/components/sponsor-form-add-item-from-inventory-popup.js
@@ -16,11 +16,10 @@ import {
Typography
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
-import Tooltip from "@mui/material/Tooltip";
-import ImageIcon from "@mui/icons-material/Image";
import Box from "@mui/material/Box";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
+import { ImagePreviewCell } from "../../../../components/image-preview-cell";
import { formatRateFromCents } from "../../../../utils/rate-helpers";
import { addInventoryItems } from "../../../../actions/sponsor-forms-actions";
import { getInventoryItems } from "../../../../actions/inventory-item-actions";
@@ -146,23 +145,18 @@ const SponsorFormAddItemFromInventoryPopup = ({
header: "",
width: 40,
align: "center",
- render: (row) =>
- row.images?.length > 0 ? (
-
-
-
- window.open(
- row.images[0].file_url,
- "_blank",
- "noopener,noreferrer"
- )
- }
- />
-
-
- ) : null
+ render: (row) => {
+ const img = row.images?.[0];
+ const url = img?.file_url ?? img?.file_path;
+ if (!url) return null;
+ return (
+
+ );
+ }
}
];
diff --git a/src/pages/sponsors/sponsor-form-item-list-page/index.js b/src/pages/sponsors/sponsor-form-item-list-page/index.js
index b8b2822a0..03768bc3a 100644
--- a/src/pages/sponsors/sponsor-form-item-list-page/index.js
+++ b/src/pages/sponsors/sponsor-form-item-list-page/index.js
@@ -25,10 +25,8 @@ import {
Grid2
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
-import IconButton from "@mui/material/IconButton";
-import Tooltip from "@mui/material/Tooltip";
-import ImageIcon from "@mui/icons-material/Image";
import MuiTableEditable from "openstack-uicore-foundation/lib/components/mui/editable-table";
+import { ImagePreviewCell } from "../../../components/image-preview-cell";
import {
deleteSponsorFormItem,
getSponsorFormItem,
@@ -186,23 +184,18 @@ const SponsorFormItemListPage = ({
header: "",
width: 40,
align: "center",
- render: (row) =>
- row.images?.length > 0 ? (
-
-
-
- window.open(
- row.images[0].file_url,
- "_blank",
- "noopener,noreferrer"
- )
- }
- />
-
-
- ) : null
+ render: (row) => {
+ const img = row.images?.[0];
+ const url = img?.file_url ?? img?.file_path;
+ if (!url) return null;
+ return (
+
+ );
+ }
}
];
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
index ddeb525d5..2b65abe0a 100644
--- a/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js
@@ -14,14 +14,13 @@ import {
FormControlLabel,
Grid2,
IconButton,
- Tooltip,
Typography
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
-import ImageIcon from "@mui/icons-material/Image";
import SwapVertIcon from "@mui/icons-material/SwapVert";
import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
+import { ImagePreviewCell } from "../../../../../../../components/image-preview-cell";
import { formatRateFromCents } from "../../../../../../../utils/rate-helpers";
import {
DEFAULT_CURRENT_PAGE,
@@ -145,23 +144,18 @@ const SponsorFormItemFromInventoryPopup = ({
header: "",
width: 40,
align: "center",
- render: (row) =>
- row.images?.length > 0 ? (
-
-
-
- window.open(
- row.images[0].file_url,
- "_blank",
- "noopener,noreferrer"
- )
- }
- />
-
-
- ) : null
+ render: (row) => {
+ const img = row.images?.[0];
+ const url = img?.file_url ?? img?.file_path;
+ if (!url) return null;
+ return (
+
+ );
+ }
}
];
diff --git a/src/utils/__tests__/methods.test.js b/src/utils/__tests__/methods.test.js
index ac5275c43..0200a1756 100644
--- a/src/utils/__tests__/methods.test.js
+++ b/src/utils/__tests__/methods.test.js
@@ -1,4 +1,8 @@
-import { getMediaInputValue, normalizeSelectAllField } from "../methods";
+import {
+ getMediaInputValue,
+ isImageUrl,
+ normalizeSelectAllField
+} from "../methods";
const FIXED_NOW = 1_772_551_911_231;
beforeAll(() => jest.spyOn(Date, "now").mockReturnValue(FIXED_NOW));
@@ -120,4 +124,33 @@ describe("getMediaInputValue", () => {
});
});
});
+
+ describe("isImageUrl", () => {
+ it.each(["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"])(
+ "returns true for .%s extension",
+ (ext) => {
+ expect(isImageUrl(`https://example.com/file.${ext}`)).toBe(true);
+ }
+ );
+
+ it("is case-insensitive", () => {
+ expect(isImageUrl("https://example.com/file.JPG")).toBe(true);
+ expect(isImageUrl("https://example.com/file.PNG")).toBe(true);
+ });
+
+ it("works with query strings", () => {
+ expect(isImageUrl("https://example.com/file.png?v=123")).toBe(true);
+ });
+
+ it.each(["pdf", "mp4", "doc", "csv", "pptx"])(
+ "returns false for .%s extension",
+ (ext) => {
+ expect(isImageUrl(`https://example.com/file.${ext}`)).toBe(false);
+ }
+ );
+
+ it("returns false for empty string", () => {
+ expect(isImageUrl("")).toBe(false);
+ });
+ });
});
diff --git a/src/utils/methods.js b/src/utils/methods.js
index dd426a3dd..a2bc7edab 100644
--- a/src/utils/methods.js
+++ b/src/utils/methods.js
@@ -634,5 +634,13 @@ export const formatDate = (
.format(format);
};
-export const getFileUploadAllowedExtensions = () =>
- window.FILE_UPLOAD_ALLOWED_EXTENSIONS?.split(",").filter(Boolean) ?? [];
+export const getFileUploadAllowedExtensions = () => {
+ const ext = window.FILE_UPLOAD_ALLOWED_EXTENSIONS;
+ if (!ext) return [];
+ return (Array.isArray(ext) ? ext : String(ext).split(","))
+ .map((s) => String(s).trim())
+ .filter(Boolean);
+};
+
+export const isImageUrl = (url) =>
+ /\.(jpe?g|png|gif|webp|svg|bmp)(\?|$)/i.test(url);