Changed record chip functionality from onClick to anchor tag (#5462)

[#4422](https://github.com/twentyhq/twenty/issues/4422)

Demo:



https://github.com/twentyhq/twenty/assets/155670906/f8027ab2-c579-45f7-9f08-f4441a346ae7



Within the demo, we show the various areas in which the Command/CTRL +
Click functionality works. The table cells within the People and
Companies tab open within both the current tab and new tab due to
unchanged functionality within RecordTableCell. We did this to ensure we
could get a PR within by the end of the week.

In this commit, we ONLY edited EntityChip.tsx. We did this by:

- Removing useNavigate() and handleLinkClick/onClick functionality

- Wrapping InnerEntityChip in an anchor tag

This allowed for Command/CTRL + Click functionality to work. Clickable
left cells on tables, left side menu, and data model navigation
files/areas DID NOT get updated.

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
ktang520
2024-05-20 02:31:39 -07:00
committed by GitHub
parent 8b5f79ddbf
commit 1ceeb68da8
22 changed files with 154 additions and 120 deletions
@@ -1,4 +1,4 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconCheckbox, IconSearch, IconSettings } from 'twenty-ui';
@@ -20,7 +20,6 @@ export const MainNavigationDrawerItems = () => {
const { toggleCommandMenu } = useCommandMenu();
const isTasksPage = useIsTasksPage();
const currentUserDueTaskCount = useRecoilValue(currentUserDueTaskCountState);
const navigate = useNavigate();
const location = useLocation();
const setNavigationMemorizedUrl = useSetRecoilState(
navigationMemorizedUrlState,
@@ -38,9 +37,9 @@ export const MainNavigationDrawerItems = () => {
/>
<NavigationDrawerItem
label="Settings"
to={'/settings/profile'}
onClick={() => {
setNavigationMemorizedUrl(location.pathname + location.search);
navigate('/settings/profile');
}}
Icon={IconSettings}
/>
@@ -1,4 +1,4 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import { useIcons } from 'twenty-ui';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
@@ -10,7 +10,6 @@ import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemV
export const ObjectMetadataNavItems = () => {
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const navigate = useNavigate();
const { getIcon } = useIcons();
const currentPath = useLocation().pathname;
@@ -63,9 +62,6 @@ export const ObjectMetadataNavItems = () => {
to={navigationPath}
active={currentPath === `/objects/${objectMetadataItem.namePlural}`}
Icon={getIcon(objectMetadataItem.icon)}
onClick={() => {
navigate(navigationPath);
}}
/>
);
})}
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent, IconTwentyStar } from 'twenty-ui';
@@ -7,13 +8,16 @@ import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSetti
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SettingsObjectFieldDataTypeProps = {
onClick?: () => void;
to?: string;
Icon?: IconComponent;
label?: string;
value: SettingsSupportedFieldType;
};
const StyledDataType = styled.div<{ value: SettingsSupportedFieldType }>`
const StyledDataType = styled.div<{
value: SettingsSupportedFieldType;
to?: string;
}>`
align-items: center;
border: 1px solid transparent;
border-radius: ${({ theme }) => theme.border.radius.sm};
@@ -23,9 +27,10 @@ const StyledDataType = styled.div<{ value: SettingsSupportedFieldType }>`
height: 20px;
overflow: hidden;
padding: 0 ${({ theme }) => theme.spacing(2)};
text-decoration: none;
${({ onClick }) =>
onClick
${({ to }) =>
to
? css`
cursor: pointer;
`
@@ -47,7 +52,7 @@ const StyledLabelContainer = styled.div`
`;
export const SettingsObjectFieldDataType = ({
onClick,
to,
value,
Icon: IconFromProps,
label: labelFromProps,
@@ -64,7 +69,7 @@ export const SettingsObjectFieldDataType = ({
`;
return (
<StyledDataType onClick={onClick} value={value}>
<StyledDataType as={to ? Link : 'div'} to={to} value={value}>
<StyledIcon size={theme.icon.size.sm} />
<StyledLabelContainer>{label}</StyledLabelContainer>
</StyledDataType>
@@ -1,5 +1,4 @@
import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Nullable, useIcons } from 'twenty-ui';
@@ -22,6 +21,7 @@ type SettingsObjectFieldItemTableRowProps = {
identifierType?: Nullable<FieldIdentifierType>;
variant?: 'field-type' | 'identifier';
isRemoteObjectField?: boolean;
to?: string;
};
export const StyledObjectFieldTableRow = styled(TableRow)`
@@ -44,11 +44,11 @@ export const SettingsObjectFieldItemTableRow = ({
identifierType,
variant = 'field-type',
isRemoteObjectField,
to,
}: SettingsObjectFieldItemTableRowProps) => {
const theme = useTheme();
const { getIcon } = useIcons();
const Icon = getIcon(fieldMetadataItem.icon);
const navigate = useNavigate();
const getRelationMetadata = useGetRelationMetadata();
@@ -68,7 +68,7 @@ export const SettingsObjectFieldItemTableRow = ({
: undefined;
return (
<StyledObjectFieldTableRow>
<StyledObjectFieldTableRow to={to}>
<StyledNameTableCell>
{!!Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
@@ -90,15 +90,10 @@ export const SettingsObjectFieldItemTableRow = ({
<SettingsObjectFieldDataType
Icon={RelationIcon}
label={relationObjectMetadataItem?.labelPlural}
onClick={
to={
relationObjectMetadataItem?.namePlural &&
!relationObjectMetadataItem.isSystem
? () =>
navigate(
`/settings/objects/${getObjectSlug(
relationObjectMetadataItem,
)}`,
)
? `/settings/objects/${getObjectSlug(relationObjectMetadataItem)}`
: undefined
}
value={fieldType}
@@ -13,7 +13,7 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
type SettingsObjectItemTableRowProps = {
action: ReactNode;
objectItem: ObjectMetadataItem;
onClick?: () => void;
to?: string;
};
export const StyledObjectTableRow = styled(TableRow)`
@@ -33,7 +33,7 @@ const StyledActionTableCell = styled(TableCell)`
export const SettingsObjectItemTableRow = ({
action,
objectItem,
onClick,
to,
}: SettingsObjectItemTableRowProps) => {
const theme = useTheme();
@@ -45,7 +45,7 @@ export const SettingsObjectItemTableRow = ({
const objectTypeLabel = getObjectTypeLabel(objectItem);
return (
<StyledObjectTableRow key={objectItem.namePlural} onClick={onClick}>
<StyledObjectTableRow key={objectItem.namePlural} to={to}>
<StyledNameTableCell>
{!!Icon && (
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
@@ -26,15 +26,15 @@ const StyledIconChevronRight = styled(IconChevronRight)`
export const SettingsApiKeysFieldItemTableRow = ({
fieldItem,
onClick,
to,
}: {
fieldItem: ApiFieldItem;
onClick: () => void;
to: string;
}) => {
const theme = useTheme();
return (
<StyledApisFieldTableRow onClick={() => onClick()}>
<StyledApisFieldTableRow to={to}>
<StyledNameTableCell>{fieldItem.name}</StyledNameTableCell>
<TableCell
color={
@@ -1,4 +1,3 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -21,8 +20,6 @@ const StyledTableRow = styled(TableRow)`
`;
export const SettingsApiKeysTable = () => {
const navigate = useNavigate();
const { records: apiKeys } = useFindManyRecords<ApiKey>({
objectNameSingular: CoreObjectNameSingular.ApiKey,
filter: { revokedAt: { is: 'NULL' } },
@@ -41,9 +38,7 @@ export const SettingsApiKeysTable = () => {
<SettingsApiKeysFieldItemTableRow
key={fieldItem.id}
fieldItem={fieldItem as ApiFieldItem}
onClick={() => {
navigate(`/settings/developers/api-keys/${fieldItem.id}`);
}}
to={`/settings/developers/api-keys/${fieldItem.id}`}
/>
))}
</StyledTableBody>
@@ -28,15 +28,15 @@ const StyledIconChevronRight = styled(IconChevronRight)`
export const SettingsDevelopersWebhookTableRow = ({
fieldItem,
onClick,
to,
}: {
fieldItem: Webhook;
onClick: () => void;
to: string;
}) => {
const theme = useTheme();
return (
<StyledApisFieldTableRow onClick={onClick}>
<StyledApisFieldTableRow to={to}>
<StyledUrlTableCell>{fieldItem.targetUrl}</StyledUrlTableCell>
<StyledIconTableCell>
<StyledIconChevronRight
@@ -10,9 +10,8 @@ export const SettingsReadDocumentationButton = () => {
accent="default"
size="small"
Icon={IconBook2}
onClick={() => {
window.open('https://docs.twenty.com');
}}
to={'https://docs.twenty.com'}
target="_blank"
></Button>
);
};
@@ -1,4 +1,3 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@@ -19,8 +18,6 @@ const StyledTableRow = styled(TableRow)`
`;
export const SettingsWebhooksTable = () => {
const navigate = useNavigate();
const { records: webhooks } = useFindManyRecords<Webhook>({
objectNameSingular: CoreObjectNameSingular.Webhook,
});
@@ -37,11 +34,7 @@ export const SettingsWebhooksTable = () => {
<SettingsDevelopersWebhookTableRow
key={webhookFieldItem.id}
fieldItem={webhookFieldItem}
onClick={() => {
navigate(
`/settings/developers/webhooks/${webhookFieldItem.id}`,
);
}}
to={`/settings/developers/webhooks/${webhookFieldItem.id}`}
/>
))}
</StyledTableBody>
@@ -1,4 +1,4 @@
import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { IconArrowUpRight, IconBolt, IconPlus, Pill } from 'twenty-ui';
@@ -12,7 +12,7 @@ interface SettingsIntegrationComponentProps {
integration: SettingsIntegration;
}
const StyledContainer = styled.div`
const StyledContainer = styled.div<{ to?: string }>`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@@ -22,9 +22,11 @@ const StyledContainer = styled.div`
flex-direction: row;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(3)};
text-decoration: none;
color: ${({ theme }) => theme.font.color.primary};
${({ onClick }) =>
isDefined(onClick) &&
${({ to }) =>
isDefined(to) &&
css`
cursor: pointer;
`}
@@ -57,16 +59,10 @@ const StyledLogo = styled.img`
export const SettingsIntegrationComponent = ({
integration,
}: SettingsIntegrationComponentProps) => {
const navigate = useNavigate();
const navigateToIntegrationPage = () => navigate(integration.link);
const openExternalLink = () => window.open(integration.link);
return (
<StyledContainer
onClick={
integration.type === 'Active' ? navigateToIntegrationPage : undefined
}
to={integration.type === 'Active' ? integration.link : undefined}
as={integration.type === 'Active' ? Link : 'div'}
>
<StyledSection>
<StyledIntegrationLogo>
@@ -86,21 +82,23 @@ export const SettingsIntegrationComponent = ({
<Status color="green" text="Active" />
) : integration.type === 'Add' ? (
<Button
onClick={navigateToIntegrationPage}
to={integration.link}
Icon={IconPlus}
title="Add"
size="small"
/>
) : integration.type === 'Use' ? (
<Button
onClick={openExternalLink}
to={integration.link}
target="_blank"
Icon={IconBolt}
title="Use"
size="small"
/>
) : (
<Button
onClick={openExternalLink}
to={integration.link}
target="_blank"
Icon={IconArrowUpRight}
title={integration.linkText}
size="small"
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom';
import styled from '@emotion/styled';
import { SettingsIntegrationComponent } from '@/settings/integrations/components/SettingsIntegrationComponent';
@@ -16,13 +17,15 @@ const StyledIntegrationGroupHeader = styled.div`
justify-content: space-between;
`;
const StyledGroupLink = styled.div`
const StyledGroupLink = styled(Link)`
align-items: start;
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
cursor: pointer;
text-decoration: none;
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledIntegrationsSection = styled.div`
@@ -39,7 +42,8 @@ export const SettingsIntegrationGroup = ({
<H2Title title={integrationGroup.title} />
{integrationGroup.hyperlink && (
<StyledGroupLink
onClick={() => window.open(integrationGroup.hyperlink ?? '')}
target={'_blank'}
to={integrationGroup.hyperlink ?? ''}
>
<div>{integrationGroup.hyperlinkText}</div>
<div></div>
@@ -1,4 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent, Pill } from 'twenty-ui';
@@ -22,9 +24,14 @@ export type ButtonProps = {
disabled?: boolean;
focus?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
to?: string;
target?: string;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
const StyledButton = styled('button', {
shouldForwardProp: (prop) =>
!['fullWidth'].includes(prop) && isPropValid(prop),
})<
Pick<
ButtonProps,
| 'fullWidth'
@@ -34,6 +41,8 @@ const StyledButton = styled.button<
| 'accent'
| 'focus'
| 'justify'
| 'to'
| 'target'
>
>`
align-items: center;
@@ -210,6 +219,7 @@ const StyledButton = styled.button<
}
}}
text-decoration: none;
border-radius: ${({ position, theme }) => {
switch (position) {
case 'left':
@@ -273,6 +283,8 @@ export const Button = ({
justify = 'flex-start',
focus = false,
onClick,
to,
target,
}: ButtonProps) => {
const theme = useTheme();
@@ -288,6 +300,9 @@ export const Button = ({
accent={accent}
className={className}
onClick={onClick}
to={to}
as={to ? Link : 'button'}
target={target}
>
{Icon && <Icon size={theme.icon.size.sm} />}
{title}
@@ -1,8 +1,14 @@
import { Link } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid';
import styled from '@emotion/styled';
const StyledTableRow = styled.div<{
const StyledTableRow = styled('div', {
shouldForwardProp: (prop) =>
!['isSelected'].includes(prop) && isPropValid(prop),
})<{
isSelected?: boolean;
onClick?: () => void;
to?: string;
}>`
background-color: ${({ isSelected, theme }) =>
isSelected ? theme.accent.quaternary : 'transparent'};
@@ -13,12 +19,36 @@ const StyledTableRow = styled.div<{
transition: background-color
${({ theme }) => theme.animation.duration.normal}s;
width: 100%;
text-decoration: none;
&:hover {
background-color: ${({ onClick, theme }) =>
onClick ? theme.background.transparent.light : 'transparent'};
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
background-color: ${({ onClick, to, theme }) =>
onClick || to ? theme.background.transparent.light : 'transparent'};
cursor: ${({ onClick, to }) => (onClick || to ? 'pointer' : 'default')};
}
`;
export { StyledTableRow as TableRow };
type TableRowProps = {
isSelected?: boolean;
onClick?: () => void;
to?: string;
className?: string;
};
export const TableRow = ({
isSelected,
onClick,
to,
className,
children,
}: React.PropsWithChildren<TableRowProps>) => (
<StyledTableRow
isSelected={isSelected}
onClick={onClick}
className={className}
to={to}
as={to ? Link : 'div'}
>
{children}
</StyledTableRow>
);
@@ -1,4 +1,5 @@
import { useNavigate } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
@@ -29,14 +30,19 @@ type StyledItemProps = {
danger?: boolean;
level: 1 | 2;
soon?: boolean;
to?: string;
};
const StyledItem = styled.div<StyledItemProps>`
const StyledItem = styled('div', {
shouldForwardProp: (prop) =>
!['active', 'danger', 'soon'].includes(prop) && isPropValid(prop),
})<StyledItemProps>`
align-items: center;
background: ${(props) =>
props.active ? props.theme.background.transparent.light : 'inherit'};
border: none;
border-radius: ${({ theme }) => theme.border.radius.sm};
text-decoration: none;
color: ${(props) => {
if (props.active === true) {
return props.theme.font.color.primary;
@@ -153,6 +159,8 @@ export const NavigationDrawerItem = ({
aria-selected={active}
danger={danger}
soon={soon}
as={to ? Link : 'div'}
to={to ? to : undefined}
>
{Icon && <Icon size={theme.icon.size.md} stroke={theme.icon.stroke.md} />}
<StyledItemLabel>{label}</StyledItemLabel>
@@ -156,6 +156,7 @@ export const SettingsObjectDetail = () => {
}
fieldMetadataItem={activeMetadataField}
isRemoteObjectField={activeObjectMetadataItem.isRemote}
// to={`./${getFieldSlug(activeMetadataField)}`}
ActionIcon={
<SettingsObjectFieldActiveActionDropdown
isCustomField={!!activeMetadataField.isCustom}
@@ -184,9 +184,7 @@ export const SettingsObjectNewFieldStep1 = () => {
title="Add Custom Field"
size="small"
variant="secondary"
onClick={() =>
navigate(`/settings/objects/${objectSlug}/new-field/step-2`)
}
to={`/settings/objects/${objectSlug}/new-field/step-2`}
/>
</StyledSection>
</SettingsPageContainer>
@@ -82,13 +82,9 @@ export const SettingsObjects = () => {
stroke={theme.icon.stroke.sm}
/>
}
onClick={() =>
navigate(
`/settings/objects/${getObjectSlug(
activeObjectMetadataItem,
)}`,
)
}
to={`/settings/objects/${getObjectSlug(
activeObjectMetadataItem,
)}`}
/>
))}
</TableSection>
@@ -1,4 +1,3 @@
import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { IconPlus, IconSettings } from 'twenty-ui';
@@ -20,8 +19,6 @@ const StyledButtonContainer = styled.div`
`;
export const SettingsDevelopers = () => {
const navigate = useNavigate();
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
@@ -41,9 +38,7 @@ export const SettingsDevelopers = () => {
title="Create API key"
size="small"
variant="secondary"
onClick={() => {
navigate('/settings/developers/api-keys/new');
}}
to={'/settings/developers/api-keys/new'}
/>
</StyledButtonContainer>
</Section>
@@ -59,9 +54,7 @@ export const SettingsDevelopers = () => {
title="Create Webhook"
size="small"
variant="secondary"
onClick={() => {
navigate('/settings/developers/webhooks/new');
}}
to={'/settings/developers/webhooks/new'}
/>
</StyledButtonContainer>
</Section>
@@ -28,7 +28,7 @@ export const SettingsDevelopersApiKeysNew = () => {
name: string;
expirationDate: number | null;
}>({
expirationDate: EXPIRATION_DATES[0].value,
expirationDate: EXPIRATION_DATES[5].value,
name: '',
});
@@ -1,4 +1,6 @@
import { MouseEvent, ReactNode } from 'react';
import { Link } from 'react-router-dom';
import isPropValid from '@emotion/is-prop-valid';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
@@ -33,17 +35,22 @@ type ChipProps = {
rightComponent?: ReactNode;
className?: string;
onClick?: (event: MouseEvent<HTMLDivElement>) => void;
to?: string;
};
const StyledContainer = styled.div<
const StyledContainer = styled('div', {
shouldForwardProp: (prop) =>
!['clickable', 'maxWidth'].includes(prop) && isPropValid(prop),
})<
Pick<
ChipProps,
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant'
'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant' | 'to'
>
>`
--chip-horizontal-padding: ${({ theme }) => theme.spacing(1)};
--chip-vertical-padding: ${({ theme }) => theme.spacing(1)};
text-decoration: none;
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme, disabled }) =>
@@ -158,22 +165,27 @@ export const Chip = ({
maxWidth,
className,
onClick,
}: ChipProps) => (
<StyledContainer
data-testid="chip"
clickable={clickable}
variant={variant}
accent={accent}
size={size}
disabled={disabled}
className={className}
maxWidth={maxWidth}
onClick={onClick}
>
{leftComponent}
<StyledLabel>
<OverflowingTextWithTooltip text={label} />
</StyledLabel>
{rightComponent}
</StyledContainer>
);
to,
}: ChipProps) => {
return (
<StyledContainer
data-testid="chip"
clickable={clickable}
variant={variant}
accent={accent}
size={size}
disabled={disabled}
className={className}
maxWidth={maxWidth}
onClick={onClick}
as={to ? Link : 'div'}
to={to ? to : undefined}
>
{leftComponent}
<StyledLabel>
<OverflowingTextWithTooltip text={label} />
</StyledLabel>
{rightComponent}
</StyledContainer>
);
};
@@ -1,5 +1,4 @@
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { isNonEmptyString } from '@sniptt/guards';
@@ -37,14 +36,11 @@ export const EntityChip = ({
className,
maxWidth,
}: EntityChipProps) => {
const navigate = useNavigate();
const theme = useTheme();
const handleLinkClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (isNonEmptyString(linkToEntity)) {
event.preventDefault();
event.stopPropagation();
navigate(linkToEntity);
}
};
@@ -75,6 +71,7 @@ export const EntityChip = ({
onClick={handleLinkClick}
className={className}
maxWidth={maxWidth}
to={linkToEntity}
/>
);
};