Data settings new layout - anchor navigation (#8334)

Follow-up of https://github.com/twentyhq/twenty/pull/7979
Navigation between settings and fields tabs is now reflected in URL. 
<img width="1106" alt="Capture d’écran 2024-11-07 à 18 38 57"
src="https://github.com/user-attachments/assets/24b153ef-9e68-4aa2-8e3a-6bf70834c5ad">

---------

Co-authored-by: gitstart-twenty <gitstart-twenty@users.noreply.github.com>
Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com>
Co-authored-by: Weiko <corentin@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Marie
2024-11-11 14:06:38 +01:00
committed by GitHub
parent 9d6a850ee8
commit bec4da496d
13 changed files with 143 additions and 74 deletions

View File

@ -14,6 +14,7 @@ jobs:
ci: ci:
timeout-minutes: 10 timeout-minutes: 10
runs-on: ubuntu-latest runs-on: ubuntu-latest
uses: tinybirdco/ci/.github/workflows/ci.yml@main
steps: steps:
- name: Check for changed files - name: Check for changed files
id: changed-files id: changed-files
@ -28,7 +29,6 @@ jobs:
run: echo "No relevant changes. Skipping CI." run: echo "No relevant changes. Skipping CI."
- name: Check twenty-tinybird package - name: Check twenty-tinybird package
uses: tinybirdco/ci/.github/workflows/ci.yml@main
with: with:
data_project_dir: packages/twenty-tinybird data_project_dir: packages/twenty-tinybird
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}

View File

@ -1,5 +1,4 @@
name: CI Website name: CI Website
timeout-minutes: 10
on: on:
push: push:
branches: branches:

View File

