diff --git a/app/client/src/widgets/TableWidgetV2/widget/__tests__/derived.test/htmlColumns.test.js b/app/client/src/widgets/TableWidgetV2/widget/__tests__/derived.test/htmlColumns.test.js index 7e40f9055e53..bb5a2d551f63 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/__tests__/derived.test/htmlColumns.test.js +++ b/app/client/src/widgets/TableWidgetV2/widget/__tests__/derived.test/htmlColumns.test.js @@ -183,6 +183,46 @@ describe("HTML columns", () => { delete input.searchText; }); + it("validate search works when a javascript object is sent in HTMLcolumn", () => { + const jsObjectInput = _.cloneDeep(input); + + jsObjectInput.processedTableData[0].status = { + color: "yellow", + text: "Adventure", + }; + jsObjectInput.searchText = "Adventure"; + const expected = [ + { + id: 1, + name: "Jim Doe", + status: { + color: "yellow", + text: "Adventure", + }, + __originalIndex__: 0, + }, + ]; + + let result = getFilteredTableData(jsObjectInput, moment, _); + + expect(result).toStrictEqual(expected); + }); + + it("validate search does not filter based on html attributes", () => { + input.searchText = "span"; + const expected = []; + + let result = getFilteredTableData(input, moment, _); + + expect(result).toStrictEqual(expected); + + input.searchText = "color"; + result = getFilteredTableData(input, moment, _); + + expect(result).toStrictEqual(expected); + delete input.searchText; + }); + it("validates filters on table for HTML columns", () => { input.filters = [ { diff --git a/app/client/src/widgets/TableWidgetV2/widget/derived.js b/app/client/src/widgets/TableWidgetV2/widget/derived.js index 14885dbed5fe..b4a318dba384 100644 --- a/app/client/src/widgets/TableWidgetV2/widget/derived.js +++ b/app/client/src/widgets/TableWidgetV2/widget/derived.js @@ -284,13 +284,46 @@ export default { const getTextFromHTML = (html) => { if (!html) return ""; - const tempDiv = document.createElement("div"); + if (typeof html === "object") { + html = JSON.stringify(html); + } + + try { + const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; + tempDiv.innerHTML = html; - return tempDiv.textContent || tempDiv.innerText || ""; + return tempDiv.textContent || tempDiv.innerText || ""; + } catch (e) { + return ""; + } }; + /** + * Since getTextFromHTML is an expensive operation, we need to avoid calling it unnecessarily + * This optimization ensures that getTextFromHTML is only called when required + */ + const columnsWithHTML = Object.values(props.primaryColumns).filter( + (column) => column.columnType === "html", + ); + const htmlColumnAliases = new Set( + columnsWithHTML.map((column) => column.alias), + ); + + const isFilteringByColumnThatHasHTML = props.filters?.some((filter) => + htmlColumnAliases.has(filter.column), + ); + const isSortingByColumnThatHasHTML = + props.sortOrder?.column && htmlColumnAliases.has(props.sortOrder.column); + + const shouldExtractHTMLText = !!( + props.searchText || + isFilteringByColumnThatHasHTML || + isSortingByColumnThatHasHTML + ); + const getKeyForExtractedTextFromHTML = (columnAlias) => + `__htmlExtractedText_${columnAlias}__`; + /* extend processedTableData with values from * - computedValues, in case of normal column * - empty values, in case of derived column @@ -325,6 +358,12 @@ export default { ...processedTableData[index], [column.alias]: computedValue, }; + + if (shouldExtractHTMLText && column.columnType === "html") { + processedTableData[index][ + getKeyForExtractedTextFromHTML(column.alias) + ] = getTextFromHTML(computedValue); + } }); }); } @@ -514,11 +553,23 @@ export default { ); } } - case "html": + case "html": { + const htmlExtractedTextA = + processedA[ + getKeyForExtractedTextFromHTML(sortByColumnOriginalId) + ]; + const htmlExtractedTextB = + processedB[ + getKeyForExtractedTextFromHTML(sortByColumnOriginalId) + ]; + return sortByOrder( - getTextFromHTML(processedA[sortByColumnOriginalId]) > - getTextFromHTML(processedB[sortByColumnOriginalId]), + (htmlExtractedTextA ?? + getTextFromHTML(processedA[sortByColumnOriginalId])) > + (htmlExtractedTextB ?? + getTextFromHTML(processedB[sortByColumnOriginalId])), ); + } default: return sortByOrder( processedA[sortByColumnOriginalId].toString().toLowerCase() > @@ -715,10 +766,6 @@ export default { (column) => column.columnType === "url" && column.displayText, ); - const columnsWithHTML = Object.values(props.primaryColumns).filter( - (column) => column.columnType === "html", - ); - /* * For select columns with label and values, we need to include the label value * in the search and filter data @@ -814,17 +861,23 @@ export default { return acc; }, {}); + let htmlValues = {}; + /* * We don't want html tags and inline styles to match in search */ - const htmlValues = columnsWithHTML.reduce((acc, column) => { - const value = row[column.alias]; + if (shouldExtractHTMLText) { + htmlValues = columnsWithHTML.reduce((acc, column) => { + const value = row[column.alias]; - acc[column.alias] = - value === null || value === undefined ? "" : getTextFromHTML(value); + acc[column.alias] = _.isNil(value) + ? "" + : row[getKeyForExtractedTextFromHTML(column.alias)] ?? + getTextFromHTML(value); - return acc; - }, {}); + return acc; + }, {}); + } const displayedRow = { ...row, @@ -832,13 +885,12 @@ export default { ...displayTextValues, ...htmlValues, }; - const htmlColumns = columnsWithHTML.map((column) => column.alias); if (searchKey) { const combinedRowContent = [ ...Object.values(_.omit(displayedRow, hiddenColumns)), ...Object.values( - _.omit(originalRow, [...hiddenColumns, ...htmlColumns]), + _.omit(originalRow, [...hiddenColumns, ...htmlColumnAliases]), ), ] .join(", ") @@ -875,12 +927,16 @@ export default { /* * We don't want html tags and inline styles to match in filter conditions */ - const isHTMLColumn = htmlColumns.includes(props.filters[i].column); + const isHTMLColumn = htmlColumnAliases.has(props.filters[i].column); const originalColValue = isHTMLColumn - ? getTextFromHTML(originalRow[props.filters[i].column]) + ? originalRow[ + getKeyForExtractedTextFromHTML(props.filters[i].column) + ] ?? getTextFromHTML(originalRow[props.filters[i].column]) : originalRow[props.filters[i].column]; const displayedColValue = isHTMLColumn - ? getTextFromHTML(displayedRow[props.filters[i].column]) + ? displayedRow[ + getKeyForExtractedTextFromHTML(props.filters[i].column) + ] ?? getTextFromHTML(displayedRow[props.filters[i].column]) : displayedRow[props.filters[i].column]; filterResult =