Implement new version of the side panel sub header (#16683)
## Before <img width="822" height="302" alt="CleanShot 2025-12-18 at 17 08 09@2x" src="https://github.com/user-attachments/assets/d4d0f783-64f2-4164-9a4d-42c341fa828d" /> ## After <img width="836" height="370" alt="CleanShot 2025-12-18 at 17 07 49@2x" src="https://github.com/user-attachments/assets/aca7a72f-bb83-4e40-89d2-f62bb7c8f053" />
This commit is contained in:
@@ -13,11 +13,13 @@ import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
const StyledNavigationIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronLeft = styled(IconChevronLeft)`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
`;
|
||||
|
||||
export const CommandMenuBackButton = () => {
|
||||
@@ -50,7 +52,7 @@ export const CommandMenuBackButton = () => {
|
||||
clickableComponent={
|
||||
<StyledNavigationIcon onContextMenu={handleBackButtonContextMenu}>
|
||||
<IconButton
|
||||
Icon={IconChevronLeft}
|
||||
Icon={StyledIconChevronLeft}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={goBackFromCommandMenu}
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import { SidePanelHeaderTitleSyncEffect } from '@/command-menu/components/SidePanelHeaderSyncEffect';
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { commandMenuShouldFocusTitleInputComponentState } from '@/command-menu/states/commandMenuShouldFocusTitleInputComponentState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { AppTooltip, type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
width: fit-content;
|
||||
max-width: 420px;
|
||||
& > input:disabled {
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeaderType = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledHeaderIconContainer = styled.div`
|
||||
align-self: flex-start;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type SidePanelHeaderProps = {
|
||||
Icon: IconComponent;
|
||||
iconColor: string;
|
||||
initialTitle: string;
|
||||
headerType: string;
|
||||
iconTooltip?: string;
|
||||
} & (
|
||||
| {
|
||||
disabled: true;
|
||||
onTitleChange?: never;
|
||||
}
|
||||
| {
|
||||
disabled?: boolean;
|
||||
onTitleChange: (newTitle: string) => void;
|
||||
}
|
||||
);
|
||||
|
||||
export const SidePanelHeader = ({
|
||||
Icon,
|
||||
iconColor,
|
||||
initialTitle,
|
||||
headerType,
|
||||
disabled,
|
||||
onTitleChange,
|
||||
iconTooltip,
|
||||
}: SidePanelHeaderProps) => {
|
||||
const [shouldFocusTitleInput, setShouldFocusTitleInput] =
|
||||
useRecoilComponentState(commandMenuShouldFocusTitleInputComponentState);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState(initialTitle);
|
||||
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
|
||||
const handleChange = (newTitle: string) => {
|
||||
setTitle(newTitle);
|
||||
};
|
||||
|
||||
const saveTitle = () => {
|
||||
onTitleChange?.(title);
|
||||
updateCommandMenuPageInfo({
|
||||
pageTitle: title,
|
||||
pageIcon: Icon,
|
||||
});
|
||||
};
|
||||
|
||||
const tooltipId = `side-panel-icon-tooltip-${headerType.replace(/\s+/g, '-')}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeaderTitleSyncEffect
|
||||
initialTitle={initialTitle}
|
||||
setTitle={setTitle}
|
||||
/>
|
||||
<StyledHeader data-testid="side-panel-header">
|
||||
<StyledHeaderIconContainer id={tooltipId}>
|
||||
<Icon
|
||||
color={iconColor}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
size={theme.icon.size.lg}
|
||||
/>
|
||||
</StyledHeaderIconContainer>
|
||||
{iconTooltip && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${tooltipId}`}
|
||||
content={iconTooltip}
|
||||
place="top"
|
||||
/>
|
||||
)}
|
||||
<StyledHeaderInfo>
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="side-panel-title-input"
|
||||
disabled={disabled}
|
||||
sizeVariant="md"
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => {
|
||||
setTitle(initialTitle);
|
||||
}}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
shouldOpen={shouldFocusTitleInput}
|
||||
onOpen={() => setShouldFocusTitleInput(false)}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<StyledHeaderType>{headerType}</StyledHeaderType>
|
||||
</StyledHeaderInfo>
|
||||
</StyledHeader>
|
||||
</>
|
||||
);
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type SidePanelHeaderTitleSyncEffectProps = {
|
||||
initialTitle: string;
|
||||
setTitle: (title: string) => void;
|
||||
};
|
||||
|
||||
export const SidePanelHeaderTitleSyncEffect = ({
|
||||
initialTitle,
|
||||
setTitle,
|
||||
}: SidePanelHeaderTitleSyncEffectProps) => {
|
||||
useEffect(() => {
|
||||
setTitle(initialTitle);
|
||||
}, [initialTitle, setTitle]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { THEME_LIGHT } from 'twenty-ui/theme';
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { SidePanelHeader } from '../SidePanelHeader';
|
||||
|
||||
const meta: Meta<typeof SidePanelHeader> = {
|
||||
title: 'Modules/CommandMenu/SidePanelHeader',
|
||||
component: SidePanelHeader,
|
||||
args: {
|
||||
onTitleChange: fn(),
|
||||
},
|
||||
argTypes: {},
|
||||
decorators: [
|
||||
ComponentDecorator,
|
||||
(Story) => (
|
||||
<CommandMenuPageComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'side-panel-header-story-instance' }}
|
||||
>
|
||||
<Story />
|
||||
</CommandMenuPageComponentInstanceContext.Provider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
disableHotkeyInitialization: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof SidePanelHeader>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
headerType: 'Action',
|
||||
iconColor: THEME_LIGHT.font.color.tertiary,
|
||||
initialTitle: 'Create Record',
|
||||
Icon: IconPlus,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(await canvas.findByText('Create Record')).toBeVisible();
|
||||
expect(await canvas.findByText('Action')).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const EditableTitle: Story = {
|
||||
args: {
|
||||
headerType: 'Action',
|
||||
iconColor: THEME_LIGHT.font.color.tertiary,
|
||||
initialTitle: 'Create Record',
|
||||
Icon: IconPlus,
|
||||
onTitleChange: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const titleText = await canvas.findByText('Create Record');
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = await canvas.findByDisplayValue('Create Record');
|
||||
|
||||
const NEW_TITLE = 'New Title';
|
||||
|
||||
await userEvent.clear(titleInput);
|
||||
await userEvent.type(titleInput, NEW_TITLE);
|
||||
|
||||
await userEvent.keyboard('{Enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onTitleChange).toHaveBeenCalledWith(NEW_TITLE);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
headerType: 'Action',
|
||||
iconColor: THEME_LIGHT.font.color.tertiary,
|
||||
initialTitle: 'Create Record',
|
||||
Icon: IconPlus,
|
||||
disabled: true,
|
||||
onTitleChange: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Create Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Create Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
expect(args.onTitleChange).not.toHaveBeenCalled();
|
||||
},
|
||||
};
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledTextContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(0.2)};
|
||||
`;
|
||||
|
||||
type CommandMenuSubPageNavigationHeaderProps = {
|
||||
title: string;
|
||||
onBackClick: () => void;
|
||||
};
|
||||
|
||||
export const CommandMenuSubPageNavigationHeader = ({
|
||||
onBackClick,
|
||||
title,
|
||||
}: CommandMenuSubPageNavigationHeaderProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<IconButton
|
||||
onClick={onBackClick}
|
||||
Icon={IconChevronLeft}
|
||||
variant="tertiary"
|
||||
/>
|
||||
<StyledTextContainer>{title}</StyledTextContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
height: 40px;
|
||||
`;
|
||||
|
||||
const StyledText = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
type SidePanelSubPageNavigationHeaderProps = {
|
||||
title: string;
|
||||
onBackClick: () => void;
|
||||
};
|
||||
|
||||
export const SidePanelSubPageNavigationHeader = ({
|
||||
onBackClick,
|
||||
title,
|
||||
}: SidePanelSubPageNavigationHeaderProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<IconButton
|
||||
onClick={onBackClick}
|
||||
Icon={IconChevronLeft}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
<StyledText>{title}</StyledText>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
+24
-22
@@ -1,6 +1,6 @@
|
||||
import { CommandMenuSubPageNavigationHeader } from '@/command-menu/pages/common/components/CommandMenuSubPageNavigationHeader';
|
||||
import { ChartFiltersSettingsInitializeStateEffect } from '@/command-menu/pages/page-layout/components/ChartFiltersSettingsInitializeStateEffect';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { SidePanelSubPageNavigationHeader } from '@/command-menu/pages/common/components/SidePanelSubPageNavigationHeader';
|
||||
import { ChartFiltersSettingsInitializeStateEffect } from '@/command-menu/pages/page-layout/components/ChartFiltersSettingsInitializeStateEffect';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdateCurrentWidgetConfig } from '@/command-menu/pages/page-layout/hooks/useUpdateCurrentWidgetConfig';
|
||||
import { type ChartWidget } from '@/command-menu/pages/page-layout/types/ChartWidget';
|
||||
@@ -95,30 +95,32 @@ export const ChartFiltersSettings = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledChartFiltersPageContainer>
|
||||
<CommandMenuSubPageNavigationHeader
|
||||
<>
|
||||
<SidePanelSubPageNavigationHeader
|
||||
title={t`Filter`}
|
||||
onBackClick={goBackFromCommandMenu}
|
||||
/>
|
||||
<div>
|
||||
<InputLabel>{t`Conditions`}</InputLabel>
|
||||
<RecordFilterGroupsComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
<StyledChartFiltersPageContainer>
|
||||
<div>
|
||||
<InputLabel>{t`Conditions`}</InputLabel>
|
||||
<RecordFilterGroupsComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<AdvancedFilterCommandMenuContainer
|
||||
onUpdate={handleFiltersUpdate}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isWorkflowFindRecords={false}
|
||||
/>
|
||||
<ChartFiltersSettingsInitializeStateEffect
|
||||
initialChartFilters={chartWidgetConfiguration.filter}
|
||||
/>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</div>
|
||||
</StyledChartFiltersPageContainer>
|
||||
<RecordFiltersComponentInstanceContext.Provider
|
||||
value={{ instanceId }}
|
||||
>
|
||||
<AdvancedFilterCommandMenuContainer
|
||||
onUpdate={handleFiltersUpdate}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
isWorkflowFindRecords={false}
|
||||
/>
|
||||
<ChartFiltersSettingsInitializeStateEffect
|
||||
initialChartFilters={chartWidgetConfiguration.filter}
|
||||
/>
|
||||
</RecordFiltersComponentInstanceContext.Provider>
|
||||
</RecordFilterGroupsComponentInstanceContext.Provider>
|
||||
</div>
|
||||
</StyledChartFiltersPageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user