496 add open in full page button on command menu record page (#10659)

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

I upgraded react tabler icons to the latest version to be able to use
the newest icons.

The option menu was no longer accessible on right drawer record pages,
this pr fixes this and creates a new button which opens the record show
page.
This button is accessible via the shortcut `Command` + `Enter`


https://github.com/user-attachments/assets/570071b2-4406-40bd-be48-a0e5e430ed70
This commit is contained in:
Raphaël Bosi
2025-03-05 12:02:31 +01:00
committed by GitHub
parent 03c945ef97
commit f3e667a651
19 changed files with 173 additions and 40 deletions

1
.nvmrc
View File

@ -1 +0,0 @@
18.17.1

View File

@ -54,7 +54,7 @@
"@sniptt/guards": "^0.2.0",
"@stoplight/elements": "^8.0.5",
"@swc/jest": "^0.2.29",
"@tabler/icons-react": "^2.44.0",
"@tabler/icons-react": "^3.31.0",
"@types/dompurify": "^3.0.5",
"@types/facepaint": "^1.2.5",
"@types/lodash.camelcase": "^4.3.7",

View File

@ -0,0 +1,66 @@
import { RightDrawerActionMenuDropdownHotkeyScope } from '@/action-menu/types/RightDrawerActionMenuDropdownHotkeyScope';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { AppPath } from '@/types/AppPath';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';
import { Button, IconBrowserMaximize, getOsControlSymbol } from 'twenty-ui';
import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledLink = styled(Link)`
text-decoration: none;
`;
type RecordShowRightDrawerOpenRecordButtonProps = {
objectNameSingular: string;
record: ObjectRecord;
};
export const RecordShowRightDrawerOpenRecordButton = ({
objectNameSingular,
record,
}: RecordShowRightDrawerOpenRecordButtonProps) => {
const { closeCommandMenu } = useCommandMenu();
const to = getLinkToShowPage(objectNameSingular, record);
const navigate = useNavigateApp();
const handleOpenRecord = () => {
navigate(AppPath.RecordShowPage, {
objectNameSingular,
objectRecordId: record.id,
});
closeCommandMenu();
};
useScopedHotkeys(
['ctrl+Enter,meta+Enter'],
handleOpenRecord,
AppHotkeyScope.CommandMenuOpen,
[closeCommandMenu, navigate, objectNameSingular, record.id],
);
useScopedHotkeys(
['ctrl+Enter,meta+Enter'],
handleOpenRecord,
RightDrawerActionMenuDropdownHotkeyScope.RightDrawerActionMenuDropdown,
[closeCommandMenu, navigate, objectNameSingular, record.id],
);
return (
<StyledLink to={to} onClick={closeCommandMenu}>
<Button
title="Open"
variant="primary"
accent="blue"
size="medium"
Icon={IconBrowserMaximize}
hotkeys={[getOsControlSymbol(), '⏎']}
/>
</StyledLink>
);
};

View File

@ -70,7 +70,7 @@ export const RightDrawerActionMenuDropdown = () => {
}}
data-select-disable
clickableComponent={
<Button title="Actions" hotkeys={[getOsControlSymbol(), 'O']} />
<Button title="Options" hotkeys={[getOsControlSymbol(), 'O']} />
}
dropdownPlacement="top-end"
dropdownOffset={{ y: parseInt(theme.spacing(2), 10) }}

View File

@ -16,11 +16,11 @@ import { msg } from '@lingui/core/macro';
import { userEvent, waitFor, within } from '@storybook/test';
import {
ComponentDecorator,
getCanvasElementForDropdownTesting,
IconFileExport,
IconHeart,
IconTrash,
MenuItemAccent,
getCanvasElementForDropdownTesting,
} from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
@ -124,19 +124,19 @@ export const WithButtonClicks: Story = {
play: async () => {
const canvas = within(getCanvasElementForDropdownTesting());
let actionButton = await canvas.findByText('Actions');
let actionButton = await canvas.findByText('Options');
await userEvent.click(actionButton);
const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton);
actionButton = await canvas.findByText('Actions');
actionButton = await canvas.findByText('Options');
await userEvent.click(actionButton);
const addToFavoritesButton = await canvas.findByText('Add to favorites');
await userEvent.click(addToFavoritesButton);
actionButton = await canvas.findByText('Actions');
actionButton = await canvas.findByText('Options');
await userEvent.click(actionButton);
const exportButton = await canvas.findByText('Export');

View File

@ -22,11 +22,14 @@ import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelect
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
@ -278,6 +281,56 @@ export const useCommandMenu = () => {
)
.getValue();
if (!objectMetadataItem) {
throw new Error(
`No object metadata item found for object name ${objectNameSingular}`,
);
}
set(
contextStoreCurrentObjectMetadataItemComponentState.atomFamily({
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}),
objectMetadataItem,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}),
{
mode: 'selection',
selectedRecordIds: [recordId],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}),
1,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}),
ContextStoreViewType.ShowPage,
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}),
snapshot
.getLoadable(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
}),
)
.getValue(),
);
const Icon = objectMetadataItem?.icon
? getIcon(objectMetadataItem.icon)
: getIcon('IconList');

