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:
+230
@@ -0,0 +1,230 @@
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
|
||||
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { type WorkflowVersion } from '@/workflow/types/Workflow';
|
||||
import { getAgentIdFromStep } from '@/workflow/utils/getAgentIdFromStep';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { useUpdateAgentLabel } from '@/workflow/workflow-steps/hooks/useUpdateAgentLabel';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
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 { t } from '@lingui/core/macro';
|
||||
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';
|
||||
|
||||
const StyledWorkflowStepInfoContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledWorkflowStepIcon = styled.div<{ iconColor: string }>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ iconColor }) => iconColor};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledWorkflowStepTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledWorkflowStepType = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const CommandMenuWorkflowStepInfo = ({
|
||||
commandMenuPageInstanceId,
|
||||
}: {
|
||||
commandMenuPageInstanceId: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
|
||||
const workflowId = useRecoilComponentValue(
|
||||
commandMenuWorkflowIdComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const workflowStepId = useRecoilComponentValue(
|
||||
commandMenuWorkflowStepIdComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const isReadonly =
|
||||
commandMenuPage === CommandMenuPages.WorkflowStepView ||
|
||||
commandMenuPage === CommandMenuPages.WorkflowRunStepView;
|
||||
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
const { updateWorkflowVersionStep } = useUpdateWorkflowVersionStep();
|
||||
const { updateOneRecord: updateOneWorkflowVersion } =
|
||||
useUpdateOneRecord<WorkflowVersion>({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkflowVersion,
|
||||
});
|
||||
|
||||
const {
|
||||
trigger,
|
||||
steps,
|
||||
id: workflowVersionId,
|
||||
} = workflowWithCurrentVersion?.currentVersion ?? {
|
||||
trigger: null,
|
||||
steps: null,
|
||||
id: undefined,
|
||||
};
|
||||
|
||||
const isTriggerStep = workflowStepId === TRIGGER_STEP_ID;
|
||||
|
||||
const stepDefinition =
|
||||
isDefined(workflowStepId) && isDefined(trigger)
|
||||
? isTriggerStep || isDefined(steps)
|
||||
? getStepDefinitionOrThrow({
|
||||
stepId: workflowStepId,
|
||||
trigger,
|
||||
steps,
|
||||
})
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
const isTrigger = stepDefinition?.type === 'trigger';
|
||||
|
||||
const agentId = getAgentIdFromStep(stepDefinition);
|
||||
const { updateAgentLabel } = useUpdateAgentLabel(agentId);
|
||||
const stepName =
|
||||
isDefined(stepDefinition) && isDefined(stepDefinition.definition)
|
||||
? isTrigger
|
||||
? (stepDefinition.definition.name ??
|
||||
(stepDefinition.definition.type === 'MANUAL'
|
||||
? 'Launch manually'
|
||||
: 'Trigger'))
|
||||
: (stepDefinition.definition.name ?? 'Action')
|
||||
: '';
|
||||
|
||||
const [editedTitle, setEditedTitle] = useState<string | null>(null);
|
||||
|
||||
const title =
|
||||
editedTitle ?? (isDefined(stepName) && stepName !== '' ? stepName : '');
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setEditedTitle(newTitle);
|
||||
};
|
||||
|
||||
if (
|
||||
!isDefined(workflowId) ||
|
||||
!isDefined(workflowStepId) ||
|
||||
!isDefined(workflowWithCurrentVersion?.currentVersion) ||
|
||||
!isDefined(stepDefinition) ||
|
||||
!isDefined(stepDefinition.definition)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const headerIcon = isTrigger
|
||||
? getTriggerIcon(stepDefinition.definition)
|
||||
: getActionIcon(stepDefinition.definition.type);
|
||||
|
||||
const headerIconColor = isTrigger
|
||||
? getTriggerIconColor({
|
||||
theme,
|
||||
triggerType: stepDefinition.definition.type,
|
||||
})
|
||||
: getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
});
|
||||
|
||||
const headerType = isTrigger ? 'Trigger' : 'Action';
|
||||
|
||||
const Icon = getIcon(headerIcon ?? 'IconDefault');
|
||||
|
||||
const saveTitle = async () => {
|
||||
if (!isDefined(workflowVersionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageTitle: title,
|
||||
pageIcon: Icon,
|
||||
});
|
||||
|
||||
if (isTrigger) {
|
||||
await updateOneWorkflowVersion({
|
||||
idToUpdate: workflowVersionId,
|
||||
updateOneRecordInput: {
|
||||
trigger: {
|
||||
...stepDefinition.definition,
|
||||
name: title,
|
||||
} as typeof stepDefinition.definition,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await updateWorkflowVersionStep({
|
||||
workflowVersionId,
|
||||
step: {
|
||||
...stepDefinition.definition,
|
||||
name: title,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(agentId)) {
|
||||
await updateAgentLabel(title);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWorkflowStepInfoContainer>
|
||||
{headerIcon && (
|
||||
<StyledWorkflowStepIcon iconColor={headerIconColor}>
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
</StyledWorkflowStepIcon>
|
||||
)}
|
||||
<StyledWorkflowStepTitleContainer>
|
||||
<TitleInput
|
||||
instanceId={`workflow-step-title-${commandMenuPageInstanceId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
/>
|
||||
</StyledWorkflowStepTitleContainer>
|
||||
<StyledWorkflowStepType>
|
||||
{isTrigger ? t`Trigger` : t`Action`}
|
||||
</StyledWorkflowStepType>
|
||||
</StyledWorkflowStepInfoContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user