@ -64,7 +64,9 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
{tabs.length > 1 && ( {tabs.length > 1 && (
<StyledCalenderContainer> <StyledCalenderContainer>
<TabList <TabList
tabListId={SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID} tabListInstanceId={
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID
}
tabs={tabs} tabs={tabs}
/> />
</StyledCalenderContainer> </StyledCalenderContainer>

View File

@ -63,7 +63,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
{tabs.length > 1 && ( {tabs.length > 1 && (
<StyledMessageContainer> <StyledMessageContainer>
<TabList <TabList
tabListId={SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID} tabListInstanceId={
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID
}
tabs={tabs} tabs={tabs}
/> />
</StyledMessageContainer> </StyledMessageContainer>

View File

@ -84,7 +84,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
const HeaderTabList = ( const HeaderTabList = (
<StyledTabList <StyledTabList
tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID} tabListInstanceId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
tabs={files tabs={files
.filter((file) => file.path !== '.env') .filter((file) => file.path !== '.env')
.map((file) => { .map((file) => {

View File

@ -131,8 +131,9 @@ export const ShowPageSubContainer = ({
<StyledShowPageRightContainer isMobile={isMobile}> <StyledShowPageRightContainer isMobile={isMobile}>
<StyledTabListContainer> <StyledTabListContainer>
<TabList <TabList
behaveAsLinks={!isInRightDrawer}
loading={loading || isNewViewableRecordLoading} loading={loading || isNewViewableRecordLoading}
tabListId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`} tabListInstanceId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`}
tabs={tabs} tabs={tabs}
/> />
</StyledTabListContainer> </StyledTabListContainer>

View File

@ -1,6 +1,7 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import { Link } from 'react-router-dom';
import { IconComponent, Pill } from 'twenty-ui'; import { IconComponent, Pill } from 'twenty-ui';
type TabProps = { type TabProps = {
@ -12,9 +13,14 @@ type TabProps = {
onClick?: () => void; onClick?: () => void;
disabled?: boolean; disabled?: boolean;
pill?: string | ReactElement; pill?: string | ReactElement;
to?: string;
}; };
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>` const StyledTab = styled.button<{
active?: boolean;
disabled?: boolean;
to?: string;
}>`
align-items: center; align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-color: ${({ theme, active }) => border-color: ${({ theme, active }) =>
@ -26,6 +32,10 @@ const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
? theme.font.color.light ? theme.font.color.light
: theme.font.color.secondary}; : theme.font.color.secondary};
cursor: pointer; cursor: pointer;
background-color: transparent;
border-left: none;
border-right: none;
border-top: none;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
@ -33,6 +43,7 @@ const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
margin-bottom: 0; margin-bottom: 0;
padding: ${({ theme }) => theme.spacing(2) + ' 0'}; padding: ${({ theme }) => theme.spacing(2) + ' 0'};
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')}; pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
text-decoration: none;
`; `;
const StyledHover = styled.span` const StyledHover = styled.span`
@ -61,6 +72,7 @@ export const Tab = ({
className, className,
disabled, disabled,
pill, pill,
to,
}: TabProps) => { }: TabProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -70,6 +82,8 @@ export const Tab = ({
className={className} className={className}
disabled={disabled} disabled={disabled}
data-testid={'tab-' + id} data-testid={'tab-' + id}
as={to ? Link : 'button'}
to={to}
> >
<StyledHover> <StyledHover>
{Icon && <Icon size={theme.icon.size.md} />} {Icon && <Icon size={theme.icon.size.md} />}

View File

@ -7,6 +7,7 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope'; import { TabListScope } from '@/ui/layout/tab/scopes/TabListScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { TabListFromUrlOptionalEffect } from '@/ui/layout/tab/components/TabListFromUrlOptionalEffect';
import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard'; import { LayoutCard } from '@/ui/layout/tab/types/LayoutCard';
import { Tab } from './Tab'; import { Tab } from './Tab';
@ -21,10 +22,11 @@ export type SingleTabProps = {
}; };
type TabListProps = { type TabListProps = {
tabListId: string; tabListInstanceId: string;
tabs: SingleTabProps[]; tabs: SingleTabProps[];
loading?: boolean; loading?: boolean;
className?: string; className?: string;
behaveAsLinks?: boolean;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -38,13 +40,14 @@ const StyledContainer = styled.div`
export const TabList = ({ export const TabList = ({
tabs, tabs,
tabListId, tabListInstanceId,
loading, loading,
className, className,
behaveAsLinks = true,
}: TabListProps) => { }: TabListProps) => {
const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || ''; const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || '';
const { activeTabIdState, setActiveTabId } = useTabList(tabListId); const { activeTabIdState, setActiveTabId } = useTabList(tabListInstanceId);
const activeTabId = useRecoilValue(activeTabIdState); const activeTabId = useRecoilValue(activeTabIdState);
@ -53,7 +56,11 @@ export const TabList = ({
}, [initialActiveTabId, setActiveTabId]); }, [initialActiveTabId, setActiveTabId]);
return ( return (
<TabListScope tabListScopeId={tabListId}> <TabListScope tabListScopeId={tabListInstanceId}>
<TabListFromUrlOptionalEffect
componentInstanceId={tabListInstanceId}
tabListIds={tabs.map((tab) => tab.id)}
/>
<ScrollWrapper enableYScroll={false} contextProviderName="tabList"> <ScrollWrapper enableYScroll={false} contextProviderName="tabList">
<StyledContainer className={className}> <StyledContainer className={className}>
{tabs {tabs
@ -65,11 +72,14 @@ export const TabList = ({
title={tab.title} title={tab.title}
Icon={tab.Icon} Icon={tab.Icon}
active={tab.id === activeTabId} active={tab.id === activeTabId}
onClick={() => {
setActiveTabId(tab.id);
}}
disabled={tab.disabled ?? loading} disabled={tab.disabled ?? loading}
pill={tab.pill} pill={tab.pill}
to={behaveAsLinks ? `#${tab.id}` : undefined}
onClick={() => {
if (!behaveAsLinks) {
setActiveTabId(tab.id);
}
}}
/> />
))} ))}
</StyledContainer> </StyledContainer>

View File

@ -0,0 +1,33 @@
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
type TabListFromUrlOptionalEffectProps = {
componentInstanceId: string;
tabListIds: string[];
};
export const TabListFromUrlOptionalEffect = ({
componentInstanceId,
tabListIds,
}: TabListFromUrlOptionalEffectProps) => {
const location = useLocation();
const { activeTabIdState } = useTabList(componentInstanceId);
const { setActiveTabId } = useTabList(componentInstanceId);
const hash = location.hash.replace('#', '');
const activeTabId = useRecoilValue(activeTabIdState);
useEffect(() => {
if (hash === activeTabId) {
return;
}
if (tabListIds.includes(hash)) {
setActiveTabId(hash);
}
}, [hash, activeTabId, setActiveTabId, tabListIds]);
return <></>;
};

View File

@ -1,6 +1,6 @@
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { expect, within } from '@storybook/test'; import { expect, within } from '@storybook/test';
import { ComponentDecorator, IconCheckbox } from 'twenty-ui'; import { ComponentWithRouterDecorator, IconCheckbox } from 'twenty-ui';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
@ -39,7 +39,7 @@ const meta: Meta<typeof TabList> = {
title: 'UI/Layout/Tab/TabList', title: 'UI/Layout/Tab/TabList',
component: TabList, component: TabList,
args: { args: {
tabListId: 'tab-list-id', tabListInstanceId: 'tab-list-id',
tabs: tabs, tabs: tabs,
}, },
decorators: [ decorators: [
@ -48,7 +48,7 @@ const meta: Meta<typeof TabList> = {
<Story /> <Story />
</RecoilScope> </RecoilScope>
), ),
ComponentDecorator, ComponentWithRouterDecorator,
], ],
}; };

View File

@ -26,10 +26,11 @@ import {
IconPlus, IconPlus,
IconSettings, IconSettings,
IconTool, IconTool,
isDefined,
MAIN_COLORS, MAIN_COLORS,
UndecoratedLink, UndecoratedLink,
isDefined,
} from 'twenty-ui'; } from 'twenty-ui';
import { SETTINGS_OBJECT_DETAIL_TABS } from '~/pages/settings/data-model/constants/SettingsObjectDetailTabs';
import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState'; import { updatedObjectSlugState } from '~/pages/settings/data-model/states/updatedObjectSlugState';
const StyledTabListContainer = styled.div` const StyledTabListContainer = styled.div`
@ -63,11 +64,6 @@ const StyledTitleContainer = styled.div`
display: flex; display: flex;
`; `;
const TAB_LIST_COMPONENT_ID = 'object-details-tab-list';
const FIELDS_TAB_ID = 'fields';
const SETTINGS_TAB_ID = 'settings';
const INDEXES_TAB_ID = 'indexes';
export const SettingsObjectDetailPage = () => { export const SettingsObjectDetailPage = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -82,7 +78,9 @@ export const SettingsObjectDetailPage = () => {
findActiveObjectMetadataItemBySlug(objectSlug) ?? findActiveObjectMetadataItemBySlug(objectSlug) ??
findActiveObjectMetadataItemBySlug(updatedObjectSlug); findActiveObjectMetadataItemBySlug(updatedObjectSlug);
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); const { activeTabIdState } = useTabList(
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID,
);
const activeTabId = useRecoilValue(activeTabIdState); const activeTabId = useRecoilValue(activeTabIdState);
const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState); const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState);
@ -105,19 +103,19 @@ export const SettingsObjectDetailPage = () => {
const tabs = [ const tabs = [
{ {
id: FIELDS_TAB_ID, id: SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.FIELDS,
title: 'Fields', title: 'Fields',
Icon: IconListDetails, Icon: IconListDetails,
hide: false, hide: false,
}, },
{ {
id: SETTINGS_TAB_ID, id: SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.SETTINGS,
title: 'Settings', title: 'Settings',
Icon: IconSettings, Icon: IconSettings,
hide: false, hide: false,
}, },
{ {
id: INDEXES_TAB_ID, id: SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.INDEXES,
title: 'Indexes', title: 'Indexes',
Icon: IconCodeCircle, Icon: IconCodeCircle,
hide: !isAdvancedModeEnabled || !isUniqueIndexesEnabled, hide: !isAdvancedModeEnabled || !isUniqueIndexesEnabled,
@ -127,11 +125,11 @@ export const SettingsObjectDetailPage = () => {
const renderActiveTabContent = () => { const renderActiveTabContent = () => {
switch (activeTabId) { switch (activeTabId) {
case FIELDS_TAB_ID: case SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.FIELDS:
return <ObjectFields objectMetadataItem={objectMetadataItem} />; return <ObjectFields objectMetadataItem={objectMetadataItem} />;
case SETTINGS_TAB_ID: case SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.SETTINGS:
return <ObjectSettings objectMetadataItem={objectMetadataItem} />; return <ObjectSettings objectMetadataItem={objectMetadataItem} />;
case INDEXES_TAB_ID: case SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.INDEXES:
return <ObjectIndexes objectMetadataItem={objectMetadataItem} />; return <ObjectIndexes objectMetadataItem={objectMetadataItem} />;
default: default:
return <></>; return <></>;
@ -141,49 +139,51 @@ export const SettingsObjectDetailPage = () => {
const objectTypeLabel = getObjectTypeLabel(objectMetadataItem); const objectTypeLabel = getObjectTypeLabel(objectMetadataItem);
return ( return (
<SubMenuTopBarContainer <>
title={ <SubMenuTopBarContainer
<StyledTitleContainer> links={[
<H3Title title={objectMetadataItem.labelPlural} /> {
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} /> children: 'Workspace',
</StyledTitleContainer> href: getSettingsPagePath(SettingsPath.Workspace),
} },
links={[ { children: 'Objects', href: '/settings/objects' },
{ {
children: 'Workspace', children: objectMetadataItem.labelPlural,
href: getSettingsPagePath(SettingsPath.Workspace), },
}, ]}
{ children: 'Objects', href: '/settings/objects' }, actionButton={
{ activeTabId === SETTINGS_OBJECT_DETAIL_TABS.TABS_IDS.FIELDS && (
children: objectMetadataItem.labelPlural, <UndecoratedLink to={'./new-field/select'}>
}, <Button
]} title="New Field"
actionButton={ variant="primary"
activeTabId === FIELDS_TAB_ID && ( size="small"
<UndecoratedLink to={'./new-field/select'}> accent="blue"
<Button Icon={IconPlus}
title="New Field" />
variant="primary" </UndecoratedLink>
size="small" )
accent="blue" }
Icon={IconPlus} >
<SettingsPageContainer>
<StyledTitleContainer>
<H3Title title={objectMetadataItem.labelPlural} />
<StyledObjectTypeTag objectTypeLabel={objectTypeLabel} />
</StyledTitleContainer>
<StyledTabListContainer>
<TabList
tabListInstanceId={
SETTINGS_OBJECT_DETAIL_TABS.COMPONENT_INSTANCE_ID
}
tabs={tabs}
className="tab-list"
/> />
</UndecoratedLink> </StyledTabListContainer>
) <StyledContentContainer>
} {renderActiveTabContent()}
> </StyledContentContainer>
<SettingsPageContainer> </SettingsPageContainer>
<StyledTabListContainer> </SubMenuTopBarContainer>
<TabList </>
tabListId={TAB_LIST_COMPONENT_ID}
tabs={tabs}
className="tab-list"
/>
</StyledTabListContainer>
<StyledContentContainer>
{renderActiveTabContent()}
</StyledContentContainer>
</SettingsPageContainer>
</SubMenuTopBarContainer>
); );
}; };

View File

@ -0,0 +1,8 @@
export const SETTINGS_OBJECT_DETAIL_TABS = {
COMPONENT_INSTANCE_ID: 'setting-object-details-tab-list',
TABS_IDS: {
FIELDS: 'fields',
SETTINGS: 'settings',
INDEXES: 'indexes',
},
} as const;

View File

@ -263,7 +263,7 @@ export const SettingsServerlessFunctionDetail = () => {
> >
<SettingsPageContainer> <SettingsPageContainer>
<Section> <Section>
<TabList tabListId={TAB_LIST_COMPONENT_ID} tabs={tabs} /> <TabList tabListInstanceId={TAB_LIST_COMPONENT_ID} tabs={tabs} />
</Section> </Section>
{renderActiveTabContent()} {renderActiveTabContent()}
</SettingsPageContainer> </SettingsPageContainer>