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
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
import { FormSelectFieldInput } from '@/object-record/record-field/ui/form-types/components/FormSelectFieldInput';
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
@@ -11,19 +10,13 @@ import {
type CronTriggerInterval,
} from '@/workflow/workflow-trigger/constants/CronTriggerIntervalOptions';
import { getCronTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getCronTriggerDefaultSettings';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { getTriggerScheduleDescription } from '@/workflow/workflow-trigger/utils/getTriggerScheduleDescription';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { isNumber } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/display';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
type WorkflowEditTriggerCronFormProps = {
@@ -53,19 +46,10 @@ export const WorkflowEditTriggerCronForm = ({
trigger,
triggerOptions,
}: WorkflowEditTriggerCronFormProps) => {
const theme = useTheme();
const [errorMessages, setErrorMessages] = useState<FormErrorMessages>({});
const [errorMessagesVisible, setErrorMessagesVisible] = useState(false);
const dateLocale = useRecoilValue(dateLocaleState);
const { getIcon } = useIcons();
const headerIcon = getTriggerIcon(trigger);
const defaultLabel = getTriggerDefaultLabel(trigger);
const headerTitle = trigger.name ?? defaultLabel;
const headerType = getTriggerHeaderType(trigger);
const customDescription = getTriggerScheduleDescription(
trigger,
dateLocale.localeCatalog,
@@ -77,24 +61,6 @@ export const WorkflowEditTriggerCronForm = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
initialTitle={headerTitle}
headerType={headerType}
disabled={triggerOptions.readonly}
iconTooltip={getTriggerDefaultLabel(trigger)}
/>
<WorkflowStepBody>
<FormSelectFieldInput
label={t`Trigger interval`}
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { type FieldMultiSelectValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { SelectControl } from '@/ui/input/components/SelectControl';
@@ -16,10 +15,6 @@ import { type WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
@@ -122,10 +117,6 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
[systemObjects, searchInputValue],
);
const defaultLabel = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
const handleOptionClick = (value: string) => {
if (triggerOptions.readonly === true) {
return;
@@ -174,24 +165,6 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
initialTitle={defaultLabel}
headerType={headerType}
disabled={triggerOptions.readonly}
iconTooltip={getTriggerDefaultLabel(trigger)}
/>
<WorkflowStepBody>
<StyledRecordTypeSelectContainer fullWidth>
<StyledLabel>Record Type</StyledLabel>
@@ -1,4 +1,3 @@
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { IconPicker } from '@/ui/input/components/IconPicker';
import { Select } from '@/ui/input/components/Select';
@@ -10,10 +9,6 @@ import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/Workflo
import { MANUAL_TRIGGER_AVAILABILITY_TYPE_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityTypeOptions';
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@@ -76,12 +71,6 @@ export const WorkflowEditTriggerManual = ({
const availability = trigger.settings.availability;
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
const availabilityDescriptions = {
SINGLE_RECORD: t`The selected record will be passed to your workflow`,
BULK_RECORDS: t`The selected records (up to ${maxRecordsFormatted}) will be passed to your workflow`,
@@ -90,24 +79,6 @@ export const WorkflowEditTriggerManual = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
Icon={getIcon(headerIcon)}
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
initialTitle={headerTitle}
headerType={headerType}
disabled={triggerOptions.readonly}
iconTooltip={getTriggerDefaultLabel(trigger)}
/>
<WorkflowStepBody>
<Select
dropdownId="workflow-edit-manual-trigger-availability"
@@ -1,5 +1,4 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
import { Select } from '@/ui/input/components/Select';
import { TextInput } from '@/ui/input/components/TextInput';
@@ -12,10 +11,6 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
import { WEBHOOK_TRIGGER_AUTHENTICATION_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerAuthenticationOptions';
import { WEBHOOK_TRIGGER_HTTP_METHOD_OPTIONS } from '@/workflow/workflow-trigger/constants/WebhookTriggerHttpMethodOptions';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
import { getWebhookTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getWebhookTriggerDefaultSettings';
import { useTheme } from '@emotion/react';
import { isNonEmptyString } from '@sniptt/guards';
@@ -26,7 +21,7 @@ import {
buildOutputSchemaFromValue,
TRIGGER_STEP_ID,
} from 'twenty-shared/workflow';
import { IconCopy, useIcons } from 'twenty-ui/display';
import { IconCopy } from 'twenty-ui/display';
import { useDebouncedCallback } from 'use-debounce';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
@@ -60,7 +55,6 @@ export const WorkflowEditTriggerWebhookForm = ({
const { copyToClipboard } = useCopyToClipboard();
const [errorMessages, setErrorMessages] = useState<FormErrorMessages>({});
const [errorMessagesVisible, setErrorMessagesVisible] = useState(false);
const { getIcon } = useIcons();
const workflowVisualizerWorkflowId = useRecoilComponentValue(
workflowVisualizerWorkflowIdComponentState,
);
@@ -70,11 +64,6 @@ export const WorkflowEditTriggerWebhookForm = ({
setErrorMessagesVisible(true);
};
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
const headerIcon = getTriggerIcon(trigger);
const headerType = getTriggerHeaderType(trigger);
const webhookUrl = `${REACT_APP_SERVER_BASE_URL}/webhooks/workflows/${currentWorkspace?.id}/${workflowVisualizerWorkflowId}`;
const displayWebhookUrl = webhookUrl.replace(/^(https?:\/\/)?(www\.)?/, '');
@@ -89,27 +78,6 @@ export const WorkflowEditTriggerWebhookForm = ({
return (
<>
<SidePanelHeader
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate(
{
...trigger,
name: newName,
},
{ computeOutputSchema: false },
);
}}
Icon={getIcon(headerIcon)}
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
initialTitle={headerTitle}
headerType={headerType}
disabled={triggerOptions.readonly}
iconTooltip={getTriggerDefaultLabel(trigger)}
/>
<WorkflowStepBody>
<TextInput
label="Live URL"