feat: optimize HTML text extraction in TableWidgetV2 (#38153)

This commit is contained in:
Rahul Barwal 2024-12-23 15:49:46 +05:30 committed by GitHub
parent d52490cabb
commit dcb27d2c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 117 additions and 21 deletions

View File

@ -183,6 +183,46 @@ describe("HTML columns", () => {
delete input.searchText; 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", () => { it("validates filters on table for HTML columns", () => {
input.filters = [ input.filters = [
{ {

View File

@ -284,13 +284,46 @@ export default {
const getTextFromHTML = (html) => { const getTextFromHTML = (html) => {
if (!html) return ""; if (!html) return "";
const tempDiv = document.createElement("div"); if (typeof html === "object") {
html = JSON.stringify(html);
}
tempDiv.innerHTML = html; try {
const tempDiv = document.createElement("div");
return tempDiv.textContent || tempDiv.innerText || ""; tempDiv.innerHTML = html;
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 /* extend processedTableData with values from
* - computedValues, in case of normal column * - computedValues, in case of normal column
* - empty values, in case of derived column * - empty values, in case of derived column
@ -325,6 +358,12 @@ export default {
...processedTableData[index], ...processedTableData[index],
[column.alias]: computedValue, [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( return sortByOrder(
getTextFromHTML(processedA[sortByColumnOriginalId]) > (htmlExtractedTextA ??
getTextFromHTML(processedB[sortByColumnOriginalId]), getTextFromHTML(processedA[sortByColumnOriginalId])) >
(htmlExtractedTextB ??
getTextFromHTML(processedB[sortByColumnOriginalId])),
); );
}
default: default:
return sortByOrder( return sortByOrder(
processedA[sortByColumnOriginalId].toString().toLowerCase() > processedA[sortByColumnOriginalId].toString().toLowerCase() >
@ -715,10 +766,6 @@ export default {
(column) => column.columnType === "url" && column.displayText, (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 * For select columns with label and values, we need to include the label value
* in the search and filter data * in the search and filter data
@ -814,17 +861,23 @@ export default {
return acc; return acc;
}, {}); }, {});
let htmlValues = {};
/* /*
* We don't want html tags and inline styles to match in search * We don't want html tags and inline styles to match in search
*/ */
const htmlValues = columnsWithHTML.reduce((acc, column) => { if (shouldExtractHTMLText) {
const value = row[column.alias]; htmlValues = columnsWithHTML.reduce((acc, column) => {
const value = row[column.alias];
acc[column.alias] = acc[column.alias] = _.isNil(value)
value === null || value === undefined ? "" : getTextFromHTML(value); ? ""
: row[getKeyForExtractedTextFromHTML(column.alias)] ??
getTextFromHTML(value);
return acc; return acc;
}, {}); }, {});
}
const displayedRow = { const displayedRow = {
...row, ...row,
@ -832,13 +885,12 @@ export default {
...displayTextValues, ...displayTextValues,
...htmlValues, ...htmlValues,
}; };
const htmlColumns = columnsWithHTML.map((column) => column.alias);
if (searchKey) { if (searchKey) {
const combinedRowContent = [ const combinedRowContent = [
...Object.values(_.omit(displayedRow, hiddenColumns)), ...Object.values(_.omit(displayedRow, hiddenColumns)),
...Object.values( ...Object.values(
_.omit(originalRow, [...hiddenColumns, ...htmlColumns]), _.omit(originalRow, [...hiddenColumns, ...htmlColumnAliases]),
), ),
] ]
.join(", ") .join(", ")
@ -875,12 +927,16 @@ export default {
/* /*
* We don't want html tags and inline styles to match in filter conditions * 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 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]; : originalRow[props.filters[i].column];
const displayedColValue = isHTMLColumn 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]; : displayedRow[props.filters[i].column];
filterResult = filterResult =