feat: fix Command Menu Side Panel Layout (#15883)

[Figma
Design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=81380-344641&t=FpjWNOK2gZuDQQfr-0)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> Adds a side panel layout for the Command Menu, routes modals into a
local container, updates the top bar and context chips, and standardizes
small button sizes.
> 
> - **Command Menu**:
> - **Side Panel Layout**: Introduces `CommandMenuSidePanelLayout` with
animated width, hosts `CommandMenuRouter`, and provides a modal
container via `ModalContainerContext`.
> - **Top Bar**: Redesign (`CommandMenuTopBar`) with back icon, optional
AI sparkles action, compact height
(`COMMAND_MENU_SEARCH_BAR_HEIGHT=40`), and updated placeholder.
> - **Context Chips**: Adds `CommandMenuLastContextChip` and
`CommandMenuRecordInfo`; extends `CommandMenuContextChip` with `page`
prop; updates `CommandMenuContextChipGroups` to render last chip as
record info when applicable.
> - **Container Simplification**: `CommandMenuContainer` simplified to
just provide contexts and `AgentChatProvider`.
> - **Modal System**:
> - Adds `ModalContainerContext` and updates `Modal` to portal into
provided container; `Modal.Backdrop` supports `isInContainer`.
> - Updates usages (e.g., `UserOrMetadataLoader`, `ActionModal`) to
align with new modal behavior.
> - **Page Integration**:
> - Replaces `PageBody` with `CommandMenuSidePanelLayout` in
`RecordShowPage` and `RecordIndexContainerGater`.
> - Removes global `CommandMenuRouter` from `DefaultLayout` (keeps
keyboard shortcuts).
> - **UI/Styling**:
> - Standardizes several buttons to `size="small"` (e.g., command
actions, open record, options, reply, workflow footer).
> - Adjusts `ShowPageSubContainer` styling when rendered inside command
menu.
>   - Storybook tests updated for new placeholder text.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
81fcaa145618a2fa1c3e11e2dc88fe832c51374c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Devessier <baptiste@devessier.fr>
Co-authored-by: Aman Raj <92664006+araj00@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Abdul Rahman
2025-11-21 20:42:41 +05:30
committed by GitHub
parent 28b8a4f7ec
commit 04b0a65e73
102 changed files with 1913 additions and 1548 deletions
@@ -0,0 +1,94 @@
import { renderHook } from '@testing-library/react';
import { useUpdateAgentLabel } from '../useUpdateAgentLabel';
const mockUpdateAgent = jest.fn();
const mockUseFindOneAgentQuery = jest.fn();
jest.mock('~/generated-metadata/graphql', () => ({
useFindOneAgentQuery: jest.fn(),
useUpdateOneAgentMutation: () => [mockUpdateAgent],
}));
describe('useUpdateAgentLabel', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseFindOneAgentQuery.mockReturnValue({
data: undefined,
});
(
require('~/generated-metadata/graphql').useFindOneAgentQuery as jest.Mock
).mockImplementation(mockUseFindOneAgentQuery);
});
it('should skip query when agentId is undefined', () => {
renderHook(() => useUpdateAgentLabel(undefined));
expect(mockUseFindOneAgentQuery).toHaveBeenCalledWith({
variables: { id: '' },
skip: true,
});
});
it('should fetch agent when agentId is provided', () => {
const mockAgent = {
id: 'agent-123',
label: 'Original Label',
};
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: mockAgent,
},
});
renderHook(() => useUpdateAgentLabel('agent-123'));
expect(mockUseFindOneAgentQuery).toHaveBeenCalledWith({
variables: { id: 'agent-123' },
skip: false,
});
});
it('should update agent label when agent is defined', async () => {
const mockAgent = {
id: 'agent-123',
label: 'Original Label',
};
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: mockAgent,
},
});
mockUpdateAgent.mockResolvedValue({});
const { result } = renderHook(() => useUpdateAgentLabel('agent-123'));
await result.current.updateAgentLabel('New Label');
expect(mockUpdateAgent).toHaveBeenCalledWith({
variables: {
input: {
id: 'agent-123',
label: 'New Label',
},
},
refetchQueries: ['FindOneAgent'],
});
});
it('should not update agent label when agent is undefined', async () => {
mockUseFindOneAgentQuery.mockReturnValue({
data: {
findOneAgent: undefined,
},
});
const { result } = renderHook(() => useUpdateAgentLabel('agent-123'));
await result.current.updateAgentLabel('New Label');
expect(mockUpdateAgent).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,34 @@
import { isDefined } from 'twenty-shared/utils';
import {
useFindOneAgentQuery,
useUpdateOneAgentMutation,
} from '~/generated-metadata/graphql';
export const useUpdateAgentLabel = (agentId: string | undefined) => {
const { data: agentData } = useFindOneAgentQuery({
variables: { id: agentId || '' },
skip: !isDefined(agentId),
});
const [updateAgent] = useUpdateOneAgentMutation();
const agent = agentData?.findOneAgent;
const updateAgentLabel = async (newLabel: string) => {
if (!isDefined(agent)) {
return;
}
await updateAgent({
variables: {
input: {
id: agent.id,
label: newLabel,
},
},
refetchQueries: ['FindOneAgent'],
});
};
return { updateAgentLabel };
};