mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 23:37:21 +03:00
Create ESLint rule to discourage usage of navigate() and prefer Link (#5642)
### Description Create ESLint rule to discourage usage of navigate() and prefer Link ### Refs #5468 ### Demo ![Capture-2024-05-29-112852](https://github.com/twentyhq/twenty/assets/140154534/28378c09-86bb-49d3-9e9a-49aa1c07ad11) ![Capture-2024-05-29-112843](https://github.com/twentyhq/twenty/assets/140154534/2c05ea92-e19b-49ae-acb9-07f6ec9182ab) Fixes #5468 --------- Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com> Co-authored-by: v1b3m <vibenjamin6@gmail.com> Co-authored-by: Matheus <matheus_benini@hotmail.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
parent
234e062232
commit
bb7d94a455
@ -47,6 +47,7 @@ module.exports = {
|
|||||||
'@nx/workspace-explicit-boolean-predicates-in-if': 'error',
|
'@nx/workspace-explicit-boolean-predicates-in-if': 'error',
|
||||||
'@nx/workspace-use-getLoadable-and-getValue-to-get-atoms': 'error',
|
'@nx/workspace-use-getLoadable-and-getValue-to-get-atoms': 'error',
|
||||||
'@nx/workspace-useRecoilCallback-has-dependency-array': 'error',
|
'@nx/workspace-useRecoilCallback-has-dependency-array': 'error',
|
||||||
|
'@nx/workspace-no-navigate-prefer-link': 'error',
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'react/jsx-key': 'off',
|
'react/jsx-key': 'off',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
import {
|
import {
|
||||||
IconBaselineDensitySmall,
|
IconBaselineDensitySmall,
|
||||||
@ -27,6 +26,7 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu
|
|||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate';
|
import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate';
|
||||||
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
|
import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle';
|
||||||
@ -50,8 +50,6 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
}: RecordIndexOptionsDropdownContentProps) => {
|
}: RecordIndexOptionsDropdownContentProps) => {
|
||||||
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
|
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
|
||||||
|
|
||||||
const [currentMenu, setCurrentMenu] = useState<
|
const [currentMenu, setCurrentMenu] = useState<
|
||||||
@ -68,13 +66,9 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
objectNameSingular: objectNameSingular,
|
objectNameSingular: objectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleEditClick = () => {
|
const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
|
||||||
navigate(
|
|
||||||
getSettingsPagePath(SettingsPath.ObjectDetail, {
|
|
||||||
objectSlug: objectNamePlural,
|
objectSlug: objectNamePlural,
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape],
|
[Key.Escape],
|
||||||
@ -194,13 +188,12 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<UndecoratedLink to={settingsUrl}>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
<MenuItem
|
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
|
||||||
onClick={handleEditClick}
|
|
||||||
LeftIcon={IconSettings}
|
|
||||||
text="Edit Fields"
|
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
|
</UndecoratedLink>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { H2Title, IconCalendarEvent, IconMailCog } from 'twenty-ui';
|
import { H2Title, IconCalendarEvent, IconMailCog } from 'twenty-ui';
|
||||||
|
|
||||||
@ -6,6 +6,7 @@ import { SettingsNavigationCard } from '@/settings/components/SettingsNavigation
|
|||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
|
||||||
const StyledCardsContainer = styled.div`
|
const StyledCardsContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -14,8 +15,6 @@ const StyledCardsContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsSettingsSection = () => {
|
export const SettingsAccountsSettingsSection = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
@ -23,24 +22,16 @@ export const SettingsAccountsSettingsSection = () => {
|
|||||||
description="Configure your emails and calendar settings."
|
description="Configure your emails and calendar settings."
|
||||||
/>
|
/>
|
||||||
<StyledCardsContainer>
|
<StyledCardsContainer>
|
||||||
<SettingsNavigationCard
|
<UndecoratedLink to={getSettingsPagePath(SettingsPath.AccountsEmails)}>
|
||||||
Icon={IconMailCog}
|
<SettingsNavigationCard Icon={IconMailCog} title="Emails">
|
||||||
title="Emails"
|
|
||||||
onClick={() =>
|
|
||||||
navigate(getSettingsPagePath(SettingsPath.AccountsEmails))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Set email visibility, manage your blocklist and more.
|
Set email visibility, manage your blocklist and more.
|
||||||
</SettingsNavigationCard>
|
</SettingsNavigationCard>
|
||||||
<SettingsNavigationCard
|
</UndecoratedLink>
|
||||||
Icon={IconCalendarEvent}
|
<Link to={getSettingsPagePath(SettingsPath.AccountsCalendars)}>
|
||||||
title="Calendar"
|
<SettingsNavigationCard Icon={IconCalendarEvent} title="Calendar">
|
||||||
onClick={() =>
|
|
||||||
navigate(getSettingsPagePath(SettingsPath.AccountsCalendars))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Configure and customize your calendar preferences.
|
Configure and customize your calendar preferences.
|
||||||
</SettingsNavigationCard>
|
</SettingsNavigationCard>
|
||||||
|
</Link>
|
||||||
</StyledCardsContainer>
|
</StyledCardsContainer>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
|
@ -27,10 +27,6 @@ export const SettingsIntegrationDatabaseConnectionShowContainer = () => {
|
|||||||
navigate(`${settingsIntegrationsPagePath}/${databaseKey}`);
|
navigate(`${settingsIntegrationsPagePath}/${databaseKey}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onEdit = () => {
|
|
||||||
navigate('./edit');
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsIntegrationsPagePath = getSettingsPagePath(
|
const settingsIntegrationsPagePath = getSettingsPagePath(
|
||||||
SettingsPath.Integrations,
|
SettingsPath.Integrations,
|
||||||
);
|
);
|
||||||
@ -57,7 +53,6 @@ export const SettingsIntegrationDatabaseConnectionShowContainer = () => {
|
|||||||
connectionId={connection.id}
|
connectionId={connection.id}
|
||||||
connectionLabel={connection.label}
|
connectionLabel={connection.label}
|
||||||
onRemove={deleteConnection}
|
onRemove={deleteConnection}
|
||||||
onEdit={onEdit}
|
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
|
@ -7,6 +7,7 @@ import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
|||||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
|
|
||||||
type SettingsIntegrationDatabaseConnectionSummaryCardProps = {
|
type SettingsIntegrationDatabaseConnectionSummaryCardProps = {
|
||||||
@ -14,7 +15,6 @@ type SettingsIntegrationDatabaseConnectionSummaryCardProps = {
|
|||||||
connectionId: string;
|
connectionId: string;
|
||||||
connectionLabel: string;
|
connectionLabel: string;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onEdit: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledDatabaseLogoContainer = styled.div`
|
const StyledDatabaseLogoContainer = styled.div`
|
||||||
@ -34,7 +34,6 @@ export const SettingsIntegrationDatabaseConnectionSummaryCard = ({
|
|||||||
connectionId,
|
connectionId,
|
||||||
connectionLabel,
|
connectionLabel,
|
||||||
onRemove,
|
onRemove,
|
||||||
onEdit,
|
|
||||||
}: SettingsIntegrationDatabaseConnectionSummaryCardProps) => {
|
}: SettingsIntegrationDatabaseConnectionSummaryCardProps) => {
|
||||||
const dropdownId =
|
const dropdownId =
|
||||||
'settings-integration-database-connection-summary-card-dropdown';
|
'settings-integration-database-connection-summary-card-dropdown';
|
||||||
@ -69,11 +68,9 @@ export const SettingsIntegrationDatabaseConnectionSummaryCard = ({
|
|||||||
text="Remove"
|
text="Remove"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
/>
|
/>
|
||||||
<MenuItem
|
<UndecoratedLink to="./edit">
|
||||||
LeftIcon={IconPencil}
|
<MenuItem LeftIcon={IconPencil} text="Edit" />
|
||||||
text="Edit"
|
</UndecoratedLink>
|
||||||
onClick={onEdit}
|
|
||||||
/>
|
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { ComponentProps, ReactNode } from 'react';
|
import { ComponentProps, ReactNode } from 'react';
|
||||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
@ -12,6 +11,7 @@ import {
|
|||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
||||||
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
|
import { isNavigationDrawerOpenState } from '@/ui/navigation/states/isNavigationDrawerOpenState';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
@ -103,7 +103,6 @@ export const PageHeader = ({
|
|||||||
loading,
|
loading,
|
||||||
}: PageHeaderProps) => {
|
}: PageHeaderProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
|
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
|
||||||
|
|
||||||
@ -116,12 +115,13 @@ export const PageHeader = ({
|
|||||||
</StyledTopBarButtonContainer>
|
</StyledTopBarButtonContainer>
|
||||||
)}
|
)}
|
||||||
{hasBackButton && (
|
{hasBackButton && (
|
||||||
|
<UndecoratedLink to={-1}>
|
||||||
<IconButton
|
<IconButton
|
||||||
Icon={IconChevronLeft}
|
Icon={IconChevronLeft}
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => navigate(-1)}
|
|
||||||
variant="tertiary"
|
variant="tertiary"
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
)}
|
)}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<StyledSkeletonLoader />
|
<StyledSkeletonLoader />
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledUndecoratedLink = styled(Link)`
|
||||||
|
text-decoration: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type UndecoratedLinkProps = {
|
||||||
|
to: string | number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
replace?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UndecoratedLink = ({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
replace = false,
|
||||||
|
}: UndecoratedLinkProps) => {
|
||||||
|
return (
|
||||||
|
<StyledUndecoratedLink to={to as string} replace={replace}>
|
||||||
|
{children}
|
||||||
|
</StyledUndecoratedLink>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,32 @@
|
|||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { userEvent, within } from '@storybook/test';
|
||||||
|
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||||
|
|
||||||
|
const meta: Meta<typeof UndecoratedLink> = {
|
||||||
|
title: 'UI/navigation/link/UndecoratedLink',
|
||||||
|
component: UndecoratedLink,
|
||||||
|
decorators: [ComponentWithRouterDecorator],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof UndecoratedLink>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Go Home',
|
||||||
|
to: '/home',
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const link = canvas.getByText('Go Home');
|
||||||
|
|
||||||
|
await userEvent.click(link);
|
||||||
|
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
expect(href).toBe('/home');
|
||||||
|
},
|
||||||
|
};
|
@ -1,9 +1,9 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { IconChevronLeft } from 'twenty-ui';
|
import { IconChevronLeft } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
|
||||||
type NavigationDrawerBackButtonProps = {
|
type NavigationDrawerBackButtonProps = {
|
||||||
@ -36,22 +36,19 @@ export const NavigationDrawerBackButton = ({
|
|||||||
title,
|
title,
|
||||||
}: NavigationDrawerBackButtonProps) => {
|
}: NavigationDrawerBackButtonProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
|
||||||
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
|
const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledIconAndButtonContainer
|
<UndecoratedLink to={navigationMemorizedUrl} replace>
|
||||||
onClick={() => {
|
<StyledIconAndButtonContainer>
|
||||||
navigate(navigationMemorizedUrl, { replace: true });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconChevronLeft
|
<IconChevronLeft
|
||||||
size={theme.icon.size.md}
|
size={theme.icon.size.md}
|
||||||
stroke={theme.icon.stroke.lg}
|
stroke={theme.icon.stroke.lg}
|
||||||
/>
|
/>
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</StyledIconAndButtonContainer>
|
</StyledIconAndButtonContainer>
|
||||||
|
</UndecoratedLink>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { useAuthorizeAppMutation } from '~/generated/graphql';
|
import { useAuthorizeAppMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -115,12 +116,9 @@ export const Authorize = () => {
|
|||||||
</StyledAppsContainer>
|
</StyledAppsContainer>
|
||||||
<StyledText>{app?.name} wants to access your account</StyledText>
|
<StyledText>{app?.name} wants to access your account</StyledText>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<MainButton
|
<UndecoratedLink to={AppPath.Index}>
|
||||||
title="Cancel"
|
<MainButton title="Cancel" variant="secondary" fullWidth />
|
||||||
variant="secondary"
|
</UndecoratedLink>
|
||||||
onClick={() => navigate(AppPath.Index)}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<MainButton title="Authorize" onClick={handleAuthorize} fullWidth />
|
<MainButton title="Authorize" onClick={handleAuthorize} fullWidth />
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</StyledCardWrapper>
|
</StyledCardWrapper>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconCheck, RGBA } from 'twenty-ui';
|
import { IconCheck, RGBA } from 'twenty-ui';
|
||||||
@ -8,6 +7,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
|
|||||||
import { Title } from '@/auth/components/Title';
|
import { Title } from '@/auth/components/Title';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
|
|
||||||
const StyledCheckContainer = styled.div`
|
const StyledCheckContainer = styled.div`
|
||||||
@ -28,11 +28,7 @@ const StyledButtonContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const PaymentSuccess = () => {
|
export const PaymentSuccess = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const handleButtonClick = () => {
|
|
||||||
navigate(AppPath.CreateWorkspace);
|
|
||||||
};
|
|
||||||
const color =
|
const color =
|
||||||
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
|
theme.name === 'light' ? theme.grayScale.gray90 : theme.grayScale.gray10;
|
||||||
return (
|
return (
|
||||||
@ -45,7 +41,9 @@ export const PaymentSuccess = () => {
|
|||||||
<Title>All set!</Title>
|
<Title>All set!</Title>
|
||||||
<SubTitle>Your account has been activated.</SubTitle>
|
<SubTitle>Your account has been activated.</SubTitle>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<MainButton title="Start" onClick={handleButtonClick} width={200} />
|
<UndecoratedLink to={AppPath.CreateWorkspace}>
|
||||||
|
<MainButton title="Start" width={200} />
|
||||||
|
</UndecoratedLink>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
|
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
|
||||||
@ -11,6 +10,7 @@ import {
|
|||||||
AnimatedPlaceholderErrorSubTitle,
|
AnimatedPlaceholderErrorSubTitle,
|
||||||
AnimatedPlaceholderErrorTitle,
|
AnimatedPlaceholderErrorTitle,
|
||||||
} from '@/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled';
|
} from '@/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||||
|
|
||||||
const StyledBackDrop = styled.div`
|
const StyledBackDrop = styled.div`
|
||||||
@ -33,8 +33,6 @@ const StyledButtonContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle title="Page Not Found | Twenty" />
|
<PageTitle title="Page Not Found | Twenty" />
|
||||||
@ -51,11 +49,9 @@ export const NotFound = () => {
|
|||||||
</AnimatedPlaceholderErrorSubTitle>
|
</AnimatedPlaceholderErrorSubTitle>
|
||||||
</AnimatedPlaceholderEmptyTextContainer>
|
</AnimatedPlaceholderEmptyTextContainer>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<MainButton
|
<UndecoratedLink to={AppPath.Index}>
|
||||||
title="Back to content"
|
<MainButton title="Back to content" fullWidth />
|
||||||
fullWidth
|
</UndecoratedLink>
|
||||||
onClick={() => navigate(AppPath.Index)}
|
|
||||||
/>
|
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</AnimatedPlaceholderErrorContainer>
|
</AnimatedPlaceholderErrorContainer>
|
||||||
</StyledBackDrop>
|
</StyledBackDrop>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
@ -25,6 +24,7 @@ import { Button } from '@/ui/input/button/components/Button';
|
|||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
import {
|
import {
|
||||||
useBillingPortalSessionQuery,
|
useBillingPortalSessionQuery,
|
||||||
useUpdateBillingSubscriptionMutation,
|
useUpdateBillingSubscriptionMutation,
|
||||||
@ -66,7 +66,6 @@ const SWITCH_INFOS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsBilling = () => {
|
export const SettingsBilling = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const onboardingStatus = useOnboardingStatus();
|
const onboardingStatus = useOnboardingStatus();
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
@ -140,10 +139,6 @@ export const SettingsBilling = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const redirectToSubscribePage = () => {
|
|
||||||
navigate(AppPath.PlanRequired);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconCurrencyDollar} title="Billing">
|
<SubMenuTopBarContainer Icon={IconCurrencyDollar} title="Billing">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
@ -158,20 +153,22 @@ export const SettingsBilling = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{displaySubscriptionCanceledInfo && (
|
{displaySubscriptionCanceledInfo && (
|
||||||
|
<UndecoratedLink to={AppPath.PlanRequired}>
|
||||||
<Info
|
<Info
|
||||||
text={'Subscription canceled. Please start a new one'}
|
text={'Subscription canceled. Please start a new one'}
|
||||||
buttonTitle={'Subscribe'}
|
buttonTitle={'Subscribe'}
|
||||||
accent={'danger'}
|
accent={'danger'}
|
||||||
onClick={redirectToSubscribePage}
|
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
)}
|
)}
|
||||||
{displaySubscribeInfo ? (
|
{displaySubscribeInfo ? (
|
||||||
|
<UndecoratedLink to={AppPath.PlanRequired}>
|
||||||
<Info
|
<Info
|
||||||
text={'Your workspace does not have an active subscription'}
|
text={'Your workspace does not have an active subscription'}
|
||||||
buttonTitle={'Subscribe'}
|
buttonTitle={'Subscribe'}
|
||||||
accent={'danger'}
|
accent={'danger'}
|
||||||
onClick={redirectToSubscribePage}
|
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Section>
|
<Section>
|
||||||
|
@ -31,6 +31,7 @@ import { Table } from '@/ui/layout/table/components/Table';
|
|||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
|
||||||
const StyledDiv = styled.div`
|
const StyledDiv = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -214,19 +215,20 @@ export const SettingsObjectDetail = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
{shouldDisplayAddFieldButton && (
|
{shouldDisplayAddFieldButton && (
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
|
<UndecoratedLink
|
||||||
|
to={
|
||||||
|
deactivatedMetadataFields.length
|
||||||
|
? './new-field/step-1'
|
||||||
|
: './new-field/step-2'
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconPlus}
|
Icon={IconPlus}
|
||||||
title="Add Field"
|
title="Add Field"
|
||||||
size="small"
|
size="small"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
deactivatedMetadataFields.length
|
|
||||||
? './new-field/step-1'
|
|
||||||
: './new-field/step-2',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
</StyledDiv>
|
</StyledDiv>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import {
|
||||||
@ -29,6 +28,7 @@ import { Section } from '@/ui/layout/section/components/Section';
|
|||||||
import { Table } from '@/ui/layout/table/components/Table';
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
import { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
|
||||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
@ -40,7 +40,6 @@ const StyledH1Title = styled(H1Title)`
|
|||||||
|
|
||||||
export const SettingsObjects = () => {
|
export const SettingsObjects = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
const { activeObjectMetadataItems, inactiveObjectMetadataItems } =
|
||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
@ -52,15 +51,14 @@ export const SettingsObjects = () => {
|
|||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<StyledH1Title title="Objects" />
|
<StyledH1Title title="Objects" />
|
||||||
|
<UndecoratedLink to={getSettingsPagePath(SettingsPath.NewObject)}>
|
||||||
<Button
|
<Button
|
||||||
Icon={IconPlus}
|
Icon={IconPlus}
|
||||||
title="Add object"
|
title="Add object"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() =>
|
|
||||||
navigate(getSettingsPagePath(SettingsPath.NewObject))
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<div>
|
<div>
|
||||||
<SettingsObjectCoverImage />
|
<SettingsObjectCoverImage />
|
||||||
|
@ -22,6 +22,10 @@ import {
|
|||||||
rule as noHardcodedColors,
|
rule as noHardcodedColors,
|
||||||
RULE_NAME as noHardcodedColorsName,
|
RULE_NAME as noHardcodedColorsName,
|
||||||
} from './rules/no-hardcoded-colors';
|
} from './rules/no-hardcoded-colors';
|
||||||
|
import {
|
||||||
|
rule as noNavigatePreferLink,
|
||||||
|
RULE_NAME as noNavigatePreferLinkName,
|
||||||
|
} from './rules/no-navigate-prefer-link';
|
||||||
import {
|
import {
|
||||||
rule as noStateUseref,
|
rule as noStateUseref,
|
||||||
RULE_NAME as noStateUserefName,
|
RULE_NAME as noStateUserefName,
|
||||||
@ -83,5 +87,6 @@ module.exports = {
|
|||||||
[maxConstsPerFileName]: maxConstsPerFile,
|
[maxConstsPerFileName]: maxConstsPerFile,
|
||||||
[useRecoilCallbackHasDependencyArrayName]:
|
[useRecoilCallbackHasDependencyArrayName]:
|
||||||
useRecoilCallbackHasDependencyArray,
|
useRecoilCallbackHasDependencyArray,
|
||||||
|
[noNavigatePreferLinkName]: noNavigatePreferLink,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
59
tools/eslint-rules/rules/no-navigate-prefer-link.spec.ts
Normal file
59
tools/eslint-rules/rules/no-navigate-prefer-link.spec.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { TSESLint } from '@typescript-eslint/utils';
|
||||||
|
|
||||||
|
import { rule, RULE_NAME } from './no-navigate-prefer-link';
|
||||||
|
|
||||||
|
const ruleTester = new TSESLint.RuleTester({
|
||||||
|
parser: require.resolve('@typescript-eslint/parser'),
|
||||||
|
});
|
||||||
|
|
||||||
|
ruleTester.run(RULE_NAME, rule, {
|
||||||
|
valid: [
|
||||||
|
{
|
||||||
|
code: 'if(someVar) { navigate("/"); }',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<Link to="/"><Button>Click me</Button></Link>',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<Button onClick={() =>{ navigate("/"); doSomething(); }} />',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
{
|
||||||
|
code: '<Button onClick={() => navigate("/")} />',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'preferLink',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: '<Button onClick={() => { navigate("/");} } />',
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
messageId: 'preferLink',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
101
tools/eslint-rules/rules/no-navigate-prefer-link.ts
Normal file
101
tools/eslint-rules/rules/no-navigate-prefer-link.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
|
||||||
|
|
||||||
|
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-no-navigate-prefer-link"
|
||||||
|
export const RULE_NAME = 'no-navigate-prefer-link';
|
||||||
|
|
||||||
|
export const rule = ESLintUtils.RuleCreator(() => __filename)({
|
||||||
|
name: RULE_NAME,
|
||||||
|
meta: {
|
||||||
|
type: 'suggestion',
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
'Discourage usage of navigate() where a simple <Link> component would suffice.',
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
preferLink: 'Use <Link> instead of navigate() for pure navigation.',
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
},
|
||||||
|
defaultOptions: [],
|
||||||
|
create: (context) => {
|
||||||
|
const functionMap: Record<string, TSESTree.ArrowFunctionExpression> = {};
|
||||||
|
|
||||||
|
const checkFunctionBodyHasSingleNavigateCall = (
|
||||||
|
func: TSESTree.ArrowFunctionExpression,
|
||||||
|
) => {
|
||||||
|
// Check for simple arrow function with single navigate call
|
||||||
|
if (
|
||||||
|
func.body.type === 'CallExpression' &&
|
||||||
|
func.body.callee.type === 'Identifier' &&
|
||||||
|
func.body.callee.name === 'navigate'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for block arrow function with single navigate call
|
||||||
|
if (
|
||||||
|
func.body.type === 'BlockStatement' &&
|
||||||
|
func.body.body.length === 1 &&
|
||||||
|
func.body.body[0].type === 'ExpressionStatement' &&
|
||||||
|
func.body.body[0].expression.type === 'CallExpression' &&
|
||||||
|
func.body.body[0].expression.callee.type === 'Identifier' &&
|
||||||
|
func.body.body[0].expression.callee.name === 'navigate'
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
VariableDeclarator: (node) => {
|
||||||
|
// Check for function declaration on onClick
|
||||||
|
if (
|
||||||
|
node.init &&
|
||||||
|
node.init.type === 'ArrowFunctionExpression' &&
|
||||||
|
node.id.type === 'Identifier'
|
||||||
|
) {
|
||||||
|
const func = node.init;
|
||||||
|
functionMap[node.id.name] = func;
|
||||||
|
|
||||||
|
if (checkFunctionBodyHasSingleNavigateCall(func)) {
|
||||||
|
context.report({
|
||||||
|
node: func,
|
||||||
|
messageId: 'preferLink',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
JSXAttribute: (node) => {
|
||||||
|
// Check for navigate call directly on onClick
|
||||||
|
if (
|
||||||
|
node.name.name === 'onClick' &&
|
||||||
|
node.value.type === 'JSXExpressionContainer'
|
||||||
|
) {
|
||||||
|
const expression = node.value.expression;
|
||||||
|
|
||||||
|
if (
|
||||||
|
expression.type === 'ArrowFunctionExpression' &&
|
||||||
|
checkFunctionBodyHasSingleNavigateCall(expression)
|
||||||
|
) {
|
||||||
|
context.report({
|
||||||
|
node: expression,
|
||||||
|
messageId: 'preferLink',
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
expression.type === 'Identifier' &&
|
||||||
|
functionMap[expression.name]
|
||||||
|
) {
|
||||||
|
const func = functionMap[expression.name];
|
||||||
|
if (checkFunctionBodyHasSingleNavigateCall(func)) {
|
||||||
|
context.report({
|
||||||
|
node: expression,
|
||||||
|
messageId: 'preferLink',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user