Unclosed tag" });
+ const htmlCell = screen.getByTestId("t--table-widget-v2-html-cell");
+
+ expect(htmlCell.querySelector("div")).toHaveTextContent("Unclosed tag");
+ });
+ });
+});
diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.test.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/__tests__/PlainTextCell.test.tsx
similarity index 97%
rename from app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.test.tsx
rename to app/client/src/widgets/TableWidgetV2/component/cellComponents/__tests__/PlainTextCell.test.tsx
index da956d8e1ce2..484bd4152fd8 100644
--- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.test.tsx
+++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/__tests__/PlainTextCell.test.tsx
@@ -1,4 +1,4 @@
-import { getCellText } from "./PlainTextCell";
+import { getCellText } from "../PlainTextCell";
import { ColumnTypes } from "widgets/TableWidgetV2/constants";
describe("DefaultRendere - ", () => {
diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.test.ts b/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.test.ts
index b691ac718ba1..deea6b9ca911 100644
--- a/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.test.ts
+++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.test.ts
@@ -90,4 +90,28 @@ describe("TransformTableDataIntoArrayOfArray", () => {
JSON.stringify(expectedCsvData),
);
});
+
+ it("work as expected with html", () => {
+ const data = [
+ {
+ id: "abc
",
+ },
+ {
+ id: "
",
+ },
+ ];
+ const csvData = transformTableDataIntoCsv({
+ columns,
+ data,
+ });
+ const expectedCsvData = [
+ ["Id"],
+ ["
abc
"],
+ ["
"],
+ ];
+
+ expect(JSON.stringify(csvData)).toStrictEqual(
+ JSON.stringify(expectedCsvData),
+ );
+ });
});
diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.ts b/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.ts
index 5cec938a6c76..3922972ed4c3 100644
--- a/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.ts
+++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/Utilities.ts
@@ -1,3 +1,4 @@
+import { ColumnTypes } from "widgets/TableWidgetV2/constants";
import type { TableColumnProps } from "../../Constants";
import { isString } from "lodash";
@@ -35,7 +36,12 @@ export const transformTableDataIntoCsv = (props: {
? value.replace("\n", " ")
: value;
- if (isString(value) && value.includes(",")) {
+ // HTML columns output multi line strings. We need to quote them to avoid CSV parsing issues.
+ const shouldQuote =
+ (isString(value) && value.includes(",")) ||
+ column.metaProperties.type === ColumnTypes.HTML;
+
+ if (shouldQuote) {
csvDataRow.push(`"${value}"`);
} else {
csvDataRow.push(value);
diff --git a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx
index bf6a82b5fb1f..483916108169 100644
--- a/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx
+++ b/app/client/src/widgets/TableWidgetV2/component/header/actions/filter/CascadeFields.tsx
@@ -180,6 +180,12 @@ const typeOperatorsMap: Record
= {
{ label: "empty", value: "empty", type: "" },
{ label: "not empty", value: "notEmpty", type: "" },
],
+ [ColumnTypes.HTML]: [
+ { label: "contains", value: "contains", type: "input" },
+ { label: "does not contain", value: "doesNotContain", type: "input" },
+ { label: "empty", value: "empty", type: "" },
+ { label: "not empty", value: "notEmpty", type: "" },
+ ],
};
const operatorOptions: DropdownOption[] = [
@@ -197,6 +203,7 @@ const columnTypeNameMap: Record = {
[ReadOnlyColumnTypes.CHECKBOX]: "Check",
[ReadOnlyColumnTypes.SWITCH]: "Check",
[ReadOnlyColumnTypes.SELECT]: "Text",
+ [ReadOnlyColumnTypes.HTML]: "HTML",
};
function RenderOption(props: { type: string; title: string; active: boolean }) {
diff --git a/app/client/src/widgets/TableWidgetV2/constants.ts b/app/client/src/widgets/TableWidgetV2/constants.ts
index c88992218216..717cda817ab6 100644
--- a/app/client/src/widgets/TableWidgetV2/constants.ts
+++ b/app/client/src/widgets/TableWidgetV2/constants.ts
@@ -146,6 +146,7 @@ export enum ColumnTypes {
CHECKBOX = "checkbox",
SWITCH = "switch",
CURRENCY = "currency",
+ HTML = "html",
}
export enum ReadOnlyColumnTypes {
@@ -158,6 +159,7 @@ export enum ReadOnlyColumnTypes {
CHECKBOX = "checkbox",
SWITCH = "switch",
SELECT = "select",
+ HTML = "html",
}
export const ActionColumnTypes = [
@@ -165,6 +167,7 @@ export const ActionColumnTypes = [
ColumnTypes.ICON_BUTTON,
ColumnTypes.MENU_BUTTON,
ColumnTypes.EDIT_ACTIONS,
+ ColumnTypes.HTML,
];
export const FilterableColumnTypes = [
@@ -175,6 +178,7 @@ export const FilterableColumnTypes = [
ColumnTypes.SELECT,
ColumnTypes.CHECKBOX,
ColumnTypes.SWITCH,
+ ColumnTypes.HTML,
];
export const DEFAULT_BUTTON_COLOR = "rgb(3, 179, 101)";
@@ -242,3 +246,6 @@ export const ALLOW_TABLE_WIDGET_SERVER_SIDE_FILTERING =
export const CUSTOM_LOADING_STATE_ENABLED =
FEATURE_FLAG["release_table_custom_loading_state_enabled"];
+
+export const HTML_COLUMN_TYPE_ENABLED =
+ FEATURE_FLAG["release_table_html_column_type_enabled"];
diff --git a/app/client/src/widgets/TableWidgetV2/widget/derived.js b/app/client/src/widgets/TableWidgetV2/widget/derived.js
index c62e58ac6f9f..14885dbed5fe 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/derived.js
+++ b/app/client/src/widgets/TableWidgetV2/widget/derived.js
@@ -281,6 +281,16 @@ export default {
return [];
}
+ const getTextFromHTML = (html) => {
+ if (!html) return "";
+
+ const tempDiv = document.createElement("div");
+
+ tempDiv.innerHTML = html;
+
+ return tempDiv.textContent || tempDiv.innerText || "";
+ };
+
/* extend processedTableData with values from
* - computedValues, in case of normal column
* - empty values, in case of derived column
@@ -504,6 +514,11 @@ export default {
);
}
}
+ case "html":
+ return sortByOrder(
+ getTextFromHTML(processedA[sortByColumnOriginalId]) >
+ getTextFromHTML(processedB[sortByColumnOriginalId]),
+ );
default:
return sortByOrder(
processedA[sortByColumnOriginalId].toString().toLowerCase() >
@@ -700,6 +715,10 @@ 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
@@ -781,32 +800,51 @@ export default {
});
}
- const displayedRow = {
- ...row,
- ...labelValuesForSelectCell,
- ...columnWithDisplayText.reduce((acc, column) => {
- let displayText;
+ const displayTextValues = columnWithDisplayText.reduce((acc, column) => {
+ let displayText;
- if (_.isArray(column.displayText)) {
- displayText = column.displayText[row.__originalIndex__];
- } else {
- displayText = column.displayText;
- }
+ if (_.isArray(column.displayText)) {
+ displayText = column.displayText[row.__originalIndex__];
+ } else {
+ displayText = column.displayText;
+ }
+
+ acc[column.alias] = displayText;
+
+ return acc;
+ }, {});
+
+ /*
+ * We don't want html tags and inline styles to match in search
+ */
+ const htmlValues = columnsWithHTML.reduce((acc, column) => {
+ const value = row[column.alias];
- acc[column.alias] = displayText;
+ acc[column.alias] =
+ value === null || value === undefined ? "" : getTextFromHTML(value);
- return acc;
- }, {}),
+ return acc;
+ }, {});
+
+ const displayedRow = {
+ ...row,
+ ...labelValuesForSelectCell,
+ ...displayTextValues,
+ ...htmlValues,
};
+ const htmlColumns = columnsWithHTML.map((column) => column.alias);
if (searchKey) {
- isSearchKeyFound = [
+ const combinedRowContent = [
...Object.values(_.omit(displayedRow, hiddenColumns)),
- ...Object.values(_.omit(originalRow, hiddenColumns)),
+ ...Object.values(
+ _.omit(originalRow, [...hiddenColumns, ...htmlColumns]),
+ ),
]
.join(", ")
- .toLowerCase()
- .includes(searchKey);
+ .toLowerCase();
+
+ isSearchKeyFound = combinedRowContent.includes(searchKey);
}
if (!isSearchKeyFound) {
@@ -834,15 +872,20 @@ export default {
ConditionFunctions[props.filters[i].condition];
if (conditionFunction) {
+ /*
+ * We don't want html tags and inline styles to match in filter conditions
+ */
+ const isHTMLColumn = htmlColumns.includes(props.filters[i].column);
+ const originalColValue = isHTMLColumn
+ ? getTextFromHTML(originalRow[props.filters[i].column])
+ : originalRow[props.filters[i].column];
+ const displayedColValue = isHTMLColumn
+ ? getTextFromHTML(displayedRow[props.filters[i].column])
+ : displayedRow[props.filters[i].column];
+
filterResult =
- conditionFunction(
- originalRow[props.filters[i].column],
- props.filters[i].value,
- ) ||
- conditionFunction(
- displayedRow[props.filters[i].column],
- props.filters[i].value,
- );
+ conditionFunction(originalColValue, props.filters[i].value) ||
+ conditionFunction(displayedColValue, props.filters[i].value);
}
} catch (e) {
filterResult = false;
diff --git a/app/client/src/widgets/TableWidgetV2/widget/derived.test.js b/app/client/src/widgets/TableWidgetV2/widget/derived.test.js
index e12cb35ff306..8b306b09207d 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/derived.test.js
+++ b/app/client/src/widgets/TableWidgetV2/widget/derived.test.js
@@ -1646,6 +1646,582 @@ describe("Validates getFilteredTableData Properties", () => {
expect(result).toStrictEqual(expected);
});
+
+ describe("HTML columns", () => {
+ const input = {
+ tableData: [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ },
+ ],
+ processedTableData: [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ __originalIndex__: 0,
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ __originalIndex__: 1,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ __originalIndex__: 2,
+ },
+ ],
+ sortOrder: { column: "id", order: "asc" },
+ columnOrder: ["id", "name", "status"],
+ primaryColumns: {
+ id: {
+ index: 1,
+ width: 150,
+ id: "id",
+ alias: "id",
+ originalId: "id",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "number",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "id",
+ isAscOrder: false,
+ },
+ name: {
+ index: 0,
+ width: 150,
+ id: "name",
+ alias: "name",
+ originalId: "name",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "awesome",
+ isAscOrder: undefined,
+ },
+ status: {
+ index: 0,
+ width: 150,
+ id: "status",
+ alias: "status",
+ originalId: "status",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "html",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "Status",
+ isAscOrder: undefined,
+ },
+ },
+ tableColumns: [
+ {
+ index: 0,
+ width: 150,
+ id: "name",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "text",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "awesome",
+ isAscOrder: undefined,
+ },
+ {
+ index: 1,
+ width: 150,
+ id: "id",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "number",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "id",
+ isAscOrder: false,
+ },
+ {
+ index: 0,
+ width: 150,
+ id: "status",
+ alias: "status",
+ originalId: "status",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "html",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "Status",
+ isAscOrder: undefined,
+ },
+ ],
+ };
+
+ input.orderedTableColumns = Object.values(input.primaryColumns).sort(
+ (a, b) => {
+ return input.columnOrder[a.id] < input.columnOrder[b.id];
+ },
+ );
+ const { getFilteredTableData } = derivedProperty;
+
+ it("validate search on table for HTML columns", () => {
+ input.searchText = "Pending";
+ const expected = [
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ __originalIndex__: 1,
+ },
+ ];
+
+ let result = getFilteredTableData(input, moment, _);
+
+ expect(result).toStrictEqual(expected);
+ delete input.searchText;
+ });
+
+ it("validates filters on table for HTML columns", () => {
+ input.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "Active",
+ },
+ ];
+ const expected = [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ __originalIndex__: 0,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ __originalIndex__: 2,
+ },
+ ];
+
+ let result = getFilteredTableData(input, moment, _);
+
+ expect(result).toStrictEqual(expected);
+ delete input.filters;
+ });
+
+ it("validates sort on table for HTML columns", () => {
+ input.sortOrder = { column: "status", order: "desc" };
+ let expected = [
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ __originalIndex__: 1,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ __originalIndex__: 0,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ __originalIndex__: 2,
+ },
+ ];
+
+ let result = getFilteredTableData(input, moment, _);
+
+ expect(result).toStrictEqual(expected);
+
+ input.sortOrder = { column: "status", order: "asc" };
+ expected = [
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ __originalIndex__: 2,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ __originalIndex__: 0,
+ },
+
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ __originalIndex__: 1,
+ },
+ ];
+
+ result = getFilteredTableData(input, moment, _);
+ expect(result).toStrictEqual(expected);
+ });
+
+ it("validates tags are not filterable in html content", () => {
+ input.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "span",
+ },
+ ];
+ const expected = [];
+
+ let result = getFilteredTableData(input, moment, _);
+
+ expect(result).toStrictEqual(expected);
+
+ input.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "color",
+ },
+ ];
+ result = getFilteredTableData(input, moment, _);
+ expect(result).toStrictEqual(expected);
+ delete input.filters;
+ });
+ it("validates tags are not searchable in html content", () => {
+ 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 multiple HTML column filters with AND condition", () => {
+ const multiFilterInput = _.cloneDeep(input);
+
+ multiFilterInput.processedTableData = [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ role: "Admin
",
+ __originalIndex__: 0,
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending",
+ role: "User
",
+ __originalIndex__: 1,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ role: "Admin
",
+ __originalIndex__: 2,
+ },
+ ];
+
+ multiFilterInput.primaryColumns.role = {
+ index: 3,
+ width: 150,
+ id: "role",
+ alias: "role",
+ originalId: "role",
+ horizontalAlignment: "LEFT",
+ verticalAlignment: "CENTER",
+ columnType: "html",
+ textColor: "#231F20",
+ textSize: "PARAGRAPH",
+ fontStyle: "REGULAR",
+ enableFilter: true,
+ enableSort: true,
+ isVisible: true,
+ isDerived: false,
+ label: "Role",
+ isAscOrder: undefined,
+ };
+
+ multiFilterInput.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "Active",
+ },
+ {
+ condition: "contains",
+ column: "role",
+ value: "Admin",
+ operator: "AND",
+ },
+ ];
+
+ const expected = [
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ role: "Admin
",
+ __originalIndex__: 2,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "Active",
+ role: "Admin
",
+ __originalIndex__: 0,
+ },
+ ];
+
+ let result = getFilteredTableData(multiFilterInput, moment, _);
+
+ expect(result).toStrictEqual(expected);
+ delete input.filters;
+ });
+
+ it("validates complex HTML content with nested elements and attributes", () => {
+ const complexHTMLInput = _.cloneDeep(input);
+
+ complexHTMLInput.processedTableData = [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status:
+ 'Active
',
+ __originalIndex__: 0,
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status:
+ 'Pending
',
+ __originalIndex__: 1,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status:
+ 'Active
',
+ __originalIndex__: 2,
+ },
+ ];
+
+ // Test searching through complex HTML
+ complexHTMLInput.searchText = "Active";
+ let expected = [
+ {
+ id: 3,
+ name: "Elon Musk",
+ status:
+ 'Active
',
+ __originalIndex__: 2,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status:
+ 'Active
',
+ __originalIndex__: 0,
+ },
+ ];
+
+ let result = getFilteredTableData(complexHTMLInput, moment, _);
+
+ expect(result).toStrictEqual(expected);
+ delete complexHTMLInput.searchText;
+
+ // Test sorting with complex HTML
+ complexHTMLInput.sortOrder = { column: "status", order: "desc" };
+ expected = [
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status:
+ 'Pending
',
+ __originalIndex__: 1,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status:
+ 'Active
',
+ __originalIndex__: 0,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status:
+ 'Active
',
+ __originalIndex__: 2,
+ },
+ ];
+
+ result = getFilteredTableData(complexHTMLInput, moment, _);
+ expect(result).toStrictEqual(expected);
+ });
+
+ it("validates HTML columns with special characters and entities", () => {
+ const specialCharHTMLInput = _.cloneDeep(input);
+
+ specialCharHTMLInput.processedTableData = [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "© Active & Ready",
+ __originalIndex__: 0,
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: "Pending > Review",
+ __originalIndex__: 1,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "© Active & Ready",
+ __originalIndex__: 2,
+ },
+ ];
+
+ // Test filtering with HTML entities
+ specialCharHTMLInput.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "Active & Ready",
+ },
+ ];
+
+ const expected = [
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "© Active & Ready",
+ __originalIndex__: 2,
+ },
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: "© Active & Ready",
+ __originalIndex__: 0,
+ },
+ ];
+
+ let result = getFilteredTableData(specialCharHTMLInput, moment, _);
+
+ expect(result).toStrictEqual(expected);
+ delete specialCharHTMLInput.filters;
+ });
+
+ it("validates filtering with null and undefined values in HTML columns", () => {
+ const nullUndefinedInput = _.cloneDeep(input);
+
+ nullUndefinedInput.processedTableData = [
+ {
+ id: 1,
+ name: "Jim Doe",
+ status: null,
+ __originalIndex__: 0,
+ },
+ {
+ id: 2,
+ name: "Usain Bolt",
+ status: undefined,
+ __originalIndex__: 1,
+ },
+ {
+ id: 3,
+ name: "Elon Musk",
+ status: "Active",
+ __originalIndex__: 2,
+ },
+ ];
+
+ // Test filtering for null values
+ nullUndefinedInput.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "null",
+ },
+ ];
+
+ let result = getFilteredTableData(nullUndefinedInput, moment, _);
+
+ expect(result).toStrictEqual([]);
+
+ // Test filtering for undefined values
+ nullUndefinedInput.filters = [
+ {
+ condition: "contains",
+ column: "status",
+ value: "undefined",
+ },
+ ];
+
+ result = getFilteredTableData(nullUndefinedInput, moment, _);
+ expect(result).toStrictEqual([]);
+
+ delete nullUndefinedInput.filters;
+ });
+ });
});
describe("Validate getSelectedRow function", () => {
diff --git a/app/client/src/widgets/TableWidgetV2/widget/index.tsx b/app/client/src/widgets/TableWidgetV2/widget/index.tsx
index 4d8e31b3e57b..586fcd9bc2d3 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/index.tsx
+++ b/app/client/src/widgets/TableWidgetV2/widget/index.tsx
@@ -3,6 +3,7 @@ import log from "loglevel";
import memoizeOne from "memoize-one";
import _, {
+ cloneDeep,
filter,
isArray,
isEmpty,
@@ -58,6 +59,7 @@ import {
DEFAULT_MENU_VARIANT,
defaultEditableCell,
EditableCellActions,
+ HTML_COLUMN_TYPE_ENABLED,
InlineEditingSaveOptions,
ORIGINAL_INDEX_KEY,
PaginationDirection,
@@ -139,6 +141,7 @@ import {
import IconSVG from "../icon.svg";
import ThumbnailSVG from "../thumbnail.svg";
import { klonaRegularWithTelemetry } from "utils/helpers";
+import HTMLCell from "../component/cellComponents/HTMLCell";
const ReactTableComponent = lazy(async () =>
retryPromise(async () => import("../component")),
@@ -911,6 +914,36 @@ class TableWidgetV2 extends BaseWidget {
//dont neet to batch this since single action
this.hydrateStickyColumns();
}
+
+ /**
+ * Why we are doing this?
+ * This is a safety net! Consider this scenario:
+ * 1. HTML column type is enabled.
+ * 2. User creates a table with HTML columns.
+ * 3. HTML column type is disabled. (For any reason)
+ *
+ * In this scenario, we don't want incomplete experience for the user.
+ * Without this safety net, the property pane will not show the HTML as type and the `ColumnType` will be lost(and empty), which is confusing for the user.
+ * With this safety net, we will update the column type to TEXT.
+ * @rahulbarwal Remove this once we remove the feature flag
+ */
+ if (!TableWidgetV2.getFeatureFlag(HTML_COLUMN_TYPE_ENABLED)) {
+ const updatedPrimaryColumns = cloneDeep(this.props.primaryColumns);
+ let hasHTMLColumns = false;
+
+ Object.values(updatedPrimaryColumns).forEach(
+ (column: ColumnProperties) => {
+ if (column.columnType === ColumnTypes.HTML) {
+ column.columnType = ColumnTypes.TEXT;
+ hasHTMLColumns = true;
+ }
+ },
+ );
+
+ if (hasHTMLColumns) {
+ this.updateWidgetProperty("primaryColumns", updatedPrimaryColumns);
+ }
+ }
}
componentDidUpdate(prevProps: TableWidgetProps) {
@@ -2517,6 +2550,25 @@ class TableWidgetV2 extends BaseWidget {
/>
);
+ case ColumnTypes.HTML:
+ return (
+
+ );
+
default:
let validationErrorMessage;
diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/ColumnType.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/ColumnType.ts
new file mode 100644
index 000000000000..ee66fa0aa194
--- /dev/null
+++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/ColumnType.ts
@@ -0,0 +1,137 @@
+import {
+ ColumnTypes,
+ HTML_COLUMN_TYPE_ENABLED,
+ type TableWidgetProps,
+} from "widgets/TableWidgetV2/constants";
+import { composePropertyUpdateHook } from "widgets/WidgetUtils";
+
+import Widget from "../../../index";
+import {
+ showByColumnType,
+ updateCurrencyDefaultValues,
+ updateMenuItemsSource,
+ updateNumberColumnTypeTextAlignment,
+ updateThemeStylesheetsInColumns,
+} from "../../../propertyUtils";
+const ColumnTypeOptions = [
+ {
+ label: "Button",
+ value: ColumnTypes.BUTTON,
+ },
+ {
+ label: "Checkbox",
+ value: ColumnTypes.CHECKBOX,
+ },
+ {
+ label: "Currency",
+ value: ColumnTypes.CURRENCY,
+ },
+ {
+ label: "Date",
+ value: ColumnTypes.DATE,
+ },
+ {
+ label: "Icon button",
+ value: ColumnTypes.ICON_BUTTON,
+ },
+ {
+ label: "Image",
+ value: ColumnTypes.IMAGE,
+ },
+ {
+ label: "Menu button",
+ value: ColumnTypes.MENU_BUTTON,
+ },
+ {
+ label: "Number",
+ value: ColumnTypes.NUMBER,
+ },
+ {
+ label: "Plain text",
+ value: ColumnTypes.TEXT,
+ },
+ {
+ label: "Select",
+ value: ColumnTypes.SELECT,
+ },
+ {
+ label: "Switch",
+ value: ColumnTypes.SWITCH,
+ },
+ {
+ label: "URL",
+ value: ColumnTypes.URL,
+ },
+ {
+ label: "Video",
+ value: ColumnTypes.VIDEO,
+ },
+];
+
+// TODO: @rahulbarwal Remove this once we have a feature flag for this
+// This is a temporary solution to position the HTML column type alphabetically
+const columnTypeWithHtml = [
+ ...ColumnTypeOptions.slice(0, 4),
+ { label: "HTML", value: ColumnTypes.HTML },
+ ...ColumnTypeOptions.slice(4),
+];
+
+export const columnTypeConfig = {
+ propertyName: "columnType",
+ label: "Column type",
+ helpText:
+ "Type of column to be shown corresponding to the data of the column",
+ controlType: "DROP_DOWN",
+ // TODO: Remove this once we have a feature flag for this
+ // Since we want to position the column types alphabetically, right now this is hardcoded
+ options: ColumnTypeOptions,
+ updateHook: composePropertyUpdateHook([
+ updateNumberColumnTypeTextAlignment,
+ updateThemeStylesheetsInColumns,
+ updateMenuItemsSource,
+ updateCurrencyDefaultValues,
+ ]),
+ dependencies: ["primaryColumns", "columnOrder", "childStylesheet"],
+ isBindProperty: false,
+ isTriggerProperty: false,
+ hidden: (props: TableWidgetProps, propertyPath: string) => {
+ const isHTMLColumnTypeEnabled = Widget.getFeatureFlag(
+ HTML_COLUMN_TYPE_ENABLED,
+ );
+
+ return (
+ isHTMLColumnTypeEnabled ||
+ showByColumnType(props, propertyPath, [ColumnTypes.EDIT_ACTIONS])
+ );
+ },
+};
+
+export const columnTypeWithHtmlConfig = {
+ propertyName: "columnType",
+ label: "Column type",
+ helpText:
+ "Type of column to be shown corresponding to the data of the column",
+ controlType: "DROP_DOWN",
+ // TODO: Remove this once we have a feature flag for this
+ // Since we want to position the column types alphabetically, right now this is hardcoded
+ options: columnTypeWithHtml,
+ updateHook: composePropertyUpdateHook([
+ updateNumberColumnTypeTextAlignment,
+ updateThemeStylesheetsInColumns,
+ updateMenuItemsSource,
+ updateCurrencyDefaultValues,
+ ]),
+ dependencies: ["primaryColumns", "columnOrder", "childStylesheet"],
+ isBindProperty: false,
+ isTriggerProperty: false,
+ hidden: (props: TableWidgetProps, propertyPath: string) => {
+ const isHTMLColumnTypeEnabled = Widget.getFeatureFlag(
+ HTML_COLUMN_TYPE_ENABLED,
+ );
+
+ return (
+ !isHTMLColumnTypeEnabled ||
+ showByColumnType(props, propertyPath, [ColumnTypes.EDIT_ACTIONS])
+ );
+ },
+};
diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/index.ts
similarity index 85%
rename from app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts
rename to app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/index.ts
index fb1b4ab11d67..6296da47e1a2 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data.ts
+++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/Data/index.ts
@@ -1,99 +1,21 @@
import { ValidationTypes } from "constants/WidgetValidation";
+import { get } from "lodash";
+import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
+import { CurrencyDropdownOptions } from "widgets/CurrencyInputWidget/component/CurrencyCodeDropdown";
import type { TableWidgetProps } from "widgets/TableWidgetV2/constants";
import { ColumnTypes, DateInputFormat } from "widgets/TableWidgetV2/constants";
-import { get } from "lodash";
import {
getBasePropertyPath,
hideByColumnType,
- showByColumnType,
uniqueColumnAliasValidation,
- updateCurrencyDefaultValues,
- updateMenuItemsSource,
- updateNumberColumnTypeTextAlignment,
- updateThemeStylesheetsInColumns,
-} from "../../propertyUtils";
-import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType";
-import { composePropertyUpdateHook } from "widgets/WidgetUtils";
-import { CurrencyDropdownOptions } from "widgets/CurrencyInputWidget/component/CurrencyCodeDropdown";
+} from "../../../propertyUtils";
+import { columnTypeConfig, columnTypeWithHtmlConfig } from "./ColumnType";
export default {
sectionName: "Data",
children: [
- {
- propertyName: "columnType",
- label: "Column type",
- helpText:
- "Type of column to be shown corresponding to the data of the column",
- controlType: "DROP_DOWN",
- options: [
- {
- label: "Button",
- value: ColumnTypes.BUTTON,
- },
- {
- label: "Checkbox",
- value: ColumnTypes.CHECKBOX,
- },
- {
- label: "Currency",
- value: ColumnTypes.CURRENCY,
- },
- {
- label: "Date",
- value: ColumnTypes.DATE,
- },
- {
- label: "Icon button",
- value: ColumnTypes.ICON_BUTTON,
- },
- {
- label: "Image",
- value: ColumnTypes.IMAGE,
- },
- {
- label: "Menu button",
- value: ColumnTypes.MENU_BUTTON,
- },
- {
- label: "Number",
- value: ColumnTypes.NUMBER,
- },
- {
- label: "Plain text",
- value: ColumnTypes.TEXT,
- },
- {
- label: "Select",
- value: ColumnTypes.SELECT,
- },
- {
- label: "Switch",
- value: ColumnTypes.SWITCH,
- },
- {
- label: "URL",
- value: ColumnTypes.URL,
- },
- {
- label: "Video",
- value: ColumnTypes.VIDEO,
- },
- ],
- updateHook: composePropertyUpdateHook([
- updateNumberColumnTypeTextAlignment,
- updateThemeStylesheetsInColumns,
- updateMenuItemsSource,
- updateCurrencyDefaultValues,
- ]),
- dependencies: ["primaryColumns", "columnOrder", "childStylesheet"],
- isBindProperty: false,
- isTriggerProperty: false,
- hidden: (props: TableWidgetProps, propertyPath: string) => {
- return showByColumnType(props, propertyPath, [
- ColumnTypes.EDIT_ACTIONS,
- ]);
- },
- },
+ columnTypeConfig,
+ columnTypeWithHtmlConfig,
{
helpText: "The alias that you use in selectedrow",
propertyName: "alias",
@@ -173,6 +95,7 @@ export default {
ColumnTypes.SWITCH,
ColumnTypes.SELECT,
ColumnTypes.CURRENCY,
+ ColumnTypes.HTML,
]);
},
dependencies: ["primaryColumns", "columnOrder"],
diff --git a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/index.ts b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/index.ts
index 79446122f428..b71d607a7eeb 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/index.ts
+++ b/app/client/src/widgets/TableWidgetV2/widget/propertyConfig/PanelConfig/index.ts
@@ -4,6 +4,7 @@ import Basic from "./Basic";
import BorderAndShadow from "./BorderAndShadow";
import Color from "./Color";
import Data from "./Data";
+import DateProperties from "./DateProperties";
import DiscardButtonproperties, {
discardButtonStyleConfig,
} from "./DiscardButtonproperties";
@@ -16,7 +17,6 @@ import SaveButtonProperties, {
import Select from "./Select";
import TextFormatting from "./TextFormatting";
import Validations from "./Validation";
-import DateProperties from "./DateProperties";
export default {
editableTitle: true,
diff --git a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts
index 20f5d00561e5..502c8527779a 100644
--- a/app/client/src/widgets/TableWidgetV2/widget/utilities.ts
+++ b/app/client/src/widgets/TableWidgetV2/widget/utilities.ts
@@ -770,12 +770,23 @@ export const getColumnType = (
return ColumnTypes.NUMBER;
case "boolean":
return ColumnTypes.CHECKBOX;
- case "string":
- return dateFormatOptions.some(({ value: format }) =>
+ case "string": {
+ const isHTML = /<[^>]*>/.test(columnValue as string);
+
+ if (isHTML) {
+ return ColumnTypes.HTML;
+ }
+
+ const isAnyValidDate = dateFormatOptions.some(({ value: format }) =>
moment(columnValue as string, format, true).isValid(),
- )
- ? ColumnTypes.DATE
- : ColumnTypes.TEXT;
+ );
+
+ if (isAnyValidDate) {
+ return ColumnTypes.DATE;
+ }
+
+ return ColumnTypes.TEXT;
+ }
default:
return ColumnTypes.TEXT;
}