fix: Enhance URL handling in table by rendering URL column types with <a> tag. (#37179)

## Description
<ins>Problem</ins>

URLs in table were not being rendered as links, resulting in
inconsistent user experience(missing context menus.

<ins>Root cause</ins>

URLs were rendered in `<div>` instead of `<a>`, making the component
lack links related features..

<ins>Solution</ins>

This PR handles... 

- Rendering URLs as links in BasicCell for a better user experience.
- Adding specific types for column properties for more robust data
validation and type checking.
- Adding unit tests for BasicCell functionality to ensure accurate
rendering and behavior.

- Simplifies the AutoToolTipComponent by removing unncessary
`LinkWrapper` component


Fixes #24769
_or_  
Fixes `Issue URL`
> [!WARNING]  
> _If no issue exists, please create an issue first, and check with the
maintainers if the issue is valid._

## Automation

/ok-to-test tags="@tag.Table"

### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/11681339029>
> Commit: b7c5d176b35407923a120bb19e40252e3a61b628
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=11681339029&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.Table`
> Spec:
> <hr>Tue, 05 Nov 2024 10:23:38 UTC
<!-- end of auto-generated comment: Cypress test results  -->


## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Enhanced type safety for `compactMode` and `columnType` properties
across various components.
- Improved rendering logic in the `AutoToolTipComponent` for better
control based on `columnType`.
	- Optimized rendering in the `BasicCell` component using `useMemo`.

- **Bug Fixes**
- Resolved inconsistencies in type definitions for `BasicCell`,
`PlainTextCell`, and `SelectCell` components.
- Updated tooltip behavior in the `AutoToolTipComponent` to ensure
accurate rendering.

- **Tests**
- Introduced a new test suite for the `BasicCell` component, ensuring
proper rendering and interaction behaviors.
- Refined test cases for the `AutoToolTipComponent` to verify accurate
rendering under various conditions.
- Updated test case for URL column verification to check attributes
directly instead of navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Rahul Barwal 2024-11-06 10:27:57 +05:30 committed by GitHub
parent 4e18827512
commit 0288f5b9ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 120 additions and 80 deletions

View File

@ -20,18 +20,23 @@ describe(
table.ReadTableRowColumnData(3, 0, "v2").then(($cellData) => {
expect($cellData).to.eq("Profile pic");
});
table.AssertURLColumnNavigation(
0,
0,
"https://randomuser.me/api/portraits/med/women/39.jpg",
"v2",
);
table.AssertURLColumnNavigation(
3,
0,
"https://randomuser.me/api/portraits/med/men/52.jpg",
"v2",
);
agHelper
.GetElement(`${table._tableRowColumnData(0, 0, "v2")} a`)
.should(
"have.attr",
"href",
"https://randomuser.me/api/portraits/med/women/39.jpg",
)
.should("have.attr", "target", "_blank");
agHelper
.GetElement(`${table._tableRowColumnData(3, 0, "v2")} a`)
.should(
"have.attr",
"href",
"https://randomuser.me/api/portraits/med/men/52.jpg",
)
.should("have.attr", "target", "_blank");
});
},
);

View File

@ -546,7 +546,7 @@ export enum IMAGE_VERTICAL_ALIGN {
}
export interface BaseCellComponentProps {
compactMode: string;
compactMode: CompactMode;
isHidden: boolean;
allowCellWrapping?: boolean;
horizontalAlignment?: CellAlignment;

View File

@ -137,7 +137,7 @@ interface Props {
children: React.ReactNode;
title: string;
tableWidth?: number;
columnType?: string;
columnType?: ColumnTypes;
className?: string;
compactMode?: string;
allowCellWrapping?: boolean;
@ -152,38 +152,6 @@ interface Props {
isCellDisabled?: boolean;
}
function LinkWrapper(props: Props) {
const content = useToolTip(props.children, props.title);
return (
<CellWrapper
allowCellWrapping={props.allowCellWrapping}
cellBackground={props.cellBackground}
className="cell-wrapper"
compactMode={props.compactMode}
fontStyle={props.fontStyle}
horizontalAlignment={props.horizontalAlignment}
isCellDisabled={props.isCellDisabled}
isCellVisible={props.isCellVisible}
isHidden={props.isHidden}
isHyperLink
isTextType
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
window.open(props.url, "_blank");
}}
textColor={props.textColor}
textSize={props.textSize}
verticalAlignment={props.verticalAlignment}
>
<div className="link-text">{content}</div>
<OpenNewTabIconWrapper className="hidden-icon">
<OpenNewTabIcon />
</OpenNewTabIconWrapper>
</CellWrapper>
);
}
export function AutoToolTipComponent(props: Props) {
const content = useToolTip(
props.children,
@ -191,12 +159,27 @@ export function AutoToolTipComponent(props: Props) {
props.columnType === ColumnTypes.BUTTON,
);
if (props.columnType === ColumnTypes.URL && props.title) {
return <LinkWrapper {...props} />;
}
let contentToRender;
if (props.columnType === ColumnTypes.BUTTON && props.title) {
return content;
switch (props.columnType) {
case ColumnTypes.BUTTON:
if (props.title) {
return content;
}
break;
case ColumnTypes.URL:
contentToRender = (
<>
<div className="link-text">{content}</div>
<OpenNewTabIconWrapper className="hidden-icon">
<OpenNewTabIcon />
</OpenNewTabIconWrapper>
</>
);
break;
default:
contentToRender = content;
}
return (
@ -212,12 +195,13 @@ export function AutoToolTipComponent(props: Props) {
isCellDisabled={props.isCellDisabled}
isCellVisible={props.isCellVisible}
isHidden={props.isHidden}
isHyperLink={props.columnType === ColumnTypes.URL}
isTextType
textColor={props.textColor}
textSize={props.textSize}
verticalAlignment={props.verticalAlignment}
>
{content}
{contentToRender}
</CellWrapper>
</ColumnWrapper>
);

View File

@ -54,7 +54,7 @@ test("does not show tooltip for non-button types", () => {
test("handles empty tooltip", () => {
const { getByText } = render(
<AutoToolTipComponent columnType={ColumnTypes.BUTTON} title="">
<AutoToolTipComponent columnType={ColumnTypes.BUTTON} title="Empty button">
<button>Empty button</button>
</AutoToolTipComponent>,
);
@ -86,25 +86,6 @@ test("does not show tooltip for non-truncated text", () => {
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
test("opens a new tab for URL column type when clicked", () => {
const openSpy = jest.spyOn(window, "open").mockImplementation(() => null);
render(
<AutoToolTipComponent
columnType={ColumnTypes.URL}
title="Go to Google"
url="https://www.google.com"
>
<span>Go to Google</span>
</AutoToolTipComponent>,
);
fireEvent.click(screen.getByText("Go to Google"));
expect(openSpy).toHaveBeenCalledWith("https://www.google.com", "_blank");
openSpy.mockRestore();
});
describe("isButtonTextTruncated", () => {
function mockElementWidths(
offsetWidth: number,

View File

@ -0,0 +1,56 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import { BasicCell, type PropType } from "./BasicCell";
import { ColumnTypes } from "widgets/TableWidgetV2/constants";
import { CompactModeTypes } from "widgets/TableWidget/component/Constants";
describe("BasicCell Component", () => {
const defaultProps: PropType = {
value: "Test Value",
onEdit: jest.fn(),
isCellEditable: false,
hasUnsavedChanges: false,
columnType: ColumnTypes.TEXT,
url: "",
compactMode: CompactModeTypes.DEFAULT,
isHidden: false,
isCellVisible: true,
accentColor: "",
tableWidth: 100,
disabledEditIcon: false,
disabledEditIconMessage: "",
};
it("renders the value", () => {
render(<BasicCell {...defaultProps} />);
expect(screen.getByText("Test Value")).toBeInTheDocument();
});
it("renders a link when columnType is URL", () => {
render(
<BasicCell
{...defaultProps}
columnType={ColumnTypes.URL}
url="http://example.com"
/>,
);
const link = screen.getByText("Test Value");
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "http://example.com");
});
it("calls onEdit when double-clicked", () => {
render(<BasicCell {...defaultProps} isCellEditable />);
fireEvent.doubleClick(screen.getByText("Test Value"));
expect(defaultProps.onEdit).toHaveBeenCalled();
});
it("forwards ref to the div element", () => {
const ref = React.createRef<HTMLDivElement>();
render(<BasicCell {...defaultProps} ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});

View File

@ -1,12 +1,13 @@
import type { Ref } from "react";
import React, { useCallback } from "react";
import React, { useCallback, useMemo } from "react";
import { Tooltip } from "@blueprintjs/core";
import styled from "styled-components";
import type { BaseCellComponentProps } from "../Constants";
import type { BaseCellComponentProps, CompactMode } from "../Constants";
import { TABLE_SIZES } from "../Constants";
import { TooltipContentWrapper } from "../TableStyledWrappers";
import AutoToolTipComponent from "./AutoToolTipComponent";
import { importSvg } from "@appsmith/ads-old";
import { ColumnTypes } from "widgets/TableWidgetV2/constants";
const EditIcon = importSvg(
async () => import("assets/icons/control/edit-variant1.svg"),
@ -55,7 +56,7 @@ const Content = styled.div`
const StyledEditIcon = styled.div<{
accentColor?: string;
backgroundColor?: string;
compactMode: string;
compactMode: CompactMode;
disabledEditIcon: boolean;
}>`
position: absolute;
@ -74,12 +75,12 @@ const StyledEditIcon = styled.div<{
}
`;
type PropType = BaseCellComponentProps & {
export type PropType = BaseCellComponentProps & {
accentColor: string;
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
columnType: string;
columnType: ColumnTypes;
tableWidth: number;
isCellEditable?: boolean;
isCellEditMode?: boolean;
@ -128,6 +129,18 @@ export const BasicCell = React.forwardRef(
},
[onEdit, disabledEditIcon, isCellEditable],
);
const contentToRender = useMemo(() => {
switch (columnType) {
case ColumnTypes.URL:
return (
<a href={url} rel="noopener noreferrer" target="_blank">
{value}
</a>
);
default:
return value;
}
}, [columnType, url, value]);
return (
<Wrapper
@ -157,7 +170,7 @@ export const BasicCell = React.forwardRef(
url={url}
verticalAlignment={verticalAlignment}
>
<Content ref={contentRef}>{value}</Content>
<Content ref={contentRef}>{contentToRender}</Content>
</StyledAutoToolTipComponent>
{isCellEditable && (
<StyledEditIcon

View File

@ -39,7 +39,7 @@ export type RenderDefaultPropsType = BaseCellComponentProps & {
// TODO: Fix this the next time the file is edited
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
columnType: string;
columnType: ColumnTypes;
tableWidth: number;
isCellEditable: boolean;
isCellEditMode?: boolean;

View File

@ -11,6 +11,7 @@ import {
} from "../Constants";
import { CellWrapper } from "../TableStyledWrappers";
import { BasicCell } from "./BasicCell";
import type { ColumnTypes } from "widgets/TableWidget/component/Constants";
const StyledSelectComponent = styled(SelectComponent)<{
accentColor: string;
@ -61,7 +62,7 @@ type SelectProps = BaseCellComponentProps & {
alias: string;
accentColor: string;
autoOpen: boolean;
columnType: string;
columnType: ColumnTypes;
borderRadius: string;
options?: DropdownOption[];
onFilterChange: (