diff --git a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md index 0a33e2287146..8e59f522b9c9 100644 --- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md +++ b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md @@ -1,6 +1,6 @@ -**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. -**Points**: 150 Points -**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. +**Side Quest**: Like & Re-Tweet oss.gg Launch Tweet. Quote-tweet it tagging @twentycrm to say you’ll be contributing. +**Points**: 50 Points +**Proof**: Add a screenshot of the retweet to the PR description. Add a link to your retweet in the list below. Please follow the following schema: diff --git a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md index 0a33e2287146..187d3f83035b 100644 --- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md +++ b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md @@ -1,6 +1,6 @@ -**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. -**Points**: 150 Points -**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. +**Side Quest**: Share a tweet about your favorite feature in Twenty. Tweet about your favorite feature in Twenty and mention @twentycrm. +**Points**: 50 Points +**Proof**: Add a screenshot of the tweet to the PR description. Add a link to your tweet in the list below. Please follow the following schema: diff --git a/oss-gg/twenty-side-quest/3-bug-report.md b/oss-gg/twenty-side-quest/3-bug-report.md new file mode 100644 index 000000000000..d393a2cbeac8 --- /dev/null +++ b/oss-gg/twenty-side-quest/3-bug-report.md @@ -0,0 +1,23 @@ +**Side Quest**: Create a bug report. Use the Twenty bug issue template to report a bug in detail, including steps to reproduce it. +**Points**: 50-150 Points +**Proof**: Add a link to your bug report in the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR NAME +» Link to bug report: https://github.com/twentyhq/twenty/issues/... + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 10-October-2024 by Devansh Baghel +» Link to bug report: https://github.com/twentyhq/twenty/issues/7560 + +--- diff --git a/oss-gg/twenty-side-quest/3-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md similarity index 77% rename from oss-gg/twenty-side-quest/3-meme-magic.md rename to oss-gg/twenty-side-quest/4-meme-magic.md index 0a33e2287146..111b1893084f 100644 --- a/oss-gg/twenty-side-quest/3-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -1,4 +1,4 @@ -**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. +**Side Quest**: Meme Magic: Craft a meme where the number twenty plays a role. Tweet it, and tag @twentycrm. **Points**: 150 Points **Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. diff --git a/oss-gg/twenty-side-quest/4-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md similarity index 60% rename from oss-gg/twenty-side-quest/4-gif-magic.md rename to oss-gg/twenty-side-quest/5-gif-magic.md index 0e38ace584d6..c76de36f87f0 100644 --- a/oss-gg/twenty-side-quest/4-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -1,4 +1,4 @@ -**Side Quest**: GIF Magic - Craft a GIF where a brick plays a role. Upload it to GIPHY with tags 'open source', 'foss', 'papermarkio'. +**Side Quest**: Gif Magic: Create a gif related to Twenty. Tweet it, and tag @twentycrm. **Points**: 150 Points **Proof**: Add a screenshot of GIF on Giphy to the PR description. Add a link to your GIPHY in the list below. @@ -7,7 +7,7 @@ Please follow the following schema: --- » 05-April-2024 by YOUR NAME -» Link to Tweet: https://giphy.com/... +» Link to gif: https://giphy.com/... --- @@ -18,6 +18,6 @@ Your turn 👇 //////////////////////////// » 01-October-2024 by YOUR NAME -» Link to Tweet: https://x.com/... +» Link to gif: https://giphy.com/... --- diff --git a/oss-gg/twenty-side-quest/5-quest-wizard.md b/oss-gg/twenty-side-quest/6-quest-wizard.md similarity index 85% rename from oss-gg/twenty-side-quest/5-quest-wizard.md rename to oss-gg/twenty-side-quest/6-quest-wizard.md index 2dfe4bd9c86b..9543e3767d6f 100644 --- a/oss-gg/twenty-side-quest/5-quest-wizard.md +++ b/oss-gg/twenty-side-quest/6-quest-wizard.md @@ -1,4 +1,4 @@ -**Side Quest**: Complete all papermarkio side quests +**Side Quest**: Complete all Twenty side quests **Points**: 300 Points **Proof**: Add screenshots for each side quest to the PR description. Add your name to the list below. diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 64d5248023f6..7b49d1c42fcb 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -352,15 +352,12 @@ export enum FieldMetadataType { Currency = 'CURRENCY', Date = 'DATE', DateTime = 'DATE_TIME', - Email = 'EMAIL', Emails = 'EMAILS', FullName = 'FULL_NAME', - Link = 'LINK', Links = 'LINKS', MultiSelect = 'MULTI_SELECT', Number = 'NUMBER', Numeric = 'NUMERIC', - Phone = 'PHONE', Phones = 'PHONES', Position = 'POSITION', Rating = 'RATING', diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index a1d58b1d9dd3..9e930133b3fb 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -263,15 +263,12 @@ export enum FieldMetadataType { Currency = 'CURRENCY', Date = 'DATE', DateTime = 'DATE_TIME', - Email = 'EMAIL', Emails = 'EMAILS', FullName = 'FULL_NAME', - Link = 'LINK', Links = 'LINKS', MultiSelect = 'MULTI_SELECT', Number = 'NUMBER', Numeric = 'NUMERIC', - Phone = 'PHONE', Phones = 'PHONES', Position = 'POSITION', Rating = 'RATING', diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx new file mode 100644 index 000000000000..d34b6c83fc54 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx @@ -0,0 +1,49 @@ +import styled from '@emotion/styled'; + +import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +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 { useRecoilValue } from 'recoil'; + +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 ActionMenuBar = () => { + const contextStoreTargetedRecordIds = useRecoilValue( + contextStoreTargetedRecordIdsState, + ); + + const actionMenuId = useAvailableComponentInstanceIdOrThrow( + ActionMenuComponentInstanceContext, + ); + + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentState, + ); + + return ( + + + {contextStoreTargetedRecordIds.length} selected: + + {actionMenuEntries.map((entry, index) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx new file mode 100644 index 000000000000..02802ec4a616 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx @@ -0,0 +1,49 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; + +type ActionMenuBarEntryProps = { + entry: ActionMenuEntry; +}; + +const StyledButton = styled.div<{ accent: MenuItemAccent }>` + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${(props) => + props.accent === 'danger' + ? props.theme.color.red + : props.theme.font.color.secondary}; + cursor: pointer; + display: flex; + justify-content: center; + + padding: ${({ theme }) => theme.spacing(2)}; + transition: background 0.1s ease; + user-select: none; + + &:hover { + background: ${({ theme, accent }) => + accent === 'danger' + ? theme.background.danger + : theme.background.tertiary}; + } +`; + +const StyledButtonLabel = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +export const ActionMenuBarEntry = ({ entry }: ActionMenuBarEntryProps) => { + const theme = useTheme(); + return ( + entry.onClick?.()} + > + {entry.Icon && } + {entry.label} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx new file mode 100644 index 000000000000..fc4af90b199b --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuConfirmationModals.tsx @@ -0,0 +1,18 @@ +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const ActionMenuConfirmationModals = () => { + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentState, + ); + + return ( +
+ {actionMenuEntries.map((actionMenuEntry, index) => + actionMenuEntry.ConfirmationModal ? ( +
{actionMenuEntry.ConfirmationModal}
+ ) : null, + )} +
+ ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx new file mode 100644 index 000000000000..b443c4307c66 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx @@ -0,0 +1,84 @@ +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { PositionType } from '../types/PositionType'; + +import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; + +type StyledContainerProps = { + position: PositionType; +}; + +const StyledContainerActionMenuDropdown = styled.div` + align-items: flex-start; + background: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-shadow: ${({ theme }) => theme.boxShadow.strong}; + display: flex; + flex-direction: column; + + left: ${(props) => `${props.position.x}px`}; + position: fixed; + top: ${(props) => `${props.position.y}px`}; + + transform: translateX(-50%); + width: auto; +`; + +export const ActionMenuDropdown = () => { + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentState, + ); + + const actionMenuId = useAvailableComponentInstanceIdOrThrow( + ActionMenuComponentInstanceContext, + ); + + const actionMenuDropdownPosition = useRecoilValue( + extractComponentState( + actionMenuDropdownPositionComponentState, + `action-menu-dropdown-${actionMenuId}`, + ), + ); + + //TODO: remove this + const width = actionMenuEntries.some( + (actionMenuEntry) => actionMenuEntry.label === 'Remove from favorites', + ) + ? 200 + : undefined; + + return ( + + ( + + ))} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx new file mode 100644 index 000000000000..60355cc9256f --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx @@ -0,0 +1,46 @@ +import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const ActionMenuEffect = () => { + const contextStoreTargetedRecordIds = useRecoilValue( + contextStoreTargetedRecordIdsState, + ); + + const actionMenuId = useAvailableComponentInstanceIdOrThrow( + ActionMenuComponentInstanceContext, + ); + + const { openActionBar, closeActionBar } = useActionMenu(actionMenuId); + + const isDropdownOpen = useRecoilValue( + extractComponentState( + isDropdownOpenComponentState, + `action-menu-dropdown-${actionMenuId}`, + ), + ); + + useEffect(() => { + if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) { + // 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 (contextStoreTargetedRecordIds.length === 0) { + closeActionBar(); + } + }, [ + contextStoreTargetedRecordIds, + openActionBar, + closeActionBar, + isDropdownOpen, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx new file mode 100644 index 000000000000..3a340feb85ef --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuEntriesProvider.tsx @@ -0,0 +1,25 @@ +import { EmptyActionMenuEntriesEffect } from '@/action-menu/components/EmptyActionMenuEntriesEffect'; +import { NonEmptyActionMenuEntriesEffect } from '@/action-menu/components/NonEmptyActionMenuEntriesEffect'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { useRecoilValue } from 'recoil'; + +export const ActionMenuEntriesProvider = () => { + //TODO: Refactor this + const contextStoreCurrentObjectMetadataId = useRecoilValue( + contextStoreCurrentObjectMetadataIdState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx new file mode 100644 index 000000000000..aca1e5fdae22 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/EmptyActionMenuEntriesEffect.tsx @@ -0,0 +1,14 @@ +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useEffect } from 'react'; + +export const EmptyActionMenuEntriesEffect = () => { + const setActionMenuEntries = useSetRecoilComponentStateV2( + actionMenuEntriesComponentState, + ); + useEffect(() => { + setActionMenuEntries([]); + }, [setActionMenuEntries]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx new file mode 100644 index 000000000000..210db62500cd --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/NonEmptyActionMenuEntriesEffect.tsx @@ -0,0 +1,28 @@ +import { useComputeActionsBasedOnContextStore } from '@/action-menu/hooks/useComputeActionsBasedOnContextStore'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useEffect } from 'react'; + +export const NonEmptyActionMenuEntriesEffect = ({ + contextStoreCurrentObjectMetadataId, +}: { + contextStoreCurrentObjectMetadataId: string; +}) => { + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: contextStoreCurrentObjectMetadataId, + }); + const { availableActionsInContext } = useComputeActionsBasedOnContextStore({ + objectMetadataItem, + }); + + const setActionMenuEntries = useSetRecoilComponentStateV2( + actionMenuEntriesComponentState, + ); + + useEffect(() => { + setActionMenuEntries(availableActionsInContext); + }, [availableActionsInContext, setActionMenuEntries]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx new file mode 100644 index 000000000000..907d6caf3f4f --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx @@ -0,0 +1,101 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; +import { userEvent, waitFor, within } from '@storybook/test'; +import { IconCheckbox, IconTrash } from 'twenty-ui'; + +const deleteMock = jest.fn(); +const markAsDoneMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/ActionMenuBar', + component: ActionMenuBar, + decorators: [ + (Story) => ( + { + set(contextStoreTargetedRecordIdsState, ['1', '2', '3']); + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + [ + { + label: 'Delete', + Icon: IconTrash, + onClick: deleteMock, + }, + { + label: 'Mark as done', + Icon: IconCheckbox, + onClick: markAsDoneMock, + }, + ], + ); + set( + isBottomBarOpenedComponentState.atomFamily({ + instanceId: 'action-bar-story-action-menu', + }), + true, + ); + }} + > + + + + + ), + ], + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export default meta; + +type Story = StoryObj; + +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); + + const markAsDoneButton = await canvas.findByText('Mark as done'); + await userEvent.click(markAsDoneButton); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + expect(markAsDoneMock).toHaveBeenCalled(); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx new file mode 100644 index 000000000000..a84e033312db --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx @@ -0,0 +1,56 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; + +import { ComponentDecorator, IconCheckbox, IconTrash } from 'twenty-ui'; +import { ActionMenuBarEntry } from '../ActionMenuBarEntry'; + +const meta: Meta = { + title: 'Modules/ActionMenu/ActionMenuBarEntry', + component: ActionMenuBarEntry, + decorators: [ComponentDecorator], +}; + +export default meta; + +type Story = StoryObj; + +const deleteMock = jest.fn(); +const markAsDoneMock = jest.fn(); + +export const Default: Story = { + args: { + entry: { + label: 'Delete', + Icon: IconTrash, + onClick: deleteMock, + }, + }, +}; + +export const WithDangerAccent: Story = { + args: { + entry: { + label: 'Delete', + Icon: IconTrash, + onClick: deleteMock, + accent: 'danger', + }, + }, +}; + +export const WithInteraction: Story = { + args: { + entry: { + label: 'Mark as done', + Icon: IconCheckbox, + onClick: markAsDoneMock, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = await canvas.findByText('Mark as done'); + await userEvent.click(button); + expect(markAsDoneMock).toHaveBeenCalled(); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx new file mode 100644 index 000000000000..4848f44eeec5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx @@ -0,0 +1,102 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/testing-library'; +import { RecoilRoot } from 'recoil'; + +import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; +import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui'; + +const deleteMock = jest.fn(); +const markAsDoneMock = jest.fn(); +const addToFavoritesMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/ActionMenuDropdown', + component: ActionMenuDropdown, + decorators: [ + (Story) => ( + { + set( + extractComponentState( + actionMenuDropdownPositionComponentState, + 'action-menu-dropdown-story', + ), + { x: 10, y: 10 }, + ); + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + [ + { + label: 'Delete', + Icon: IconTrash, + onClick: deleteMock, + }, + { + label: 'Mark as done', + Icon: IconCheckbox, + onClick: markAsDoneMock, + }, + { + label: 'Add to favorites', + Icon: IconHeart, + onClick: addToFavoritesMock, + }, + ], + ); + set( + extractComponentState( + isDropdownOpenComponentState, + 'action-menu-dropdown-story-action-menu', + ), + true, + ); + }} + > + + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + actionMenuId: 'story', + }, +}; + +export const WithInteractions: Story = { + args: { + actionMenuId: 'story', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const deleteButton = await canvas.findByText('Delete'); + await userEvent.click(deleteButton); + expect(deleteMock).toHaveBeenCalled(); + + const markAsDoneButton = await canvas.findByText('Mark as done'); + await userEvent.click(markAsDoneButton); + expect(markAsDoneMock).toHaveBeenCalled(); + + const addToFavoritesButton = await canvas.findByText('Add to favorites'); + await userEvent.click(addToFavoritesButton); + expect(addToFavoritesMock).toHaveBeenCalled(); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts new file mode 100644 index 000000000000..0f37475adedc --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useActionMenu.test.ts @@ -0,0 +1,83 @@ +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'; + + 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(`action-bar-${actionMenuId}`); + expect(openDropdown).toHaveBeenCalledWith( + `action-menu-dropdown-${actionMenuId}`, + ); + }); + + it('should call the correct functions when opening action bar', () => { + const { result } = renderHook(() => useActionMenu(actionMenuId)); + + act(() => { + result.current.openActionBar(); + }); + + expect(closeDropdown).toHaveBeenCalledWith( + `action-menu-dropdown-${actionMenuId}`, + ); + expect(openBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`); + }); + + it('should call the correct function when closing action menu dropdown', () => { + const { result } = renderHook(() => useActionMenu(actionMenuId)); + + act(() => { + result.current.closeActionMenuDropdown(); + }); + + expect(closeDropdown).toHaveBeenCalledWith( + `action-menu-dropdown-${actionMenuId}`, + ); + }); + + it('should call the correct function when closing action bar', () => { + const { result } = renderHook(() => useActionMenu(actionMenuId)); + + act(() => { + result.current.closeActionBar(); + }); + + expect(closeBottomBar).toHaveBeenCalledWith(`action-bar-${actionMenuId}`); + }); +}); diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts new file mode 100644 index 000000000000..881cadd694e1 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/hooks/useActionMenu.ts @@ -0,0 +1,32 @@ +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 openActionMenuDropdown = () => { + closeBottomBar(`action-bar-${actionMenuId}`); + openDropdown(`action-menu-dropdown-${actionMenuId}`); + }; + + const openActionBar = () => { + closeDropdown(`action-menu-dropdown-${actionMenuId}`); + openBottomBar(`action-bar-${actionMenuId}`); + }; + + const closeActionMenuDropdown = () => { + closeDropdown(`action-menu-dropdown-${actionMenuId}`); + }; + + const closeActionBar = () => { + closeBottomBar(`action-bar-${actionMenuId}`); + }; + + return { + openActionMenuDropdown, + openActionBar, + closeActionBar, + closeActionMenuDropdown, + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx b/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx new file mode 100644 index 000000000000..899908fb2a4c --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/hooks/useComputeActionsBasedOnContextStore.tsx @@ -0,0 +1,150 @@ +import { useHandleFavoriteButton } from '@/action-menu/hooks/useHandleFavoriteButton'; +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; +import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; +import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; +import { + displayedExportProgress, + useExportTableData, +} from '@/object-record/record-index/options/hooks/useExportTableData'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useCallback, useMemo, useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { + IconFileExport, + IconHeart, + IconHeartOff, + IconTrash, + isDefined, +} from 'twenty-ui'; + +export const useComputeActionsBasedOnContextStore = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { + const contextStoreTargetedRecordIds = useRecoilValue( + contextStoreTargetedRecordIdsState, + ); + + const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = + useState(false); + + const { handleFavoriteButtonClick } = useHandleFavoriteButton( + contextStoreTargetedRecordIds, + objectMetadataItem, + ); + + const baseTableDataParams = { + delayMs: 100, + objectNameSingular: objectMetadataItem.nameSingular, + recordIndexId: objectMetadataItem.namePlural, + }; + + const { deleteTableData } = useDeleteTableData(baseTableDataParams); + + const handleDeleteClick = useCallback(() => { + deleteTableData(contextStoreTargetedRecordIds); + }, [deleteTableData, contextStoreTargetedRecordIds]); + + const { progress, download } = useExportTableData({ + ...baseTableDataParams, + filename: `${objectMetadataItem.nameSingular}.csv`, + }); + + const isRemote = objectMetadataItem.isRemote; + + const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; + + const canDelete = + !isObjectMetadataReadOnly(objectMetadataItem) && + numberOfSelectedRecords < DELETE_MAX_COUNT; + + const menuActions: ActionMenuEntry[] = useMemo( + () => + [ + { + label: displayedExportProgress(progress), + Icon: IconFileExport, + accent: 'default', + onClick: () => download(), + } satisfies ActionMenuEntry, + canDelete + ? ({ + label: 'Delete', + Icon: IconTrash, + accent: 'danger', + onClick: () => { + setIsDeleteRecordsModalOpen(true); + }, + ConfirmationModal: ( + handleDeleteClick()} + deleteButtonText={`Delete ${ + numberOfSelectedRecords > 1 ? 'Records' : 'Record' + }`} + /> + ), + } satisfies ActionMenuEntry) + : undefined, + ].filter(isDefined), + [ + download, + progress, + canDelete, + handleDeleteClick, + isDeleteRecordsModalOpen, + numberOfSelectedRecords, + ], + ); + + const hasOnlyOneRecordSelected = contextStoreTargetedRecordIds.length === 1; + + const { favorites } = useFavorites(); + + const isFavorite = + isNonEmptyString(contextStoreTargetedRecordIds[0]) && + !!favorites?.find( + (favorite) => favorite.recordId === contextStoreTargetedRecordIds[0], + ); + + return { + availableActionsInContext: [ + ...menuActions, + ...(!isRemote && isFavorite && hasOnlyOneRecordSelected + ? [ + { + label: 'Remove from favorites', + Icon: IconHeartOff, + onClick: handleFavoriteButtonClick, + }, + ] + : []), + ...(!isRemote && !isFavorite && hasOnlyOneRecordSelected + ? [ + { + label: 'Add to favorites', + Icon: IconHeart, + onClick: handleFavoriteButtonClick, + }, + ] + : []), + ], + }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts b/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts new file mode 100644 index 000000000000..b965fe61f2d1 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/hooks/useHandleFavoriteButton.ts @@ -0,0 +1,49 @@ +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecoilCallback } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useHandleFavoriteButton = ( + selectedRecordIds: string[], + objectMetadataItem: ObjectMetadataItem, + callback?: () => void, +) => { + const { createFavorite, favorites, deleteFavorite } = useFavorites(); + + const handleFavoriteButtonClick = useRecoilCallback( + ({ snapshot }) => + () => { + if (selectedRecordIds.length > 1) { + return; + } + + const selectedRecordId = selectedRecordIds[0]; + const selectedRecord = snapshot + .getLoadable(recordStoreFamilyState(selectedRecordId)) + .getValue(); + + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === selectedRecordId, + ); + + const isFavorite = !!selectedRecordId && !!foundFavorite; + + if (isFavorite) { + deleteFavorite(foundFavorite.id); + } else if (isDefined(selectedRecord)) { + createFavorite(selectedRecord, objectMetadataItem.nameSingular); + } + callback?.(); + }, + [ + callback, + createFavorite, + deleteFavorite, + favorites, + objectMetadataItem.nameSingular, + selectedRecordIds, + ], + ); + return { handleFavoriteButtonClick }; +}; diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts new file mode 100644 index 000000000000..f2f8f06b1372 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts @@ -0,0 +1,11 @@ +import { PositionType } from '@/action-menu/types/PositionType'; +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const actionMenuDropdownPositionComponentState = + createComponentState({ + key: 'actionMenuDropdownPositionComponentState', + defaultValue: { + x: null, + y: null, + }, + }); diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts new file mode 100644 index 000000000000..8ba3a259da68 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/states/actionMenuEntriesComponentState.ts @@ -0,0 +1,11 @@ +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ActionMenuEntry } from '../types/ActionMenuEntry'; + +export const actionMenuEntriesComponentState = createComponentStateV2< + ActionMenuEntry[] +>({ + key: 'actionMenuEntriesComponentState', + defaultValue: [], + componentInstanceContext: ActionMenuComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts b/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts new file mode 100644 index 000000000000..1e64690c744d --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/states/contexts/ActionMenuComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const ActionMenuComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts new file mode 100644 index 000000000000..fcadbee366f0 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/types/ActionBarHotKeyScope.ts @@ -0,0 +1,3 @@ +export enum ActionBarHotkeyScope { + ActionBar = 'action-bar', +} diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarItemAccent.ts b/packages/twenty-front/src/modules/action-menu/types/ActionBarItemAccent.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarItemAccent.ts rename to packages/twenty-front/src/modules/action-menu/types/ActionBarItemAccent.ts diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts new file mode 100644 index 000000000000..9c0e2df42edf --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuDropdownHotKeyScope.ts @@ -0,0 +1,3 @@ +export enum ActionMenuDropdownHotkeyScope { + ActionMenuDropdown = 'action-menu-dropdown', +} diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts similarity index 90% rename from packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts rename to packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts index 416a41419f62..d363a8fcd252 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuEntry.ts @@ -3,7 +3,7 @@ import { IconComponent } from 'twenty-ui'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; -export type ContextMenuEntry = { +export type ActionMenuEntry = { label: string; Icon: IconComponent; accent?: MenuItemAccent; diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/PositionType.ts b/packages/twenty-front/src/modules/action-menu/types/PositionType.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/context-menu/types/PositionType.ts rename to packages/twenty-front/src/modules/action-menu/types/PositionType.ts diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts index 6e77fc50907d..8660d7617de1 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useOpenEmailThreadRightDrawer.test.ts @@ -1,5 +1,5 @@ -import { act } from 'react-dom/test-utils'; 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'; diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index 24a54e677ac5..f0eca529d762 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -18,6 +18,7 @@ import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui'; import { formatToHumanReadableDate } from '~/utils/date-utils'; import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; +import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension'; const StyledLeftContent = styled.div` align-items: center; @@ -62,7 +63,12 @@ const StyledLinkContainer = styled.div` export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { const theme = useTheme(); const [isEditing, setIsEditing] = useState(false); - const [attachmentName, setAttachmentName] = useState(attachment.name); + + const { name: originalFileName, extension: attachmentFileExtension } = + getFileNameAndExtension(attachment.name); + + const [attachmentFileName, setAttachmentFileName] = + useState(originalFileName); const fieldContext = useMemo( () => ({ recoilScopeId: attachment?.id ?? '' }), @@ -85,16 +91,36 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { setIsEditing(true); }; - const handleOnBlur = () => { + const saveAttachmentName = () => { setIsEditing(false); + + const newFileName = `${attachmentFileName}${attachmentFileExtension}`; + updateOneAttachment({ idToUpdate: attachment.id, - updateOneRecordInput: { name: attachmentName }, + updateOneRecordInput: { name: newFileName }, }); }; - const handleOnChange = (newName: string) => { - setAttachmentName(newName); + const handleOnBlur = () => { + saveAttachmentName(); + }; + + const handleOnChange = (newFileName: string) => { + setAttachmentFileName(newFileName); + }; + + const handleOnKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + saveAttachmentName(); + } + }; + + const handleDownload = () => { + downloadFile( + attachment.fullPath, + `${attachmentFileName}${attachmentFileExtension}`, + ); }; return ( @@ -104,11 +130,11 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { {isEditing ? ( ) : ( @@ -129,9 +155,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { { - downloadFile(attachment.fullPath, attachment.name); - }} + onDownload={handleDownload} onRename={handleRename} /> diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index d6cf7ff02ad3..2b3706b9e1da 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -167,18 +167,12 @@ export const CommandMenu = () => { [closeCommandMenu], ); - const isTwentyOrmEnabled = useIsFeatureEnabled( - 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', - ); - const isWorkspaceMigratedForSearch = useIsFeatureEnabled( 'IS_WORKSPACE_MIGRATED_FOR_SEARCH', ); const isSearchEnabled = - useIsFeatureEnabled('IS_SEARCH_ENABLED') && - isTwentyOrmEnabled && - isWorkspaceMigratedForSearch; + useIsFeatureEnabled('IS_SEARCH_ENABLED') && isWorkspaceMigratedForSearch; const { records: peopleFromFindMany } = useFindManyRecords({ skip: !isCommandMenuOpened || isSearchEnabled, diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx index 03e7e04802f9..c3f381c1bcd5 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountEmailAliases.tsx @@ -1,6 +1,6 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; -import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; +import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { IconRefresh } from 'twenty-ui'; diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx index 45a4ca00b20b..7f74a129b652 100644 --- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx @@ -1,6 +1,6 @@ import { InformationBanner } from '@/information-banner/components/InformationBanner'; -import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum'; import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect'; +import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { IconRefresh } from 'twenty-ui'; diff --git a/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts b/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts index e73031015a44..ea74778ff536 100644 --- a/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts +++ b/packages/twenty-front/src/modules/information-banner/hooks/useAccountToReconnect.ts @@ -1,6 +1,6 @@ import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { currentUserState } from '@/auth/states/currentUserState'; -import { InformationBannerKeys } from '@/information-banner/enums/InformationBannerKeys.enum'; +import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useRecoilValue } from 'recoil'; diff --git a/packages/twenty-front/src/modules/information-banner/enums/InformationBannerKeys.enum.ts b/packages/twenty-front/src/modules/information-banner/types/InformationBannerKeys.ts similarity index 100% rename from packages/twenty-front/src/modules/information-banner/enums/InformationBannerKeys.enum.ts rename to packages/twenty-front/src/modules/information-banner/types/InformationBannerKeys.ts diff --git a/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts b/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts index 282710253650..f3599783cf7e 100644 --- a/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts +++ b/packages/twenty-front/src/modules/object-metadata/constants/SortableFieldMetadataTypes.ts @@ -7,8 +7,6 @@ export const SORTABLE_FIELD_METADATA_TYPES = [ FieldMetadataType.Text, FieldMetadataType.Boolean, FieldMetadataType.Select, - FieldMetadataType.Phone, - FieldMetadataType.Email, FieldMetadataType.Emails, FieldMetadataType.FullName, FieldMetadataType.Rating, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx index 876846f2bd4b..f3d33774b119 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx @@ -1,5 +1,6 @@ import { renderHook } from '@testing-library/react'; +import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -25,4 +26,15 @@ describe('useObjectMetadataItem', () => { expect(objectMetadataItem.id).toBe(opportunityObjectMetadata?.id); }); + + it('should throw an error when invalid object name singular is provided', async () => { + expect(() => + renderHook( + () => useObjectMetadataItem({ objectNameSingular: 'invalid-object' }), + { + wrapper: Wrapper, + }, + ), + ).toThrow(ObjectMetadataItemNotFoundError); + }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts new file mode 100644 index 000000000000..c2fe8825aa68 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts @@ -0,0 +1,44 @@ +import { renderHook } from '@testing-library/react'; + +import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + +describe('useObjectMetadataItemById', () => { + const opportunityObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + + if (!opportunityObjectMetadata) { + throw new Error('Opportunity object metadata not found'); + } + + it('should return correct properties', async () => { + const { result } = renderHook( + () => + useObjectMetadataItemById({ + objectId: opportunityObjectMetadata.id, + }), + { + wrapper: Wrapper, + }, + ); + + const { objectMetadataItem } = result.current; + + expect(objectMetadataItem.id).toBe(opportunityObjectMetadata.id); + }); + + it('should throw an error when invalid ID is provided', async () => { + expect(() => + renderHook(() => useObjectMetadataItemById({ objectId: 'invalid-id' }), { + wrapper: Wrapper, + }), + ).toThrow(ObjectMetadataItemNotFoundError); + }); +}); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectIsRemote.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectIsRemote.ts deleted file mode 100644 index 0f3295dc5c29..000000000000 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectIsRemote.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; - -export const useObjectIsRemote = (objectMetadataItem: ObjectMetadataItem) => { - return objectMetadataItem.isRemote ?? false; -}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts new file mode 100644 index 000000000000..6d9730f2d6db --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts @@ -0,0 +1,25 @@ +import { useRecoilValue } from 'recoil'; + +import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { isDefined } from '~/utils/isDefined'; + +export const useObjectMetadataItemById = ({ + objectId, +}: { + objectId: string; +}) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectId, + ); + + if (!isDefined(objectMetadataItem)) { + throw new ObjectMetadataItemNotFoundError(objectId, objectMetadataItems); + } + + return { + objectMetadataItem, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index dd496e70d3d2..8ac533f76aac 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -31,4 +31,5 @@ export enum CoreObjectNameSingular { Workflow = 'workflow', MessageChannelMessageAssociation = 'messageChannelMessageAssociation', WorkflowVersion = 'workflowVersion', + WorkflowRun = 'workflowRun', } diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts index 7ebeb0afee26..a110acdceba4 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions.ts @@ -26,10 +26,8 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({ FieldMetadataType.DateTime, FieldMetadataType.Date, FieldMetadataType.Text, - FieldMetadataType.Email, FieldMetadataType.Emails, FieldMetadataType.Number, - FieldMetadataType.Link, FieldMetadataType.Links, FieldMetadataType.FullName, FieldMetadataType.Address, @@ -68,8 +66,6 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => { return 'DATE_TIME'; case FieldMetadataType.Date: return 'DATE'; - case FieldMetadataType.Link: - return 'LINK'; case FieldMetadataType.Links: return 'LINKS'; case FieldMetadataType.FullName: @@ -78,12 +74,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => { return 'NUMBER'; case FieldMetadataType.Currency: return 'CURRENCY'; - case FieldMetadataType.Email: - return 'EMAIL'; case FieldMetadataType.Emails: return 'EMAILS'; - case FieldMetadataType.Phone: - return 'PHONE'; case FieldMetadataType.Phones: return 'PHONES'; case FieldMetadataType.Relation: diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isObjectMetadataReadOnly.ts b/packages/twenty-front/src/modules/object-metadata/utils/isObjectMetadataReadOnly.ts new file mode 100644 index 000000000000..c6455e009c9d --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/isObjectMetadataReadOnly.ts @@ -0,0 +1,8 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; + +export const isObjectMetadataReadOnly = ( + objectMetadataItem: Pick, +) => + objectMetadataItem.isRemote || + isWorkflowSubObjectMetadata(objectMetadataItem.nameSingular); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowSubObjectMetadata.ts b/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowSubObjectMetadata.ts new file mode 100644 index 000000000000..1ad6c0cbb290 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/isWorkflowSubObjectMetadata.ts @@ -0,0 +1,7 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; + +export const isWorkflowSubObjectMetadata = ( + objectMetadataNameSingular?: string, +) => + objectMetadataNameSingular === CoreObjectNameSingular.WorkflowVersion || + objectMetadataNameSingular === CoreObjectNameSingular.WorkflowRun; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index d379313a0496..bf29d99ee1f9 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -26,10 +26,8 @@ export const mapFieldMetadataToGraphQLQuery = ({ const fieldIsSimpleValue = [ FieldMetadataType.Uuid, FieldMetadataType.Text, - FieldMetadataType.Phone, FieldMetadataType.DateTime, FieldMetadataType.Date, - FieldMetadataType.Email, FieldMetadataType.Number, FieldMetadataType.Boolean, FieldMetadataType.Rating, @@ -97,14 +95,6 @@ ${mapObjectMetadataToGraphQLQuery({ }`; } - if (fieldType === FieldMetadataType.Link) { - return `${field.name} -{ - label - url -}`; - } - if (fieldType === FieldMetadataType.Links) { return `${field.name} { diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts index 2ab3b25344fa..21db7bcf49f4 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts @@ -131,7 +131,6 @@ export const getRecordNodeFromRecord = ({ }, ]; } - case FieldMetadataType.Link: case FieldMetadataType.Links: case FieldMetadataType.Address: case FieldMetadataType.FullName: diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx index 1f34cdbe1e59..c924bbc7ab75 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/__stories__/MultipleFiltersDropdownButton.stories.tsx @@ -88,7 +88,7 @@ const meta: Meta = { fieldMetadataId: '2', iconName: 'Icon123', label: 'Email', - type: FieldMetadataType.Email, + type: FieldMetadataType.Emails, }, { fieldMetadataId: '3', diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts index 833ebbd3a24f..0624fe937ef7 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts @@ -4,16 +4,13 @@ import { PickLiteral } from '~/types/PickLiteral'; export type FilterableFieldType = PickLiteral< FieldType, | 'TEXT' - | 'PHONE' | 'PHONES' - | 'EMAIL' | 'EMAILS' | 'DATE_TIME' | 'DATE' | 'NUMBER' | 'CURRENCY' | 'FULL_NAME' - | 'LINK' | 'LINKS' | 'RELATION' | 'ADDRESS' diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx index 66ea29aa06cc..023a229edeca 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx @@ -1,7 +1,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; import { getOperandsForFilterDefinition } from '../getOperandsForFilterType'; describe('getOperandsForFilterType', () => { @@ -34,10 +34,8 @@ describe('getOperandsForFilterType', () => { const testCases = [ ['TEXT', [...containsOperands, ...emptyOperands]], - ['EMAIL', [...containsOperands, ...emptyOperands]], ['FULL_NAME', [...containsOperands, ...emptyOperands]], ['ADDRESS', [...containsOperands, ...emptyOperands]], - ['LINK', [...containsOperands, ...emptyOperands]], ['LINKS', [...containsOperands, ...emptyOperands]], ['ACTOR', [...containsOperands, ...emptyOperands]], ['CURRENCY', [...numberOperands, ...emptyOperands]], diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index ba263c173c4c..688aa02b6c79 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -14,12 +14,9 @@ export const getOperandsForFilterDefinition = ( switch (filterDefinition.type) { case 'TEXT': - case 'EMAIL': case 'EMAILS': case 'FULL_NAME': case 'ADDRESS': - case 'PHONE': - case 'LINK': case 'LINKS': case 'ARRAY': case 'PHONES': diff --git a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx deleted file mode 100644 index b7cce8057efb..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { useCallback, useMemo, useState } from 'react'; -import { useRecoilCallback, useSetRecoilState } from 'recoil'; -import { IconFileExport, IconHeart, IconHeartOff, IconTrash } from 'twenty-ui'; - -import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; -import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; -import { - displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; -import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; -import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry'; -import { isDefined } from '~/utils/isDefined'; - -type useRecordActionBarProps = { - objectMetadataItem: ObjectMetadataItem; - selectedRecordIds: string[]; - callback?: () => void; - totalNumberOfRecordsSelected?: number; -}; - -export const useRecordActionBar = ({ - objectMetadataItem, - selectedRecordIds, - callback, - totalNumberOfRecordsSelected, -}: useRecordActionBarProps) => { - const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); - const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); - const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = - useState(false); - - const { createFavorite, favorites, deleteFavorite } = useFavorites(); - - const handleFavoriteButtonClick = useRecoilCallback( - ({ snapshot }) => - () => { - if (selectedRecordIds.length > 1) { - return; - } - - const selectedRecordId = selectedRecordIds[0]; - const selectedRecord = snapshot - .getLoadable(recordStoreFamilyState(selectedRecordId)) - .getValue(); - - const foundFavorite = favorites?.find( - (favorite) => favorite.recordId === selectedRecordId, - ); - - const isFavorite = !!selectedRecordId && !!foundFavorite; - - if (isFavorite) { - deleteFavorite(foundFavorite.id); - } else if (isDefined(selectedRecord)) { - createFavorite(selectedRecord, objectMetadataItem.nameSingular); - } - callback?.(); - }, - [ - callback, - createFavorite, - deleteFavorite, - favorites, - objectMetadataItem.nameSingular, - selectedRecordIds, - ], - ); - - const baseTableDataParams = { - delayMs: 100, - objectNameSingular: objectMetadataItem.nameSingular, - recordIndexId: objectMetadataItem.namePlural, - }; - - const { deleteTableData } = useDeleteTableData(baseTableDataParams); - - const handleDeleteClick = useCallback(() => { - deleteTableData(selectedRecordIds); - }, [deleteTableData, selectedRecordIds]); - - const { progress, download } = useExportTableData({ - ...baseTableDataParams, - filename: `${objectMetadataItem.nameSingular}.csv`, - }); - - const isRemoteObject = objectMetadataItem.isRemote; - - const numberOfSelectedRecords = - totalNumberOfRecordsSelected ?? selectedRecordIds.length; - const canDelete = - !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; - - const menuActions: ContextMenuEntry[] = useMemo( - () => - [ - { - label: displayedExportProgress(progress), - Icon: IconFileExport, - accent: 'default', - onClick: () => download(), - } satisfies ContextMenuEntry, - canDelete - ? ({ - label: 'Delete', - Icon: IconTrash, - accent: 'danger', - onClick: () => { - setIsDeleteRecordsModalOpen(true); - }, - ConfirmationModal: ( - handleDeleteClick()} - deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' - }`} - /> - ), - } satisfies ContextMenuEntry) - : undefined, - ].filter(isDefined), - [ - download, - progress, - canDelete, - handleDeleteClick, - isDeleteRecordsModalOpen, - numberOfSelectedRecords, - ], - ); - - const hasOnlyOneRecordSelected = selectedRecordIds.length === 1; - - const isFavorite = - isNonEmptyString(selectedRecordIds[0]) && - !!favorites?.find((favorite) => favorite.recordId === selectedRecordIds[0]); - - return { - setContextMenuEntries: useCallback(() => { - setContextMenuEntries([ - ...menuActions, - ...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected - ? [ - { - label: 'Remove from favorites', - Icon: IconHeartOff, - onClick: handleFavoriteButtonClick, - }, - ] - : []), - ...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected - ? [ - { - label: 'Add to favorites', - Icon: IconHeart, - onClick: handleFavoriteButtonClick, - }, - ] - : []), - ]); - }, [ - menuActions, - handleFavoriteButtonClick, - hasOnlyOneRecordSelected, - isFavorite, - isRemoteObject, - setContextMenuEntries, - ]), - - setActionBarEntries: useCallback(() => { - setActionBarEntriesState([ - /* - { - label: 'Actions', - Icon: IconClick, - subActions: - - /* [ - { - label: 'Enrich', - Icon: IconPuzzle, - onClick: handleExecuteQuickActionOnClick, - }, - { - label: 'Send to mailjet', - Icon: IconMail, - }, - ], - */ - ...menuActions, - ]); - }, [menuActions, setActionBarEntriesState]), - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx deleted file mode 100644 index 584cbeab3b5f..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; - -type RecordBoardActionBarProps = { - recordBoardId: string; -}; - -export const RecordBoardActionBar = ({ - recordBoardId, -}: RecordBoardActionBarProps) => { - const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); - - const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - - if (!selectedRecordIds.length) { - return null; - } - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 2c3b0daf75cc..ff408eb407de 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -69,7 +69,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { useListenClickOutsideByClassName({ classNames: ['record-board-card'], - excludeClassNames: ['action-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'context-menu'], callback: resetRecordSelection, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx deleted file mode 100644 index fed35a6506d3..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu'; - -type RecordBoardContextMenuProps = { - recordBoardId: string; -}; - -export const RecordBoardContextMenu = ({ - recordBoardId, -}: RecordBoardContextMenuProps) => { - const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); - - const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - - if (!selectedRecordIds.length) { - return null; - } - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index a14c85119d51..8b2d11837ae1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -1,17 +1,23 @@ -import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; +import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; -export const useRecordBoardSelection = (recordBoardId?: string) => { - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); +export const useRecordBoardSelection = (recordBoardId: string) => { const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = useRecordBoardStates(recordBoardId); const resetRecordSelection = useRecoilCallback( ({ snapshot, set }) => () => { - setContextMenuOpenState(false); + const isActionMenuDropdownOpenState = extractComponentState( + isDropdownOpenComponentState, + `action-menu-dropdown-${recordBoardId}`, + ); + + set(isActionMenuDropdownOpenState, false); + const recordIds = snapshot .getLoadable(selectedRecordIdsSelector()) .getValue(); @@ -21,9 +27,9 @@ export const useRecordBoardSelection = (recordBoardId?: string) => { } }, [ + recordBoardId, selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState, - setContextMenuOpenState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index d336467e8db3..a2ea5b03def3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -1,6 +1,9 @@ +import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; +import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; +import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; import { FieldContext, RecordUpdateHook, @@ -17,10 +20,10 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; -import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState'; -import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState'; import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; +import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import styled from '@emotion/styled'; import { ReactNode, useContext, useState } from 'react'; import { useInView } from 'react-intersection-observer'; @@ -175,17 +178,27 @@ export const RecordBoardCard = ({ const record = useRecoilValue(recordStoreFamilyState(recordId)); - const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); + const recordBoardId = useAvailableScopeIdOrThrow( + RecordBoardScopeInternalContext, + ); + + const setActionMenuDropdownPosition = useSetRecoilState( + extractComponentState( + actionMenuDropdownPositionComponentState, + `action-menu-dropdown-${recordBoardId}`, + ), + ); + + const { openActionMenuDropdown } = useActionMenu(recordBoardId); - const handleContextMenu = (event: React.MouseEvent) => { + const handleActionMenuDropdown = (event: React.MouseEvent) => { event.preventDefault(); setIsCurrentCardSelected(true); - setContextMenuPosition({ + setActionMenuDropdownPosition({ x: event.clientX, y: event.clientY, }); - setContextMenuOpenState(true); + openActionMenuDropdown(); }; const PreventSelectOnClickContainer = ({ @@ -235,7 +248,7 @@ export const RecordBoardCard = ({ ); return ( - + {!isCreating && } = { }, }; -export const linkFieldDefinition: FieldDefinition = { - fieldMetadataId, - label: 'LinkedIn URL', - iconName: 'url', - type: FieldMetadataType.Link, - defaultValue: { url: '', label: '' }, - metadata: { - fieldName: 'linkedInURL', - placeHolder: 'https://linkedin.com/user', - }, -}; - const phonesFieldMetadataItem = mockedPersonObjectMetadataItem.fields?.find( ({ name }) => name === 'phones', ); diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx index 781ac98a650f..41740a1a4191 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldDisplay.tsx @@ -13,7 +13,6 @@ import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-type import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; -import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; @@ -27,13 +26,10 @@ import { ChipFieldDisplay } from '../meta-types/display/components/ChipFieldDisp import { CurrencyFieldDisplay } from '../meta-types/display/components/CurrencyFieldDisplay'; import { DateFieldDisplay } from '../meta-types/display/components/DateFieldDisplay'; import { DateTimeFieldDisplay } from '../meta-types/display/components/DateTimeFieldDisplay'; -import { EmailFieldDisplay } from '../meta-types/display/components/EmailFieldDisplay'; import { FullNameFieldDisplay } from '../meta-types/display/components/FullNameFieldDisplay'; import { JsonFieldDisplay } from '../meta-types/display/components/JsonFieldDisplay'; -import { LinkFieldDisplay } from '../meta-types/display/components/LinkFieldDisplay'; import { MultiSelectFieldDisplay } from '../meta-types/display/components/MultiSelectFieldDisplay'; import { NumberFieldDisplay } from '../meta-types/display/components/NumberFieldDisplay'; -import { PhoneFieldDisplay } from '../meta-types/display/components/PhoneFieldDisplay'; import { RelationToOneFieldDisplay } from '../meta-types/display/components/RelationToOneFieldDisplay'; import { SelectFieldDisplay } from '../meta-types/display/components/SelectFieldDisplay'; import { TextFieldDisplay } from '../meta-types/display/components/TextFieldDisplay'; @@ -42,12 +38,9 @@ import { isFieldAddress } from '../types/guards/isFieldAddress'; import { isFieldCurrency } from '../types/guards/isFieldCurrency'; import { isFieldDate } from '../types/guards/isFieldDate'; import { isFieldDateTime } from '../types/guards/isFieldDateTime'; -import { isFieldEmail } from '../types/guards/isFieldEmail'; import { isFieldFullName } from '../types/guards/isFieldFullName'; -import { isFieldLink } from '../types/guards/isFieldLink'; import { isFieldMultiSelect } from '../types/guards/isFieldMultiSelect'; import { isFieldNumber } from '../types/guards/isFieldNumber'; -import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldRawJson } from '../types/guards/isFieldRawJson'; import { isFieldSelect } from '../types/guards/isFieldSelect'; import { isFieldText } from '../types/guards/isFieldText'; @@ -67,23 +60,16 @@ export const FieldDisplay = () => { ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - ) : isFieldPhone(fieldDefinition) || - isFieldDisplayedAsPhone(fieldDefinition) ? ( - ) : isFieldText(fieldDefinition) ? ( ) : isFieldUuid(fieldDefinition) ? ( - ) : isFieldEmail(fieldDefinition) ? ( - ) : isFieldDateTime(fieldDefinition) ? ( ) : isFieldDate(fieldDefinition) ? ( ) : isFieldNumber(fieldDefinition) ? ( - ) : isFieldLink(fieldDefinition) ? ( - ) : isFieldLinks(fieldDefinition) ? ( ) : isFieldCurrency(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx index 144a24a8b8b0..1f36c34548d8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FieldInput.tsx @@ -11,44 +11,37 @@ import { RawJsonFieldInput } from '@/object-record/record-field/meta-types/input import { RelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput'; import { SelectFieldInput } from '@/object-record/record-field/meta-types/input/components/SelectFieldInput'; import { RecordFieldInputScope } from '@/object-record/record-field/scopes/RecordFieldInputScope'; -import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; -import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone'; -import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; -import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; -import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; -import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; -import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRelationFromManyObjects } from '@/object-record/record-field/types/guards/isFieldRelationFromManyObjects'; import { isFieldRelationToOneObject } from '@/object-record/record-field/types/guards/isFieldRelationToOneObject'; -import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ArrayFieldInput } from '@/object-record/record-field/meta-types/input/components/ArrayFieldInput'; import { RichTextFieldInput } from '@/object-record/record-field/meta-types/input/components/RichTextFieldInput'; +import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; +import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; +import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; +import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; +import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; +import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; +import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; +import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; +import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; +import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; +import { isFieldRating } from '@/object-record/record-field/types/guards/isFieldRating'; +import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; +import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { FieldContext } from '../contexts/FieldContext'; import { BooleanFieldInput } from '../meta-types/input/components/BooleanFieldInput'; import { CurrencyFieldInput } from '../meta-types/input/components/CurrencyFieldInput'; import { DateTimeFieldInput } from '../meta-types/input/components/DateTimeFieldInput'; -import { EmailFieldInput } from '../meta-types/input/components/EmailFieldInput'; -import { LinkFieldInput } from '../meta-types/input/components/LinkFieldInput'; import { NumberFieldInput } from '../meta-types/input/components/NumberFieldInput'; -import { PhoneFieldInput } from '../meta-types/input/components/PhoneFieldInput'; import { RatingFieldInput } from '../meta-types/input/components/RatingFieldInput'; import { RelationToOneFieldInput } from '../meta-types/input/components/RelationToOneFieldInput'; import { TextFieldInput } from '../meta-types/input/components/TextFieldInput'; import { FieldInputEvent } from '../types/FieldInputEvent'; -import { isFieldAddress } from '../types/guards/isFieldAddress'; -import { isFieldBoolean } from '../types/guards/isFieldBoolean'; -import { isFieldCurrency } from '../types/guards/isFieldCurrency'; -import { isFieldDateTime } from '../types/guards/isFieldDateTime'; -import { isFieldEmail } from '../types/guards/isFieldEmail'; -import { isFieldLink } from '../types/guards/isFieldLink'; -import { isFieldNumber } from '../types/guards/isFieldNumber'; -import { isFieldPhone } from '../types/guards/isFieldPhone'; -import { isFieldRating } from '../types/guards/isFieldRating'; import { isFieldText } from '../types/guards/isFieldText'; type FieldInputProps = { @@ -84,15 +77,6 @@ export const FieldInput = ({ ) : isFieldRelationFromManyObjects(fieldDefinition) ? ( - ) : isFieldPhone(fieldDefinition) || - isFieldDisplayedAsPhone(fieldDefinition) ? ( - ) : isFieldPhones(fieldDefinition) ? ( ) : isFieldText(fieldDefinition) ? ( @@ -103,14 +87,6 @@ export const FieldInput = ({ onTab={onTab} onShiftTab={onShiftTab} /> - ) : isFieldEmail(fieldDefinition) ? ( - ) : isFieldEmails(fieldDefinition) ? ( ) : isFieldFullName(fieldDefinition) ? ( @@ -145,14 +121,6 @@ export const FieldInput = ({ onTab={onTab} onShiftTab={onShiftTab} /> - ) : isFieldLink(fieldDefinition) ? ( - ) : isFieldLinks(fieldDefinition) ? ( ) : isFieldCurrency(fieldDefinition) ? ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldReadOnly.ts index e4e4970c0b0d..48bb034244ea 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldReadOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldReadOnly.ts @@ -3,14 +3,16 @@ import { useContext } from 'react'; import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; import { FieldContext } from '../contexts/FieldContext'; +import { isFieldMetadataReadOnly } from '../utils/isFieldMetadataReadOnly'; export const useIsFieldReadOnly = () => { const { fieldDefinition } = useContext(FieldContext); + const { metadata } = fieldDefinition; + return ( - fieldDefinition.metadata.fieldName === 'noteTargets' || - fieldDefinition.metadata.fieldName === 'taskTargets' || isFieldActor(fieldDefinition) || - isFieldRichText(fieldDefinition) + isFieldRichText(fieldDefinition) || + isFieldMetadataReadOnly(metadata) ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts index 0aa155a868c9..d12e1b73bbb8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/usePersistField.ts @@ -35,14 +35,8 @@ import { isFieldCurrency } from '../types/guards/isFieldCurrency'; import { isFieldCurrencyValue } from '../types/guards/isFieldCurrencyValue'; import { isFieldDateTime } from '../types/guards/isFieldDateTime'; import { isFieldDateTimeValue } from '../types/guards/isFieldDateTimeValue'; -import { isFieldEmail } from '../types/guards/isFieldEmail'; -import { isFieldEmailValue } from '../types/guards/isFieldEmailValue'; -import { isFieldLink } from '../types/guards/isFieldLink'; -import { isFieldLinkValue } from '../types/guards/isFieldLinkValue'; import { isFieldNumber } from '../types/guards/isFieldNumber'; import { isFieldNumberValue } from '../types/guards/isFieldNumberValue'; -import { isFieldPhone } from '../types/guards/isFieldPhone'; -import { isFieldPhoneValue } from '../types/guards/isFieldPhoneValue'; import { isFieldRating } from '../types/guards/isFieldRating'; import { isFieldRatingValue } from '../types/guards/isFieldRatingValue'; import { isFieldText } from '../types/guards/isFieldText'; @@ -68,9 +62,6 @@ export const usePersistField = () => { const fieldIsText = isFieldText(fieldDefinition) && isFieldTextValue(valueToPersist); - const fieldIsEmail = - isFieldEmail(fieldDefinition) && isFieldEmailValue(valueToPersist); - const fieldIsEmails = isFieldEmails(fieldDefinition) && isFieldEmailsValue(valueToPersist); @@ -81,9 +72,6 @@ export const usePersistField = () => { const fieldIsDate = isFieldDate(fieldDefinition) && isFieldDateValue(valueToPersist); - const fieldIsLink = - isFieldLink(fieldDefinition) && isFieldLinkValue(valueToPersist); - const fieldIsLinks = isFieldLinks(fieldDefinition) && isFieldLinksValue(valueToPersist); @@ -105,9 +93,6 @@ export const usePersistField = () => { isFieldFullName(fieldDefinition) && isFieldFullNameValue(valueToPersist); - const fieldIsPhone = - isFieldPhone(fieldDefinition) && isFieldPhoneValue(valueToPersist); - const fieldIsPhones = isFieldPhones(fieldDefinition) && isFieldPhonesValue(valueToPersist); @@ -133,15 +118,12 @@ export const usePersistField = () => { fieldIsRelationToOneObject || fieldIsText || fieldIsBoolean || - fieldIsEmail || fieldIsEmails || fieldIsRating || fieldIsNumber || fieldIsDateTime || fieldIsDate || - fieldIsPhone || fieldIsPhones || - fieldIsLink || fieldIsLinks || fieldIsCurrency || fieldIsFullName || diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailFieldDisplay.tsx deleted file mode 100644 index 60a7caa6e32a..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/EmailFieldDisplay.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useEmailFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useEmailFieldDisplay'; -import { EmailDisplay } from '@/ui/field/display/components/EmailDisplay'; - -export const EmailFieldDisplay = () => { - const { fieldValue } = useEmailFieldDisplay(); - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/LinkFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/LinkFieldDisplay.tsx deleted file mode 100644 index bb1989852f4c..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/LinkFieldDisplay.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useLinkFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useLinkFieldDisplay'; -import { LinkDisplay } from '@/ui/field/display/components/LinkDisplay'; - -export const LinkFieldDisplay = () => { - const { fieldValue } = useLinkFieldDisplay(); - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhoneFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhoneFieldDisplay.tsx deleted file mode 100644 index acfd141a3240..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/PhoneFieldDisplay.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { usePhoneFieldDisplay } from '@/object-record/record-field/meta-types/hooks/usePhoneFieldDisplay'; -import { PhoneDisplay } from '@/ui/field/display/components/PhoneDisplay'; - -export const PhoneFieldDisplay = () => { - const { fieldValue } = usePhoneFieldDisplay(); - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailField.ts deleted file mode 100644 index a12e4a54f609..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailField.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { FieldEmailValue } from '@/object-record/record-field/types/FieldMetadata'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldContext } from '../../contexts/FieldContext'; -import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; -import { isFieldEmail } from '../../types/guards/isFieldEmail'; - -export const useEmailField = () => { - const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - - assertFieldMetadata(FieldMetadataType.Email, isFieldEmail, fieldDefinition); - - const fieldName = fieldDefinition.metadata.fieldName; - - const [fieldValue, setFieldValue] = useRecoilState( - recordStoreFamilySelector({ - recordId, - fieldName: fieldName, - }), - ); - - const { setDraftValue, getDraftValueSelector } = - useRecordFieldInput(`${recordId}-${fieldName}`); - - const draftValue = useRecoilValue(getDraftValueSelector()); - - return { - fieldDefinition, - draftValue, - setDraftValue, - fieldValue, - setFieldValue, - hotkeyScope, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailFieldDisplay.ts deleted file mode 100644 index cb7684a4f493..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useEmailFieldDisplay.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useContext } from 'react'; - -import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; - -import { FieldContext } from '../../contexts/FieldContext'; - -export const useEmailFieldDisplay = () => { - const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - - const fieldName = fieldDefinition.metadata.fieldName; - - const fieldValue = useRecordFieldValue(recordId, fieldName); - - return { - fieldDefinition, - fieldValue, - hotkeyScope, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkField.ts deleted file mode 100644 index f1b5ed7e6a30..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkField.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useContext } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldContext } from '../../contexts/FieldContext'; -import { usePersistField } from '../../hooks/usePersistField'; -import { FieldLinkValue } from '../../types/FieldMetadata'; -import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; -import { isFieldLink } from '../../types/guards/isFieldLink'; -import { isFieldLinkValue } from '../../types/guards/isFieldLinkValue'; - -export const useLinkField = () => { - const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - - assertFieldMetadata(FieldMetadataType.Link, isFieldLink, fieldDefinition); - - const fieldName = fieldDefinition.metadata.fieldName; - - const [fieldValue, setFieldValue] = useRecoilState( - recordStoreFamilySelector({ - recordId, - fieldName: fieldName, - }), - ); - - const { setDraftValue, getDraftValueSelector } = - useRecordFieldInput(`${recordId}-${fieldName}`); - - const draftValue = useRecoilValue(getDraftValueSelector()); - - const persistField = usePersistField(); - - const persistLinkField = (newValue: FieldLinkValue) => { - if (!isFieldLinkValue(newValue)) { - return; - } - - persistField(newValue); - }; - - return { - fieldDefinition, - fieldValue, - draftValue, - setDraftValue, - setFieldValue, - hotkeyScope, - persistLinkField, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkFieldDisplay.ts deleted file mode 100644 index 570857ffae10..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useLinkFieldDisplay.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useContext } from 'react'; - -import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; - -import { FieldContext } from '../../contexts/FieldContext'; -import { FieldLinkValue } from '../../types/FieldMetadata'; - -export const useLinkFieldDisplay = () => { - const { recordId, fieldDefinition } = useContext(FieldContext); - - const fieldName = fieldDefinition.metadata.fieldName; - const fieldValue = useRecordFieldValue( - recordId, - fieldName, - ); - - return { - fieldDefinition, - fieldValue, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneField.ts deleted file mode 100644 index 2e20254c82b7..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneField.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { useContext } from 'react'; -import { isPossiblePhoneNumber } from 'libphonenumber-js'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecordFieldInput'; -import { FieldPhoneValue } from '@/object-record/record-field/types/FieldMetadata'; -import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone'; -import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldContext } from '../../contexts/FieldContext'; -import { usePersistField } from '../../hooks/usePersistField'; -import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; -import { isFieldPhone } from '../../types/guards/isFieldPhone'; - -export const usePhoneField = () => { - const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - - try { - // TODO: temporary - remove when 'Phone' field in 'Person' object - // is migrated to use FieldMetadataType.Phone as type. - assertFieldMetadata( - FieldMetadataType.Text, - isFieldDisplayedAsPhone, - fieldDefinition, - ); - } catch { - assertFieldMetadata(FieldMetadataType.Phone, isFieldPhone, fieldDefinition); - } - - const fieldName = fieldDefinition.metadata.fieldName; - - const [fieldValue, setFieldValue] = useRecoilState( - recordStoreFamilySelector({ - recordId, - fieldName: fieldName, - }), - ); - - const persistField = usePersistField(); - - const persistPhoneField = (newPhoneValue: string) => { - if (!isPossiblePhoneNumber(newPhoneValue) && newPhoneValue !== '') return; - - persistField(newPhoneValue); - }; - const { setDraftValue, getDraftValueSelector } = - useRecordFieldInput(`${recordId}-${fieldName}`); - - const draftValue = useRecoilValue(getDraftValueSelector()); - - return { - fieldDefinition, - fieldValue, - setFieldValue, - draftValue, - setDraftValue, - hotkeyScope, - persistPhoneField, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneFieldDisplay.ts deleted file mode 100644 index 3ba7d4f1b25c..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/usePhoneFieldDisplay.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useContext } from 'react'; - -import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; - -import { FieldContext } from '../../contexts/FieldContext'; - -export const usePhoneFieldDisplay = () => { - const { recordId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - - const fieldName = fieldDefinition.metadata.fieldName; - - const fieldValue = useRecordFieldValue(recordId, fieldName); - - return { - fieldDefinition, - fieldValue, - hotkeyScope, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailFieldInput.tsx deleted file mode 100644 index 29ed4b2f15c6..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailFieldInput.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { TextInput } from '@/ui/field/input/components/TextInput'; - -import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; -import { usePersistField } from '../../../hooks/usePersistField'; -import { useEmailField } from '../../hooks/useEmailField'; - -import { FieldInputEvent } from './DateTimeFieldInput'; - -export type EmailFieldInputProps = { - onClickOutside?: FieldInputEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const EmailFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: EmailFieldInputProps) => { - const { fieldDefinition, draftValue, setDraftValue, hotkeyScope } = - useEmailField(); - - const persistField = usePersistField(); - - const handleEnter = (newText: string) => { - onEnter?.(() => persistField(newText)); - }; - - const handleEscape = (newText: string) => { - onEscape?.(() => persistField(newText)); - }; - - const handleClickOutside = ( - event: MouseEvent | TouchEvent, - newText: string, - ) => { - onClickOutside?.(() => persistField(newText)); - }; - - const handleTab = (newText: string) => { - onTab?.(() => persistField(newText)); - }; - - const handleShiftTab = (newText: string) => { - onShiftTab?.(() => persistField(newText)); - }; - - const handleChange = (newText: string) => { - setDraftValue(newText); - }; - - return ( - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinkFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinkFieldInput.tsx deleted file mode 100644 index 758c95fb11a3..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinkFieldInput.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { TextInput } from '@/ui/field/input/components/TextInput'; - -import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; -import { useLinkField } from '../../hooks/useLinkField'; - -import { FieldInputEvent } from './DateTimeFieldInput'; - -type LinkFieldInputProps = { - onClickOutside?: FieldInputEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const LinkFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: LinkFieldInputProps) => { - const { draftValue, setDraftValue, hotkeyScope, persistLinkField } = - useLinkField(); - - const handleEnter = (newURL: string) => { - onEnter?.(() => - persistLinkField({ - url: newURL, - label: '', - }), - ); - }; - - const handleEscape = (newURL: string) => { - onEscape?.(() => - persistLinkField({ - url: newURL, - label: '', - }), - ); - }; - - const handleClickOutside = ( - event: MouseEvent | TouchEvent, - newURL: string, - ) => { - onClickOutside?.(() => - persistLinkField({ - url: newURL, - label: '', - }), - ); - }; - - const handleTab = (newURL: string) => { - onTab?.(() => - persistLinkField({ - url: newURL, - label: '', - }), - ); - }; - - const handleShiftTab = (newURL: string) => { - onShiftTab?.(() => - persistLinkField({ - url: newURL, - label: '', - }), - ); - }; - - const handleChange = (newURL: string) => { - setDraftValue({ - url: newURL, - label: '', - }); - }; - - return ( - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhoneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhoneFieldInput.tsx deleted file mode 100644 index 179455da6a0e..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhoneFieldInput.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { PhoneInput } from '@/ui/field/input/components/PhoneInput'; - -import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; -import { usePhoneField } from '../../hooks/usePhoneField'; - -import { FieldInputEvent } from './DateTimeFieldInput'; - -export type PhoneFieldInputProps = { - onClickOutside?: FieldInputEvent; - onEnter?: FieldInputEvent; - onEscape?: FieldInputEvent; - onTab?: FieldInputEvent; - onShiftTab?: FieldInputEvent; -}; - -export const PhoneFieldInput = ({ - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: PhoneFieldInputProps) => { - const { - fieldDefinition, - draftValue, - setDraftValue, - hotkeyScope, - persistPhoneField, - } = usePhoneField(); - - const handleEnter = (newText: string) => { - onEnter?.(() => persistPhoneField(newText)); - }; - - const handleEscape = (newText: string) => { - onEscape?.(() => persistPhoneField(newText)); - }; - - const handleClickOutside = ( - event: MouseEvent | TouchEvent, - newText: string, - ) => { - onClickOutside?.(() => persistPhoneField(newText)); - }; - - const handleTab = (newText: string) => { - onTab?.(() => persistPhoneField(newText)); - }; - - const handleShiftTab = (newText: string) => { - onShiftTab?.(() => persistPhoneField(newText)); - }; - - const handleChange = (newText: string) => { - setDraftValue(newText); - }; - - return ( - - - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx deleted file mode 100644 index b8154f0545de..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useEffect } from 'react'; -import { Decorator, Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; - -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { FieldMetadataType } from '~/generated/graphql'; -import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; - -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; -import { useEmailField } from '../../../hooks/useEmailField'; -import { EmailFieldInput, EmailFieldInputProps } from '../EmailFieldInput'; - -const EmailFieldValueSetterEffect = ({ value }: { value: string }) => { - const { setFieldValue } = useEmailField(); - - useEffect(() => { - setFieldValue(value); - }, [setFieldValue, value]); - - return <>; -}; - -type EmailFieldInputWithContextProps = EmailFieldInputProps & { - value: string; - recordId?: string; -}; - -const EmailFieldInputWithContext = ({ - recordId, - value, - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: EmailFieldInputWithContextProps) => { - const setHotKeyScope = useSetHotkeyScope(); - - useEffect(() => { - setHotKeyScope('hotkey-scope'); - }, [setHotKeyScope]); - - return ( -
- - - - -
-
- ); -}; - -const enterJestFn = fn(); -const escapeJestfn = fn(); -const clickOutsideJestFn = fn(); -const tabJestFn = fn(); -const shiftTabJestFn = fn(); - -const clearMocksDecorator: Decorator = (Story, context) => { - if (context.parameters.clearMocks === true) { - enterJestFn.mockClear(); - escapeJestfn.mockClear(); - clickOutsideJestFn.mockClear(); - tabJestFn.mockClear(); - shiftTabJestFn.mockClear(); - } - return ; -}; - -const meta: Meta = { - title: 'UI/Data/Field/Input/EmailFieldInput', - component: EmailFieldInputWithContext, - args: { - value: 'username@email.com', - onEnter: enterJestFn, - onEscape: escapeJestfn, - onClickOutside: clickOutsideJestFn, - onTab: tabJestFn, - onShiftTab: shiftTabJestFn, - }, - argTypes: { - onEnter: { control: false }, - onEscape: { control: false }, - onClickOutside: { control: false }, - onTab: { control: false }, - onShiftTab: { control: false }, - }, - decorators: [clearMocksDecorator, SnackBarDecorator], - parameters: { - clearMocks: true, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = {}; - -export const Enter: Story = { - play: async () => { - expect(enterJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{enter}'); - expect(enterJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const Escape: Story = { - play: async () => { - expect(escapeJestfn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{esc}'); - expect(escapeJestfn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const ClickOutside: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); - - const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); - - await waitFor(() => { - userEvent.click(emptyDiv); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const Tab: Story = { - play: async () => { - expect(tabJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{tab}'); - expect(tabJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const ShiftTab: Story = { - play: async () => { - expect(shiftTabJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{shift>}{tab}'); - expect(shiftTabJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx deleted file mode 100644 index 6300d789bdf6..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { useEffect } from 'react'; -import { Decorator, Meta, StoryObj } from '@storybook/react'; -import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; - -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { FieldMetadataType } from '~/generated/graphql'; -import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; - -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; -import { usePhoneField } from '../../../hooks/usePhoneField'; -import { PhoneFieldInput, PhoneFieldInputProps } from '../PhoneFieldInput'; - -const PhoneFieldValueSetterEffect = ({ value }: { value: string }) => { - const { setFieldValue } = usePhoneField(); - - useEffect(() => { - setFieldValue(value); - }, [setFieldValue, value]); - - return <>; -}; - -type PhoneFieldInputWithContextProps = PhoneFieldInputProps & { - value: string; - recordId?: string; -}; - -const PhoneFieldInputWithContext = ({ - recordId, - value, - onEnter, - onEscape, - onClickOutside, - onTab, - onShiftTab, -}: PhoneFieldInputWithContextProps) => { - const setHotKeyScope = useSetHotkeyScope(); - - useEffect(() => { - setHotKeyScope('hotkey-scope'); - }, [setHotKeyScope]); - - return ( -
- - - - -
-
- ); -}; - -const enterJestFn = fn(); -const escapeJestfn = fn(); -const clickOutsideJestFn = fn(); -const tabJestFn = fn(); -const shiftTabJestFn = fn(); - -const clearMocksDecorator: Decorator = (Story, context) => { - if (context.parameters.clearMocks === true) { - enterJestFn.mockClear(); - escapeJestfn.mockClear(); - clickOutsideJestFn.mockClear(); - tabJestFn.mockClear(); - shiftTabJestFn.mockClear(); - } - return ; -}; - -const meta: Meta = { - title: 'UI/Data/Field/Input/PhoneFieldInput', - component: PhoneFieldInputWithContext, - args: { - value: '+1-12-123-456', - isPositive: true, - onEnter: enterJestFn, - onEscape: escapeJestfn, - onClickOutside: clickOutsideJestFn, - onTab: tabJestFn, - onShiftTab: shiftTabJestFn, - }, - argTypes: { - onEnter: { control: false }, - onEscape: { control: false }, - onClickOutside: { control: false }, - onTab: { control: false }, - onShiftTab: { control: false }, - }, - decorators: [clearMocksDecorator, SnackBarDecorator], - parameters: { - clearMocks: true, - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = {}; - -export const Enter: Story = { - play: async () => { - expect(enterJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{enter}'); - expect(enterJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const Escape: Story = { - play: async () => { - expect(escapeJestfn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{esc}'); - expect(escapeJestfn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const ClickOutside: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - expect(clickOutsideJestFn).toHaveBeenCalledTimes(0); - - const emptyDiv = canvas.getByTestId('data-field-input-click-outside-div'); - - await waitFor(() => { - userEvent.click(emptyDiv); - expect(clickOutsideJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const Tab: Story = { - play: async () => { - expect(tabJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{tab}'); - expect(tabJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; - -export const ShiftTab: Story = { - play: async () => { - expect(shiftTabJestFn).toHaveBeenCalledTimes(0); - - await waitFor(() => { - userEvent.keyboard('{shift>}{tab}'); - expect(shiftTabJestFn).toHaveBeenCalledTimes(1); - }); - }, -}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts index 9ab99492f651..6bd2ee66810b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldInputDraftValue.ts @@ -6,15 +6,12 @@ import { FieldCurrencyValue, FieldDateTimeValue, FieldEmailsValue, - FieldEmailValue, FieldFullNameValue, FieldJsonValue, FieldLinksValue, - FieldLinkValue, FieldMultiSelectValue, FieldNumberValue, FieldPhonesValue, - FieldPhoneValue, FieldRatingValue, FieldRelationFromManyValue, FieldRelationToOneValue, @@ -27,13 +24,11 @@ import { export type FieldTextDraftValue = string; export type FieldNumberDraftValue = number; export type FieldDateTimeDraftValue = string; -export type FieldPhoneDraftValue = string; export type FieldPhonesDraftValue = { primaryPhoneNumber: string; primaryPhoneCountryCode: string; additionalPhones?: PhoneRecord[] | null; }; -export type FieldEmailDraftValue = string; export type FieldEmailsDraftValue = { primaryEmail: string; additionalEmails: string[] | null; @@ -42,7 +37,6 @@ export type FieldSelectDraftValue = string; export type FieldMultiSelectDraftValue = string[]; export type FieldRelationDraftValue = string; export type FieldRelationManyDraftValue = string[]; -export type FieldLinkDraftValue = { url: string; label: string }; export type FieldLinksDraftValue = { primaryLinkLabel: string; primaryLinkUrl: string; @@ -80,36 +74,30 @@ export type FieldInputDraftValue = FieldValue extends FieldTextValue ? FieldNumberDraftValue : FieldValue extends FieldBooleanValue ? FieldBooleanValue - : FieldValue extends FieldPhoneValue - ? FieldPhoneDraftValue - : FieldValue extends FieldPhonesValue - ? FieldPhonesDraftValue - : FieldValue extends FieldEmailValue - ? FieldEmailDraftValue - : FieldValue extends FieldEmailsValue - ? FieldEmailsDraftValue - : FieldValue extends FieldLinkValue - ? FieldLinkDraftValue - : FieldValue extends FieldLinksValue - ? FieldLinksDraftValue - : FieldValue extends FieldCurrencyValue - ? FieldCurrencyDraftValue - : FieldValue extends FieldFullNameValue - ? FieldFullNameDraftValue - : FieldValue extends FieldRatingValue - ? FieldRatingValue - : FieldValue extends FieldSelectValue - ? FieldSelectDraftValue - : FieldValue extends FieldMultiSelectValue - ? FieldMultiSelectDraftValue - : FieldValue extends FieldRelationToOneValue - ? FieldRelationDraftValue - : FieldValue extends FieldRelationFromManyValue - ? FieldRelationManyDraftValue - : FieldValue extends FieldAddressValue - ? FieldAddressDraftValue - : FieldValue extends FieldJsonValue - ? FieldJsonDraftValue - : FieldValue extends FieldActorValue - ? FieldActorDraftValue - : never; + : FieldValue extends FieldPhonesValue + ? FieldPhonesDraftValue + : FieldValue extends FieldEmailsValue + ? FieldEmailsDraftValue + : FieldValue extends FieldLinksValue + ? FieldLinksDraftValue + : FieldValue extends FieldCurrencyValue + ? FieldCurrencyDraftValue + : FieldValue extends FieldFullNameValue + ? FieldFullNameDraftValue + : FieldValue extends FieldRatingValue + ? FieldRatingValue + : FieldValue extends FieldSelectValue + ? FieldSelectDraftValue + : FieldValue extends FieldMultiSelectValue + ? FieldMultiSelectDraftValue + : FieldValue extends FieldRelationToOneValue + ? FieldRelationDraftValue + : FieldValue extends FieldRelationFromManyValue + ? FieldRelationManyDraftValue + : FieldValue extends FieldAddressValue + ? FieldAddressDraftValue + : FieldValue extends FieldJsonValue + ? FieldJsonDraftValue + : FieldValue extends FieldActorValue + ? FieldActorDraftValue + : never; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index c6994db5a5f8..8c924c6ab123 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -183,13 +183,10 @@ export type FieldDateValue = string | null; export type FieldNumberValue = number | null; export type FieldBooleanValue = boolean; -export type FieldPhoneValue = string; -export type FieldEmailValue = string; export type FieldEmailsValue = { primaryEmail: string; additionalEmails: string[] | null; }; -export type FieldLinkValue = { url: string; label: string }; export type FieldLinksValue = { primaryLinkLabel: string; primaryLinkUrl: string; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmail.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmail.ts deleted file mode 100644 index 676cca39bdfd..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmail.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldDefinition } from '../FieldDefinition'; -import { FieldEmailMetadata, FieldMetadata } from '../FieldMetadata'; - -export const isFieldEmail = ( - field: Pick, 'type'>, -): field is FieldDefinition => - field.type === FieldMetadataType.Email; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailValue.ts deleted file mode 100644 index 2ce954057096..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldEmailValue.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isString } from '@sniptt/guards'; - -import { FieldEmailValue } from '../FieldMetadata'; - -// TODO: add zod -export const isFieldEmailValue = ( - fieldValue: unknown, -): fieldValue is FieldEmailValue => isString(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLink.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLink.ts deleted file mode 100644 index 6f98150d0149..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLink.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldDefinition } from '../FieldDefinition'; -import { FieldLinkMetadata, FieldMetadata } from '../FieldMetadata'; - -export const isFieldLink = ( - field: Pick, 'type'>, -): field is FieldDefinition => - field.type === FieldMetadataType.Link; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinkValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinkValue.ts deleted file mode 100644 index b075dc746f13..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldLinkValue.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from 'zod'; - -import { FieldLinkValue } from '../FieldMetadata'; - -const linkSchema = z.object({ - url: z.string(), - label: z.string(), -}); - -export const isFieldLinkValue = ( - fieldValue: unknown, -): fieldValue is FieldLinkValue => linkSchema.safeParse(fieldValue).success; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhone.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhone.ts deleted file mode 100644 index 0de71e270863..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhone.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -import { FieldDefinition } from '../FieldDefinition'; -import { FieldMetadata, FieldPhoneMetadata } from '../FieldMetadata'; - -export const isFieldPhone = ( - field: Pick, 'type'>, -): field is FieldDefinition => - field.type === FieldMetadataType.Phone; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhoneValue.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhoneValue.ts deleted file mode 100644 index 4bb79a9e611a..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldPhoneValue.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isString } from '@sniptt/guards'; - -import { FieldPhoneValue } from '../FieldMetadata'; - -// TODO: add zod -export const isFieldPhoneValue = ( - fieldValue: unknown, -): fieldValue is FieldPhoneValue => isString(fieldValue); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts index 47fe783322c2..07761aef8822 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts @@ -2,7 +2,6 @@ import { booleanFieldDefinition, fieldMetadataId, fullNameFieldDefinition, - linkFieldDefinition, relationFieldDefinition, selectFieldDefinition, } from '@/object-record/record-field/__mocks__/fieldDefinitions'; @@ -101,19 +100,4 @@ describe('isFieldValueEmpty', () => { }), ).toBe(false); }); - - it('should return correct value for link field', () => { - expect( - isFieldValueEmpty({ - fieldDefinition: linkFieldDefinition, - fieldValue: { url: '', label: '' }, - }), - ).toBe(true); - expect( - isFieldValueEmpty({ - fieldDefinition: linkFieldDefinition, - fieldValue: { url: 'https://linkedin.com/user-slug', label: '' }, - }), - ).toBe(false); - }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts index c668801e33aa..06d16fdb8ffb 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromString.ts @@ -5,10 +5,8 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; -import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; -import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; @@ -33,14 +31,10 @@ export const computeDraftValueFromString = ({ isFieldText(fieldDefinition) || isFieldDateTime(fieldDefinition) || isFieldNumber(fieldDefinition) || - isFieldEmail(fieldDefinition) || isFieldRelation(fieldDefinition) ) { return value as FieldInputDraftValue; } - if (isFieldLink(fieldDefinition)) { - return { url: value, label: value } as FieldInputDraftValue; - } if (isFieldCurrency(fieldDefinition)) { return { diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeEmptyDraftValue.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeEmptyDraftValue.ts index af950cf2041f..7433a30554e8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeEmptyDraftValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeEmptyDraftValue.ts @@ -5,7 +5,6 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata' import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFieldCurrency'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; -import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; @@ -27,7 +26,6 @@ export const computeEmptyDraftValue = ({ isFieldText(fieldDefinition) || isFieldDateTime(fieldDefinition) || isFieldNumber(fieldDefinition) || - isFieldEmail(fieldDefinition) || isFieldRelation(fieldDefinition) || isFieldRawJson(fieldDefinition) ) { diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx index 5743a74cbb1a..afb99cda1334 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx @@ -11,9 +11,6 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { isFieldArray } from '@/object-record/record-field/types/guards/isFieldArray'; -import { isFieldEmail } from '../types/guards/isFieldEmail'; -import { isFieldLink } from '../types/guards/isFieldLink'; -import { isFieldPhone } from '../types/guards/isFieldPhone'; export const getFieldButtonIcon = ( fieldDefinition: @@ -24,9 +21,6 @@ export const getFieldButtonIcon = ( if (isUndefinedOrNull(fieldDefinition)) return undefined; if ( - isFieldLink(fieldDefinition) || - isFieldEmail(fieldDefinition) || - isFieldPhone(fieldDefinition) || isFieldDisplayedAsPhone(fieldDefinition) || isFieldMultiSelect(fieldDefinition) || (isFieldRelation(fieldDefinition) && diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldMetadataReadOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldMetadataReadOnly.ts new file mode 100644 index 000000000000..a24ed100352d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldMetadataReadOnly.ts @@ -0,0 +1,19 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata'; +import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; + +export const isFieldMetadataReadOnly = (fieldMetadata: FieldMetadata) => { + if ( + fieldMetadata.fieldName === 'noteTargets' || + fieldMetadata.fieldName === 'taskTargets' + ) { + return true; + } + + return ( + isWorkflowSubObjectMetadata(fieldMetadata.objectMetadataNameSingular) || + (fieldMetadata.objectMetadataNameSingular === + CoreObjectNameSingular.Workflow && + fieldMetadata.fieldName !== 'name') + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index 0323ef19e099..4f7d2fe2b570 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -13,19 +13,15 @@ import { isFieldCurrency } from '@/object-record/record-field/types/guards/isFie import { isFieldCurrencyValue } from '@/object-record/record-field/types/guards/isFieldCurrencyValue'; import { isFieldDate } from '@/object-record/record-field/types/guards/isFieldDate'; import { isFieldDateTime } from '@/object-record/record-field/types/guards/isFieldDateTime'; -import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldEmail'; import { isFieldEmails } from '@/object-record/record-field/types/guards/isFieldEmails'; import { isFieldEmailsValue } from '@/object-record/record-field/types/guards/isFieldEmailsValue'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue'; -import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink'; -import { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue'; import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; -import { isFieldPhone } from '@/object-record/record-field/types/guards/isFieldPhone'; import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; import { isFieldPhonesValue } from '@/object-record/record-field/types/guards/isFieldPhonesValue'; import { isFieldPosition } from '@/object-record/record-field/types/guards/isFieldPosition'; @@ -60,12 +56,10 @@ export const isFieldValueEmpty = ({ isFieldDate(fieldDefinition) || isFieldNumber(fieldDefinition) || isFieldRating(fieldDefinition) || - isFieldEmail(fieldDefinition) || isFieldBoolean(fieldDefinition) || isFieldRelation(fieldDefinition) || isFieldRawJson(fieldDefinition) || isFieldRichText(fieldDefinition) || - isFieldPhone(fieldDefinition) || isFieldPosition(fieldDefinition) ) { return isValueEmpty(fieldValue); @@ -101,10 +95,6 @@ export const isFieldValueEmpty = ({ ); } - if (isFieldLink(fieldDefinition)) { - return !isFieldLinkValue(fieldValue) || isValueEmpty(fieldValue?.url); - } - if (isFieldAddress(fieldDefinition)) { return ( !isFieldAddressValue(fieldValue) || diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts index 5d4890f42e8f..e004288ceba0 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts @@ -32,8 +32,6 @@ export const applyEmptyFilters = ( switch (definition.type) { case 'TEXT': - case 'EMAIL': - case 'PHONE': emptyRecordFilter = { or: [ { [correspondingField.name]: { ilike: '' } as StringFilter }, @@ -113,16 +111,6 @@ export const applyEmptyFilters = ( } break; } - case 'LINK': - emptyRecordFilter = { - or: [ - { [correspondingField.name]: { url: { ilike: '' } } as URLFilter }, - { - [correspondingField.name]: { url: { is: 'NULL' } } as URLFilter, - }, - ], - }; - break; case 'LINKS': { if (!isCompositeField) { const linksFilters = generateILikeFiltersForCompositeFields( diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts index f83c82d0e865..2ddb31f0b12d 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts @@ -17,7 +17,6 @@ import { PhonesFilter, RecordGqlOperationFilter, StringFilter, - URLFilter, UUIDFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { isMatchingBooleanFilter } from '@/object-record/record-filter/utils/isMatchingBooleanFilter'; @@ -144,8 +143,6 @@ export const isRecordMatchingFilter = ({ } switch (objectMetadataField.type) { - case FieldMetadataType.Email: - case FieldMetadataType.Phone: case FieldMetadataType.Select: case FieldMetadataType.Rating: case FieldMetadataType.MultiSelect: @@ -155,22 +152,6 @@ export const isRecordMatchingFilter = ({ value: record[filterKey], }); } - case FieldMetadataType.Link: { - const urlFilter = filterValue as URLFilter; - - return ( - (urlFilter.url === undefined || - isMatchingStringFilter({ - stringFilter: urlFilter.url, - value: record[filterKey].url, - })) && - (urlFilter.label === undefined || - isMatchingStringFilter({ - stringFilter: urlFilter.label, - value: record[filterKey].label, - })) - ); - } case FieldMetadataType.FullName: { const fullNameFilter = filterValue as FullNameFilter; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index ea849199cdda..476818a56684 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -10,7 +10,6 @@ import { RecordGqlOperationFilter, RelationFilter, StringFilter, - URLFilter, UUIDFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; @@ -388,43 +387,6 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( ); } break; - case 'LINK': - switch (rawUIFilter.operand) { - case ViewFilterOperand.Contains: - objectRecordFilters.push({ - [correspondingField.name]: { - url: { - ilike: `%${rawUIFilter.value}%`, - }, - } as URLFilter, - }); - break; - case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - not: { - [correspondingField.name]: { - url: { - ilike: `%${rawUIFilter.value}%`, - }, - } as URLFilter, - }, - }); - break; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - applyEmptyFilters( - rawUIFilter.operand, - correspondingField, - objectRecordFilters, - rawUIFilter.definition, - ); - break; - default: - throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, - ); - } - break; case 'LINKS': { const linksFilters = generateILikeFiltersForCompositeFields( rawUIFilter.value, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index 8bca151080a2..b34fc8b7ac98 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -4,9 +4,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { RecordBoardActionBar } from '@/object-record/record-board/action-bar/components/RecordBoardActionBar'; import { RecordBoard } from '@/object-record/record-board/components/RecordBoard'; -import { RecordBoardContextMenu } from '@/object-record/record-board/context-menu/components/RecordBoardContextMenu'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; @@ -51,8 +49,6 @@ export const RecordIndexBoardContainer = ({ }} > - - ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx index c4ad79ed412f..354abcf09dab 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx @@ -6,9 +6,7 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; -import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; @@ -79,8 +77,6 @@ export const RecordIndexBoardDataLoaderEffect = ({ setNavigationMemorizedUrl, ]); - const { resetRecordSelection } = useRecordBoardSelection(recordBoardId); - useEffect(() => { setObjectSingularName(objectNameSingular); }, [objectNameSingular, setObjectSingularName]); @@ -125,12 +121,6 @@ export const RecordIndexBoardDataLoaderEffect = ({ const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ - objectMetadataItem, - selectedRecordIds, - callback: resetRecordSelection, - }); - const setContextStoreTargetedRecordIds = useSetRecoilState( contextStoreTargetedRecordIdsState, ); @@ -140,9 +130,8 @@ export const RecordIndexBoardDataLoaderEffect = ({ ); useEffect(() => { - setActionBarEntries?.(); - setContextMenuEntries?.(); - }, [setActionBarEntries, setContextMenuEntries]); + setContextStoreTargetedRecordIds(selectedRecordIds); + }, [selectedRecordIds, setContextStoreTargetedRecordIds]); useEffect(() => { setContextStoreTargetedRecordIds(selectedRecordIds); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 3af3237929f4..cb0d934656d0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -23,6 +23,13 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; + +import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; +import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect'; +import { ActionMenuEntriesProvider } from '@/action-menu/components/ActionMenuEntriesProvider'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; @@ -191,6 +198,15 @@ export const RecordIndexContainer = () => { /> )} + + + + + + + diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx index bb8c0197a940..f167ad13f19d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx @@ -2,6 +2,7 @@ import { useRecoilValue } from 'recoil'; import { useIcons } from 'twenty-ui'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; @@ -30,8 +31,11 @@ export const RecordIndexPageHeader = () => { const recordIndexViewType = useRecoilValue(recordIndexViewTypeState); - const isTable = - recordIndexViewType === ViewType.Table && !objectMetadataItem?.isRemote; + const shouldDisplayAddButton = objectMetadataItem + ? !isObjectMetadataReadOnly(objectMetadataItem) + : false; + + const isTable = recordIndexViewType === ViewType.Table; const pageHeaderTitle = objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural); @@ -43,11 +47,12 @@ export const RecordIndexPageHeader = () => { return ( - {isTable ? ( - - ) : ( - - )} + {shouldDisplayAddButton && + (isTable ? ( + + ) : ( + + ))} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx index 50bb639f5237..f1dec5e3e7e6 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainer.tsx @@ -2,9 +2,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext'; import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; -import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; -import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu'; import { useContext } from 'react'; type RecordIndexTableContainerProps = { @@ -37,9 +35,7 @@ export const RecordIndexTableContainer = ({ viewBarId={viewBarId} updateRecordMutation={updateEntity} /> - - ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index ce8bb6572d8c..ce5dc7279ded 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -5,14 +5,10 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; -import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState'; type RecordIndexTableContainerEffectProps = { objectNameSingular: string; @@ -28,7 +24,6 @@ export const RecordIndexTableContainerEffect = ({ const { setAvailableTableColumns, setOnEntityCountChange, - resetTableRowSelection, selectedRowIdsSelector, setOnToggleColumnFilter, setOnToggleColumnSort, @@ -58,34 +53,8 @@ export const RecordIndexTableContainerEffect = ({ setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const { tableRowIdsState, hasUserSelectedAllRowsState } = - useRecordTableStates(recordTableId); - - // TODO: verify this instance id works - const entityCountInCurrentView = useRecoilComponentValueV2( - entityCountInCurrentViewComponentState, - recordTableId, - ); - const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); - const tableRowIds = useRecoilValue(tableRowIdsState); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const numSelected = - hasUserSelectedAllRows && entityCountInCurrentView - ? selectedRowIds.length === tableRowIds.length - ? entityCountInCurrentView - : entityCountInCurrentView - - (tableRowIds.length - selectedRowIds.length) // unselected row Ids - : selectedRowIds.length; - - const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ - objectMetadataItem, - selectedRecordIds: selectedRowIds, - callback: resetTableRowSelection, - totalNumberOfRecordsSelected: numSelected, - }); - const handleToggleColumnFilter = useHandleToggleColumnFilter({ objectNameSingular, viewBarId, @@ -110,11 +79,6 @@ export const RecordIndexTableContainerEffect = ({ ); }, [setOnToggleColumnSort, handleToggleColumnSort]); - useEffect(() => { - setActionBarEntries?.(); - setContextMenuEntries?.(); - }, [setActionBarEntries, setContextMenuEntries]); - useEffect(() => { setOnEntityCountChange( () => (entityCount: number) => setRecordCountInCurrentView(entityCount), diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index 36a3d7a1a2fe..eefeb3540878 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -24,6 +24,7 @@ import { } from '@/object-record/record-field/contexts/FieldContext'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly'; import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; @@ -180,6 +181,8 @@ export const RecordDetailRelationRecordsListItem = ({ [isExpanded], ); + const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata); + return ( <> @@ -195,37 +198,39 @@ export const RecordDetailRelationRecordsListItem = ({ accent="tertiary" /> - - - } - dropdownComponents={ - - + - {!isAccountOwnerRelation && ( + } + dropdownComponents={ + - )} - - } - dropdownHotkeyScope={{ scope: dropdownScopeId }} - /> - + {!isAccountOwnerRelation && ( + + )} + + } + dropdownHotkeyScope={{ scope: dropdownScopeId }} + /> + + )} diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 4e2d194036f6..81e0c37c76e7 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -12,6 +12,7 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect'; import { useUpdateRelationFromManyFieldInput } from '@/object-record/record-field/meta-types/input/hooks/useUpdateRelationFromManyFieldInput'; import { FieldRelationMetadata } from '@/object-record/record-field/types/FieldMetadata'; +import { isFieldMetadataReadOnly } from '@/object-record/record-field/utils/isFieldMetadataReadOnly'; import { RecordDetailRelationRecordsList } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsList'; import { RecordDetailSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailSection'; import { RecordDetailSectionHeader } from '@/object-record/record-show/record-detail-section/components/RecordDetailSectionHeader'; @@ -158,6 +159,8 @@ export const RecordDetailRelationSection = ({ recordId, }); + const canEdit = !isFieldMetadataReadOnly(fieldDefinition.metadata); + if (loading) return null; return ( @@ -178,49 +181,51 @@ export const RecordDetailRelationSection = ({ hideRightAdornmentOnMouseLeave={!isDropdownOpen && !isMobile} areRecordsAvailable={relationRecords.length > 0} rightAdornment={ - - - } - dropdownComponents={ - - {isToOneObject ? ( - - ) : ( - <> - - - + + } + dropdownComponents={ + + {isToOneObject ? ( + - - )} - - } - dropdownHotkeyScope={{ - scope: dropdownId, - }} - /> - + ) : ( + <> + + + + + )} + + } + dropdownHotkeyScope={{ + scope: dropdownId, + }} + /> + + ) } /> {showContent()} diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx deleted file mode 100644 index 0b2c810bc15c..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; -import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState'; - -export const RecordTableActionBar = ({ - recordTableId, -}: { - recordTableId: string; -}) => { - const { - selectedRowIdsSelector, - tableRowIdsState, - hasUserSelectedAllRowsState, - } = useRecordTableStates(recordTableId); - - // TODO: verify this instance id works - const entityCountInCurrentView = useRecoilComponentValueV2( - entityCountInCurrentViewComponentState, - recordTableId, - ); - - const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); - const tableRowIds = useRecoilValue(tableRowIdsState); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - - const totalNumberOfSelectedRecords = - hasUserSelectedAllRows && entityCountInCurrentView - ? selectedRowIds.length === tableRowIds.length - ? entityCountInCurrentView - : entityCountInCurrentView - - (tableRowIds.length - selectedRowIds.length) // unselected row Ids - : selectedRowIds.length; - - if (!selectedRowIds.length) { - return null; - } - - return ( - - ); -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index ccec1297bf3b..f195b2b68649 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -70,7 +70,9 @@ export const RecordTable = ({ ) : ( - + )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx index 6d5c5b7e0e75..8e827ae3e4e6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableContextProvider.tsx @@ -12,7 +12,7 @@ import { OpenTableCellArgs, useOpenRecordTableCellV2, } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; -import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu'; +import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown'; import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition'; @@ -75,12 +75,15 @@ export const RecordTableContextProvider = ({ moveSoftFocusToCell(cellPosition); }; - const { triggerContextMenu } = useTriggerContextMenu({ + const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({ recordTableId, }); - const handleContextMenu = (event: React.MouseEvent, recordId: string) => { - triggerContextMenu(event, recordId); + const handleActionMenuDropdown = ( + event: React.MouseEvent, + recordId: string, + ) => { + triggerActionMenuDropdown(event, recordId); }; const { handleContainerMouseEnter } = useHandleContainerMouseEnter({ @@ -99,7 +102,7 @@ export const RecordTableContextProvider = ({ onMoveFocus: handleMoveFocus, onCloseTableCell: handleCloseTableCell, onMoveSoftFocusToCell: handleMoveSoftFocusToCell, - onContextMenu: handleContextMenu, + onActionMenuDropdownOpened: handleActionMenuDropdown, onCellMouseEnter: handleContainerMouseEnter, visibleTableColumns, recordTableId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx index 8b94a325f36a..0dff4b429dca 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx @@ -46,7 +46,7 @@ export const RecordTableInternalEffect = ({ useListenClickOutsideByClassName({ classNames: ['entity-table-cell'], - excludeClassNames: ['action-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'context-menu'], callback: () => { resetTableRowSelection(); }, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 268608e183cc..b7a46e64829d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -81,7 +81,9 @@ export const RecordTableWithWrappers = ({ /> { + resetTableRowSelection(); + }} onDragSelectionChange={setRowSelected} /> diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index b866e344842f..b03187ceae68 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -70,7 +70,7 @@ const meta: Meta = { onMoveFocus: () => {}, onCloseTableCell: () => {}, onMoveSoftFocusToCell: () => {}, - onContextMenu: () => {}, + onActionMenuDropdownOpened: () => {}, onCellMouseEnter: () => {}, visibleTableColumns: mockPerformance.visibleTableColumns as any, objectNameSingular: diff --git a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx deleted file mode 100644 index d9f712ef82e4..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; -import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu'; - -export const RecordTableContextMenu = ({ - recordTableId, -}: { - recordTableId: string; -}) => { - const { selectedRowIdsSelector } = useRecordTableStates(recordTableId); - - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - - if (!selectedRowIds.length) { - return null; - } - - return ; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts index cf8a8afed861..ba2ea9f7a92d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableContext.ts @@ -24,7 +24,10 @@ export type RecordTableContextProps = { onMoveFocus: (direction: MoveFocusDirection) => void; onCloseTableCell: () => void; onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void; - onContextMenu: (event: React.MouseEvent, recordId: string) => void; + onActionMenuDropdownOpened: ( + event: React.MouseEvent, + recordId: string, + ) => void; onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void; visibleTableColumns: ColumnDefinition[]; recordTableId: string; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx index c25a3cf11990..7ea1deb12616 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx @@ -1,4 +1,3 @@ -import { useObjectIsRemote } from '@/object-metadata/hooks/useObjectIsRemote'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll'; @@ -18,7 +17,7 @@ export const RecordTableEmptyState = () => { const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const noRecordAtAll = totalCount === 0; - const isRemote = useObjectIsRemote(objectMetadataItem); + const isRemote = objectMetadataItem.isRemote; const isSoftDeleteActive = useRecoilValue(isSoftDeleteActiveState); diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 80c5ecaefeb1..c5f120a6de63 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -8,7 +8,10 @@ import { AnimatedPlaceholderEmptyTitle, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; +import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; +import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { Button } from '@/ui/input/button/components/Button'; +import { useContext } from 'react'; import { IconComponent } from 'twenty-ui'; type RecordTableEmptyStateDisplayProps = { @@ -28,6 +31,9 @@ export const RecordTableEmptyStateDisplay = ({ subTitle, title, }: RecordTableEmptyStateDisplayProps) => { + const { objectMetadataItem } = useContext(RecordTableContext); + const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); + return ( @@ -37,12 +43,14 @@ export const RecordTableEmptyStateDisplay = ({ {subTitle} -