From 0b05eddfbdf185dc1b6a9c1bf1f7836f58e47e4e Mon Sep 17 00:00:00 2001 From: Jonathan Irhodia Date: Wed, 3 Dec 2025 11:36:03 +0100 Subject: [PATCH 1/4] fix: zero value formatting to "N/A" (#714) --- .../src/api/utils/customDataUtils.test.ts | 29 +++++++++++++++++++ .../src/api/utils/customDataUtils.tsx | 14 +++++++++ .../custom-modules/TableComponents.tsx | 3 -- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/api/utils/customDataUtils.test.ts b/packages/frontend/src/api/utils/customDataUtils.test.ts index 8188d8a84..aba715411 100644 --- a/packages/frontend/src/api/utils/customDataUtils.test.ts +++ b/packages/frontend/src/api/utils/customDataUtils.test.ts @@ -3,6 +3,7 @@ import { TCustomItem } from "src/api/types"; import { validateCustomData, evaluateTranslationTemplate, + formatCustomDataField, } from "./customDataUtils"; export const validCustomData: JSONValue = [ @@ -93,4 +94,32 @@ describe("Tests for custom data utilities", () => { ); }); }); + + test("formatCustomDataField: zero numeric values show N/A", () => { + // number format + expect( + formatCustomDataField({ rawField: "0", format: "number" }).field + ).toBe("N/A"); + + expect( + formatCustomDataField({ rawField: "0", format: "decimal" }).field + ).toBe("N/A"); + expect( + formatCustomDataField({ rawField: "0.00", format: "decimal" }).field + ).toBe("N/A"); + + expect( + formatCustomDataField({ rawField: "0", format: "currency" }).field + ).toBe("N/A"); + + // percentage: zero should be formatted normally (eg. "0%") not N/A + expect( + formatCustomDataField({ + rawField: "0%", + format: "percentage", + }).field + ).toBe( + formatCustomDataField({ rawField: "0", format: "percentage" }).field + ); + }); }); diff --git a/packages/frontend/src/api/utils/customDataUtils.tsx b/packages/frontend/src/api/utils/customDataUtils.tsx index bb24a7df0..319d60979 100644 --- a/packages/frontend/src/api/utils/customDataUtils.tsx +++ b/packages/frontend/src/api/utils/customDataUtils.tsx @@ -307,6 +307,13 @@ export const formatCustomDataField: ( error: undefined, }; } + // When the type is numeric and the value is zero, show N/A instead of "0" + const parsedNumber = Number( + String(rawField).replace(/[^0-9.-]+/g, "") + ); + if (!Number.isNaN(parsedNumber) && parsedNumber === 0) { + return { field: "N/A", error: undefined }; + } return { field: formatNumber({ value: rawField }).value, error: undefined, @@ -325,6 +332,12 @@ export const formatCustomDataField: ( error: undefined, }; } + const parsedNumber = Number( + String(rawField).replace(/[^0-9.-]+/g, "") + ); + if (!Number.isNaN(parsedNumber) && parsedNumber === 0) { + return { field: "N/A", error: undefined }; + } return { field: formatNumber({ value: rawField, @@ -347,6 +360,7 @@ export const formatCustomDataField: ( error: undefined, }; } + // For percentages, keep zero values as formatted strings (e.g., "0%") return { field: formatNumber({ value: rawField, diff --git a/packages/frontend/src/components/custom-modules/TableComponents.tsx b/packages/frontend/src/components/custom-modules/TableComponents.tsx index 942d0d3f8..a69febb83 100644 --- a/packages/frontend/src/components/custom-modules/TableComponents.tsx +++ b/packages/frontend/src/components/custom-modules/TableComponents.tsx @@ -232,7 +232,6 @@ export const GridBasedTable: React.FC = ({ gridAutoRows: "min-content", }} > - {/* Headers */} {visibleColumns.map((column) => (
= ({
))} - {/* Cells */} {items.map((item) => visibleColumns.map((column) => { const rawValue = @@ -282,7 +280,6 @@ export const GridBasedTable: React.FC = ({ className="px-5 py-2 border-b border-borderLine hover:bg-background max-w-[200px] min-h-0" style={{ minWidth: `${minCellSize}px` }} > - {/* Your cell content rendering logic */} {column.format === "image" && imageUri ? ( Date: Mon, 8 Dec 2025 13:36:28 +0100 Subject: [PATCH 2/4] feat: Add row link functionality to GridBasedTable (#715) Co-authored-by: Xharles --- .../custom-modules/TableComponents.tsx | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/custom-modules/TableComponents.tsx b/packages/frontend/src/components/custom-modules/TableComponents.tsx index a69febb83..ca6cf74a9 100644 --- a/packages/frontend/src/components/custom-modules/TableComponents.tsx +++ b/packages/frontend/src/components/custom-modules/TableComponents.tsx @@ -1,3 +1,4 @@ +import React from "react"; import { twMerge } from "@alphaday/ui-kit"; import { TRemoteCustomLayoutEntry, @@ -218,13 +219,42 @@ interface IGridBasedTableProps { export const GridBasedTable: React.FC = ({ columnsLayout, items, + rowProps, minCellSize, options, }) => { + const gridRef = React.useRef(null); const visibleColumns = columnsLayout.filter((col) => !col.hidden); + const getRowLink = (item: TCustomItem): string | undefined => { + if (rowProps?.uri_ref !== undefined) { + const uriRef = item[rowProps.uri_ref]; + return typeof uriRef === "string" ? uriRef : undefined; + } + return undefined; + }; + + const handleRowClick = (rowLink: string | undefined) => { + if (rowLink) window.open(rowLink, "_blank"); + }; + + const handleRowHover = (itemId: string | number, isEntering: boolean) => { + if (!gridRef.current) return; + const cells = gridRef.current.querySelectorAll( + `[data-row-id="${itemId}"]` + ); + cells.forEach((cell) => { + if (isEntering) { + cell.classList.add("bg-backgroundVariant200"); + } else { + cell.classList.remove("bg-backgroundVariant200"); + } + }); + }; + return (
= ({
))} - {items.map((item) => - visibleColumns.map((column) => { + {items.map((item) => { + const rowLink = getRowLink(item); + return visibleColumns.map((column) => { const rawValue = column.template !== undefined ? evaluateTranslationTemplate(column.template, item) @@ -277,8 +308,19 @@ export const GridBasedTable: React.FC = ({ return (
handleRowClick(rowLink), + onMouseEnter: () => + handleRowHover(item.id, true), + onMouseLeave: () => + handleRowHover(item.id, false), + })} > {column.format === "image" && imageUri ? ( = ({ )}
); - }) - )} + }); + })} ); }; From 559b2255674443c21299d4415a667d683f8025c6 Mon Sep 17 00:00:00 2001 From: Jonathan Irhodia Date: Wed, 10 Dec 2025 09:39:50 +0100 Subject: [PATCH 3/4] feat: Add support for image links (#716) --- .../frontend/src/api/utils/customDataUtils.tsx | 14 ++++++++++++++ .../src/components/image/ImageModule.tsx | 3 +++ .../src/components/image/ImageWidget.tsx | 15 ++++++++++++++- .../containers/image/OneColImageContainer.tsx | 17 +++++------------ .../containers/image/TwoColImageContainer.tsx | 17 +++++------------ 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/api/utils/customDataUtils.tsx b/packages/frontend/src/api/utils/customDataUtils.tsx index 319d60979..3a43cf39e 100644 --- a/packages/frontend/src/api/utils/customDataUtils.tsx +++ b/packages/frontend/src/api/utils/customDataUtils.tsx @@ -741,3 +741,17 @@ export const customDataAsCardData: ( return undefined; } }; + +export const validateImageCustomData = ( + customData: TRemoteCustomData | undefined +): { imageUrl: string | undefined; imageLink: string | undefined } => { + let imageUrl = customData?.[0]?.image_url; + let imageLink = customData?.[0]?.image_link; + if (typeof imageUrl !== "string") { + imageUrl = undefined; + } + if (typeof imageLink !== "string") { + imageLink = undefined; + } + return { imageUrl, imageLink }; +}; diff --git a/packages/frontend/src/components/image/ImageModule.tsx b/packages/frontend/src/components/image/ImageModule.tsx index e81958b25..7d10cb2e3 100644 --- a/packages/frontend/src/components/image/ImageModule.tsx +++ b/packages/frontend/src/components/image/ImageModule.tsx @@ -4,6 +4,7 @@ import ImageWidget from "./ImageWidget"; interface IImageModule { imageUrl: string | undefined; + imageLink?: string; title: string; contentHeight?: string; isLoading: boolean; @@ -14,6 +15,7 @@ interface IImageModule { export const ImageModule: FC = ({ imageUrl, + imageLink, title, contentHeight, isLoading, @@ -36,6 +38,7 @@ export const ImageModule: FC = ({ > void; @@ -13,6 +14,7 @@ interface IImageWidget { const ImageWidget: FC = memo(function ImageWidget({ title, imageUrl, + imageLink, isLoading, showImage, onAspectRatioDetected, @@ -55,7 +57,18 @@ const ImageWidget: FC = memo(function ImageWidget({ } return ( -
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions +
{ + if (imageLink) { + window.open(imageLink, "_blank"); + } + }} + style={{ cursor: imageLink ? "pointer" : "default" }} + role="banner" + > {imageLoading && } { - let imageUrl = customData?.[0]?.image_url; - if (typeof imageUrl !== "string") { - imageUrl = undefined; - } - return { imageUrl }; -}; - const OneColImageContainer: FC = ({ moduleData }) => { - const { imageUrl } = validateCustomData(moduleData.widget.custom_data); + const { imageUrl, imageLink } = validateImageCustomData( + moduleData.widget.custom_data + ); const contentHeight = useMemo(() => { return `${CONFIG.WIDGETS.ONE_COL_IMAGE.WIDGET_HEIGHT || 600}px`; @@ -26,6 +18,7 @@ const OneColImageContainer: FC = ({ moduleData }) => { }> { - let imageUrl = customData?.[0]?.image_url; - if (typeof imageUrl !== "string") { - imageUrl = undefined; - } - return { imageUrl }; -}; - const TwoColImageContainer: FC = ({ moduleData, onAspectRatioDetected, @@ -32,7 +22,9 @@ const TwoColImageContainer: FC = ({ number | null >(null); - const { imageUrl } = validateCustomData(moduleData.widget.custom_data); + const { imageUrl, imageLink } = validateImageCustomData( + moduleData.widget.custom_data + ); const previousImageUrl = useRef(imageUrl); if (previousImageUrl.current !== imageUrl) { @@ -75,6 +67,7 @@ const TwoColImageContainer: FC = ({ }> Date: Wed, 10 Dec 2025 10:30:08 +0100 Subject: [PATCH 4/4] Merge pull request #718 from AlphadayHQ/chore/version-bump-v3.8.4 chore: Version bump v3.8.4 --- packages/frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 6fc4caf98..4ea7e9ff5 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -1,7 +1,7 @@ { "name": "@alphaday/frontend", "private": true, - "version": "3.8.3", + "version": "3.8.4", "type": "module", "scripts": { "prepare": "export VITE_COMMIT=$(git rev-parse --short HEAD)",