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}
-
+ {!isReadOnly && (
+
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
index 1b6263739111..58779a443da2 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts
@@ -1,7 +1,9 @@
import { useRecoilCallback } from 'recoil';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
export const useResetTableRowSelection = (recordTableId?: string) => {
const {
@@ -20,7 +22,19 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
}
set(hasUserSelectedAllRowsState, false);
+
+ const isActionMenuDropdownOpenState = extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ );
+
+ set(isActionMenuDropdownOpenState, false);
},
- [tableRowIdsState, isRowSelectedFamilyState, hasUserSelectedAllRowsState],
+ [
+ tableRowIdsState,
+ hasUserSelectedAllRowsState,
+ recordTableId,
+ isRowSelectedFamilyState,
+ ],
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
index 1e977ecaec4e..52ae0772d769 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellBaseContainer.tsx
@@ -71,10 +71,10 @@ export const RecordTableCellBaseContainer = ({
}
};
- const { onContextMenu } = useContext(RecordTableContext);
+ const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
- const handleContextMenu = (event: React.MouseEvent) => {
- onContextMenu(event, recordId);
+ const handleActionMenuDropdown = (event: React.MouseEvent) => {
+ onActionMenuDropdownOpened(event, recordId);
};
const { hotkeyScope } = useContext(FieldContext);
@@ -87,7 +87,7 @@ export const RecordTableCellBaseContainer = ({
onMouseLeave={handleContainerMouseLeave}
onMouseMove={handleContainerMouseMove}
onClick={handleContainerClick}
- onContextMenu={handleContextMenu}
+ onContextMenu={handleActionMenuDropdown}
backgroundColorTransparentSecondary={
theme.background.transparent.secondary
}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
index 645e2bc4ab6e..459211537244 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx
@@ -1,13 +1,12 @@
import styled from '@emotion/styled';
import { useCallback, useContext } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
+import { useRecoilValue } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from '@/ui/input/components/Checkbox';
-import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
const StyledContainer = styled.div`
align-items: center;
@@ -24,14 +23,12 @@ export const RecordTableCellCheckbox = () => {
const { recordId } = useContext(RecordTableRowContext);
const { isRowSelectedFamilyState } = useRecordTableStates();
- const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
const { setCurrentRowSelected } = useSetCurrentRowSelected();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const handleClick = useCallback(() => {
setCurrentRowSelected(!currentRowSelected);
- setActionBarOpenState(true);
- }, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
+ }, [currentRowSelected, setCurrentRowSelected]);
return (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts
new file mode 100644
index 000000000000..0870b7bb4d5a
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts
@@ -0,0 +1,67 @@
+import { useRecoilCallback } from 'recoil';
+
+import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
+import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
+import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
+import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
+import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
+import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
+
+export const useTriggerActionMenuDropdown = ({
+ recordTableId,
+}: {
+ recordTableId: string;
+}) => {
+ const triggerActionMenuDropdown = useRecoilCallback(
+ ({ set, snapshot }) =>
+ (event: React.MouseEvent, recordId: string) => {
+ event.preventDefault();
+
+ const tableScopeId = getScopeIdFromComponentId(recordTableId);
+
+ set(
+ extractComponentState(
+ actionMenuDropdownPositionComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ ),
+ {
+ x: event.clientX,
+ y: event.clientY,
+ },
+ );
+
+ const isRowSelectedFamilyState = extractComponentFamilyState(
+ isRowSelectedComponentFamilyState,
+ tableScopeId,
+ );
+
+ const isRowSelected = getSnapshotValue(
+ snapshot,
+ isRowSelectedFamilyState(recordId),
+ );
+
+ if (isRowSelected !== true) {
+ set(isRowSelectedFamilyState(recordId), true);
+ }
+
+ const isActionMenuDropdownOpenState = extractComponentState(
+ isDropdownOpenComponentState,
+ `action-menu-dropdown-${recordTableId}`,
+ );
+
+ const isActionBarOpenState = isBottomBarOpenedComponentState.atomFamily(
+ {
+ instanceId: `action-bar-${recordTableId}`,
+ },
+ );
+
+ set(isActionBarOpenState, false);
+ set(isActionMenuDropdownOpenState, true);
+ },
+ [recordTableId],
+ );
+
+ return { triggerActionMenuDropdown };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts
deleted file mode 100644
index b9c04a8b7769..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useRecoilCallback } from 'recoil';
-
-import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
-import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
-import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
-import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
-import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
-
-export const useTriggerContextMenu = ({
- recordTableId,
-}: {
- recordTableId: string;
-}) => {
- const triggerContextMenu = useRecoilCallback(
- ({ set, snapshot }) =>
- (event: React.MouseEvent, recordId: string) => {
- event.preventDefault();
-
- const tableScopeId = getScopeIdFromComponentId(recordTableId);
-
- set(contextMenuPositionState, {
- x: event.clientX,
- y: event.clientY,
- });
- set(contextMenuIsOpenState, true);
-
- const isRowSelectedFamilyState = extractComponentFamilyState(
- isRowSelectedComponentFamilyState,
- tableScopeId,
- );
-
- const isRowSelected = getSnapshotValue(
- snapshot,
- isRowSelectedFamilyState(recordId),
- );
-
- if (isRowSelected !== true) {
- set(isRowSelectedFamilyState(recordId), true);
- }
- },
- [recordTableId],
- );
-
- return { triggerContextMenu };
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx
index 566ca51cd643..556e1e9845d5 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx
@@ -73,7 +73,11 @@ const StyledTableHead = styled.thead<{
}
`;
-export const RecordTableHeader = () => {
+export const RecordTableHeader = ({
+ objectMetadataNameSingular,
+}: {
+ objectMetadataNameSingular: string;
+}) => {
const { visibleTableColumnsSelector } = useRecordTableStates();
const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector());
@@ -84,7 +88,11 @@ export const RecordTableHeader = () => {
{visibleTableColumns.map((column) => (
-
+
))}
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
index 4084b95dd9b2..ff3fc98580ee 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
@@ -3,6 +3,8 @@ import { useCallback, useMemo, useState } from 'react';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconPlus } from 'twenty-ui';
+import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
+import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
@@ -91,11 +93,17 @@ const StyledHeaderIcon = styled.div`
export const RecordTableHeaderCell = ({
column,
+ objectMetadataNameSingular,
}: {
column: ColumnDefinition;
+ objectMetadataNameSingular: string;
}) => {
const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates();
+ const { objectMetadataItem } = useObjectMetadataItem({
+ objectNameSingular: objectMetadataNameSingular,
+ });
+
const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState(
resizeFieldOffsetState,
);
@@ -190,6 +198,8 @@ export const RecordTableHeaderCell = ({
createNewTableRecord();
};
+ const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
+
return (
- {(useIsMobile() || iconVisibility) && !!column.isLabelIdentifier && (
-
-
-
- )}
+ {(useIsMobile() || iconVisibility) &&
+ !!column.isLabelIdentifier &&
+ !isReadOnly && (
+
+
+
+ )}
{!disableColumnResize && (
isValidPhoneNumber(value),
- errorMessage: fieldName + ' is not valid',
- level: 'error',
- },
- ];
case FieldMetadataType.Relation:
return [
{
diff --git a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
index a0a69349fc35..b936f09833df 100644
--- a/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
+++ b/packages/twenty-front/src/modules/object-record/utils/generateEmptyFieldValue.ts
@@ -8,20 +8,12 @@ export const generateEmptyFieldValue = (
fieldMetadataItem: Pick,
) => {
switch (fieldMetadataItem.type) {
- case FieldMetadataType.Email:
- case FieldMetadataType.Phone:
case FieldMetadataType.Text: {
return '';
}
case FieldMetadataType.Emails: {
return { primaryEmail: '', additionalEmails: null };
}
- case FieldMetadataType.Link: {
- return {
- label: '',
- url: '',
- };
- }
case FieldMetadataType.Links: {
return { primaryLinkUrl: '', primaryLinkLabel: '', secondaryLinks: null };
}
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
index 4181fd65e807..b2fb02c29f62 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts
@@ -6,7 +6,6 @@ import {
FieldEmailsValue,
FieldFullNameValue,
FieldLinksValue,
- FieldLinkValue,
FieldPhonesValue,
} from '@/object-record/record-field/types/FieldMetadata';
import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs';
@@ -38,8 +37,8 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
[FieldMetadataType.Currency]: {
label: 'Currency',
Icon: IllustrationIconCurrency,
- subFields: ['amountMicros', 'currencyCode'],
- filterableSubFields: ['amountMicros', 'currencyCode'],
+ subFields: ['amountMicros'],
+ filterableSubFields: ['amountMicros'],
labelBySubField: {
amountMicros: 'Amount',
currencyCode: 'Currency',
@@ -69,18 +68,6 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
},
category: 'Basic',
} as const satisfies SettingsCompositeFieldTypeConfig,
- [FieldMetadataType.Link]: {
- label: 'Link',
- Icon: IllustrationIconLink,
- exampleValue: { url: 'www.twenty.com', label: '' },
- category: 'Basic',
- subFields: ['url', 'label'],
- filterableSubFields: ['url', 'label'],
- labelBySubField: {
- url: 'URL',
- label: 'Label',
- },
- } as const satisfies SettingsCompositeFieldTypeConfig,
[FieldMetadataType.Links]: {
label: 'Links',
Icon: IllustrationIconLink,
@@ -178,8 +165,8 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
label: 'Actor',
Icon: IllustrationIconSetting,
category: 'Basic',
- subFields: ['source', 'name', 'workspaceMemberId'],
- filterableSubFields: ['source', 'name', 'workspaceMemberId'],
+ subFields: ['source'],
+ filterableSubFields: ['source'],
labelBySubField: {
source: 'Source',
name: 'Name',
diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
index 42f291b60eff..1039646c47ee 100644
--- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts
@@ -4,10 +4,8 @@ import {
IllustrationIconCalendarEvent,
IllustrationIconCalendarTime,
IllustrationIconJson,
- IllustrationIconMail,
IllustrationIconNumbers,
IllustrationIconOneToMany,
- IllustrationIconPhone,
IllustrationIconSetting,
IllustrationIconStar,
IllustrationIconTag,
@@ -22,11 +20,9 @@ import {
FieldBooleanValue,
FieldDateTimeValue,
FieldDateValue,
- FieldEmailValue,
FieldJsonValue,
FieldMultiSelectValue,
FieldNumberValue,
- FieldPhoneValue,
FieldRatingValue,
FieldRelationValue,
FieldRichTextValue,
@@ -114,17 +110,6 @@ export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFiel
Icon: IllustrationIconOneToMany,
category: 'Relation',
} as const satisfies SettingsFieldTypeConfig>,
- [FieldMetadataType.Email]: {
- label: 'Email',
- Icon: IllustrationIconMail,
- category: 'Basic',
- } as const satisfies SettingsFieldTypeConfig,
- [FieldMetadataType.Phone]: {
- label: 'Phone',
- Icon: IllustrationIconPhone,
- exampleValue: '+1234-567-890',
- category: 'Basic',
- } as const satisfies SettingsFieldTypeConfig,
[FieldMetadataType.Rating]: {
label: 'Rating',
Icon: IllustrationIconStar,
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
index 882b57f7d1c1..9b50515b10b9 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
@@ -113,11 +113,9 @@ const previewableTypes = [
FieldMetadataType.DateTime,
FieldMetadataType.Emails,
FieldMetadataType.FullName,
- FieldMetadataType.Link,
FieldMetadataType.Links,
FieldMetadataType.MultiSelect,
FieldMetadataType.Number,
- FieldMetadataType.Phone,
FieldMetadataType.Phones,
FieldMetadataType.Rating,
FieldMetadataType.RawJson,
diff --git a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts
index f1186c6d1047..ddf7b0d579e0 100644
--- a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts
+++ b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts
@@ -5,7 +5,6 @@ import { PickLiteral } from '~/types/PickLiteral';
export const COMPOSITE_FIELD_TYPES = [
'CURRENCY',
'EMAILS',
- 'LINK',
'LINKS',
'ADDRESS',
'PHONES',
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
index f7fdf0218239..2bad3fa95ffc 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx
@@ -2,7 +2,6 @@ import { useEffect } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
-import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SIGN_IN_BACKGROUND_MOCK_COLUMN_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockColumnDefinitions';
import { SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS } from '@/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions';
@@ -23,14 +22,10 @@ export const SignInBackgroundMockContainerEffect = ({
recordTableId,
viewId,
}: SignInBackgroundMockContainerEffectProps) => {
- const {
- setAvailableTableColumns,
- setOnEntityCountChange,
- setTableColumns,
- resetTableRowSelection,
- } = useRecordTable({
- recordTableId,
- });
+ const { setAvailableTableColumns, setOnEntityCountChange, setTableColumns } =
+ useRecordTable({
+ recordTableId,
+ });
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
@@ -75,17 +70,6 @@ export const SignInBackgroundMockContainerEffect = ({
setTableColumns,
]);
- const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
- objectMetadataItem,
- selectedRecordIds: [],
- callback: resetTableRowSelection,
- });
-
- useEffect(() => {
- setActionBarEntries?.();
- setContextMenuEntries?.();
- }, [setActionBarEntries, setContextMenuEntries]);
-
useEffect(() => {
setOnEntityCountChange(
() => (entityCount: number) => setRecordCountInCurrentView(entityCount),
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
index 6ce3a80a5d4a..2b17a655549d 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx
@@ -2,8 +2,6 @@ import styled from '@emotion/styled';
import { IconBuildingSkyscraper } from 'twenty-ui';
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
-import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
-import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
import { SignInBackgroundMockContainer } from '@/sign-in-background-mock/components/SignInBackgroundMockContainer';
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
import { PageBody } from '@/ui/layout/page/PageBody';
@@ -29,8 +27,6 @@ export const SignInBackgroundMockPage = () => {
-
-
diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions.ts
index f3d24ae7a95c..92040bbb8aae 100644
--- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions.ts
+++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockFilterDefinitions.ts
@@ -43,13 +43,13 @@ export const SIGN_IN_BACKGROUND_MOCK_FILTER_DEFINITIONS = [
fieldMetadataId: '20202020-a61d-4b78-b998-3fd88b4f73a1',
label: 'Linkedin',
iconName: 'IconBrandLinkedin',
- type: 'LINK',
+ type: 'LINKS',
},
{
fieldMetadataId: '20202020-46e3-479a-b8f4-77137c74daa6',
label: 'X',
iconName: 'IconBrandX',
- type: 'LINK',
+ type: 'LINKS',
},
{
fieldMetadataId: '20202020-4a5a-466f-92d9-c3870d9502a9',
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
index 9e2e2b48ef95..963887f11e0d 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx
@@ -1,6 +1,5 @@
import { isNonEmptyString } from '@sniptt/guards';
-import { FieldLinkValue } from '@/object-record/record-field/types/FieldMetadata';
import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink';
import {
LinkType,
@@ -8,7 +7,7 @@ import {
} from '@/ui/navigation/link/components/SocialLink';
type LinkDisplayProps = {
- value?: FieldLinkValue;
+ value?: { url: string; label?: string };
};
export const LinkDisplay = ({ value }: LinkDisplayProps) => {
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx
new file mode 100644
index 000000000000..46ccff887a7a
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/BottomBar.tsx
@@ -0,0 +1,61 @@
+import styled from '@emotion/styled';
+
+import { useBottomBarInternalHotkeyScopeManagement } from '@/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement';
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+
+const StyledContainerActionBar = styled.div`
+ align-items: center;
+ background: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ bottom: 38px;
+ box-shadow: ${({ theme }) => theme.boxShadow.strong};
+ display: flex;
+ height: 48px;
+ width: max-content;
+ left: 50%;
+ padding-left: ${({ theme }) => theme.spacing(2)};
+ padding-right: ${({ theme }) => theme.spacing(2)};
+ position: absolute;
+ top: auto;
+
+ transform: translateX(-50%);
+ z-index: 1;
+`;
+
+type BottomBarProps = {
+ bottomBarId: string;
+ bottomBarHotkeyScopeFromParent: HotkeyScope;
+ children: React.ReactNode;
+};
+
+export const BottomBar = ({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+ children,
+}: BottomBarProps) => {
+ const isBottomBarOpen = useRecoilComponentValueV2(
+ isBottomBarOpenedComponentState,
+ bottomBarId,
+ );
+
+ useBottomBarInternalHotkeyScopeManagement({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+ });
+
+ if (!isBottomBarOpen) {
+ return null;
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx
new file mode 100644
index 000000000000..8562f67cdce3
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx
@@ -0,0 +1,65 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { IconPlus } from 'twenty-ui';
+
+import { Button } from '@/ui/input/button/components/Button';
+import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import styled from '@emotion/styled';
+import { RecoilRoot } from 'recoil';
+
+const StyledContainer = styled.div`
+ display: flex;
+ gap: 10px;
+`;
+
+const meta: Meta = {
+ title: 'UI/Layout/BottomBar/BottomBar',
+ component: BottomBar,
+ args: {
+ bottomBarId: 'test',
+ bottomBarHotkeyScopeFromParent: { scope: 'test' },
+ children: (
+
+
+
+
+
+ ),
+ },
+ argTypes: {
+ bottomBarId: { control: false },
+ bottomBarHotkeyScopeFromParent: { control: false },
+ children: { control: false },
+ },
+};
+
+export default meta;
+
+export const Default: StoryObj = {
+ decorators: [
+ (Story) => (
+ {
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: 'test',
+ }),
+ true,
+ );
+ }}
+ >
+
+
+ ),
+ ],
+};
+
+export const Closed: StoryObj = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts
new file mode 100644
index 000000000000..bfae8a470fac
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBar.ts
@@ -0,0 +1,87 @@
+import { useRecoilCallback } from 'recoil';
+
+import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
+import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
+import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { isDefined } from '~/utils/isDefined';
+
+export const useBottomBar = () => {
+ const {
+ setHotkeyScopeAndMemorizePreviousScope,
+ goBackToPreviousHotkeyScope,
+ } = usePreviousHotkeyScope();
+
+ const closeBottomBar = useRecoilCallback(
+ ({ set }) =>
+ (specificComponentId: string) => {
+ goBackToPreviousHotkeyScope();
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ false,
+ );
+ },
+ [goBackToPreviousHotkeyScope],
+ );
+
+ const openBottomBar = useRecoilCallback(
+ ({ set, snapshot }) =>
+ (specificComponentId: string, customHotkeyScope?: HotkeyScope) => {
+ const bottomBarHotkeyScope = snapshot
+ .getLoadable(
+ bottomBarHotkeyComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ )
+ .getValue();
+
+ set(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ true,
+ );
+
+ if (isDefined(customHotkeyScope)) {
+ setHotkeyScopeAndMemorizePreviousScope(
+ customHotkeyScope.scope,
+ customHotkeyScope.customScopes,
+ );
+ } else if (isDefined(bottomBarHotkeyScope)) {
+ setHotkeyScopeAndMemorizePreviousScope(
+ bottomBarHotkeyScope.scope,
+ bottomBarHotkeyScope.customScopes,
+ );
+ }
+ },
+ [setHotkeyScopeAndMemorizePreviousScope],
+ );
+
+ const toggleBottomBar = useRecoilCallback(
+ ({ snapshot }) =>
+ (specificComponentId: string) => {
+ const isBottomBarOpen = snapshot
+ .getLoadable(
+ isBottomBarOpenedComponentState.atomFamily({
+ instanceId: specificComponentId,
+ }),
+ )
+ .getValue();
+
+ if (isBottomBarOpen) {
+ closeBottomBar(specificComponentId);
+ } else {
+ openBottomBar(specificComponentId);
+ }
+ },
+ [closeBottomBar, openBottomBar],
+ );
+
+ return {
+ closeBottomBar,
+ openBottomBar,
+ toggleBottomBar,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts
new file mode 100644
index 000000000000..a35ccfc69ead
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/hooks/useBottomBarInternalHotkeyScopeManagement.ts
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+
+import { bottomBarHotkeyComponentState } from '@/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
+import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
+
+export const useBottomBarInternalHotkeyScopeManagement = ({
+ bottomBarId,
+ bottomBarHotkeyScopeFromParent,
+}: {
+ bottomBarId?: string;
+ bottomBarHotkeyScopeFromParent?: HotkeyScope;
+}) => {
+ const [bottomBarHotkeyScope, setBottomBarHotkeyScope] =
+ useRecoilComponentStateV2(bottomBarHotkeyComponentState, bottomBarId);
+
+ useEffect(() => {
+ if (!isDeeplyEqual(bottomBarHotkeyScopeFromParent, bottomBarHotkeyScope)) {
+ setBottomBarHotkeyScope(bottomBarHotkeyScopeFromParent);
+ }
+ }, [
+ bottomBarHotkeyScope,
+ bottomBarHotkeyScopeFromParent,
+ setBottomBarHotkeyScope,
+ ]);
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts
new file mode 100644
index 000000000000..89144d6c8c30
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/bottomBarHotkeyComponentState.ts
@@ -0,0 +1,11 @@
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+
+export const bottomBarHotkeyComponentState = createComponentStateV2<
+ HotkeyScope | null | undefined
+>({
+ key: 'bottomBarHotkeyComponentState',
+ defaultValue: null,
+ componentInstanceContext: BottomBarInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx
new file mode 100644
index 000000000000..e2b29e54cfc3
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext.tsx
@@ -0,0 +1,3 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const BottomBarInstanceContext = createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts
new file mode 100644
index 000000000000..071ad8de53cf
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState.ts
@@ -0,0 +1,8 @@
+import { BottomBarInstanceContext } from '@/ui/layout/bottom-bar/states/contexts/BottomBarInstanceContext';
+import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
+
+export const isBottomBarOpenedComponentState = createComponentStateV2({
+ key: 'isBottomBarOpenedComponentState',
+ defaultValue: false,
+ componentInstanceContext: BottomBarInstanceContext,
+});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx
deleted file mode 100644
index c020f42709c6..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import styled from '@emotion/styled';
-import { useEffect, useRef } from 'react';
-import { useRecoilValue, useSetRecoilState } from 'recoil';
-
-import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
-import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-import { isDefined } from '~/utils/isDefined';
-import { ActionBarItem } from './ActionBarItem';
-
-type ActionBarProps = {
- selectedIds?: string[];
- totalNumberOfSelectedRecords?: number;
-};
-
-const StyledContainerActionBar = styled.div`
- align-items: center;
- background: ${({ theme }) => theme.background.secondary};
- border: 1px solid ${({ theme }) => theme.border.color.medium};
- border-radius: ${({ theme }) => theme.border.radius.md};
- bottom: 38px;
- box-shadow: ${({ theme }) => theme.boxShadow.strong};
- display: flex;
- height: 48px;
- width: max-content;
- left: 50%;
- padding-left: ${({ theme }) => theme.spacing(2)};
- padding-right: ${({ theme }) => theme.spacing(2)};
- position: absolute;
- top: auto;
-
- transform: translateX(-50%);
- z-index: 1;
-`;
-
-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 ActionBar = ({
- selectedIds = [],
- totalNumberOfSelectedRecords,
-}: ActionBarProps) => {
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
-
- useEffect(() => {
- if (selectedIds && selectedIds.length > 1) {
- setContextMenuOpenState(false);
- }
- }, [selectedIds, setContextMenuOpenState]);
-
- const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
- const actionBarEntries = useRecoilValue(actionBarEntriesState);
- const wrapperRef = useRef(null);
-
- if (contextMenuIsOpen) {
- return null;
- }
-
- const selectedNumberLabel =
- totalNumberOfSelectedRecords ?? selectedIds?.length;
-
- const showSelectedNumberLabel =
- isDefined(totalNumberOfSelectedRecords) || Array.isArray(selectedIds);
-
- return (
- <>
-
- {showSelectedNumberLabel && (
- {selectedNumberLabel} selected:
- )}
- {actionBarEntries.map((item, index) => (
-
- ))}
-
-
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx
deleted file mode 100644
index dbb7623d25d4..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { IconChevronDown } from 'twenty-ui';
-
-import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
-import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
-
-type ActionBarItemProps = {
- item: ActionBarEntry;
-};
-
-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 ActionBarItem = ({ item }: ActionBarItemProps) => {
- const theme = useTheme();
- const dropdownId = `action-bar-item-${item.label}`;
- const { toggleDropdown, closeDropdown } = useDropdown(dropdownId);
- return (
- <>
- {Array.isArray(item.subActions) ? (
-
- {item.Icon && }
- {item.label}
-
-
- }
- dropdownComponents={
-
- {item.subActions.map((subAction) => (
- {
- closeDropdown();
- subAction.onClick?.();
- }}
- />
- ))}
-
- }
- />
- ) : (
- item.onClick?.()}
- >
- {item.Icon && }
- {item.label}
-
- )}
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx
deleted file mode 100644
index 9610eb43b311..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { useSetRecoilState } from 'recoil';
-import { ComponentDecorator } from 'twenty-ui';
-
-import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
-import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
-
-import { actionBarOpenState } from '../../states/actionBarIsOpenState';
-import { ActionBar } from '../ActionBar';
-
-const FilledActionBar = () => {
- const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
- setActionBarOpenState(true);
- return ;
-};
-
-const meta: Meta = {
- title: 'UI/Navigation/ActionBar/ActionBar',
- component: FilledActionBar,
- decorators: [
- MemoryRouterDecorator,
- (Story) => (
- {}}
- >
-
-
- ),
- ComponentDecorator,
- ],
- args: { selectedIds: ['TestId'] },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts
deleted file mode 100644
index 35f8cab412b7..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarEntriesState.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { ActionBarEntry } from '../types/ActionBarEntry';
-
-export const actionBarEntriesState = createState({
- key: 'actionBarEntriesState',
- defaultValue: [],
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts
deleted file mode 100644
index 0ef918e6652e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/states/actionBarIsOpenState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createState } from 'twenty-ui';
-
-export const actionBarOpenState = createState({
- key: 'actionBarOpenState',
- defaultValue: false,
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts
deleted file mode 100644
index a276736cfb1c..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
-
-export type ActionBarEntry = ContextMenuEntry & {
- subActions?: ActionBarEntry[];
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx
deleted file mode 100644
index e27c6096cb07..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { useRef } from 'react';
-import styled from '@emotion/styled';
-import { useRecoilValue } from 'recoil';
-
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
-import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
-import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
-import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-import { contextMenuEntriesState } from '../states/contextMenuEntriesState';
-import { contextMenuIsOpenState } from '../states/contextMenuIsOpenState';
-import { PositionType } from '../types/PositionType';
-
-import { ContextMenuItem } from './ContextMenuItem';
-
-type StyledContainerProps = {
- position: PositionType;
-};
-
-const StyledContainerContextMenu = 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;
- gap: 1px;
-
- left: ${(props) => `${props.position.x}px`};
- position: fixed;
- top: ${(props) => `${props.position.y}px`};
-
- transform: translateX(-50%);
- width: auto;
- z-index: 2;
-`;
-
-export const ContextMenu = () => {
- const contextMenuPosition = useRecoilValue(contextMenuPositionState);
- const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
- const contextMenuEntries = useRecoilValue(contextMenuEntriesState);
- const wrapperRef = useRef(null);
- const actionBarEntries = useRecoilValue(actionBarEntriesState);
-
- if (!contextMenuIsOpen) {
- return null;
- }
-
- const width = contextMenuEntries.some(
- (contextMenuEntry) => contextMenuEntry.label === 'Remove from favorites',
- )
- ? 200
- : undefined;
-
- return (
- <>
-
-
-
- {contextMenuEntries.map((item, index) => {
- return ;
- })}
-
-
-
-
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx
deleted file mode 100644
index 4ec9822a6750..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
-import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
-
-type ContextMenuItemProps = {
- item: ContextMenuEntry;
-};
-
-export const ContextMenuItem = ({ item }: ContextMenuItemProps) => (
-
-);
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx
deleted file mode 100644
index 2b2f29e5c4ee..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { useSetRecoilState } from 'recoil';
-import { ComponentDecorator } from 'twenty-ui';
-
-import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope';
-import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
-
-import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState';
-import { contextMenuPositionState } from '../../states/contextMenuPositionState';
-import { ContextMenu } from '../ContextMenu';
-
-const FilledContextMenu = () => {
- const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
- setContextMenuPosition({
- x: 100,
- y: 10,
- });
- const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
- setContextMenuOpenState(true);
- return ;
-};
-
-const meta: Meta = {
- title: 'UI/Navigation/ContextMenu/ContextMenu',
- component: FilledContextMenu,
- decorators: [
- MemoryRouterDecorator,
- (Story) => (
- {}}
- >
-
-
- ),
- ComponentDecorator,
- ],
- args: { selectedIds: ['TestId'] },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts
deleted file mode 100644
index 1e22b562187e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuEntriesState.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { ContextMenuEntry } from '../types/ContextMenuEntry';
-
-export const contextMenuEntriesState = createState({
- key: 'contextMenuEntriesState',
- defaultValue: [],
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts
deleted file mode 100644
index d5aec39b905e..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuIsOpenState.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createState } from 'twenty-ui';
-
-export const contextMenuIsOpenState = createState({
- key: 'contextMenuIsOpenState',
- defaultValue: false,
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts
deleted file mode 100644
index a47df13eb017..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/states/contextMenuPositionState.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { createState } from 'twenty-ui';
-
-import { PositionType } from '@/ui/navigation/context-menu/types/PositionType';
-
-export const contextMenuPositionState = createState({
- key: 'contextMenuPositionState',
- defaultValue: {
- x: null,
- y: null,
- },
-});
diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts
deleted file mode 100644
index ceae88f5beea..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuItemAccent.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type ContextMenuItemAccent = 'default' | 'danger';
diff --git a/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx
deleted file mode 100644
index becc1b72ab66..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/shared/__stories__/NavigationModal.stories.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-import { IconTrash } from 'twenty-ui';
-
-import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
-import SharedNavigationModal from '@/ui/navigation/shared/components/NavigationModal';
-
-const meta: Meta = {
- title: 'UI/Navigation/Shared/SharedNavigationModal',
- component: SharedNavigationModal,
- args: {
- actionBarEntries: [
- {
- ConfirmationModal: (
- {}}
- setIsOpen={() => {}}
- isOpen={false}
- subtitle="Subtitle"
- />
- ),
- Icon: IconTrash,
- label: 'Label',
- onClick: () => {},
- },
- ],
- customClassName: 'customClassName',
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx b/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx
deleted file mode 100644
index eeba07087165..000000000000
--- a/packages/twenty-front/src/modules/ui/navigation/shared/components/NavigationModal.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
-
-type SharedNavigationModalProps = {
- actionBarEntries: ActionBarEntry[];
- customClassName: string;
-};
-
-const SharedNavigationModal = ({
- actionBarEntries,
- customClassName,
-}: SharedNavigationModalProps) => {
- return (
-
- {actionBarEntries.map((actionBarEntry, index) =>
- actionBarEntry.ConfirmationModal ? (
-
{actionBarEntry.ConfirmationModal}
- ) : null,
- )}
-
- );
-};
-
-export default SharedNavigationModal;
diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
index 8c1bd4be4b79..6c3a0f96ab81 100644
--- a/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
+++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/utils/isNonTextWritingKey.ts
@@ -35,7 +35,7 @@ export const isNonTextWritingKey = (key: string) => {
'Delete',
'End',
'PageDown',
- 'ContextMenu',
+ 'ActionMenuDropdown',
'PrintScreen',
'BrowserBack',
'BrowserForward',
diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx
new file mode 100644
index 000000000000..e40a00da25ee
--- /dev/null
+++ b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx
@@ -0,0 +1,15 @@
+import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
+import { useEffect } from 'react';
+import { useSetRecoilState } from 'recoil';
+
+export const RecordShowPageEffect = ({ recordId }: { recordId: string }) => {
+ const setContextStoreTargetedRecordIds = useSetRecoilState(
+ contextStoreTargetedRecordIdsState,
+ );
+
+ useEffect(() => {
+ setContextStoreTargetedRecordIds([recordId]);
+ }, [recordId, setContextStoreTargetedRecordIds]);
+
+ return null;
+};
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx
index 8c3f77e08b35..cfc683510006 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx
@@ -43,12 +43,9 @@ export const SettingsObjectNewFieldSelect = () => {
});
const excludedFieldTypes: SettingsFieldType[] = (
[
- FieldMetadataType.Link,
FieldMetadataType.Numeric,
FieldMetadataType.RichText,
FieldMetadataType.Actor,
- FieldMetadataType.Email,
- FieldMetadataType.Phone,
] as const
).filter(isDefined);
diff --git a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
index 11774aba44bf..ba9c9dfeb224 100644
--- a/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
+++ b/packages/twenty-front/src/testing/decorators/RecordTableDecorator.tsx
@@ -24,7 +24,7 @@ export const RecordTableDecorator: Decorator = (Story) => {
onCellMouseEnter: () => {},
onCloseTableCell: () => {},
onOpenTableCell: () => {},
- onContextMenu: () => {},
+ onActionMenuDropdownOpened: () => {},
onMoveFocus: () => {},
onMoveSoftFocusToCell: () => {},
onUpsertRecord: () => {},
diff --git a/packages/twenty-front/src/utils/file/getFileNameAndExtension.ts b/packages/twenty-front/src/utils/file/getFileNameAndExtension.ts
new file mode 100644
index 000000000000..231648cd5c93
--- /dev/null
+++ b/packages/twenty-front/src/utils/file/getFileNameAndExtension.ts
@@ -0,0 +1,8 @@
+export const getFileNameAndExtension = (filenameWithExtension: string) => {
+ const lastDotIndex = filenameWithExtension.lastIndexOf('.');
+
+ return {
+ name: filenameWithExtension.substring(0, lastDotIndex),
+ extension: filenameWithExtension.substring(lastDotIndex),
+ };
+};
diff --git a/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts b/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts
new file mode 100644
index 000000000000..84a8179ef036
--- /dev/null
+++ b/packages/twenty-front/src/utils/string/__tests__/turnIntoEmptyStringIfWhitespacesOnly.test.ts
@@ -0,0 +1,19 @@
+import { turnIntoEmptyStringIfWhitespacesOnly } from '../turnIntoEmptyStringIfWhitespacesOnly';
+
+describe('turnIntoEmptyStringIfWhitespacesOnly', () => {
+ it('should return an empty string for whitespace-only input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' ')).toBe('');
+ expect(turnIntoEmptyStringIfWhitespacesOnly('\t\n ')).toBe('');
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' \n\r\t')).toBe('');
+ });
+
+ it('should return the original string for non-whitespace input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly('hello')).toBe('hello');
+ expect(turnIntoEmptyStringIfWhitespacesOnly(' hello ')).toBe(' hello ');
+ expect(turnIntoEmptyStringIfWhitespacesOnly('123')).toBe('123');
+ });
+
+ it('should handle empty string input', () => {
+ expect(turnIntoEmptyStringIfWhitespacesOnly('')).toBe('');
+ });
+});
diff --git a/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts b/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts
new file mode 100644
index 000000000000..fd1a2e105eaa
--- /dev/null
+++ b/packages/twenty-front/src/utils/string/__tests__/turnIntoUndefinedIfWhitespacesOnly.test.ts
@@ -0,0 +1,19 @@
+import { turnIntoUndefinedIfWhitespacesOnly } from '../turnIntoUndefinedIfWhitespacesOnly';
+
+describe('turnIntoUndefinedIfWhitespacesOnly', () => {
+ it('should return undefined for whitespace-only input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly(' ')).toBeUndefined();
+ expect(turnIntoUndefinedIfWhitespacesOnly('\t\n ')).toBeUndefined();
+ expect(turnIntoUndefinedIfWhitespacesOnly(' \n\r\t')).toBeUndefined();
+ });
+
+ it('should return the original string for non-whitespace input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly('hello')).toBe('hello');
+ expect(turnIntoUndefinedIfWhitespacesOnly(' hello ')).toBe(' hello ');
+ expect(turnIntoUndefinedIfWhitespacesOnly('123')).toBe('123');
+ });
+
+ it('should handle empty string input', () => {
+ expect(turnIntoUndefinedIfWhitespacesOnly('')).toBeUndefined();
+ });
+});
diff --git a/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts b/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
index 1f2d257a5917..5fb13d010bad 100644
--- a/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
+++ b/packages/twenty-front/src/utils/string/turnIntoUndefinedIfWhitespacesOnly.ts
@@ -1,5 +1,5 @@
export const turnIntoUndefinedIfWhitespacesOnly = (
value: string,
): string | undefined => {
- return value.trim() === '' ? undefined : value.trim();
+ return value.trim() === '' ? undefined : value;
};
diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts
index 9d98d33abd75..ec6414db9b94 100644
--- a/packages/twenty-server/src/database/commands/database-command.module.ts
+++ b/packages/twenty-server/src/database/commands/database-command.module.ts
@@ -7,7 +7,6 @@ import { DataSeedDemoWorkspaceCommand } from 'src/database/commands/data-seed-de
import { DataSeedDemoWorkspaceModule } from 'src/database/commands/data-seed-demo-workspace/data-seed-demo-workspace.module';
import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-workspace.command';
import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question';
-import { UpgradeTo0_30CommandModule } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module';
import { UpgradeTo0_31CommandModule } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@@ -47,7 +46,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
DataSeedDemoWorkspaceModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataVersionModule,
- UpgradeTo0_30CommandModule,
UpgradeTo0_31CommandModule,
FeatureFlagModule,
],
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command.ts
deleted file mode 100644
index 9356d7d920c1..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import { InjectRepository } from '@nestjs/typeorm';
-
-import chalk from 'chalk';
-import { isDefined } from 'class-validator';
-import { Command } from 'nest-commander';
-import { Repository } from 'typeorm';
-
-import {
- ActiveWorkspacesCommandOptions,
- ActiveWorkspacesCommandRunner,
-} from 'src/database/commands/active-workspaces.command';
-import { TypeORMService } from 'src/database/typeorm/typeorm.service';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
-import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
-import { ViewService } from 'src/modules/view/services/view.service';
-@Command({
- name: 'upgrade-0.30:fix-email-field-migration',
- description:
- 'Fix migration - delete deprecated email fields and add emails to person views',
-})
-export class FixEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- @InjectRepository(FieldMetadataEntity, 'metadata')
- private readonly fieldMetadataRepository: Repository,
- private readonly fieldMetadataService: FieldMetadataService,
- private readonly typeORMService: TypeORMService,
- private readonly dataSourceService: DataSourceService,
- private readonly viewService: ViewService,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: ActiveWorkspacesCommandOptions,
- workspaceIds: string[],
- ): Promise {
- this.logger.log('Running command to fix migration');
-
- for (const workspaceId of workspaceIds) {
- let dataSourceMetadata;
-
- this.logger.log(`Running command for workspace ${workspaceId}`);
- try {
- dataSourceMetadata =
- await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
- workspaceId,
- );
-
- if (!dataSourceMetadata) {
- throw new Error(
- `Could not find dataSourceMetadata for workspace ${workspaceId}`,
- );
- }
-
- const workspaceDataSource =
- await this.typeORMService.connectToDataSource(dataSourceMetadata);
-
- if (!workspaceDataSource) {
- throw new Error(
- `Could not connect to dataSource for workspace ${workspaceId}`,
- );
- }
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Could not connect to workspace data source for workspace ${workspaceId}`,
- ),
- );
- continue;
- }
-
- try {
- const deprecatedPersonEmailFieldsMetadata =
- await this.fieldMetadataRepository.findBy({
- standardId: PERSON_STANDARD_FIELD_IDS.email,
- workspaceId: workspaceId,
- });
-
- const migratedEmailFieldMetadata = await this.fieldMetadataRepository
- .findBy({
- standardId: PERSON_STANDARD_FIELD_IDS.emails,
- workspaceId: workspaceId,
- })
- .then((fields) => fields[0]);
-
- const personEmailFieldWasMigratedButHasDuplicate =
- deprecatedPersonEmailFieldsMetadata.length > 0 &&
- isDefined(migratedEmailFieldMetadata);
-
- if (!personEmailFieldWasMigratedButHasDuplicate) {
- this.logger.log(
- chalk.yellow('No fields to migrate for workspace ' + workspaceId),
- );
- continue;
- }
-
- for (const deprecatedEmailFieldMetadata of deprecatedPersonEmailFieldsMetadata) {
- await this.fieldMetadataService.deleteOneField(
- { id: deprecatedEmailFieldMetadata.id },
- workspaceId,
- );
- this.logger.log(
- chalk.green(`Deleted email field for workspace ${workspaceId}.`),
- );
- }
-
- const personObjectMetadaIdForWorkspace =
- migratedEmailFieldMetadata.objectMetadataId;
-
- if (!isDefined(personObjectMetadaIdForWorkspace)) {
- this.logger.log(
- chalk.red(
- `Could not find person object for workspace ${workspaceId}. Could not add emails to person view`,
- ),
- );
- continue;
- }
-
- const personViewsIds =
- await this.viewService.getViewsIdsForObjectMetadataId({
- workspaceId,
- objectMetadataId: personObjectMetadaIdForWorkspace as string,
- });
-
- await this.viewService.addFieldToViews({
- workspaceId: workspaceId,
- fieldId: migratedEmailFieldMetadata.id,
- viewsIds: personViewsIds,
- positions: personViewsIds.reduce((acc, personView) => {
- if (!personView.id) {
- return acc;
- }
- acc[personView.id] = 4;
-
- return acc;
- }, []),
- });
- this.logger.log(chalk.green(`Added emails to view ${workspaceId}.`));
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Running command on workspace ${workspaceId} failed with error: ${error}`,
- ),
- );
- continue;
- } finally {
- this.logger.log(
- chalk.green(`Finished running command for workspace ${workspaceId}.`),
- );
- }
-
- this.logger.log(chalk.green(`Command completed!`));
- }
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts
deleted file mode 100644
index f2e98d6600a7..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { InjectRepository } from '@nestjs/typeorm';
-
-import chalk from 'chalk';
-import { Command } from 'nest-commander';
-import { Any, Repository } from 'typeorm';
-
-import {
- ActiveWorkspacesCommandOptions,
- ActiveWorkspacesCommandRunner,
-} from 'src/database/commands/active-workspaces.command';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import {
- FieldMetadataEntity,
- FieldMetadataType,
-} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
-import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
-
-@Command({
- name: 'upgrade-0.30:fix-view-filter-operand-for-date-time',
- description: 'Fix the view filter operand for date time fields',
-})
-export class FixViewFilterOperandForDateTimeCommand extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- @InjectRepository(FieldMetadataEntity, 'metadata')
- private readonly fieldMetadataRepository: Repository,
- private readonly dataSourceService: DataSourceService,
- private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: ActiveWorkspacesCommandOptions,
- workspaceIds: string[],
- ): Promise {
- for (const workspaceId of workspaceIds) {
- try {
- const dataSourceMetadata =
- await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
- workspaceId,
- );
-
- if (!dataSourceMetadata) {
- throw new Error(
- `Could not find dataSourceMetadata for workspace ${workspaceId}`,
- );
- }
-
- const viewFilterRepository =
- await this.twentyORMGlobalManager.getRepositoryForWorkspace(
- workspaceId,
- 'viewFilter',
- );
-
- const dateTimeFieldMetadata = await this.fieldMetadataRepository.find({
- where: {
- workspaceId,
- type: FieldMetadataType.DATE_TIME,
- },
- });
-
- const dateTimeFieldMetadataIds = dateTimeFieldMetadata.map(
- (fieldMetadata) => fieldMetadata.id,
- );
-
- const lessThanUpdatedResult = await viewFilterRepository.update(
- {
- operand: 'lessThan',
- fieldMetadataId: Any(dateTimeFieldMetadataIds),
- },
- {
- operand: 'isBefore',
- },
- );
-
- const greaterThanUpdatedResult = await viewFilterRepository.update(
- {
- operand: 'greaterThan',
- fieldMetadataId: Any(dateTimeFieldMetadataIds),
- },
- {
- operand: 'isAfter',
- },
- );
-
- this.logger.log(
- `Updated ${(lessThanUpdatedResult.affected ?? 0) + (greaterThanUpdatedResult.affected ?? 0)} view filters for workspace ${workspaceId}`,
- );
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Error running command for workspace ${workspaceId}: ${error}`,
- ),
- );
- continue;
- } finally {
- this.logger.log(
- chalk.green(`Finished running command for workspace ${workspaceId}.`),
- );
- }
-
- this.logger.log(chalk.green(`Command completed!`));
- }
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts
deleted file mode 100644
index 2e4969297a6b..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts
+++ /dev/null
@@ -1,360 +0,0 @@
-import { InjectRepository } from '@nestjs/typeorm';
-
-import chalk from 'chalk';
-import { Command } from 'nest-commander';
-import { QueryRunner, Repository } from 'typeorm';
-
-import {
- ActiveWorkspacesCommandOptions,
- ActiveWorkspacesCommandRunner,
-} from 'src/database/commands/active-workspaces.command';
-import { TypeORMService } from 'src/database/typeorm/typeorm.service';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
-import {
- FieldMetadataEntity,
- FieldMetadataType,
-} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
-import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
-import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
-import { computeTableName } from 'src/engine/utils/compute-table-name.util';
-import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
-import { ViewService } from 'src/modules/view/services/view.service';
-import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
-@Command({
- name: 'upgrade-0.30:migrate-email-fields-to-emails',
- description: 'Migrating fields of deprecated type EMAIL to type EMAILS',
-})
-export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- @InjectRepository(FieldMetadataEntity, 'metadata')
- private readonly fieldMetadataRepository: Repository,
- @InjectRepository(ObjectMetadataEntity, 'metadata')
- private readonly objectMetadataRepository: Repository,
- private readonly fieldMetadataService: FieldMetadataService,
- private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
- private readonly typeORMService: TypeORMService,
- private readonly dataSourceService: DataSourceService,
- private readonly viewService: ViewService,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: ActiveWorkspacesCommandOptions,
- workspaceIds: string[],
- ): Promise {
- this.logger.log(
- 'Running command to migrate email type fields to emails type',
- );
-
- for (const workspaceId of workspaceIds) {
- let dataSourceMetadata;
- let workspaceQueryRunner;
-
- this.logger.log(`Running command for workspace ${workspaceId}`);
- try {
- dataSourceMetadata =
- await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
- workspaceId,
- );
-
- if (!dataSourceMetadata) {
- throw new Error(
- `Could not find dataSourceMetadata for workspace ${workspaceId}`,
- );
- }
-
- const workspaceDataSource =
- await this.typeORMService.connectToDataSource(dataSourceMetadata);
-
- if (!workspaceDataSource) {
- throw new Error(
- `Could not connect to dataSource for workspace ${workspaceId}`,
- );
- }
-
- workspaceQueryRunner = workspaceDataSource.createQueryRunner();
-
- await workspaceQueryRunner.connect();
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Could not connect to workspace data source for workspace ${workspaceId}`,
- ),
- );
- continue;
- }
-
- try {
- await this.migratePersonEmailFieldToEmailsField(
- workspaceId,
- workspaceQueryRunner,
- dataSourceMetadata,
- );
-
- const customFieldsWithEmailType =
- await this.fieldMetadataRepository.find({
- where: {
- workspaceId,
- type: FieldMetadataType.EMAIL,
- isCustom: true,
- },
- });
-
- for (const customFieldWithEmailType of customFieldsWithEmailType) {
- const objectMetadata = await this.objectMetadataRepository.findOne({
- where: { id: customFieldWithEmailType.objectMetadataId },
- });
-
- if (!objectMetadata) {
- throw new Error(
- `Could not find objectMetadata for field ${customFieldWithEmailType.name}`,
- );
- }
-
- this.logger.log(
- `Attempting to migrate custom field ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular}.`,
- );
-
- const fieldName = customFieldWithEmailType.name;
- const { id: _id, ...fieldWithEmailTypeWithoutId } =
- customFieldWithEmailType;
-
- const emailDefaultValue = fieldWithEmailTypeWithoutId.defaultValue;
-
- const defaultValueForEmailsField = {
- primaryEmail: emailDefaultValue,
- additionalEmails: null,
- };
-
- try {
- const tmpNewEmailsField = await this.fieldMetadataService.createOne(
- {
- ...fieldWithEmailTypeWithoutId,
- type: FieldMetadataType.EMAILS,
- defaultValue: defaultValueForEmailsField,
- name: `${fieldName}Tmp`,
- } satisfies CreateFieldInput,
- );
-
- const tableName = computeTableName(
- objectMetadata.nameSingular,
- objectMetadata.isCustom,
- );
-
- // Migrate data from email to emails.primaryEmail
- await this.migrateDataWithinTable({
- sourceColumnName: `${customFieldWithEmailType.name}`,
- targetColumnName: `${tmpNewEmailsField.name}PrimaryEmail`,
- tableName,
- workspaceQueryRunner,
- dataSourceMetadata,
- });
-
- // Duplicate email field's views behaviour for new emails field
- await this.viewService.removeFieldFromViews({
- workspaceId: workspaceId,
- fieldId: tmpNewEmailsField.id,
- });
-
- const viewFieldRepository =
- await this.twentyORMGlobalManager.getRepositoryForWorkspace(
- workspaceId,
- 'viewField',
- );
- const viewFieldsWithDeprecatedField =
- await viewFieldRepository.find({
- where: {
- fieldMetadataId: customFieldWithEmailType.id,
- isVisible: true,
- },
- });
-
- await this.viewService.addFieldToViews({
- workspaceId: workspaceId,
- fieldId: tmpNewEmailsField.id,
- viewsIds: viewFieldsWithDeprecatedField
- .filter((viewField) => viewField.viewId !== null)
- .map((viewField) => viewField.viewId as string),
- positions: viewFieldsWithDeprecatedField.reduce(
- (acc, viewField) => {
- if (!viewField.viewId) {
- return acc;
- }
- acc[viewField.viewId] = viewField.position;
-
- return acc;
- },
- [],
- ),
- });
-
- // Delete email field
- await this.fieldMetadataService.deleteOneField(
- { id: customFieldWithEmailType.id },
- workspaceId,
- );
-
- // Rename temporary emails field
- await this.fieldMetadataService.updateOne(tmpNewEmailsField.id, {
- id: tmpNewEmailsField.id,
- workspaceId: tmpNewEmailsField.workspaceId,
- name: `${fieldName}`,
- isCustom: false,
- });
-
- this.logger.log(
- `Migration of ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular} done!`,
- );
- } catch (error) {
- this.logger.log(
- `Failed to migrate field ${customFieldWithEmailType.name} on ${objectMetadata.nameSingular}, rolling back.`,
- );
-
- // Re-create initial field if it was deleted
- const initialField =
- await this.fieldMetadataService.findOneWithinWorkspace(
- workspaceId,
- {
- where: {
- name: `${customFieldWithEmailType.name}`,
- objectMetadataId: customFieldWithEmailType.objectMetadataId,
- },
- },
- );
-
- const tmpNewEmailsField =
- await this.fieldMetadataService.findOneWithinWorkspace(
- workspaceId,
- {
- where: {
- name: `${customFieldWithEmailType.name}Tmp`,
- objectMetadataId: customFieldWithEmailType.objectMetadataId,
- },
- },
- );
-
- if (!initialField) {
- this.logger.log(
- `Re-creating initial Email field ${customFieldWithEmailType.name} but of type emails`, // Cannot create email fields anymore
- );
- const restoredField = await this.fieldMetadataService.createOne({
- ...customFieldWithEmailType,
- defaultValue: defaultValueForEmailsField,
- type: FieldMetadataType.EMAILS,
- });
- const tableName = computeTableName(
- objectMetadata.nameSingular,
- objectMetadata.isCustom,
- );
-
- if (tmpNewEmailsField) {
- this.logger.log(
- `Restoring data in field ${customFieldWithEmailType.name}`,
- );
- await this.migrateDataWithinTable({
- sourceColumnName: `${tmpNewEmailsField.name}PrimaryEmail`,
- targetColumnName: `${restoredField.name}PrimaryEmail`,
- tableName,
- workspaceQueryRunner,
- dataSourceMetadata,
- });
- } else {
- this.logger.log(
- `Failed to restore data in link field ${customFieldWithEmailType.name}`,
- );
- }
- }
-
- if (tmpNewEmailsField) {
- await this.fieldMetadataService.deleteOneField(
- { id: tmpNewEmailsField.id },
- workspaceId,
- );
- }
- }
- }
- } catch (error) {
- await workspaceQueryRunner.release();
-
- this.logger.log(
- chalk.red(
- `Running command on workspace ${workspaceId} failed with error: ${error}`,
- ),
- );
- continue;
- } finally {
- await workspaceQueryRunner.release();
- }
-
- this.logger.log(chalk.green(`Command completed!`));
- }
- }
-
- private async migratePersonEmailFieldToEmailsField(
- workspaceId: string,
- workspaceQueryRunner: any,
- dataSourceMetadata: any,
- ) {
- this.logger.log(`Migrating person email field of type EMAIL to EMAILS`);
-
- const personEmailFieldMetadata = await this.fieldMetadataRepository.findOne(
- {
- where: {
- workspaceId,
- standardId: PERSON_STANDARD_FIELD_IDS.email,
- },
- },
- );
-
- if (!personEmailFieldMetadata) {
- this.logger.log(
- `Could not find person email field with standardId ${PERSON_STANDARD_FIELD_IDS.email}, skipping migration`,
- );
-
- return;
- }
-
- await this.migrateDataWithinTable({
- sourceColumnName: 'email',
- targetColumnName: 'emailsPrimaryEmail',
- tableName: 'person',
- workspaceQueryRunner,
- dataSourceMetadata,
- });
-
- if (personEmailFieldMetadata) {
- await this.fieldMetadataService.deleteOneField(
- {
- id: personEmailFieldMetadata.id,
- },
- workspaceId,
- );
- }
- }
-
- private async migrateDataWithinTable({
- sourceColumnName,
- targetColumnName,
- tableName,
- workspaceQueryRunner,
- dataSourceMetadata,
- }: {
- sourceColumnName: string;
- targetColumnName: string;
- tableName: string;
- workspaceQueryRunner: QueryRunner;
- dataSourceMetadata: DataSourceEntity;
- }) {
- await workspaceQueryRunner.query(
- `UPDATE "${dataSourceMetadata.schema}"."${tableName}" SET "${targetColumnName}" = "${sourceColumnName}"`,
- );
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts
deleted file mode 100644
index 6560bb9625dc..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts
+++ /dev/null
@@ -1,366 +0,0 @@
-import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
-
-import chalk from 'chalk';
-import { isDefined, isEmpty } from 'class-validator';
-import { parsePhoneNumber } from 'libphonenumber-js';
-import { Command } from 'nest-commander';
-import { DataSource, QueryRunner, Repository } from 'typeorm';
-
-import {
- ActiveWorkspacesCommandOptions,
- ActiveWorkspacesCommandRunner,
-} from 'src/database/commands/active-workspaces.command';
-import { TypeORMService } from 'src/database/typeorm/typeorm.service';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
-import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
-import {
- FieldMetadataEntity,
- FieldMetadataType,
-} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
-import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
-import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
-import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
-import { computeTableName } from 'src/engine/utils/compute-table-name.util';
-import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
-import { ViewService } from 'src/modules/view/services/view.service';
-import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
-
-type MigratePhoneFieldsToPhonesCommandOptions = ActiveWorkspacesCommandOptions;
-@Command({
- name: 'upgrade-0.30:migrate-phone-fields-to-phones',
- description: 'Migrating fields of deprecated type PHONE to type PHONES',
-})
-export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- @InjectRepository(FieldMetadataEntity, 'metadata')
- private readonly fieldMetadataRepository: Repository,
- @InjectRepository(ObjectMetadataEntity, 'metadata')
- private readonly objectMetadataRepository: Repository,
- @InjectDataSource('metadata')
- private readonly metadataDataSource: DataSource,
- private readonly fieldMetadataService: FieldMetadataService,
- private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
- private readonly typeORMService: TypeORMService,
- private readonly dataSourceService: DataSourceService,
- private readonly viewService: ViewService,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: MigratePhoneFieldsToPhonesCommandOptions,
- workspaceIds: string[],
- ): Promise {
- this.logger.log(
- 'Running command to migrate phone type fields to phones type',
- );
- for (const workspaceId of workspaceIds) {
- this.logger.log(`Running command for workspace ${workspaceId}`);
- try {
- const dataSourceMetadata =
- await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId(
- workspaceId,
- );
-
- if (!dataSourceMetadata) {
- throw new Error(
- `Could not find dataSourceMetadata for workspace ${workspaceId}`,
- );
- }
- const workspaceDataSource =
- await this.typeORMService.connectToDataSource(dataSourceMetadata);
-
- if (!workspaceDataSource) {
- throw new Error(
- `Could not connect to dataSource for workspace ${workspaceId}`,
- );
- }
- const standardPersonPhoneFieldWithTextType =
- await this.fieldMetadataRepository.findOneBy({
- workspaceId,
- standardId: PERSON_STANDARD_FIELD_IDS.phone,
- });
-
- if (!standardPersonPhoneFieldWithTextType) {
- throw new Error(
- `Could not find standard phone field on person for workspace ${workspaceId}`,
- );
- }
-
- await this.migrateStandardPersonPhoneField({
- standardPersonPhoneField: standardPersonPhoneFieldWithTextType,
- workspaceDataSource,
- workspaceSchemaName: dataSourceMetadata.schema,
- });
-
- const fieldsWithPhoneType = await this.fieldMetadataRepository.find({
- where: {
- workspaceId,
- type: FieldMetadataType.PHONE,
- },
- });
-
- for (const deprecatedPhoneField of fieldsWithPhoneType) {
- await this.migrateCustomPhoneField({
- phoneField: deprecatedPhoneField,
- workspaceDataSource,
- workspaceSchemaName: dataSourceMetadata.schema,
- });
- }
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Field migration on workspace ${workspaceId} failed with error: ${error}`,
- ),
- );
- continue;
- }
- this.logger.log(chalk.green(`Command completed!`));
- }
- }
-
- private async migrateStandardPersonPhoneField({
- standardPersonPhoneField,
- workspaceDataSource,
- workspaceSchemaName,
- }: {
- standardPersonPhoneField: FieldMetadataEntity;
- workspaceDataSource: DataSource;
- workspaceSchemaName: string;
- }) {
- const personObjectMetadata = await this.objectMetadataRepository.findOne({
- where: { id: standardPersonPhoneField.objectMetadataId },
- });
-
- if (!personObjectMetadata) {
- throw new Error(
- `Could not find Person objectMetadata (id ${standardPersonPhoneField.objectMetadataId})`,
- );
- }
-
- this.logger.log(`Attempting to migrate standard person phone field.`);
- const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
-
- await workspaceQueryRunner.connect();
- const { id: _id, ...deprecatedPhoneFieldWithoutId } =
- standardPersonPhoneField;
-
- const workspaceId = standardPersonPhoneField.workspaceId;
-
- try {
- let standardPersonPhonesField =
- await this.fieldMetadataRepository.findOneBy({
- workspaceId,
- standardId: PERSON_STANDARD_FIELD_IDS.phones,
- });
-
- if (!standardPersonPhonesField) {
- standardPersonPhonesField = await this.fieldMetadataService.createOne({
- ...deprecatedPhoneFieldWithoutId,
- label: 'Phones',
- type: FieldMetadataType.PHONES,
- defaultValue: null,
- name: 'phones',
- } satisfies CreateFieldInput);
-
- // StandardId and isCustom are not exposed in CreateFieldInput
- await this.metadataDataSource.query(
- `UPDATE "metadata"."fieldMetadata" SET "standardId" = $1, "isCustom" = $2 where "id"=$3`,
- [
- PERSON_STANDARD_FIELD_IDS.phones,
- 'false',
- standardPersonPhonesField.id,
- ],
- );
-
- await this.viewService.removeFieldFromViews({
- workspaceId: workspaceId,
- fieldId: standardPersonPhonesField.id,
- });
- }
-
- // Copy phone data from Text type to Phones type
- await this.copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({
- workspaceQueryRunner,
- workspaceSchemaName,
- });
-
- // Add (deprecated) to Phone field label
- await this.metadataDataSource.query(
- `UPDATE "metadata"."fieldMetadata" SET "label" = $1 where "id"=$2`,
- ['Phone (deprecated)', standardPersonPhoneField.id],
- );
-
- // Add new phones field to views and hide deprecated phone field
- const viewFieldRepository =
- await this.twentyORMGlobalManager.getRepositoryForWorkspace(
- workspaceId,
- 'viewField',
- );
- const viewFieldsWithDeprecatedPhoneField = await viewFieldRepository.find(
- {
- where: {
- fieldMetadataId: standardPersonPhoneField.id,
- isVisible: true,
- },
- },
- );
-
- await this.viewService.addFieldToViews({
- workspaceId: workspaceId,
- fieldId: standardPersonPhonesField.id,
- viewsIds: viewFieldsWithDeprecatedPhoneField
- .filter((viewField) => viewField.viewId !== null)
- .map((viewField) => viewField.viewId as string),
- positions: viewFieldsWithDeprecatedPhoneField.reduce(
- (acc, viewField) => {
- if (!viewField.viewId) {
- return acc;
- }
- acc[viewField.viewId] = viewField.position;
-
- return acc;
- },
- [],
- ),
- size: 150,
- });
-
- await this.viewService.removeFieldFromViews({
- workspaceId: workspaceId,
- fieldId: standardPersonPhoneField.id,
- });
-
- this.logger.log(
- `Migration of standard person phone field to phones is done!`,
- );
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Failed to migrate field standard person phone field to phones, rolling back. (Error: ${error})`,
- ),
- );
-
- // Delete new phones field if it was created
- const newPhonesField =
- await this.fieldMetadataService.findOneWithinWorkspace(workspaceId, {
- where: {
- name: 'phones',
- objectMetadataId: standardPersonPhoneField.objectMetadataId,
- },
- });
-
- if (newPhonesField) {
- this.logger.log(
- `Deleting phones field of type Phone as part of rolling back.`,
- );
- await this.fieldMetadataService.deleteOneField(
- { id: newPhonesField.id },
- workspaceId,
- );
- }
-
- // Revert Phone field label (remove (deprecated))
- await this.metadataDataSource.query(
- `UPDATE "metadata"."fieldMetadata" SET "label" = $1 where "id"=$2`,
- ['Phone', standardPersonPhoneField.id],
- );
- } finally {
- await workspaceQueryRunner.release();
- }
- }
-
- private async migrateCustomPhoneField({
- phoneField,
- workspaceDataSource,
- workspaceSchemaName,
- }: {
- phoneField: FieldMetadataEntity;
- workspaceDataSource: DataSource;
- workspaceSchemaName: string;
- }) {
- if (!phoneField) return;
- const objectMetadata = await this.objectMetadataRepository.findOne({
- where: { id: phoneField.objectMetadataId },
- });
-
- if (!objectMetadata) {
- throw new Error(
- `Could not find objectMetadata for field ${phoneField.name}`,
- );
- }
- this.logger.log(
- `Attempting to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`,
- );
- const workspaceQueryRunner = workspaceDataSource.createQueryRunner();
-
- await workspaceQueryRunner.connect();
-
- try {
- await this.metadataDataSource.query(
- `UPDATE "metadata"."fieldMetadata" SET "type" = $1 where "id"=$2`,
- [FieldMetadataType.TEXT, phoneField.id],
- );
-
- await workspaceQueryRunner.query(
- `ALTER TABLE "${workspaceSchemaName}"."${computeTableName(objectMetadata.nameSingular, objectMetadata.isCustom)}" ALTER COLUMN "${computeColumnName(phoneField.name)}" TYPE TEXT`,
- );
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Failed to migrate field ${phoneField.name} on ${objectMetadata.nameSingular} from Phone to Text.`,
- ),
- );
- } finally {
- await workspaceQueryRunner.release();
- }
- }
-
- private async copyAndParseDeprecatedPhoneFieldDataIntoNewPhonesField({
- workspaceQueryRunner,
- workspaceSchemaName,
- }: {
- workspaceQueryRunner: QueryRunner;
- workspaceSchemaName: string;
- }) {
- const deprecatedPhoneFieldRows = await workspaceQueryRunner.query(
- `SELECT id, phone FROM "${workspaceSchemaName}"."person" WHERE
- phone IS NOT null`,
- );
-
- for (const row of deprecatedPhoneFieldRows) {
- const phoneColumnValue = row['phone'];
-
- if (isDefined(phoneColumnValue) && !isEmpty(phoneColumnValue)) {
- const query = `UPDATE "${workspaceSchemaName}"."person" SET "phonesPrimaryPhoneCountryCode" = $1,"phonesPrimaryPhoneNumber" = $2 where "id"=$3 AND ("phonesPrimaryPhoneCountryCode" IS NULL OR "phonesPrimaryPhoneCountryCode" = '');`;
-
- try {
- const parsedPhoneColumnValue = parsePhoneNumber(phoneColumnValue);
-
- await workspaceQueryRunner.query(query, [
- `+${parsedPhoneColumnValue.countryCallingCode}`,
- parsedPhoneColumnValue.nationalNumber,
- row.id,
- ]);
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Could not save phone number ${phoneColumnValue}, will try again storing value as is without parsing, with default country code.`,
- ),
- );
- // Store the invalid string for invalid phone numbers
- await workspaceQueryRunner.query(query, [
- '',
- phoneColumnValue,
- row.id,
- ]);
- }
- }
- }
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending.ts
deleted file mode 100644
index 3d1744910f48..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { InjectRepository } from '@nestjs/typeorm';
-
-import chalk from 'chalk';
-import { Command } from 'nest-commander';
-import { IsNull, Repository } from 'typeorm';
-
-import {
- ActiveWorkspacesCommandOptions,
- ActiveWorkspacesCommandRunner,
-} from 'src/database/commands/active-workspaces.command';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
-import {
- MessageChannelSyncStage,
- MessageChannelWorkspaceEntity,
-} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
-@Command({
- name: 'upgrade-0.30:set-stale-message-sync-back-to-pending',
- description: 'Set stale message sync back to pending',
-})
-export class SetStaleMessageSyncBackToPendingCommand extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- _passedParam: string[],
- _options: ActiveWorkspacesCommandOptions,
- workspaceIds: string[],
- ): Promise {
- this.logger.log(
- 'Running command to set stale message sync back to pending',
- );
-
- for (const workspaceId of workspaceIds) {
- this.logger.log(`Running command for workspace ${workspaceId}`);
-
- try {
- const dataSource =
- await this.twentyORMGlobalManager.getDataSourceForWorkspace(
- workspaceId,
- );
-
- const messageChannelRepository =
- await this.twentyORMGlobalManager.getRepositoryForWorkspace(
- workspaceId,
- 'messageChannel',
- );
-
- dataSource.transaction(async (entityManager) => {
- await messageChannelRepository.update(
- {
- syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
- syncStageStartedAt: IsNull(),
- },
- {
- syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_PENDING,
- },
- entityManager,
- );
-
- await messageChannelRepository.update(
- {
- syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
- syncStageStartedAt: IsNull(),
- },
- {
- syncStage:
- MessageChannelSyncStage.PARTIAL_MESSAGE_LIST_FETCH_PENDING,
- },
- entityManager,
- );
- });
- } catch (error) {
- this.logger.log(
- chalk.red(
- `Running command on workspace ${workspaceId} failed with error: ${error}`,
- ),
- );
- continue;
- }
-
- this.logger.log(chalk.green(`Command completed!`));
- }
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts
deleted file mode 100644
index 25ff077ca5e1..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { InjectRepository } from '@nestjs/typeorm';
-
-import { Command } from 'nest-commander';
-import { Repository } from 'typeorm';
-
-import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command';
-import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command';
-import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command';
-import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command';
-import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command';
-import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
-
-interface UpdateTo0_30CommandOptions {
- workspaceId?: string;
-}
-
-@Command({
- name: 'upgrade-0.30',
- description: 'Upgrade to 0.30',
-})
-export class UpgradeTo0_30Command extends ActiveWorkspacesCommandRunner {
- constructor(
- @InjectRepository(Workspace, 'core')
- protected readonly workspaceRepository: Repository,
- private readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
- private readonly migrateEmailFieldsToEmails: MigrateEmailFieldsToEmailsCommand,
- private readonly setStaleMessageSyncBackToPendingCommand: SetStaleMessageSyncBackToPendingCommand,
- private readonly fixEmailFieldsToEmailsCommand: FixEmailFieldsToEmailsCommand,
- private readonly migratePhoneFieldsToPhones: MigratePhoneFieldsToPhonesCommand,
- private readonly fixViewFilterOperandForDateTimeCommand: FixViewFilterOperandForDateTimeCommand,
- ) {
- super(workspaceRepository);
- }
-
- async executeActiveWorkspacesCommand(
- passedParam: string[],
- options: UpdateTo0_30CommandOptions,
- workspaceIds: string[],
- ): Promise {
- await this.syncWorkspaceMetadataCommand.executeActiveWorkspacesCommand(
- passedParam,
- {
- ...options,
- force: true,
- },
- workspaceIds,
- );
- await this.migrateEmailFieldsToEmails.executeActiveWorkspacesCommand(
- passedParam,
- options,
- workspaceIds,
- );
- await this.setStaleMessageSyncBackToPendingCommand.executeActiveWorkspacesCommand(
- passedParam,
- options,
- workspaceIds,
- );
- await this.fixEmailFieldsToEmailsCommand.executeActiveWorkspacesCommand(
- passedParam,
- options,
- workspaceIds,
- );
- await this.migratePhoneFieldsToPhones.executeActiveWorkspacesCommand(
- passedParam,
- options,
- workspaceIds,
- );
- await this.fixViewFilterOperandForDateTimeCommand.executeActiveWorkspacesCommand(
- passedParam,
- options,
- workspaceIds,
- );
- }
-}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts
deleted file mode 100644
index 191bad9352cb..000000000000
--- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Module } from '@nestjs/common';
-import { TypeOrmModule } from '@nestjs/typeorm';
-
-import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command';
-import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command';
-import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command';
-import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command';
-import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending';
-import { UpgradeTo0_30Command } from 'src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command';
-import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
-import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
-import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
-import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module';
-import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
-import { ViewModule } from 'src/modules/view/view.module';
-
-@Module({
- imports: [
- TypeOrmModule.forFeature([Workspace], 'core'),
- WorkspaceSyncMetadataCommandsModule,
- DataSourceModule,
- WorkspaceMetadataVersionModule,
- FieldMetadataModule,
- TypeOrmModule.forFeature(
- [FieldMetadataEntity, ObjectMetadataEntity],
- 'metadata',
- ),
- TypeORMModule,
- ViewModule,
- ],
- providers: [
- UpgradeTo0_30Command,
- MigrateEmailFieldsToEmailsCommand,
- SetStaleMessageSyncBackToPendingCommand,
- FixEmailFieldsToEmailsCommand,
- MigratePhoneFieldsToPhonesCommand,
- FixViewFilterOperandForDateTimeCommand,
- ],
-})
-export class UpgradeTo0_30CommandModule {}
diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
index 013f226635f2..97ce8581f042 100644
--- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
+++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts
@@ -55,11 +55,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
- {
- key: FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- workspaceId: workspaceId,
- value: true,
- },
{
key: FeatureFlagKey.IsSearchEnabled,
workspaceId: workspaceId,
diff --git a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
index 337cdda3e2ae..cc23b7e26a99 100644
--- a/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
+++ b/packages/twenty-server/src/engine/api/__mocks__/object-metadata-item.mock.ts
@@ -104,20 +104,6 @@ const fieldUuidMock = {
defaultValue: null,
};
-const fieldPhoneMock = {
- name: 'fieldPhone',
- type: FieldMetadataType.PHONE,
- isNullable: true,
- defaultValue: null,
-};
-
-const fieldEmailMock = {
- name: 'fieldEmail',
- type: FieldMetadataType.EMAIL,
- isNullable: true,
- defaultValue: null,
-};
-
const fieldDateTimeMock = {
name: 'fieldDateTime',
type: FieldMetadataType.DATE_TIME,
@@ -253,9 +239,7 @@ const fieldPhonesMock = {
export const fields = [
fieldUuidMock,
fieldTextMock,
- fieldPhoneMock,
fieldPhonesMock,
- fieldEmailMock,
fieldEmailsMock,
fieldDateTimeMock,
fieldDateMock,
diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
index e6f3273b9c09..a0fdbf0d377d 100644
--- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts
@@ -107,12 +107,9 @@ export class GraphqlQuerySearchResolverService
options.authContext.workspace.id,
);
- const isQueryRunnerTwentyORMEnabled =
- featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED;
-
const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED;
- if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) {
+ if (!isSearchEnabled) {
throw new GraphqlQueryRunnerException(
'This endpoint is not available yet, please use findMany instead.',
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts
index 06a8d5507b2d..4fe199acfd04 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class CreateManyResolverFactory
@@ -21,8 +18,6 @@ export class CreateManyResolverFactory
public static methodName = 'createMany' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class CreateManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.createMany(args, options);
- }
-
- return await this.workspaceQueryRunnerService.createMany(args, options);
+ return await this.graphqlQueryRunnerService.createMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts
index 5922d05550d6..dce8a669a00e 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class CreateOneResolverFactory
@@ -21,8 +18,6 @@ export class CreateOneResolverFactory
public static methodName = 'createOne' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class CreateOneResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.createOne(args, options);
- }
-
- return await this.workspaceQueryRunnerService.createOne(args, options);
+ return await this.graphqlQueryRunnerService.createOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts
index 4a32ad5ea1c5..abd58e356e69 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class DeleteManyResolverFactory
@@ -21,8 +18,6 @@ export class DeleteManyResolverFactory
public static methodName = 'deleteMany' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class DeleteManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.deleteMany(args, options);
- }
-
- return await this.workspaceQueryRunnerService.deleteMany(args, options);
+ return await this.graphqlQueryRunnerService.deleteMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts
index d58ebe02fd56..5e1b62488609 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class DeleteOneResolverFactory
@@ -21,8 +18,6 @@ export class DeleteOneResolverFactory
public static methodName = 'deleteOne' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class DeleteOneResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.deleteOne(args, options);
- }
-
- return await this.workspaceQueryRunnerService.deleteOne(args, options);
+ return await this.graphqlQueryRunnerService.deleteOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
index e90b93309c6f..26c55baddba1 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class DestroyManyResolverFactory
@@ -21,8 +18,6 @@ export class DestroyManyResolverFactory
public static methodName = 'destroyMany' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,23 +38,7 @@ export class DestroyManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.destroyMany(
- args,
- options,
- );
- }
-
- return await this.workspaceQueryRunnerService.destroyMany(
- args,
- options,
- );
+ return await this.graphqlQueryRunnerService.destroyMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts
index f8b57ad22cca..16af5fa676ec 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class FindDuplicatesResolverFactory
@@ -21,8 +18,6 @@ export class FindDuplicatesResolverFactory
public static methodName = 'findDuplicates' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,20 +38,7 @@ export class FindDuplicatesResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.findDuplicates(
- args,
- options,
- );
- }
-
- return await this.workspaceQueryRunnerService.findDuplicates(
+ return await this.graphqlQueryRunnerService.findDuplicates(
args,
options,
);
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts
index d92210040535..c7d07785790c 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class RestoreManyResolverFactory
@@ -21,8 +18,6 @@ export class RestoreManyResolverFactory
public static methodName = 'restoreMany' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,23 +38,7 @@ export class RestoreManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.restoreMany(
- args,
- options,
- );
- }
-
- return await this.workspaceQueryRunnerService.restoreMany(
- args,
- options,
- );
+ return await this.graphqlQueryRunnerService.restoreMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts
index 11027e4cc4fd..1987ff1dad95 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class UpdateManyResolverFactory
@@ -21,8 +18,6 @@ export class UpdateManyResolverFactory
public static methodName = 'updateMany' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class UpdateManyResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.updateMany(args, options);
- }
-
- return await this.workspaceQueryRunnerService.updateMany(args, options);
+ return await this.graphqlQueryRunnerService.updateMany(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts
index 13a2e4f714d1..09aaf2a3e6ff 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts
@@ -10,9 +10,6 @@ import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-
import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service';
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
-import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
-import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
-import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@Injectable()
export class UpdateOneResolverFactory
@@ -21,8 +18,6 @@ export class UpdateOneResolverFactory
public static methodName = 'updateOne' as const;
constructor(
- private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
- private readonly featureFlagService: FeatureFlagService,
private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService,
) {}
@@ -43,17 +38,7 @@ export class UpdateOneResolverFactory
objectMetadataMapItem: internalContext.objectMetadataMapItem,
};
- const isQueryRunnerTwentyORMEnabled =
- await this.featureFlagService.isFeatureEnabled(
- FeatureFlagKey.IsQueryRunnerTwentyORMEnabled,
- internalContext.authContext.workspace.id,
- );
-
- if (isQueryRunnerTwentyORMEnabled) {
- return await this.graphqlQueryRunnerService.updateOne(args, options);
- }
-
- return await this.workspaceQueryRunnerService.updateOne(args, options);
+ return await this.graphqlQueryRunnerService.updateOne(args, options);
} catch (error) {
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
index fb6597e19be9..dc2fbbfd2a21 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
@@ -61,8 +61,6 @@ export class TypeMapperService {
const typeScalarMapping = new Map([
[FieldMetadataType.UUID, UUIDScalarType],
[FieldMetadataType.TEXT, GraphQLString],
- [FieldMetadataType.PHONE, GraphQLString],
- [FieldMetadataType.EMAIL, GraphQLString],
[FieldMetadataType.DATE_TIME, GraphQLISODateTime],
[FieldMetadataType.DATE, GraphQLISODateTime],
[FieldMetadataType.BOOLEAN, GraphQLBoolean],
@@ -101,8 +99,6 @@ export class TypeMapperService {
>([
[FieldMetadataType.UUID, IDFilterType],
[FieldMetadataType.TEXT, StringFilterType],
- [FieldMetadataType.PHONE, StringFilterType],
- [FieldMetadataType.EMAIL, StringFilterType],
[FieldMetadataType.DATE_TIME, DateFilterType],
[FieldMetadataType.DATE, DateFilterType],
[FieldMetadataType.BOOLEAN, BooleanFilterType],
@@ -129,8 +125,6 @@ export class TypeMapperService {
const typeOrderByMapping = new Map([
[FieldMetadataType.UUID, OrderByDirectionType],
[FieldMetadataType.TEXT, OrderByDirectionType],
- [FieldMetadataType.PHONE, OrderByDirectionType],
- [FieldMetadataType.EMAIL, OrderByDirectionType],
[FieldMetadataType.DATE_TIME, OrderByDirectionType],
[FieldMetadataType.DATE, OrderByDirectionType],
[FieldMetadataType.BOOLEAN, OrderByDirectionType],
diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
index 6976586f5a18..4b2dd84f1e5f 100644
--- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
+++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/map-field-metadata-to-graphql-query.utils.ts
@@ -18,8 +18,6 @@ export const mapFieldMetadataToGraphqlQuery = (
const fieldIsSimpleValue = [
FieldMetadataType.UUID,
FieldMetadataType.TEXT,
- FieldMetadataType.PHONE,
- FieldMetadataType.EMAIL,
FieldMetadataType.DATE_TIME,
FieldMetadataType.DATE,
FieldMetadataType.BOOLEAN,
@@ -89,14 +87,6 @@ export const mapFieldMetadataToGraphqlQuery = (
}
}
}`;
- } else if (fieldType === FieldMetadataType.LINK) {
- return `
- ${field.name}
- {
- label
- url
- }
- `;
} else if (fieldType === FieldMetadataType.LINKS) {
return `
${field.name}
diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts
index c07565936aa9..0a4d5135709f 100644
--- a/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts
+++ b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts
@@ -1,15 +1,14 @@
import { Logger } from '@nestjs/common';
-import { Command, CommandRunner } from 'nest-commander';
+import { Command, CommandRunner, Option } from 'nest-commander';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
-// TODO: implement dry-run
@Command({
name: 'cache:flush',
- description: 'Completely flush cache',
+ description: 'Flush cache for specific keys matching the pattern',
})
export class FlushCacheCommand extends CommandRunner {
private readonly logger = new Logger(FlushCacheCommand.name);
@@ -21,9 +20,28 @@ export class FlushCacheCommand extends CommandRunner {
super();
}
- async run(): Promise {
- this.logger.log('Flushing cache...');
- await this.cacheStorage.flush();
+ async run(
+ passedParams: string[],
+ options?: Record,
+ ): Promise {
+ const pattern = options?.pattern || '*';
+
+ this.logger.log(`Flushing cache for pattern: ${pattern}...`);
+
+ if (pattern === '*') {
+ await this.cacheStorage.flush();
+ } else {
+ await this.cacheStorage.flushByPattern(pattern);
+ }
+
this.logger.log('Cache flushed');
}
+
+ @Option({
+ flags: '-p, --pattern ',
+ description: 'Pattern to flush specific cache keys (e.g., engine:*)',
+ })
+ parsePattern(val: string): string {
+ return val;
+ }
}
diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts
index e5195c18ad8a..7b132c7fbd57 100644
--- a/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts
@@ -1,5 +1,5 @@
-import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
+import { Inject, Injectable } from '@nestjs/common';
import { RedisCache } from 'cache-manager-redis-yet';
@@ -67,6 +67,31 @@ export class CacheStorageService {
return this.cache.reset();
}
+ async flushByPattern(scanPattern: string): Promise {
+ if (!this.isRedisCache()) {
+ throw new Error('flushByPattern is only supported with Redis cache');
+ }
+
+ const redisClient = (this.cache as RedisCache).store.client;
+ let cursor = 0;
+
+ do {
+ const result = await redisClient.scan(cursor, {
+ MATCH: scanPattern,
+ COUNT: 100,
+ });
+
+ const nextCursor = result.cursor;
+ const keys = result.keys;
+
+ if (keys.length > 0) {
+ await redisClient.del(keys);
+ }
+
+ cursor = nextCursor;
+ } while (cursor !== 0);
+ }
+
private isRedisCache() {
return (this.cache.store as any)?.name === 'redis';
}
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
index 5f10e3be0d75..bde4e50992fa 100644
--- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts
@@ -1,20 +1,8 @@
-import {
- fields,
- objectMetadataItemMock,
-} from 'src/engine/api/__mocks__/object-metadata-item.mock';
+import { objectMetadataItemMock } from 'src/engine/api/__mocks__/object-metadata-item.mock';
import { computeSchemaComponents } from 'src/engine/core-modules/open-api/utils/components.utils';
-import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
describe('computeSchemaComponents', () => {
- it('should test all non-deprecated field types', () => {
- expect(fields.map((field) => field.type)).toEqual(
- Object.keys(FieldMetadataType).filter(
- (key) =>
- key !== FieldMetadataType.LINK && key !== FieldMetadataType.TS_VECTOR,
- ),
- );
- });
it('should compute schema components', () => {
expect(
computeSchemaComponents([
@@ -32,9 +20,6 @@ describe('computeSchemaComponents', () => {
fieldText: {
type: 'string',
},
- fieldPhone: {
- type: 'string',
- },
fieldPhones: {
properties: {
additionalPhones: {
@@ -49,10 +34,6 @@ describe('computeSchemaComponents', () => {
},
type: 'object',
},
- fieldEmail: {
- type: 'string',
- format: 'email',
- },
fieldEmails: {
type: 'object',
properties: {
@@ -216,9 +197,6 @@ describe('computeSchemaComponents', () => {
fieldText: {
type: 'string',
},
- fieldPhone: {
- type: 'string',
- },
fieldPhones: {
properties: {
additionalPhones: {
@@ -233,10 +211,6 @@ describe('computeSchemaComponents', () => {
},
type: 'object',
},
- fieldEmail: {
- type: 'string',
- format: 'email',
- },
fieldEmails: {
type: 'object',
properties: {
@@ -399,9 +373,6 @@ describe('computeSchemaComponents', () => {
fieldText: {
type: 'string',
},
- fieldPhone: {
- type: 'string',
- },
fieldPhones: {
properties: {
additionalPhones: {
@@ -416,10 +387,6 @@ describe('computeSchemaComponents', () => {
},
type: 'object',
},
- fieldEmail: {
- type: 'string',
- format: 'email',
- },
fieldEmails: {
type: 'object',
properties: {
diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
index 4b1910e1c08a..07ad4770c782 100644
--- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
+++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts
@@ -56,11 +56,8 @@ const getFieldProperties = (
case FieldMetadataType.UUID:
return { type: 'string', format: 'uuid' };
case FieldMetadataType.TEXT:
- case FieldMetadataType.PHONE:
case FieldMetadataType.RICH_TEXT:
return { type: 'string' };
- case FieldMetadataType.EMAIL:
- return { type: 'string', format: 'email' };
case FieldMetadataType.DATE_TIME:
return { type: 'string', format: 'date-time' };
case FieldMetadataType.DATE:
@@ -139,7 +136,6 @@ const getSchemaComponentsProperties = ({
enum: field.options.map((option: { value: string }) => option.value),
};
break;
- case FieldMetadataType.LINK:
case FieldMetadataType.LINKS:
case FieldMetadataType.CURRENCY:
case FieldMetadataType.FULL_NAME:
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
index de361576a7bb..fbc999d49e70 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/index.ts
@@ -5,7 +5,6 @@ import { addressCompositeType } from 'src/engine/metadata-modules/field-metadata
import { currencyCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type';
import { emailsCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/emails.composite-type';
import { fullNameCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
-import { linkCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/link.composite-type';
import { linksCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type';
import { phonesCompositeType } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@@ -14,7 +13,6 @@ export const compositeTypeDefinitions = new Map<
FieldMetadataType,
CompositeType
>([
- [FieldMetadataType.LINK, linkCompositeType],
[FieldMetadataType.LINKS, linksCompositeType],
[FieldMetadataType.CURRENCY, currencyCompositeType],
[FieldMetadataType.FULL_NAME, fullNameCompositeType],
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts
deleted file mode 100644
index 4037a5d5c9f8..000000000000
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/link.composite-type.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { CompositeType } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
-
-import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-
-export const linkCompositeType: CompositeType = {
- type: FieldMetadataType.LINK,
- properties: [
- {
- name: 'label',
- type: FieldMetadataType.TEXT,
- hidden: false,
- isRequired: false,
- },
- {
- name: 'url',
- type: FieldMetadataType.TEXT,
- hidden: false,
- isRequired: false,
- },
- ],
-};
-
-export type LinkMetadata = {
- label: string;
- url: string;
-};
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
index a617f8ad971e..42d98b4d6e55 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts
@@ -71,16 +71,6 @@ export class FieldMetadataDefaultValueDate {
value: Date | null;
}
-export class FieldMetadataDefaultValueLink {
- @ValidateIf((object, value) => value !== null)
- @IsQuotedString()
- label: string | null;
-
- @ValidateIf((object, value) => value !== null)
- @IsQuotedString()
- url: string | null;
-}
-
export class FieldMetadataDefaultValueCurrency {
@ValidateIf((object, value) => value !== null)
@IsNumberString()
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
index 5035fd3ed760..5ab017f17b12 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts
@@ -24,16 +24,13 @@ import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-met
export enum FieldMetadataType {
UUID = 'UUID',
TEXT = 'TEXT',
- PHONE = 'PHONE',
PHONES = 'PHONES',
- EMAIL = 'EMAIL',
EMAILS = 'EMAILS',
DATE_TIME = 'DATE_TIME',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC',
- LINK = 'LINK',
LINKS = 'LINKS',
CURRENCY = 'CURRENCY',
FULL_NAME = 'FULL_NAME',
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
index ae56a6d3ab60..d38872cf4c5c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts
@@ -145,20 +145,6 @@ export class FieldMetadataService extends TypeOrmQueryService {
it('should generate a nullable value false for TEXT, EMAIL, PHONE no matter what the input is', () => {
expect(generateNullable(FieldMetadataType.TEXT, false)).toEqual(false);
- expect(generateNullable(FieldMetadataType.PHONE, false)).toEqual(false);
- expect(generateNullable(FieldMetadataType.EMAIL, false)).toEqual(false);
expect(generateNullable(FieldMetadataType.TEXT, true)).toEqual(false);
- expect(generateNullable(FieldMetadataType.PHONE, true)).toEqual(false);
- expect(generateNullable(FieldMetadataType.EMAIL, true)).toEqual(false);
expect(generateNullable(FieldMetadataType.TEXT)).toEqual(false);
- expect(generateNullable(FieldMetadataType.PHONE)).toEqual(false);
- expect(generateNullable(FieldMetadataType.EMAIL)).toEqual(false);
});
it('should should return true if no input is given', () => {
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts
index 3695a072ec9c..60e4b04261c3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/validate-default-value-based-on-type.spec.ts
@@ -40,32 +40,6 @@ describe('validateDefaultValueForType', () => {
).toBe(false);
});
- it('should validate string default value for PHONE type', () => {
- expect(
- validateDefaultValueForType(FieldMetadataType.PHONE, "'+123456789'")
- .isValid,
- ).toBe(true);
- });
-
- it('should return false for invalid string default value for PHONE type', () => {
- expect(
- validateDefaultValueForType(FieldMetadataType.PHONE, 123).isValid,
- ).toBe(false);
- });
-
- it('should validate string default value for EMAIL type', () => {
- expect(
- validateDefaultValueForType(FieldMetadataType.EMAIL, "'test@example.com'")
- .isValid,
- ).toBe(true);
- });
-
- it('should return false for invalid string default value for EMAIL type', () => {
- expect(
- validateDefaultValueForType(FieldMetadataType.EMAIL, 123).isValid,
- ).toBe(false);
- });
-
it('should validate number default value for NUMBER type', () => {
expect(
validateDefaultValueForType(FieldMetadataType.NUMBER, 100).isValid,
@@ -90,27 +64,6 @@ describe('validateDefaultValueForType', () => {
).toBe(false);
});
- // LINK type
- it('should validate LINK default value', () => {
- expect(
- validateDefaultValueForType(FieldMetadataType.LINK, {
- label: "'http://example.com'",
- url: "'Example'",
- }).isValid,
- ).toBe(true);
- });
-
- it('should return false for invalid LINK default value', () => {
- expect(
- validateDefaultValueForType(
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-expect-error Just for testing purposes
- { label: 123, url: {} },
- FieldMetadataType.LINK,
- ).isValid,
- ).toBe(false);
- });
-
// CURRENCY type
it('should validate CURRENCY default value', () => {
expect(
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
index 958ff0e3212b..3d9f8bcf368a 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-default-value.ts
@@ -7,8 +7,6 @@ export function generateDefaultValue(
): FieldMetadataDefaultValue {
switch (type) {
case FieldMetadataType.TEXT:
- case FieldMetadataType.PHONE:
- case FieldMetadataType.EMAIL:
return "''";
case FieldMetadataType.EMAILS:
return {
@@ -31,11 +29,6 @@ export function generateDefaultValue(
addressLat: null,
addressLng: null,
};
- case FieldMetadataType.LINK:
- return {
- url: "''",
- label: "''",
- };
case FieldMetadataType.CURRENCY:
return {
amountMicros: null,
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts
index 5724314847b0..759012070ffc 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/generate-nullable.ts
@@ -11,8 +11,6 @@ export function generateNullable(
switch (type) {
case FieldMetadataType.TEXT:
- case FieldMetadataType.PHONE:
- case FieldMetadataType.EMAIL:
return false;
default:
return inputNullableValue ?? true;
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
index 110673bd1cf6..911162c9ae39 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util.ts
@@ -3,7 +3,6 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi
export const isCompositeFieldMetadataType = (
type: FieldMetadataType,
): type is
- | FieldMetadataType.LINK
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME
| FieldMetadataType.ADDRESS
@@ -12,7 +11,6 @@ export const isCompositeFieldMetadataType = (
| FieldMetadataType.EMAILS
| FieldMetadataType.PHONES => {
return [
- FieldMetadataType.LINK,
FieldMetadataType.CURRENCY,
FieldMetadataType.FULL_NAME,
FieldMetadataType.ADDRESS,
diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
index 06303046259d..a5cea6d00482 100644
--- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/validate-default-value-for-type.util.ts
@@ -15,7 +15,6 @@ import {
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueEmails,
FieldMetadataDefaultValueFullName,
- FieldMetadataDefaultValueLink,
FieldMetadataDefaultValueLinks,
FieldMetadataDefaultValueNowFunction,
FieldMetadataDefaultValueNumber,
@@ -34,8 +33,6 @@ export const defaultValueValidatorsMap = {
FieldMetadataDefaultValueUuidFunction,
],
[FieldMetadataType.TEXT]: [FieldMetadataDefaultValueString],
- [FieldMetadataType.PHONE]: [FieldMetadataDefaultValueString],
- [FieldMetadataType.EMAIL]: [FieldMetadataDefaultValueString],
[FieldMetadataType.DATE_TIME]: [
FieldMetadataDefaultValueDateTime,
FieldMetadataDefaultValueNowFunction,
@@ -44,7 +41,6 @@ export const defaultValueValidatorsMap = {
[FieldMetadataType.BOOLEAN]: [FieldMetadataDefaultValueBoolean],
[FieldMetadataType.NUMBER]: [FieldMetadataDefaultValueNumber],
[FieldMetadataType.NUMERIC]: [FieldMetadataDefaultValueString],
- [FieldMetadataType.LINK]: [FieldMetadataDefaultValueLink],
[FieldMetadataType.CURRENCY]: [FieldMetadataDefaultValueCurrency],
[FieldMetadataType.FULL_NAME]: [FieldMetadataDefaultValueFullName],
[FieldMetadataType.RATING]: [FieldMetadataDefaultValueString],
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts
index 643819ddcb7d..7cf5edb0e39d 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory.ts
@@ -21,8 +21,6 @@ import {
export type BasicFieldMetadataType =
| FieldMetadataType.UUID
| FieldMetadataType.TEXT
- | FieldMetadataType.PHONE
- | FieldMetadataType.EMAIL
| FieldMetadataType.NUMERIC
| FieldMetadataType.NUMBER
| FieldMetadataType.BOOLEAN
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
index 41c133621e7e..31738c0225d9 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts
@@ -23,7 +23,6 @@ export type CompositeFieldMetadataType =
| FieldMetadataType.ADDRESS
| FieldMetadataType.CURRENCY
| FieldMetadataType.FULL_NAME
- | FieldMetadataType.LINK
| FieldMetadataType.LINKS
| FieldMetadataType.EMAILS
| FieldMetadataType.PHONES;
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
index 67955d0bb411..46ec14ad3fc1 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts
@@ -18,9 +18,6 @@ export const fieldMetadataTypeToColumnType = (
case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.ARRAY:
return 'text';
- case FieldMetadataType.PHONE:
- case FieldMetadataType.EMAIL:
- return 'varchar';
case FieldMetadataType.NUMERIC:
return 'numeric';
case FieldMetadataType.NUMBER:
diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
index 53aa564e4f82..5df5da052f2b 100644
--- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts
@@ -52,24 +52,6 @@ export class WorkspaceMigrationFactory {
},
},
],
- [
- FieldMetadataType.PHONE,
- {
- factory: this.basicColumnActionFactory,
- options: {
- defaultValue: '',
- },
- },
- ],
- [
- FieldMetadataType.EMAIL,
- {
- factory: this.basicColumnActionFactory,
- options: {
- defaultValue: '',
- },
- },
- ],
[FieldMetadataType.NUMERIC, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.NUMBER, { factory: this.basicColumnActionFactory }],
[FieldMetadataType.POSITION, { factory: this.basicColumnActionFactory }],
@@ -84,7 +66,6 @@ export class WorkspaceMigrationFactory {
FieldMetadataType.MULTI_SELECT,
{ factory: this.enumColumnActionFactory },
],
- [FieldMetadataType.LINK, { factory: this.compositeColumnActionFactory }],
[
FieldMetadataType.CURRENCY,
{ factory: this.compositeColumnActionFactory },
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
index 624504052ce4..764c48237ff6 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
@@ -191,6 +191,9 @@ export const TIMELINE_ACTIVITY_STANDARD_FIELD_IDS = {
opportunity: '20202020-7664-4a35-a3df-580d389fd527',
task: '20202020-b2f5-415c-9135-a31dfe49501b',
note: '20202020-ec55-4135-8da5-3a20badc0156',
+ workflow: '20202020-616c-4ad3-a2e9-c477c341e295',
+ workflowVersion: '20202020-74f1-4711-a129-e14ca0ecd744',
+ workflowRun: '20202020-96f0-401b-9186-a3a0759225ac',
custom: '20202020-4a71-41b0-9f83-9cdcca3f8b14',
linkedRecordCachedName: '20202020-cfdb-4bef-bbce-a29f41230934',
linkedRecordId: '20202020-2e0e-48c0-b445-ee6c1e61687d',
@@ -204,6 +207,8 @@ export const FAVORITE_STANDARD_FIELD_IDS = {
company: '20202020-cff5-4682-8bf9-069169e08279',
opportunity: '20202020-dabc-48e1-8318-2781a2b32aa2',
workflow: '20202020-b11b-4dc8-999a-6bd0a947b463',
+ workflowVersion: '20202020-e1b8-4caf-b55a-3ab4d4cbcd21',
+ workflowRun: '20202020-db5a-4fe4-9a13-9afa22b1e762',
task: '20202020-1b1b-4b3b-8b1b-7f8d6a1d7d5c',
note: '20202020-1f25-43fe-8b00-af212fdde824',
view: '20202020-5a93-4fa9-acce-e73481a0bbdf',
@@ -411,6 +416,7 @@ export const WORKFLOW_STANDARD_FIELD_IDS = {
runs: '20202020-759b-4340-b58b-e73595c4df4f',
eventListeners: '20202020-0229-4c66-832e-035c67579a38',
favorites: '20202020-c554-4c41-be7a-cf9cd4b0d512',
+ timelineActivities: '20202020-906e-486a-a798-131a5f081faf',
};
export const WORKFLOW_RUN_STANDARD_FIELD_IDS = {
@@ -423,6 +429,8 @@ export const WORKFLOW_RUN_STANDARD_FIELD_IDS = {
position: '20202020-7802-4c40-ae89-1f506fe3365c',
createdBy: '20202020-6007-401a-8aa5-e6f38581a6f3',
output: '20202020-7be4-4db2-8ac6-3ff0d740843d',
+ favorites: '20202020-4baf-4604-b899-2f7fcfbbf90d',
+ timelineActivities: '20202020-af4d-4eb0-babc-eb960a45b356',
};
export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = {
@@ -433,6 +441,8 @@ export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = {
position: '20202020-791d-4950-ab28-0e704767ae1c',
runs: '20202020-1d08-46df-901a-85045f18099a',
steps: '20202020-5988-4a64-b94a-1f9b7b989039',
+ favorites: '20202020-b8e0-4e57-928d-b51671cc71f2',
+ timelineActivities: '20202020-fcb0-4695-b17e-3b43a421c633',
};
export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = {
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts
index 8c7594b49db1..8703879e5e64 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts
@@ -73,9 +73,7 @@ describe('getTsVectorColumnExpressionFromFields', () => {
const fields = [nameFullNameField, jobTitleTextField, emailsEmailsField];
const result = getTsVectorColumnExpressionFromFields(fields);
const expected = `
- CASE
- WHEN "deletedAt" IS NULL THEN
- to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' ||
+ to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' ||
COALESCE(
replace(
"emailsPrimaryEmail",
@@ -85,19 +83,8 @@ describe('getTsVectorColumnExpressionFromFields', () => {
''
)
)
- ELSE NULL
- END
`.trim();
expect(result.trim()).toBe(expected);
});
-
- it('should include CASE statement for handling deletedAt', () => {
- const fields = [nameTextField];
- const result = getTsVectorColumnExpressionFromFields(fields);
-
- expect(result).toContain('CASE');
- expect(result).toContain('WHEN "deletedAt" IS NULL THEN');
- expect(result).toContain('ELSE NULL');
- });
});
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts
index 816c7b64d288..83cabf58b278 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts
@@ -23,15 +23,7 @@ export const getTsVectorColumnExpressionFromFields = (
);
const concatenatedExpression = columnExpressions.join(" || ' ' || ");
- const tsVectorExpression = `to_tsvector('simple', ${concatenatedExpression})`;
-
- return `
- CASE
- WHEN "deletedAt" IS NULL THEN
- ${tsVectorExpression}
- ELSE NULL
- END
- `;
+ return `to_tsvector('simple', ${concatenatedExpression})`;
};
const getColumnExpressionsFromField = (
diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts
index e2124ab2a9e6..0bbd2cf5a074 100644
--- a/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts
+++ b/packages/twenty-server/src/modules/calendar/calendar-event-participant-manager/listeners/calendar-event-participant-person.listener.ts
@@ -32,10 +32,7 @@ export class CalendarEventParticipantPersonListener {
>,
) {
for (const eventPayload of payload.events) {
- if (
- eventPayload.properties.after.emails?.primaryEmail === null &&
- eventPayload.properties.after.email === null
- ) {
+ if (eventPayload.properties.after.emails?.primaryEmail === null) {
continue;
}
@@ -44,9 +41,7 @@ export class CalendarEventParticipantPersonListener {
CalendarEventParticipantMatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.after.emails?.primaryEmail ??
- eventPayload.properties.after.email, // TODO
+ email: eventPayload.properties.after.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
@@ -64,16 +59,14 @@ export class CalendarEventParticipantPersonListener {
objectRecordUpdateEventChangedProperties(
eventPayload.properties.before,
eventPayload.properties.after,
- ).includes('email')
+ ).includes('emails')
) {
// TODO: modify this job to take an array of participants to match
await this.messageQueueService.add(
CalendarEventParticipantUnmatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.before.emails?.primaryEmail ??
- eventPayload.properties.before.email,
+ email: eventPayload.properties.before.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
@@ -82,9 +75,7 @@ export class CalendarEventParticipantPersonListener {
CalendarEventParticipantMatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.after.emails?.primaryEmail ??
- eventPayload.properties.after.email,
+ email: eventPayload.properties.after.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
diff --git a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts
index 7b4447b40584..d9fd73740494 100644
--- a/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts
+++ b/packages/twenty-server/src/modules/contact-creation-manager/services/create-company-and-contact.service.ts
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { isDefined } from 'class-validator';
import chunk from 'lodash.chunk';
import compact from 'lodash.compact';
import { Any, EntityManager, Repository } from 'typeorm';
@@ -13,7 +12,6 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
-import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { CONTACTS_CREATION_BATCH_SIZE } from 'src/modules/contact-creation-manager/constants/contacts-creation-batch-size.constant';
@@ -54,13 +52,6 @@ export class CreateCompanyAndContactService {
return [];
}
- const emailsFieldMetadata = await this.fieldMetadataRepository.findOne({
- where: {
- workspaceId: workspaceId,
- standardId: PERSON_STANDARD_FIELD_IDS.emails,
- },
- });
-
const personRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
workspaceId,
@@ -89,16 +80,14 @@ export class CreateCompanyAndContactService {
}
const alreadyCreatedContacts = await personRepository.find({
- where: isDefined(emailsFieldMetadata)
- ? {
- emails: { primaryEmail: Any(uniqueHandles) },
- }
- : { email: Any(uniqueHandles) },
+ where: {
+ emails: { primaryEmail: Any(uniqueHandles) },
+ },
});
- const alreadyCreatedContactEmails: string[] = isDefined(emailsFieldMetadata)
- ? alreadyCreatedContacts?.map(({ emails }) => emails?.primaryEmail)
- : alreadyCreatedContacts?.map(({ email }) => email);
+ const alreadyCreatedContactEmails: string[] = alreadyCreatedContacts?.map(
+ ({ emails }) => emails?.primaryEmail,
+ );
const filteredContactsToCreate = uniqueContacts.filter(
(participant) =>
diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts
index 0aa6b7d3ae4d..db425cda4e09 100644
--- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts
@@ -22,6 +22,8 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
+import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
+import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@@ -128,6 +130,48 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity {
})
workflowId: string;
+ @WorkspaceRelation({
+ standardId: FAVORITE_STANDARD_FIELD_IDS.workflowVersion,
+ type: RelationMetadataType.MANY_TO_ONE,
+ label: 'Workflow',
+ description: 'Favorite workflow version',
+ icon: 'IconSettingsAutomation',
+ inverseSideTarget: () => WorkflowVersionWorkspaceEntity,
+ inverseSideFieldKey: 'favorites',
+ })
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ @WorkspaceIsNullable()
+ workflowVersion: Relation | null;
+
+ @WorkspaceJoinColumn('workflowVersion')
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ workflowVersionId: string;
+
+ @WorkspaceRelation({
+ standardId: FAVORITE_STANDARD_FIELD_IDS.workflowRun,
+ type: RelationMetadataType.MANY_TO_ONE,
+ label: 'Workflow',
+ description: 'Favorite workflow run',
+ icon: 'IconSettingsAutomation',
+ inverseSideTarget: () => WorkflowRunWorkspaceEntity,
+ inverseSideFieldKey: 'favorites',
+ })
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ @WorkspaceIsNullable()
+ workflowRun: Relation | null;
+
+ @WorkspaceJoinColumn('workflowRun')
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ workflowRunId: string;
+
@WorkspaceRelation({
standardId: FAVORITE_STANDARD_FIELD_IDS.task,
type: RelationMetadataType.MANY_TO_ONE,
diff --git a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts
index 195a88ea4e3c..e0dc0dd6e2d3 100644
--- a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts
+++ b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts
@@ -7,7 +7,6 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
-import { PERSON_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity';
import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
@@ -60,35 +59,19 @@ export class MatchParticipantService<
...new Set(participants.map((participant) => participant.handle)),
];
- const emailsFieldMetadata = await this.fieldMetadataRepository.findOne({
- where: {
- workspaceId: workspaceId,
- standardId: PERSON_STANDARD_FIELD_IDS.emails,
- },
- });
-
const personRepository =
await this.twentyORMManager.getRepository(
'person',
);
- const people = emailsFieldMetadata
- ? await personRepository.find(
- {
- where: {
- emails: Any(uniqueParticipantsHandles),
- },
- },
- transactionManager,
- )
- : await personRepository.find(
- {
- where: {
- email: Any(uniqueParticipantsHandles),
- },
- },
- transactionManager,
- );
+ const people = await personRepository.find(
+ {
+ where: {
+ emails: Any(uniqueParticipantsHandles),
+ },
+ },
+ transactionManager,
+ );
const workspaceMemberRepository =
await this.twentyORMManager.getRepository(
@@ -105,10 +88,8 @@ export class MatchParticipantService<
);
for (const handle of uniqueParticipantsHandles) {
- const person = people.find((person) =>
- emailsFieldMetadata
- ? person.emails?.primaryEmail === handle
- : person.email === handle,
+ const person = people.find(
+ (person) => person.emails?.primaryEmail === handle,
);
const workspaceMember = workspaceMembers.find(
diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts
index 1d4a0c6c38aa..53bf3329a2eb 100644
--- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts
+++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-person.listener.ts
@@ -32,10 +32,7 @@ export class MessageParticipantPersonListener {
>,
) {
for (const eventPayload of payload.events) {
- if (
- !eventPayload.properties.after.emails?.primaryEmail &&
- !eventPayload.properties.after.email
- ) {
+ if (!eventPayload.properties.after.emails?.primaryEmail) {
continue;
}
@@ -43,9 +40,7 @@ export class MessageParticipantPersonListener {
MessageParticipantMatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.after.emails?.primaryEmail ??
- eventPayload.properties.after.email,
+ email: eventPayload.properties.after.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
@@ -60,10 +55,6 @@ export class MessageParticipantPersonListener {
) {
for (const eventPayload of payload.events) {
if (
- objectRecordUpdateEventChangedProperties(
- eventPayload.properties.before,
- eventPayload.properties.after,
- ).includes('email') ||
objectRecordUpdateEventChangedProperties(
eventPayload.properties.before,
eventPayload.properties.after,
@@ -73,9 +64,7 @@ export class MessageParticipantPersonListener {
MessageParticipantUnmatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.before.emails?.primaryEmail ??
- eventPayload.properties.before.email,
+ email: eventPayload.properties.before.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
@@ -84,9 +73,7 @@ export class MessageParticipantPersonListener {
MessageParticipantMatchParticipantJob.name,
{
workspaceId: payload.workspaceId,
- email:
- eventPayload.properties.after.emails?.primaryEmail ??
- eventPayload.properties.after.email,
+ email: eventPayload.properties.after.emails?.primaryEmail,
personId: eventPayload.recordId,
},
);
diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts
index d2cccd1d9edd..39fcead68cb7 100644
--- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts
@@ -63,16 +63,6 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsNullable()
[NAME_FIELD_NAME]: FullNameMetadata | null;
- @WorkspaceField({
- standardId: PERSON_STANDARD_FIELD_IDS.email,
- type: FieldMetadataType.EMAIL,
- label: 'Email',
- description: 'Contact’s Email',
- icon: 'IconMail',
- })
- @WorkspaceIsDeprecated()
- email: string;
-
@WorkspaceField({
standardId: PERSON_STANDARD_FIELD_IDS.emails,
type: FieldMetadataType.EMAILS,
diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts
index d683c1c1f012..8d0eff81ac66 100644
--- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts
@@ -1,5 +1,6 @@
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
+import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
@@ -7,6 +8,7 @@ import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-en
import { WorkspaceDynamicRelation } from 'src/engine/twenty-orm/decorators/workspace-dynamic-relation.decorator';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
+import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator';
import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator';
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
@@ -19,6 +21,9 @@ import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.work
import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity';
import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity';
+import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
+import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
+import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@WorkspaceEntity({
@@ -182,6 +187,69 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceJoinColumn('task')
taskId: string | null;
+ @WorkspaceRelation({
+ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflow,
+ type: RelationMetadataType.MANY_TO_ONE,
+ label: 'Workflow',
+ description: 'Event workflow',
+ icon: 'IconTargetArrow',
+ inverseSideTarget: () => WorkflowWorkspaceEntity,
+ inverseSideFieldKey: 'timelineActivities',
+ })
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ @WorkspaceIsNullable()
+ workflow: Relation | null;
+
+ @WorkspaceJoinColumn('workflow')
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ workflowId: string | null;
+
+ @WorkspaceRelation({
+ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowVersion,
+ type: RelationMetadataType.MANY_TO_ONE,
+ label: 'WorkflowVersion',
+ description: 'Event workflow version',
+ icon: 'IconTargetArrow',
+ inverseSideTarget: () => WorkflowVersionWorkspaceEntity,
+ inverseSideFieldKey: 'timelineActivities',
+ })
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ @WorkspaceIsNullable()
+ workflowVersion: Relation | null;
+
+ @WorkspaceJoinColumn('workflowVersion')
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ workflowVersionId: string | null;
+
+ @WorkspaceRelation({
+ standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.workflowRun,
+ type: RelationMetadataType.MANY_TO_ONE,
+ label: 'Workflow Run',
+ description: 'Event workflow run',
+ icon: 'IconTargetArrow',
+ inverseSideTarget: () => WorkflowRunWorkspaceEntity,
+ inverseSideFieldKey: 'timelineActivities',
+ })
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ @WorkspaceIsNullable()
+ workflowRun: Relation | null;
+
+ @WorkspaceJoinColumn('workflowRun')
+ @WorkspaceGate({
+ featureFlag: FeatureFlagKey.IsWorkflowEnabled,
+ })
+ workflowRunId: string | null;
+
@WorkspaceDynamicRelation({
type: RelationMetadataType.MANY_TO_ONE,
argsFactory: (oppositeObjectMetadata) => ({
diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts
index 6d163d03c2e4..d59b593d2185 100644
--- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts
+++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook.ts
@@ -1,9 +1,16 @@
+import { InjectRepository } from '@nestjs/typeorm';
+
+import { Repository } from 'typeorm';
+
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
+import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
+import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
+import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
@@ -17,10 +24,15 @@ import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-ob
export class WorkflowCreateManyPostQueryHook
implements WorkspaceQueryPostHookInstance
{
- constructor(private readonly twentyORMManager: TwentyORMManager) {}
+ constructor(
+ private readonly twentyORMManager: TwentyORMManager,
+ private readonly workspaceEventEmitter: WorkspaceEventEmitter,
+ @InjectRepository(ObjectMetadataEntity, 'metadata')
+ private readonly objectMetadataRepository: Repository,
+ ) {}
async execute(
- _authContext: AuthContext,
+ authContext: AuthContext,
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise {
@@ -29,14 +41,39 @@ export class WorkflowCreateManyPostQueryHook
'workflowVersion',
);
+ const workflowVersionsToCreate = payload.map((workflow) => {
+ return workflowVersionRepository.create({
+ workflowId: workflow.id,
+ status: WorkflowVersionStatus.DRAFT,
+ name: 'v1',
+ });
+ });
+
await Promise.all(
- payload.map((workflow) => {
- return workflowVersionRepository.insert({
- workflowId: workflow.id,
- status: WorkflowVersionStatus.DRAFT,
- name: 'v1',
- });
+ workflowVersionsToCreate.map((workflowVersion) => {
+ return workflowVersionRepository.save(workflowVersion);
+ }),
+ );
+
+ const objectMetadata = await this.objectMetadataRepository.findOneOrFail({
+ where: {
+ nameSingular: 'workflowVersion',
+ },
+ });
+
+ this.workspaceEventEmitter.emit(
+ `workflowVersion.created`,
+ workflowVersionsToCreate.map((workflowVersionToCreate) => {
+ return {
+ userId: authContext.user?.id,
+ recordId: workflowVersionToCreate.id,
+ objectMetadata,
+ properties: {
+ after: workflowVersionToCreate,
+ },
+ } satisfies ObjectRecordCreateEvent;
}),
+ authContext.workspace.id,
);
}
}
diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts
index 78f7f98123be..b9447a0619cf 100644
--- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts
+++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook.ts
@@ -1,9 +1,16 @@
+import { InjectRepository } from '@nestjs/typeorm';
+
+import { Repository } from 'typeorm';
+
import { WorkspaceQueryPostHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
+import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
+import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
+import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
@@ -17,10 +24,15 @@ import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-ob
export class WorkflowCreateOnePostQueryHook
implements WorkspaceQueryPostHookInstance
{
- constructor(private readonly twentyORMManager: TwentyORMManager) {}
+ constructor(
+ private readonly twentyORMManager: TwentyORMManager,
+ private readonly workspaceEventEmitter: WorkspaceEventEmitter,
+ @InjectRepository(ObjectMetadataEntity, 'metadata')
+ private readonly objectMetadataRepository: Repository,
+ ) {}
async execute(
- _authContext: AuthContext,
+ authContext: AuthContext,
_objectName: string,
payload: WorkflowWorkspaceEntity[],
): Promise {
@@ -31,10 +43,33 @@ export class WorkflowCreateOnePostQueryHook
'workflowVersion',
);
- await workflowVersionRepository.insert({
+ const workflowVersionToCreate = await workflowVersionRepository.create({
workflowId: workflow.id,
status: WorkflowVersionStatus.DRAFT,
name: 'v1',
});
+
+ await workflowVersionRepository.save(workflowVersionToCreate);
+
+ const objectMetadata = await this.objectMetadataRepository.findOneOrFail({
+ where: {
+ nameSingular: 'workflowVersion',
+ },
+ });
+
+ this.workspaceEventEmitter.emit(
+ `workflowVersion.created`,
+ [
+ {
+ userId: authContext.user?.id,
+ recordId: workflowVersionToCreate.id,
+ objectMetadata,
+ properties: {
+ after: workflowVersionToCreate,
+ },
+ } satisfies ObjectRecordCreateEvent,
+ ],
+ authContext.workspace.id,
+ );
}
}
diff --git a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
index bbcde310b9c3..870484d75b99 100644
--- a/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
+++ b/packages/twenty-server/src/modules/workflow/common/query-hooks/workflow-query-hook.module.ts
@@ -1,7 +1,11 @@
import { Module } from '@nestjs/common';
+import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
+
+import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkflowCreateManyPostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.post-query.hook';
import { WorkflowCreateManyPreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-many.pre-query.hook';
+import { WorkflowCreateOnePostQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-one.post-query.hook';
import { WorkflowCreateOnePreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-create-one.pre-query.hook';
import { WorkflowRunCreateManyPreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-run-create-many.pre-query.hook';
import { WorkflowRunCreateOnePreQueryHook } from 'src/modules/workflow/common/query-hooks/workflow-run-create-one.pre-query.hook';
@@ -17,6 +21,9 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-version-validation.workspace-service';
@Module({
+ imports: [
+ NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'),
+ ],
providers: [
WorkflowCreateOnePreQueryHook,
WorkflowCreateManyPreQueryHook,
@@ -30,6 +37,7 @@ import { WorkflowVersionValidationWorkspaceService } from 'src/modules/workflow/
WorkflowVersionUpdateManyPreQueryHook,
WorkflowVersionDeleteOnePreQueryHook,
WorkflowVersionDeleteManyPreQueryHook,
+ WorkflowCreateOnePostQueryHook,
WorkflowCreateManyPostQueryHook,
WorkflowVersionValidationWorkspaceService,
WorkflowCommonWorkspaceService,
diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts
index 7994c56a348c..402891c1d186 100644
--- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity.ts
@@ -34,7 +34,6 @@ export class WorkflowEventListenerWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'The workflow event listener name',
- icon: 'IconPhoneCheck',
})
eventName: string;
diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts
index 347536f57a82..aa14605f74a0 100644
--- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts
@@ -6,7 +6,10 @@ import {
FieldActorSource,
} from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
-import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
+import {
+ RelationMetadataType,
+ RelationOnDeleteAction,
+} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator';
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
@@ -17,6 +20,8 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
import { WORKFLOW_RUN_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
+import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
+import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
@@ -45,7 +50,7 @@ export type WorkflowRunOutput = {
labelPlural: 'Workflow Runs',
description: 'A workflow run',
labelIdentifierStandardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name,
- icon: 'IconHistory',
+ icon: 'IconSettingsAutomation',
})
@WorkspaceGate({
featureFlag: FeatureFlagKey.IsWorkflowEnabled,
@@ -56,7 +61,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'Name of the workflow run',
- icon: 'IconText',
+ icon: 'IconSettingsAutomation',
})
name: string;
@@ -134,6 +139,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.RAW_JSON,
label: 'Output',
description: 'Json object to provide output of the workflow run',
+ icon: 'IconText',
})
@WorkspaceIsNullable()
output: WorkflowRunOutput | null;
@@ -177,4 +183,27 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceJoinColumn('workflow')
workflowId: string;
+
+ @WorkspaceRelation({
+ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.favorites,
+ type: RelationMetadataType.ONE_TO_MANY,
+ label: 'Favorites',
+ description: 'Favorites linked to the workflow run',
+ icon: 'IconHeart',
+ inverseSideTarget: () => FavoriteWorkspaceEntity,
+ onDelete: RelationOnDeleteAction.CASCADE,
+ })
+ @WorkspaceIsSystem()
+ favorites: Relation;
+
+ @WorkspaceRelation({
+ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.timelineActivities,
+ type: RelationMetadataType.ONE_TO_MANY,
+ label: 'Timeline Activities',
+ description: 'Timeline activities linked to the run',
+ inverseSideTarget: () => TimelineActivityWorkspaceEntity,
+ onDelete: RelationOnDeleteAction.CASCADE,
+ })
+ @WorkspaceIsSystem()
+ timelineActivities: Relation;
}
diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts
index 5ec2b4ade7da..8e39c789ad27 100644
--- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts
@@ -14,11 +14,10 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator';
import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator';
-import {
- WORKFLOW_RUN_STANDARD_FIELD_IDS,
- WORKFLOW_VERSION_STANDARD_FIELD_IDS,
-} from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
+import { WORKFLOW_VERSION_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
+import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
+import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type';
@@ -64,7 +63,7 @@ const WorkflowVersionStatusOptions = [
labelSingular: 'Workflow Version',
labelPlural: 'Workflow Versions',
description: 'A workflow version',
- icon: 'IconVersions',
+ icon: 'IconSettingsAutomation',
labelIdentifierStandardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.name,
})
@WorkspaceGate({
@@ -76,7 +75,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
type: FieldMetadataType.TEXT,
label: 'Name',
description: 'The workflow version name',
- icon: 'IconVersions',
+ icon: 'IconSettingsAutomation',
})
name: string;
@@ -139,14 +138,37 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
workflowId: string;
@WorkspaceRelation({
- standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.workflowVersion,
+ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.runs,
type: RelationMetadataType.ONE_TO_MANY,
label: 'Runs',
description: 'Workflow runs linked to the version.',
- icon: 'IconVersions',
+ icon: 'IconRun',
inverseSideTarget: () => WorkflowRunWorkspaceEntity,
onDelete: RelationOnDeleteAction.SET_NULL,
})
@WorkspaceIsNullable()
runs: Relation;
+
+ @WorkspaceRelation({
+ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.favorites,
+ type: RelationMetadataType.ONE_TO_MANY,
+ label: 'Favorites',
+ description: 'Favorites linked to the workflow version',
+ icon: 'IconHeart',
+ inverseSideTarget: () => FavoriteWorkspaceEntity,
+ onDelete: RelationOnDeleteAction.CASCADE,
+ })
+ @WorkspaceIsSystem()
+ favorites: Relation;
+
+ @WorkspaceRelation({
+ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.timelineActivities,
+ type: RelationMetadataType.ONE_TO_MANY,
+ label: 'Timeline Activities',
+ description: 'Timeline activities linked to the version',
+ inverseSideTarget: () => TimelineActivityWorkspaceEntity,
+ onDelete: RelationOnDeleteAction.CASCADE,
+ })
+ @WorkspaceIsSystem()
+ timelineActivities: Relation;
}
diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts
index 167f449740ff..0ff94c93408d 100644
--- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts
@@ -16,6 +16,7 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re
import { WORKFLOW_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
+import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
import { WorkflowEventListenerWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-event-listener.workspace-entity';
import { WorkflowRunWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
@@ -109,9 +110,8 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
description: 'Workflow versions linked to the workflow.',
icon: 'IconVersions',
inverseSideTarget: () => WorkflowVersionWorkspaceEntity,
- onDelete: RelationOnDeleteAction.SET_NULL,
+ onDelete: RelationOnDeleteAction.CASCADE,
})
- @WorkspaceIsNullable()
versions: Relation;
@WorkspaceRelation({
@@ -119,34 +119,43 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
type: RelationMetadataType.ONE_TO_MANY,
label: 'Runs',
description: 'Workflow runs linked to the workflow.',
- icon: 'IconVersions',
+ icon: 'IconRun',
inverseSideTarget: () => WorkflowRunWorkspaceEntity,
- onDelete: RelationOnDeleteAction.SET_NULL,
+ onDelete: RelationOnDeleteAction.CASCADE,
})
- @WorkspaceIsNullable()
- runs: Relation;
+ runs: Relation;
@WorkspaceRelation({
standardId: WORKFLOW_STANDARD_FIELD_IDS.eventListeners,
type: RelationMetadataType.ONE_TO_MANY,
label: 'Event Listeners',
description: 'Workflow event listeners linked to the workflow.',
- icon: 'IconVersions',
inverseSideTarget: () => WorkflowEventListenerWorkspaceEntity,
- onDelete: RelationOnDeleteAction.SET_NULL,
+ onDelete: RelationOnDeleteAction.CASCADE,
})
- @WorkspaceIsNullable()
+ @WorkspaceIsSystem()
eventListeners: Relation;
@WorkspaceRelation({
standardId: WORKFLOW_STANDARD_FIELD_IDS.favorites,
type: RelationMetadataType.ONE_TO_MANY,
label: 'Favorites',
- description: 'Favorites linked to the contact',
+ description: 'Favorites linked to the workflow',
icon: 'IconHeart',
inverseSideTarget: () => FavoriteWorkspaceEntity,
onDelete: RelationOnDeleteAction.CASCADE,
})
@WorkspaceIsSystem()
favorites: Relation;
+
+ @WorkspaceRelation({
+ standardId: WORKFLOW_STANDARD_FIELD_IDS.timelineActivities,
+ type: RelationMetadataType.ONE_TO_MANY,
+ label: 'Timeline Activities',
+ description: 'Timeline activities linked to the workflow',
+ inverseSideTarget: () => TimelineActivityWorkspaceEntity,
+ onDelete: RelationOnDeleteAction.CASCADE,
+ })
+ @WorkspaceIsSystem()
+ timelineActivities: Relation;
}
diff --git a/packages/twenty-zapier/jest.config.ts b/packages/twenty-zapier/jest.config.ts
new file mode 100644
index 000000000000..39406fb49f2c
--- /dev/null
+++ b/packages/twenty-zapier/jest.config.ts
@@ -0,0 +1,9 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ transform: {
+ '^.+\\.ts?$': 'ts-jest',
+ },
+ moduleFileExtensions: ['ts', 'js'],
+ transformIgnorePatterns: ['/node_modules/'],
+};
diff --git a/packages/twenty-zapier/package.json b/packages/twenty-zapier/package.json
index a87732489045..30790678791d 100644
--- a/packages/twenty-zapier/package.json
+++ b/packages/twenty-zapier/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-zapier",
- "version": "1.0.1",
+ "version": "1.0.2",
"description": "Effortlessly sync Twenty with 3000+ apps. Automate tasks, boost productivity, and supercharge your customer relationships!",
"main": "src/index.ts",
"scripts": {
@@ -25,6 +25,7 @@
"convertedByCLIVersion": "15.4.1"
},
"dependencies": {
+ "dotenv": "^16.4.5",
"zapier-platform-core": "15.5.1"
},
"devDependencies": {
diff --git a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts
index 3eb1433cd2c0..4d2d4600ac63 100644
--- a/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts
+++ b/packages/twenty-zapier/src/test/utils/computeInputFields.test.ts
@@ -1,5 +1,5 @@
import { computeInputFields } from '../../utils/computeInputFields';
-import { InputField } from '../../utils/data.types';
+import { FieldMetadataType, InputField } from '../../utils/data.types';
describe('computeInputFields', () => {
test('should create Person input fields properly', () => {
@@ -11,7 +11,7 @@ describe('computeInputFields', () => {
edges: [
{
node: {
- type: 'RELATION',
+ type: FieldMetadataType.RELATION,
name: 'favorites',
label: 'Favorites',
description: 'Favorites linked to the contact',
@@ -21,7 +21,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'CURRENCY',
+ type: FieldMetadataType.CURRENCY,
name: 'annualSalary',
label: 'Annual Salary',
description: 'Annual Salary of the Person',
@@ -31,7 +31,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'jobTitle',
label: 'Job Title',
description: 'Contact’s job title',
@@ -43,7 +43,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'DATE_TIME',
+ type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Update date',
description: null,
@@ -55,7 +55,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'FULL_NAME',
+ type: FieldMetadataType.FULL_NAME,
name: 'name',
label: 'Name',
description: 'Contact’s name',
@@ -68,7 +68,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'UUID',
+ type: FieldMetadataType.UUID,
name: 'id',
label: 'Id',
description: null,
@@ -81,7 +81,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'NUMBER',
+ type: FieldMetadataType.NUMBER,
name: 'recordPosition',
label: 'RecordPosition',
description: 'Record Position',
@@ -91,7 +91,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'LINK',
+ type: FieldMetadataType.LINK,
name: 'xLink',
label: 'X',
description: 'Contact’s X/Twitter account',
@@ -101,7 +101,17 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'EMAIL',
+ type: FieldMetadataType.LINKS,
+ name: 'whatsapp',
+ label: 'Whatsapp',
+ description: 'Contact’s Whatsapp account',
+ isNullable: true,
+ defaultValue: null,
+ },
+ },
+ {
+ node: {
+ type: FieldMetadataType.EMAIL,
name: 'email',
label: 'Email',
description: 'Contact’s Email',
@@ -113,7 +123,7 @@ describe('computeInputFields', () => {
},
{
node: {
- type: 'UUID',
+ type: FieldMetadataType.UUID,
name: 'companyId',
label: 'Company id (foreign key)',
description: 'Contact’s company id foreign key',
@@ -190,6 +200,27 @@ describe('computeInputFields', () => {
helpText: 'Contact’s X/Twitter account: Link Label',
required: false,
},
+ {
+ key: 'whatsapp__url',
+ label: 'Whatsapp: Url',
+ type: 'string',
+ helpText: 'Contact’s Whatsapp account: Link Url',
+ required: false,
+ },
+ {
+ key: 'whatsapp__label',
+ label: 'Whatsapp: Label',
+ type: 'string',
+ helpText: 'Contact’s Whatsapp account: Link Label',
+ required: false,
+ },
+ {
+ key: 'whatsapp__secondaryLinks',
+ label: 'Whatsapp: Secondary Lings',
+ type: 'string',
+ helpText: 'Contact’s Whatsapp account: Link Label',
+ required: false,
+ },
{
key: 'email',
label: 'Email',
diff --git a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts
index 872879900166..d01fb2b38b76 100644
--- a/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts
+++ b/packages/twenty-zapier/src/test/utils/handleQueryParams.test.ts
@@ -14,6 +14,20 @@ describe('utils.handleQueryParams', () => {
domainName: 'Company Domain Name',
linkedinUrl__url: '/linkedin_url',
linkedinUrl__label: 'Test linkedinUrl',
+ whatsapp__primaryLinkUrl: '/whatsapp_url',
+ whatsapp__primaryLinkLabel: 'Whatsapp Link',
+ whatsapp__secondaryLinks: [
+ "{url: '/secondary_whatsapp_url',label: 'Secondary Whatsapp Link'}",
+ ],
+ emails: {
+ primaryEmail: 'primary@email.com',
+ additionalEmails: ['secondary@email.com'],
+ },
+ phones: {
+ primaryPhoneNumber: '322110011',
+ primaryPhoneCountryCode: '+33',
+ additionalPhones: ["{ phoneNumber: '322110012', countryCode: '+33' }"],
+ },
xUrl__url: '/x_url',
xUrl__label: 'Test xUrl',
annualRecurringRevenue: 100000,
@@ -23,9 +37,12 @@ describe('utils.handleQueryParams', () => {
const result = handleQueryParams(inputData);
const expectedResult =
'name: "Company Name", ' +
- 'address: { addressCity: "Paris" }, ' +
+ 'address: {addressCity: "Paris"}, ' +
'domainName: "Company Domain Name", ' +
'linkedinUrl: {url: "/linkedin_url", label: "Test linkedinUrl"}, ' +
+ 'whatsapp: {primaryLinkUrl: "/whatsapp_url", primaryLinkLabel: "Whatsapp Link", secondaryLinks: [{url: \'/secondary_whatsapp_url\',label: \'Secondary Whatsapp Link\'}]}, ' +
+ 'emails: {primaryEmail: "primary@email.com", additionalEmails: ["secondary@email.com"]}, ' +
+ 'phones: {primaryPhoneNumber: "322110011", primaryPhoneCountryCode: "+33", additionalPhones: [{ phoneNumber: \'322110012\', countryCode: \'+33\' }]}, ' +
'xUrl: {url: "/x_url", label: "Test xUrl"}, ' +
'annualRecurringRevenue: 100000, ' +
'idealCustomerProfile: true, ' +
diff --git a/packages/twenty-zapier/src/utils/computeInputFields.ts b/packages/twenty-zapier/src/utils/computeInputFields.ts
index f2eabc331692..dbc24cc134b2 100644
--- a/packages/twenty-zapier/src/utils/computeInputFields.ts
+++ b/packages/twenty-zapier/src/utils/computeInputFields.ts
@@ -5,15 +5,21 @@ import {
NodeField,
} from '../utils/data.types';
+const getListFromFieldMetadataType = (fieldMetadataType: FieldMetadataType) => {
+ return fieldMetadataType === FieldMetadataType.ARRAY;
+};
+
const getTypeFromFieldMetadataType = (
- fieldMetadataType: string,
+ fieldMetadataType: FieldMetadataType,
): string | undefined => {
switch (fieldMetadataType) {
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
+ case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.LINK:
+ case FieldMetadataType.ARRAY:
case FieldMetadataType.RATING:
return 'string';
case FieldMetadataType.DATE_TIME:
@@ -23,6 +29,7 @@ const getTypeFromFieldMetadataType = (
case FieldMetadataType.BOOLEAN:
return 'boolean';
case FieldMetadataType.NUMBER:
+ case FieldMetadataType.POSITION:
return 'integer';
case FieldMetadataType.NUMERIC:
return 'number';
@@ -35,7 +42,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
switch (nodeField.type) {
case FieldMetadataType.FULL_NAME: {
const firstName: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'firstName',
label: 'First Name',
description: 'First Name',
@@ -43,7 +50,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const lastName: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'lastName',
label: 'Last Name',
description: 'Last Name',
@@ -54,7 +61,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
}
case FieldMetadataType.LINK: {
const url: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'url',
label: 'Url',
description: 'Link Url',
@@ -62,7 +69,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const label: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'label',
label: 'Label',
description: 'Link Label',
@@ -73,7 +80,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
}
case FieldMetadataType.CURRENCY: {
const amountMicros: NodeField = {
- type: 'NUMBER',
+ type: FieldMetadataType.NUMBER,
name: 'amountMicros',
label: 'Amount Micros',
description: 'Amount Micros. eg: set 3210000 for 3.21$',
@@ -81,7 +88,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const currencyCode: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'currencyCode',
label: 'Currency Code',
description: 'Currency Code. eg: USD, EUR, etc...',
@@ -92,7 +99,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
}
case FieldMetadataType.ADDRESS: {
const address1: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressStreet1',
label: 'Address',
description: 'Address',
@@ -100,7 +107,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const address2: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressStreet2',
label: 'Address 2',
description: 'Address 2',
@@ -108,7 +115,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const city: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressCity',
label: 'City',
description: 'City',
@@ -116,7 +123,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const state: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressState',
label: 'State',
description: 'State',
@@ -124,7 +131,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const postalCode: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressPostalCode',
label: 'Postal Code',
description: 'Postal Code',
@@ -132,7 +139,7 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
defaultValue: null,
};
const country: NodeField = {
- type: 'TEXT',
+ type: FieldMetadataType.TEXT,
name: 'addressCountry',
label: 'Country',
description: 'Country',
@@ -141,6 +148,84 @@ const get_subfieldsFromField = (nodeField: NodeField): NodeField[] => {
};
return [address1, address2, city, state, postalCode, country];
}
+ case FieldMetadataType.PHONES: {
+ const primaryPhoneNumber: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'primaryPhoneNumber',
+ label: 'Primary Phone Number',
+ description: 'Primary Phone Number. 600112233',
+ isNullable: true,
+ defaultValue: null,
+ };
+ const primaryPhoneCountryCode: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'primaryPhoneCountryCode',
+ label: 'Primary Phone Country Code',
+ description: 'Primary Phone Country Code. eg: +33',
+ isNullable: true,
+ defaultValue: null,
+ };
+ const additionalPhones: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'additionalPhones',
+ label: 'Additional Phones',
+ description: 'Additional Phones',
+ isNullable: true,
+ defaultValue: null,
+ placeholder: '{ number: "", countryCode: "" }',
+ list: true,
+ };
+ return [primaryPhoneNumber, primaryPhoneCountryCode, additionalPhones];
+ }
+ case FieldMetadataType.EMAILS: {
+ const primaryEmail: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'primaryEmail',
+ label: 'Primary Email',
+ description: 'Primary Email',
+ isNullable: true,
+ defaultValue: null,
+ };
+ const additionalEmails: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'additionalEmails',
+ label: 'Additional Emails',
+ description: 'Additional Emails',
+ list: true,
+ isNullable: true,
+ defaultValue: null,
+ };
+ return [primaryEmail, additionalEmails];
+ }
+ case FieldMetadataType.LINKS: {
+ const primaryLinkLabel: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'primaryLinkLabel',
+ label: 'Primary Link Label',
+ description: 'Primary Link Label',
+ isNullable: true,
+ defaultValue: null,
+ };
+ const primaryLinkUrl: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'primaryLinkUrl',
+ label: 'Primary Link Url',
+ description: 'Primary Link Url',
+ isNullable: true,
+ defaultValue: null,
+ };
+ const secondaryLinks: NodeField = {
+ type: FieldMetadataType.TEXT,
+ name: 'secondaryLinks',
+ label: 'Secondary Links',
+ description: 'Secondary Links',
+ isNullable: true,
+ defaultValue: null,
+ placeholder: '{ url: "", label: "" }',
+ list: true,
+ };
+ return [primaryLinkLabel, primaryLinkUrl, secondaryLinks];
+ }
default:
throw new Error(`Unknown nodeField type: ${nodeField.type}`);
}
@@ -161,6 +246,9 @@ export const computeInputFields = (
case FieldMetadataType.FULL_NAME:
case FieldMetadataType.LINK:
case FieldMetadataType.CURRENCY:
+ case FieldMetadataType.PHONES:
+ case FieldMetadataType.EMAILS:
+ case FieldMetadataType.LINKS:
case FieldMetadataType.ADDRESS:
for (const subNodeField of get_subfieldsFromField(nodeField)) {
const field = {
@@ -169,12 +257,15 @@ export const computeInputFields = (
type: getTypeFromFieldMetadataType(subNodeField.type),
helpText: `${nodeField.description}: ${subNodeField.description}`,
required: isFieldRequired(subNodeField),
+ list: !!subNodeField.list,
+ placeholder: subNodeField.placeholder,
} as InputField;
result.push(field);
}
break;
case FieldMetadataType.UUID:
case FieldMetadataType.TEXT:
+ case FieldMetadataType.RICH_TEXT:
case FieldMetadataType.PHONE:
case FieldMetadataType.EMAIL:
case FieldMetadataType.DATE_TIME:
@@ -182,6 +273,8 @@ export const computeInputFields = (
case FieldMetadataType.BOOLEAN:
case FieldMetadataType.NUMBER:
case FieldMetadataType.NUMERIC:
+ case FieldMetadataType.POSITION:
+ case FieldMetadataType.ARRAY:
case FieldMetadataType.RATING: {
const nodeFieldType = getTypeFromFieldMetadataType(nodeField.type);
if (!nodeFieldType) {
@@ -196,6 +289,7 @@ export const computeInputFields = (
type: nodeFieldType,
helpText: nodeField.description,
required,
+ list: getListFromFieldMetadataType(nodeField.type),
};
result.push(field);
break;
diff --git a/packages/twenty-zapier/src/utils/data.types.ts b/packages/twenty-zapier/src/utils/data.types.ts
index ba73ae18e09a..9c6b35d33b4b 100644
--- a/packages/twenty-zapier/src/utils/data.types.ts
+++ b/packages/twenty-zapier/src/utils/data.types.ts
@@ -3,12 +3,14 @@ export type InputData = { [x: string]: any };
export type ObjectData = { id: string } | { [x: string]: any };
export type NodeField = {
- type: string;
+ type: FieldMetadataType;
name: string;
label: string;
description: string | null;
isNullable: boolean;
defaultValue: object | null;
+ list?: boolean;
+ placeholder?: string;
};
export type Node = {
@@ -28,26 +30,39 @@ export type InputField = {
type: string;
helpText: string | null;
required: boolean;
+ list?: boolean;
+ placeholder?: string;
};
export enum FieldMetadataType {
UUID = 'UUID',
TEXT = 'TEXT',
PHONE = 'PHONE',
+ PHONES = 'PHONES',
EMAIL = 'EMAIL',
+ EMAILS = 'EMAILS',
DATE_TIME = 'DATE_TIME',
DATE = 'DATE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMERIC = 'NUMERIC',
LINK = 'LINK',
+ LINKS = 'LINKS',
CURRENCY = 'CURRENCY',
FULL_NAME = 'FULL_NAME',
RATING = 'RATING',
SELECT = 'SELECT',
MULTI_SELECT = 'MULTI_SELECT',
- RELATION = 'RELATION',
+ POSITION = 'POSITION',
ADDRESS = 'ADDRESS',
+ RICH_TEXT = 'RICH_TEXT',
+ ARRAY = 'ARRAY',
+
+ // Ignored fieldTypes
+ RELATION = 'RELATION',
+ RAW_JSON = 'RAW_JSON',
+ ACTOR = 'ACTOR',
+ TS_VECTOR = 'TS_VECTOR',
}
export type Schema = {
diff --git a/packages/twenty-zapier/src/utils/handleQueryParams.ts b/packages/twenty-zapier/src/utils/handleQueryParams.ts
index 5bbb58815656..15039e9e35e6 100644
--- a/packages/twenty-zapier/src/utils/handleQueryParams.ts
+++ b/packages/twenty-zapier/src/utils/handleQueryParams.ts
@@ -1,5 +1,17 @@
import { InputData } from '../utils/data.types';
+const OBJECT_SUBFIELD_NAMES = ['secondaryLinks', 'additionalPhones'];
+
+const formatArrayInputData = (
+ key: string,
+ arrayInputData: InputData,
+): string => {
+ if (OBJECT_SUBFIELD_NAMES.includes(key)) {
+ return `${arrayInputData[key].join('","')}`;
+ }
+ return `"${arrayInputData[key].join('","')}"`;
+};
+
const handleQueryParams = (inputData: InputData): string => {
const formattedInputData: InputData = {};
Object.keys(inputData).forEach((key) => {
@@ -17,7 +29,11 @@ const handleQueryParams = (inputData: InputData): string => {
let result = '';
Object.keys(formattedInputData).forEach((key) => {
let quote = '';
- if (typeof formattedInputData[key] === 'object') {
+ if (Array.isArray(formattedInputData[key])) {
+ result = result.concat(
+ `${key}: [${formatArrayInputData(key, formattedInputData)}], `,
+ );
+ } else if (typeof formattedInputData[key] === 'object') {
result = result.concat(
`${key}: {${handleQueryParams(formattedInputData[key])}}, `,
);
diff --git a/packages/twenty-zapier/tsconfig.json b/packages/twenty-zapier/tsconfig.json
index 1a3eb323e4d1..cebedee143e9 100644
--- a/packages/twenty-zapier/tsconfig.json
+++ b/packages/twenty-zapier/tsconfig.json
@@ -9,5 +9,12 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
- }
+ },
+ "exclude": [
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.test.tsx",
+ "jest.config.ts"
+ ]
}
diff --git a/yarn.lock b/yarn.lock
index a2066d9d3725..025638bc3dc5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24321,7 +24321,7 @@ __metadata:
languageName: node
linkType: hard
-"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0":
+"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.0, dotenv@npm:^16.4.5":
version: 16.4.5
resolution: "dotenv@npm:16.4.5"
checksum: 10c0/48d92870076832af0418b13acd6e5a5a3e83bb00df690d9812e94b24aff62b88ade955ac99a05501305b8dc8f1b0ee7638b18493deb6fe93d680e5220936292f
@@ -43852,6 +43852,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "twenty-zapier@workspace:packages/twenty-zapier"
dependencies:
+ dotenv: "npm:^16.4.5"
jest: "npm:29.7.0"
rimraf: "npm:^3.0.2"
zapier-platform-cli: "npm:^15.4.1"