-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat: Implements HTML as a column type in table widget. #37997
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2b626c1
17eecc7
62267ea
9fc0503
5965306
19f7f3b
a751320
d31d2d8
21c9eee
0954018
e9e554f
fff05a8
a432d7c
7b7fe4b
bdd9bc5
fe5be6b
005e6fa
b193802
0063ed2
e10781f
46793ad
1ab1818
3d017b8
29595a0
e3aaf42
46c29c8
aecd362
c1ec9f3
532bc70
51cd403
5f873fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { htmlTableData } from "../../../../../../fixtures/htmlCellInTableWidgetV2"; | ||
| import { featureFlagIntercept } from "../../../../../../support/Objects/FeatureFlags"; | ||
| import { | ||
| agHelper, | ||
| entityExplorer, | ||
| propPane, | ||
| table, | ||
| } from "../../../../../../support/Objects/ObjectsCore"; | ||
|
|
||
| describe( | ||
| "Table Filter for HTML Cell", | ||
| { tags: ["@tag.Widget", "@tag.Table"] }, | ||
| function () { | ||
| before(() => { | ||
| featureFlagIntercept({ | ||
| release_table_html_column_type_enabled: true, | ||
| }); | ||
| entityExplorer.DragDropWidgetNVerify("tablewidgetv2", 650, 250); | ||
| propPane.EnterJSContext("Table data", JSON.stringify(htmlTableData)); | ||
| }); | ||
|
|
||
| it("1. Ensures HTML column type is available", function () { | ||
| table.ReadTableRowColumnData(1, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Active"); | ||
| }); | ||
| }); | ||
|
|
||
| it("2. Verify HTML columns are searchable", function () { | ||
| table.ReadTableRowColumnData(1, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Active"); | ||
| table.SearchTable($cellData); | ||
| table.ReadTableRowColumnData(0, 3, "v2").then((afterSearch) => { | ||
| expect(afterSearch).to.eq($cellData); | ||
| }); | ||
| }); | ||
| table.RemoveSearchTextNVerify("1", "v2"); | ||
| }); | ||
|
|
||
| it("3. Verify Table Filter for HTML columns", function () { | ||
| propPane.ExpandIfCollapsedSection("search\\&filters"); | ||
| agHelper.AssertExistingToggleState("Allow filtering", "false"); | ||
| propPane.TogglePropertyState("Allow filtering", "On"); | ||
|
|
||
| table.OpenNFilterTable("status", "contains", "Active"); | ||
| table.ReadTableRowColumnData(0, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Active"); | ||
| }); | ||
| table.RemoveFilterNVerify("1", true, true, 0, "v2"); | ||
|
|
||
| table.OpenNFilterTable("status", "contains", "Suspended"); | ||
| table.ReadTableRowColumnData(0, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Suspended"); | ||
| }); | ||
| table.RemoveFilterNVerify("1", true, true, 0, "v2"); | ||
|
|
||
| table.OpenNFilterTable("status", "empty", ""); | ||
| table.ReadTableRowColumnData(0, 0, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("1"); | ||
| }); | ||
| table.RemoveFilterNVerify("1", true, true, 0, "v2"); | ||
|
|
||
| table.OpenNFilterTable("status", "not empty", ""); | ||
| table.ReadTableRowColumnData(0, 0, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("2"); | ||
| }); | ||
| table.RemoveFilterNVerify("1", true, true, 0, "v2"); | ||
| }); | ||
|
|
||
| it("4. Verify Table sorting for HTML columns", function () { | ||
| table.SortColumn("status", "asc"); | ||
| table.ReadTableRowColumnData(0, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Active"); | ||
| }); | ||
| table.SortColumn("status", "desc"); | ||
| table.ReadTableRowColumnData(0, 3, "v2").then(($cellData) => { | ||
| expect($cellData).to.include("Suspended"); | ||
| }); | ||
| }); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| export const htmlTableData = [ | ||
| { | ||
| id: 1, | ||
| name: "John Smith", | ||
| email: "john.smith@email.com", | ||
| role: undefined, | ||
| status: null, | ||
| applicationDate: "2024-02-15", | ||
| lastUpdated: "2024-03-20", | ||
| department: "Engineering", | ||
| }, | ||
| { | ||
| id: 2, | ||
| name: "Emma Wilson", | ||
| email: "emma.w@email.com", | ||
| role: "Designer", | ||
| status: | ||
| "<span style='background-color: #22c55e; color: white; padding: 4px 12px; border-radius: 20px; font-size: 14px;'><strong>Active</strong></span>", | ||
| applicationDate: "2024-03-01", | ||
| lastUpdated: "2024-03-19", | ||
| department: "Design", | ||
| }, | ||
| { | ||
| id: 3, | ||
| name: "Michael Brown", | ||
| email: "m.brown@email.com", | ||
| role: "Manager", | ||
| status: | ||
| "<span style='background-color: #ef4444; color: white; padding: 4px 12px; border-radius: 20px; font-size: 14px;'><strong>Suspended</strong></span>", | ||
| applicationDate: "2024-01-10", | ||
| lastUpdated: "2024-03-18", | ||
| department: "Operations", | ||
| }, | ||
| { | ||
| id: 4, | ||
| name: "Sarah Davis", | ||
| email: "sarah.d@email.com", | ||
| role: "Developer", | ||
| status: | ||
| "<span style='background-color: #22c55e; color: white; padding: 4px 12px; border-radius: 20px; font-size: 14px;'><strong>Active</strong></span>", | ||
| applicationDate: "2024-02-20", | ||
| lastUpdated: "2024-03-17", | ||
| department: "Engineering", | ||
| }, | ||
| { | ||
| id: 5, | ||
| name: "James Wilson", | ||
| email: "j.wilson@email.com", | ||
| role: "Analyst", | ||
| status: | ||
| "<span style='background-color: #3b82f6; color: white; padding: 4px 12px; border-radius: 20px; font-size: 14px;'><strong>Reviewing</strong></span>", | ||
| applicationDate: "2024-03-05", | ||
| lastUpdated: "2024-03-16", | ||
| department: "Analytics", | ||
| }, | ||
| ]; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import type { RenderMode } from "constants/WidgetConstants"; | ||
| import Interweave from "interweave"; | ||
| import { isEqual } from "lodash"; | ||
| import React, { useEffect, useMemo, useRef } from "react"; | ||
| import styled from "styled-components"; | ||
| import LinkFilter from "widgets/TextWidget/component/filters/LinkFilter"; | ||
| import type { BaseCellComponentProps } from "../../Constants"; | ||
| import { CellWrapper } from "../../TableStyledWrappers"; | ||
| import { extractHTMLTags, sendHTMLCellAnalytics } from "./utils"; | ||
|
|
||
| const HTMLContainer = styled.div` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI there is actually a name for such components that styles basic HTML elements. It's called "Prose". https://design-system.agriculture.gov.au/components/prose If this was WDS, I would have created Prose component and use it here and in the text component instead of repeating.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Btw, I am saying not to do this in this PR, this is just an FYI
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is good to know! will keep this in mind. Thanks. |
||
| & { | ||
| height: 100%; | ||
| width: 100%; | ||
| position: relative; | ||
| } | ||
| ul { | ||
| list-style-type: disc; | ||
| list-style-position: inside; | ||
| } | ||
| ol { | ||
| list-style-type: decimal; | ||
| list-style-position: inside; | ||
| } | ||
| ul ul, | ||
| ol ul { | ||
| list-style-type: circle; | ||
| list-style-position: inside; | ||
| margin-left: 15px; | ||
| } | ||
| ol ol, | ||
| ul ol { | ||
| list-style-type: lower-latin; | ||
| list-style-position: inside; | ||
| margin-left: 15px; | ||
| } | ||
| h1 { | ||
| font-size: 2em; | ||
| margin: 0.67em 0; | ||
| } | ||
| h2 { | ||
| font-size: 1.5em; | ||
| margin: 0.75em 0; | ||
| } | ||
| h3 { | ||
| font-size: 1.17em; | ||
| margin: 0.83em 0; | ||
| } | ||
| h5 { | ||
| font-size: 0.83em; | ||
| margin: 1.5em 0; | ||
| } | ||
| h6 { | ||
| font-size: 0.75em; | ||
| margin: 1.67em 0; | ||
| } | ||
| h1, | ||
| h2, | ||
| h3, | ||
| h4, | ||
| h5, | ||
| h6 { | ||
| font-weight: bold; | ||
| } | ||
| a { | ||
| color: #106ba3; | ||
| text-decoration: none; | ||
| &:hover { | ||
| text-decoration: underline; | ||
| } | ||
| } | ||
| `; | ||
|
|
||
| export interface HTMLCellProps extends BaseCellComponentProps { | ||
| value: string; | ||
| fontSize?: string; | ||
| renderMode: RenderMode; | ||
| } | ||
|
|
||
| const HTMLCell = (props: HTMLCellProps) => { | ||
| const { | ||
| allowCellWrapping, | ||
| cellBackground, | ||
| compactMode, | ||
| fontStyle, | ||
| horizontalAlignment, | ||
| isCellDisabled, | ||
| isCellVisible, | ||
| isHidden, | ||
| renderMode, | ||
| textColor, | ||
| textSize, | ||
| value, | ||
| verticalAlignment, | ||
| } = props; | ||
|
|
||
| const previousTagsRef = useRef<string[]>([]); | ||
|
|
||
| const interweaveCompatibleValue = useMemo(() => { | ||
| if (value === null || value === undefined) return ""; | ||
|
|
||
| return String(value); | ||
| }, [value]); | ||
|
|
||
| /** | ||
| * For analytics, we want to know what tags are being used by users in HTMLCell? | ||
| * This will help us in knowing usage patterns and identifying if something is not working out. | ||
| */ | ||
| const extractedTags = useMemo(() => { | ||
rahulbarwal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!interweaveCompatibleValue) return []; | ||
|
|
||
| return extractHTMLTags(interweaveCompatibleValue); | ||
| }, [interweaveCompatibleValue]); | ||
|
|
||
| useEffect(() => { | ||
| const areTagsChanged = !isEqual( | ||
| [...extractedTags].sort(), | ||
| [...previousTagsRef.current].sort(), | ||
| ); | ||
|
|
||
| if (extractedTags.length > 0 && areTagsChanged) { | ||
| sendHTMLCellAnalytics(extractedTags); | ||
| previousTagsRef.current = extractedTags; | ||
| } | ||
| }, [extractedTags, renderMode]); | ||
|
|
||
| return ( | ||
| <CellWrapper | ||
| allowCellWrapping={allowCellWrapping} | ||
| cellBackground={cellBackground} | ||
| className="cell-wrapper" | ||
| compactMode={compactMode} | ||
| fontStyle={fontStyle} | ||
| horizontalAlignment={horizontalAlignment} | ||
| isCellDisabled={isCellDisabled} | ||
| isCellVisible={isCellVisible} | ||
| isHidden={isHidden} | ||
| textColor={textColor} | ||
| textSize={textSize} | ||
| verticalAlignment={verticalAlignment} | ||
| > | ||
| <HTMLContainer data-testid="t--table-widget-v2-html-cell"> | ||
| <Interweave | ||
| content={interweaveCompatibleValue} | ||
| filters={[new LinkFilter()]} | ||
| newWindow | ||
| /> | ||
| </HTMLContainer> | ||
| </CellWrapper> | ||
| ); | ||
| }; | ||
|
|
||
| export default HTMLCell; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import AnalyticsUtil from "ee/utils/AnalyticsUtil"; | ||
rahulbarwal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import { debounce } from "lodash"; | ||
|
|
||
| export const sendHTMLCellAnalytics = debounce( | ||
| (tags: string[]) => { | ||
| AnalyticsUtil.logEvent("TABLE_WIDGET_V2_HTML_CELL_USAGE", { | ||
| tags: tags, | ||
| }); | ||
| }, | ||
| 1000, | ||
| { leading: true, trailing: false, maxWait: 5000 }, | ||
| ); | ||
|
|
||
| export function extractHTMLTags(htmlString: string): string[] { | ||
| const div = document.createElement("div"); | ||
|
|
||
| div.innerHTML = htmlString; | ||
| const elements = Array.from(div.getElementsByTagName("*")); | ||
| const uniqueTags = new Set( | ||
| elements.map((element) => element.tagName.toLowerCase()), | ||
| ); | ||
|
|
||
| return Array.from(uniqueTags); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.