271 remove is command menu v2 enabled (#10809)

Closes https://github.com/twentyhq/core-team-issues/issues/271

This PR
- Removes the feature flag IS_COMMAND_MENU_V2_ENABLED
- Removes all old Right drawer components
- Removes the Action menu bar
- Removes unused Copilot page
This commit is contained in:
Raphaël Bosi
2025-03-12 16:26:29 +01:00
committed by GitHub
parent 1b0413bf8b
commit daa501549e
124 changed files with 281 additions and 4222 deletions

View File

@ -27,10 +27,6 @@ export const RecordActionMenuEntriesSetter = () => {
FeatureFlagKey.IsWorkflowEnabled,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
if (!isDefined(contextStoreCurrentObjectMetadataItem)) {
return null;
}
@ -40,10 +36,7 @@ export const RecordActionMenuEntriesSetter = () => {
contextStoreTargetedRecordsRule,
);
const actionConfig = getActionConfig(
contextStoreCurrentObjectMetadataItem,
isCommandMenuV2Enabled,
);
const actionConfig = getActionConfig(contextStoreCurrentObjectMetadataItem);
const actionsToRegister = isDefined(viewType)
? Object.values(actionConfig ?? {}).filter((action) =>

View File

@ -1,6 +1,5 @@
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
@ -8,10 +7,9 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { t } from '@lingui/core/macro';
import { isNull } from '@sniptt/guards';
import { useCallback, useContext, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { getOsControlSymbol } from 'twenty-ui';
@ -39,8 +37,6 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const { sortedFavorites: favorites } = useFavorites();
const { deleteFavorite } = useDeleteFavorite();
const { closeRightDrawer } = useRightDrawer();
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
@ -63,8 +59,6 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const isRemoteObject = objectMetadataItem.isRemote;
const { isInRightDrawer } = useContext(ActionMenuContext);
const shouldBeRegistered =
!isRemoteObject &&
isNull(selectedRecord?.deletedAt) &&
@ -91,9 +85,6 @@ export const useDeleteSingleRecordAction: ActionHookWithObjectMetadataItem = ({
subtitle={t`Are you sure you want to delete this record? It can be recovered from the Command menu (${osControlSymbol} + K).`}
onConfirmClick={() => {
handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer({ emitCloseEvent: false });
}
}}
confirmButtonText={'Delete Record'}
/>

View File

@ -1,14 +1,12 @@
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { AppPath } from '@/types/AppPath';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useCallback, useContext, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@ -35,8 +33,6 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { closeRightDrawer } = useRightDrawer();
const handleDeleteClick = useCallback(async () => {
resetTableRowSelection();
@ -54,8 +50,6 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
const isRemoteObject = objectMetadataItem.isRemote;
const { isInRightDrawer } = useContext(ActionMenuContext);
const shouldBeRegistered =
!hasObjectReadOnlyPermission &&
!isRemoteObject &&
@ -82,9 +76,6 @@ export const useDestroySingleRecordAction: ActionHookWithObjectMetadataItem = ({
}
onConfirmClick={async () => {
await handleDeleteClick();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
confirmButtonText={'Permanently Destroy Record'}
/>

View File

@ -1,6 +1,5 @@
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
import { ActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/ActionHook';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
@ -9,9 +8,8 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useState } from 'react';
import { useCallback, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
@ -35,8 +33,6 @@ export const useRestoreSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
const { closeRightDrawer } = useRightDrawer();
const handleRestoreClick = useCallback(async () => {
resetTableRowSelection();
@ -55,8 +51,6 @@ export const useRestoreSingleRecordAction: ActionHookWithObjectMetadataItem = ({
useRecoilComponentValueV2(contextStoreCurrentViewTypeComponentState) ===
ContextStoreViewType.ShowPage;
const { isInRightDrawer } = useContext(ActionMenuContext);
const shouldBeRegistered =
!isRemoteObject &&
isDefined(selectedRecord?.deletedAt) &&
@ -73,9 +67,6 @@ export const useRestoreSingleRecordAction: ActionHookWithObjectMetadataItem = ({
const handleConfirmClick = () => {
handleRestoreClick();
if (isInRightDrawer) {
closeRightDrawer({ emitCloseEvent: false });
}
};
return {

View File

@ -1,4 +1,3 @@
import { DEFAULT_ACTIONS_CONFIG_V1 } from '@/action-menu/actions/record-actions/constants/DefaultActionsConfigV1';
import { DEFAULT_ACTIONS_CONFIG_V2 } from '@/action-menu/actions/record-actions/constants/DefaultActionsConfigV2';
import { WORKFLOW_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowActionsConfig';
import { WORKFLOW_RUNS_ACTIONS_CONFIG } from '@/action-menu/actions/record-actions/constants/WorkflowRunsActionsConfig';
@ -6,10 +5,7 @@ import { WORKFLOW_VERSIONS_ACTIONS_CONFIG } from '@/action-menu/actions/record-a
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const getActionConfig = (
objectMetadataItem: ObjectMetadataItem,
isCommandMenuV2Enabled: boolean,
) => {
export const getActionConfig = (objectMetadataItem: ObjectMetadataItem) => {
switch (objectMetadataItem.nameSingular) {
case CoreObjectNameSingular.Workflow:
return WORKFLOW_ACTIONS_CONFIG;
@ -18,8 +14,6 @@ export const getActionConfig = (
case CoreObjectNameSingular.WorkflowRun:
return WORKFLOW_RUNS_ACTIONS_CONFIG;
default:
return isCommandMenuV2Enabled
? DEFAULT_ACTIONS_CONFIG_V2
: DEFAULT_ACTIONS_CONFIG_V1;
return DEFAULT_ACTIONS_CONFIG_V2;
}
};

View File

@ -1,10 +1,7 @@
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { Key } from 'ts-key-enum';
import { Button, getOsControlSymbol } from 'twenty-ui';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const CmdEnterActionButton = ({
title,
@ -15,15 +12,10 @@ export const CmdEnterActionButton = ({
onClick: () => void;
disabled?: boolean;
}) => {
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
useScopedHotkeys(
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
() => onClick(),
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[onClick],
);

View File

@ -3,10 +3,8 @@ import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/
import { RecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionMenuEntriesSetter';
import { RunWorkflowRecordAgnosticActionMenuEntriesSetter } from '@/action-menu/actions/record-agnostic-actions/components/RunWorkflowRecordAgnosticActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
@ -21,10 +19,6 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
contextStoreCurrentObjectMetadataItemComponentState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
@ -54,14 +48,9 @@ export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
},
}}
>
{isCommandMenuV2Enabled ? (
<>{!isMobile && <RecordIndexActionMenuButtons />}</>
) : (
<RecordIndexActionMenuBar />
)}
{!isMobile && <RecordIndexActionMenuButtons />}
<RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter />
<RecordAgnosticActionMenuEntriesSetter />
{isWorkflowEnabled && (

View File

@ -1,51 +0,0 @@
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
const StyledLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const RecordIndexActionMenuBar = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (contextStoreNumberOfSelectedRecords === 0) {
return null;
}
return (
<BottomBar
bottomBarId={getActionBarIdFromActionMenuId(actionMenuId)}
bottomBarHotkeyScopeFromParent={{ scope: ActionBarHotkeyScope.ActionBar }}
>
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} />
))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar>
);
};

View File

@ -1,76 +0,0 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
export const RecordIndexActionMenuEffect = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
const { openActionBar, closeActionBar } = useActionMenu(actionMenuId);
// Using closeActionBar here was causing a bug because it goes back to the
// previous hotkey scope, and we don't want that here.
const setIsBottomBarOpened = useSetRecoilComponentStateV2(
isBottomBarOpenedComponentState,
getActionBarIdFromActionMenuId(actionMenuId),
);
const isDropdownOpen = useRecoilValue(
extractComponentState(
isDropdownOpenComponentState,
getActionMenuDropdownIdFromActionMenuId(actionMenuId),
),
);
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => {
if (
contextStoreNumberOfSelectedRecords > 0 &&
!isDropdownOpen &&
!isRightDrawerOpen &&
!isCommandMenuOpened
) {
// We only handle opening the ActionMenuBar here, not the Dropdown.
// The Dropdown is already managed by sync handlers for events like
// right-click to open and click outside to close.
openActionBar();
}
if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) {
closeActionBar();
}
}, [
contextStoreNumberOfSelectedRecords,
openActionBar,
closeActionBar,
isDropdownOpen,
isRightDrawerOpen,
isCommandMenuOpened,
]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
setIsBottomBarOpened(false);
}
}, [isRightDrawerOpen, isCommandMenuOpened, setIsBottomBarOpened]);
return null;
};

View File

@ -5,40 +5,19 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen
import { RecordShowActionMenuButtons } from '@/action-menu/components/RecordShowActionMenuButtons';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
export const RecordShowActionMenu = ({
isFavorite,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}: {
isFavorite: boolean;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
handleFavoriteButtonClick: () => void;
}) => {
export const RecordShowActionMenu = () => {
const contextStoreCurrentObjectMetadataItem = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemComponentState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsWorkflowEnabled,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
<>
{contextStoreCurrentObjectMetadataItem && (
@ -48,19 +27,7 @@ export const RecordShowActionMenu = ({
onActionExecutedCallback: () => {},
}}
>
{isCommandMenuV2Enabled ? (
<RecordShowActionMenuButtons />
) : (
<RecordShowPageBaseHeader
{...{
isFavorite,
record,
objectMetadataItem,
objectNameSingular,
handleFavoriteButtonClick,
}}
/>
)}
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
<RecordAgnosticActionMenuEntriesSetter />

View File

@ -6,17 +6,14 @@ import { getRightDrawerActionMenuDropdownIdFromActionMenuId } from '@/action-men
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { i18n } from '@lingui/core';
import { Key } from 'ts-key-enum';
import { Button, MenuItem, getOsControlSymbol } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { Button, getOsControlSymbol, MenuItem } from 'twenty-ui';
export const RightDrawerActionMenuDropdown = () => {
const actionMenuEntries = useRecoilComponentValueV2(
@ -31,10 +28,6 @@ export const RightDrawerActionMenuDropdown = () => {
const theme = useTheme();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
useScopedHotkeys(
[Key.Escape, 'ctrl+o,meta+o'],
() => {
@ -53,9 +46,7 @@ export const RightDrawerActionMenuDropdown = () => {
getRightDrawerActionMenuDropdownIdFromActionMenuId(actionMenuId),
);
},
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[openDropdown],
);

View File

@ -1,141 +0,0 @@
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import {
ActionMenuEntry,
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { RecordSortsComponentInstanceContext } from '@/object-record/record-sort/states/context/RecordSortsComponentInstanceContext';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { msg } from '@lingui/core/macro';
import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { userEvent, waitFor, within } from '@storybook/test';
import { RecoilRoot } from 'recoil';
import { IconTrash, RouterDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const deleteMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar,
decorators: [
RouterDecorator,
I18nFrontDecorator,
(Story) => (
<RecordFilterGroupsComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecordFiltersComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecordSortsComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<RecoilRoot
initializeState={({ set }) => {
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
{
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily(
{
instanceId: 'story-action-menu',
},
),
3,
);
const map = new Map<string, ActionMenuEntry>();
map.set('delete', {
isPinned: true,
scope: ActionMenuEntryScope.RecordSelection,
type: ActionMenuEntryType.Standard,
key: 'delete',
label: msg`Delete`,
position: 0,
Icon: IconTrash,
onClick: deleteMock,
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
}),
map,
);
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId:
getActionBarIdFromActionMenuId('story-action-menu'),
}),
true,
);
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
>
<Story />
</ActionMenuComponentInstanceContext.Provider>
</RecoilRoot>
</ContextStoreComponentInstanceContext.Provider>
</RecordSortsComponentInstanceContext.Provider>
</RecordFiltersComponentInstanceContext.Provider>
</RecordFilterGroupsComponentInstanceContext.Provider>
),
],
args: {
actionMenuId: 'story-action-menu',
},
};
export default meta;
type Story = StoryObj<typeof RecordIndexActionMenuBar>;
export const Default: Story = {
args: {
actionMenuId: 'story-action-menu',
},
};
export const WithCustomSelection: Story = {
args: {
actionMenuId: 'story-action-menu',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const selectionText = await canvas.findByText('3 selected:');
expect(selectionText).toBeInTheDocument();
},
};
export const WithButtonClicks: Story = {
args: {
actionMenuId: 'story-action-menu',
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton);
await waitFor(() => {
expect(deleteMock).toHaveBeenCalled();
});
},
};

View File

@ -1,82 +0,0 @@
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
import { useActionMenu } from '../useActionMenu';
const openBottomBar = jest.fn();
const closeBottomBar = jest.fn();
const openDropdown = jest.fn();
const closeDropdown = jest.fn();
jest.mock('@/ui/layout/bottom-bar/hooks/useBottomBar', () => ({
useBottomBar: jest.fn(() => ({
openBottomBar: openBottomBar,
closeBottomBar: closeBottomBar,
})),
}));
jest.mock('@/ui/layout/dropdown/hooks/useDropdownV2', () => ({
useDropdownV2: jest.fn(() => ({
openDropdown: openDropdown,
closeDropdown: closeDropdown,
})),
}));
describe('useActionMenu', () => {
const actionMenuId = 'test-action-menu';
const actionBarId = getActionBarIdFromActionMenuId(actionMenuId);
const actionMenuDropdownId =
getActionMenuDropdownIdFromActionMenuId(actionMenuId);
it('should return the correct functions', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
expect(result.current).toHaveProperty('openActionMenuDropdown');
expect(result.current).toHaveProperty('openActionBar');
expect(result.current).toHaveProperty('closeActionBar');
expect(result.current).toHaveProperty('closeActionMenuDropdown');
});
it('should call the correct functions when opening action menu dropdown', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.openActionMenuDropdown();
});
expect(closeBottomBar).toHaveBeenCalledWith(actionBarId);
expect(openDropdown).toHaveBeenCalledWith(actionMenuDropdownId);
});
it('should call the correct functions when opening action bar', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.openActionBar();
});
expect(closeDropdown).toHaveBeenCalledWith(actionMenuDropdownId);
expect(openBottomBar).toHaveBeenCalledWith(actionBarId);
});
it('should call the correct function when closing action menu dropdown', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.closeActionMenuDropdown();
});
expect(closeDropdown).toHaveBeenCalledWith(actionMenuDropdownId);
});
it('should call the correct function when closing action bar', () => {
const { result } = renderHook(() => useActionMenu(actionMenuId));
act(() => {
result.current.closeActionBar();
});
expect(closeBottomBar).toHaveBeenCalledWith(actionBarId);
});
});

View File

@ -1,38 +0,0 @@
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { useBottomBar } from '@/ui/layout/bottom-bar/hooks/useBottomBar';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
export const useActionMenu = (actionMenuId: string) => {
const { openDropdown, closeDropdown } = useDropdownV2();
const { openBottomBar, closeBottomBar } = useBottomBar();
const actionBarId = getActionBarIdFromActionMenuId(actionMenuId);
const actionMenuDropdownId =
getActionMenuDropdownIdFromActionMenuId(actionMenuId);
const openActionMenuDropdown = () => {
closeBottomBar(actionBarId);
openDropdown(actionMenuDropdownId);
};
const openActionBar = () => {
closeDropdown(actionMenuDropdownId);
openBottomBar(actionBarId);
};
const closeActionMenuDropdown = () => {
closeDropdown(actionMenuDropdownId);
};
const closeActionBar = () => {
closeBottomBar(actionBarId);
};
return {
openActionMenuDropdown,
openActionBar,
closeActionBar,
closeActionMenuDropdown,
};
};

View File

@ -1,7 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilValue } from 'recoil';
import { IconCheck, IconQuestionMark, IconX } from 'twenty-ui';
import { CalendarEventParticipant } from '@/activities/calendar/types/CalendarEventParticipant';
@ -9,7 +8,6 @@ import { ParticipantChip } from '@/activities/components/ParticipantChip';
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
const StyledInlineCellBaseContainer = styled.div`
align-items: center;
@ -68,9 +66,6 @@ export const CalendarEventParticipantsResponseStatusField = ({
participants: CalendarEventParticipant[];
}) => {
const theme = useTheme();
const isRightDrawerAnimationCompleted = useRecoilValue(
isRightDrawerAnimationCompletedState,
);
const Icon = {
Yes: <IconCheck stroke={theme.icon.stroke.sm} />,
@ -103,9 +98,7 @@ export const CalendarEventParticipantsResponseStatusField = ({
</StyledLabelContainer>
</StyledLabelAndIconContainer>
<StyledDiv ref={participantsContainerRef}>
{isRightDrawerAnimationCompleted && (
<ExpandableList isChipCountDisplayed>{styledChips}</ExpandableList>
)}
</StyledDiv>
</StyledInlineCellBaseContainer>
</StyledPropertyBox>

View File

@ -6,13 +6,11 @@ import { useRecoilValue } from 'recoil';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import {
Avatar,
@ -24,7 +22,6 @@ import {
} from 'twenty-ui';
import {
CalendarChannelVisibility,
FeatureFlagKey,
TimelineCalendarEvent,
} from '~/generated-metadata/graphql';
@ -117,11 +114,7 @@ export const CalendarEventRow = ({
const theme = useTheme();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { displayCurrentEventCursor = false } = useContext(CalendarContext);
const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();
const { openCalendarEventInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const startsAt = getCalendarEventStartDate(calendarEvent);
const endsAt = getCalendarEventEndDate(calendarEvent);
@ -145,11 +138,7 @@ export const CalendarEventRow = ({
onClick={
showTitle
? () => {
if (isCommandMenuV2Enabled) {
openCalendarEventInCommandMenu(calendarEvent.id);
} else {
openCalendarEventRightDrawer(calendarEvent.id);
}
}
: undefined
}

View File

@ -1,36 +0,0 @@
import { useRecoilValue } from 'recoil';
import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails';
import { CalendarEventDetailsEffect } from '@/activities/calendar/components/CalendarEventDetailsEffect';
import { FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE } from '@/activities/calendar/graphql/operation-signatures/FindOneCalendarEventOperationSignature';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
export const RightDrawerCalendarEvent = () => {
const { upsertRecords } = useUpsertRecordsInStore();
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const { record: calendarEvent } = useFindOneRecord<CalendarEvent>({
objectNameSingular:
FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.objectNameSingular,
objectRecordId: viewableRecordId ?? '',
recordGqlFields: FIND_ONE_CALENDAR_EVENT_OPERATION_SIGNATURE.fields,
onCompleted: (record) => upsertRecords([record]),
});
if (!calendarEvent) {
return null;
}
return (
<RecordFieldValueSelectorContextProvider>
<CalendarEventDetailsEffect record={calendarEvent} />
<RecordValueSetterEffect recordId={calendarEvent.id} />
<CalendarEventDetails calendarEvent={calendarEvent} />
</RecordFieldValueSelectorContextProvider>
);
};

View File

@ -1,33 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
describe('useOpenCalendarEventRightDrawer', () => {
it('opens the right drawer with the calendar event', () => {
const { result } = renderHook(
() => {
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return {
...useOpenCalendarEventRightDrawer(),
isRightDrawerOpen,
viewableRecordId,
};
},
{ wrapper: RecoilRoot },
);
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openCalendarEventRightDrawer('1234');
});
expect(result.current.isRightDrawerOpen).toBe(true);
expect(result.current.viewableRecordId).toBe('1234');
});
});

View File

@ -1,25 +0,0 @@
import { useSetRecoilState } from 'recoil';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconCalendarEvent } from 'twenty-ui';
export const useOpenCalendarEventRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const openCalendarEventRightDrawer = (calendarEventId: string) => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewCalendarEvent, {
title: 'Calendar Event',
Icon: IconCalendarEvent,
});
setViewableRecordId(calendarEventId);
};
return { openCalendarEventRightDrawer };
};

View File

@ -1,38 +1,35 @@
import { useApolloClient } from '@apollo/client';
import { PartialBlock } from '@blocknote/core';
import { useCreateBlockNote } from '@blocknote/react';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import { useCallback, useMemo } from 'react';
import { useRecoilCallback, useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared';
import { useDebouncedCallback } from 'use-debounce';
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect';
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { PartialBlock } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import '@blocknote/mantine/style.css';
import { useCreateBlockNote } from '@blocknote/react';
import '@blocknote/react/style.css';
import { FeatureFlagKey } from '~/generated/graphql';
import { isArray, isNonEmptyString } from '@sniptt/guards';
type ActivityRichTextEditorProps = {
activityId: string;
@ -50,10 +47,6 @@ export const ActivityRichTextEditor = ({
const cache = useApolloClient().cache;
const activity = activityInStore as Task | Note | null;
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { objectMetadataItem: objectMetadataItemActivity } =
useObjectMetadataItem({
objectNameSingular: activityObjectNameSingular,
@ -280,9 +273,7 @@ export const ActivityRichTextEditor = ({
editor.setTextCursorPosition(newBlockId, 'end');
editor.focus();
},
isCommandMenuV2Enabled
? AppHotkeyScope.CommandMenuOpen
: RightDrawerHotkeyScope.RightDrawer,
AppHotkeyScope.CommandMenuOpen,
[],
{
preventDefault: false,

View File

@ -1,54 +0,0 @@
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState';
import {
AutosizeTextInput,
AutosizeTextInputVariant,
} from '@/ui/input/components/AutosizeTextInput';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-start;
overflow-y: auto;
position: relative;
`;
const StyledChatArea = styled.div`
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
padding: ${({ theme }) => theme.spacing(6)};
padding-bottom: 0px;
`;
const StyledNewMessageArea = styled.div`
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(6)};
padding-top: 0px;
`;
export const RightDrawerAIChat = () => {
const setCopilotQuery = useSetRecoilState(copilotQueryState);
return (
<StyledContainer>
<StyledChatArea>{/* TODO */}</StyledChatArea>
<StyledNewMessageArea>
<AutosizeTextInput
autoFocus
placeholder="Ask anything"
variant={AutosizeTextInputVariant.Icon}
onValidate={(text) => {
setCopilotQuery(text);
}}
/>
</StyledNewMessageArea>
</StyledContainer>
);
};

View File

@ -1,18 +0,0 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconSparkles } from 'twenty-ui';
export const useOpenCopilotRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.Copilot, {
title: 'Copilot',
Icon: IconSparkles,
});
};
};

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const copilotQueryState = createState({
key: 'activities/copilot-query',
defaultValue: '',
});

View File

@ -1,19 +1,10 @@
import styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil';
import { Avatar, GRAY_SCALE } from 'twenty-ui';
import { ActivityRow } from '@/activities/components/ActivityRow';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
FeatureFlagKey,
MessageChannelVisibility,
TimelineThread,
} from '~/generated/graphql';
import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql';
import { formatToHumanReadableDate } from '~/utils/date-utils';
const StyledHeading = styled.div<{ unread: boolean }>`
@ -77,11 +68,7 @@ type EmailThreadPreviewProps = {
};
export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
const { openEmailThread } = useEmailThread();
const { openEmailThreadInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const visibility = thread.visibility;
@ -103,48 +90,19 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
false,
];
const { isSameEventThanRightDrawerClose } = useRightDrawer();
const handleThreadClick = useRecoilCallback(
({ snapshot }) =>
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const clickJustTriggeredEmailDrawerClose =
isSameEventThanRightDrawerClose(event.nativeEvent);
const emailThreadIdWhenEmailThreadWasClosed = snapshot
.getLoadable(emailThreadIdWhenEmailThreadWasClosedState)
.getValue();
const handleThreadClick = () => {
const canOpen =
thread.visibility === MessageChannelVisibility.SHARE_EVERYTHING &&
(!clickJustTriggeredEmailDrawerClose ||
emailThreadIdWhenEmailThreadWasClosed !== thread.id);
thread.visibility === MessageChannelVisibility.SHARE_EVERYTHING;
if (canOpen) {
if (isCommandMenuV2Enabled) {
openEmailThreadInCommandMenu(thread.id);
} else {
openEmailThread(thread.id);
}
}
},
[
isCommandMenuV2Enabled,
isSameEventThanRightDrawerClose,
openEmailThread,
openEmailThreadInCommandMenu,
thread.id,
thread.visibility,
],
);
};
const isDisabled = visibility !== MessageChannelVisibility.SHARE_EVERYTHING;
return (
<ActivityRow
onClick={(event) => handleThreadClick(event)}
disabled={isDisabled}
>
<ActivityRow onClick={handleThreadClick} disabled={isDisabled}>
<StyledHeading unread={!thread.read}>
<StyledParticipantsContainer>
<Avatar

View File

@ -1,68 +0,0 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
const viewableEmailThreadId = '1234';
describe('useEmailThread', () => {
it('should open email thread', () => {
const { result } = renderHook(
() => {
const emailThread = useEmailThread();
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return { ...emailThread, isRightDrawerOpen, viewableRecordId };
},
{ wrapper: RecoilRoot },
);
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openEmailThread(viewableEmailThreadId);
});
expect(result.current.isRightDrawerOpen).toBe(true);
expect(result.current.viewableRecordId).toBe(viewableEmailThreadId);
});
it('should close email thread if trying to open the same thread id', () => {
const { result } = renderHook(
() => {
const emailThread = useEmailThread();
const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
isRightDrawerOpenState,
);
const [viewableRecordId, setViewableRecordId] = useRecoilState(
viewableRecordIdState,
);
return {
...emailThread,
isRightDrawerOpen,
viewableRecordId,
setIsRightDrawerOpen,
setViewableRecordId,
};
},
{ wrapper: RecoilRoot },
);
act(() => {
result.current.setIsRightDrawerOpen(true);
result.current.setViewableRecordId(viewableEmailThreadId);
});
act(() => {
result.current.openEmailThread(viewableEmailThreadId);
});
expect(result.current.isRightDrawerOpen).toBe(false);
expect(result.current.viewableRecordId).toBeNull();
});
});

View File

@ -1,36 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
export const useEmailThread = () => {
const { closeRightDrawer } = useRightDrawer();
const openEmailThreadRightDrawer = useOpenEmailThreadRightDrawer();
const openEmailThread = useRecoilCallback(
({ snapshot, set }) =>
(threadId: string) => {
const isRightDrawerOpen = snapshot
.getLoadable(isRightDrawerOpenState)
.getValue();
const viewableEmailThreadId = snapshot
.getLoadable(viewableRecordIdState)
.getValue();
if (isRightDrawerOpen && viewableEmailThreadId === threadId) {
set(viewableRecordIdState, null);
closeRightDrawer();
return;
}
openEmailThreadRightDrawer();
set(viewableRecordIdState, threadId);
},
[closeRightDrawer, openEmailThreadRightDrawer],
);
return { openEmailThread };
};

View File

@ -1,44 +0,0 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { Button, IconArrowsVertical } from 'twenty-ui';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
const StyledButtonContainer = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
padding: 16px 24px;
`;
export const IntermediaryMessages = ({
messages,
}: {
messages: EmailThreadMessageWithSender[];
}) => {
const [areMessagesOpen, setAreMessagesOpen] = useState(false);
if (messages.length === 0) {
return null;
}
return areMessagesOpen ? (
messages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
/>
))
) : (
<StyledButtonContainer>
<Button
Icon={IconArrowsVertical}
title={`${messages.length} email${messages.length > 1 ? 's' : ''}`}
size="small"
onClick={() => setAreMessagesOpen(true)}
/>
</StyledButtonContainer>
);
};

View File

@ -1,185 +0,0 @@
import styled from '@emotion/styled';
import { useEffect, useMemo } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
import { EmailLoader } from '@/activities/emails/components/EmailLoader';
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages';
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
import { ConnectedAccountProvider } from 'twenty-shared';
import { Button, IconArrowBackUp } from 'twenty-ui';
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
height: 85%;
overflow-y: auto;
`;
const StyledButtonContainer = styled.div<{ isMobile: boolean }>`
background: ${({ theme }) => theme.background.secondary};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
height: ${({ isMobile }) => (isMobile ? '100px' : '50px')};
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
box-sizing: border-box;
`;
export const RightDrawerEmailThread = () => {
const setMessageThread = useSetRecoilState(messageThreadState);
const isMobile = useIsMobile();
const {
thread,
messages,
fetchMoreMessages,
threadLoading,
messageThreadExternalId,
connectedAccountHandle,
messageChannelLoading,
connectedAccountProvider,
lastMessageExternalId,
} = useRightDrawerEmailThread();
useEffect(() => {
if (!messages[0]?.messageThread) {
return;
}
setMessageThread(messages[0]?.messageThread);
});
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
);
useRegisterClickOutsideListenerCallback({
callbackId:
'EmailThreadClickOutsideCallBack-' + (thread?.id ?? 'no-thread-id'),
callbackFunction: useRecoilCallback(
({ set }) =>
() => {
set(
emailThreadIdWhenEmailThreadWasClosedState,
thread?.id ?? 'no-thread-id',
);
},
[thread],
),
});
const messagesCount = messages.length;
const is5OrMoreMessages = messagesCount >= 5;
const firstMessages = messages.slice(
0,
is5OrMoreMessages ? 2 : messagesCount - 1,
);
const intermediaryMessages = is5OrMoreMessages
? messages.slice(2, messagesCount - 1)
: [];
const lastMessage = messages[messagesCount - 1];
const subject = messages[0]?.subject;
const canReply = useMemo(() => {
return (
connectedAccountHandle &&
connectedAccountProvider &&
lastMessage &&
messageThreadExternalId != null
);
}, [
connectedAccountHandle,
connectedAccountProvider,
lastMessage,
messageThreadExternalId,
]);
const handleReplyClick = () => {
if (!canReply) {
return;
}
let url: string;
switch (connectedAccountProvider) {
case ConnectedAccountProvider.MICROSOFT:
url = `https://outlook.office.com/mail/deeplink?ItemID=${lastMessageExternalId}`;
window.open(url, '_blank');
break;
case ConnectedAccountProvider.GOOGLE:
url = `https://mail.google.com/mail/?authuser=${connectedAccountHandle}#all/${messageThreadExternalId}`;
window.open(url, '_blank');
break;
case null:
throw new Error('Account provider not provided');
default:
assertUnreachable(connectedAccountProvider);
}
};
if (!thread || !messages.length) {
return null;
}
return (
<StyledWrapper>
<StyledContainer>
{threadLoading ? (
<EmailLoader loadingText="Loading thread" />
) : (
<>
<EmailThreadHeader
subject={subject}
lastMessageSentAt={lastMessage.receivedAt}
/>
{firstMessages.map((message) => (
<EmailThreadMessage
key={message.id}
sender={message.sender}
participants={message.messageParticipants}
body={message.text}
sentAt={message.receivedAt}
/>
))}
<IntermediaryMessages messages={intermediaryMessages} />
<EmailThreadMessage
key={lastMessage.id}
sender={lastMessage.sender}
participants={lastMessage.messageParticipants}
body={lastMessage.text}
sentAt={lastMessage.receivedAt}
isExpanded
/>
<CustomResolverFetchMoreLoader
loading={threadLoading}
onLastRowVisible={fetchMoreMessages}
/>
</>
)}
</StyledContainer>
{canReply && !messageChannelLoading && (
<StyledButtonContainer isMobile={isMobile}>
<Button
onClick={handleReplyClick}
title="Reply"
Icon={IconArrowBackUp}
disabled={!canReply}
/>
</StyledButtonContainer>
)}
</StyledWrapper>
);
};