View File

@ -4,6 +4,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
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 { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId';
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';
@ -52,11 +53,11 @@ export const RightDrawerRecord = () => {
>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: `record-show-${objectRecordId}`,
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
value={{ instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID }}
>
<StyledRightDrawerRecord isMobile={isMobile}>
<RecordFieldValueSelectorContextProvider>

View File

@ -0,0 +1 @@
export const RIGHT_DRAWER_RECORD_INSTANCE_ID = 'right-drawer-record';

View File

@ -1,4 +1,5 @@
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
import { RecordShowRightDrawerOpenRecordButton } from '@/action-menu/components/RecordShowRightDrawerOpenRecordButton';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { CardComponents } from '@/object-record/record-show/components/CardComponents';
@ -133,8 +134,16 @@ export const ShowPageSubContainer = ({
<StyledContentContainer isInRightDrawer={isInRightDrawer}>
{renderActiveTabContent()}
</StyledContentContainer>
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
<RightDrawerFooter actions={[<RecordShowRightDrawerActionMenu />]} />
{isInRightDrawer && recordFromStore && (
<RightDrawerFooter
actions={[
<RecordShowRightDrawerActionMenu />,
<RecordShowRightDrawerOpenRecordButton
objectNameSingular={targetableObject.targetObjectNameSingular}
record={recordFromStore}
/>,
]}
/>
)}
</StyledShowPageRightContainer>
</>

View File

@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftRaw from '../assets/microsoft.svg?react';
interface IconMicrosoftProps {
size?: number;
size?: number | string;
}
export const IconMicrosoft = (props: IconMicrosoftProps) => {

View File

@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftCalendarRaw from '../assets/microsoft-calendar.svg?react';
interface IconMicrosoftCalendarProps {
size?: number;
size?: number | string;
}
export const IconMicrosoftCalendar = (props: IconMicrosoftCalendarProps) => {

View File

@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftOutlookRaw from '../assets/microsoft-outlook.svg?react';
interface IconMicrosoftOutlookProps {
size?: number;
size?: number | string;
}
export const IconMicrosoftOutlook = (props: IconMicrosoftOutlookProps) => {

View File

@ -12,9 +12,10 @@ export {
IconArrowDown,
IconArrowLeft,
IconArrowRight,
IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal,
IconArrowsVertical,
IconAt,
IconBaselineDensitySmall,
IconBell,
@ -30,6 +31,7 @@ export {
IconBrandLinkedin,
IconBrandX,
IconBriefcase,
IconBrowserMaximize,
IconBuildingSkyscraper,
IconCalendar,
IconCalendarDue,
@ -42,8 +44,8 @@ export {
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconChevronsRight,
IconChevronUp,
IconChevronsRight,
IconCircleDot,
IconCircleOff,
IconCirclePlus,
@ -53,19 +55,15 @@ export {
IconClockPlay,
IconClockShare,
IconCode,
IconStepInto,
IconLogin2,
IconLogout,
IconCodeCircle,
IconCoins,
IconColorSwatch,
IconMessageCircle as IconComment,
IconCube,
IconTypography,
IconCopy,
IconCreativeCommonsSa,
IconCreditCard,
IconCsv,
IconCube,
IconCurrencyAfghani,
IconCurrencyBahraini,
IconCurrencyBaht,
@ -186,6 +184,8 @@ export {
IconLoader,
IconLock,
IconLockOpen,
IconLogin2,
IconLogout,
IconMail,
IconMailCog,
IconMap,
@ -250,6 +250,7 @@ export {
IconSquareKey,
IconSquareRoundedCheck,
IconSquareRoundedX,
IconStepInto,
IconTable,
IconTag,
IconTags,
@ -263,6 +264,7 @@ export {
IconTimelineEvent,
IconTrash,
IconTrashX,
IconTypography,
IconUnlink,
IconUpload,
IconUser,
@ -278,4 +280,4 @@ export {
IconX,
} from '@tabler/icons-react';
export type { TablerIconsProps } from '@tabler/icons-react';
export type { IconProps as TablerIconsProps } from '@tabler/icons-react';

View File

@ -893,6 +893,7 @@ import {
IconBroadcastOff,
IconBrowser,
IconBrowserCheck,
IconBrowserMaximize,
IconBrowserOff,
IconBrowserPlus,
IconBrowserX,
@ -8393,4 +8394,5 @@ export const ALL_ICONS = {
IconZoomReset,
IconZzz,
IconZzzOff,
IconBrowserMaximize,
};

View File

@ -1,9 +1,10 @@
// eslint-disable-next-line no-restricted-imports
import { FunctionComponent } from 'react';
export type IconComponentProps = {
className?: string;
size?: number;
stroke?: number;
size?: number | string;
stroke?: number | string;
color?: string;
};

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { motion, useAnimation } from 'framer-motion';
import { useEffect, useMemo } from 'react';
interface CircularProgressBarProps {
size?: number;

View File

@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconComponent } from '@ui/display';
import React from 'react';
import React, { FunctionComponent } from 'react';
export type MainButtonVariant = 'primary' | 'secondary';
@ -103,7 +103,7 @@ const StyledButton = styled.button<
`;
type MainButtonProps = Props & {
Icon?: IconComponent;
Icon?: IconComponent | FunctionComponent<{ size: number }>;
};
export const MainButton = ({

View File

@ -1,7 +1,7 @@
export type { TablerIconsProps } from '@tabler/icons-react';
export {
IconBook,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
} from '@tabler/icons-react';
export type { IconProps as TablerIconsProps } from '@tabler/icons-react';

View File

@ -16553,22 +16553,21 @@ __metadata:
languageName: node
linkType: hard
"@tabler/icons-react@npm:^2.44.0":
version: 2.47.0
resolution: "@tabler/icons-react@npm:2.47.0"
"@tabler/icons-react@npm:^3.31.0":
version: 3.31.0
resolution: "@tabler/icons-react@npm:3.31.0"
dependencies:
"@tabler/icons": "npm:2.47.0"
prop-types: "npm:^15.7.2"
"@tabler/icons": "npm:3.31.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
checksum: 10c0/09a78a1b88aab69a11cd783c7d1bb75aeabe2ed03e3f957d00113dfde974ec73ee850551c430664634469ebcf32db749497a81bd30c01f2fb3425884cba7429c
react: ">= 16"
checksum: 10c0/aceefe1b6c12a2e64921900f49c09c0c24bcd837dfb3d4274914065e33a5175944f1e25ce9a80d110272e5ae0fefc24062a38f1ec75a4ec79c63d40acb9d1d12
languageName: node
linkType: hard
"@tabler/icons@npm:2.47.0":
version: 2.47.0
resolution: "@tabler/icons@npm:2.47.0"
checksum: 10c0/ffdffa1289ca958f7d7c06c19e35c51a664d916df510fb44fb673abc2031747f2b3c47e21ba4c1ca07a6872b793f2063179bbbfb6afd1c68b522ca333460a7f3
"@tabler/icons@npm:3.31.0":
version: 3.31.0
resolution: "@tabler/icons@npm:3.31.0"
checksum: 10c0/03fcfff0b705e1474030acee2ff0523bfa1fda7a29a33d031a9fd8e3b52ecc2e0380f9cf5e81bd054b30dc0acdb819b1e08ab746805be8d660934a08b2c3545e
languageName: node
linkType: hard
@ -47190,7 +47189,7 @@ __metadata:
"@swc/core": "npm:1.7.42"
"@swc/helpers": "npm:~0.5.2"
"@swc/jest": "npm:^0.2.29"
"@tabler/icons-react": "npm:^2.44.0"
"@tabler/icons-react": "npm:^3.31.0"
"@testing-library/jest-dom": "npm:^6.1.5"
"@testing-library/react": "npm:14.0.0"
"@types/addressparser": "npm:^1.0.3"