Enforce front project structure through ESLINT (#7863)
Fixes: https://github.com/twentyhq/twenty/issues/7329
This commit is contained in:
@ -0,0 +1,31 @@
|
||||
import { css, Global, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
scrollbar-width: 4px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const BlankLayout = () => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
styles={css`
|
||||
body {
|
||||
background: ${theme.background.tertiary};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<StyledLayout>
|
||||
<Outlet />
|
||||
</StyledLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,120 @@
|
||||
import { AuthModal } from '@/auth/components/AuthModal';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||
import { AppNavigationDrawer } from '@/navigation/components/AppNavigationDrawer';
|
||||
import { MobileNavigationBar } from '@/navigation/components/MobileNavigationBar';
|
||||
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage';
|
||||
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
|
||||
import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize';
|
||||
import { css, Global, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
scrollbar-color: ${({ theme }) => theme.border.color.medium};
|
||||
scrollbar-width: 4px;
|
||||
width: 100%;
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledPageContainer = styled(motion.div)`
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const StyledAppNavigationDrawer = styled(AppNavigationDrawer)`
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 0 1 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const DefaultLayout = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isSettingsPage = useIsSettingsPage();
|
||||
const theme = useTheme();
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global
|
||||
styles={css`
|
||||
body {
|
||||
background: ${theme.background.tertiary};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<StyledLayout>
|
||||
<CommandMenu />
|
||||
<KeyboardShortcutMenu />
|
||||
|
||||
<StyledPageContainer
|
||||
animate={{
|
||||
marginLeft:
|
||||
isSettingsPage && !isMobile
|
||||
? (windowsWidth -
|
||||
(OBJECT_SETTINGS_WIDTH +
|
||||
NAV_DRAWER_WIDTHS.menu.desktop.expanded +
|
||||
64)) /
|
||||
2
|
||||
: 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
<StyledAppNavigationDrawer />
|
||||
<StyledMainContainer>
|
||||
{showAuthModal ? (
|
||||
<>
|
||||
<SignInBackgroundMockPage />
|
||||
<AnimatePresence mode="wait">
|
||||
<LayoutGroup>
|
||||
<AuthModal>
|
||||
<Outlet />
|
||||
</AuthModal>
|
||||
</LayoutGroup>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
) : (
|
||||
<AppErrorBoundary>
|
||||
<Outlet />
|
||||
</AppErrorBoundary>
|
||||
)}
|
||||
</StyledMainContainer>
|
||||
</StyledPageContainer>
|
||||
{isMobile && <MobileNavigationBar />}
|
||||
</StyledLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
|
||||
type PageAddButtonProps = {
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const PageAddButton = ({ onClick }: PageAddButtonProps) => (
|
||||
<IconButton
|
||||
Icon={IconPlus}
|
||||
dataTestId="add-button"
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
onClick={onClick}
|
||||
ariaLabel="Add"
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,50 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer';
|
||||
|
||||
import { PagePanel } from './PagePanel';
|
||||
|
||||
type PageBodyProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-height: 0;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type LeftContainerProps = {
|
||||
isRightDrawerOpen?: boolean;
|
||||
};
|
||||
|
||||
const StyledLeftContainer = styled.div<LeftContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const PageBody = ({ children }: PageBodyProps) => (
|
||||
<StyledMainContainer>
|
||||
<StyledLeftContainer>
|
||||
<PagePanel>{children}</PagePanel>
|
||||
</StyledLeftContainer>
|
||||
<RightDrawer />
|
||||
</StyledMainContainer>
|
||||
);
|
||||
@ -0,0 +1,9 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export { StyledContainer as PageContainer };
|
||||
@ -0,0 +1,22 @@
|
||||
import { IconHeart } from 'twenty-ui';
|
||||
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
|
||||
type PageFavoriteButtonProps = {
|
||||
isFavorite: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export const PageFavoriteButton = ({
|
||||
isFavorite,
|
||||
onClick,
|
||||
}: PageFavoriteButtonProps) => (
|
||||
<IconButton
|
||||
Icon={IconHeart}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
data-testid="add-button"
|
||||
accent={isFavorite ? 'danger' : 'default'}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,165 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactNode } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconComponent,
|
||||
IconX,
|
||||
MOBILE_VIEWPORT,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton';
|
||||
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
export const PAGE_BAR_MIN_HEIGHT = 40;
|
||||
|
||||
const StyledTopBarContainer = styled.div<{ width?: number }>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
justify-content: space-between;
|
||||
min-height: ${PAGE_BAR_MIN_HEIGHT}px;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: 0;
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
width: ${({ width }) => width + 'px' || '100%'};
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLeftContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTopBarIconStyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledPageActionContainer = styled.div`
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTopBarButtonContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type PageHeaderProps = {
|
||||
title: ReactNode;
|
||||
hasClosePageButton?: boolean;
|
||||
onClosePage?: () => void;
|
||||
hasPaginationButtons?: boolean;
|
||||
hasPreviousRecord?: boolean;
|
||||
hasNextRecord?: boolean;
|
||||
navigateToPreviousRecord?: () => void;
|
||||
navigateToNextRecord?: () => void;
|
||||
Icon?: IconComponent;
|
||||
children?: ReactNode;
|
||||
width?: number;
|
||||
};
|
||||
|
||||
export const PageHeader = ({
|
||||
title,
|
||||
hasClosePageButton,
|
||||
onClosePage,
|
||||
hasPaginationButtons,
|
||||
hasPreviousRecord,
|
||||
hasNextRecord,
|
||||
navigateToPreviousRecord,
|
||||
navigateToNextRecord,
|
||||
Icon,
|
||||
children,
|
||||
width,
|
||||
}: PageHeaderProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const theme = useTheme();
|
||||
const isNavigationDrawerExpanded = useRecoilValue(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledTopBarContainer width={width}>
|
||||
<StyledLeftContainer>
|
||||
{!isMobile && !isNavigationDrawerExpanded && (
|
||||
<StyledTopBarButtonContainer>
|
||||
<NavigationDrawerCollapseButton direction="right" />
|
||||
</StyledTopBarButtonContainer>
|
||||
)}
|
||||
{hasClosePageButton && (
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={() => onClosePage?.()}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledTopBarIconStyledTitleContainer>
|
||||
{hasPaginationButtons && (
|
||||
<>
|
||||
<IconButton
|
||||
Icon={IconChevronUp}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
disabled={!hasPreviousRecord}
|
||||
onClick={() => navigateToPreviousRecord?.()}
|
||||
/>
|
||||
<IconButton
|
||||
Icon={IconChevronDown}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
disabled={!hasNextRecord}
|
||||
onClick={() => navigateToNextRecord?.()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{Icon && <Icon size={theme.icon.size.md} />}
|
||||
<StyledTitleContainer data-testid="top-bar-title">
|
||||
{typeof title === 'string' ? (
|
||||
<OverflowingTextWithTooltip text={title} />
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
</StyledTopBarIconStyledTitleContainer>
|
||||
</StyledLeftContainer>
|
||||
<StyledPageActionContainer>{children}</StyledPageActionContainer>
|
||||
</StyledTopBarContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
type PageHotkeysEffectProps = {
|
||||
onAddButtonClick?: () => void;
|
||||
};
|
||||
|
||||
export const PageHotkeysEffect = ({
|
||||
onAddButtonClick,
|
||||
}: PageHotkeysEffectProps) => {
|
||||
useScopedHotkeys('c', () => onAddButtonClick?.(), TableHotkeyScope.Table, [
|
||||
onAddButtonClick,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
|
||||
const StyledPanel = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type PagePanelProps = {
|
||||
children: React.ReactNode;
|
||||
hasInformationBar?: boolean;
|
||||
};
|
||||
|
||||
export const PagePanel = ({ children }: PagePanelProps) => (
|
||||
<StyledPanel>{children}</StyledPanel>
|
||||
);
|
||||
@ -0,0 +1,52 @@
|
||||
import { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer';
|
||||
|
||||
import { PagePanel } from './PagePanel';
|
||||
|
||||
type RightDrawerContainerProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
background: ${({ theme }) => theme.background.noisy};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-height: 0;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
padding-left: 0;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
padding-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
type LeftContainerProps = {
|
||||
isRightDrawerOpen?: boolean;
|
||||
};
|
||||
|
||||
const StyledLeftContainer = styled.div<LeftContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RightDrawerContainer = ({
|
||||
children,
|
||||
}: RightDrawerContainerProps) => (
|
||||
<StyledMainContainer>
|
||||
<StyledLeftContainer>
|
||||
<PagePanel>{children}</PagePanel>
|
||||
</StyledLeftContainer>
|
||||
<RightDrawer />
|
||||
</StyledMainContainer>
|
||||
);
|
||||
@ -0,0 +1,43 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
const StyledOuterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInnerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: ${() => (useIsMobile() ? 'column' : 'row')};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const StyledScrollWrapper = styled(ScrollWrapper)`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
`;
|
||||
|
||||
export type ShowPageContainerProps = {
|
||||
children: ReactElement[] | ReactElement;
|
||||
};
|
||||
|
||||
export const ShowPageContainer = ({ children }: ShowPageContainerProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
return isMobile ? (
|
||||
<StyledOuterContainer>
|
||||
<StyledScrollWrapper contextProviderName="showPageContainer">
|
||||
<StyledInnerContainer>{children}</StyledInnerContainer>
|
||||
</StyledScrollWrapper>
|
||||
</StyledOuterContainer>
|
||||
) : (
|
||||
<StyledOuterContainer>
|
||||
<StyledInnerContainer>{children}</StyledInnerContainer>
|
||||
</StyledOuterContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,53 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { JSX, ReactNode } from 'react';
|
||||
|
||||
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbProps,
|
||||
} from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import { PageBody } from './PageBody';
|
||||
import { PageHeader } from './PageHeader';
|
||||
|
||||
type SubMenuTopBarContainerProps = {
|
||||
children: JSX.Element | JSX.Element[];
|
||||
title?: string;
|
||||
actionButton?: ReactNode;
|
||||
className?: string;
|
||||
links: BreadcrumbProps['links'];
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: 1.2;
|
||||
margin: ${({ theme }) => theme.spacing(8, 8, 2)};
|
||||
`;
|
||||
|
||||
export const SubMenuTopBarContainer = ({
|
||||
children,
|
||||
title,
|
||||
actionButton,
|
||||
className,
|
||||
links,
|
||||
}: SubMenuTopBarContainerProps) => {
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<PageHeader title={<Breadcrumb links={links} />}>
|
||||
{actionButton}
|
||||
</PageHeader>
|
||||
<PageBody>
|
||||
<InformationBannerWrapper />
|
||||
{title && <StyledTitle>{title}</StyledTitle>}
|
||||
{children}
|
||||
</PageBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user