import React, { useCallback, useRef } from 'react'; import { Platform } from 'react-native'; import ReactNativeStraightTableViewManager from '../specs/SimpleGridNativeComponent'; import { useSetAtom } from 'jotai'; import { setDragBoxAtom, setExpandedCellAtom, setSearchingTableColumnAtom, } from '../atoms'; import CarbonTheme from '@qlik/react-native-carbon/src/core/CarbonTheme'; import type { ColumnHeader, Cols, DragBoxEvent, EndReachedEvent, ExpandCellEvent, GrandTotalCell, HeaderPressedEvent, Layout, SearchColumnEvent, SelectionsChangedEvent, SimpleGridProps, TableData, Totals, } from './SimpleGrid.types'; export type { SimpleGridLayout, SimpleGridProps } from './SimpleGrid.types'; const clampColorChannel = (value: number) => Math.max(0, Math.min(255, Math.round(value))); const toHexChannel = (value: number) => clampColorChannel(value).toString(16).padStart(2, '0').toUpperCase(); const normalizeColor = (value?: string | null) => { if (!value || typeof value !== 'string') { return undefined; } try { const trimmedValue = value.trim(); const rgbaMatch = trimmedValue.match( /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i ); if (!rgbaMatch) { return trimmedValue; } const [, red, green, blue, alpha] = rgbaMatch; const hexRed = toHexChannel(Number(red)); const hexGreen = toHexChannel(Number(green)); const hexBlue = toHexChannel(Number(blue)); if (alpha === undefined) { return `#${hexRed}${hexGreen}${hexBlue}`; } const normalizedAlpha = Number(alpha); const alphaValue = Number.isFinite(normalizedAlpha) ? Math.max(0, Math.min(1, normalizedAlpha)) * 255 : 255; const hexAlpha = toHexChannel(alphaValue); return `#${hexAlpha}${hexRed}${hexGreen}${hexBlue}`; } catch (error) { // Fallback if color parsing fails return undefined; } }; const resolveLayoutBackgroundColor = (layout: Layout) => { try { const generalComponent = layout.components?.find( component => component.key === 'general' ); const bgColor = generalComponent?.bgColor; if (!bgColor) { return undefined; } if (bgColor.useColorExpression && bgColor.colorExpression) { const colorExpression = String(bgColor.colorExpression); return normalizeColor(colorExpression); } const color = String(bgColor.color); return normalizeColor(color); } catch (error) { return undefined; } }; const transformTotals = ( layout: Layout, table: TableData ): Totals | undefined => { let totals; let rowIndex = 0; let values = table?.columns?.map((col: ColumnHeader, index: number) => { if (col.isDim && index === 0) { return layout.totals.label || 'Totals'; } if (!col.isDim && rowIndex < layout.qHyperCube.qGrandTotalRow.length) { return layout.qHyperCube.qGrandTotalRow[rowIndex++].qText; } return ''; }); const rawTotalsPosition = table?.totalsPosition; const totalsPosition = rawTotalsPosition === 'top' || rawTotalsPosition === 'bottom' ? rawTotalsPosition : 'noTotals'; const hasGrandTotals = (layout?.qHyperCube?.qGrandTotalRow?.length ?? 0) > 0; const shouldBuildTotals = hasGrandTotals && totalsPosition !== 'noTotals'; const show = shouldBuildTotals; if (shouldBuildTotals) { totals = { label: layout.totals.label, position: totalsPosition, rows: layout.qHyperCube.qGrandTotalRow as ReadonlyArray, show, values: values as ReadonlyArray, }; } if (layout.qHyperCube?.qGrandTotalRow?.length === 0) { totals = undefined; } return totals; }; const SimpleGrid: React.FC = ({ contentStyle, clearSelections, layout, model, name, rect, style, tableData, theme, themeData, translations, onConfirmSelections, onEndReached, onHeaderPressed, onSelectionsChanged, }) => { const draggingBox = useSetAtom(setDragBoxAtom); const expandCell = useSetAtom(setExpandedCellAtom); const searchColumn = useSetAtom(setSearchingTableColumnAtom); const carbonTheme = useRef( new CarbonTheme({ theme: themeData }) ); const signalSearch = useCallback( async (column: ColumnHeader) => { try { if (column.dataColIdx === undefined) return; const props = await model.getEffectiveProperties(); if ( props?.qHyperCubeDef?.qDimensions[column.dataColIdx].qDef .qFieldDefs?.[0] ) { column.qCardinal = layout.qHyperCube.qDimensionInfo[column.dataColIdx].qCardinal; column.label = props?.qHyperCubeDef?.qDimensions[ column.dataColIdx ].qDef.qFieldDefs?.[0]; const fieldLabel = props?.qHyperCubeDef?.qDimensions[column.dataColIdx].qDef .qFieldLabels?.[0]; column.display = !fieldLabel || fieldLabel.length === 0 ? column.label : fieldLabel; const qFallbackTitle = layout?.qHyperCube?.qDimensionInfo[column.dataColIdx] ?.qFallbackTitle; if (qFallbackTitle && qFallbackTitle.length > 0) { column.display = qFallbackTitle; } } else { const qDimInfo = layout.qHyperCube.qDimensionInfo[column.dataColIdx]; const groupFieldDef = qDimInfo.qGroupFieldDefs?.[0]; if (groupFieldDef && groupFieldDef.length > 0) { column.label = groupFieldDef; } else { column.label = qDimInfo.qFallbackTitle; } column.display = qDimInfo.qFallbackTitle; } searchColumn({ searching: true, column }); } catch (error) { } }, [model, searchColumn, layout] ); // Unique key ensures React treats different tables as different component instances // This resets all React-side state (refs, callbacks, etc.) when switching tables const tableId = layout?.qInfo?.qId || 'default'; const rawLiveTotalsPosition = tableData?.totalsPosition; const liveTotalsPosition = rawLiveTotalsPosition === 'top' || rawLiveTotalsPosition === 'bottom' ? rawLiveTotalsPosition : 'noTotals'; const tableKey = `${tableId}-${liveTotalsPosition}`; const totals = transformTotals(layout, tableData); const cols: Cols = { header: tableData?.columns, footer: totals ? layout.qHyperCube.qGrandTotalRow : undefined, totals, }; const rows = { rowsJSON: JSON.stringify(tableData?.rows || []), reset: tableData?.reset, }; const resolvedLayoutBackgroundColor = resolveLayoutBackgroundColor(layout); // Helper to safely extract string values from theme const safeGetThemeColor = (path: string, defaultValue: string | null) => { try { const value = carbonTheme?.current?.getValue(path, defaultValue); return typeof value === 'string' ? value : defaultValue; } catch (error) { return defaultValue; } }; const themeProps = { ...theme, headerBackgroundColor: normalizeColor( safeGetThemeColor('object.straightTable.header.backgroundColor', '#F0F0F0') ), backgroundColor: resolvedLayoutBackgroundColor ?? 'white', even: normalizeColor( safeGetThemeColor('object.straightTable.mobile.rows.even.backgroundColor', null) ) ?? undefined, }; const handleConfirmSelections = useCallback(() => { try { onConfirmSelections?.(); } catch (error) { } }, [onConfirmSelections]); const handleEndReached = useCallback( (event: EndReachedEvent) => { try { onEndReached?.(event); } catch (error) { } }, [onEndReached] ); const handleHeaderPressed = useCallback( (event: HeaderPressedEvent) => { try { onHeaderPressed?.(event); } catch (error) { } }, [onHeaderPressed] ); const handleSearchColumn = useCallback( (event: SearchColumnEvent) => { try { const column = JSON.parse(event.nativeEvent.column); signalSearch(column); } catch (error) { } }, [signalSearch] ); const handleSelectionsChanged = useCallback( (event: SelectionsChangedEvent) => { try { if (Platform.OS === 'ios') { const selections = JSON.parse(event.nativeEvent.selections); // Call the consumer's callback with parsed selections in nativeEvent if (onSelectionsChanged) { onSelectionsChanged({ nativeEvent: { selections } }); } return; } onSelectionsChanged(event); } catch (error) { } }, [onSelectionsChanged] ); const onDragBox = useCallback( (event: DragBoxEvent) => { try { draggingBox({ dragging: event.nativeEvent.dragging }); } catch (error) { } }, [draggingBox] ); const onExpandCell = useCallback( (event: ExpandCellEvent) => { try { const row = JSON.parse(event.nativeEvent.row); const col = JSON.parse(event.nativeEvent.col); expandCell({ expand: true, data: { row, col }, titles: translations.headerValues, viewData: layout?.isDataView, }); } catch (error) { } }, [expandCell, layout?.isDataView, translations.headerValues] ); return ( ); }; export default SimpleGrid;