View File

@ -1,40 +0,0 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { IconMail } from 'twenty-ui';
const mockOpenRightDrawer = jest.fn();
const mockSetHotkeyScope = jest.fn();
jest.mock('@/ui/layout/right-drawer/hooks/useRightDrawer', () => ({
useRightDrawer: () => ({
openRightDrawer: mockOpenRightDrawer,
}),
}));
jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
useSetHotkeyScope: () => mockSetHotkeyScope,
}));
test('useOpenEmailThreadRightDrawer opens the email thread right drawer', () => {
const { result } = renderHook(() => useOpenEmailThreadRightDrawer());
act(() => {
result.current();
});
expect(mockSetHotkeyScope).toHaveBeenCalledWith(
RightDrawerHotkeyScope.RightDrawer,
{ goto: false },
);
expect(mockOpenRightDrawer).toHaveBeenCalledWith(
RightDrawerPages.ViewEmailThread,
{
title: 'Email Thread',
Icon: IconMail,
},
);
});

View File

@ -1,416 +0,0 @@
import { renderHook, waitFor } from '@testing-library/react';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import gql from 'graphql-tag';
import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread';
const mocks = [
{
request: {
query: gql`
query FindOneMessageThread($objectRecordId: ID!) {
messageThread(filter: { id: { eq: $objectRecordId } }) {
__typename
id
}
}
`,
variables: { objectRecordId: '1' },
},
result: jest.fn(() => ({
data: {
messageThread: {
id: '1',
__typename: 'MessageThread',
},
},
})),
},
{
request: {
query: gql`
query FindManyMessages(
$filter: MessageFilterInput
$orderBy: [MessageOrderByInput]
$lastCursor: String
$limit: Int
) {
messages(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
createdAt
headerMessageId
id
messageParticipants {
edges {
node {
__typename
displayName
handle
id
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
role
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
}
}
}
messageThread {
__typename
id
}
receivedAt
subject
text
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: { messageThreadId: { eq: '1' } },
orderBy: [{ receivedAt: 'AscNullsLast' }],
lastCursor: undefined,
limit: 10,
},
},
result: jest.fn(() => ({
data: {
messages: {
edges: [
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'message',
input: {
id: '1',
text: 'Message 1',
createdAt: '2024-10-03T10:20:10.145Z',
},
}),
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'message',
input: {
id: '2',
text: 'Message 2',
createdAt: '2024-10-03T10:20:10.145Z',
},
}),
cursor: '2',
},
],
totalCount: 2,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '1',
endCursor: '2',
},
},
},
})),
},
{
request: {
query: gql`
query FindManyMessageParticipants(
$filter: MessageParticipantFilterInput
$orderBy: [MessageParticipantOrderByInput]
$lastCursor: String
$limit: Int
) {
messageParticipants(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
displayName
handle
id
messageId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
context
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
role
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
totalCount
}
}
`,
variables: {
filter: { messageId: { in: ['1', '2'] }, role: { eq: 'from' } },
orderBy: undefined,
lastCursor: undefined,
limit: undefined,
},
},
result: jest.fn(() => ({
data: {
messageParticipants: {
edges: [
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-1',
role: 'from',
messageId: '1',
},
}),
cursor: '1',
},
{
node: generateEmptyJestRecordNode({
objectNameSingular: 'messageParticipant',
input: {
id: 'messageParticipant-2',
role: 'from',
messageId: '2',
},
}),
cursor: '2',
},
],
totalCount: 2,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: '1',
endCursor: '2',
},
},
},
})),
},
];
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks: mocks,
onInitializeRecoilSnapshot: ({ set }) => {
set(viewableRecordIdState, '1');
},
});
describe('useRightDrawerEmailThread', () => {
it('should return correct values', async () => {
const mockMessages = [
{
__typename: 'Message',
createdAt: '2024-10-03T10:20:10.145Z',
headerMessageId: '',
id: '1',
messageParticipants: [],
messageThread: null,
receivedAt: null,
sender: {
__typename: 'MessageParticipant',
displayName: '',
handle: '',
id: 'messageParticipant-1',
messageId: '1',
person: null,
role: 'from',
workspaceMember: null,
},
subject: '',
text: 'Message 1',
},
{
__typename: 'Message',
createdAt: '2024-10-03T10:20:10.145Z',
headerMessageId: '',
id: '2',
messageParticipants: [],
messageThread: null,
receivedAt: null,
sender: {
__typename: 'MessageParticipant',
displayName: '',
handle: '',
id: 'messageParticipant-2',
messageId: '2',
person: null,
role: 'from',
workspaceMember: null,
},
subject: '',
text: 'Message 2',
},
];
const { result } = renderHook(() => useRightDrawerEmailThread(), {
wrapper: Wrapper,
});
await waitFor(() => {
expect(result.current.thread).toBeDefined();
expect(result.current.messages).toEqual(mockMessages);
expect(result.current.threadLoading).toBeFalsy();
expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
});
});
});

View File

@ -1,18 +0,0 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconMail } from 'twenty-ui';
export const useOpenEmailThreadRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewEmailThread, {
title: 'Email Thread',
Icon: IconMail,
});
};
};

View File

@ -1,186 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { fetchAllThreadMessagesOperationSignatureFactory } from '@/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory';
import { EmailThread } from '@/activities/emails/types/EmailThread';
import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage';
import { MessageChannel } from '@/accounts/types/MessageChannel';
import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender';
import { MessageChannelMessageAssociation } from '@/activities/emails/types/MessageChannelMessageAssociation';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { isDefined } from 'twenty-shared';
export const useRightDrawerEmailThread = () => {
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const { upsertRecords } = useUpsertRecordsInStore();
const [lastMessageId, setLastMessageId] = useState<string | null>(null);
const [lastMessageChannelId, setLastMessageChannelId] = useState<
string | null
>(null);
const [isMessagesFetchComplete, setIsMessagesFetchComplete] = useState(false);
const { record: thread } = useFindOneRecord<EmailThread>({
objectNameSingular: CoreObjectNameSingular.MessageThread,
objectRecordId: viewableRecordId ?? '',
recordGqlFields: {
id: true,
},
onCompleted: (record) => {
upsertRecords([record]);
},
});
const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE =
fetchAllThreadMessagesOperationSignatureFactory({
messageThreadId: viewableRecordId,
});
const {
records: messages,
loading: messagesLoading,
fetchMoreRecords,
hasNextPage,
} = useFindManyRecords<EmailThreadMessage>({
limit: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.limit,
filter: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.filter,
objectNameSingular:
FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.objectNameSingular,
orderBy: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.variables.orderBy,
recordGqlFields: FETCH_ALL_MESSAGES_OPERATION_SIGNATURE.fields,
skip: !viewableRecordId,
});
const fetchMoreMessages = useCallback(() => {
if (!messagesLoading && hasNextPage) {
fetchMoreRecords();
} else if (!hasNextPage) {
setIsMessagesFetchComplete(true);
}
}, [fetchMoreRecords, messagesLoading, hasNextPage]);
useEffect(() => {
if (messages.length > 0 && isMessagesFetchComplete) {
const lastMessage = messages[messages.length - 1];
setLastMessageId(lastMessage.id);
}
}, [messages, isMessagesFetchComplete]);
// TODO: introduce nested filters so we can retrieve the message sender directly from the message query
const { records: messageSenders } =
useFindManyRecords<EmailThreadMessageParticipant>({
filter: {
messageId: {
in: messages.map(({ id }) => id),
},
role: {
eq: 'from',
},
},
objectNameSingular: CoreObjectNameSingular.MessageParticipant,
recordGqlFields: {
id: true,
role: true,
displayName: true,
messageId: true,
handle: true,
person: true,
workspaceMember: true,
},
skip: messages.length === 0,
});
const { records: messageChannelMessageAssociationData } =
useFindManyRecords<MessageChannelMessageAssociation>({
filter: {
messageId: {
eq: lastMessageId ?? '',
},
},
objectNameSingular:
CoreObjectNameSingular.MessageChannelMessageAssociation,
recordGqlFields: {
id: true,
messageId: true,
messageChannelId: true,
messageThreadExternalId: true,
messageExternalId: true,
},
skip: !lastMessageId || !isMessagesFetchComplete,
});
useEffect(() => {
if (messageChannelMessageAssociationData.length > 0) {
setLastMessageChannelId(
messageChannelMessageAssociationData[0].messageChannelId,
);
}
}, [messageChannelMessageAssociationData]);
const { records: messageChannelData, loading: messageChannelLoading } =
useFindManyRecords<MessageChannel>({
filter: {
id: {
eq: lastMessageChannelId ?? '',
},
},
objectNameSingular: CoreObjectNameSingular.MessageChannel,
recordGqlFields: {
id: true,
handle: true,
connectedAccount: {
id: true,
provider: true,
},
},
skip: !lastMessageChannelId,
});
const messageThreadExternalId =
messageChannelMessageAssociationData.length > 0
? messageChannelMessageAssociationData[0].messageThreadExternalId
: null;
const lastMessageExternalId =
messageChannelMessageAssociationData.length > 0
? messageChannelMessageAssociationData[0].messageExternalId
: null;
const connectedAccountHandle =
messageChannelData.length > 0 ? messageChannelData[0].handle : null;
const messagesWithSender: EmailThreadMessageWithSender[] = messages
.map((message) => {
const sender = messageSenders.find(
(messageSender) => messageSender.messageId === message.id,
);
if (!sender) {
return null;
}
return {
...message,
sender,
};
})
.filter(isDefined);
const connectedAccount =
messageChannelData.length > 0
? messageChannelData[0]?.connectedAccount
: null;
const connectedAccountProvider = connectedAccount?.provider ?? null;
return {
thread,
messages: messagesWithSender,
messageThreadExternalId,
connectedAccountHandle,
connectedAccountProvider,
threadLoading: messagesLoading,
messageChannelLoading,
lastMessageExternalId,
fetchMoreMessages,
};
};

View File

@ -1,40 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { ReactNode } from 'react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
const Wrapper = ({ children }: { children: ReactNode }) => (
<RecoilRoot>
<MockedProvider addTypename={false}>{children}</MockedProvider>
</RecoilRoot>
);
describe('useOpenActivityRightDrawer', () => {
it('works as expected', () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const viewableRecordId = useRecoilValue(viewableRecordIdState);
return {
openActivityRightDrawer,
viewableRecordId,
};
},
{
wrapper: Wrapper,
},
);
expect(result.current.viewableRecordId).toBeNull();
act(() => {
result.current.openActivityRightDrawer('123');
});
expect(result.current.viewableRecordId).toBe('123');
});
});

View File

@ -1,63 +0,0 @@
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { IconList } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const useOpenActivityRightDrawer = ({
objectNameSingular,
}: {
objectNameSingular: CoreObjectNameSingular;
}) => {
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
useRightDrawer();
const [viewableRecordId, setViewableRecordId] = useRecoilState(
viewableRecordIdState,
);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const setHotkeyScope = useSetHotkeyScope();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { openRecordInCommandMenu } = useCommandMenu();
return (activityId: string) => {
if (
isRightDrawerOpen &&
rightDrawerPage === RightDrawerPages.ViewRecord &&
viewableRecordId === activityId
) {
return;
}
if (isCommandMenuV2Enabled) {
openRecordInCommandMenu({
recordId: activityId,
objectNameSingular,
isNewRecord: false,
});
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableRecordId(activityId);
setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord, {
title: objectNameSingular,
Icon: IconList,
});
}
};
};

View File

@ -1,27 +1,20 @@
import { useSetRecoilState } from 'recoil';
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { Note } from '@/activities/types/Note';
import { NoteTarget } from '@/activities/types/NoteTarget';
import { Task } from '@/activities/types/Task';
import { TaskTarget } from '@/activities/types/TaskTarget';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { IconList } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
export const useOpenCreateActivityDrawer = ({
activityObjectNameSingular,
@ -30,10 +23,6 @@ export const useOpenCreateActivityDrawer = ({
| CoreObjectNameSingular.Note
| CoreObjectNameSingular.Task;
}) => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
const { createOneRecord: createOneActivity } = useCreateOneRecord<
(Task | Note) & { position: 'first' | 'last' }
>({
@ -64,10 +53,6 @@ export const useOpenCreateActivityDrawer = ({
isUpsertingActivityInDBState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { openRecordInCommandMenu } = useCommandMenu();
const openCreateActivityDrawer = async ({
@ -78,12 +63,6 @@ export const useOpenCreateActivityDrawer = ({
customAssignee?: WorkspaceMember;
}) => {
setIsNewViewableRecordLoading(true);
if (!isCommandMenuV2Enabled) {
openRightDrawer(RightDrawerPages.ViewRecord, {
title: activityObjectNameSingular,
Icon: IconList,
});
}
setViewableRecordId(null);
setViewableRecordNameSingular(activityObjectNameSingular);
@ -125,15 +104,11 @@ export const useOpenCreateActivityDrawer = ({
setActivityTargetableEntityArray([]);
}
if (isCommandMenuV2Enabled) {
openRecordInCommandMenu({
recordId: activity.id,
objectNameSingular: activityObjectNameSingular,
isNewRecord: true,
});
} else {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
}
setViewableRecordId(activity.id);

View File

@ -5,8 +5,6 @@ import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-pic
import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerPickableMorphItemsComponentState';
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useRecoilCallback } from 'recoil';
type OpenActivityTargetInlineCellEditModeProps = {
@ -15,9 +13,6 @@ type OpenActivityTargetInlineCellEditModeProps = {
};
export const useOpenActivityTargetInlineCellEditMode = () => {
const { toggleClickOutsideListener: toggleRightDrawerClickOustideListener } =
useClickOutsideListener(RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID);
const { performSearch: multipleRecordPickerPerformSearch } =
useMultipleRecordPickerPerformSearch();
@ -66,8 +61,6 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
'',
);
toggleRightDrawerClickOustideListener(false);
multipleRecordPickerPerformSearch({
multipleRecordPickerInstanceId: recordPickerInstanceId,
forceSearchFilter: '',
@ -83,7 +76,7 @@ export const useOpenActivityTargetInlineCellEditMode = () => {
),
});
},
[multipleRecordPickerPerformSearch, toggleRightDrawerClickOustideListener],
[multipleRecordPickerPerformSearch],
);
return { openActivityTargetInlineCellEditMode };

View File

@ -1,9 +1,9 @@
import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { Note } from '@/activities/types/Note';
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
@ -68,9 +68,7 @@ export const NoteCard = ({
note: Note;
isSingleNote: boolean;
}) => {
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Note,
});
const { openRecordInCommandMenu } = useCommandMenu();
const body = getActivityPreview(note?.bodyV2?.blocknote ?? null);
@ -84,7 +82,12 @@ export const NoteCard = ({
return (
<StyledCard isSingleNote={isSingleNote}>
<StyledCardDetailsContainer
onClick={() => openActivityRightDrawer(note.id)}
onClick={() =>
openRecordInCommandMenu({
recordId: note.id,
objectNameSingular: CoreObjectNameSingular.Note,
})
}
>
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent>

View File

@ -7,13 +7,13 @@ import {
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { ActivityRow } from '@/activities/components/ActivityRow';
import { Task } from '@/activities/types/Task';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
import { useCompleteTask } from '../hooks/useCompleteTask';
@ -78,9 +78,7 @@ const StyledCheckboxContainer = styled.div`
export const TaskRow = ({ task }: { task: Task }) => {
const theme = useTheme();
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const { openRecordInCommandMenu } = useCommandMenu();
const body = getActivitySummary(task?.bodyV2?.blocknote ?? null);
@ -96,7 +94,10 @@ export const TaskRow = ({ task }: { task: Task }) => {
return (
<ActivityRow
onClick={() => {
openActivityRightDrawer(task.id);
openRecordInCommandMenu({
recordId: task.id,
objectNameSingular: CoreObjectNameSingular.Task,
});
}}
>
<StyledLeftSideContainer>

View File

@ -1,11 +1,11 @@
import styled from '@emotion/styled';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import {
EventRowDynamicComponentProps,
StyledEventRowItemAction,
StyledEventRowItemColumn,
} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { isNonEmptyString } from '@sniptt/guards';
@ -55,9 +55,7 @@ export const EventRowActivity = ({
? event.linkedRecordCachedName
: 'Untitled';
const openActivityRightDrawer = useOpenActivityRightDrawer({
objectNameSingular,
});
const { openRecordInCommandMenu } = useCommandMenu();
return (
<>
@ -66,7 +64,12 @@ export const EventRowActivity = ({
{`${eventAction} a related ${eventObject}`}
</StyledEventRowItemAction>
<StyledLinkedActivity
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
onClick={() =>
openRecordInCommandMenu({
recordId: event.linkedRecordId,
objectNameSingular,
})
}
>
{activityTitle}
</StyledLinkedActivity>

View File

@ -24,7 +24,6 @@ export const CommandMenu = () => {
const {
noResults,
copilotCommands,
matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands,
@ -47,10 +46,6 @@ export const CommandMenu = () => {
);
const commandGroups: CommandGroupConfig[] = [
{
heading: t`Copilot`,
items: copilotCommands,
},
{
heading: t`Record Selection`,
items: matchingStandardActionRecordSelectionCommands.concat(

View File

@ -11,7 +11,6 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@ -24,11 +23,9 @@ import {
Button,
IconChevronLeft,
IconX,
LightIconButton,
getOsControlSymbol,
useIsMobile,
} from 'twenty-ui';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledInputContainer = styled.div`
align-items: center;
@ -75,13 +72,6 @@ const StyledContentContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledCloseButtonContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
`;
const StyledCloseButtonWrapper = styled.div<{ isVisible: boolean }>`
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
`;
@ -110,10 +100,6 @@ export const CommandMenuTopBar = () => {
const theme = useTheme();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { contextChips } = useCommandMenuContextChips();
const location = useLocation();
@ -127,8 +113,6 @@ export const CommandMenuTopBar = () => {
return (
<StyledInputContainer>
<StyledContentContainer>
{isCommandMenuV2Enabled && (
<>
<AnimatePresence>
{commandMenuPage !== CommandMenuPages.Root && (
<motion.div
@ -154,8 +138,6 @@ export const CommandMenuTopBar = () => {
) : (
<CommandMenuContextChipGroups contextChips={contextChips} />
)}
</>
)}
{(commandMenuPage === CommandMenuPages.Root ||
commandMenuPage === CommandMenuPages.SearchRecords) && (
<>
@ -171,7 +153,6 @@ export const CommandMenuTopBar = () => {
</StyledContentContainer>
{!isMobile && (
<StyledCloseButtonWrapper isVisible={isButtonVisible}>
{isCommandMenuV2Enabled ? (
<Button
Icon={IconX}
dataTestId="page-header-close-command-menu-button"
@ -182,16 +163,6 @@ export const CommandMenuTopBar = () => {
ariaLabel="Close command menu"
onClick={closeCommandMenu}
/>
) : (
<StyledCloseButtonContainer>
<LightIconButton
accent={'tertiary'}
size={'medium'}
Icon={IconX}
onClick={closeCommandMenu}
/>
</StyledCloseButtonContainer>
)}
</StyledCloseButtonWrapper>
)}
</StyledInputContainer>

View File

@ -1,4 +1,3 @@
import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
import { CommandMenu } from '@/command-menu/components/CommandMenu';
import { CommandMenuCalendarEventPage } from '@/command-menu/pages/calendar-event/components/CommandMenuCalendarEventPage';
import { CommandMenuMessageThreadPage } from '@/command-menu/pages/message-thread/components/CommandMenuMessageThreadPage';
@ -19,7 +18,6 @@ export const COMMAND_MENU_PAGES_CONFIG = new Map<
[CommandMenuPages.ViewRecord, <CommandMenuRecordPage />],
[CommandMenuPages.ViewEmailThread, <CommandMenuMessageThreadPage />],
[CommandMenuPages.ViewCalendarEvent, <CommandMenuCalendarEventPage />],
[CommandMenuPages.Copilot, <RightDrawerAIChat />],
[
CommandMenuPages.WorkflowStepSelectTriggerType,
<CommandMenuWorkflowSelectTriggerType />,

View File

@ -3,49 +3,20 @@ import {
ActionMenuEntryScope,
ActionMenuEntryType,
} from '@/action-menu/types/ActionMenuEntry';
import { useOpenCopilotRightDrawer } from '@/activities/copilot/right-drawer/hooks/useOpenCopilotRightDrawer';
import { copilotQueryState } from '@/activities/copilot/right-drawer/states/copilotQueryState';
import { COMMAND_MENU_NAVIGATE_COMMANDS } from '@/command-menu/constants/CommandMenuNavigateCommands';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import {
Command,
CommandScope,
CommandType,
} from '@/command-menu/types/Command';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { i18n } from '@lingui/core';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { IconSparkles } from 'twenty-ui';
import { useDebounce } from 'use-debounce';
import { FeatureFlagKey } from '~/generated/graphql';
export const useCommandMenuCommands = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);
const commandMenuSearch = useRecoilValue(commandMenuSearchState);
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
const isCopilotEnabled = useIsFeatureEnabled(FeatureFlagKey.IsCopilotEnabled);
const setCopilotQuery = useSetRecoilState(copilotQueryState);
const openCopilotRightDrawer = useOpenCopilotRightDrawer();
const copilotCommand: Command = {
id: 'copilot',
to: '', // TODO
Icon: IconSparkles,
label: 'Open Copilot',
type: CommandType.Navigate,
onCommandClick: () => {
setCopilotQuery(deferredCommandMenuSearch);
openCopilotRightDrawer();
},
};
const copilotCommands: Command[] = isCopilotEnabled ? [copilotCommand] : [];
const navigateCommands = Object.values(COMMAND_MENU_NAVIGATE_COMMANDS);
const actionRecordSelectionCommands: Command[] = actionMenuEntries
@ -144,7 +115,6 @@ export const useCommandMenuCommands = () => {
}));
return {
copilotCommands,
navigateCommands,
actionRecordSelectionCommands,
actionGlobalCommands,

View File

@ -8,11 +8,9 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useCommandMenuHotKeys = () => {
const {
@ -33,10 +31,6 @@ export const useCommandMenuHotKeys = () => {
COMMAND_MENU_COMPONENT_INSTANCE_ID,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
useScopedHotkeys(
'ctrl+k,meta+k',
() => {
@ -71,7 +65,7 @@ export const useCommandMenuHotKeys = () => {
useScopedHotkeys(
[Key.Backspace, Key.Delete],
() => {
if (isNonEmptyString(commandMenuSearch) || !isCommandMenuV2Enabled) {
if (isNonEmptyString(commandMenuSearch)) {
return;
}

View File

@ -9,7 +9,6 @@ export const useMatchingCommandMenuCommands = ({
const { matchCommands } = useMatchCommands({ commandMenuSearch });
const {
copilotCommands,
navigateCommands,
actionRecordSelectionCommands,
actionObjectCommands,
@ -49,7 +48,6 @@ export const useMatchingCommandMenuCommands = ({
return {
noResults,
copilotCommands,
matchingStandardActionRecordSelectionCommands,
matchingStandardActionObjectCommands,
matchingWorkflowRunRecordSelectionCommands,

View File

@ -1,4 +1,4 @@
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { t } from '@lingui/core/macro';
@ -24,13 +24,7 @@ export const useSearchRecords = () => {
},
});
const openNoteRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Note,
});
const openTaskRightDrawer = useOpenActivityRightDrawer({
objectNameSingular: CoreObjectNameSingular.Task,
});
const { openRecordInCommandMenu } = useCommandMenu();
const commands = useMemo(() => {
return (globalSearchData?.globalSearch ?? []).map((searchRecord) => {
@ -63,14 +57,20 @@ export const useSearchRecords = () => {
to: '',
onCommandClick: () => {
searchRecord.objectSingularName === 'task'
? openTaskRightDrawer(searchRecord.recordId)
: openNoteRightDrawer(searchRecord.recordId);
? openRecordInCommandMenu({
recordId: searchRecord.recordId,
objectNameSingular: CoreObjectNameSingular.Task,
})
: openRecordInCommandMenu({
recordId: searchRecord.recordId,
objectNameSingular: CoreObjectNameSingular.Note,
});
},
};
}
return command;
});
}, [globalSearchData, openTaskRightDrawer, openNoteRightDrawer]);
}, [globalSearchData, openRecordInCommandMenu]);
return {
loading,

View File

@ -1,7 +1,5 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import {
WorkflowTriggerType,
WorkflowWithCurrentVersion,
@ -14,10 +12,8 @@ import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/Other
import { TRIGGER_STEP_ID } from '@/workflow/workflow-trigger/constants/TriggerStepId';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useSetRecoilState } from 'recoil';
import { MenuItemCommand, useIcons } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const CommandMenuWorkflowSelectTriggerTypeContent = ({
workflow,
@ -29,12 +25,8 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
const { openRightDrawer } = useRightDrawer();
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
const { openWorkflowEditStepInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const handleTriggerTypeClick = ({
type,
@ -56,18 +48,11 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
setWorkflowSelectedNode(TRIGGER_STEP_ID);
if (isCommandMenuV2Enabled) {
openWorkflowEditStepInCommandMenu(
workflow.id,
defaultLabel,
getIcon(icon),
);
} else {
openRightDrawer(RightDrawerPages.WorkflowStepEdit, {
title: defaultLabel,
Icon: getIcon(icon),
});
}
};
};

View File

@ -1,6 +1,4 @@
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { Button, IconButton, IconHeart, IconHeartOff } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { Button, IconHeart, IconHeartOff } from 'twenty-ui';
type PageFavoriteButtonProps = {
isFavorite: boolean;
@ -13,13 +11,7 @@ export const PageFavoriteButton = ({
}: PageFavoriteButtonProps) => {
const title = isFavorite ? 'Remove from favorites' : 'Add to favorites';
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (
<>
{isCommandMenuV2Enabled ? (
<Button
Icon={isFavorite ? IconHeartOff : IconHeart}
dataTestId="favorite-button"
@ -30,16 +22,5 @@ export const PageFavoriteButton = ({
onClick={onClick}
ariaLabel={title}
/>
) : (
<IconButton
Icon={IconHeart}
size="medium"
variant="secondary"
data-testid="add-button"
accent={isFavorite ? 'danger' : 'default'}
onClick={onClick}
/>
)}
</>
);
};

View File

@ -9,7 +9,7 @@ import {
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useRecoilValue } from 'recoil';
@ -41,9 +41,7 @@ export const RecordChip = ({
const { openRecordInCommandMenu } = useCommandMenu();
const recordIndexOpenRecordIn = useRecoilValue(
recordIndexOpenRecordInSelector,
);
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
// TODO temporary until we create a record show page for Workspaces members
if (forceDisableClick) {

View File

@ -24,24 +24,16 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewType } from '@/views/types/ViewType';
import { useDeleteViewFromCurrentState } from '@/views/view-picker/hooks/useDeleteViewFromCurrentState';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownMenuContent = () => {
const { t } = useLingui();
const {
recordIndexId,
objectMetadataItem,
viewType,
onContentChange,
closeDropdown,
} = useOptionsDropdown();
const { recordIndexId, objectMetadataItem, onContentChange, closeDropdown } =
useOptionsDropdown();
const { getIcon } = useIcons();
const { currentView } = useGetCurrentViewOnly();
@ -70,10 +62,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
viewBarId: recordIndexId,
});
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { deleteViewFromCurrentState } = useDeleteViewFromCurrentState();
const setViewPickerReferenceViewId = useSetRecoilComponentStateV2(
viewPickerReferenceViewIdComponentState,
@ -95,8 +83,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
{currentView?.name}
</DropdownMenuHeader>
{(isCommandMenuV2Enabled || viewType === ViewType.Kanban) && (
<>
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem
onClick={() => onContentChange('viewSettings')}
@ -106,8 +92,6 @@ export const ObjectOptionsDropdownMenuContent = () => {
/>
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItemsContainer scrollable={false}>
<MenuItem

View File

@ -15,10 +15,8 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const ObjectOptionsDropdownViewSettingsContent = () => {
const { t } = useLingui();
@ -41,17 +39,12 @@ export const ObjectOptionsDropdownViewSettingsContent = () => {
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (
<>
<DropdownMenuHeader StartIcon={IconChevronLeft} onClick={resetContent}>
{t`View settings`}
</DropdownMenuHeader>
<DropdownMenuItemsContainer>
{isCommandMenuV2Enabled && (
<MenuItem
onClick={() => onContentChange('viewSettingsOpenIn')}
LeftIcon={
@ -67,7 +60,6 @@ export const ObjectOptionsDropdownViewSettingsContent = () => {
}
hasSubMenu
/>
)}
{viewType === ViewType.Kanban && (
<MenuItemToggle
LeftIcon={IconBaselineDensitySmall}

View File

@ -4,7 +4,6 @@ import { useContext, useRef } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
@ -24,6 +23,7 @@ import { currentRecordSortsComponentState } from '@/object-record/record-sort/st
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
@ -76,10 +76,11 @@ export const RecordBoard = () => {
);
const actionMenuId = getActionMenuIdFromRecordIndexId(recordBoardId);
const { closeActionMenuDropdown } = useActionMenu(actionMenuId);
const { closeDropdown } = useDropdownV2();
const handleDragSelectionStart = () => {
closeActionMenuDropdown();
closeDropdown(actionMenuId);
toggleClickOutsideListener(false);
};

View File

@ -1,4 +1,3 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
@ -8,13 +7,14 @@ import { RecordBoardScopeInternalContext } from '@/object-record/record-board/sc
import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState';
import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState';
import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector';
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
import { RecordBoardCardBody } from '@/object-record/record-board/record-board-card/components/RecordBoardCardBody';
import { RecordBoardCardHeader } from '@/object-record/record-board/record-board-card/components/RecordBoardCardHeader';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { AppPath } from '@/types/AppPath';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
@ -119,11 +119,9 @@ export const RecordBoardCard = ({
),
);
const { openActionMenuDropdown } = useActionMenu(actionMenuId);
const { openDropdown } = useDropdownV2();
const recordIndexOpenRecordIn = useRecoilValue(
recordIndexOpenRecordInSelector,
);
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
const handleActionMenuDropdown = (event: React.MouseEvent) => {
event.preventDefault();
@ -132,7 +130,7 @@ export const RecordBoardCard = ({
x: event.clientX,
y: event.clientY,
});
openActionMenuDropdown();
openDropdown(actionMenuDropdownId);
};
const handleCardClick = () => {

View File

@ -19,7 +19,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { getFieldButtonIcon } from '@/object-record/record-field/utils/getFieldButtonIcon';
import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { RecordInlineCellEditMode } from '@/object-record/record-inline-cell/components/RecordInlineCellEditMode';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
@ -125,9 +125,7 @@ export const RecordBoardCardHeader = ({
return [updateEntity, { loading: false }];
};
const recordIndexOpenRecordIn = useRecoilValue(
recordIndexOpenRecordInSelector,
);
const recordIndexOpenRecordIn = useRecoilValue(recordIndexOpenRecordInState);
return (
<RecordBoardCardHeaderContainer showCompactView={showCompactView}>

View File

@ -1,3 +1,4 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
@ -6,11 +7,8 @@ import { SingleRecordPicker } from '@/object-record/record-picker/single-record-
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { IconList } from 'twenty-ui';
import { v4 } from 'uuid';
export const RecordBoardColumnNewOpportunity = ({
@ -34,14 +32,15 @@ export const RecordBoardColumnNewOpportunity = ({
const { createOneRecord: createCompany } = useCreateOneRecord({
objectNameSingular: CoreObjectNameSingular.Company,
});
const { openRightDrawer } = useRightDrawer();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
);
const createCompanyOpportunityAndOpenRightDrawer = async (
const { openRecordInCommandMenu } = useCommandMenu();
const createCompanyOpportunityAndOpenCommandMenu = async (
searchInput?: string,
) => {
const newRecordId = v4();
@ -53,9 +52,9 @@ export const RecordBoardColumnNewOpportunity = ({
setViewableRecordId(newRecordId);
setViewableRecordNameSingular(CoreObjectNameSingular.Company);
openRightDrawer(RightDrawerPages.ViewRecord, {
title: 'Company',
Icon: IconList,
openRecordInCommandMenu({
recordId: newRecordId,
objectNameSingular: CoreObjectNameSingular.Company,
});
if (isDefined(createdCompany)) {
@ -74,7 +73,7 @@ export const RecordBoardColumnNewOpportunity = ({
company ? handleEntitySelect(position, company) : null
}
objectNameSingular={CoreObjectNameSingular.Company}
onCreate={createCompanyOpportunityAndOpenRightDrawer}
onCreate={createCompanyOpportunityAndOpenCommandMenu}
/>
</OverlayContainer>
)}

View File

@ -9,13 +9,8 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import { IconEye } from 'twenty-ui';
import {
FeatureFlagKey,
FieldMetadataType,
RelationDefinitionType,
} from '~/generated-metadata/graphql';
@ -47,11 +42,7 @@ export const useAddNewRecordAndOpenRightDrawer = ({
.nameSingular ?? 'workspaceMember',
});
const { openRightDrawer } = useRightDrawer();
const { openRecordInCommandMenu } = useCommandMenu();
const isCommandMenuEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
if (
relationObjectMetadataNameSingular === 'workspaceMember' ||
@ -118,17 +109,10 @@ export const useAddNewRecordAndOpenRightDrawer = ({
setViewableRecordId(newRecordId);
setViewableRecordNameSingular(relationObjectMetadataNameSingular);
if (isCommandMenuEnabled) {
openRecordInCommandMenu({
recordId: newRecordId,
objectNameSingular: relationObjectMetadataNameSingular,
});
} else {
openRightDrawer(RightDrawerPages.ViewRecord, {
title: 'View Record',
Icon: IconEye,
});
}
},
};
};

View File

@ -14,13 +14,10 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledContainer = styled.div`
display: flex;
@ -47,10 +44,6 @@ export const RecordIndexContainer = () => {
objectNameSingular,
} = useRecordIndexContextOrThrow();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (
<>
<StyledContainer>
@ -96,9 +89,6 @@ export const RecordIndexContainer = () => {
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
</StyledContainerWithPadding>
)}
{!isCommandMenuV2Enabled && (
<RecordIndexActionMenu indexId={recordIndexId} />
)}
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
</>

View File

@ -1,20 +1,10 @@
import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton';
import { RecordIndexPageTableAddButton } from '@/object-record/record-index/components/RecordIndexPageTableAddButton';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { capitalize, isDefined } from 'twenty-shared';
import { capitalize } from 'twenty-shared';
import { useIcons } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
export const RecordIndexPageHeader = () => {
const { findObjectMetadataItemByNamePlural } =
@ -28,48 +18,14 @@ export const RecordIndexPageHeader = () => {
const { getIcon } = useIcons();
const Icon = getIcon(objectMetadataItem?.icon);
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
const { recordIndexId } = useRecordIndexContextOrThrow();
const numberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isObjectMetadataItemReadOnly =
isDefined(objectMetadataItem) &&
isObjectMetadataReadOnly(objectMetadataItem);
const shouldDisplayAddButton =
(numberOfSelectedRecords === 0 || !isCommandMenuV2Enabled) &&
!isObjectMetadataItemReadOnly &&
!isCommandMenuV2Enabled;
const isTable = recordIndexViewType === ViewType.Table;
const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
return (
<PageHeader title={pageHeaderTitle} Icon={Icon}>
{shouldDisplayAddButton &&
/**
* TODO: Logic between Table and Kanban should be merged here when we move some states to record-index
*/
(isTable ? (
<RecordIndexPageTableAddButton />
) : (
<RecordIndexPageKanbanAddButton />
))}
{isCommandMenuV2Enabled && (
<RecordIndexActionMenu indexId={recordIndexId} />
)}
<PageHeaderOpenCommandMenuButton />
</PageHeader>
);

View File

@ -1,22 +0,0 @@
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown';
import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexPageTableAddButton = () => {
const hasRecordGroups = useRecoilComponentValueV2(
hasRecordGroupsComponentSelector,
);
if (!hasRecordGroups) {
return <RecordIndexPageTableAddButtonNoGroup />;
}
return (
<RecordIndexAddRecordInGroupDropdown
dropdownId="record-index-page-table-add-button-dropdown"
clickableComponent={<PageAddButton />}
/>
);
};

View File

@ -1,24 +0,0 @@
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
export const RecordIndexPageTableAddButtonNoGroup = () => {
const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const { createNewTableRecord } = useCreateNewTableRecord({
objectMetadataItem,
recordTableId: recordIndexId,
});
const handleCreateNewTableRecord = () => {
createNewTableRecord();
};
return (
<>
<PageHotkeysEffect onAddButtonClick={handleCreateNewTableRecord} />
<PageAddButton onClick={handleCreateNewTableRecord} />
</>
);
};

View File

@ -1,21 +0,0 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { checkIfFeatureFlagIsEnabledOnWorkspace } from '@/workspace/utils/checkIfFeatureFlagIsEnabledOnWorkspace';
import { selector } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const recordIndexOpenRecordInSelector = selector<ViewOpenRecordInType>({
key: 'recordIndexOpenRecordInSelector',
get: ({ get }) => {
const currentWorkspace = get(currentWorkspaceState);
const isCommandMenuV2Enabled = checkIfFeatureFlagIsEnabledOnWorkspace(
FeatureFlagKey.IsCommandMenuV2Enabled,
currentWorkspace,
);
return isCommandMenuV2Enabled
? get(recordIndexOpenRecordInState)
: ViewOpenRecordInType.RECORD_PAGE;
},
});

View File

@ -1,20 +1,15 @@
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { isFieldValueReadOnly } from '@/object-record/record-field/utils/isFieldValueReadOnly';
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope';
import { RightDrawerTitleRecordInlineCell } from '@/object-record/record-right-drawer/components/RightDrawerTitleRecordInlineCell';
import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
import { FieldMetadataType } from '~/generated/graphql';
type SummaryCardProps = {
objectNameSingular: string;
@ -51,16 +46,6 @@ export const SummaryCard = ({
const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular);
const isMobile = useIsMobile() || isInRightDrawer;
const isReadOnly = isFieldValueReadOnly({
objectNameSingular,
isRecordDeleted: recordFromStore?.isDeleted,
contextStoreCurrentViewType: ContextStoreViewType.ShowPage,
});
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) {
return <ShowPageSummaryCardSkeletonLoader />;
}
@ -101,13 +86,7 @@ export const SummaryCard = ({
isDisplayModeFixHeight: true,
}}
>
{isCommandMenuV2Enabled ? (
<RecordTitleCell sizeVariant="md" />
) : isInRightDrawer ? (
<RightDrawerTitleRecordInlineCell />
) : (
<RecordInlineCell readonly={isReadOnly} />
)}
</FieldContext.Provider>
}
avatarType={recordIdentifier?.avatarType ?? 'rounded'}

View File

@ -5,20 +5,16 @@ import { recordIndexOpenRecordInState } from '@/object-record/record-index/state
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { useRecordTitleCell } from '@/object-record/record-title-cell/hooks/useRecordTitleCell';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { AppPath } from '@/types/AppPath';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared';
import { v4 } from 'uuid';
import { FeatureFlagKey } from '~/generated/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
export const useCreateNewTableRecord = ({
@ -34,11 +30,6 @@ export const useCreateNewTableRecord = ({
const setHotkeyScope = useSetHotkeyScope();
const setPendingRecordId = useSetRecoilComponentStateV2(
recordTablePendingRecordIdComponentState,
recordTableId,
);
const recordTablePendingRecordIdByGroupFamilyState =
useRecoilComponentCallbackStateV2(
recordTablePendingRecordIdByGroupComponentFamilyState,
@ -50,10 +41,6 @@ export const useCreateNewTableRecord = ({
const { openRecordInCommandMenu } = useCommandMenu();
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { createOneRecord } = useCreateOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
shouldMatchRootQueryFilter: true,
@ -68,7 +55,6 @@ export const useCreateNewTableRecord = ({
async () => {
const recordId = v4();
if (isCommandMenuV2Enabled) {
const recordIndexOpenRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInState)
.getValue();
@ -84,8 +70,7 @@ export const useCreateNewTableRecord = ({
openRecordTitleCell({
recordId,
fieldMetadataId:
objectMetadataItem.labelIdentifierFieldMetadataId,
fieldMetadataId: objectMetadataItem.labelIdentifierFieldMetadataId,
});
} else {
navigate(AppPath.RecordShowPage, {
@ -93,39 +78,14 @@ export const useCreateNewTableRecord = ({
objectRecordId: recordId,
});
}
return;
}
setPendingRecordId(recordId);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(
DEFAULT_CELL_SCOPE.scope,
DEFAULT_CELL_SCOPE.customScopes,
);
if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
setActiveDropdownFocusIdAndMemorizePrevious(
getDropdownFocusIdForRecordField(
recordId,
objectMetadataItem.labelIdentifierFieldMetadataId,
'table-cell',
),
);
}
},
[
createOneRecord,
isCommandMenuV2Enabled,
navigate,
objectMetadataItem.labelIdentifierFieldMetadataId,
objectMetadataItem.nameSingular,
openRecordInCommandMenu,
openRecordTitleCell,
setActiveDropdownFocusIdAndMemorizePrevious,
setHotkeyScope,
setPendingRecordId,
setSelectedTableCellEditMode,
],
);
const createNewTableRecordInGroup = useRecoilCallback(

View File

@ -10,8 +10,6 @@ import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-tab
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useMoveEditModeToTableCellPosition } from '@/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
@ -22,7 +20,7 @@ import { isDefined } from 'twenty-shared';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useOpenFieldInputEditMode } from '@/object-record/record-field/hooks/useOpenFieldInputEditMode';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexOpenRecordInSelector } from '@/object-record/record-index/states/selectors/recordIndexOpenRecordInSelector';
import { recordIndexOpenRecordInState } from '@/object-record/record-index/states/recordIndexOpenRecordInState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
@ -30,7 +28,6 @@ import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/drop
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { useNavigate } from 'react-router-dom';
import { IconList } from 'twenty-ui';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@ -68,7 +65,6 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const initDraftValue = useInitDraftValueV2();
const { openRightDrawer } = useRightDrawer();
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
const setViewableRecordNameSingular = useSetRecoilState(
viewableRecordNameSingularState,
@ -124,7 +120,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
leaveTableFocus();
const openRecordIn = snapshot
.getLoadable(recordIndexOpenRecordInSelector)
.getLoadable(recordIndexOpenRecordInState)
.getValue();
if (openRecordIn === ViewOpenRecordInType.RECORD_PAGE) {
@ -145,10 +141,6 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
leaveTableFocus();
setViewableRecordId(recordId);
setViewableRecordNameSingular(objectNameSingular);
openRightDrawer(RightDrawerPages.ViewRecord, {
title: objectNameSingular,
Icon: IconList,
});
return;
}
@ -204,7 +196,6 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
openRecordInCommandMenu,
setViewableRecordId,
setViewableRecordNameSingular,
openRightDrawer,
setHotkeyScope,
],
);

View File

@ -2,10 +2,8 @@ import { useRecoilCallback } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
@ -37,10 +35,6 @@ export const useTriggerActionMenuDropdown = ({
getActionMenuDropdownIdFromActionMenuId(actionMenuInstanceId),
);
const isActionBarOpenState = isBottomBarOpenedComponentState.atomFamily({
instanceId: getActionBarIdFromActionMenuId(actionMenuInstanceId),
});
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
@ -63,7 +57,6 @@ export const useTriggerActionMenuDropdown = ({
set(isRowSelectedFamilyState(recordId), true);
}
set(isActionBarOpenState, false);
set(isActionMenuDropdownOpenState, true);
const actionMenuDropdownId =
@ -72,7 +65,6 @@ export const useTriggerActionMenuDropdown = ({
setActiveDropdownFocusIdAndMemorizePrevious(actionMenuDropdownId);
},
[
isActionBarOpenState,
isActionMenuDropdownOpenState,
isRowSelectedFamilyState,
recordIndexActionMenuDropdownPositionState,

View File

@ -3,11 +3,9 @@ import { IconBuildingSkyscraper } from 'twenty-ui';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
import { SignInBackgroundMockContainer } from '@/sign-in-background-mock/components/SignInBackgroundMockContainer';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
const StyledTableContainer = styled.div`
display: flex;
@ -18,10 +16,7 @@ const StyledTableContainer = styled.div`
export const SignInBackgroundMockPage = () => {
return (
<PageContainer>
<PageHeader title="Companies" Icon={IconBuildingSkyscraper}>
<PageHotkeysEffect onAddButtonClick={() => {}} />
<PageAddButton />
</PageHeader>
<PageHeader title="Companies" Icon={IconBuildingSkyscraper} />
<PageBody>
<RecordFieldValueSelectorContextProvider>
<StyledTableContainer>

View File

@ -1,61 +0,0 @@
import styled from '@emotion/styled';
import { useBottomBarInternalHotkeyScopeManagement } from '@/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement';
import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const StyledContainerActionBar = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
bottom: 38px;
box-shadow: ${({ theme }) => theme.boxShadow.strong};
display: flex;
height: 48px;
width: max-content;
left: 50%;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
position: absolute;
top: auto;
transform: translateX(-50%);
z-index: 1;
`;
type BottomBarProps = {
bottomBarId: string;
bottomBarHotkeyScopeFromParent: HotkeyScope;
children: React.ReactNode;
};
export const BottomBar = ({
bottomBarId,
bottomBarHotkeyScopeFromParent,
children,
}: BottomBarProps) => {
const isBottomBarOpen = useRecoilComponentValueV2(
isBottomBarOpenedComponentState,
bottomBarId,
);
useBottomBarInternalHotkeyScopeManagement({
bottomBarId,
bottomBarHotkeyScopeFromParent,
});
if (!isBottomBarOpen) {
return null;
}
return (
<BottomBarInstanceContext.Provider value={{ instanceId: bottomBarId }}>
<StyledContainerActionBar data-select-disable className="bottom-bar">
{children}
</StyledContainerActionBar>
</BottomBarInstanceContext.Provider>
);
};

View File

@ -1,63 +0,0 @@
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import styled from '@emotion/styled';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { Button, IconPlus } from 'twenty-ui';
const StyledContainer = styled.div`
display: flex;
gap: 10px;
`;
const meta: Meta<typeof BottomBar> = {
title: 'UI/Layout/BottomBar/BottomBar',
component: BottomBar,
args: {
bottomBarId: 'test',
bottomBarHotkeyScopeFromParent: { scope: 'test' },
children: (
<StyledContainer>
<Button title="Test 1" Icon={IconPlus} />
<Button title="Test 2" Icon={IconPlus} />
<Button title="Test 3" Icon={IconPlus} />
</StyledContainer>
),
},
argTypes: {
bottomBarId: { control: false },
bottomBarHotkeyScopeFromParent: { control: false },
children: { control: false },
},
};
export default meta;
export const Default: StoryObj<typeof BottomBar> = {
decorators: [
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId: 'test',
}),
true,
);
}}
>
<Story />
</RecoilRoot>
),
],
};
export const Closed: StoryObj<typeof BottomBar> = {
decorators: [
(Story) => (
<RecoilRoot>
<Story />
</RecoilRoot>
),
],
};

View File

@ -1,87 +0,0 @@
import { useRecoilCallback } from 'recoil';
import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { isDefined } from 'twenty-shared';
export const useBottomBar = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const closeBottomBar = useRecoilCallback(
({ set }) =>
(specificComponentId: string) => {
goBackToPreviousHotkeyScope();
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId: specificComponentId,
}),
false,
);
},
[goBackToPreviousHotkeyScope],
);
const openBottomBar = useRecoilCallback(
({ set, snapshot }) =>
(specificComponentId: string, customHotkeyScope?: HotkeyScope) => {
const bottomBarHotkeyScope = snapshot
.getLoadable(
bottomBarHotkeyComponentState.atomFamily({
instanceId: specificComponentId,
}),
)
.getValue();
set(
isBottomBarOpenedComponentState.atomFamily({
instanceId: specificComponentId,
}),
true,
);
if (isDefined(customHotkeyScope)) {
setHotkeyScopeAndMemorizePreviousScope(
customHotkeyScope.scope,
customHotkeyScope.customScopes,
);
} else if (isDefined(bottomBarHotkeyScope)) {
setHotkeyScopeAndMemorizePreviousScope(
bottomBarHotkeyScope.scope,
bottomBarHotkeyScope.customScopes,
);
}
},
[setHotkeyScopeAndMemorizePreviousScope],
);
const toggleBottomBar = useRecoilCallback(
({ snapshot }) =>
(specificComponentId: string) => {
const isBottomBarOpen = snapshot
.getLoadable(
isBottomBarOpenedComponentState.atomFamily({
instanceId: specificComponentId,
}),
)
.getValue();
if (isBottomBarOpen) {
closeBottomBar(specificComponentId);
} else {
openBottomBar(specificComponentId);
}
},
[closeBottomBar, openBottomBar],
);
return {
closeBottomBar,
openBottomBar,
toggleBottomBar,
};
};

View File

@ -1,27 +0,0 @@
import { useEffect } from 'react';
import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useBottomBarInternalHotkeyScopeManagement = ({
bottomBarId,
bottomBarHotkeyScopeFromParent,
}: {
bottomBarId?: string;
bottomBarHotkeyScopeFromParent?: HotkeyScope;
}) => {
const [bottomBarHotkeyScope, setBottomBarHotkeyScope] =
useRecoilComponentStateV2(bottomBarHotkeyComponentState, bottomBarId);
useEffect(() => {
if (!isDeeplyEqual(bottomBarHotkeyScopeFromParent, bottomBarHotkeyScope)) {
setBottomBarHotkeyScope(bottomBarHotkeyScopeFromParent);
}
}, [
bottomBarHotkeyScope,
bottomBarHotkeyScopeFromParent,
setBottomBarHotkeyScope,
]);
};

View File

@ -1,11 +0,0 @@
import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const bottomBarHotkeyComponentState = createComponentStateV2<
HotkeyScope | null | undefined
>({
key: 'bottomBarHotkeyComponentState',
defaultValue: null,
componentInstanceContext: BottomBarInstanceContext,
});

View File

@ -1,3 +0,0 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const BottomBarInstanceContext = createComponentInstanceContext();

View File

@ -1,8 +0,0 @@
import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
export const isBottomBarOpenedComponentState = createComponentStateV2<boolean>({
key: 'isBottomBarOpenedComponentState',
defaultValue: false,
componentInstanceContext: BottomBarInstanceContext,
});

View File

@ -1,21 +1,12 @@
import {
AnimatedButton,
IconButton,
IconDotsVertical,
IconX,
getOsControlSymbol,
useIsMobile,
} from 'twenty-ui';
import { AnimatedButton, getOsControlSymbol, useIsMobile } from 'twenty-ui';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { motion } from 'framer-motion';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledButtonWrapper = styled.div`
z-index: 30;
@ -108,10 +99,6 @@ export const PageHeaderOpenCommandMenuButton = () => {
const { toggleCommandMenu } = useCommandMenu();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const isMobile = useIsMobile();
const ariaLabel = isCommandMenuOpened
@ -122,11 +109,8 @@ export const PageHeaderOpenCommandMenuButton = () => {
return (
<StyledButtonWrapper>
{isCommandMenuV2Enabled ? (
<AnimatedButton
animatedSvg={
<AnimatedIcon isCommandMenuOpened={isCommandMenuOpened} />
}
animatedSvg={<AnimatedIcon isCommandMenuOpened={isCommandMenuOpened} />}
className="page-header-command-menu-button"
dataTestId="page-header-command-menu-button"
size={isMobile ? 'medium' : 'small'}
@ -143,16 +127,6 @@ export const PageHeaderOpenCommandMenuButton = () => {
ease: 'easeInOut',
}}
/>
) : (
<IconButton
Icon={isCommandMenuOpened ? IconX : IconDotsVertical}
size="medium"
dataTestId="more-showpage-button"
accent="default"
variant="secondary"
onClick={toggleCommandMenu}
/>
)}
</StyledButtonWrapper>
);
};

View File

@ -1,18 +1,12 @@
import { useHasObjectReadOnlyPermission } from '@/settings/roles/hooks/useHasObjectReadOnlyPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { Button, IconButton, IconPlus, useIsMobile } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { Button, IconPlus, useIsMobile } from 'twenty-ui';
type PageAddButtonProps = {
onClick?: () => void;
};
export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const hasObjectReadOnlyPermission = useHasObjectReadOnlyPermission();
const isMobile = useIsMobile();
@ -24,8 +18,6 @@ export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
}
return (
<>
{isCommandMenuV2Enabled ? (
<Button
Icon={IconPlus}
dataTestId="add-button"
@ -36,17 +28,5 @@ export const PageAddButton = ({ onClick }: PageAddButtonProps) => {
onClick={onClick}
ariaLabel={t`New record`}
/>
) : (
<IconButton
Icon={IconPlus}
dataTestId="add-button"
size="medium"
variant="secondary"
accent="default"
ariaLabel={t`Add`}
onClick={onClick}
/>
)}
</>
);
};

View File

@ -1,9 +1,7 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { MOBILE_VIEWPORT } from 'twenty-ui';
import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer';
import { PagePanel } from './PagePanel';
type PageBodyProps = {
@ -45,6 +43,5 @@ export const PageBody = ({ children }: PageBodyProps) => (
<StyledLeftContainer>
<PagePanel>{children}</PagePanel>
</StyledLeftContainer>
<RightDrawer />
</StyledMainContainer>
);

View File

@ -3,9 +3,6 @@ import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { useRecoilValue } from 'recoil';
import {
IconButton,
IconChevronDown,
IconChevronUp,
IconComponent,
IconX,
LightIconButton,
@ -17,8 +14,6 @@ import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawe
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
export const PAGE_BAR_MIN_HEIGHT = 40;
@ -94,11 +89,6 @@ type PageHeaderProps = {
title?: ReactNode;
hasClosePageButton?: boolean;
onClosePage?: () => void;
hasPaginationButtons?: boolean;
hasPreviousRecord?: boolean;
hasNextRecord?: boolean;
navigateToPreviousRecord?: () => void;
navigateToNextRecord?: () => void;
Icon?: IconComponent;
children?: ReactNode;
};
@ -107,9 +97,6 @@ export const PageHeader = ({
title,
hasClosePageButton,
onClosePage,
hasPaginationButtons,
navigateToPreviousRecord,
navigateToNextRecord,
Icon,
children,
}: PageHeaderProps) => {
@ -119,10 +106,6 @@ export const PageHeader = ({
isNavigationDrawerExpandedState,
);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
return (
<StyledTopBarContainer>
<StyledLeftContainer>
@ -141,25 +124,11 @@ export const PageHeader = ({
)}
<StyledTopBarIconStyledTitleContainer>
{!isCommandMenuV2Enabled && hasPaginationButtons && (
<>
<IconButton
Icon={IconChevronUp}
size="small"
variant="secondary"
onClick={() => navigateToPreviousRecord?.()}
/>
<IconButton
Icon={IconChevronDown}
size="small"
variant="secondary"
onClick={() => navigateToNextRecord?.()}
/>
</>
)}
{Icon && (
<StyledIconContainer>
{Icon && <Icon size={theme.icon.size.md} />}
<Icon size={theme.icon.size.md} />
</StyledIconContainer>
)}
{title && (
<StyledTitleContainer data-testid="top-bar-title">
{typeof title === 'string' ? (

View File

@ -1,16 +0,0 @@
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
type PageHotkeysEffectProps = {
onAddButtonClick?: () => void;
};
export const PageHotkeysEffect = ({
onAddButtonClick,
}: PageHotkeysEffectProps) => {
useScopedHotkeys('c', () => onAddButtonClick?.(), TableHotkeyScope.Table, [
onAddButtonClick,
]);
return <></>;
};

View File

@ -1,94 +0,0 @@
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RIGHT_DRAWER_ANIMATION_VARIANTS } from '@/ui/layout/right-drawer/constants/RightDrawerAnimationVariants';
import { RightDrawerAnimationVariant } from '@/ui/layout/right-drawer/types/RightDrawerAnimationVariant';
import { RightDrawerRouter } from './RightDrawerRouter';
const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>`
background: ${({ theme }) => theme.background.primary};
border-left: ${({ theme, isRightDrawerMinimized }) =>
isRightDrawerMinimized
? `1px solid ${theme.border.color.strong}`
: `1px solid ${theme.border.color.medium}`};
border-top: ${({ theme, isRightDrawerMinimized }) =>
isRightDrawerMinimized ? `1px solid ${theme.border.color.strong}` : 'none'};
border-top-left-radius: ${({ theme, isRightDrawerMinimized }) =>
isRightDrawerMinimized ? theme.border.radius.md : '0'};
box-shadow: ${({ theme, isRightDrawerMinimized }) =>
isRightDrawerMinimized ? 'none' : theme.boxShadow.light};
height: 100dvh;
overflow-x: hidden;
position: fixed;
right: 0;
top: 0;
z-index: 30;
.modal-backdrop {
background: ${({ theme }) => theme.background.overlayTertiary};
}
`;
const StyledRightDrawer = styled.div`
display: flex;
flex-direction: row;
width: 100%;
`;
export const RightDrawer = () => {
const theme = useTheme();
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const setIsRightDrawerAnimationCompleted = useSetRecoilState(
isRightDrawerAnimationCompletedState,
);
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
const isMobile = useIsMobile();
const targetVariantForAnimation: RightDrawerAnimationVariant =
!isRightDrawerOpen
? 'closed'
: isRightDrawerMinimized
? 'minimized'
: isMobile
? 'fullScreen'
: 'normal';
const handleAnimationComplete = () => {
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
};
if (!isDefined(rightDrawerPage)) {
return <></>;
}
return (
<StyledContainer
isRightDrawerMinimized={isRightDrawerMinimized}
animate={targetVariantForAnimation}
variants={RIGHT_DRAWER_ANIMATION_VARIANTS}
transition={{ duration: theme.animation.duration.normal }}
onAnimationComplete={handleAnimationComplete}
>
<StyledRightDrawer>
{isRightDrawerOpen && <RightDrawerRouter />}
</StyledRightDrawer>
</StyledContainer>
);
};

View File

@ -1,84 +0,0 @@
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import {
ClickOutsideMode,
useListenClickOutside,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { workflowReactFlowRefState } from '@/workflow/workflow-diagram/states/workflowReactFlowRefState';
import styled from '@emotion/styled';
import { useRef } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
const StyledRightDrawerPage = styled.div`
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
`;
export const RightDrawerContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
const rightDrawerRef = useRef<HTMLDivElement>(null);
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const { closeRightDrawer } = useRightDrawer();
const workflowReactFlowRef = useRecoilValue(workflowReactFlowRefState);
useListenClickOutside({
refs: [
rightDrawerRef,
...(workflowReactFlowRef ? [workflowReactFlowRef] : []),
],
excludeClassNames: ['confirmation-modal'],
listenerId: RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
callback: useRecoilCallback(
({ snapshot, set }) =>
(event) => {
const isRightDrawerOpen = snapshot
.getLoadable(isRightDrawerOpenState)
.getValue();
const isRightDrawerMinimized = snapshot
.getLoadable(isRightDrawerMinimizedState)
.getValue();
if (isRightDrawerOpen && !isRightDrawerMinimized) {
set(rightDrawerCloseEventState, event);
closeRightDrawer();
}
},
[closeRightDrawer],
),
mode: ClickOutsideMode.comparePixels,
});
useScopedHotkeys(
[Key.Escape],
() => {
if (isRightDrawerOpen && !isRightDrawerMinimized) {
closeRightDrawer();
}
},
RightDrawerHotkeyScope.RightDrawer,
[isRightDrawerOpen, isRightDrawerMinimized],
);
return (
<StyledRightDrawerPage ref={rightDrawerRef}>
{children}
</StyledRightDrawerPage>
);
};

View File

@ -1,68 +0,0 @@
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/components/RightDrawerCalendarEvent';
import { RightDrawerAIChat } from '@/activities/copilot/right-drawer/components/RightDrawerAIChat';
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { RightDrawerContainer } from '@/ui/layout/right-drawer/components/RightDrawerContainer';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { RightDrawerWorkflowEditStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowEditStep';
import { RightDrawerWorkflowRunViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowRunViewStep';
import { RightDrawerWorkflowViewStep } from '@/workflow/workflow-steps/components/RightDrawerWorkflowViewStep';
import { RightDrawerWorkflowSelectAction } from '@/workflow/workflow-steps/workflow-actions/components/RightDrawerWorkflowSelectAction';
import { RightDrawerWorkflowSelectTriggerType } from '@/workflow/workflow-trigger/components/RightDrawerWorkflowSelectTriggerType';
import { isDefined } from 'twenty-shared';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
const StyledRightDrawerBody = styled.div`
display: flex;
flex-direction: column;
height: calc(
100vh - ${({ theme }) => theme.spacing(14)} - 1px
); // (-1 for border)
//overflow: auto;
position: relative;
`;
const RIGHT_DRAWER_PAGES_CONFIG = {
[RightDrawerPages.ViewEmailThread]: <RightDrawerEmailThread />,
[RightDrawerPages.ViewCalendarEvent]: <RightDrawerCalendarEvent />,
[RightDrawerPages.ViewRecord]: <RightDrawerRecord />,
[RightDrawerPages.Copilot]: <RightDrawerAIChat />,
[RightDrawerPages.WorkflowStepSelectTriggerType]: (
<RightDrawerWorkflowSelectTriggerType />
),
[RightDrawerPages.WorkflowStepSelectAction]: (
<RightDrawerWorkflowSelectAction />
),
[RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />,
[RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />,
[RightDrawerPages.WorkflowRunStepView]: <RightDrawerWorkflowRunViewStep />,
} satisfies Record<RightDrawerPages, JSX.Element>;
export const RightDrawerRouter = () => {
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
const rightDrawerPageComponent = isDefined(rightDrawerPage) ? (
RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage]
) : (
<></>
);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
return (
<RightDrawerContainer>
<RightDrawerTopBar />
{!isRightDrawerMinimized && (
<StyledRightDrawerBody>
{rightDrawerPageComponent}
</StyledRightDrawerBody>
)}
</RightDrawerContainer>
);
};

View File

@ -1,140 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Chip, ChipAccent, ChipSize, useIcons } from 'twenty-ui';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton';
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
import { RIGHT_DRAWER_PAGE_ICONS } from '@/ui/layout/right-drawer/constants/RightDrawerPageIcons';
import { RIGHT_DRAWER_PAGE_TITLES } from '@/ui/layout/right-drawer/constants/RightDrawerPageTitles';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
const StyledTopBarWrapper = styled.div`
align-items: center;
display: flex;
`;
const StyledMinimizeTopBarTitleContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 24px;
width: 168px;
`;
const StyledMinimizeTopBarTitle = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledMinimizeTopBarIcon = styled.div`
align-items: center;
display: flex;
`;
export const RightDrawerTopBar = () => {
const isMobile = useIsMobile();
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
const [isRightDrawerMinimized, setIsRightDrawerMinimized] = useRecoilState(
isRightDrawerMinimizedState,
);
const theme = useTheme();
const handleOnclick = () => {
if (isRightDrawerMinimized) {
setIsRightDrawerMinimized(false);
}
};
const { getIcon } = useIcons();
const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState,
);
const isNewViewableRecordLoading = useRecoilValue(
isNewViewableRecordLoadingState,
);
const viewableRecordId = useRecoilValue(viewableRecordIdState);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: viewableRecordNameSingular ?? 'company',
});
if (!rightDrawerPage) {
return null;
}
const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[rightDrawerPage]);
const ObjectIcon = getIcon(objectMetadataItem.icon);
const isViewRecordRightDrawerPage =
rightDrawerPage === RightDrawerPages.ViewRecord;
const label = isViewRecordRightDrawerPage
? objectMetadataItem.labelSingular
: RIGHT_DRAWER_PAGE_TITLES[rightDrawerPage];
const Icon = isViewRecordRightDrawerPage ? ObjectIcon : PageIcon;
return (
<StyledRightDrawerTopBar
onClick={handleOnclick}
isRightDrawerMinimized={isRightDrawerMinimized}
>
{!isRightDrawerMinimized && (
<Chip
disabled={isNewViewableRecordLoading}
label={label}
leftComponent={() => <Icon size={theme.icon.size.md} />}
size={ChipSize.Large}
accent={ChipAccent.TextSecondary}
clickable={false}
/>
)}
{isRightDrawerMinimized && (
<StyledMinimizeTopBarTitleContainer>
<StyledMinimizeTopBarIcon>
<Icon size={theme.icon.size.md} />
</StyledMinimizeTopBarIcon>
<StyledMinimizeTopBarTitle>{label}</StyledMinimizeTopBarTitle>
</StyledMinimizeTopBarTitleContainer>
)}
<StyledTopBarWrapper>
{!isMobile && !isRightDrawerMinimized && (
<RightDrawerTopBarMinimizeButton />
)}
{!isMobile &&
!isRightDrawerMinimized &&
isViewRecordRightDrawerPage && (
<RightDrawerTopBarExpandButton
to={
getBasePathToShowPage({
objectNameSingular: viewableRecordNameSingular ?? '',
}) + viewableRecordId
}
/>
)}
<RightDrawerTopBarCloseButton />
</StyledTopBarWrapper>
</StyledRightDrawerTopBar>
);
};

View File

@ -1,20 +0,0 @@
import { IconX, LightIconButton } from 'twenty-ui';
import { useRightDrawer } from '../hooks/useRightDrawer';
export const RightDrawerTopBarCloseButton = () => {
const { closeRightDrawer } = useRightDrawer();
const handleButtonClick = () => {
closeRightDrawer();
};
return (
<LightIconButton
Icon={IconX}
onClick={handleButtonClick}
size="medium"
accent="tertiary"
/>
);
};

View File

@ -1,17 +0,0 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { IconExternalLink, LightIconButton, UndecoratedLink } from 'twenty-ui';
export const RightDrawerTopBarExpandButton = ({ to }: { to: string }) => {
const { closeRightDrawer } = useRightDrawer();
return (
<UndecoratedLink to={to}>
<LightIconButton
size="medium"
accent="tertiary"
Icon={IconExternalLink}
onClick={() => closeRightDrawer()}
/>
</UndecoratedLink>
);
};

View File

@ -1,21 +0,0 @@
import { IconMinus, LightIconButton } from 'twenty-ui';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
export const RightDrawerTopBarMinimizeButton = () => {
const { isRightDrawerMinimized, minimizeRightDrawer, maximizeRightDrawer } =
useRightDrawer();
const handleButtonClick = () => {
isRightDrawerMinimized ? maximizeRightDrawer() : minimizeRightDrawer();
};
return (
<LightIconButton
Icon={IconMinus}
onClick={handleButtonClick}
size="medium"
accent="tertiary"
/>
);
};

View File

@ -1,22 +0,0 @@
import styled from '@emotion/styled';
export const StyledRightDrawerTopBar = styled.div<{
isRightDrawerMinimized: boolean;
}>`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
height: ${({ isRightDrawerMinimized }) =>
isRightDrawerMinimized ? '40px' : '56px'};
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
cursor: ${({ isRightDrawerMinimized }) =>
isRightDrawerMinimized ? 'pointer' : 'default'};
`;

View File

@ -1,56 +0,0 @@
import { expect } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { RightDrawerTopBar } from '../RightDrawerTopBar';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { useSetRecoilState } from 'recoil';
import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { useEffect } from 'react';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { within } from '@storybook/test';
const RightDrawerTopBarStateSetterEffect = () => {
const setRightDrawerPage = useSetRecoilState(rightDrawerPageState);
const setIsRightDrawerMinimizedState = useSetRecoilState(
isRightDrawerMinimizedState,
);
useEffect(() => {
setRightDrawerPage(RightDrawerPages.ViewRecord);
setIsRightDrawerMinimizedState(false);
}, [setIsRightDrawerMinimizedState, setRightDrawerPage]);
return null;
};
const meta: Meta<typeof RightDrawerTopBar> = {
title: 'Modules/Activities/RightDrawer/RightDrawerTopBar',
component: RightDrawerTopBar,
decorators: [
(Story) => (
<div style={{ width: '500px' }}>
<Story />
<RightDrawerTopBarStateSetterEffect />
</div>
),
IconsProviderDecorator,
ComponentWithRouterDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
],
};
export default meta;
type Story = StoryObj<typeof RightDrawerTopBar>;
export const Default: Story = {
play: async () => {
const canvas = within(document.body);
expect(await canvas.findByText('Company')).toBeInTheDocument();
},
};

View File

@ -1,32 +0,0 @@
import { THEME_COMMON } from 'twenty-ui';
export const RIGHT_DRAWER_ANIMATION_VARIANTS = {
fullScreen: {
x: '0%',
width: '100%',
height: '100%',
bottom: '0',
top: '0',
},
normal: {
x: '0%',
width: THEME_COMMON.rightDrawerWidth,
height: '100%',
bottom: '0',
top: '0',
},
closed: {
x: '100%',
width: '0',
height: '100%',
bottom: '0',
top: 'auto',
},
minimized: {
x: '0%',
width: 220,
height: 41,
bottom: '0',
top: 'auto',
},
};

View File

@ -1,2 +0,0 @@
export const RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID =
'right-drawer-click-outside-listener';

View File

@ -1,13 +0,0 @@
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.ViewEmailThread]: 'IconMail',
[RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent',
[RightDrawerPages.ViewRecord]: 'Icon123',
[RightDrawerPages.Copilot]: 'IconSparkles',
[RightDrawerPages.WorkflowStepSelectTriggerType]: 'IconSparkles',
[RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles',
[RightDrawerPages.WorkflowStepEdit]: 'IconSparkles',
[RightDrawerPages.WorkflowStepView]: 'IconSparkles',
[RightDrawerPages.WorkflowRunStepView]: 'IconSparkles',
} satisfies Record<RightDrawerPages, string>;

View File

@ -1,13 +0,0 @@
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.ViewEmailThread]: 'Email Thread',
[RightDrawerPages.ViewCalendarEvent]: 'Calendar Event',
[RightDrawerPages.ViewRecord]: 'Record Editor',
[RightDrawerPages.Copilot]: 'Copilot',
[RightDrawerPages.WorkflowStepSelectTriggerType]: 'Workflow',
[RightDrawerPages.WorkflowStepSelectAction]: 'Workflow',
[RightDrawerPages.WorkflowStepEdit]: 'Workflow',
[RightDrawerPages.WorkflowStepView]: 'Workflow',
[RightDrawerPages.WorkflowRunStepView]: 'Workflow',
} satisfies Record<RightDrawerPages, string>;

View File

@ -1,52 +0,0 @@
import { renderHook } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { IconList } from 'twenty-ui';
import { isRightDrawerOpenState } from '../../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../../states/rightDrawerPageState';
import { RightDrawerPages } from '../../types/RightDrawerPages';
import { useRightDrawer } from '../useRightDrawer';
describe('useRightDrawer', () => {
it('Should test the default behavior of useRightDrawer and change the states as the function calls', async () => {
const useCombinedHooks = () => {
const { openRightDrawer, closeRightDrawer } = useRightDrawer();
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
return {
openRightDrawer,
closeRightDrawer,
isRightDrawerOpen,
rightDrawerPage,
};
};
const { result } = renderHook(() => useCombinedHooks(), {
wrapper: RecoilRoot,
});
expect(result.current.rightDrawerPage).toBeNull();
expect(result.current.isRightDrawerOpen).toBeFalsy();
expect(result.current.openRightDrawer).toBeInstanceOf(Function);
expect(result.current.closeRightDrawer).toBeInstanceOf(Function);
await act(async () => {
result.current.openRightDrawer(RightDrawerPages.ViewRecord, {
title: 'Company',
Icon: IconList,
});
});
expect(result.current.rightDrawerPage).toEqual(RightDrawerPages.ViewRecord);
expect(result.current.isRightDrawerOpen).toBeTruthy();
await act(async () => {
result.current.closeRightDrawer();
});
expect(result.current.isRightDrawerOpen).toBeFalsy();
});
});

View File

@ -1,114 +0,0 @@
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
import { mapRightDrawerPageToCommandMenuPage } from '@/ui/layout/right-drawer/utils/mapRightDrawerPageToCommandMenuPage';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared';
import { IconComponent } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
export const useRightDrawer = () => {
const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const rightDrawerPage = useRecoilValue(rightDrawerPageState);
const isCommandMenuV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsCommandMenuV2Enabled,
);
const { navigateCommandMenu } = useCommandMenu();
const openRightDrawer = useRecoilCallback(
({ set }) =>
(
rightDrawerPage: RightDrawerPages,
commandMenuPageInfo: {
title: string;
Icon: IconComponent;
},
) => {
if (isCommandMenuV2Enabled) {
const commandMenuPage =
mapRightDrawerPageToCommandMenuPage(rightDrawerPage);
navigateCommandMenu({
page: commandMenuPage,
pageTitle: commandMenuPageInfo.title,
pageIcon: commandMenuPageInfo.Icon,
});
return;
}
set(rightDrawerPageState, rightDrawerPage);
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, false);
},
[isCommandMenuV2Enabled, navigateCommandMenu],
);
const closeRightDrawer = useRecoilCallback(
({ set }) =>
(args?: { emitCloseEvent?: boolean }) => {
set(isRightDrawerOpenState, false);
set(isRightDrawerMinimizedState, false);
if (isDefined(args?.emitCloseEvent) && args?.emitCloseEvent) {
emitRightDrawerCloseEvent();
}
},
[],
);
const minimizeRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, true);
},
[],
);
const maximizeRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerMinimizedState, false);
set(isRightDrawerOpenState, true);
},
[],
);
const isSameEventThanRightDrawerClose = useRecoilCallback(
({ snapshot }) =>
(event: MouseEvent | TouchEvent) => {
const rightDrawerCloseEvent = snapshot
.getLoadable(rightDrawerCloseEventState)
.getValue();
const isSameEvent =
rightDrawerCloseEvent?.target === event.target &&
rightDrawerCloseEvent?.timeStamp === event.timeStamp;
return isSameEvent;
},
[],
);
return {
rightDrawerPage,
isRightDrawerOpen,
isRightDrawerMinimized,
openRightDrawer,
closeRightDrawer,
minimizeRightDrawer,
maximizeRightDrawer,
isSameEventThanRightDrawerClose,
};
};

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerAnimationCompletedState = createState<boolean>({
key: 'isRightDrawerAnimationCompletedState',
defaultValue: false,
});

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerMinimizedState = createState<boolean>({
key: 'ui/layout/is-right-drawer-minimized',
defaultValue: false,
});

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const isRightDrawerOpenState = createState<boolean>({
key: 'ui/layout/is-right-drawer-open',
defaultValue: false,
});

View File

@ -1,8 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { MessageThread } from '@/activities/emails/types/MessageThread';
export const messageThreadState = createState<MessageThread | null>({
key: 'messageThreadState',
defaultValue: null,
});

View File

@ -1,6 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
export const rightDrawerCloseEventState = createState<Event | null>({
key: 'rightDrawerCloseEventState',
defaultValue: null,
});

View File

@ -1,9 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { RightDrawerTopBarDropdownButtons } from '@/ui/layout/right-drawer/types/RightDrawerTopBarDropdownButtons';
export const rightDrawerTopBarDropdownButtonState =
createState<RightDrawerTopBarDropdownButtons | null>({
key: 'rightDrawerTopBarDropdownButtonState',
defaultValue: null,
});

View File

@ -1,8 +0,0 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { RightDrawerPages } from '../types/RightDrawerPages';
export const rightDrawerPageState = createState<RightDrawerPages | null>({
key: 'ui/layout/right-drawer-page',
defaultValue: null,
});

Some files were not shown because too many files have changed in this diff Show More