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:
@@ -1,5 +1,4 @@
|
||||
import { type ReactNode, useContext } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay';
|
||||
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
|
||||
@@ -63,19 +62,17 @@ export const ActionModal = ({
|
||||
return (
|
||||
<>
|
||||
<ActionDisplay onClick={handleClick} />
|
||||
{isModalOpened &&
|
||||
createPortal(
|
||||
<ConfirmationModal
|
||||
modalId={modalId}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onConfirmClick={handleConfirmClick}
|
||||
confirmButtonText={confirmButtonText}
|
||||
confirmButtonAccent={confirmButtonAccent}
|
||||
loading={isLoading}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
{isModalOpened && (
|
||||
<ConfirmationModal
|
||||
modalId={modalId}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onConfirmClick={handleConfirmClick}
|
||||
confirmButtonText={confirmButtonText}
|
||||
confirmButtonAccent={confirmButtonAccent}
|
||||
loading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-1
@@ -2,11 +2,12 @@ import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDi
|
||||
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
export const ActionOpenSidePanelPage = ({
|
||||
@@ -28,6 +29,8 @@ export const ActionOpenSidePanelPage = ({
|
||||
|
||||
const setCommandMenuSearchState = useSetRecoilState(commandMenuSearchState);
|
||||
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
if (!actionConfig) {
|
||||
return null;
|
||||
}
|
||||
@@ -39,6 +42,7 @@ export const ActionOpenSidePanelPage = ({
|
||||
page,
|
||||
pageTitle: t(pageTitle),
|
||||
pageIcon,
|
||||
resetNavigationStack: isCommandMenuOpened,
|
||||
});
|
||||
|
||||
if (shouldResetSearchState) {
|
||||
|
||||
+2
-8
@@ -1,4 +1,4 @@
|
||||
import { ActionModal } from '@/action-menu/actions/components/ActionModal';
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
|
||||
import { computeProgressText } from '@/action-menu/utils/computeProgressText';
|
||||
import { getActionLabel } from '@/action-menu/utils/getActionLabel';
|
||||
@@ -14,7 +14,6 @@ import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -92,12 +91,7 @@ export const DeleteMultipleRecordsAction = () => {
|
||||
|
||||
return (
|
||||
<ActionConfigContext.Provider value={actionConfigWithProgress}>
|
||||
<ActionModal
|
||||
title={t`Delete Records`}
|
||||
subtitle={t`Are you sure you want to delete these records? They can be recovered from the Command menu.`}
|
||||
onConfirmClick={handleDeleteClick}
|
||||
confirmButtonText={t`Delete Records`}
|
||||
/>
|
||||
<Action onClick={handleDeleteClick} />
|
||||
</ActionConfigContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-10
@@ -1,11 +1,10 @@
|
||||
import { ActionModal } from '@/action-menu/actions/components/ActionModal';
|
||||
import { Action } from '@/action-menu/actions/components/Action';
|
||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const DeleteSingleRecordAction = () => {
|
||||
@@ -37,12 +36,5 @@ export const DeleteSingleRecordAction = () => {
|
||||
await deleteOneRecord(recordId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionModal
|
||||
title={t`Delete Record`}
|
||||
subtitle={t`Are you sure you want to delete this record? It can be recovered from the Command menu.`}
|
||||
onConfirmClick={handleDeleteClick}
|
||||
confirmButtonText={t`Delete Record`}
|
||||
/>
|
||||
);
|
||||
return <Action onClick={handleDeleteClick} />;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export const CmdEnterActionButton = ({
|
||||
title={title}
|
||||
variant={disabled ? 'secondary' : 'primary'}
|
||||
accent="blue"
|
||||
size="medium"
|
||||
size="small"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
hotkeys={[getOsControlSymbol(), '⏎']}
|
||||
|
||||
+5
-1
@@ -66,7 +66,11 @@ export const CommandMenuActionMenuDropdown = () => {
|
||||
dropdownId={dropdownId}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<Button title={t`Options`} hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
<Button
|
||||
size="small"
|
||||
title={t`Options`}
|
||||
hotkeys={[getOsControlSymbol(), 'O']}
|
||||
/>
|
||||
}
|
||||
dropdownPlacement="top-end"
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ export const RecordShowRightDrawerOpenRecordButton = ({
|
||||
title={t`Open`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="medium"
|
||||
size="small"
|
||||
Icon={IconBrowserMaximize}
|
||||
hotkeys={[getOsControlSymbol(), '⏎']}
|
||||
onClick={handleOpenRecord}
|
||||
|
||||
@@ -90,7 +90,10 @@ export const AIChatThreadGroup = ({
|
||||
<StyledThreadItem
|
||||
onClick={() => {
|
||||
setCurrentAIChatThread(thread.id);
|
||||
openAskAIPage(thread.title);
|
||||
openAskAIPage({
|
||||
pageTitle: thread.title,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
}}
|
||||
key={thread.id}
|
||||
>
|
||||
|
||||
@@ -10,7 +10,7 @@ export const useCreateNewAIChatThread = () => {
|
||||
const [createChatThread] = useCreateChatThreadMutation({
|
||||
onCompleted: (data) => {
|
||||
setCurrentAIChatThread(data.createChatThread.id);
|
||||
openAskAIPage();
|
||||
openAskAIPage({ resetNavigationStack: false });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuNavigationHistoryDropdownId';
|
||||
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronLeft } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
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)};
|
||||
`;
|
||||
|
||||
export const CommandMenuBackButton = () => {
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
|
||||
const { contextChips } = useCommandMenuContextChips();
|
||||
|
||||
const { openDropdown } = useOpenDropdown();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const handleBackButtonContextMenu = (
|
||||
event: React.MouseEvent<HTMLDivElement>,
|
||||
) => {
|
||||
if (contextChips.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
openDropdown({
|
||||
dropdownComponentInstanceIdFromProps:
|
||||
COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<StyledNavigationIcon onContextMenu={handleBackButtonContextMenu}>
|
||||
<IconButton
|
||||
Icon={IconChevronLeft}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={goBackFromCommandMenu}
|
||||
/>
|
||||
</StyledNavigationIcon>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{contextChips.slice(0, -1).map((chip, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
LeftComponent={chip.Icons}
|
||||
onClick={() => {
|
||||
closeDropdown(COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID);
|
||||
chip.onClick?.();
|
||||
}}
|
||||
text={chip.text}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownId={COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID}
|
||||
dropdownPlacement="bottom-start"
|
||||
disableClickForClickableComponent={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,6 @@
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
|
||||
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
|
||||
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
@@ -12,19 +8,15 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
type CommandMenuContainerProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommandMenuContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { commandMenuCloseAnimationCompleteCleanup } =
|
||||
useCommandMenuCloseAnimationCompleteCleanup();
|
||||
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
}: CommandMenuContainerProps) => {
|
||||
const objectMetadataItemId = useRecoilComponentValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
@@ -46,8 +38,6 @@ export const CommandMenuContainer = ({
|
||||
currentViewId ?? '',
|
||||
);
|
||||
|
||||
useCommandMenuHotKeys();
|
||||
|
||||
return (
|
||||
<RecordComponentInstanceContextsWrapper componentInstanceId={recordIndexId}>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
@@ -56,16 +46,7 @@ export const CommandMenuContainer = ({
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
|
||||
>
|
||||
<AgentChatProvider>
|
||||
<AnimatePresence
|
||||
mode="wait"
|
||||
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
|
||||
>
|
||||
{isCommandMenuOpened && (
|
||||
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</AgentChatProvider>
|
||||
<AgentChatProvider>{children}</AgentChatProvider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
@@ -54,6 +55,10 @@ export type CommandMenuContextChipProps = {
|
||||
testId?: string;
|
||||
maxWidth?: string;
|
||||
forceEmptyText?: boolean;
|
||||
page?: {
|
||||
page: CommandMenuPages;
|
||||
pageId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const CommandMenuContextChip = ({
|
||||
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import {
|
||||
CommandMenuContextChip,
|
||||
type CommandMenuContextChipProps,
|
||||
} from './CommandMenuContextChip';
|
||||
|
||||
export const CommandMenuContextChipGroups = ({
|
||||
contextChips,
|
||||
}: {
|
||||
contextChips: CommandMenuContextChipProps[];
|
||||
}) => {
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
if (contextChips.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contextChips.length < 3) {
|
||||
return (
|
||||
<>
|
||||
{contextChips.map((chip, index) => (
|
||||
<CommandMenuContextChip
|
||||
key={index}
|
||||
maxWidth="180px"
|
||||
Icons={chip.Icons}
|
||||
text={chip.text}
|
||||
onClick={chip.onClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const firstChips = contextChips.slice(0, -1);
|
||||
const firstThreeChips = firstChips.slice(0, 3);
|
||||
const lastChip = contextChips.at(-1);
|
||||
|
||||
return (
|
||||
<>
|
||||
{firstChips.length > 0 && (
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<CommandMenuContextChip
|
||||
Icons={firstThreeChips.map((chip) => chip.Icons?.[0])}
|
||||
onClick={() => {}}
|
||||
text={`${firstChips.length}`}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
{firstChips.map((chip, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
LeftComponent={chip.Icons}
|
||||
text={chip.text}
|
||||
onClick={() => {
|
||||
closeDropdown(
|
||||
COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID,
|
||||
);
|
||||
chip.onClick?.();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
dropdownId={COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID}
|
||||
dropdownPlacement="bottom-start"
|
||||
></Dropdown>
|
||||
)}
|
||||
|
||||
{isDefined(lastChip) && (
|
||||
<CommandMenuContextChip
|
||||
Icons={lastChip.Icons}
|
||||
text={lastChip.text}
|
||||
onClick={lastChip.onClick}
|
||||
maxWidth="180px"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
|
||||
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
|
||||
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuContextChipGroupsWithRecordSelection = ({
|
||||
contextChips,
|
||||
objectMetadataItemId,
|
||||
}: {
|
||||
contextChips: CommandMenuContextChipProps[];
|
||||
objectMetadataItemId: string;
|
||||
}) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItemById({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const { records, loading, totalCount } =
|
||||
useFindManyRecordsSelectedInContextStore({
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
const { openCommandMenu } = useCommandMenu();
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Avatars = records.map((record) => (
|
||||
<CommandMenuContextRecordChipAvatars
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
key={record.id}
|
||||
record={record}
|
||||
/>
|
||||
));
|
||||
|
||||
const recordSelectionContextChip =
|
||||
totalCount && records.length > 0
|
||||
? {
|
||||
text: getSelectedRecordsContextText(
|
||||
objectMetadataItem,
|
||||
records,
|
||||
totalCount,
|
||||
),
|
||||
Icons: Avatars,
|
||||
onClick: contextChips.length > 0 ? openCommandMenu : undefined,
|
||||
withIconBackground: false,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const contextChipsWithRecordSelection = [
|
||||
recordSelectionContextChip,
|
||||
...contextChips,
|
||||
].filter(isDefined);
|
||||
|
||||
return (
|
||||
<CommandMenuContextChipGroups
|
||||
contextChips={contextChipsWithRecordSelection}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CommandMenuPageLayoutInfo } from '@/command-menu/components/CommandMenuPageLayoutInfo';
|
||||
import { CommandMenuRecordInfo } from '@/command-menu/components/CommandMenuRecordInfo';
|
||||
import { CommandMenuWorkflowStepInfo } from '@/command-menu/components/CommandMenuWorkflowStepInfo';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import styled from '@emotion/styled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { type CommandMenuContextChipProps } from './CommandMenuContextChip';
|
||||
|
||||
const StyledPageTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
type CommandMenuPageInfoProps = {
|
||||
pageChip: CommandMenuContextChipProps | undefined;
|
||||
};
|
||||
|
||||
export const CommandMenuPageInfo = ({ pageChip }: CommandMenuPageInfoProps) => {
|
||||
if (!isDefined(pageChip)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRecordPage = pageChip.page?.page === CommandMenuPages.ViewRecord;
|
||||
|
||||
if (isRecordPage && isDefined(pageChip.page?.pageId)) {
|
||||
return (
|
||||
<CommandMenuRecordInfo commandMenuPageInstanceId={pageChip.page.pageId} />
|
||||
);
|
||||
}
|
||||
|
||||
const isWorkflowStepPage = pageChip.page?.page
|
||||
? [
|
||||
CommandMenuPages.WorkflowStepEdit,
|
||||
CommandMenuPages.WorkflowStepView,
|
||||
CommandMenuPages.WorkflowRunStepView,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
if (isWorkflowStepPage && isDefined(pageChip.page?.pageId)) {
|
||||
return (
|
||||
<CommandMenuWorkflowStepInfo
|
||||
commandMenuPageInstanceId={pageChip.page.pageId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isPageLayoutPage = pageChip.page?.page
|
||||
? [
|
||||
CommandMenuPages.PageLayoutWidgetTypeSelect,
|
||||
CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
CommandMenuPages.PageLayoutGraphFilter,
|
||||
CommandMenuPages.PageLayoutIframeSettings,
|
||||
CommandMenuPages.PageLayoutTabSettings,
|
||||
].includes(pageChip.page?.page)
|
||||
: false;
|
||||
|
||||
if (isPageLayoutPage) {
|
||||
return <CommandMenuPageLayoutInfo />;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledPageTitle>
|
||||
<OverflowingTextWithTooltip text={pageChip.text ?? ''} />
|
||||
</StyledPageTitle>
|
||||
);
|
||||
};
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { usePageLayoutHeaderInfo } from '@/command-menu/components/hooks/usePageLayoutHeaderInfo';
|
||||
import { useUpdateCommandMenuPageInfo } from '@/command-menu/hooks/useUpdateCommandMenuPageInfo';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
const StyledPageLayoutInfoContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledPageLayoutIcon = 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 StyledPageLayoutTitleContainer = 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 StyledPageLayoutType = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const CommandMenuPageLayoutInfo = () => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const draftPageLayout = useRecoilComponentValue(
|
||||
pageLayoutDraftComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const pageLayoutEditingWidgetId = useRecoilComponentValue(
|
||||
pageLayoutEditingWidgetIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const [openTabId] = useRecoilComponentState(
|
||||
pageLayoutTabSettingsOpenTabIdComponentState,
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { updateCommandMenuPageInfo } = useUpdateCommandMenuPageInfo();
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
|
||||
|
||||
const [editedTitle, setEditedTitle] = useState<string | null>(null);
|
||||
|
||||
const headerInfo = usePageLayoutHeaderInfo({
|
||||
commandMenuPage,
|
||||
draftPageLayout,
|
||||
pageLayoutEditingWidgetId,
|
||||
openTabId,
|
||||
editedTitle,
|
||||
});
|
||||
|
||||
if (!headerInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
headerIcon,
|
||||
headerIconColor,
|
||||
headerType,
|
||||
title,
|
||||
isReadonly,
|
||||
tab,
|
||||
widgetInEditMode,
|
||||
} = headerInfo;
|
||||
|
||||
const Icon = headerIcon ?? getIcon('IconDefault');
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setEditedTitle(newTitle);
|
||||
};
|
||||
|
||||
const saveTitle = async () => {
|
||||
const finalTitle = editedTitle ?? title;
|
||||
|
||||
if (!isNonEmptyString(finalTitle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCommandMenuPageInfo({
|
||||
pageTitle: finalTitle,
|
||||
pageIcon: Icon,
|
||||
});
|
||||
|
||||
if (
|
||||
commandMenuPage === CommandMenuPages.PageLayoutTabSettings &&
|
||||
isDefined(tab)
|
||||
) {
|
||||
updatePageLayoutTab(tab.id, { title: finalTitle });
|
||||
} else if (isDefined(widgetInEditMode)) {
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: finalTitle,
|
||||
});
|
||||
}
|
||||
|
||||
setEditedTitle(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledPageLayoutInfoContainer>
|
||||
{isDefined(headerIcon) && (
|
||||
<StyledPageLayoutIcon iconColor={headerIconColor}>
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
</StyledPageLayoutIcon>
|
||||
)}
|
||||
<StyledPageLayoutTitleContainer>
|
||||
<TitleInput
|
||||
instanceId={`page-layout-title-${commandMenuPage}-${pageLayoutId}`}
|
||||
disabled={isReadonly}
|
||||
sizeVariant="sm"
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
placeholder={headerType}
|
||||
onEnter={saveTitle}
|
||||
onEscape={() => setEditedTitle(null)}
|
||||
onClickOutside={saveTitle}
|
||||
onTab={saveTitle}
|
||||
onShiftTab={saveTitle}
|
||||
/>
|
||||
</StyledPageLayoutTitleContainer>
|
||||
{headerType && <StyledPageLayoutType>{headerType}</StyledPageLayoutType>}
|
||||
</StyledPageLayoutInfoContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
import { viewableRecordIdComponentState } from '@/command-menu/pages/record-page/states/viewableRecordIdComponentState';
|
||||
import { viewableRecordNameSingularComponentState } from '@/command-menu/pages/record-page/states/viewableRecordNameSingularComponentState';
|
||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useIsRecordFieldReadOnly } from '@/object-record/read-only/hooks/useIsRecordFieldReadOnly';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
|
||||
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector';
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
const StyledRecordInfoContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledRecordAvatar = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledRecordTitleContainer = 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 StyledRecordDate = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const CommandMenuRecordInfo = ({
|
||||
commandMenuPageInstanceId,
|
||||
}: {
|
||||
commandMenuPageInstanceId: string;
|
||||
}) => {
|
||||
const viewableRecordNameSingular = useRecoilComponentValue(
|
||||
viewableRecordNameSingularComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const viewableRecordId = useRecoilComponentValue(
|
||||
viewableRecordIdComponentState,
|
||||
commandMenuPageInstanceId,
|
||||
);
|
||||
|
||||
const { objectNameSingular, objectRecordId } = useRecordShowPage(
|
||||
viewableRecordNameSingular!,
|
||||
viewableRecordId!,
|
||||
);
|
||||
|
||||
const recordCreatedAt = useRecoilValue<string | null>(
|
||||
recordStoreFamilySelector({
|
||||
recordId: objectRecordId,
|
||||
fieldName: 'createdAt',
|
||||
}),
|
||||
);
|
||||
|
||||
const recordIdentifier = useRecoilValue(
|
||||
recordStoreIdentifierFamilySelector({
|
||||
recordId: objectRecordId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { localeCatalog } = useRecoilValue(dateLocaleState);
|
||||
const beautifiedCreatedAt = isNonEmptyString(recordCreatedAt)
|
||||
? beautifyPastDateRelativeToNow(recordCreatedAt, localeCatalog)
|
||||
: '';
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { labelIdentifierFieldMetadataItem } =
|
||||
useLabelIdentifierFieldMetadataItem({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const isTitleReadOnly = useIsRecordFieldReadOnly({
|
||||
recordId: objectRecordId,
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
});
|
||||
|
||||
const fieldDefinition = {
|
||||
type: labelIdentifierFieldMetadataItem?.type ?? FieldMetadataType.TEXT,
|
||||
iconName: '',
|
||||
fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '',
|
||||
label: labelIdentifierFieldMetadataItem?.label ?? '',
|
||||
metadata: {
|
||||
fieldName: labelIdentifierFieldMetadataItem?.name ?? '',
|
||||
objectMetadataNameSingular: objectNameSingular,
|
||||
},
|
||||
defaultValue: labelIdentifierFieldMetadataItem?.defaultValue,
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRecordInfoContainer>
|
||||
{recordIdentifier && (
|
||||
<StyledRecordAvatar>
|
||||
<Avatar
|
||||
avatarUrl={recordIdentifier.avatarUrl}
|
||||
placeholder={recordIdentifier.name}
|
||||
placeholderColorSeed={objectRecordId}
|
||||
size="md"
|
||||
type={recordIdentifier.avatarType}
|
||||
/>
|
||||
</StyledRecordAvatar>
|
||||
)}
|
||||
<StyledRecordTitleContainer>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId: objectRecordId,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition,
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
isCentered: false,
|
||||
isDisplayModeFixHeight: true,
|
||||
isRecordFieldReadOnly: isTitleReadOnly,
|
||||
}}
|
||||
>
|
||||
<RecordTitleCell
|
||||
sizeVariant="sm"
|
||||
containerType={RecordTitleCellContainerType.PageHeader}
|
||||
/>
|
||||
</FieldContext.Provider>
|
||||
</StyledRecordTitleContainer>
|
||||
{beautifiedCreatedAt && (
|
||||
<StyledRecordDate>
|
||||
<Trans>Created {beautifiedCreatedAt}</Trans>
|
||||
</StyledRecordDate>
|
||||
)}
|
||||
</StyledRecordInfoContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +1,50 @@
|
||||
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
|
||||
import { CommandMenuContextChipGroups } from '@/command-menu/components/CommandMenuContextChipGroups';
|
||||
import { CommandMenuContextChipGroupsWithRecordSelection } from '@/command-menu/components/CommandMenuContextChipGroupsWithRecordSelection';
|
||||
import { CommandMenuBackButton } from '@/command-menu/components/CommandMenuBackButton';
|
||||
import { CommandMenuPageInfo } from '@/command-menu/components/CommandMenuPageInfo';
|
||||
import { CommandMenuTopBarInputFocusEffect } from '@/command-menu/components/CommandMenuTopBarInputFocusEffect';
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { CommandMenuTopBarRightCornerIcon } from '@/command-menu/components/CommandMenuTopBarRightCornerIcon';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
|
||||
import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
|
||||
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { AppBasePath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronLeft, IconX } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
const StyledInputContainer = styled.div<{ isMobile: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: none;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
height: ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
|
||||
height: ${({ isMobile }) =>
|
||||
isMobile
|
||||
? COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE
|
||||
: COMMAND_MENU_SEARCH_BAR_HEIGHT}px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
padding: 0 ${({ theme }) => theme.spacing(COMMAND_MENU_SEARCH_BAR_PADDING)};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
flex-shrink: 0;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
@@ -69,10 +70,8 @@ const StyledContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledCloseButtonWrapper = styled.div<{ isVisible: boolean }>`
|
||||
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const CommandMenuTopBar = () => {
|
||||
@@ -91,83 +90,75 @@ export const CommandMenuTopBar = () => {
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
);
|
||||
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
|
||||
const commandMenuNavigationStack = useRecoilValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const { contextChips } = useCommandMenuContextChips();
|
||||
|
||||
const location = useLocation();
|
||||
const isButtonVisible =
|
||||
!location.pathname.startsWith(`${AppBasePath.Root}objects/`) &&
|
||||
!location.pathname.startsWith(`${AppBasePath.Root}object/`);
|
||||
const canGoBack = commandMenuNavigationStack.length > 1;
|
||||
|
||||
const backButtonAnimationDuration =
|
||||
contextChips.length > 0 ? theme.animation.duration.instant : 0;
|
||||
const shouldShowCloseButton =
|
||||
!isMobile && commandMenuNavigationStack.length === 1;
|
||||
|
||||
const shouldShowBackButton = canGoBack;
|
||||
|
||||
const lastChip = contextChips.at(-1);
|
||||
|
||||
return (
|
||||
<StyledInputContainer>
|
||||
<StyledInputContainer isMobile={isMobile}>
|
||||
<StyledContentContainer>
|
||||
<AnimatePresence>
|
||||
{commandMenuPage !== CommandMenuPages.Root && (
|
||||
{shouldShowBackButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: backButtonAnimationDuration,
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<CommandMenuContextChip
|
||||
Icons={[<IconChevronLeft size={theme.icon.size.sm} />]}
|
||||
onClick={goBackFromCommandMenu}
|
||||
testId="command-menu-go-back-button"
|
||||
forceEmptyText={true}
|
||||
<CommandMenuBackButton />
|
||||
</motion.div>
|
||||
)}
|
||||
{shouldShowCloseButton && (
|
||||
<motion.div
|
||||
exit={{ opacity: 0, width: 0 }}
|
||||
transition={{
|
||||
duration: theme.animation.duration.instant,
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={closeCommandMenu}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isDefined(contextStoreCurrentObjectMetadataItemId) &&
|
||||
commandMenuPage !== CommandMenuPages.SearchRecords ? (
|
||||
<CommandMenuContextChipGroupsWithRecordSelection
|
||||
contextChips={contextChips}
|
||||
objectMetadataItemId={contextStoreCurrentObjectMetadataItemId}
|
||||
/>
|
||||
) : (
|
||||
<CommandMenuContextChipGroups contextChips={contextChips} />
|
||||
)}
|
||||
{lastChip &&
|
||||
commandMenuPage !== CommandMenuPages.Root &&
|
||||
commandMenuPage !== CommandMenuPages.SearchRecords && (
|
||||
<CommandMenuPageInfo pageChip={lastChip} />
|
||||
)}
|
||||
{(commandMenuPage === CommandMenuPages.Root ||
|
||||
commandMenuPage === CommandMenuPages.SearchRecords) && (
|
||||
<>
|
||||
<StyledInput
|
||||
data-testid="command-menu-search-input"
|
||||
ref={inputRef}
|
||||
value={commandMenuSearch}
|
||||
placeholder={t`Type anything`}
|
||||
placeholder={t`Type anything...`}
|
||||
onChange={handleSearchChange}
|
||||
/>
|
||||
<CommandMenuTopBarInputFocusEffect inputRef={inputRef} />
|
||||
</>
|
||||
)}
|
||||
</StyledContentContainer>
|
||||
{!isMobile && (
|
||||
<StyledCloseButtonWrapper isVisible={isButtonVisible}>
|
||||
<Button
|
||||
Icon={IconX}
|
||||
dataTestId="page-header-close-command-menu-button"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
hotkeys={[getOsControlSymbol(), 'K']}
|
||||
ariaLabel="Close command menu"
|
||||
onClick={closeCommandMenu}
|
||||
/>
|
||||
</StyledCloseButtonWrapper>
|
||||
)}
|
||||
<CommandMenuTopBarRightCornerIcon />
|
||||
</StyledInputContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
|
||||
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconHandMove, IconSparkles } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
const StyledIconButton = styled(IconButton)`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
`;
|
||||
|
||||
export const CommandMenuTopBarRightCornerIcon = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
|
||||
const commandMenuPage = useRecoilValue(commandMenuPageState);
|
||||
const commandMenuNavigationStack = useRecoilValue(
|
||||
commandMenuNavigationStackState,
|
||||
);
|
||||
|
||||
const { goBackFromCommandMenu } = useCommandMenuHistory();
|
||||
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
|
||||
|
||||
if (isMobile || !isAiEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOnAskAIPage = [
|
||||
CommandMenuPages.AskAI,
|
||||
CommandMenuPages.ViewPreviousAIChats,
|
||||
].includes(commandMenuPage);
|
||||
|
||||
const canGoBack = commandMenuNavigationStack.length > 1;
|
||||
|
||||
if (!isOnAskAIPage) {
|
||||
return (
|
||||
<StyledIconButton
|
||||
onClick={() => openAskAIPage({ resetNavigationStack: false })}
|
||||
Icon={IconSparkles}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canGoBack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledIconButton
|
||||
Icon={IconHandMove}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={goBackFromCommandMenu}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+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>
|
||||
);
|
||||
};
|
||||
+5
-4
@@ -146,7 +146,7 @@ export const LimitedPermissions: Story = {
|
||||
export const MatchingNavigate: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
const searchInput = await canvas.findByTestId('command-menu-search-input');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'ta');
|
||||
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
|
||||
@@ -156,7 +156,7 @@ export const MatchingNavigate: Story = {
|
||||
export const MatchingNavigateShortcuts: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
const searchInput = await canvas.findByTestId('command-menu-search-input');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'gp');
|
||||
expect(await canvas.findByText('Go to People')).toBeVisible();
|
||||
@@ -169,7 +169,7 @@ export const MatchingNavigateShortcuts: Story = {
|
||||
// const canvas = within(document.body);
|
||||
// const searchRecordsButton = await canvas.findByText('Search records');
|
||||
// await userEvent.click(searchRecordsButton);
|
||||
// const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
// const searchInput = await canvas.findByPlaceholderText('Type anything...');
|
||||
// await sleep(openTimeout);
|
||||
// await userEvent.type(searchInput, 'n');
|
||||
// expect(await canvas.findByText('Linkedin')).toBeVisible();
|
||||
@@ -181,7 +181,7 @@ export const MatchingNavigateShortcuts: Story = {
|
||||
export const NoResultsSearchFallback: Story = {
|
||||
play: async () => {
|
||||
const canvas = within(document.body);
|
||||
const searchInput = await canvas.findByPlaceholderText('Type anything');
|
||||
const searchInput = await canvas.findByTestId('command-menu-search-input');
|
||||
await sleep(openTimeout);
|
||||
await userEvent.type(searchInput, 'input without results');
|
||||
expect(await canvas.findByText('No results found')).toBeVisible();
|
||||
@@ -191,6 +191,7 @@ export const NoResultsSearchFallback: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
...graphqlMocks.handlers,
|
||||
graphql.query('Search', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { CommandMenuContextChipGroups } from '../CommandMenuContextChipGroups';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import {
|
||||
IconBuildingSkyscraper,
|
||||
IconSearch,
|
||||
IconSettingsAutomation,
|
||||
IconUser,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof CommandMenuContextChipGroups> = {
|
||||
title: 'Modules/CommandMenu/CommandMenuContextChipGroups',
|
||||
component: CommandMenuContextChipGroups,
|
||||
decorators: [
|
||||
(Story) => <StyledContainer>{Story()}</StyledContainer>,
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommandMenuContextChipGroups>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SingleChip: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ThreeChipsWithIcons: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSettingsAutomation size={16} />],
|
||||
text: 'Settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const FourChipsWithIcons: Story = {
|
||||
args: {
|
||||
contextChips: [
|
||||
{
|
||||
Icons: [<IconBuildingSkyscraper size={16} />],
|
||||
text: 'Company',
|
||||
},
|
||||
{
|
||||
Icons: [<IconUser size={16} />],
|
||||
text: 'Person',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSettingsAutomation size={16} />],
|
||||
text: 'Settings',
|
||||
},
|
||||
{
|
||||
Icons: [<IconSearch size={16} />],
|
||||
text: 'Search',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { type PageLayoutTab } from '@/page-layout/types/PageLayoutTab';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconAppWindow,
|
||||
IconFrame,
|
||||
type IconComponent,
|
||||
} from 'twenty-ui/display';
|
||||
import { type PageLayoutWidget } from '~/generated/graphql';
|
||||
|
||||
type PageLayoutHeaderInfo = {
|
||||
headerIcon: IconComponent | undefined;
|
||||
headerIconColor: string;
|
||||
headerType: string;
|
||||
title: string;
|
||||
isReadonly: boolean;
|
||||
tab: PageLayoutTab | undefined;
|
||||
widgetInEditMode: PageLayoutWidget | undefined;
|
||||
};
|
||||
|
||||
type UsePageLayoutHeaderInfoParams = {
|
||||
commandMenuPage: CommandMenuPages;
|
||||
draftPageLayout: {
|
||||
tabs: PageLayoutTab[];
|
||||
};
|
||||
pageLayoutEditingWidgetId: string | null | undefined;
|
||||
openTabId: string | null | undefined;
|
||||
editedTitle: string | null | undefined;
|
||||
};
|
||||
|
||||
export const usePageLayoutHeaderInfo = ({
|
||||
commandMenuPage,
|
||||
draftPageLayout,
|
||||
pageLayoutEditingWidgetId,
|
||||
openTabId,
|
||||
editedTitle,
|
||||
}: UsePageLayoutHeaderInfoParams): PageLayoutHeaderInfo | null => {
|
||||
const theme = useTheme();
|
||||
const iconColor = theme.font.color.tertiary;
|
||||
|
||||
switch (commandMenuPage) {
|
||||
case CommandMenuPages.PageLayoutTabSettings: {
|
||||
if (!isDefined(openTabId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tab = draftPageLayout.tabs.find((t) => t.id === openTabId);
|
||||
|
||||
if (!isDefined(tab)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(tab.title) && tab.title !== ''
|
||||
? tab.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconAppWindow,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`Tab`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab,
|
||||
widgetInEditMode: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutIframeSettings: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: IconFrame,
|
||||
headerIconColor: iconColor,
|
||||
headerType: t`iFrame Widget`,
|
||||
title,
|
||||
isReadonly: false,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
case CommandMenuPages.PageLayoutGraphTypeSelect:
|
||||
case CommandMenuPages.PageLayoutGraphFilter: {
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const widgetInEditMode = draftPageLayout.tabs
|
||||
.flatMap((tab) => tab.widgets)
|
||||
.find((widget) => widget.id === pageLayoutEditingWidgetId);
|
||||
|
||||
if (!isDefined(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isChartWidget(widgetInEditMode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = widgetInEditMode.configuration.graphType;
|
||||
const graphTypeInfo = GRAPH_TYPE_INFORMATION[currentGraphType];
|
||||
const graphTypeLabel = t(graphTypeInfo.label);
|
||||
|
||||
const headerType =
|
||||
commandMenuPage === CommandMenuPages.PageLayoutGraphFilter
|
||||
? t`${graphTypeLabel} Chart`
|
||||
: t`Chart`;
|
||||
|
||||
const title = isDefined(editedTitle)
|
||||
? editedTitle
|
||||
: isDefined(widgetInEditMode.title) && widgetInEditMode.title !== ''
|
||||
? widgetInEditMode.title
|
||||
: '';
|
||||
|
||||
return {
|
||||
headerIcon: graphTypeInfo.icon,
|
||||
headerIconColor: iconColor,
|
||||
headerType,
|
||||
title,
|
||||
isReadonly: commandMenuPage === CommandMenuPages.PageLayoutGraphFilter,
|
||||
tab: undefined,
|
||||
widgetInEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const COMMAND_MENU_NAVIGATION_HISTORY_DROPDOWN_ID =
|
||||
'command-menu-navigation-history-dropdown';
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const COMMAND_MENU_SEARCH_BAR_HEIGHT = 56;
|
||||
export const COMMAND_MENU_SEARCH_BAR_HEIGHT = 40;
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE = 52;
|
||||
@@ -0,0 +1 @@
|
||||
export const COMMAND_MENU_SIDE_PANEL_WIDTH = 400;
|
||||
+43
@@ -8,6 +8,8 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageInfoState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { IconDotsVertical } from 'twenty-ui/display';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
@@ -84,4 +86,45 @@ describe('useCommandMenu', () => {
|
||||
|
||||
expect(result.current.isCommandMenuOpened).toBe(false);
|
||||
});
|
||||
|
||||
it('should navigate command menu and reset navigation stack when resetNavigationStack is true', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'First Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
|
||||
expect(result.current.commandMenuPageInfo.title).toBe('First Page');
|
||||
expect(result.current.commandMenuNavigationStack).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Second Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: false,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuNavigationStack).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Reset Page',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.commandMenuPage).toBe(CommandMenuPages.Root);
|
||||
expect(result.current.commandMenuPageInfo.title).toBe('Reset Page');
|
||||
expect(result.current.commandMenuNavigationStack).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { type ActionConfig } from '@/action-menu/actions/types/ActionConfig';
|
||||
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
|
||||
import { ActionType } from '@/action-menu/actions/types/ActionType';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { useFilterActionsWithCommandMenuSearch } from '../useFilterActionsWithCommandMenuSearch';
|
||||
|
||||
const MockComponent = <div>Mock Component</div>;
|
||||
|
||||
describe('useFilterActionsWithCommandMenuSearch', () => {
|
||||
const mockActions: ActionConfig[] = [
|
||||
{
|
||||
key: 'action-1',
|
||||
label: 'Create Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.Global,
|
||||
position: 1,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
hotKeys: ['c', 'r'],
|
||||
},
|
||||
{
|
||||
key: 'action-2',
|
||||
label: 'Delete Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.RecordSelection,
|
||||
position: 2,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
hotKeys: ['d', 'e', 'l'],
|
||||
},
|
||||
{
|
||||
key: 'action-3',
|
||||
label: 'Update Record',
|
||||
type: ActionType.Standard,
|
||||
scope: ActionScope.Object,
|
||||
position: 3,
|
||||
Icon: IconPlus,
|
||||
shouldBeRegistered: () => true,
|
||||
component: MockComponent,
|
||||
},
|
||||
];
|
||||
|
||||
it('should return all actions when search is empty', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: '' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toEqual(mockActions);
|
||||
});
|
||||
|
||||
it('should filter actions by label', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Create' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].key).toBe('action-1');
|
||||
});
|
||||
|
||||
it('should filter actions by hotkeys', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'del' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].key).toBe('action-2');
|
||||
});
|
||||
|
||||
it('should return empty array when no actions match', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'xyz' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
|
||||
it('should match actions by either label or hotkeys', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useFilterActionsWithCommandMenuSearch({ commandMenuSearch: 'Record' }),
|
||||
);
|
||||
|
||||
const filtered =
|
||||
result.current.filterActionsWithCommandMenuSearch(mockActions);
|
||||
|
||||
expect(filtered).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
@@ -69,7 +69,7 @@ export const useCommandMenuHotKeys = () => {
|
||||
keys: ['@'],
|
||||
callback: () => {
|
||||
if (isAiEnabled) {
|
||||
openAskAIPage();
|
||||
openAskAIPage({ resetNavigationStack: true });
|
||||
}
|
||||
},
|
||||
containsModifier: false,
|
||||
|
||||
+16
-1
@@ -1,18 +1,33 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconSparkles } from 'twenty-ui/display';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
export const useOpenAskAIPageInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const openAskAIPage = ({
|
||||
pageTitle,
|
||||
resetNavigationStack,
|
||||
}: {
|
||||
pageTitle?: string | null;
|
||||
resetNavigationStack?: boolean;
|
||||
} = {}) => {
|
||||
const shouldReset =
|
||||
resetNavigationStack !== undefined
|
||||
? resetNavigationStack
|
||||
: isCommandMenuOpened;
|
||||
|
||||
const openAskAIPage = (pageTitle?: string | null) => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.AskAI,
|
||||
pageTitle: pageTitle ?? t`Ask AI`,
|
||||
pageIcon: IconSparkles,
|
||||
pageId: v4(),
|
||||
resetNavigationStack: shouldReset,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+4
@@ -1,11 +1,14 @@
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
import { IconSearch } from 'twenty-ui/display';
|
||||
|
||||
export const useOpenRecordsSearchPageInCommandMenu = () => {
|
||||
const { navigateCommandMenu } = useCommandMenu();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const openRecordsSearchPage = () => {
|
||||
navigateCommandMenu({
|
||||
@@ -13,6 +16,7 @@ export const useOpenRecordsSearchPageInCommandMenu = () => {
|
||||
pageTitle: t`Search`,
|
||||
pageIcon: IconSearch,
|
||||
pageId: v4(),
|
||||
resetNavigationStack: isCommandMenuOpened,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
|
||||
import { commandMenuWorkflowIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowIdComponentState';
|
||||
import { commandMenuWorkflowRunIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowRunIdComponentState';
|
||||
import { commandMenuWorkflowStepIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowStepIdComponentState';
|
||||
import { commandMenuWorkflowVersionIdComponentState } from '@/command-menu/pages/workflow/states/commandMenuWorkflowVersionIdComponentState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { type WorkflowRunStepStatus } from '@/workflow/types/Workflow';
|
||||
import { useSetInitialWorkflowRunRightDrawerTab } from '@/workflow/workflow-diagram/hooks/useSetInitialWorkflowRunRightDrawerTab';
|
||||
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconBolt,
|
||||
type IconComponent,
|
||||
@@ -67,7 +70,12 @@ export const useWorkflowCommandMenu = () => {
|
||||
|
||||
const openWorkflowEditStepInCommandMenu = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return (workflowId: string, title: string, icon: IconComponent) => {
|
||||
return (
|
||||
workflowId: string,
|
||||
title: string,
|
||||
icon: IconComponent,
|
||||
stepId?: string,
|
||||
) => {
|
||||
const pageId = v4();
|
||||
|
||||
set(
|
||||
@@ -77,6 +85,22 @@ export const useWorkflowCommandMenu = () => {
|
||||
workflowId,
|
||||
);
|
||||
|
||||
if (isDefined(stepId)) {
|
||||
set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
|
||||
set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
}
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepEdit,
|
||||
pageTitle: title,
|
||||
@@ -118,11 +142,13 @@ export const useWorkflowCommandMenu = () => {
|
||||
workflowVersionId,
|
||||
title,
|
||||
icon,
|
||||
stepId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
workflowVersionId: string;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
stepId?: string;
|
||||
}) => {
|
||||
const pageId = v4();
|
||||
|
||||
@@ -139,6 +165,22 @@ export const useWorkflowCommandMenu = () => {
|
||||
workflowVersionId,
|
||||
);
|
||||
|
||||
if (isDefined(stepId)) {
|
||||
set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
|
||||
set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowVersionId,
|
||||
}),
|
||||
stepId,
|
||||
);
|
||||
}
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowStepView,
|
||||
pageTitle: title,
|
||||
@@ -181,6 +223,19 @@ export const useWorkflowCommandMenu = () => {
|
||||
}),
|
||||
workflowRunId,
|
||||
);
|
||||
set(
|
||||
commandMenuWorkflowStepIdComponentState.atomFamily({
|
||||
instanceId: pageId,
|
||||
}),
|
||||
workflowSelectedNode,
|
||||
);
|
||||
|
||||
set(
|
||||
workflowSelectedNodeComponentState.atomFamily({
|
||||
instanceId: workflowRunId,
|
||||
}),
|
||||
workflowSelectedNode,
|
||||
);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.WorkflowRunStepView,
|
||||
|
||||
+1
@@ -172,6 +172,7 @@ export const CommandMenuMessageThreadPage = () => {
|
||||
{canReply && !messageChannelLoading && (
|
||||
<StyledButtonContainer isMobile={isMobile}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleReplyClick}
|
||||
title={t`Reply`}
|
||||
Icon={IconArrowBackUp}
|
||||
|
||||
-16
@@ -1,19 +1,13 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { ChartFiltersSettings } from '@/command-menu/pages/page-layout/components/ChartFiltersSettings';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { isChartWidget } from '@/command-menu/pages/page-layout/utils/isChartWidget';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuPageLayoutGraphFilter = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const draftPageLayout = useRecoilComponentValue(
|
||||
@@ -52,18 +46,8 @@ export const CommandMenuPageLayoutGraphFilter = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = widgetInEditMode.configuration.graphType;
|
||||
const graphTypeLabel = t(GRAPH_TYPE_INFORMATION[currentGraphType].label);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={widgetInEditMode.title}
|
||||
headerType={t`${graphTypeLabel} Chart`}
|
||||
disabled
|
||||
/>
|
||||
<ChartFiltersSettings
|
||||
widget={widgetInEditMode}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
|
||||
-26
@@ -1,15 +1,9 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { ChartSettings } from '@/command-menu/pages/page-layout/components/ChartSettings';
|
||||
import { GRAPH_TYPE_INFORMATION } from '@/command-menu/pages/page-layout/constants/GraphTypeInformation';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
|
||||
import { GraphWidgetComponentInstanceContext } from '@/page-layout/widgets/graph/states/contexts/GraphWidgetComponentInstanceContext';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
@@ -25,8 +19,6 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
pageLayoutId,
|
||||
);
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
|
||||
if (!isDefined(pageLayoutEditingWidgetId)) {
|
||||
throw new Error('Widget ID must be present while editing the widget');
|
||||
}
|
||||
@@ -41,8 +33,6 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if (
|
||||
!isDefined(widgetInEditMode.configuration) ||
|
||||
!('graphType' in widgetInEditMode.configuration)
|
||||
@@ -50,26 +40,10 @@ export const CommandMenuPageLayoutGraphTypeSelect = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentGraphType = widgetInEditMode.configuration.graphType;
|
||||
const graphTypeLabel = t(GRAPH_TYPE_INFORMATION[currentGraphType].label);
|
||||
|
||||
return (
|
||||
<GraphWidgetComponentInstanceContext.Provider
|
||||
value={{ instanceId: widgetInEditMode.id }}
|
||||
>
|
||||
<SidePanelHeader
|
||||
Icon={GRAPH_TYPE_INFORMATION[currentGraphType].icon}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={widgetInEditMode.title}
|
||||
headerType={t`${graphTypeLabel} Chart`}
|
||||
onTitleChange={(newTitle) => {
|
||||
if (isNonEmptyString(newTitle)) {
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: newTitle,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChartSettings widget={widgetInEditMode} />
|
||||
</GraphWidgetComponentInstanceContext.Provider>
|
||||
);
|
||||
|
||||
-20
@@ -1,15 +1,12 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useWidgetInEditMode } from '@/command-menu/pages/page-layout/hooks/useWidgetInEditMode';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { useUpdatePageLayoutWidget } from '@/page-layout/hooks/useUpdatePageLayoutWidget';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString, isString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { isDefined, isValidUrl } from 'twenty-shared/utils';
|
||||
import { IconFrame } from 'twenty-ui/display';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -21,8 +18,6 @@ const StyledContainer = styled.div`
|
||||
export const CommandMenuPageLayoutIframeSettings = () => {
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const { widgetInEditMode } = useWidgetInEditMode(pageLayoutId);
|
||||
|
||||
const { updatePageLayoutWidget } = useUpdatePageLayoutWidget(pageLayoutId);
|
||||
@@ -74,21 +69,6 @@ export const CommandMenuPageLayoutIframeSettings = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={IconFrame}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={widgetInEditMode.title}
|
||||
headerType={t`iFrame Widget`}
|
||||
onTitleChange={(newTitle) => {
|
||||
if (!isNonEmptyString(newTitle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updatePageLayoutWidget(widgetInEditMode.id, {
|
||||
title: newTitle,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<StyledContainer>
|
||||
<FormTextFieldInput
|
||||
label={t`URL to Embed`}
|
||||
|
||||
-18
@@ -1,26 +1,21 @@
|
||||
import { CommandGroup } from '@/command-menu/components/CommandGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { CommandMenuList } from '@/command-menu/components/CommandMenuList';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { TAB_SETTINGS_SELECTABLE_ITEM_IDS } from '@/command-menu/pages/page-layout/constants/settings/TabSettingsSelectableItemIds';
|
||||
import { usePageLayoutIdFromContextStoreTargetedRecord } from '@/command-menu/pages/page-layout/hooks/usePageLayoutFromContextStoreTargetedRecord';
|
||||
import { useDeletePageLayoutTab } from '@/page-layout/hooks/useDeletePageLayoutTab';
|
||||
import { useDuplicatePageLayoutTab } from '@/page-layout/hooks/useDuplicatePageLayoutTab';
|
||||
import { useMovePageLayoutTab } from '@/page-layout/hooks/useMovePageLayoutTab';
|
||||
import { useUpdatePageLayoutTab } from '@/page-layout/hooks/useUpdatePageLayoutTab';
|
||||
import { pageLayoutDraftComponentState } from '@/page-layout/states/pageLayoutDraftComponentState';
|
||||
import { pageLayoutTabSettingsOpenTabIdComponentState } from '@/page-layout/states/pageLayoutTabSettingsOpenTabIdComponentState';
|
||||
import { sortTabsByPosition } from '@/page-layout/utils/sortTabsByPosition';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconAppWindow,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconCopyPlus,
|
||||
@@ -28,7 +23,6 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
export const CommandMenuPageLayoutTabSettings = () => {
|
||||
const theme = useTheme();
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
const { pageLayoutId } = usePageLayoutIdFromContextStoreTargetedRecord();
|
||||
|
||||
@@ -44,7 +38,6 @@ export const CommandMenuPageLayoutTabSettings = () => {
|
||||
|
||||
const { moveLeft, moveRight } = useMovePageLayoutTab(pageLayoutId);
|
||||
const { deleteTab } = useDeletePageLayoutTab(pageLayoutId);
|
||||
const { updatePageLayoutTab } = useUpdatePageLayoutTab(pageLayoutId);
|
||||
const { duplicateTab } = useDuplicatePageLayoutTab(pageLayoutId);
|
||||
|
||||
if (!isDefined(openTabId)) {
|
||||
@@ -69,17 +62,6 @@ export const CommandMenuPageLayoutTabSettings = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={IconAppWindow}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={tab.title}
|
||||
headerType={t`Tab`}
|
||||
onTitleChange={(newTitle) => {
|
||||
if (isDefined(newTitle) && isNonEmptyString(newTitle)) {
|
||||
updatePageLayoutTab(tab.id, { title: newTitle });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<CommandMenuList
|
||||
commandGroups={[]}
|
||||
selectableItemIds={Object.values(TAB_SETTINGS_SELECTABLE_ITEM_IDS)}
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ type NavigatePageLayoutCommandMenuProps = {
|
||||
pageTitle?: string;
|
||||
pageIcon?: IconComponent;
|
||||
focusTitleInput?: boolean;
|
||||
resetNavigationStack?: boolean;
|
||||
};
|
||||
|
||||
export const useNavigatePageLayoutCommandMenu = () => {
|
||||
@@ -22,6 +23,7 @@ export const useNavigatePageLayoutCommandMenu = () => {
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
focusTitleInput = false,
|
||||
resetNavigationStack = false,
|
||||
}: NavigatePageLayoutCommandMenuProps) => {
|
||||
navigateCommandMenu({
|
||||
page: commandMenuPage,
|
||||
@@ -32,6 +34,7 @@ export const useNavigatePageLayoutCommandMenu = () => {
|
||||
? pageIcon
|
||||
: getPageLayoutIcon(commandMenuPage),
|
||||
focusTitleInput,
|
||||
resetNavigationStack,
|
||||
});
|
||||
};
|
||||
}, [navigateCommandMenu]);
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
export const commandMenuWorkflowStepIdComponentState = createComponentState<
|
||||
string | undefined
|
||||
>({
|
||||
key: 'command-menu/workflow-step-id',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: CommandMenuPageComponentInstanceContext,
|
||||
});
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowCreateStepContent = () => {
|
||||
workflowVisualizerWorkflowId,
|
||||
createdStep.name,
|
||||
getIcon(getActionIcon(createdStep.type as WorkflowActionType)),
|
||||
createdStep.id,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowEditStepTypeContent = () => {
|
||||
workflowVisualizerWorkflowId,
|
||||
updatedStep.name,
|
||||
getIcon(getActionIcon(updatedStep.type as WorkflowActionType)),
|
||||
updatedStep.id,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -66,6 +66,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = () => {
|
||||
workflowId,
|
||||
defaultLabel,
|
||||
getIcon(icon),
|
||||
TRIGGER_STEP_ID,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const MainNavigationDrawerFixedItems = () => {
|
||||
<NavigationDrawerItem
|
||||
label={t`Ask AI`}
|
||||
Icon={IconSparkles}
|
||||
onClick={() => openAskAIPage()}
|
||||
onClick={() => openAskAIPage({ resetNavigationStack: true })}
|
||||
keyboard={['@']}
|
||||
mouseUpNavigation={true}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
|
||||
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
|
||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
type CommandMenuPageLayoutProps = {
|
||||
children: ReactNode;
|
||||
isSidePanelOpen?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_SIDE_PANEL_WIDTH = 400;
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledPageBody = styled(PageBody)`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
`;
|
||||
|
||||
const StyledSidePanelWrapper = styled(motion.div)`
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSidePanel = styled(motion.aside)`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: ${DEFAULT_SIDE_PANEL_WIDTH}px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const StyledModalContainer = styled.div`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const CommandMenuPageLayout = ({
|
||||
children,
|
||||
isSidePanelOpen,
|
||||
}: CommandMenuPageLayoutProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const setTableWidthResizeIsActive = useSetRecoilState(
|
||||
tableWidthResizeIsActiveState,
|
||||
);
|
||||
|
||||
const resolvedIsSidePanelOpen =
|
||||
isSidePanelOpen ?? isCommandMenuOpened ?? false;
|
||||
|
||||
const [shouldRenderContent, setShouldRenderContent] = useState(
|
||||
resolvedIsSidePanelOpen,
|
||||
);
|
||||
|
||||
const shouldShowContent = resolvedIsSidePanelOpen || shouldRenderContent;
|
||||
|
||||
const handleAnimationComplete = () => {
|
||||
if (!resolvedIsSidePanelOpen) {
|
||||
setShouldRenderContent(false);
|
||||
}
|
||||
|
||||
setTableWidthResizeIsActive(true);
|
||||
};
|
||||
|
||||
const handleAnimationStart = () => {
|
||||
if (resolvedIsSidePanelOpen && !shouldRenderContent) {
|
||||
setShouldRenderContent(true);
|
||||
}
|
||||
|
||||
setTableWidthResizeIsActive(false);
|
||||
};
|
||||
|
||||
const handleModalContainerRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
setModalContainer(element);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useCommandMenuHotKeys();
|
||||
|
||||
if (isMobile) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledLayout>
|
||||
<StyledPageBody>{children}</StyledPageBody>
|
||||
|
||||
<StyledSidePanelWrapper
|
||||
initial={false}
|
||||
animate={{
|
||||
width: resolvedIsSidePanelOpen ? DEFAULT_SIDE_PANEL_WIDTH : 0,
|
||||
marginLeft: resolvedIsSidePanelOpen ? theme.spacing(2) : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
onAnimationStart={handleAnimationStart}
|
||||
onAnimationComplete={handleAnimationComplete}
|
||||
>
|
||||
<StyledSidePanel
|
||||
initial={false}
|
||||
animate={{
|
||||
x: resolvedIsSidePanelOpen ? 0 : DEFAULT_SIDE_PANEL_WIDTH,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
<StyledModalContainer ref={handleModalContainerRef} />
|
||||
<ModalContainerContext.Provider value={{ container: modalContainer }}>
|
||||
{shouldShowContent && <CommandMenuRouter />}
|
||||
</ModalContainerContext.Provider>
|
||||
</StyledSidePanel>
|
||||
</StyledSidePanelWrapper>
|
||||
</StyledLayout>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { COMMAND_MENU_SIDE_PANEL_WIDTH } from '@/command-menu/constants/CommandMenuSidePanelWidth';
|
||||
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
|
||||
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
|
||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { type ReactNode, useCallback, useState } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
type CommandMenuPageLayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledPageBody = styled(PageBody)`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-bottom: 0;
|
||||
padding-right: 0;
|
||||
`;
|
||||
|
||||
const StyledSidePanelWrapper = styled(motion.div)`
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSidePanel = styled(motion.aside)`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: ${COMMAND_MENU_SIDE_PANEL_WIDTH}px;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
const StyledModalContainer = styled.div`
|
||||
height: 100%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export const CommandMenuPageLayout = ({
|
||||
children,
|
||||
}: CommandMenuPageLayoutProps) => {
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
const [modalContainer, setModalContainer] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const setTableWidthResizeIsActive = useSetRecoilState(
|
||||
tableWidthResizeIsActiveState,
|
||||
);
|
||||
|
||||
const [shouldRenderContent, setShouldRenderContent] =
|
||||
useState(isCommandMenuOpened);
|
||||
|
||||
const shouldShowContent = isCommandMenuOpened || shouldRenderContent;
|
||||
|
||||
const handleAnimationComplete = () => {
|
||||
if (!isCommandMenuOpened) {
|
||||
setShouldRenderContent(false);
|
||||
}
|
||||
|
||||
setTableWidthResizeIsActive(true);
|
||||
};
|
||||
|
||||
const handleAnimationStart = () => {
|
||||
if (isCommandMenuOpened && !shouldRenderContent) {
|
||||
setShouldRenderContent(true);
|
||||
}
|
||||
|
||||
setTableWidthResizeIsActive(false);
|
||||
};
|
||||
|
||||
const handleModalContainerRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
setModalContainer(element);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useCommandMenuHotKeys();
|
||||
|
||||
if (isMobile) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledLayout>
|
||||
<StyledPageBody>{children}</StyledPageBody>
|
||||
|
||||
<StyledSidePanelWrapper
|
||||
initial={false}
|
||||
animate={{
|
||||
width: isCommandMenuOpened ? COMMAND_MENU_SIDE_PANEL_WIDTH : 0,
|
||||
marginLeft: isCommandMenuOpened ? theme.spacing(2) : 0,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
onAnimationStart={handleAnimationStart}
|
||||
onAnimationComplete={handleAnimationComplete}
|
||||
>
|
||||
<StyledSidePanel
|
||||
initial={false}
|
||||
animate={{
|
||||
x: isCommandMenuOpened ? 0 : COMMAND_MENU_SIDE_PANEL_WIDTH,
|
||||
}}
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
<StyledModalContainer ref={handleModalContainerRef} />
|
||||
<ModalContainerContext.Provider value={{ container: modalContainer }}>
|
||||
{shouldShowContent && <CommandMenuRouter />}
|
||||
</ModalContainerContext.Provider>
|
||||
</StyledSidePanel>
|
||||
</StyledSidePanelWrapper>
|
||||
</StyledLayout>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -3,6 +3,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
|
||||
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
|
||||
import { CommandMenuPageLayout } from '@/object-record/components/CommandMenuPageLayout';
|
||||
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
|
||||
@@ -13,7 +14,6 @@ import { RecordIndexPageHeader } from '@/object-record/record-index/components/R
|
||||
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
|
||||
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
|
||||
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
|
||||
import { PageBody } from '@/ui/layout/page/components/PageBody';
|
||||
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
@@ -98,14 +98,14 @@ export const RecordIndexContainerGater = () => {
|
||||
>
|
||||
<PageTitle title={objectMetadataItem.labelPlural} />
|
||||
<RecordIndexPageHeader />
|
||||
<PageBody>
|
||||
<CommandMenuPageLayout>
|
||||
<StyledIndexContainer
|
||||
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
|
||||
>
|
||||
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
|
||||
<RecordIndexContainer />
|
||||
</StyledIndexContainer>
|
||||
</PageBody>
|
||||
</CommandMenuPageLayout>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</RecordComponentInstanceContextsWrapper>
|
||||
<RecordIndexLoadBaseOnContextStoreEffect />
|
||||
|
||||
+26
-19
@@ -1,8 +1,10 @@
|
||||
import { recordTableWidthComponentState } from '@/object-record/record-table/states/recordTableWidthComponentState';
|
||||
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RecordTableWidthEffect = () => {
|
||||
@@ -10,33 +12,38 @@ export const RecordTableWidthEffect = () => {
|
||||
recordTableWidthComponentState,
|
||||
);
|
||||
|
||||
const tableWidthResizeIsActive = useRecoilValue(
|
||||
tableWidthResizeIsActiveState,
|
||||
);
|
||||
|
||||
const { scrollWrapperHTMLElement } = useScrollWrapperHTMLElement();
|
||||
|
||||
useEffect(() => {
|
||||
const tableWidth = scrollWrapperHTMLElement?.clientWidth ?? 0;
|
||||
|
||||
if (tableWidth > 0) {
|
||||
setRecordTableWidth(tableWidth);
|
||||
}
|
||||
|
||||
const tableResizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === scrollWrapperHTMLElement) {
|
||||
const newWidth = scrollWrapperHTMLElement.clientWidth;
|
||||
|
||||
setRecordTableWidth(newWidth);
|
||||
}
|
||||
if (tableWidthResizeIsActive) {
|
||||
if (tableWidth > 0) {
|
||||
setRecordTableWidth(tableWidth);
|
||||
}
|
||||
});
|
||||
|
||||
if (isDefined(scrollWrapperHTMLElement)) {
|
||||
tableResizeObserver.observe(scrollWrapperHTMLElement);
|
||||
const tableResizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === scrollWrapperHTMLElement) {
|
||||
const newWidth = scrollWrapperHTMLElement.clientWidth;
|
||||
setRecordTableWidth(newWidth);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (isDefined(scrollWrapperHTMLElement)) {
|
||||
tableResizeObserver.observe(scrollWrapperHTMLElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
tableResizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
tableResizeObserver.disconnect();
|
||||
};
|
||||
}, [setRecordTableWidth, scrollWrapperHTMLElement]);
|
||||
}, [setRecordTableWidth, scrollWrapperHTMLElement, tableWidthResizeIsActive]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const tableWidthResizeIsActiveState = createState({
|
||||
key: 'tableWidthResizeIsActiveState',
|
||||
defaultValue: true,
|
||||
});
|
||||
+1
-1
@@ -26,7 +26,7 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
type RecordTitleCellProps = {
|
||||
loading?: boolean;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
sizeVariant?: 'xs' | 'sm' | 'md';
|
||||
containerType: RecordTitleCellContainerType;
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { RecordTitleFullNameFieldInput } from '@/object-record/record-title-cell
|
||||
|
||||
type RecordTitleCellFieldInputProps = {
|
||||
instanceId: string;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
sizeVariant?: 'xs' | 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellFieldInput = ({
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { turnIntoUndefinedIfWhitespacesOnly } from '~/utils/string/turnIntoUndef
|
||||
|
||||
type RecordTitleCellTextFieldInputProps = {
|
||||
instanceId: string;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
sizeVariant?: 'xs' | 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleCellTextFieldInput = ({
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ type RecordTitleDoubleTextInputProps = {
|
||||
) => void;
|
||||
onChange?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
onPaste?: (newDoubleTextValue: FieldDoubleText) => void;
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
sizeVariant?: 'xs' | 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleDoubleTextInput = ({
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { useContext } from 'react';
|
||||
import { RecordTitleDoubleTextInput } from './RecordTitleDoubleTextInput';
|
||||
|
||||
type RecordTitleFullNameFieldInputProps = {
|
||||
sizeVariant?: 'xs' | 'md';
|
||||
sizeVariant?: 'xs' | 'sm' | 'md';
|
||||
};
|
||||
|
||||
export const RecordTitleFullNameFieldInput = ({
|
||||
|
||||
@@ -37,6 +37,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutIframeSettings,
|
||||
pageTitle: t`Edit iFrame`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,6 +45,7 @@ export const useEditPageLayoutWidget = (pageLayoutIdFromProps?: string) => {
|
||||
navigatePageLayoutCommandMenu({
|
||||
commandMenuPage: CommandMenuPages.PageLayoutGraphTypeSelect,
|
||||
pageTitle: t`Edit Graph`,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ const StyledEditor = styled.div`
|
||||
width: 100%;
|
||||
|
||||
& .editor {
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
min-height: 400px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
|
||||
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
|
||||
import { useModalContainer } from '@/ui/layout/modal/contexts/ModalContainerContext';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
|
||||
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
|
||||
@@ -13,6 +14,7 @@ import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React, { useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
const StyledModalDiv = styled(motion.div)<{
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
@@ -122,19 +124,23 @@ const StyledFooter = styled.div`
|
||||
|
||||
const StyledBackDrop = styled(motion.div)<{
|
||||
modalVariant: ModalVariants;
|
||||
isInContainer?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ theme, modalVariant }) =>
|
||||
modalVariant === 'primary' || modalVariant === 'transparent'
|
||||
? theme.background.overlayPrimary
|
||||
: modalVariant === 'secondary'
|
||||
? theme.background.overlaySecondary
|
||||
: theme.background.overlayTertiary};
|
||||
background: ${({ theme, modalVariant, isInContainer }) =>
|
||||
isInContainer
|
||||
? theme.background.overlayTertiary
|
||||
: modalVariant === 'primary' || modalVariant === 'transparent'
|
||||
? theme.background.overlayPrimary
|
||||
: modalVariant === 'secondary'
|
||||
? theme.background.overlaySecondary
|
||||
: theme.background.overlayTertiary};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
pointer-events: auto;
|
||||
position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: ${RootStackingContextZIndices.RootModalBackDrop};
|
||||
@@ -220,6 +226,8 @@ export const Modal = ({
|
||||
}: ModalProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const { container } = useModalContainer();
|
||||
const isInContainer = container !== null;
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -239,7 +247,7 @@ export const Modal = ({
|
||||
if (shouldCloseModalOnClickOutsideOrEscape) closeModal(modalId);
|
||||
};
|
||||
|
||||
return (
|
||||
const modalContent = (
|
||||
<AnimatePresence mode="wait">
|
||||
{isModalOpened && (
|
||||
<ModalComponentInstanceContext.Provider
|
||||
@@ -264,6 +272,7 @@ export const Modal = ({
|
||||
data-click-outside-id={MODAL_BACKDROP_CLICK_OUTSIDE_ID}
|
||||
onMouseDown={stopEventPropagation}
|
||||
modalVariant={modalVariant}
|
||||
isInContainer={isInContainer}
|
||||
>
|
||||
<StyledModalDiv
|
||||
ref={modalRef}
|
||||
@@ -290,6 +299,12 @@ export const Modal = ({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
if (container !== null) {
|
||||
return createPortal(modalContent, container);
|
||||
}
|
||||
|
||||
return modalContent;
|
||||
};
|
||||
|
||||
Modal.Header = ModalHeader;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type ModalContainerContextValue = {
|
||||
container: HTMLElement | null;
|
||||
};
|
||||
|
||||
export const ModalContainerContext = createContext<ModalContainerContextValue>({
|
||||
container: null,
|
||||
});
|
||||
|
||||
export const useModalContainer = () => {
|
||||
return useContext(ModalContainerContext);
|
||||
};
|
||||
@@ -1,5 +1,7 @@
|
||||
import { AuthModal } from '@/auth/components/AuthModal';
|
||||
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
|
||||
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
|
||||
import { AppPageErrorFallback } from '@/error-handler/components/AppPageErrorFallback';
|
||||
@@ -19,6 +21,7 @@ import { Global, css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useScreenSize } from 'twenty-ui/utilities';
|
||||
|
||||
const StyledLayout = styled.div`
|
||||
@@ -64,6 +67,7 @@ export const DefaultLayout = () => {
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
const useShowFullScreen = useShowFullscreen();
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -92,12 +96,7 @@ export const DefaultLayout = () => {
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
{!showAuthModal && (
|
||||
<>
|
||||
<CommandMenuRouter />
|
||||
<KeyboardShortcutMenu />
|
||||
</>
|
||||
)}
|
||||
{!showAuthModal && <KeyboardShortcutMenu />}
|
||||
{showAuthModal ? (
|
||||
<StyledAppNavigationDrawerMock />
|
||||
) : useShowFullScreen ? null : (
|
||||
@@ -125,6 +124,15 @@ export const DefaultLayout = () => {
|
||||
)}
|
||||
</StyledPageContainer>
|
||||
{isMobile && !showAuthModal && <MobileNavigationBar />}
|
||||
{isMobile && (
|
||||
<AnimatePresence>
|
||||
{isCommandMenuOpened && (
|
||||
<CommandMenuOpenContainer>
|
||||
<CommandMenuRouter />
|
||||
</CommandMenuOpenContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</AppErrorBoundary>
|
||||
</StyledLayout>
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PagePanel } from './PagePanel';
|
||||
|
||||
type PageBodyProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
@@ -38,8 +39,8 @@ const StyledLeftContainer = styled.div<LeftContainerProps>`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const PageBody = ({ children }: PageBodyProps) => (
|
||||
<StyledMainContainer>
|
||||
export const PageBody = ({ children, className }: PageBodyProps) => (
|
||||
<StyledMainContainer className={className}>
|
||||
<StyledLeftContainer>
|
||||
<PagePanel>{children}</PagePanel>
|
||||
</StyledLeftContainer>
|
||||
|
||||
+15
-5
@@ -33,12 +33,22 @@ export const SelectableListItem = ({
|
||||
const listItemRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectedItemId) {
|
||||
listItemRef.current?.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'start',
|
||||
});
|
||||
if (!isSelectedItemId || !listItemRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollContainer = listItemRef.current.closest(
|
||||
'[id^="scroll-wrapper-"]',
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (isDefined(scrollContainer) && scrollContainer.scrollTop === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
listItemRef.current.scrollIntoView({
|
||||
behavior: 'auto',
|
||||
block: 'start',
|
||||
});
|
||||
}, [isSelectedItemId]);
|
||||
|
||||
return (
|
||||
|
||||
+19
-5
@@ -20,6 +20,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import React from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledShowPageRightContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -44,12 +45,23 @@ const StyledTabList = styled(TabList)`
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>`
|
||||
const StyledContentContainer = styled.div<{
|
||||
isInRightDrawer: boolean;
|
||||
}>`
|
||||
background: ${({ theme, isInRightDrawer }) =>
|
||||
isInRightDrawer ? theme.background.secondary : theme.background.primary};
|
||||
border: ${({ theme, isInRightDrawer }) =>
|
||||
isInRightDrawer ? `1px solid ${theme.border.color.light}` : 'none'};
|
||||
border-radius: ${({ theme, isInRightDrawer }) =>
|
||||
isInRightDrawer ? theme.border.radius.md : '0'};
|
||||
flex: 1;
|
||||
margin: ${({ theme, isInRightDrawer }) =>
|
||||
isInRightDrawer ? theme.spacing(3) : '0'};
|
||||
overflow-y: auto;
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
padding-bottom: ${({ theme, isInRightDrawer }) =>
|
||||
isInRightDrawer ? theme.spacing(16) : 0};
|
||||
|
||||
.scroll-wrapper-y-enabled {
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
type ShowPageSubContainerProps = {
|
||||
@@ -109,6 +121,8 @@ export const ShowPageSubContainer = ({
|
||||
|
||||
const visibleTabs = tabs.filter((tab) => !tab.hide);
|
||||
|
||||
const isInCommandMenu = isDefined(commandMenuPageComponentInstance);
|
||||
|
||||
const displaySummaryAndFields =
|
||||
layout && !layout.hideSummaryAndFields && !isMobile && !isInRightDrawer;
|
||||
|
||||
@@ -132,7 +146,7 @@ export const ShowPageSubContainer = ({
|
||||
componentInstanceId={tabListComponentId}
|
||||
/>
|
||||
</StyledTabListContainer>
|
||||
{(isMobile || isInRightDrawer) && summaryCard}
|
||||
{(isMobile || isInRightDrawer) && !isInCommandMenu && summaryCard}
|
||||
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
|
||||
{renderActiveTabContent()}
|
||||
</StyledContentContainer>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type StepDefinition = {
|
||||
type: 'trigger' | 'action';
|
||||
definition: any;
|
||||
};
|
||||
|
||||
export const getAgentIdFromStep = (
|
||||
stepDefinition: StepDefinition | undefined,
|
||||
): string | undefined => {
|
||||
if (!isDefined(stepDefinition)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (stepDefinition.type === 'trigger') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const definition = stepDefinition.definition;
|
||||
if (!isDefined(definition)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (definition.type !== 'AI_AGENT') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
!('settings' in definition) ||
|
||||
!isDefined(definition.settings) ||
|
||||
!('input' in definition.settings) ||
|
||||
!isDefined(definition.settings.input) ||
|
||||
!('agentId' in definition.settings.input)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return definition.settings.input.agentId;
|
||||
};
|
||||
+33
-65
@@ -1,12 +1,11 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { type CommandMenuAnimationVariant } from '@/command-menu/types/CommandMenuAnimationVariant';
|
||||
import { COMMAND_MENU_SIDE_PANEL_WIDTH } from '@/command-menu/constants/CommandMenuSidePanelWidth';
|
||||
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
|
||||
import { useListenToSidePanelClosing } from '@/ui/layout/right-drawer/hooks/useListenToSidePanelClosing';
|
||||
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
|
||||
import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagram/components/WorkflowDiagramRightClickCommandMenu';
|
||||
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
|
||||
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
|
||||
import { workflowDiagramPanOnDragComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramPanOnDragComponentState';
|
||||
import { workflowDiagramWaitingNodesDimensionsComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramWaitingNodesDimensionsComponentState';
|
||||
@@ -46,16 +45,14 @@ import {
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Tag, type TagColor } from 'twenty-ui/components';
|
||||
import { THEME_COMMON } from 'twenty-ui/theme';
|
||||
|
||||
const StyledResetReactflowStyles = styled.div`
|
||||
height: 100%;
|
||||
@@ -197,8 +194,7 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
return { nodes: [], edges: [] };
|
||||
}, [workflowDiagram]);
|
||||
|
||||
const { rightDrawerState } = useRightDrawerState();
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
|
||||
|
||||
const handleEdgesChange = (
|
||||
edgeChanges: Array<EdgeChange<WorkflowDiagramEdge>>,
|
||||
@@ -237,16 +233,12 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
const setFlowViewport = useRecoilCallback(
|
||||
() =>
|
||||
({
|
||||
rightDrawerState,
|
||||
noAnimation,
|
||||
workflowDiagramFlowInitialized,
|
||||
isInRightDrawer,
|
||||
isCommandMenuOpened,
|
||||
workflowDiagram,
|
||||
}: {
|
||||
rightDrawerState: CommandMenuAnimationVariant;
|
||||
noAnimation?: boolean;
|
||||
workflowDiagramFlowInitialized: boolean;
|
||||
isInRightDrawer: boolean;
|
||||
isCommandMenuOpened: boolean;
|
||||
workflowDiagram: WorkflowDiagram | undefined;
|
||||
}) => {
|
||||
if (
|
||||
@@ -270,27 +262,29 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
|
||||
setWorkflowDiagramWaitingNodesDimensions(false);
|
||||
|
||||
let visibleRightDrawerWidth = 0;
|
||||
if (rightDrawerState === 'normal' && !isInRightDrawer) {
|
||||
const rightDrawerWidth = Number(
|
||||
THEME_COMMON.rightDrawerWidth.replace('px', ''),
|
||||
);
|
||||
const baseContainerWidth = containerRef.current.offsetWidth;
|
||||
const hasViewportBeenMoved = currentViewport.x !== 0;
|
||||
|
||||
visibleRightDrawerWidth = rightDrawerWidth;
|
||||
let adjustedContainerWidth = baseContainerWidth;
|
||||
if (isCommandMenuOpened) {
|
||||
adjustedContainerWidth =
|
||||
baseContainerWidth - COMMAND_MENU_SIDE_PANEL_WIDTH;
|
||||
} else if (hasViewportBeenMoved) {
|
||||
adjustedContainerWidth =
|
||||
baseContainerWidth + COMMAND_MENU_SIDE_PANEL_WIDTH;
|
||||
}
|
||||
|
||||
const flowBounds = reactflow.getNodesBounds(nodes);
|
||||
const viewportX =
|
||||
(containerRef.current.offsetWidth + visibleRightDrawerWidth) / 2 -
|
||||
flowBounds.width / 2;
|
||||
const centeredXPosition =
|
||||
adjustedContainerWidth / 2 - flowBounds.width / 2;
|
||||
|
||||
reactflow.setViewport(
|
||||
{
|
||||
...currentViewport,
|
||||
x: viewportX - visibleRightDrawerWidth,
|
||||
x: centeredXPosition,
|
||||
zoom: defaultFitViewOptions.maxZoom,
|
||||
},
|
||||
{ duration: noAnimation ? 0 : 300 },
|
||||
{ duration: hasViewportBeenMoved ? 300 : 0 },
|
||||
);
|
||||
},
|
||||
[reactflow, setWorkflowDiagramWaitingNodesDimensions],
|
||||
@@ -299,17 +293,14 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
const handleSetFlowViewportOnChange = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
({
|
||||
rightDrawerState,
|
||||
workflowDiagramFlowInitialized,
|
||||
isInRightDrawer,
|
||||
isCommandMenuOpened,
|
||||
}: {
|
||||
rightDrawerState: CommandMenuAnimationVariant;
|
||||
workflowDiagramFlowInitialized: boolean;
|
||||
isInRightDrawer: boolean;
|
||||
isCommandMenuOpened: boolean;
|
||||
}) => {
|
||||
setFlowViewport({
|
||||
rightDrawerState,
|
||||
isInRightDrawer,
|
||||
isCommandMenuOpened,
|
||||
workflowDiagramFlowInitialized,
|
||||
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
|
||||
});
|
||||
@@ -319,14 +310,12 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
|
||||
useEffect(() => {
|
||||
handleSetFlowViewportOnChange({
|
||||
rightDrawerState,
|
||||
workflowDiagramFlowInitialized,
|
||||
isInRightDrawer,
|
||||
isCommandMenuOpened,
|
||||
});
|
||||
}, [
|
||||
handleSetFlowViewportOnChange,
|
||||
isInRightDrawer,
|
||||
rightDrawerState,
|
||||
isCommandMenuOpened,
|
||||
workflowDiagramFlowInitialized,
|
||||
]);
|
||||
|
||||
@@ -356,16 +345,13 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
}
|
||||
|
||||
setFlowViewport({
|
||||
rightDrawerState,
|
||||
noAnimation: true,
|
||||
isInRightDrawer,
|
||||
isCommandMenuOpened,
|
||||
workflowDiagramFlowInitialized,
|
||||
workflowDiagram: updatedWorkflowDiagram,
|
||||
});
|
||||
},
|
||||
[
|
||||
isInRightDrawer,
|
||||
rightDrawerState,
|
||||
isCommandMenuOpened,
|
||||
setFlowViewport,
|
||||
workflowDiagramFlowInitialized,
|
||||
workflowDiagramState,
|
||||
@@ -373,33 +359,15 @@ export const WorkflowDiagramCanvasBase = ({
|
||||
],
|
||||
);
|
||||
|
||||
const handleInit = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
() => {
|
||||
if (!isDefined(containerRef.current)) {
|
||||
return;
|
||||
}
|
||||
const handleInit = () => {
|
||||
if (!isDefined(containerRef.current)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setFlowViewport({
|
||||
rightDrawerState,
|
||||
noAnimation: true,
|
||||
isInRightDrawer,
|
||||
workflowDiagramFlowInitialized: true,
|
||||
workflowDiagram: getSnapshotValue(snapshot, workflowDiagramState),
|
||||
});
|
||||
setWorkflowDiagramFlowInitialized(true);
|
||||
|
||||
setWorkflowDiagramFlowInitialized(true);
|
||||
|
||||
onInit?.();
|
||||
},
|
||||
[
|
||||
isInRightDrawer,
|
||||
onInit,
|
||||
rightDrawerState,
|
||||
setFlowViewport,
|
||||
workflowDiagramState,
|
||||
],
|
||||
);
|
||||
onInit?.();
|
||||
};
|
||||
|
||||
const onBeforeDelete: OnBeforeDelete<
|
||||
WorkflowDiagramNode,
|
||||
|
||||
+1
@@ -64,6 +64,7 @@ export const WorkflowDiagramEmptyTriggerReadonly = () => {
|
||||
workflowVersionId: workflowVisualizerWorkflowVersionId,
|
||||
title: t`Add a Trigger`,
|
||||
icon: getIcon(null),
|
||||
stepId: TRIGGER_STEP_ID,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ export const WorkflowDiagramStepNodeEditable = ({
|
||||
workflowVisualizerWorkflowId,
|
||||
data.name,
|
||||
getIcon(getWorkflowNodeIconKey(data)),
|
||||
id,
|
||||
);
|
||||
|
||||
return;
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ export const WorkflowDiagramStepNodeReadonly = ({
|
||||
workflowVersionId: workflowVisualizerWorkflowVersionId,
|
||||
title: data.name,
|
||||
icon: getIcon(getWorkflowNodeIconKey(data)),
|
||||
stepId: id,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+2
-25
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { workflowRunIteratorSubStepIterationIndexComponentState } from '@/command-menu/pages/workflow/step/view-run/states/workflowRunIteratorSubStepIterationIndexComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
@@ -8,13 +7,9 @@ import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/componen
|
||||
import { getIsDescendantOfIterator } from '@/workflow/workflow-steps/utils/getIsDescendantOfIterator';
|
||||
import { getWorkflowRunStepContext } from '@/workflow/workflow-steps/utils/getWorkflowRunStepContext';
|
||||
import { getWorkflowVariablesUsedInStep } from '@/workflow/workflow-steps/utils/getWorkflowVariablesUsedInStep';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconBrackets, useIcons } from 'twenty-ui/display';
|
||||
import { IconBrackets } from 'twenty-ui/display';
|
||||
import {
|
||||
type GetJsonNodeHighlighting,
|
||||
JsonNestedNode,
|
||||
@@ -25,9 +20,7 @@ import { type JsonValue } from 'type-fest';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
@@ -61,14 +54,6 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
throw new Error('The input tab must be rendered with an action step.');
|
||||
}
|
||||
|
||||
const headerTitle = stepDefinition.definition.name;
|
||||
const headerIcon = getActionIcon(stepDefinition.definition.type);
|
||||
const headerIconColor = getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
});
|
||||
const headerType = getActionHeaderTypeOrThrow(stepDefinition.definition.type);
|
||||
|
||||
const variablesUsedInStep = getWorkflowVariablesUsedInStep({
|
||||
step,
|
||||
});
|
||||
@@ -115,14 +100,6 @@ export const WorkflowRunStepInputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
disabled
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={i18n._(headerType)}
|
||||
/>
|
||||
|
||||
<WorkflowRunStepJsonContainer>
|
||||
<JsonTreeContextProvider
|
||||
value={{
|
||||
|
||||
+1
-43
@@ -1,21 +1,11 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun';
|
||||
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
|
||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||
import { WorkflowRunStepJsonContainer } from '@/workflow/workflow-steps/components/WorkflowRunStepJsonContainer';
|
||||
import { useWorkflowRunStepInfo } from '@/workflow/workflow-steps/hooks/useWorkflowRunStepInfo';
|
||||
import { getWorkflowRunStepInfoToDisplayAsOutput } from '@/workflow/workflow-steps/utils/getWorkflowRunStepInfoToDisplayAsOutput';
|
||||
import { getActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionHeaderTypeOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
|
||||
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 { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import {
|
||||
type GetJsonNodeHighlighting,
|
||||
isTwoFirstDepths,
|
||||
@@ -24,9 +14,7 @@ import {
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
const { t, i18n } = useLingui();
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
@@ -51,26 +39,6 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
throw new Error('The step is expected to be properly shaped.');
|
||||
}
|
||||
|
||||
const headerTitle = stepDefinition.definition.name;
|
||||
const headerIcon =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerIcon(stepDefinition.definition)
|
||||
: getActionIcon(stepDefinition.definition.type);
|
||||
const headerIconColor =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerIconColor({
|
||||
theme,
|
||||
triggerType: stepDefinition.definition.type,
|
||||
})
|
||||
: getActionIconColorOrThrow({
|
||||
theme,
|
||||
actionType: stepDefinition.definition.type,
|
||||
});
|
||||
const headerType =
|
||||
stepDefinition.type === 'trigger'
|
||||
? getTriggerHeaderType(stepDefinition.definition)
|
||||
: i18n._(getActionHeaderTypeOrThrow(stepDefinition.definition.type));
|
||||
|
||||
const setRedHighlightingForEveryNode: GetJsonNodeHighlighting = (keyPath) => {
|
||||
if (keyPath.startsWith('error')) {
|
||||
return 'red';
|
||||
@@ -81,16 +49,6 @@ export const WorkflowRunStepOutputDetail = ({ stepId }: { stepId: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
disabled
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={
|
||||
headerTitle ?? getTriggerDefaultLabel(workflowRun.state.flow.trigger)
|
||||
}
|
||||
headerType={headerType}
|
||||
/>
|
||||
|
||||
<WorkflowRunStepJsonContainer>
|
||||
<JsonTree
|
||||
value={stepInfoToDisplay ?? t`No output available`}
|
||||
|
||||
+6
-1
@@ -44,7 +44,11 @@ export const WorkflowStepFooter = ({
|
||||
dropdownId={dropdownId}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<Button title={t`Options`} hotkeys={[getOsControlSymbol(), 'O']} />
|
||||
<Button
|
||||
size="small"
|
||||
title={t`Options`}
|
||||
hotkeys={[getOsControlSymbol(), 'O']}
|
||||
/>
|
||||
}
|
||||
dropdownPlacement="top-end"
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}
|
||||
@@ -99,6 +103,7 @@ export const WorkflowStepFooter = ({
|
||||
|
||||
const deleteButton = (
|
||||
<Button
|
||||
size="small"
|
||||
title={t`Delete`}
|
||||
onClick={() => {
|
||||
deleteStep(stepId);
|
||||
|
||||
+94
@@ -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();
|
||||
});
|
||||
});
|
||||
+34
@@ -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 };
|
||||
};
|
||||
-39
@@ -1,5 +1,4 @@
|
||||
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useFlowOrThrow } from '@/workflow/hooks/useFlowOrThrow';
|
||||
@@ -7,8 +6,6 @@ import { type WorkflowAiAgentAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { useUpdateWorkflowVersionStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowVersionStep';
|
||||
import { AI_AGENT_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/AiAgentAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
@@ -16,7 +13,6 @@ import {
|
||||
type ModelConfiguration,
|
||||
} from 'twenty-shared/ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import {
|
||||
useFindOneAgentQuery,
|
||||
@@ -40,13 +36,6 @@ export const WorkflowEditActionAiAgent = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionAiAgentProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: AI_AGENT_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const agentId = action.settings.input.agentId;
|
||||
const { data: agentData, loading: agentLoading } = useFindOneAgentQuery({
|
||||
variables: { id: agentId || '' },
|
||||
@@ -157,34 +146,6 @@ export const WorkflowEditActionAiAgent = ({
|
||||
<RightDrawerSkeletonLoader />
|
||||
) : (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={async (newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate?.({ ...action, name: newName });
|
||||
|
||||
// Also update agent label
|
||||
if (isDefined(agent)) {
|
||||
await updateAgent({
|
||||
variables: {
|
||||
input: {
|
||||
id: agent.id,
|
||||
label: newName,
|
||||
},
|
||||
},
|
||||
refetchQueries: ['FindOneAgent'],
|
||||
});
|
||||
}
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={AI_AGENT_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<FormTextFieldInput
|
||||
multiline
|
||||
|
||||
+2
-25
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { useFullScreenModal } from '@/ui/layout/fullscreen/hooks/useFullScreenModal';
|
||||
@@ -29,9 +28,6 @@ import { WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/workflow/w
|
||||
import { serverlessFunctionTestDataFamilyState } from '@/workflow/workflow-steps/workflow-actions/code-action/states/serverlessFunctionTestDataFamilyState';
|
||||
import { WorkflowServerlessFunctionTabId } from '@/workflow/workflow-steps/workflow-actions/code-action/types/WorkflowServerlessFunctionTabId';
|
||||
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
|
||||
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
||||
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -49,7 +45,7 @@ import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { buildOutputSchemaFromValue } from 'twenty-shared/workflow';
|
||||
import { IconCode, IconPlayerPlay, useIcons } from 'twenty-ui/display';
|
||||
import { IconCode, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
@@ -95,7 +91,6 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionServerlessFunctionProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { t } = useLingui();
|
||||
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||
const isMobile = useIsMobile();
|
||||
@@ -329,13 +324,6 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
dependencies: [isFullScreen],
|
||||
});
|
||||
|
||||
const headerTitle = isDefined(action.name)
|
||||
? action.name
|
||||
: CODE_ACTION.defaultLabel;
|
||||
const headerIcon = getActionIcon(action.type);
|
||||
const headerIconColor = useActionIconColorOrThrow(action.type);
|
||||
const headerType = useActionHeaderTypeOrThrow(action.type);
|
||||
|
||||
const testLogsTextAreaId = `${serverlessFunctionId}-test-logs`;
|
||||
|
||||
const breadcrumbLinks: BreadcrumbProps['links'] = [
|
||||
@@ -344,7 +332,7 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
children: headerTitle,
|
||||
children: isDefined(action.name) ? action.name : CODE_ACTION.defaultLabel,
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
@@ -427,17 +415,6 @@ export const WorkflowEditActionServerlessFunction = ({
|
||||
WORKFLOW_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID
|
||||
}
|
||||
/>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
updateAction({ name: newName });
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={CODE_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
{activeTabId === WorkflowServerlessFunctionTabId.CODE && (
|
||||
<>
|
||||
|
||||
-21
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { type WorkflowCodeAction } from '@/workflow/types/Workflow';
|
||||
@@ -7,15 +6,10 @@ import { INDEX_FILE_NAME } from '@/serverless-functions/constants/IndexFileName'
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowEditActionServerlessFunctionFields } from '@/workflow/workflow-steps/workflow-actions/code-action/components/WorkflowEditActionServerlessFunctionFields';
|
||||
import { getWrongExportedFunctionMarkers } from '@/workflow/workflow-steps/workflow-actions/code-action/utils/getWrongExportedFunctionMarkers';
|
||||
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
||||
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||
import styled from '@emotion/styled';
|
||||
import { type Monaco } from '@monaco-editor/react';
|
||||
import { type editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { CodeEditor } from 'twenty-ui/input';
|
||||
import { SOURCE_FOLDER_NAME } from '@/serverless-functions/constants/SourceFolderName';
|
||||
|
||||
@@ -31,7 +25,6 @@ type WorkflowReadonlyActionServerlessFunctionProps = {
|
||||
export const WorkflowReadonlyActionServerlessFunction = ({
|
||||
action,
|
||||
}: WorkflowReadonlyActionServerlessFunctionProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const serverlessFunctionId = action.settings.input.serverlessFunctionId;
|
||||
const serverlessFunctionVersion =
|
||||
action.settings.input.serverlessFunctionVersion;
|
||||
@@ -58,13 +51,6 @@ export const WorkflowReadonlyActionServerlessFunction = ({
|
||||
});
|
||||
};
|
||||
|
||||
const headerTitle = isDefined(action.name)
|
||||
? action.name
|
||||
: 'Code - Serverless Function';
|
||||
const headerIcon = getActionIcon(action.type);
|
||||
const headerIconColor = useActionIconColorOrThrow(action.type);
|
||||
const headerType = useActionHeaderTypeOrThrow(action.type);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
@@ -77,13 +63,6 @@ export const WorkflowReadonlyActionServerlessFunction = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<WorkflowEditActionServerlessFunctionFields
|
||||
functionInput={action.settings.input.serverlessFunctionInput}
|
||||
|
||||
-30
@@ -1,13 +1,9 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import {
|
||||
WorkflowCreateRecordBody,
|
||||
type CreateRecordFormData,
|
||||
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
|
||||
import { CREATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CreateRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionCreateRecordProps = {
|
||||
action: WorkflowCreateRecordAction;
|
||||
@@ -25,16 +21,8 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionCreateRecordProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const isFormDisabled = actionOptions.readonly;
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: CREATE_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const handleUpdate = (formData: CreateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
@@ -56,24 +44,6 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={CREATE_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowCreateRecordBody
|
||||
defaultObjectName={action.settings.input.objectName}
|
||||
defaultObjectRecord={action.settings.input.objectRecord}
|
||||
|
||||
-27
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -8,8 +7,6 @@ import { useEffect, useState } from 'react';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { DELETE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DeleteRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -116,32 +113,8 @@ export const WorkflowEditActionDeleteRecord = ({
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: DELETE_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={DELETE_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-delete-object-name"
|
||||
|
||||
+2
-30
@@ -4,7 +4,6 @@ import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||
import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
@@ -20,8 +19,6 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -29,7 +26,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ConnectedAccountProvider, SettingsPath } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus, useIcons } from 'twenty-ui/display';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
@@ -72,7 +69,6 @@ export const WorkflowEditActionSendEmail = ({
|
||||
actionOptions,
|
||||
}: WorkflowEditActionSendEmailProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
const { triggerApisOAuth } = useTriggerApisOAuth();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
@@ -257,12 +253,6 @@ export const WorkflowEditActionSendEmail = ({
|
||||
}
|
||||
});
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: SEND_EMAIL_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const navigate = useNavigateSettings();
|
||||
|
||||
const { closeCommandMenu } = useCommandMenu();
|
||||
@@ -270,24 +260,6 @@ export const WorkflowEditActionSendEmail = ({
|
||||
return (
|
||||
!loading && (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={SEND_EMAIL_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="select-connected-account-id"
|
||||
@@ -346,7 +318,7 @@ export const WorkflowEditActionSendEmail = ({
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
children: headerTitle,
|
||||
children: isDefined(action.name) ? action.name : t`Send Email`,
|
||||
href: '#',
|
||||
},
|
||||
{
|
||||
|
||||
-27
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
@@ -10,8 +9,6 @@ import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpd
|
||||
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { UPDATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpdateRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
@@ -71,12 +68,6 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
|
||||
const isFormDisabled = actionOptions.readonly === true;
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: UPDATE_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof UpdateRecordFormData,
|
||||
updatedValue: JsonValue,
|
||||
@@ -156,24 +147,6 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={UPDATE_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-update-record-object-name"
|
||||
|
||||
-31
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import {
|
||||
@@ -6,10 +5,6 @@ import {
|
||||
type CreateRecordFormData,
|
||||
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
|
||||
|
||||
import { UPSERT_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpsertRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionUpsertRecordProps = {
|
||||
action: WorkflowUpsertRecordAction;
|
||||
actionOptions:
|
||||
@@ -26,14 +21,6 @@ export const WorkflowEditActionUpsertRecord = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionUpsertRecordProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: UPSERT_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const isFormDisabled = actionOptions.readonly === true;
|
||||
|
||||
const handleUpdate = (formData: CreateRecordFormData) => {
|
||||
@@ -57,24 +44,6 @@ export const WorkflowEditActionUpsertRecord = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={UPSERT_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowCreateRecordBody
|
||||
defaultObjectName={action.settings.input.objectName}
|
||||
defaultObjectRecord={action.settings.input.objectRecord}
|
||||
|
||||
-9
@@ -72,15 +72,6 @@ export const Disabled: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
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();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
|
||||
-27
@@ -77,15 +77,6 @@ export const DisabledWithEmptyValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Delete Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Delete Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
@@ -128,15 +119,6 @@ export const DisabledWithDefaultStaticValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Delete Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Delete Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
@@ -187,15 +169,6 @@ export const DisabledWithDefaultVariableValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Delete Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Delete Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
|
||||
-9
@@ -76,15 +76,6 @@ export const DisabledWithEmptyValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Search Records');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Search Records');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
|
||||
-3
@@ -112,7 +112,6 @@ export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(await canvas.findByText('Send Email')).toBeVisible();
|
||||
expect(await canvas.findByText('Account')).toBeVisible();
|
||||
expect(await canvas.findByText('Subject')).toBeVisible();
|
||||
expect(await canvas.findByText('Body')).toBeVisible();
|
||||
@@ -129,8 +128,6 @@ export const Configured: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(await canvas.findByText('Send Welcome Email')).toBeVisible();
|
||||
|
||||
expect(await canvas.findByText('Account')).toBeVisible();
|
||||
expect(await canvas.findByText('Subject')).toBeVisible();
|
||||
expect(await canvas.findByText('Body')).toBeVisible();
|
||||
|
||||
-27
@@ -90,15 +90,6 @@ export const DisabledWithEmptyValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Update Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Update Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
@@ -152,15 +143,6 @@ export const DisabledWithDefaultStaticValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Update Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Update Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
@@ -216,15 +198,6 @@ export const DisabledWithDefaultVariableValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Update Record');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Update Record');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
const objectSelectCurrentValue = await canvas.findByText('People');
|
||||
|
||||
await userEvent.click(objectSelectCurrentValue);
|
||||
|
||||
-29
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/ui/form-types/components/FormDateTimeFieldInput';
|
||||
import { FormNumberFieldInput } from '@/object-record/record-field/ui/form-types/components/FormNumberFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -6,8 +5,6 @@ import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/Gene
|
||||
import { type WorkflowDelayAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { DELAY_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/DelayAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import {
|
||||
@@ -32,12 +29,6 @@ export const WorkflowEditActionDelay = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionDelayProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: DELAY_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const delayOptions: Array<SelectOption<'SCHEDULED_DATE' | 'DURATION'>> = [
|
||||
{
|
||||
label: t`At a specific date or time`,
|
||||
@@ -139,28 +130,8 @@ export const WorkflowEditActionDelay = ({
|
||||
});
|
||||
};
|
||||
|
||||
const HeaderIcon = getIcon(headerIcon ?? 'IconPlayerPause');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
initialTitle={headerTitle}
|
||||
Icon={HeaderIcon}
|
||||
iconColor={headerIconColor}
|
||||
headerType={headerType}
|
||||
onTitleChange={(newTitle: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newTitle,
|
||||
});
|
||||
}}
|
||||
iconTooltip={DELAY_ACTION.defaultLabel}
|
||||
/>
|
||||
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-delay-type"
|
||||
|
||||
-30
@@ -1,14 +1,10 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowFilterAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { FILTER_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FilterAction';
|
||||
import { WorkflowEditActionFilterBody } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBody';
|
||||
import { WorkflowEditActionFilterBodyEffect } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowEditActionFilterBodyEffect';
|
||||
import { StepFilterGroupsComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFilterGroupsComponentInstanceContext';
|
||||
import { StepFiltersComponentInstanceContext } from '@/workflow/workflow-steps/workflow-actions/filter-action/states/context/StepFiltersComponentInstanceContext';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { type StepFilter, type StepFilterGroup } from 'twenty-shared/types';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionFilterProps = {
|
||||
action: WorkflowFilterAction;
|
||||
@@ -31,34 +27,8 @@ export const WorkflowEditActionFilter = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFilterProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: FILTER_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={FILTER_ACTION.defaultLabel}
|
||||
/>
|
||||
<StepFiltersComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: action.id,
|
||||
|
||||
-27
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowFindRecordsAction } from '@/workflow/types/Workflow';
|
||||
@@ -17,11 +16,9 @@ import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { FIND_RECORDS_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FindRecordsAction';
|
||||
import { WorkflowFindRecordsFilters } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFilters';
|
||||
import { WorkflowFindRecordsFiltersEffect } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsFiltersEffect';
|
||||
import { WorkflowFindRecordsSorts } from '@/workflow/workflow-steps/workflow-actions/find-records-action/components/WorkflowFindRecordsSorts';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
@@ -147,32 +144,8 @@ export const WorkflowEditActionFindRecords = ({
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: FIND_RECORDS_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={FIND_RECORDS_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-find-records-object-name"
|
||||
|
||||
-29
@@ -1,4 +1,3 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
|
||||
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
|
||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
|
||||
@@ -9,12 +8,10 @@ import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableLi
|
||||
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { FORM_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FormAction';
|
||||
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
||||
import { WorkflowFormEmptyMessage } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormEmptyMessage';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
@@ -28,7 +25,6 @@ import {
|
||||
IconGripVertical,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
useIcons,
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
@@ -136,17 +132,10 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFormBuilderProps) => {
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const { t } = useLingui();
|
||||
|
||||
const [formData, setFormData] = useState<FormData>(action.settings.input);
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: FORM_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const [selectedField, setSelectedField] = useState<string | null>(null);
|
||||
const [hoveredField, setHoveredField] = useState<string | null>(null);
|
||||
|
||||
@@ -223,24 +212,6 @@ export const WorkflowEditActionFormBuilder = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={FORM_ACTION.defaultLabel}
|
||||
/>
|
||||
<StyledWorkflowStepBody>
|
||||
{formData.length === 0 && (
|
||||
<WorkflowFormEmptyMessage data-testid="empty-form-message" />
|
||||
|
||||
-19
@@ -1,5 +1,4 @@
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenuHistory } from '@/command-menu/hooks/useCommandMenuHistory';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
@@ -10,15 +9,12 @@ import { type WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
|
||||
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
|
||||
import { FORM_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/FormAction';
|
||||
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
|
||||
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export type WorkflowEditActionFormFillerProps = {
|
||||
@@ -35,7 +31,6 @@ export const WorkflowEditActionFormFiller = ({
|
||||
actionOptions,
|
||||
}: WorkflowEditActionFormFillerProps) => {
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const { submitFormStep } = useSubmitFormStep();
|
||||
const [formData, setFormData] = useState<FormData>(action.settings.input);
|
||||
const workflowRunId = useWorkflowRunIdOrThrow();
|
||||
@@ -45,12 +40,6 @@ export const WorkflowEditActionFormFiller = ({
|
||||
|
||||
const canSubmit = !actionOptions.readonly && !isDefined(error);
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: FORM_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const onFieldUpdate = ({
|
||||
fieldId,
|
||||
value,
|
||||
@@ -111,14 +100,6 @@ export const WorkflowEditActionFormFiller = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled
|
||||
iconTooltip={FORM_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
{formData.map((field) => {
|
||||
if (field.type === 'RECORD') {
|
||||
|
||||
-9
@@ -159,15 +159,6 @@ export const DisabledWithEmptyValues: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const titleText = await canvas.findByText('Form');
|
||||
|
||||
expect(window.getComputedStyle(titleText).cursor).toBe('default');
|
||||
|
||||
await userEvent.click(titleText);
|
||||
|
||||
const titleInput = canvas.queryByDisplayValue('Form');
|
||||
expect(titleInput).not.toBeInTheDocument();
|
||||
|
||||
await canvas.findByText('Company');
|
||||
|
||||
const addFieldButton = canvas.queryByText('Add Field');
|
||||
|
||||
-223
@@ -1,223 +0,0 @@
|
||||
import {
|
||||
type WorkflowFormAction,
|
||||
type WorkflowHttpRequestAction,
|
||||
type WorkflowSendEmailAction,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import {
|
||||
workflowFormActionSettingsSchema,
|
||||
workflowHttpRequestActionSettingsSchema,
|
||||
workflowSendEmailActionSettingsSchema,
|
||||
} from 'twenty-shared/workflow';
|
||||
import { useWorkflowActionHeader } from '../useWorkflowActionHeader';
|
||||
|
||||
jest.mock('../useActionIconColorOrThrow', () => ({
|
||||
useActionIconColorOrThrow: jest.fn().mockReturnValue('blue'),
|
||||
}));
|
||||
|
||||
jest.mock('../useActionHeaderTypeOrThrow', () => ({
|
||||
useActionHeaderTypeOrThrow: jest.fn().mockReturnValue('Action'),
|
||||
}));
|
||||
|
||||
jest.mock('../../utils/getActionIcon', () => ({
|
||||
getActionIcon: jest.fn().mockReturnValue('IconHttp'),
|
||||
}));
|
||||
|
||||
const mockGetIcon = jest.fn().mockReturnValue('IconComponent');
|
||||
jest.mock('twenty-ui/display', () => ({
|
||||
useIcons: () => ({
|
||||
getIcon: mockGetIcon,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useWorkflowActionHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when action name is not defined', () => {
|
||||
it('should return default title', () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
name: '',
|
||||
type: 'HTTP_REQUEST',
|
||||
settings: workflowHttpRequestActionSettingsSchema.parse({
|
||||
input: {
|
||||
url: 'https://example.com',
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
body: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
}),
|
||||
valid: true,
|
||||
} satisfies WorkflowHttpRequestAction;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'HTTP Request',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.headerTitle).toBe('HTTP Request');
|
||||
expect(result.current.headerIcon).toBe('IconHttp');
|
||||
expect(result.current.headerIconColor).toBe('blue');
|
||||
expect(result.current.headerType).toBe('Action');
|
||||
expect(result.current.getIcon).toBe(mockGetIcon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when action name is defined', () => {
|
||||
it('should return the action name', () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
name: 'Test Action',
|
||||
type: 'HTTP_REQUEST',
|
||||
settings: workflowHttpRequestActionSettingsSchema.parse({
|
||||
input: {
|
||||
url: 'https://example.com',
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
body: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
}),
|
||||
valid: true,
|
||||
} satisfies WorkflowHttpRequestAction;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'HTTP Request',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.headerTitle).toBe('Test Action');
|
||||
expect(result.current.headerIcon).toBe('IconHttp');
|
||||
expect(result.current.headerIconColor).toBe('blue');
|
||||
expect(result.current.headerType).toBe('Action');
|
||||
expect(result.current.getIcon).toBe(mockGetIcon);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when action type is defined', () => {
|
||||
it('should return default title for HTTP request action', () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
name: '',
|
||||
type: 'HTTP_REQUEST',
|
||||
settings: workflowHttpRequestActionSettingsSchema.parse({
|
||||
input: {
|
||||
url: 'https://example.com',
|
||||
method: 'GET',
|
||||
headers: {},
|
||||
body: {},
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
}),
|
||||
valid: true,
|
||||
} satisfies WorkflowHttpRequestAction;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'HTTP Request',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.headerTitle).toBe('HTTP Request');
|
||||
expect(result.current.headerIcon).toBe('IconHttp');
|
||||
expect(result.current.headerIconColor).toBe('blue');
|
||||
expect(result.current.headerType).toBe('Action');
|
||||
expect(result.current.getIcon).toBe(mockGetIcon);
|
||||
});
|
||||
|
||||
it('should return default title for form action', () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
name: '',
|
||||
type: 'FORM',
|
||||
settings: workflowFormActionSettingsSchema.parse({
|
||||
input: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'field1',
|
||||
label: 'Field 1',
|
||||
type: FieldMetadataType.TEXT,
|
||||
required: false,
|
||||
settings: {},
|
||||
},
|
||||
],
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
}),
|
||||
valid: true,
|
||||
} satisfies WorkflowFormAction;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'Form',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.headerTitle).toBe('Form');
|
||||
expect(result.current.headerIcon).toBe('IconHttp');
|
||||
expect(result.current.headerIconColor).toBe('blue');
|
||||
expect(result.current.headerType).toBe('Action');
|
||||
expect(result.current.getIcon).toBe(mockGetIcon);
|
||||
});
|
||||
|
||||
it('should return default title for email action', () => {
|
||||
const action = {
|
||||
id: '1',
|
||||
name: '',
|
||||
type: 'SEND_EMAIL',
|
||||
settings: workflowSendEmailActionSettingsSchema.parse({
|
||||
input: {
|
||||
connectedAccountId: '1',
|
||||
email: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
body: 'Test Body',
|
||||
},
|
||||
outputSchema: {},
|
||||
errorHandlingOptions: {
|
||||
retryOnFailure: { value: false },
|
||||
continueOnFailure: { value: false },
|
||||
},
|
||||
}),
|
||||
valid: true,
|
||||
} satisfies WorkflowSendEmailAction;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: 'Email',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result.current.headerTitle).toBe('Email');
|
||||
expect(result.current.headerIcon).toBe('IconHttp');
|
||||
expect(result.current.headerIconColor).toBe('blue');
|
||||
expect(result.current.headerType).toBe('Action');
|
||||
expect(result.current.getIcon).toBe(mockGetIcon);
|
||||
});
|
||||
});
|
||||
});
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { type WorkflowAction } from '@/workflow/types/Workflow';
|
||||
import { type IconComponent, useIcons } from 'twenty-ui/display';
|
||||
import { getActionIcon } from '../utils/getActionIcon';
|
||||
import { useActionHeaderTypeOrThrow } from './useActionHeaderTypeOrThrow';
|
||||
import { useActionIconColorOrThrow } from './useActionIconColorOrThrow';
|
||||
|
||||
type UseWorkflowActionHeaderProps = {
|
||||
action: WorkflowAction;
|
||||
defaultTitle: string;
|
||||
};
|
||||
|
||||
type UseWorkflowActionHeaderReturn = {
|
||||
headerTitle: string;
|
||||
headerIcon: string | undefined;
|
||||
headerIconColor: string;
|
||||
headerType: string;
|
||||
getIcon: (iconName: string) => IconComponent;
|
||||
};
|
||||
|
||||
export const useWorkflowActionHeader = ({
|
||||
action,
|
||||
defaultTitle,
|
||||
}: UseWorkflowActionHeaderProps): UseWorkflowActionHeaderReturn => {
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const headerTitle = action.name ? action.name : defaultTitle;
|
||||
const headerIcon = getActionIcon(action.type);
|
||||
const headerIconColor = useActionIconColorOrThrow(action.type);
|
||||
const headerType = useActionHeaderTypeOrThrow(action.type);
|
||||
|
||||
return {
|
||||
headerTitle,
|
||||
headerIcon,
|
||||
headerIconColor,
|
||||
headerType,
|
||||
getIcon,
|
||||
};
|
||||
};
|
||||
+1
-24
@@ -1,11 +1,8 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormRawJsonFieldInput } from '@/object-record/record-field/ui/form-types/components/FormRawJsonFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { type WorkflowHttpRequestAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { HTTP_REQUEST_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/HttpRequestAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
|
||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
@@ -20,7 +17,7 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useEffect } from 'react';
|
||||
import { IconPlayerPlay, IconSettings, useIcons } from 'twenty-ui/display';
|
||||
import { IconPlayerPlay, IconSettings } from 'twenty-ui/display';
|
||||
import {
|
||||
HTTP_METHODS,
|
||||
JSON_RESPONSE_PLACEHOLDER,
|
||||
@@ -93,16 +90,10 @@ export const WorkflowEditActionHttpRequest = ({
|
||||
}: WorkflowEditActionHttpRequestProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const { getIcon } = useIcons();
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: HTTP_REQUEST_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const { formData, handleFieldChange, saveAction } = useHttpRequestForm({
|
||||
action,
|
||||
@@ -143,20 +134,6 @@ export const WorkflowEditActionHttpRequest = ({
|
||||
behaveAsLinks={false}
|
||||
componentInstanceId={WORKFLOW_HTTP_REQUEST_TAB_LIST_COMPONENT_ID}
|
||||
/>
|
||||
<SidePanelHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
actionOptions.onActionUpdate?.({ ...action, name: newName });
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={HTTP_REQUEST_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
{activeTabId === WorkflowHttpRequestTabId.CONFIGURATION && (
|
||||
<StyledConfigurationTabContent>
|
||||
|
||||
-4
@@ -118,10 +118,6 @@ export const Configured: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const header = await canvas.findByTestId('side-panel-header');
|
||||
const headerCanvas = within(header);
|
||||
expect(await headerCanvas.findByText('API Call')).toBeVisible();
|
||||
|
||||
const urlLabel = await canvas.findByText('URL');
|
||||
const urlInputContainer = urlLabel.closest('div')?.nextElementSibling;
|
||||
const urlEditor = urlInputContainer?.querySelector('.ProseMirror');
|
||||
|
||||
-29
@@ -1,12 +1,9 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { FormArrayFieldInput } from '@/object-record/record-field/ui/form-types/components/FormArrayFieldInput';
|
||||
import { type FieldArrayValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { type WorkflowIteratorAction } from '@/workflow/types/Workflow';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { ITERATOR_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/IteratorAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isArray, isString } from '@sniptt/guards';
|
||||
@@ -44,12 +41,6 @@ export const WorkflowEditActionIterator = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionIteratorProps) => {
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType, getIcon } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: ITERATOR_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const defaultItems = isDefined(action.settings.input.items)
|
||||
@@ -100,28 +91,8 @@ export const WorkflowEditActionIterator = ({
|
||||
saveAction(updatedFormData);
|
||||
};
|
||||
|
||||
const handleTitleChange = (newName: string) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate?.({
|
||||
...action,
|
||||
name: newName,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
onTitleChange={handleTitleChange}
|
||||
Icon={getIcon(headerIcon || 'IconRepeat')}
|
||||
iconColor={headerIconColor}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={actionOptions.readonly}
|
||||
iconTooltip={ITERATOR_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<FormArrayFieldInput
|
||||
label={t`Items to iterate over`}
|
||||
|
||||
-34
@@ -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`}
|
||||
|
||||
-27
@@ -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>
|
||||
|
||||
-29
@@ -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
-33
@@ -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"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user