;
+
+export const Default: Story = {
+ args: {
+ selectedCountryName: 'Canada',
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+
+ await canvas.findByText('Country');
+ await canvas.findByText('Canada');
+ },
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx
index bd30e957d2a6..cc192ddbca91 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/SelectFieldInput.tsx
@@ -60,30 +60,28 @@ export const SelectFieldInput = ({
];
return (
-
- {
- const option = filteredOptions.find(
- (option) => option.value === itemId,
- );
- if (isDefined(option)) {
- onSubmit?.(() => persistField(option.value));
- resetSelectedItem();
- }
- }}
- onOptionSelected={handleSubmit}
- options={fieldDefinition.metadata.options}
- onCancel={onCancel}
- defaultOption={selectedOption}
- onFilterChange={setFilteredOptions}
- onClear={
- fieldDefinition.metadata.isNullable ? handleClearField : undefined
+ {
+ const option = filteredOptions.find(
+ (option) => option.value === itemId,
+ );
+ if (isDefined(option)) {
+ onSubmit?.(() => persistField(option.value));
+ resetSelectedItem();
}
- clearLabel={fieldDefinition.label}
- />
-
+ }}
+ onOptionSelected={handleSubmit}
+ options={fieldDefinition.metadata.options}
+ onCancel={onCancel}
+ defaultOption={selectedOption}
+ onFilterChange={setFilteredOptions}
+ onClear={
+ fieldDefinition.metadata.isNullable ? handleClearField : undefined
+ }
+ clearLabel={fieldDefinition.label}
+ />
);
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts b/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts
index 9c7cc5ff2d9e..626ed968095f 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts
+++ b/packages/twenty-front/src/modules/spreadsheet-import/types/index.ts
@@ -94,7 +94,7 @@ export type SelectOption = {
// Disabled option when already select
disabled?: boolean;
// Option color
- color?: ThemeColor;
+ color?: ThemeColor | 'transparent';
};
export type Input = {
diff --git a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx
index c9ed54603b14..13ac37aa3c7d 100644
--- a/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx
+++ b/packages/twenty-front/src/modules/ui/field/display/components/SelectDisplay.tsx
@@ -1,10 +1,11 @@
-import { Tag, ThemeColor } from 'twenty-ui';
+import { IconComponent, Tag, ThemeColor } from 'twenty-ui';
type SelectDisplayProps = {
- color: ThemeColor;
+ color: ThemeColor | 'transparent';
label: string;
+ Icon?: IconComponent;
};
-export const SelectDisplay = ({ color, label }: SelectDisplayProps) => {
- return ;
+export const SelectDisplay = ({ color, label, Icon }: SelectDisplayProps) => {
+ return ;
};
diff --git a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx
index ac30ef19c81f..c8bc395f633a 100644
--- a/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx
+++ b/packages/twenty-front/src/modules/ui/field/input/components/AddressInput.tsx
@@ -260,6 +260,7 @@ export const AddressInput = ({
onFocus={getFocusHandler('addressPostcode')}
/>
diff --git a/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx b/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx
index 819d3302807f..249692646b11 100644
--- a/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/SelectInput.tsx
@@ -109,7 +109,7 @@ export const SelectInput = ({
selected={false}
text={`No ${clearLabel}`}
color="transparent"
- variant="outline"
+ variant={'outline'}
onClick={() => {
setSelectedOption(undefined);
onClear();
@@ -122,8 +122,9 @@ export const SelectInput = ({
key={option.value}
selected={selectedOption?.value === option.value}
text={option.label}
- color={option.color as TagColor}
+ color={(option.color as TagColor) ?? 'transparent'}
onClick={() => handleOptionChange(option)}
+ LeftIcon={option.icon}
/>
);
})}
diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx
index ce9861e9261f..9d18f21298d0 100644
--- a/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/internal/country/components/CountrySelect.tsx
@@ -6,9 +6,11 @@ import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'
import { Select, SelectOption } from '@/ui/input/components/Select';
export const CountrySelect = ({
+ label,
selectedCountryName,
onChange,
}: {
+ label: string;
selectedCountryName: string;
onChange: (countryCode: string) => void;
}) => {
@@ -36,7 +38,7 @@ export const CountrySelect = ({
fullWidth
dropdownId={SELECT_COUNTRY_DROPDOWN_ID}
options={options}
- label="COUNTRY"
+ label={label}
withSearchInput
onChange={onChange}
value={selectedCountryName}
diff --git a/packages/twenty-ui/src/display/tag/components/Tag.tsx b/packages/twenty-ui/src/display/tag/components/Tag.tsx
index a8ab14dd3ff3..a90ad9875f0a 100644
--- a/packages/twenty-ui/src/display/tag/components/Tag.tsx
+++ b/packages/twenty-ui/src/display/tag/components/Tag.tsx
@@ -27,7 +27,7 @@ const StyledTag = styled.h3<{
border-radius: ${BORDER_COMMON.radius.sm};
color: ${({ color, theme }) =>
color === 'transparent'
- ? theme.font.color.tertiary
+ ? theme.font.color.secondary
: theme.tag.text[color]};
display: inline-flex;
font-size: ${({ theme }) => theme.font.size.md};
diff --git a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelectTag.tsx b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelectTag.tsx
index aec752d80cb0..1217f1199c06 100644
--- a/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelectTag.tsx
+++ b/packages/twenty-ui/src/navigation/menu-item/components/MenuItemSelectTag.tsx
@@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase';
-import { IconCheck, Tag } from '@ui/display';
+import { IconCheck, IconComponent, Tag } from '@ui/display';
import { ThemeColor } from '@ui/theme';
import { StyledMenuItemSelect } from './MenuItemSelect';
@@ -14,6 +14,7 @@ type MenuItemSelectTagProps = {
color: ThemeColor | 'transparent';
text: string;
variant?: 'solid' | 'outline';
+ LeftIcon?: IconComponent | null;
};
export const MenuItemSelectTag = ({
@@ -24,9 +25,9 @@ export const MenuItemSelectTag = ({
onClick,
text,
variant = 'solid',
+ LeftIcon,
}: MenuItemSelectTagProps) => {
const theme = useTheme();
-
return (
-
+
{selected && }
From 07de458fda366464b226e0ee214254b599ae8a16 Mon Sep 17 00:00:00 2001
From: Suhotra Dey <50608734+Lucifer4255@users.noreply.github.com>
Date: Wed, 11 Dec 2024 22:44:46 +0530
Subject: [PATCH 32/58] fix:Hide Scrollbar before Scroll (#8896)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix for the issue #8847 Hide Scrollbar before Scroll.
https://github.com/user-attachments/assets/27dda89f-e3f6-4c72-bcc5-8c7e10d3c823
---------
Co-authored-by: Félix Malfait
Co-authored-by: ehconitin
---
.../scroll/components/ScrollWrapper.tsx | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
index d39ef62d1817..c5140f583a61 100644
--- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
@@ -13,13 +13,14 @@ import { overlayScrollbarsState } from '@/ui/utilities/scroll/states/overlayScro
import 'overlayscrollbars/overlayscrollbars.css';
-const StyledScrollWrapper = styled.div`
+const StyledScrollWrapper = styled.div<{ scrollHide?: boolean }>`
display: flex;
height: 100%;
width: 100%;
.os-scrollbar-handle {
- background-color: ${({ theme }) => theme.border.color.medium};
+ background-color: ${({ theme, scrollHide }) =>
+ scrollHide ? 'transparent' : theme.border.color.medium};
}
`;
@@ -61,10 +62,7 @@ export const ScrollWrapper = ({
const [initialize, instance] = useOverlayScrollbars({
options: {
- scrollbars: {
- autoHide: scrollHide ? 'scroll' : 'never',
- visibility: scrollHide ? 'hidden' : 'visible',
- },
+ scrollbars: { autoHide: 'scroll' },
overflow: {
x: enableXScroll ? undefined : 'hidden',
y: enableYScroll ? undefined : 'hidden',
@@ -92,7 +90,11 @@ export const ScrollWrapper = ({
id: contextProviderName,
}}
>
-
+
{children}
From 183fd877c45a1970c036889717b0df4c29d52ef4 Mon Sep 17 00:00:00 2001
From: khuddite <62555977+khuddite@users.noreply.github.com>
Date: Wed, 11 Dec 2024 12:23:27 -0500
Subject: [PATCH 33/58] Add a confirmation modal for relation object deletion
(#8818)
Fixes #8698
1. Summary
We decided to add a confirmation modal for the relation object deletion.
It's gonna a bit of safety to the user interactions because this action
can be disruptive even though it can be restored.
2. Solution
Used `createPortal` function to address the issue where the vertical
scrollbar shows over the modal. Added a logic that displays a
confirmation modal for deletion in [this
file](https://github.com/twentyhq/twenty/blob/d284419d66cf52e418d6622b9635334486b51040/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx).
I can update the text(title, description, and CTA) as necessary based on
the feedback.
_**However, I observed an issue that the deleted object still shows up
under the list until hard-refresh. I figured that can be addressed as a
separate issue.**_
3. Recording
https://github.com/user-attachments/assets/1a64b702-a915-49f3-a226-2c2d5af8a1d7
---
.../RecordDetailRelationRecordsListItem.tsx | 35 ++++++++++++++++++-
1 file changed, 34 insertions(+), 1 deletion(-)
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 6c9c2ed81d8e..cba3ca9c1c66 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
@@ -1,7 +1,7 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
-import { useCallback, useContext } from 'react';
+import { useCallback, useContext, useState } from 'react';
import {
AnimatedEaseInOut,
IconChevronDown,
@@ -17,6 +17,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
+import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@@ -39,6 +40,8 @@ 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 { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
+import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
+import { createPortal } from 'react-dom';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
const StyledListItem = styled(RecordDetailRecordsListItem)<{
@@ -94,6 +97,9 @@ export const RecordDetailRelationRecordsListItem = ({
}: RecordDetailRelationRecordsListItemProps) => {
const { fieldDefinition } = useContext(FieldContext);
+ const [isDeleteRelationModalOpen, setIsDeleteRelationModalOpen] =
+ useState(false);
+
const {
relationFieldMetadataId,
relationObjectMetadataNameSingular,
@@ -106,6 +112,10 @@ export const RecordDetailRelationRecordsListItem = ({
objectNameSingular: relationObjectMetadataNameSingular,
});
+ const relationObjectTypeName = getObjectTypename(
+ relationObjectMetadataNameSingular,
+ );
+
const { objectMetadataItems } = useObjectMetadataItems();
const persistField = usePersistField();
@@ -158,8 +168,13 @@ export const RecordDetailRelationRecordsListItem = ({
};
const handleDelete = async () => {
+ setIsDeleteRelationModalOpen(true);
closeDropdown();
+ };
+
+ const handleConfirmDelete = async () => {
await deleteOneRelationRecord(relationRecord.id);
+ setIsDeleteRelationModalOpen(false);
};
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
@@ -268,6 +283,24 @@ export const RecordDetailRelationRecordsListItem = ({
)}
+ {createPortal(
+
+ Are you sure you want to delete this related{' '}
+ {relationObjectMetadataNameSingular}?
+
+ This action will break all its relationships with other objects.
+ >
+ }
+ onConfirmClick={handleConfirmDelete}
+ deleteButtonText={`Delete ${relationObjectTypeName}`}
+ />,
+ document.body,
+ )}
>
);
};
From 90c26643a8c2410fecebee75f523ec655606d461 Mon Sep 17 00:00:00 2001
From: Charles Bochet
Date: Wed, 11 Dec 2024 18:56:02 +0100
Subject: [PATCH 34/58] Fix race condition while loading metadata on sign in
(#9027)
---
.../app/components/AppRouterProviders.tsx | 37 ++++++++++---------
.../effect-components/PageChangeEffect.tsx | 22 ++++++++++-
.../modules/auth/components/VerifyEffect.tsx | 7 ++++
.../src/modules/auth/hooks/useAuth.ts | 6 +++
.../components/ObjectMetadataItemsGater.tsx | 19 ++++++++++
...isAppWaitingForFreshObjectMetadataState.ts | 6 +++
.../ui/layout/hooks/useShowAuthModal.ts | 7 +---
.../isDefaultLayoutAuthModalVisibleState.ts | 2 +-
.../twenty-front/src/pages/auth/SignInUp.tsx | 16 ++++----
9 files changed, 89 insertions(+), 33 deletions(-)
create mode 100644 packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsGater.tsx
create mode 100644 packages/twenty-front/src/modules/object-metadata/states/isAppWaitingForFreshObjectMetadataState.ts
diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
index 5ba248a56a66..43ce095f4a0e 100644
--- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
+++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx
@@ -8,6 +8,7 @@ import { ClientConfigProvider } from '@/client-config/components/ClientConfigPro
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
+import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
@@ -15,6 +16,7 @@ import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogMa
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider';
import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
+import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { UserProvider } from '@/users/components/UserProvider';
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
@@ -22,7 +24,6 @@ import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProvide
import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
import { getPageTitleFromPath } from '~/utils/title-utils';
-import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
export const AppRouterProviders = () => {
const { pathname } = useLocation();
@@ -41,22 +42,24 @@ export const AppRouterProviders = () => {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
index 3934d9040d95..b9f692e0eb30 100644
--- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
-import { useRecoilValue } from 'recoil';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
setSessionId,
@@ -8,12 +8,14 @@ import {
} from '@/analytics/hooks/useEventTracker';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
+import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { AppBasePath } from '@/types/AppBasePath';
import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { useDebouncedCallback } from 'use-debounce';
import { useCleanRecoilState } from '~/hooks/useCleanRecoilState';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
@@ -50,11 +52,27 @@ export const PageChangeEffect = () => {
}
}, [location, previousLocation]);
+ const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
+ isAppWaitingForFreshObjectMetadataState,
+ );
+
+ const setIsAppWaitingForFreshObjectMetadataDebounced = useDebouncedCallback(
+ () => {
+ setIsAppWaitingForFreshObjectMetadata(false);
+ },
+ 100,
+ );
+
useEffect(() => {
if (isDefined(pageChangeEffectNavigateLocation)) {
navigate(pageChangeEffectNavigateLocation);
+ setIsAppWaitingForFreshObjectMetadataDebounced();
}
- }, [navigate, pageChangeEffectNavigateLocation]);
+ }, [
+ navigate,
+ pageChangeEffectNavigateLocation,
+ setIsAppWaitingForFreshObjectMetadataDebounced,
+ ]);
useEffect(() => {
switch (true) {
diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx
index a066ddf7ed76..e650104f014f 100644
--- a/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx
+++ b/packages/twenty-front/src/modules/auth/components/VerifyEffect.tsx
@@ -3,7 +3,9 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
+import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { AppPath } from '@/types/AppPath';
+import { useSetRecoilState } from 'recoil';
export const VerifyEffect = () => {
const [searchParams] = useSearchParams();
@@ -14,11 +16,16 @@ export const VerifyEffect = () => {
const { verify } = useAuth();
+ const setIsAppWaitingForFreshObjectMetadata = useSetRecoilState(
+ isAppWaitingForFreshObjectMetadataState,
+ );
+
useEffect(() => {
const getTokens = async () => {
if (!loginToken) {
navigate(AppPath.SignInUp);
} else {
+ setIsAppWaitingForFreshObjectMetadata(true);
await verify(loginToken);
}
};
diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
index 3129976d4371..dc9c51dce915 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
@@ -47,6 +47,7 @@ import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hook
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
+import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@@ -54,6 +55,9 @@ export const useAuth = () => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
+ const setIsAppWaitingForFreshObjectMetadataState = useSetRecoilState(
+ isAppWaitingForFreshObjectMetadataState,
+ );
const setCurrentWorkspaceMembers = useSetRecoilState(
currentWorkspaceMembersState,
);
@@ -240,6 +244,7 @@ export const useAuth = () => {
setWorkspaces(validWorkspaces);
}
+ setIsAppWaitingForFreshObjectMetadataState(true);
return {
user,
@@ -254,6 +259,7 @@ export const useAuth = () => {
setCurrentUser,
setCurrentWorkspace,
isOnAWorkspaceSubdomain,
+ setIsAppWaitingForFreshObjectMetadataState,
setCurrentWorkspaceMembers,
setCurrentWorkspaceMember,
setDateTimeFormat,
diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsGater.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsGater.tsx
new file mode 100644
index 000000000000..4ab14ba953df
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsGater.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { useRecoilValue } from 'recoil';
+
+import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
+import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
+
+export const ObjectMetadataItemsGater = ({
+ children,
+}: React.PropsWithChildren) => {
+ const isAppWaitingForFreshObjectMetadata = useRecoilValue(
+ isAppWaitingForFreshObjectMetadataState,
+ );
+
+ const shouldDisplayChildren = !isAppWaitingForFreshObjectMetadata;
+
+ return (
+ <>{shouldDisplayChildren ? <>{children}> : }>
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/states/isAppWaitingForFreshObjectMetadataState.ts b/packages/twenty-front/src/modules/object-metadata/states/isAppWaitingForFreshObjectMetadataState.ts
new file mode 100644
index 000000000000..2cbe59765fdf
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/states/isAppWaitingForFreshObjectMetadataState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const isAppWaitingForFreshObjectMetadataState = createState({
+ key: 'isAppWaitingForFreshObjectMetadataState',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts b/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts
index f81f5453625f..a12dbf59059b 100644
--- a/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts
+++ b/packages/twenty-front/src/modules/ui/layout/hooks/useShowAuthModal.ts
@@ -21,17 +21,14 @@ export const useShowAuthModal = () => {
);
return useMemo(() => {
- if (isMatchingLocation(AppPath.SignInUp)) {
- return true;
- }
-
if (isMatchingLocation(AppPath.Verify)) {
return false;
}
if (
isMatchingLocation(AppPath.Invite) ||
- isMatchingLocation(AppPath.ResetPassword)
+ isMatchingLocation(AppPath.ResetPassword) ||
+ isMatchingLocation(AppPath.SignInUp)
) {
return isDefaultLayoutAuthModalVisible;
}
diff --git a/packages/twenty-front/src/modules/ui/layout/states/isDefaultLayoutAuthModalVisibleState.ts b/packages/twenty-front/src/modules/ui/layout/states/isDefaultLayoutAuthModalVisibleState.ts
index 95c41986949e..c181a6a7d572 100644
--- a/packages/twenty-front/src/modules/ui/layout/states/isDefaultLayoutAuthModalVisibleState.ts
+++ b/packages/twenty-front/src/modules/ui/layout/states/isDefaultLayoutAuthModalVisibleState.ts
@@ -2,5 +2,5 @@ import { createState } from 'twenty-ui';
export const isDefaultLayoutAuthModalVisibleState = createState({
key: 'isDefaultLayoutAuthModalVisibleState',
- defaultValue: false,
+ defaultValue: true,
});
diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx
index 20b76a3ee010..2c8a1f6b843c 100644
--- a/packages/twenty-front/src/pages/auth/SignInUp.tsx
+++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx
@@ -5,21 +5,21 @@ import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
-import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
-import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
-import { AnimatedEaseIn } from 'twenty-ui';
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
-import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
-import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
+import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
+import { SignInUpGlobalScopeForm } from '@/auth/sign-in-up/components/SignInUpGlobalScopeForm';
import { SignInUpSSOIdentityProviderSelection } from '@/auth/sign-in-up/components/SignInUpSSOIdentityProviderSelection';
-import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
-import { useMemo } from 'react';
-import { isDefined } from '~/utils/isDefined';
+import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
+import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useGetPublicWorkspaceDataBySubdomain } from '@/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain';
+import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
+import { useMemo } from 'react';
+import { AnimatedEaseIn } from 'twenty-ui';
+import { isDefined } from '~/utils/isDefined';
export const SignInUp = () => {
const { form } = useSignInUpForm();
From c776179eccc807b32402d42af344407e4080d85f Mon Sep 17 00:00:00 2001
From: Weiko
Date: Wed, 11 Dec 2024 19:23:51 +0100
Subject: [PATCH 35/58] start 0.40.0 canary (#9029)
---
packages/twenty-emails/package.json | 2 +-
packages/twenty-front/package.json | 2 +-
packages/twenty-server/package.json | 2 +-
packages/twenty-ui/package.json | 2 +-
packages/twenty-website/package.json | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json
index 4a7581a2d7f9..9887a8b94024 100644
--- a/packages/twenty-emails/package.json
+++ b/packages/twenty-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-emails",
- "version": "0.34.0-canary",
+ "version": "0.40.0-canary",
"description": "",
"author": "",
"private": true,
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 32f54997ed30..dd610f0d60e3 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-front",
- "version": "0.34.0-canary",
+ "version": "0.40.0-canary",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json
index fe2ddacf7d4b..6de9253d6e35 100644
--- a/packages/twenty-server/package.json
+++ b/packages/twenty-server/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-server",
- "version": "0.34.0-canary",
+ "version": "0.40.0-canary",
"description": "",
"author": "",
"private": true,
diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json
index 7a995aba0540..ec65a2cced58 100644
--- a/packages/twenty-ui/package.json
+++ b/packages/twenty-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-ui",
- "version": "0.34.0-canary",
+ "version": "0.40.0-canary",
"type": "module",
"main": "./src/index.ts",
"exports": {
diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json
index 7e03c6872332..0030a953ff7f 100644
--- a/packages/twenty-website/package.json
+++ b/packages/twenty-website/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-website",
- "version": "0.34.0-canary",
+ "version": "0.40.0-canary",
"private": true,
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js",
From bce5be85a398c9c089dabf82aea3de44bdde7153 Mon Sep 17 00:00:00 2001
From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com>
Date: Thu, 12 Dec 2024 04:00:39 -0300
Subject: [PATCH 36/58] add info to customer table and stripe customer data
(#9004)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Solves (https://github.com/twentyhq/private-issues/issues/194)
**TLDR**
Updates the billingCustomer table data using stripe webhooks event, also
updates the customer's metadata in stripe, in order to contain the
workspaceId associated to this customer.
**In order to test**
Billing:
- Set IS_BILLING_ENABLED to true
- Add your BILLING_STRIPE_SECRET and BILLING_STRIPE_API_KEY
- Add your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID (use the one in testMode
> Base Plan)
-
Authenticate with your account in the stripe CLI
Run the command: stripe listen --forward-to
http://localhost:3000/billing/webhooks
Run the twenty workker
Authenticate yourself on the app choose a plan and run the app normally.
In stripe and in posgress the customer table data should be added.
**Next steps**
Learn more about integrations tests and implement some for this PR.
---------
Co-authored-by: Félix Malfait
---
.../billing/billing.controller.ts | 11 +-
.../core-modules/billing/billing.module.ts | 9 +-
.../enums/billing-webhook-events.enum.ts | 3 +
...ts => update-subscription-quantity.job.ts} | 10 +-
.../billing-workspace-member.listener.ts | 14 +-
.../billing-portal.workspace-service.ts | 4 +-
.../billing-webhook-entitlement.service.ts | 52 ++++++
.../billing-webhook-subscription.service.ts | 114 ++++++++++++
.../services/billing-webhook.service.ts | 171 ------------------
.../billing/stripe/stripe.service.ts | 20 +-
...ent-to-entitlement-repository-data.util.ts | 23 +++
...-event-to-customer-repository-data.util.ts | 14 ++
...-subscription-item-repository-data.util.ts | 23 +++
...nt-to-subscription-repository-data.util.ts | 67 +++++++
.../core-modules/message-queue/jobs.module.ts | 10 +-
15 files changed, 344 insertions(+), 201 deletions(-)
rename packages/twenty-server/src/engine/core-modules/billing/jobs/{update-subscription.job.ts => update-subscription-quantity.job.ts} (86%)
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts
delete mode 100644 packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts
diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts
index 9cac8ec106ce..61179279c105 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts
@@ -16,7 +16,8 @@ import {
} from 'src/engine/core-modules/billing/billing.exception';
import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
-import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service';
+import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
+import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
@Controller('billing')
export class BillingController {
@@ -24,7 +25,8 @@ export class BillingController {
constructor(
private readonly stripeService: StripeService,
- private readonly billingWehbookService: BillingWebhookService,
+ private readonly billingWebhookSubscriptionService: BillingWebhookSubscriptionService,
+ private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService,
private readonly billingSubscriptionService: BillingSubscriptionService,
) {}
@@ -61,7 +63,7 @@ export class BillingController {
return;
}
- await this.billingWehbookService.processStripeEvent(
+ await this.billingWebhookSubscriptionService.processStripeEvent(
workspaceId,
event.data,
);
@@ -70,7 +72,7 @@ export class BillingController {
event.type === WebhookEvent.CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED
) {
try {
- await this.billingWehbookService.processCustomerActiveEntitlement(
+ await this.billingWebhookEntitlementService.processStripeEvent(
event.data,
);
} catch (error) {
@@ -82,6 +84,7 @@ export class BillingController {
}
}
}
+
res.status(200).end();
}
}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts
index 0664c74682dd..9b42e8878878 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts
@@ -13,14 +13,15 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
-import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service';
+import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
+import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
+import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
@Module({
imports: [
@@ -46,7 +47,8 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
controllers: [BillingController],
providers: [
BillingSubscriptionService,
- BillingWebhookService,
+ BillingWebhookSubscriptionService,
+ BillingWebhookEntitlementService,
BillingPortalWorkspaceService,
BillingResolver,
BillingWorkspaceMemberListener,
@@ -55,7 +57,6 @@ import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/doma
exports: [
BillingSubscriptionService,
BillingPortalWorkspaceService,
- BillingWebhookService,
BillingService,
],
})
diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts
index efb1e5f571ba..132e796fc512 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts
@@ -4,4 +4,7 @@ export enum WebhookEvent {
CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted',
SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded',
CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated',
+ CUSTOMER_CREATED = 'customer.created',
+ CUSTOMER_DELETED = 'customer.deleted',
+ CUSTOMER_UPDATED = 'customer.updated',
}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts
similarity index 86%
rename from packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts
rename to packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts
index 5843a4f36bac..301322448f3e 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription-quantity.job.ts
@@ -7,14 +7,14 @@ import { Processor } from 'src/engine/core-modules/message-queue/decorators/proc
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
-export type UpdateSubscriptionJobData = { workspaceId: string };
+export type UpdateSubscriptionQuantityJobData = { workspaceId: string };
@Processor({
queueName: MessageQueue.billingQueue,
scope: Scope.REQUEST,
})
-export class UpdateSubscriptionJob {
- protected readonly logger = new Logger(UpdateSubscriptionJob.name);
+export class UpdateSubscriptionQuantityJob {
+ protected readonly logger = new Logger(UpdateSubscriptionQuantityJob.name);
constructor(
private readonly billingSubscriptionService: BillingSubscriptionService,
@@ -22,8 +22,8 @@ export class UpdateSubscriptionJob {
private readonly twentyORMManager: TwentyORMManager,
) {}
- @Process(UpdateSubscriptionJob.name)
- async handle(data: UpdateSubscriptionJobData): Promise {
+ @Process(UpdateSubscriptionQuantityJob.name)
+ async handle(data: UpdateSubscriptionQuantityJobData): Promise {
const workspaceMemberRepository =
await this.twentyORMManager.getRepository(
'workspaceMember',
diff --git a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts
index 9911bec57a6f..1cfe6efb9390 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts
@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common';
+import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
+import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
import {
- UpdateSubscriptionJob,
- UpdateSubscriptionJobData,
-} from 'src/engine/core-modules/billing/jobs/update-subscription.job';
+ UpdateSubscriptionQuantityJob,
+ UpdateSubscriptionQuantityJobData,
+} from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
@@ -11,8 +13,6 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
-import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
-import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
@Injectable()
export class BillingWorkspaceMemberListener {
@@ -33,8 +33,8 @@ export class BillingWorkspaceMemberListener {
return;
}
- await this.messageQueueService.add(
- UpdateSubscriptionJob.name,
+ await this.messageQueueService.add(
+ UpdateSubscriptionQuantityJob.name,
{ workspaceId: payload.workspaceId },
);
}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts
index a601108a8f51..344b6d18992b 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts
@@ -6,12 +6,11 @@ import { Repository } from 'typeorm';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
-import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
+import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { assert } from 'src/utils/assert';
-import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
@Injectable()
export class BillingPortalWorkspaceService {
@@ -19,7 +18,6 @@ export class BillingPortalWorkspaceService {
constructor(
private readonly stripeService: StripeService,
private readonly domainManagerService: DomainManagerService,
- private readonly environmentService: EnvironmentService,
@InjectRepository(BillingSubscription, 'core')
private readonly billingSubscriptionRepository: Repository,
@InjectRepository(UserWorkspace, 'core')
diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts
new file mode 100644
index 000000000000..1f34877b7ec2
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts
@@ -0,0 +1,52 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+
+import Stripe from 'stripe';
+import { Repository } from 'typeorm';
+
+import {
+ BillingException,
+ BillingExceptionCode,
+} from 'src/engine/core-modules/billing/billing.exception';
+import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
+import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
+import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util';
+@Injectable()
+export class BillingWebhookEntitlementService {
+ protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
+ constructor(
+ @InjectRepository(BillingSubscription, 'core')
+ private readonly billingSubscriptionRepository: Repository,
+ @InjectRepository(BillingEntitlement, 'core')
+ private readonly billingEntitlementRepository: Repository,
+ ) {}
+
+ async processStripeEvent(
+ data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
+ ) {
+ const billingSubscription =
+ await this.billingSubscriptionRepository.findOne({
+ where: { stripeCustomerId: data.object.customer },
+ });
+
+ if (!billingSubscription) {
+ throw new BillingException(
+ 'Billing customer not found',
+ BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND,
+ );
+ }
+
+ const workspaceId = billingSubscription.workspaceId;
+
+ await this.billingEntitlementRepository.upsert(
+ transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
+ workspaceId,
+ data,
+ ),
+ {
+ conflictPaths: ['workspaceId', 'key'],
+ skipUpdateIfNoValuesChanged: true,
+ },
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts
new file mode 100644
index 000000000000..3d773e47ce00
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts
@@ -0,0 +1,114 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+
+import Stripe from 'stripe';
+import { Repository } from 'typeorm';
+
+import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
+import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
+import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
+import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
+import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
+import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
+import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util';
+import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
+import {
+ Workspace,
+ WorkspaceActivationStatus,
+} from 'src/engine/core-modules/workspace/workspace.entity';
+@Injectable()
+export class BillingWebhookSubscriptionService {
+ protected readonly logger = new Logger(
+ BillingWebhookSubscriptionService.name,
+ );
+ constructor(
+ private readonly stripeService: StripeService,
+ @InjectRepository(BillingSubscription, 'core')
+ private readonly billingSubscriptionRepository: Repository,
+ @InjectRepository(BillingSubscriptionItem, 'core')
+ private readonly billingSubscriptionItemRepository: Repository,
+ @InjectRepository(Workspace, 'core')
+ private readonly workspaceRepository: Repository,
+ @InjectRepository(BillingCustomer, 'core')
+ private readonly billingCustomerRepository: Repository,
+ ) {}
+
+ async processStripeEvent(
+ workspaceId: string,
+ data:
+ | Stripe.CustomerSubscriptionUpdatedEvent.Data
+ | Stripe.CustomerSubscriptionCreatedEvent.Data
+ | Stripe.CustomerSubscriptionDeletedEvent.Data,
+ ) {
+ const workspace = await this.workspaceRepository.findOne({
+ where: { id: workspaceId },
+ });
+
+ if (!workspace) {
+ return;
+ }
+
+ await this.billingCustomerRepository.upsert(
+ transformStripeSubscriptionEventToCustomerRepositoryData(
+ workspaceId,
+ data,
+ ),
+ {
+ conflictPaths: ['workspaceId', 'stripeCustomerId'],
+ skipUpdateIfNoValuesChanged: true,
+ },
+ );
+
+ await this.billingSubscriptionRepository.upsert(
+ transformStripeSubscriptionEventToSubscriptionRepositoryData(
+ workspaceId,
+ data,
+ ),
+ {
+ conflictPaths: ['stripeSubscriptionId'],
+ skipUpdateIfNoValuesChanged: true,
+ },
+ );
+
+ const billingSubscription =
+ await this.billingSubscriptionRepository.findOneOrFail({
+ where: { stripeSubscriptionId: data.object.id },
+ });
+
+ await this.billingSubscriptionItemRepository.upsert(
+ transformStripeSubscriptionEventToSubscriptionItemRepositoryData(
+ billingSubscription.id,
+ data,
+ ),
+ {
+ conflictPaths: ['billingSubscriptionId', 'stripeProductId'],
+ skipUpdateIfNoValuesChanged: true,
+ },
+ );
+
+ if (
+ data.object.status === SubscriptionStatus.Canceled ||
+ data.object.status === SubscriptionStatus.Unpaid
+ ) {
+ await this.workspaceRepository.update(workspaceId, {
+ activationStatus: WorkspaceActivationStatus.INACTIVE,
+ });
+ }
+
+ if (
+ (data.object.status === SubscriptionStatus.Active ||
+ data.object.status === SubscriptionStatus.Trialing ||
+ data.object.status === SubscriptionStatus.PastDue) &&
+ workspace.activationStatus == WorkspaceActivationStatus.INACTIVE
+ ) {
+ await this.workspaceRepository.update(workspaceId, {
+ activationStatus: WorkspaceActivationStatus.ACTIVE,
+ });
+ }
+
+ await this.stripeService.updateCustomerMetadataWorkspaceId(
+ String(data.object.customer),
+ workspaceId,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts
deleted file mode 100644
index d4580612474b..000000000000
--- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { Injectable, Logger } from '@nestjs/common';
-import { InjectRepository } from '@nestjs/typeorm';
-
-import Stripe from 'stripe';
-import { Repository } from 'typeorm';
-
-import {
- BillingException,
- BillingExceptionCode,
-} from 'src/engine/core-modules/billing/billing.exception';
-import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
-import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
-import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
-import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
-import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
-import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
-import {
- Workspace,
- WorkspaceActivationStatus,
-} from 'src/engine/core-modules/workspace/workspace.entity';
-
-@Injectable()
-export class BillingWebhookService {
- protected readonly logger = new Logger(BillingWebhookService.name);
- constructor(
- @InjectRepository(BillingSubscription, 'core')
- private readonly billingSubscriptionRepository: Repository,
- @InjectRepository(BillingEntitlement, 'core')
- private readonly billingEntitlementRepository: Repository,
- @InjectRepository(BillingSubscriptionItem, 'core')
- private readonly billingSubscriptionItemRepository: Repository,
- @InjectRepository(Workspace, 'core')
- private readonly workspaceRepository: Repository,
- ) {}
-
- async processStripeEvent(
- workspaceId: string,
- data:
- | Stripe.CustomerSubscriptionUpdatedEvent.Data
- | Stripe.CustomerSubscriptionCreatedEvent.Data
- | Stripe.CustomerSubscriptionDeletedEvent.Data,
- ) {
- const workspace = await this.workspaceRepository.findOne({
- where: { id: workspaceId },
- });
-
- if (!workspace) {
- return;
- }
-
- await this.billingSubscriptionRepository.upsert(
- {
- workspaceId,
- stripeCustomerId: data.object.customer as string,
- stripeSubscriptionId: data.object.id,
- status: data.object.status as SubscriptionStatus,
- interval: data.object.items.data[0].plan.interval,
- cancelAtPeriodEnd: data.object.cancel_at_period_end,
- currency: data.object.currency.toUpperCase(),
- currentPeriodEnd: new Date(data.object.current_period_end * 1000),
- currentPeriodStart: new Date(data.object.current_period_start * 1000),
- metadata: data.object.metadata,
- collectionMethod:
- data.object.collection_method.toUpperCase() as BillingSubscriptionCollectionMethod,
- automaticTax: data.object.automatic_tax ?? undefined,
- cancellationDetails: data.object.cancellation_details ?? undefined,
- endedAt: data.object.ended_at
- ? new Date(data.object.ended_at * 1000)
- : undefined,
- trialStart: data.object.trial_start
- ? new Date(data.object.trial_start * 1000)
- : undefined,
- trialEnd: data.object.trial_end
- ? new Date(data.object.trial_end * 1000)
- : undefined,
- cancelAt: data.object.cancel_at
- ? new Date(data.object.cancel_at * 1000)
- : undefined,
- canceledAt: data.object.canceled_at
- ? new Date(data.object.canceled_at * 1000)
- : undefined,
- },
- {
- conflictPaths: ['stripeSubscriptionId'],
- skipUpdateIfNoValuesChanged: true,
- },
- );
-
- const billingSubscription =
- await this.billingSubscriptionRepository.findOneOrFail({
- where: { stripeSubscriptionId: data.object.id },
- });
-
- await this.billingSubscriptionItemRepository.upsert(
- data.object.items.data.map((item) => {
- return {
- billingSubscriptionId: billingSubscription.id,
- stripeSubscriptionId: data.object.id,
- stripeProductId: item.price.product as string,
- stripePriceId: item.price.id,
- stripeSubscriptionItemId: item.id,
- quantity: item.quantity,
- metadata: item.metadata,
- billingThresholds: item.billing_thresholds ?? undefined,
- };
- }),
- {
- conflictPaths: ['billingSubscriptionId', 'stripeProductId'],
- skipUpdateIfNoValuesChanged: true,
- },
- );
-
- if (
- data.object.status === SubscriptionStatus.Canceled ||
- data.object.status === SubscriptionStatus.Unpaid
- ) {
- await this.workspaceRepository.update(workspaceId, {
- activationStatus: WorkspaceActivationStatus.INACTIVE,
- });
- }
-
- if (
- (data.object.status === SubscriptionStatus.Active ||
- data.object.status === SubscriptionStatus.Trialing ||
- data.object.status === SubscriptionStatus.PastDue) &&
- workspace.activationStatus == WorkspaceActivationStatus.INACTIVE
- ) {
- await this.workspaceRepository.update(workspaceId, {
- activationStatus: WorkspaceActivationStatus.ACTIVE,
- });
- }
- }
-
- async processCustomerActiveEntitlement(
- data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
- ) {
- const billingSubscription =
- await this.billingSubscriptionRepository.findOne({
- where: { stripeCustomerId: data.object.customer },
- });
-
- if (!billingSubscription) {
- throw new BillingException(
- 'Billing customer not found',
- BillingExceptionCode.BILLING_CUSTOMER_NOT_FOUND,
- );
- }
-
- const workspaceId = billingSubscription.workspaceId;
- const stripeCustomerId = data.object.customer;
-
- const activeEntitlementsKeys = data.object.entitlements.data.map(
- (entitlement) => entitlement.lookup_key,
- );
-
- await this.billingEntitlementRepository.upsert(
- Object.values(BillingEntitlementKey).map((key) => {
- return {
- workspaceId,
- key,
- value: activeEntitlementsKeys.includes(key),
- stripeCustomerId,
- };
- }),
- {
- conflictPaths: ['workspaceId', 'key'],
- skipUpdateIfNoValuesChanged: true,
- },
- );
- }
-}
diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts
index 7e4a7e2570e7..efd9c02acb6b 100644
--- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts
@@ -5,9 +5,9 @@ import Stripe from 'stripe';
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
+import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
-import { DomainManagerService } from 'src/engine/core-modules/domain-manager/service/domain-manager.service';
@Injectable()
export class StripeService {
@@ -114,7 +114,10 @@ export class StripeService {
success_url: successUrl,
cancel_url: cancelUrl,
});
- }
+ } // I prefered to not create a customer with metadat before the checkout, because it would break the tax calculation
+ // Indeed when the checkout session is created, the customer is created and the tax calculation is done
+ // If we create a customer before the checkout session, the tax calculation is not done and the checkout session will fail
+ // I think that it's not risk worth to create a customer before the checkout session, it would only complicate the code for no signigicant gain
async collectLastInvoice(stripeSubscriptionId: string) {
const subscription = await this.stripe.subscriptions.retrieve(
@@ -148,6 +151,19 @@ export class StripeService {
);
}
+ async updateCustomerMetadataWorkspaceId(
+ stripeCustomerId: string,
+ workspaceId: string,
+ ) {
+ await this.stripe.customers.update(stripeCustomerId, {
+ metadata: { workspaceId: workspaceId },
+ });
+ }
+
+ async getCustomer(stripeCustomerId: string) {
+ return await this.stripe.customers.retrieve(stripeCustomerId);
+ }
+
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
const productPrices: ProductPriceEntity[] = Object.values(
prices
diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts
new file mode 100644
index 000000000000..7577f9a90a21
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts
@@ -0,0 +1,23 @@
+import Stripe from 'stripe';
+
+import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
+
+export const transformStripeEntitlementUpdatedEventToEntitlementRepositoryData =
+ (
+ workspaceId: string,
+ data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
+ ) => {
+ const stripeCustomerId = data.object.customer;
+ const activeEntitlementsKeys = data.object.entitlements.data.map(
+ (entitlement) => entitlement.lookup_key,
+ );
+
+ return Object.values(BillingEntitlementKey).map((key) => {
+ return {
+ workspaceId,
+ key,
+ value: activeEntitlementsKeys.includes(key),
+ stripeCustomerId,
+ };
+ });
+ };
diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts
new file mode 100644
index 000000000000..3cb313e27c56
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts
@@ -0,0 +1,14 @@
+import Stripe from 'stripe';
+
+export const transformStripeSubscriptionEventToCustomerRepositoryData = (
+ workspaceId: string,
+ data:
+ | Stripe.CustomerSubscriptionUpdatedEvent.Data
+ | Stripe.CustomerSubscriptionCreatedEvent.Data
+ | Stripe.CustomerSubscriptionDeletedEvent.Data,
+) => {
+ return {
+ workspaceId,
+ stripeCustomerId: String(data.object.customer),
+ };
+};
diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts
new file mode 100644
index 000000000000..9817937c3362
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts
@@ -0,0 +1,23 @@
+import Stripe from 'stripe';
+
+export const transformStripeSubscriptionEventToSubscriptionItemRepositoryData =
+ (
+ billingSubscriptionId: string,
+ data:
+ | Stripe.CustomerSubscriptionUpdatedEvent.Data
+ | Stripe.CustomerSubscriptionCreatedEvent.Data
+ | Stripe.CustomerSubscriptionDeletedEvent.Data,
+ ) => {
+ return data.object.items.data.map((item) => {
+ return {
+ billingSubscriptionId,
+ stripeSubscriptionId: data.object.id,
+ stripeProductId: String(item.price.product),
+ stripePriceId: item.price.id,
+ stripeSubscriptionItemId: item.id,
+ quantity: item.quantity,
+ metadata: item.metadata,
+ billingThresholds: item.billing_thresholds ?? undefined,
+ };
+ });
+ };
diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts
new file mode 100644
index 000000000000..2b684dac48f6
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts
@@ -0,0 +1,67 @@
+import Stripe from 'stripe';
+
+import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
+import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
+
+export const transformStripeSubscriptionEventToSubscriptionRepositoryData = (
+ workspaceId: string,
+ data:
+ | Stripe.CustomerSubscriptionUpdatedEvent.Data
+ | Stripe.CustomerSubscriptionCreatedEvent.Data
+ | Stripe.CustomerSubscriptionDeletedEvent.Data,
+) => {
+ return {
+ workspaceId,
+ stripeCustomerId: String(data.object.customer),
+ stripeSubscriptionId: data.object.id,
+ status: getSubscriptionStatus(data.object.status),
+ interval: data.object.items.data[0].plan.interval,
+ cancelAtPeriodEnd: data.object.cancel_at_period_end,
+ currency: data.object.currency.toUpperCase(),
+ currentPeriodEnd: new Date(data.object.current_period_end * 1000),
+ currentPeriodStart: new Date(data.object.current_period_start * 1000),
+ metadata: data.object.metadata,
+ collectionMethod:
+ BillingSubscriptionCollectionMethod[
+ data.object.collection_method.toUpperCase()
+ ],
+ automaticTax: data.object.automatic_tax ?? undefined,
+ cancellationDetails: data.object.cancellation_details ?? undefined,
+ endedAt: data.object.ended_at
+ ? new Date(data.object.ended_at * 1000)
+ : undefined,
+ trialStart: data.object.trial_start
+ ? new Date(data.object.trial_start * 1000)
+ : undefined,
+ trialEnd: data.object.trial_end
+ ? new Date(data.object.trial_end * 1000)
+ : undefined,
+ cancelAt: data.object.cancel_at
+ ? new Date(data.object.cancel_at * 1000)
+ : undefined,
+ canceledAt: data.object.canceled_at
+ ? new Date(data.object.canceled_at * 1000)
+ : undefined,
+ };
+};
+
+const getSubscriptionStatus = (status: Stripe.Subscription.Status) => {
+ switch (status) {
+ case 'active':
+ return SubscriptionStatus.Active;
+ case 'canceled':
+ return SubscriptionStatus.Canceled;
+ case 'incomplete':
+ return SubscriptionStatus.Incomplete;
+ case 'incomplete_expired':
+ return SubscriptionStatus.IncompleteExpired;
+ case 'past_due':
+ return SubscriptionStatus.PastDue;
+ case 'paused':
+ return SubscriptionStatus.Paused;
+ case 'trialing':
+ return SubscriptionStatus.Trialing;
+ case 'unpaid':
+ return SubscriptionStatus.Unpaid;
+ }
+};
diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
index 7e624cc67290..2061e9889a0b 100644
--- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts
@@ -8,15 +8,15 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { WorkspaceQueryRunnerJobModule } from 'src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
-import { UpdateSubscriptionJob } from 'src/engine/core-modules/billing/jobs/update-subscription.job';
+import { UpdateSubscriptionQuantityJob } from 'src/engine/core-modules/billing/jobs/update-subscription-quantity.job';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
+import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
+import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { HandleWorkspaceMemberDeletedJob } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
-import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job';
-import { EmailModule } from 'src/engine/core-modules/email/email.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job';
@@ -26,8 +26,8 @@ import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-c
import { MessagingModule } from 'src/modules/messaging/messaging.module';
import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module';
import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module';
-import { WorkflowModule } from 'src/modules/workflow/workflow.module';
import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
+import { WorkflowModule } from 'src/modules/workflow/workflow.module';
@Module({
imports: [
@@ -57,7 +57,7 @@ import { WebhookJobModule } from 'src/modules/webhook/jobs/webhook-job.module';
CleanInactiveWorkspaceJob,
EmailSenderJob,
DataSeedDemoWorkspaceJob,
- UpdateSubscriptionJob,
+ UpdateSubscriptionQuantityJob,
HandleWorkspaceMemberDeletedJob,
],
})
From 182ebb63941373c3a9646780acd268dbfce04955 Mon Sep 17 00:00:00 2001
From: BOHEUS <56270748+BOHEUS@users.noreply.github.com>
Date: Thu, 12 Dec 2024 10:05:25 +0000
Subject: [PATCH 37/58] Playwright tests - stage 1 - login with email and
password test (#8988)
Scenario:
https://github.com/twentyhq/twenty/issues/8469#issuecomment-2471420099
To launch this test, `yarn playwright test --project Authentication`
must be used in `packages/twenty-e2e-testing` directory (for some reason
when launching this test from IDE, be Webstorm or VSCode, it won't fetch
the data from .env)
---
packages/twenty-e2e-testing/.env.example | 2 +-
.../twenty-e2e-testing/lib/pom/loginPage.ts | 2 +-
.../twenty-e2e-testing/playwright.config.ts | 8 ++++++--
.../tests/authentication/login.spec.ts | 17 +++++++++++++++++
4 files changed, 25 insertions(+), 4 deletions(-)
create mode 100644 packages/twenty-e2e-testing/tests/authentication/login.spec.ts
diff --git a/packages/twenty-e2e-testing/.env.example b/packages/twenty-e2e-testing/.env.example
index 29209f8c8d4b..e0fbc4a32275 100644
--- a/packages/twenty-e2e-testing/.env.example
+++ b/packages/twenty-e2e-testing/.env.example
@@ -1,5 +1,5 @@
# Note that provide always without trailing forward slash to have expected behaviour
-FRONTEND_BASE_URL=http://localhost:3001
+FRONTEND_BASE_URL=http://app.localhost:3001
CI_DEFAULT_BASE_URL=https://demo.twenty.com
DEFAULT_LOGIN=tim@apple.dev
NEW_WORKSPACE_LOGIN=test@apple.dev
diff --git a/packages/twenty-e2e-testing/lib/pom/loginPage.ts b/packages/twenty-e2e-testing/lib/pom/loginPage.ts
index dc60d3f7a799..15aa1ca22277 100644
--- a/packages/twenty-e2e-testing/lib/pom/loginPage.ts
+++ b/packages/twenty-e2e-testing/lib/pom/loginPage.ts
@@ -10,7 +10,7 @@ export class LoginPage {
private readonly forgotPasswordButton: Locator;
private readonly passwordField: Locator;
private readonly revealPasswordButton: Locator;
- private readonly signInButton: Locator;
+ readonly signInButton: Locator;
private readonly signUpButton: Locator;
private readonly previewImageButton: Locator;
private readonly uploadImageButton: Locator;
diff --git a/packages/twenty-e2e-testing/playwright.config.ts b/packages/twenty-e2e-testing/playwright.config.ts
index 964b31e3210f..f3aafad006f8 100644
--- a/packages/twenty-e2e-testing/playwright.config.ts
+++ b/packages/twenty-e2e-testing/playwright.config.ts
@@ -16,12 +16,12 @@ export default defineConfig({
fullyParallel: true, // false only for specific tests, overwritten in specific projects or global setups of projects
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
- workers: process.env.CI ? 1 : undefined, // undefined = amount of projects * amount of tests
+ workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized
timeout: 30 * 1000, // timeout can be changed
use: {
baseURL: process.env.CI
? process.env.CI_DEFAULT_BASE_URL
- : (process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001'),
+ : (process.env.FRONTEND_BASE_URL ?? 'http://app.localhost:3001'),
trace: 'retain-on-failure', // trace takes EVERYTHING from page source, records every single step, should be used only when normal debugging won't work
screenshot: 'on', // either 'on' here or in different method in modules, if 'on' all screenshots are overwritten each time the test is run
headless: true, // instead of changing it to false, run 'yarn test:e2e:debug' or 'yarn test:e2e:ui'
@@ -56,6 +56,10 @@ export default defineConfig({
},
dependencies: ['Login setup'],
},
+ {
+ name: 'Authentication',
+ testMatch: /authentication\/.*\.spec\.ts/,
+ },
//{
// name: 'webkit',
diff --git a/packages/twenty-e2e-testing/tests/authentication/login.spec.ts b/packages/twenty-e2e-testing/tests/authentication/login.spec.ts
new file mode 100644
index 000000000000..a898cf0d9330
--- /dev/null
+++ b/packages/twenty-e2e-testing/tests/authentication/login.spec.ts
@@ -0,0 +1,17 @@
+import { test as base, expect } from '../../lib/fixtures/screenshot';
+import { LoginPage } from '../../lib/pom/loginPage';
+
+// fixture
+const test = base.extend<{ loginPage: LoginPage }>({
+ loginPage: async ({ page }, use) => {
+ await use(new LoginPage(page));
+ },
+});
+
+test('Check login with email', async ({ loginPage }) => {
+ await loginPage.typeEmail(process.env.DEFAULT_LOGIN);
+ await loginPage.clickContinueButton();
+ await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
+ await loginPage.clickSignInButton();
+ await expect(loginPage.signInButton).not.toBeVisible();
+});
From d7da73f0eca13642fac0de9fb3f0efe9033ef4b3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?=
Date: Thu, 12 Dec 2024 11:50:13 +0100
Subject: [PATCH 38/58] feat: record group add new (#8925)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix #8757
This PR is adding the Add new button on view groups.
Also this PR fix an issue where the pending record can be draggable, and
is causing error.
It also start to issues with the way we're using Context.
We're initializing pretty much all Context like this:
```typescript
export const RecordTableContext = createContext(
{} as RecordTableContextProps,
);
```
This is causing issues when by mistake we use the context like this
outside the Provider hierarchy:
```typescript
const context = useContext(RecordTableContext);
```
This is going to fail silently, and all the context variables become
undefined...
To fix this I've introduced an util called `createRequiredContext`, this
one is returning an array containing the provider and the hook to
retrieve the context.
The context is initialized to undefined inside this utility, this way we
can check if the value has been initialized with the provider to check
if we're inside it. It'll throw an error if this one is used outside the
provider.
The return values are properly typed, so `undefined` is not added to the
value of the Context.
I'll create a followup ticket to use this new utility function, if
that's ok and replace it everywhere in the codebase.
We can also consider adding a eslint rule to warn about the use of
`createContext` directly.
---
.../ObjectFilterDropdownFilterSelect.tsx | 5 +-
.../MultipleFiltersDropdownButton.stories.tsx | 40 +++--
.../ObjectOptionsDropdownContent.stories.tsx | 7 +-
.../useSearchRecordGroupField.test.tsx | 6 +-
.../hooks/useSearchRecordGroupField.ts | 6 +-
.../components/ObjectSortDropdownButton.tsx | 5 +-
.../perf/ChipFieldDisplay.perf.stories.tsx | 26 +++
.../hooks/useCurrentRecordGroupId.ts | 2 +-
.../hooks/useRecordGroupActions.ts | 6 +-
.../record-group/hooks/useSetRecordGroup.ts | 5 +-
.../components/RecordIndexContainer.tsx | 6 +-
...textStoreNumberOfSelectedRecordsEffect.tsx | 6 +-
...tainerContextStoreObjectMetadataEffect.tsx | 6 +-
...RecordIndexFiltersToContextStoreEffect.tsx | 6 +-
.../components/RecordIndexPageHeader.tsx | 25 +--
.../RecordIndexPageKanbanAddButton.tsx | 40 ++---
.../RecordIndexPageTableAddButton.tsx | 16 ++
.../RecordIndexPageTableAddButtonInGroup.tsx | 80 +++++++++
.../RecordIndexPageTableAddButtonNoGroup.tsx | 21 +++
.../components/RecordIndexRecordChip.tsx | 5 +-
.../components/RecordIndexTableContainer.tsx | 5 +-
.../RecordIndexTableContainerEffect.tsx | 8 +-
...tPropsContext.ts => RecordIndexContext.ts} | 9 +-
.../record-table/components/RecordTable.tsx | 97 ++++-------
.../components/RecordTableContextProvider.tsx | 97 ++---------
...dTableNoRecordGroupBodyContextProvider.tsx | 95 +++++++++++
...ordTableRecordGroupBodyContextProvider.tsx | 99 +++++++++++
.../components/RecordTableRecordGroupRows.tsx | 47 +++--
.../components/RecordTableWithWrappers.tsx | 50 +++---
.../perf/RecordTableCell.perf.stories.tsx | 108 ++++++------
.../contexts/RecordTableBodyContext.ts | 36 ++++
.../contexts/RecordTableContext.ts | 36 +---
.../contexts/RecordTableRowContext.ts | 1 +
.../components/RecordTableEmptyState.tsx | 7 +-
.../RecordTableEmptyStateDisplay.tsx | 5 +-
.../RecordTableEmptyStateNoRecordAtAll.tsx | 7 +-
...dTableEmptyStateNoRecordFoundForFilter.tsx | 7 +-
.../RecordTableEmptyStateSoftDelete.tsx | 5 +-
.../useUpsertTableRecordInGroup.test.tsx} | 89 ++++++----
.../useUpsertTableRecordNoGroup.test.tsx | 160 ++++++++++++++++++
.../internal/useUpsertTableRecordInGroup.ts | 87 ++++++++++
.../internal/useUpsertTableRecordNoGroup.ts} | 47 ++---
.../hooks/useCreateNewTableRecordInGroup.ts | 68 ++++++++
.../hooks/useCreateNewTableRecords.ts | 85 ++++++++--
.../record-table/hooks/useRecordTable.ts | 7 -
...RecordTableBodyDragDropContextProvider.tsx | 6 +-
...BodyRecordGroupDragDropContextProvider.tsx | 6 +-
.../RecordTableBodyUnselectEffect.tsx | 5 +-
.../RecordTableNoRecordGroupBody.tsx | 15 +-
.../RecordTableNoRecordGroupBodyEffect.tsx | 6 +-
.../RecordTableRecordGroupBodyEffect.tsx | 6 +-
.../RecordTableRecordGroupsBody.tsx | 27 ++-
.../RecordTableCellBaseContainer.tsx | 4 +-
.../components/RecordTableCellDisplayMode.tsx | 5 +-
.../RecordTableCellFieldContextWrapper.tsx | 6 +-
.../components/RecordTableCellFieldInput.tsx | 7 +-
.../components/RecordTableCellGrip.tsx | 28 +--
.../RecordTableCellSoftFocusMode.tsx | 6 +-
.../useCloseRecordTableCellInGroup.test.tsx | 101 +++++++++++
.../useCloseRecordTableCellNoGroup.test.tsx} | 54 +++---
.../useCloseRecordTableCellInGroup.ts | 56 ++++++
.../useCloseRecordTableCellNoGroup.ts} | 24 ++-
.../hooks/useCloseRecordTableCell.ts | 38 -----
.../hooks/useOpenRecordTableCellFromCell.ts | 9 +-
.../hooks/useOpenRecordTableCellV2.ts | 5 +-
.../components/RecordTableHeader.tsx | 19 +--
.../components/RecordTableHeaderCell.tsx | 18 +-
.../RecordTableHeaderPlusButtonContent.tsx | 7 +-
.../RecordTablePendingRecordGroupRow.tsx | 25 +++
.../components/RecordTableRowWrapper.tsx | 15 +-
.../RecordTableRecordGroupEmptyRow.tsx | 7 +
.../RecordTableRecordGroupSectionAddNew.tsx | 36 ++++
.../RecordTableRecordGroupSectionLoadMore.tsx | 5 +-
...dingRecordIdByGroupComponentFamilyState.ts | 10 ++
...ecordTablePendingRecordIdComponentState.ts | 2 +-
.../SignInBackgroundMockContainer.tsx | 7 +-
.../hooks/useCreateViewFromCurrentView.ts | 5 +-
.../pages/object-record/RecordIndexPage.tsx | 14 +-
.../decorators/RecordTableDecorator.tsx | 30 ++--
.../src/utils/createRequiredContext.ts | 20 +++
.../src/utils/createRootPropsContext.ts | 11 --
81 files changed, 1544 insertions(+), 682 deletions(-)
create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx
rename packages/twenty-front/src/modules/object-record/record-index/contexts/{RecordIndexRootPropsContext.ts => RecordIndexContext.ts} (53%)
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts
rename packages/twenty-front/src/modules/object-record/record-table/{record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx => hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx} (60%)
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts
rename packages/twenty-front/src/modules/object-record/record-table/{record-table-cell/hooks/useUpsertRecord.ts => hooks/internal/useUpsertTableRecordNoGroup.ts} (65%)
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx
rename packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/{__tests__/useCloseRecordTableCell.test.tsx => internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx} (62%)
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts
rename packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/{useCloseRecordTableCellV2.ts => internal/useCloseRecordTableCellNoGroup.ts} (66%)
delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew.tsx
create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState.ts
create mode 100644 packages/twenty-front/src/utils/createRequiredContext.ts
delete mode 100644 packages/twenty-front/src/utils/createRootPropsContext.ts
diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx
index d879d35f6790..a86bc18ab015 100644
--- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx
@@ -9,7 +9,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
@@ -20,7 +20,6 @@ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
-import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
@@ -57,7 +56,7 @@ type ObjectFilterDropdownFilterSelectProps = {
export const ObjectFilterDropdownFilterSelect = ({
isAdvancedFilterButtonVisible,
}: ObjectFilterDropdownFilterSelectProps) => {
- const { recordIndexId } = useContext(RecordIndexRootPropsContext);
+ const { recordIndexId } = useRecordIndexContextOrThrow();
const {
setObjectFilterDropdownSearchInput,
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 90d85da61edf..ea06072c09be 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
@@ -1,9 +1,12 @@
import { Meta, StoryObj } from '@storybook/react';
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { MultipleFiltersDropdownButton } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
+import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
@@ -19,6 +22,7 @@ import { FieldMetadataType } from '~/generated/graphql';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const meta: Meta = {
title:
@@ -26,6 +30,9 @@ const meta: Meta = {
component: MultipleFiltersDropdownButton,
decorators: [
(Story) => {
+ const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
+ (item) => item.nameSingular === CoreObjectNameSingular.Company,
+ )!;
const instanceId = 'entity-tasks-filter-scope';
const setAvailableFilterDefinitions = useSetRecoilComponentStateV2(
availableFilterDefinitionsComponentState,
@@ -91,19 +98,30 @@ const meta: Meta = {
},
]);
return (
- '',
+ onIndexRecordsLoaded: () => {},
+ objectNamePlural: CoreObjectNamePlural.Company,
+ objectNameSingular: CoreObjectNameSingular.Company,
+ objectMetadataItem: companyObjectMetadataItem,
+ recordIndexId: instanceId,
+ }}
>
- {} }}
+
-
-
-
-
-
-
-
+ {} }}
+ >
+
+
+
+
+
+
+
+
);
},
ObjectMetadataItemsDecorator,
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
index f53d7f995b79..ce819f7496e6 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/__stories__/ObjectOptionsDropdownContent.stories.tsx
@@ -7,7 +7,7 @@ import { ObjectOptionsDropdownContent } from '@/object-record/object-options-dro
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { ObjectOptionsContentId } from '@/object-record/object-options-dropdown/types/ObjectOptionsContentId';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
@@ -76,11 +76,10 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
)!;
return (
- '',
onIndexRecordsLoaded: () => {},
- onCreateRecord: () => {},
objectNamePlural: 'companies',
objectNameSingular: 'company',
objectMetadataItem: companyObjectMetadataItem,
@@ -102,7 +101,7 @@ const createStory = (contentId: ObjectOptionsContentId | null): Story => ({
-
+
);
},
],
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx
index caa57250eaaf..cf2ffba00fa2 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/__tests__/useSearchRecordGroupField.test.tsx
@@ -1,5 +1,5 @@
import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { renderHook } from '@testing-library/react';
import { act } from 'react';
@@ -11,13 +11,13 @@ describe('useSearchRecordGroupField', () => {
renderHook(() => useSearchRecordGroupField(), {
wrapper: ({ children }) => (
-
+
{children}
-
+
),
});
diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts
index 250554f52471..633acc0bb34e 100644
--- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts
+++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useSearchRecordGroupField.ts
@@ -1,11 +1,11 @@
import { objectOptionsDropdownSearchInputComponentState } from '@/object-record/object-options-dropdown/states/objectOptionsDropdownSearchInputComponentState';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
-import { useContext, useMemo } from 'react';
+import { useMemo } from 'react';
import { FieldMetadataType } from '~/generated-metadata/graphql';
export const useSearchRecordGroupField = () => {
- const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
+ const { objectMetadataItem } = useRecordIndexContextOrThrow();
const [recordGroupFieldSearchInput, setRecordGroupFieldSearchInput] =
useRecoilComponentStateV2(objectOptionsDropdownSearchInputComponentState);
diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx
index ebad9ac412a3..014905a5e63c 100644
--- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx
@@ -5,7 +5,7 @@ import { IconChevronDown, MenuItem, useIcons } from 'twenty-ui';
import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId';
import { useObjectSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useObjectSortDropdown';
import { ObjectSortDropdownScope } from '@/object-record/object-sort-dropdown/scopes/ObjectSortDropdownScope';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
@@ -16,7 +16,6 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
-import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection';
export const StyledInput = styled.input`
@@ -77,7 +76,7 @@ export const ObjectSortDropdownButton = ({
resetSearchInput,
} = useObjectSortDropdown();
- const { recordIndexId } = useContext(RecordIndexRootPropsContext);
+ const { recordIndexId } = useRecordIndexContextOrThrow();
const { isDropdownOpen } = useDropdown(OBJECT_SORT_DROPDOWN_ID);
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx
index ad1235d24117..bc2964a29499 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/ChipFieldDisplay.perf.stories.tsx
@@ -1,15 +1,41 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from 'twenty-ui';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ChipFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ChipFieldDisplay';
+import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
const meta: Meta = {
title: 'UI/Data/Field/Display/ChipFieldDisplay',
decorators: [
+ (Story) => {
+ const instanceId = 'child-field-display-scope';
+
+ const companyObjectMetadataItem = generatedMockObjectMetadataItems.find(
+ (item) => item.nameSingular === CoreObjectNameSingular.Company,
+ )!;
+
+ return (
+ '',
+ onIndexRecordsLoaded: () => {},
+ objectNamePlural: CoreObjectNamePlural.Company,
+ objectNameSingular: CoreObjectNameSingular.Company,
+ objectMetadataItem: companyObjectMetadataItem,
+ recordIndexId: instanceId,
+ }}
+ >
+
+
+ );
+ },
MemoryRouterDecorator,
ChipGeneratorsDecorator,
getFieldDecorator('person', 'name'),
diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts
index b9960a19c6ce..721994105ef6 100644
--- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useCurrentRecordGroupId.ts
@@ -1,7 +1,7 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { useContext } from 'react';
-export const useCurrentRecordGroupId = () => {
+export const useCurrentRecordGroupId = (): string => {
const context = useContext(RecordGroupContext);
if (!context) {
diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts
index b1bc9829d768..2c88b9609eb0 100644
--- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupActions.ts
@@ -5,7 +5,7 @@ import { RecordBoardColumnContext } from '@/object-record/record-board/record-bo
import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility';
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { RecordGroupAction } from '@/object-record/record-group/types/RecordGroupActions';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useContext, useMemo } from 'react';
@@ -17,9 +17,7 @@ export const useRecordGroupActions = () => {
const navigate = useNavigate();
const location = useLocation();
- const { objectNameSingular, recordIndexId } = useContext(
- RecordIndexRootPropsContext,
- );
+ const { objectNameSingular, recordIndexId } = useRecordIndexContextOrThrow();
const { columnDefinition: recordGroupDefinition } = useContext(
RecordBoardColumnContext,
diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts
index 934208dc5c82..37dc16c1d578 100644
--- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts
+++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts
@@ -2,15 +2,14 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
-import { useContext } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordGroup = (viewId?: string) => {
- const { objectMetadataItem } = useContext(RecordIndexRootPropsContext);
+ const { objectMetadataItem } = useRecordIndexContextOrThrow();
const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2(
recordGroupIdsComponentState,
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 02bc1ed52dff..9fc7bfcbb345 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
@@ -17,7 +17,7 @@ import { recordIndexSortsState } from '@/object-record/record-index/states/recor
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
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';
@@ -39,7 +39,7 @@ import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
-import { useCallback, useContext } from 'react';
+import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
const StyledContainer = styled.div`
@@ -67,7 +67,7 @@ export const RecordIndexContainer = () => {
recordIndexId,
objectMetadataItem,
objectNameSingular,
- } = useContext(RecordIndexRootPropsContext);
+ } = useRecordIndexContextOrThrow();
const setRecordGroup = useSetRecordGroup(recordIndexId);
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx
index 2c8106f6d7e6..375e5cfc02c2 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx
@@ -5,11 +5,11 @@ import { computeContextStoreFilters } from '@/context-store/utils/computeContext
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useFindManyRecordIndexTableParams } from '@/object-record/record-index/hooks/useFindManyRecordIndexTableParams';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
-import { useContext, useEffect } from 'react';
+import { useEffect } from 'react';
export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
() => {
@@ -21,7 +21,7 @@ export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
contextStoreTargetedRecordsRuleComponentState,
);
- const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
+ const { objectNamePlural } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx
index 03a02ee36f10..88ad438b3a4d 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx
@@ -1,15 +1,15 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
-import { useContext, useEffect } from 'react';
+import { useEffect } from 'react';
export const RecordIndexContainerContextStoreObjectMetadataEffect = () => {
const setContextStoreCurrentObjectMetadataItem = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
- const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
+ const { objectNamePlural } = useRecordIndexContextOrThrow();
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx
index 334abf9ec96c..d009c9c61390 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect.tsx
@@ -1,8 +1,8 @@
-import { useContext, useEffect } from 'react';
+import { useEffect } from 'react';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState';
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
@@ -12,7 +12,7 @@ import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-sta
import { useRecoilValue } from 'recoil';
export const RecordIndexFiltersToContextStoreEffect = () => {
- const { recordIndexId } = useContext(RecordIndexRootPropsContext);
+ const { recordIndexId } = useRecordIndexContextOrThrow();
const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
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 3e8abc738d92..c2d6584ff391 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
@@ -3,16 +3,14 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
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 { RecordIndexPageTableAddButton } from '@/object-record/record-index/components/RecordIndexPageTableAddButton';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
-import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
-import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ViewType } from '@/views/types/ViewType';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
-import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined, useIcons } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
@@ -21,17 +19,13 @@ export const RecordIndexPageHeader = () => {
const { findObjectMetadataItemByNamePlural } =
useFilteredObjectMetadataItems();
- const { objectNamePlural, onCreateRecord } = useContext(
- RecordIndexRootPropsContext,
- );
+ const { objectNamePlural } = useRecordIndexContextOrThrow();
const objectMetadataItem =
findObjectMetadataItemByNamePlural(objectNamePlural);
const { getIcon } = useIcons();
- const Icon = getIcon(
- findObjectMetadataItemByNamePlural(objectNamePlural)?.icon,
- );
+ const Icon = getIcon(objectMetadataItem?.icon);
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
@@ -56,17 +50,14 @@ export const RecordIndexPageHeader = () => {
const pageHeaderTitle =
objectMetadataItem?.labelPlural ?? capitalize(objectNamePlural);
- const handleAddButtonClick = () => {
- onCreateRecord();
- };
-
return (
-
-
{shouldDisplayAddButton &&
+ /**
+ * TODO: Logic between Table and Kanban should be merged here when we move some states to record-index
+ */
(isTable ? (
-
+
) : (
))}
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx
index c86ef48d6a5a..5bf7c3b9cacd 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx
@@ -1,4 +1,3 @@
-import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard';
import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled';
@@ -6,33 +5,20 @@ import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-re
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
-import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
-import styled from '@emotion/styled';
-import { useCallback, useContext } from 'react';
+import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
-const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
- width: 100%;
-`;
-
-const StyledDropDownMenu = styled(DropdownMenu)`
- width: 200px;
-`;
-
export const RecordIndexPageKanbanAddButton = () => {
const dropdownId = `record-index-page-add-button-dropdown`;
- const { recordIndexId, objectNameSingular } = useContext(
- RecordIndexRootPropsContext,
- );
- const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
+ const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
const visibleRecordGroupIds = useRecoilComponentValueV2(
visibleRecordGroupIdsComponentSelector,
@@ -95,17 +81,15 @@ export const RecordIndexPageKanbanAddButton = () => {
clickableComponent={}
dropdownId={dropdownId}
dropdownComponents={
-
-
- {visibleRecordGroupIds.map((recordGroupId) => (
-
- ))}
-
-
+
+ {visibleRecordGroupIds.map((recordGroupId) => (
+
+ ))}
+
}
dropdownHotkeyScope={{ scope: dropdownId }}
/>
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx
new file mode 100644
index 000000000000..fe169ffe7150
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx
@@ -0,0 +1,16 @@
+import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
+import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup';
+import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+
+export const RecordIndexPageTableAddButton = () => {
+ const hasRecordGroups = useRecoilComponentValueV2(
+ hasRecordGroupsComponentSelector,
+ );
+
+ if (!hasRecordGroups) {
+ return ;
+ }
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx
new file mode 100644
index 000000000000..c40959b1fa44
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx
@@ -0,0 +1,80 @@
+import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState';
+import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
+import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition';
+import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { useCreateNewTableRecordInGroup } from '@/object-record/record-table/hooks/useCreateNewTableRecordInGroup';
+import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
+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 { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { useRecoilCallback } from 'recoil';
+
+export const RecordIndexPageTableAddButtonInGroup = () => {
+ const dropdownId = `record-index-page-table-add-button-dropdown`;
+
+ const { objectMetadataItem } = useRecordIndexContextOrThrow();
+
+ const visibleRecordGroupIds = useRecoilComponentValueV2(
+ visibleRecordGroupIdsComponentSelector,
+ );
+
+ const recordGroupFieldMetadata = useRecoilComponentValueV2(
+ recordGroupFieldMetadataComponentState,
+ );
+
+ const isRecordGroupTableSectionToggledState =
+ useRecoilComponentCallbackStateV2(
+ isRecordGroupTableSectionToggledComponentState,
+ );
+
+ const selectFieldMetadataItem = objectMetadataItem.fields.find(
+ (field) => field.id === recordGroupFieldMetadata?.id,
+ );
+
+ const { createNewTableRecordInGroup } = useCreateNewTableRecordInGroup();
+
+ const { closeDropdown } = useDropdown(dropdownId);
+
+ const handleCreateNewTableRecordInGroup = useRecoilCallback(
+ ({ set }) =>
+ (recordGroup: RecordGroupDefinition) => {
+ set(isRecordGroupTableSectionToggledState(recordGroup.id), true);
+ createNewTableRecordInGroup(recordGroup.id);
+ closeDropdown();
+ },
+ [
+ closeDropdown,
+ createNewTableRecordInGroup,
+ isRecordGroupTableSectionToggledState,
+ ],
+ );
+
+ if (!selectFieldMetadataItem) {
+ return null;
+ }
+
+ return (
+ }
+ dropdownId={dropdownId}
+ dropdownComponents={
+
+ {visibleRecordGroupIds.map((recordGroupId) => (
+
+ ))}
+
+ }
+ dropdownHotkeyScope={{ scope: dropdownId }}
+ />
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx
new file mode 100644
index 000000000000..b9d4fbe65ba1
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup.tsx
@@ -0,0 +1,21 @@
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
+import { PageAddButton } from '@/ui/layout/page/components/PageAddButton';
+import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect';
+
+export const RecordIndexPageTableAddButtonNoGroup = () => {
+ const { recordIndexId } = useRecordIndexContextOrThrow();
+
+ const { createNewTableRecord } = useCreateNewTableRecord(recordIndexId);
+
+ const handleCreateNewTableRecord = () => {
+ createNewTableRecord();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx
index b02591e8c813..c2fe3344e678 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx
@@ -1,8 +1,7 @@
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-import { useContext } from 'react';
import { AvatarChip, AvatarChipVariant, ChipSize } from 'twenty-ui';
export type RecordIdentifierChipProps = {
@@ -20,7 +19,7 @@ export const RecordIdentifierChip = ({
size,
maxWidth,
}: RecordIdentifierChipProps) => {
- const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
+ const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const { recordChipData } = useRecordChipData({
objectNameSingular,
record,
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 f1dec5e3e7e6..9d3d588c3862 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
@@ -1,9 +1,8 @@
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 { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
-import { useContext } from 'react';
type RecordIndexTableContainerProps = {
recordTableId: string;
@@ -14,7 +13,7 @@ export const RecordIndexTableContainer = ({
recordTableId,
viewBarId,
}: RecordIndexTableContainerProps) => {
- const { objectNameSingular } = useContext(RecordIndexRootPropsContext);
+ const { objectNameSingular } = useRecordIndexContextOrThrow();
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
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 aa7e470c9c70..098c8d646743 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
@@ -1,8 +1,8 @@
-import { useContext, useEffect } from 'react';
+import { useEffect } from 'react';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
@@ -13,9 +13,7 @@ import { ViewField } from '@/views/types/ViewField';
import { useRecoilCallback } from 'recoil';
export const RecordIndexTableContainerEffect = () => {
- const { recordIndexId, objectNameSingular } = useContext(
- RecordIndexRootPropsContext,
- );
+ const { recordIndexId, objectNameSingular } = useRecordIndexContextOrThrow();
const viewBarId = recordIndexId;
diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts
similarity index 53%
rename from packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts
rename to packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts
index 267501019b46..cdc52ae7c8fb 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexContext.ts
@@ -1,15 +1,14 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
-import { createRootPropsContext } from '~/utils/createRootPropsContext';
+import { createRequiredContext } from '~/utils/createRequiredContext';
-export type RecordIndexRootPropsContextProps = {
+export type RecordIndexContextValue = {
indexIdentifierUrl: (recordId: string) => string;
onIndexRecordsLoaded: () => void;
- onCreateRecord: () => void;
objectNamePlural: string;
objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
recordIndexId: string;
};
-export const RecordIndexRootPropsContext =
- createRootPropsContext();
+export const [RecordIndexContextProvider, useRecordIndexContextOrThrow] =
+ createRequiredContext('RecordIndexContext');
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 4d6f5331bccc..cc428d1980c6 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
@@ -3,10 +3,9 @@ import { isNonEmptyString, isNull } from '@sniptt/guards';
import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
-import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
-import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableStickyEffect } from '@/object-record/record-table/components/RecordTableStickyEffect';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { RecordTableBodyUnselectEffect } from '@/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect';
@@ -29,19 +28,9 @@ const StyledTable = styled.table`
width: 100%;
`;
-type RecordTableProps = {
- viewBarId: string;
- recordTableId: string;
- objectNameSingular: string;
- onColumnsChange: (columns: any) => void;
-};
+export const RecordTable = () => {
+ const { recordTableId, objectNameSingular } = useRecordTableContextOrThrow();
-export const RecordTable = ({
- viewBarId,
- recordTableId,
- objectNameSingular,
- onColumnsChange,
-}: RecordTableProps) => {
const tableBodyRef = useRef(null);
const { toggleClickOutsideListener } = useClickOutsideListener(
@@ -82,51 +71,39 @@ export const RecordTable = ({
}
return (
-
-
- {!hasRecordGroups ? (
-
- ) : (
-
- )}
-
- {recordTableIsEmpty ? (
-
- ) : (
- <>
-
-
- {!hasRecordGroups ? (
-
- ) : (
-
- )}
-
-
- {
- resetTableRowSelection();
- toggleClickOutsideListener(false);
- }}
- onDragSelectionChange={setRowSelected}
- onDragSelectionEnd={() => {
- toggleClickOutsideListener(true);
- }}
- />
- >
- )}
-
-
+ <>
+ {!hasRecordGroups ? (
+
+ ) : (
+
+ )}
+
+ {recordTableIsEmpty ? (
+
+ ) : (
+ <>
+
+
+ {!hasRecordGroups ? (
+
+ ) : (
+
+ )}
+
+
+ {
+ resetTableRowSelection();
+ toggleClickOutsideListener(false);
+ }}
+ onDragSelectionChange={setRowSelected}
+ onDragSelectionEnd={() => {
+ toggleClickOutsideListener(true);
+ }}
+ />
+ >
+ )}
+ >
);
};
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 5ad568dc0879..d7720d129f0e 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
@@ -1,117 +1,44 @@
import { ReactNode } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
-import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
-import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
-import { useCloseRecordTableCellV2 } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2';
-import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
-import {
- OpenTableCellArgs,
- useOpenRecordTableCellV2,
-} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
-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 { RecordTableContextProvider as RecordTableContextInternalProvider } from '@/object-record/record-table/contexts/RecordTableContext';
+
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
-import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
-import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+type RecordTableContextProviderProps = {
+ viewBarId: string;
+ recordTableId: string;
+ objectNameSingular: string;
+ children: ReactNode;
+};
+
export const RecordTableContextProvider = ({
viewBarId,
recordTableId,
objectNameSingular,
children,
-}: {
- viewBarId: string;
- recordTableId: string;
- objectNameSingular: string;
- children: ReactNode;
-}) => {
+}: RecordTableContextProviderProps) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
- const { upsertRecord } = useUpsertRecord({
- objectNameSingular,
- recordTableId,
- });
-
- const handleUpsertRecord = ({
- persistField,
- recordId,
- fieldName,
- }: {
- persistField: () => void;
- recordId: string;
- fieldName: string;
- }) => {
- upsertRecord(persistField, recordId, fieldName);
- };
-
- const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
-
- const handleOpenTableCell = (args: OpenTableCellArgs) => {
- openTableCell(args);
- };
-
- const { moveFocus } = useRecordTableMoveFocus(recordTableId);
-
- const handleMoveFocus = (direction: MoveFocusDirection) => {
- moveFocus(direction);
- };
-
- const { closeTableCell } = useCloseRecordTableCellV2(recordTableId);
-
- const handleCloseTableCell = () => {
- closeTableCell();
- };
-
- const { moveSoftFocusToCell } =
- useMoveSoftFocusToCellOnHoverV2(recordTableId);
-
- const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
- moveSoftFocusToCell(cellPosition);
- };
-
- const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
- recordTableId,
- });
-
- const handleActionMenuDropdown = (
- event: React.MouseEvent,
- recordId: string,
- ) => {
- triggerActionMenuDropdown(event, recordId);
- };
-
- const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
- recordTableId,
- });
-
const visibleTableColumns = useRecoilComponentValueV2(
visibleTableColumnsComponentSelector,
recordTableId,
);
return (
-
{children}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
new file mode 100644
index 000000000000..c88ff0ac1973
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider.tsx
@@ -0,0 +1,95 @@
+import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
+import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
+import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
+import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
+import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
+import {
+ OpenTableCellArgs,
+ useOpenRecordTableCellV2,
+} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
+import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
+import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
+import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
+import { ReactNode } from 'react';
+
+type RecordTableNoRecordGroupBodyContextProviderProps = {
+ children?: ReactNode;
+};
+
+export const RecordTableNoRecordGroupBodyContextProvider = ({
+ children,
+}: RecordTableNoRecordGroupBodyContextProviderProps) => {
+ const { recordTableId } = useRecordTableContextOrThrow();
+
+ const { upsertTableRecordNoGroup } = useUpsertTableRecordNoGroup();
+
+ const handleUpsertTableRecordNoRecordGroup = ({
+ persistField,
+ recordId,
+ fieldName,
+ }: {
+ persistField: () => void;
+ recordId: string;
+ fieldName: string;
+ }) => {
+ upsertTableRecordNoGroup(persistField, recordId, fieldName);
+ };
+
+ const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
+
+ const handleOpenTableCell = (args: OpenTableCellArgs) => {
+ openTableCell(args);
+ };
+
+ const { moveFocus } = useRecordTableMoveFocus(recordTableId);
+
+ const handleMoveFocus = (direction: MoveFocusDirection) => {
+ moveFocus(direction);
+ };
+
+ const { closeTableCellNoGroup } = useCloseRecordTableCellNoGroup();
+
+ const handleCloseTableCell = () => {
+ closeTableCellNoGroup();
+ };
+
+ const { moveSoftFocusToCell } =
+ useMoveSoftFocusToCellOnHoverV2(recordTableId);
+
+ const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
+ moveSoftFocusToCell(cellPosition);
+ };
+
+ const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
+ recordTableId,
+ });
+
+ const handleActionMenuDropdown = (
+ event: React.MouseEvent,
+ recordId: string,
+ ) => {
+ triggerActionMenuDropdown(event, recordId);
+ };
+
+ const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
+ recordTableId,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
new file mode 100644
index 000000000000..002a61624a95
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider.tsx
@@ -0,0 +1,99 @@
+import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useHandleContainerMouseEnter } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
+import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
+import { useRecordTableMoveFocus } from '@/object-record/record-table/hooks/useRecordTableMoveFocus';
+import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
+import { useMoveSoftFocusToCellOnHoverV2 } from '@/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCellOnHoverV2';
+import {
+ OpenTableCellArgs,
+ useOpenRecordTableCellV2,
+} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
+import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
+import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
+import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
+import { ReactNode } from 'react';
+
+type RecordTableRecordGroupBodyContextProviderProps = {
+ recordGroupId: string;
+ children?: ReactNode;
+};
+
+export const RecordTableRecordGroupBodyContextProvider = ({
+ recordGroupId,
+ children,
+}: RecordTableRecordGroupBodyContextProviderProps) => {
+ const { recordTableId } = useRecordTableContextOrThrow();
+
+ const { upsertTableRecordInGroup } =
+ useUpsertTableRecordInGroup(recordGroupId);
+
+ const handleupsertTableRecordInGroup = ({
+ persistField,
+ recordId,
+ fieldName,
+ }: {
+ persistField: () => void;
+ recordId: string;
+ fieldName: string;
+ }) => {
+ upsertTableRecordInGroup(persistField, recordId, fieldName);
+ };
+
+ const { openTableCell } = useOpenRecordTableCellV2(recordTableId);
+
+ const handleOpenTableCell = (args: OpenTableCellArgs) => {
+ openTableCell(args);
+ };
+
+ const { moveFocus } = useRecordTableMoveFocus(recordTableId);
+
+ const handleMoveFocus = (direction: MoveFocusDirection) => {
+ moveFocus(direction);
+ };
+
+ const { closeTableCellInGroup } =
+ useCloseRecordTableCellInGroup(recordGroupId);
+
+ const handlecloseTableCellInGroup = () => {
+ closeTableCellInGroup();
+ };
+
+ const { moveSoftFocusToCell } =
+ useMoveSoftFocusToCellOnHoverV2(recordTableId);
+
+ const handleMoveSoftFocusToCell = (cellPosition: TableCellPosition) => {
+ moveSoftFocusToCell(cellPosition);
+ };
+
+ const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
+ recordTableId,
+ });
+
+ const handleActionMenuDropdown = (
+ event: React.MouseEvent,
+ recordId: string,
+ ) => {
+ triggerActionMenuDropdown(event, recordId);
+ };
+
+ const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
+ recordTableId,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx
index 01688556de96..c944fd92a27f 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRecordGroupRows.tsx
@@ -1,7 +1,10 @@
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
+import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
+import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
+import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@@ -30,24 +33,32 @@ export const RecordTableRecordGroupRows = () => {
[allRecordIds],
);
+ if (!isRecordGroupTableSectionToggled) {
+ return null;
+ }
+
return (
- isRecordGroupTableSectionToggled &&
- recordIdsByGroup.map((recordId, rowIndexInGroup) => {
- const rowIndex = rowIndexMap.get(recordId);
-
- if (!isDefined(rowIndex)) {
- return null;
- }
-
- return (
-
- );
- })
+ <>
+ {recordIdsByGroup.map((recordId, rowIndexInGroup) => {
+ const rowIndex = rowIndexMap.get(recordId);
+
+ if (!isDefined(rowIndex)) {
+ return null;
+ }
+
+ return (
+
+ );
+ })}
+
+
+
+ >
);
};
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 d00e81bc3d0f..9273985454c9 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
@@ -16,6 +16,8 @@ import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext
import { useRecordTable } from '../hooks/useRecordTable';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
+import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
+import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Key } from 'ts-key-enum';
@@ -96,27 +98,33 @@ export const RecordTableWithWrappers = ({
);
return (
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
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 6b2a39237f5c..f2f99633ed45 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
@@ -14,12 +14,13 @@ import {
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
+import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
+import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import { mockPerformance } from './mock';
@@ -61,78 +62,83 @@ const meta: Meta = {
(Story) => {
return (
- {},
- onOpenTableCell: () => {},
- onMoveFocus: () => {},
- onCloseTableCell: () => {},
- onMoveSoftFocusToCell: () => {},
- onActionMenuDropdownOpened: () => {},
- onCellMouseEnter: () => {},
visibleTableColumns: mockPerformance.visibleTableColumns as any,
objectNameSingular:
mockPerformance.objectMetadataItem.nameSingular,
- recordTableId: 'recordTableId',
}}
>
{}}
>
- {},
+ onOpenTableCell: () => {},
+ onMoveFocus: () => {},
+ onCloseTableCell: () => {},
+ onMoveSoftFocusToCell: () => {},
+ onActionMenuDropdownOpened: () => {},
+ onCellMouseEnter: () => {},
}}
>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
);
},
diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts
new file mode 100644
index 000000000000..4884c8e25b75
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableBodyContext.ts
@@ -0,0 +1,36 @@
+import React from 'react';
+
+import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
+import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
+import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
+import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
+import { createRequiredContext } from '~/utils/createRequiredContext';
+
+export type RecordTableBodyContextProps = {
+ recordGroupId?: string;
+ onUpsertRecord: ({
+ persistField,
+ recordId,
+ fieldName,
+ }: {
+ persistField: () => void;
+ recordId: string;
+ fieldName: string;
+ }) => void;
+ onOpenTableCell: (args: OpenTableCellArgs) => void;
+ onMoveFocus: (direction: MoveFocusDirection) => void;
+ onCloseTableCell: () => void;
+ onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
+ onActionMenuDropdownOpened: (
+ event: React.MouseEvent,
+ recordId: string,
+ ) => void;
+ onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
+};
+
+export const [
+ RecordTableBodyContextProvider,
+ useRecordTableBodyContextOrThrow,
+] = createRequiredContext(
+ 'RecordTableBodyContext',
+);
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 ba2ea9f7a92d..09fadd7f2032 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
@@ -1,39 +1,15 @@
-import React, { createContext } from 'react';
-
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
-import { HandleContainerMouseEnterArgs } from '@/object-record/record-table/hooks/internal/useHandleContainerMouseEnter';
-import { OpenTableCellArgs } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
-import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
-import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
+import { createRequiredContext } from '~/utils/createRequiredContext';
-export type RecordTableContextProps = {
+export type RecordTableContextValue = {
+ recordTableId: string;
viewBarId: string;
+ objectNameSingular: string;
objectMetadataItem: ObjectMetadataItem;
- onUpsertRecord: ({
- persistField,
- recordId,
- fieldName,
- }: {
- persistField: () => void;
- recordId: string;
- fieldName: string;
- }) => void;
- onOpenTableCell: (args: OpenTableCellArgs) => void;
- onMoveFocus: (direction: MoveFocusDirection) => void;
- onCloseTableCell: () => void;
- onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
- onActionMenuDropdownOpened: (
- event: React.MouseEvent,
- recordId: string,
- ) => void;
- onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
visibleTableColumns: ColumnDefinition[];
- recordTableId: string;
- objectNameSingular: string;
};
-export const RecordTableContext = createContext(
- {} as RecordTableContextProps,
-);
+export const [RecordTableContextProvider, useRecordTableContextOrThrow] =
+ createRequiredContext('RecordTableContext');
diff --git a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts
index ac7f91a6bcb1..3ff64f7adc3a 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/contexts/RecordTableRowContext.ts
@@ -11,6 +11,7 @@ export type RecordTableRowContextProps = {
isDragging: boolean;
dragHandleProps: DraggableProvidedDragHandleProps | null;
inView?: boolean;
+ isDragDisabled?: boolean;
};
export const RecordTableRowContext = createContext(
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 47988fbc57c7..680ab7b40684 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,16 +1,15 @@
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll';
import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter';
import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote';
import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
-import { useContext } from 'react';
export const RecordTableEmptyState = () => {
- const { objectNameSingular, recordTableId, objectMetadataItem } =
- useContext(RecordTableContext);
+ const { recordTableId, objectNameSingular, objectMetadataItem } =
+ useRecordTableContextOrThrow();
const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 });
const noRecordAtAll = totalCount === 0;
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 1d97ed9f461b..8b12987a0488 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
@@ -1,6 +1,5 @@
import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
-import { useContext } from 'react';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -29,7 +28,7 @@ export const RecordTableEmptyStateDisplay = ({
subTitle,
title,
}: RecordTableEmptyStateDisplayProps) => {
- const { objectMetadataItem } = useContext(RecordTableContext);
+ const { objectMetadataItem } = useRecordTableContextOrThrow();
const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem);
return (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx
index 00678d00ec30..5fdb72c75d55 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll.tsx
@@ -1,15 +1,14 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
-import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordAtAll = () => {
- const { createNewTableRecord } = useCreateNewTableRecord();
+ const { objectMetadataItem, recordTableId } = useRecordTableContextOrThrow();
- const { objectMetadataItem } = useContext(RecordTableContext);
+ const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
const handleButtonClick = () => {
createNewTableRecord();
diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx
index f733a636ddcc..3debe89d9a38 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter.tsx
@@ -1,15 +1,14 @@
import { IconPlus } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords';
-import { useContext } from 'react';
export const RecordTableEmptyStateNoRecordFoundForFilter = () => {
- const { createNewTableRecord } = useCreateNewTableRecord();
+ const { recordTableId, objectMetadataItem } = useRecordTableContextOrThrow();
- const { objectMetadataItem } = useContext(RecordTableContext);
+ const { createNewTableRecord } = useCreateNewTableRecord(recordTableId);
const handleButtonClick = () => {
createNewTableRecord();
diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx
index df31ce6a36ca..1321aca74a2e 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx
@@ -2,16 +2,15 @@ import { IconFilterOff } from 'twenty-ui';
import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
-import { useContext } from 'react';
export const RecordTableEmptyStateSoftDelete = () => {
const { objectMetadataItem, objectNameSingular, recordTableId } =
- useContext(RecordTableContext);
+ useRecordTableContextOrThrow();
const { deleteCombinedViewFilter } =
useDeleteCombinedViewFilters(recordTableId);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx
similarity index 60%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx
rename to packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx
index 779061ecd9c0..ee690ec18b88 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordInGroup.test.tsx
@@ -1,5 +1,5 @@
-import { act, renderHook } from '@testing-library/react';
-import { ReactNode } from 'react';
+import { renderHook } from '@testing-library/react';
+import { ReactNode, act } from 'react';
import { RecoilRoot } from 'recoil';
import { createState } from 'twenty-ui';
@@ -9,12 +9,16 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
-import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
+import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
+import { useUpsertTableRecordInGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup';
+import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
+import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const draftValue = 'updated Name';
+const recordGroupId = 'recordGroupId';
// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
// (this is not easy to maintain while refactoring)
@@ -37,8 +41,11 @@ jest.mock(
}),
);
-const pendingRecordIdState = createState({
- key: 'pendingRecordIdState',
+const recordTablePendingRecordIdByGroupComponentFamilyState = createFamilyState<
+ string | null,
+ string
+>({
+ key: 'recordTablePendingRecordIdByGroupComponentFamilyState',
defaultValue: null,
});
@@ -60,46 +67,64 @@ const Wrapper = ({
{
snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
- snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
+ snapshot.set(
+ recordTablePendingRecordIdByGroupComponentFamilyState(recordGroupId),
+ pendingRecordIdMockedValue,
+ );
snapshot.set(draftValueState, draftValueMockedValue);
}}
>
-
-
- {children}
-
-
+
+
+ {children}
+
+
+
+
);
-describe('useUpsertRecord', () => {
+describe('useUpsertTableRecordInGroup', () => {
beforeEach(async () => {
createOneRecordMock.mockClear();
updateOneRecordMock.mockClear();
});
it('calls update record if there is no pending record', async () => {
- const { result } = renderHook(
- () =>
- useUpsertRecord({
+ /**
+ * {
objectNameSingular: 'person',
recordTableId: 'recordTableId',
- }),
+ }
+ */
+ const { result } = renderHook(
+ () => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
@@ -111,7 +136,7 @@ describe('useUpsertRecord', () => {
);
await act(async () => {
- await result.current.upsertRecord(
+ await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',
@@ -124,11 +149,7 @@ describe('useUpsertRecord', () => {
it('calls update record if pending record is empty', async () => {
const { result } = renderHook(
- () =>
- useUpsertRecord({
- objectNameSingular: 'person',
- recordTableId: 'recordTableId',
- }),
+ () => useUpsertTableRecordInGroup(recordGroupId),
{
wrapper: ({ children }) =>
Wrapper({
@@ -140,7 +161,7 @@ describe('useUpsertRecord', () => {
);
await act(async () => {
- await result.current.upsertRecord(
+ await result.current.upsertTableRecordInGroup(
updateOneRecordMock,
'recordId',
'name',
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx
new file mode 100644
index 000000000000..d1f29e2c9afe
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/__tests__/useUpsertTableRecordNoGroup.test.tsx
@@ -0,0 +1,160 @@
+import { renderHook } from '@testing-library/react';
+import { ReactNode, act } from 'react';
+import { RecoilRoot } from 'recoil';
+import { createState } from 'twenty-ui';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
+import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
+import { useUpsertTableRecordNoGroup } from '@/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup';
+import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
+import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
+import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
+
+const draftValue = 'updated Name';
+
+// Todo refactor this test to inject the states in a cleaner way instead of mocking hooks
+// (this is not easy to maintain while refactoring)
+jest.mock('@/object-record/hooks/useCreateOneRecord', () => ({
+ __esModule: true,
+ useCreateOneRecord: jest.fn(),
+}));
+
+const draftValueState = createState({
+ key: 'draftValueState',
+ defaultValue: null,
+});
+jest.mock(
+ '@/object-record/record-field/hooks/internal/useRecordFieldInputStates',
+ () => ({
+ __esModule: true,
+ useRecordFieldInputStates: jest.fn(() => ({
+ getDraftValueSelector: () => draftValueState,
+ })),
+ }),
+);
+
+const pendingRecordIdState = createState({
+ key: 'pendingRecordIdState',
+ defaultValue: null,
+});
+
+const createOneRecordMock = jest.fn();
+const updateOneRecordMock = jest.fn();
+(useCreateOneRecord as jest.Mock).mockReturnValue({
+ createOneRecord: createOneRecordMock,
+});
+
+const Wrapper = ({
+ children,
+ pendingRecordIdMockedValue,
+ draftValueMockedValue,
+}: {
+ children: ReactNode;
+ pendingRecordIdMockedValue: string | null;
+ draftValueMockedValue: string | null;
+}) => (
+ {
+ snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
+ snapshot.set(pendingRecordIdState, pendingRecordIdMockedValue);
+ snapshot.set(draftValueState, draftValueMockedValue);
+ }}
+ >
+
+
+
+
+ {children}
+
+
+
+
+
+);
+
+describe('useUpsertTableRecordNoGroup', () => {
+ beforeEach(async () => {
+ createOneRecordMock.mockClear();
+ updateOneRecordMock.mockClear();
+ });
+
+ it('calls update record if there is no pending record', async () => {
+ /**
+ * {
+ objectNameSingular: 'person',
+ recordTableId: 'recordTableId',
+ }
+ */
+ const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
+ wrapper: ({ children }) =>
+ Wrapper({
+ pendingRecordIdMockedValue: null,
+ draftValueMockedValue: null,
+ children,
+ }),
+ });
+
+ await act(async () => {
+ await result.current.upsertTableRecordNoGroup(
+ updateOneRecordMock,
+ 'recordId',
+ 'name',
+ );
+ });
+
+ expect(createOneRecordMock).not.toHaveBeenCalled();
+ expect(updateOneRecordMock).toHaveBeenCalled();
+ });
+
+ it('calls update record if pending record is empty', async () => {
+ const { result } = renderHook(() => useUpsertTableRecordNoGroup(), {
+ wrapper: ({ children }) =>
+ Wrapper({
+ pendingRecordIdMockedValue: null,
+ draftValueMockedValue: draftValue,
+ children,
+ }),
+ });
+
+ await act(async () => {
+ await result.current.upsertTableRecordNoGroup(
+ updateOneRecordMock,
+ 'recordId',
+ 'name',
+ );
+ });
+
+ expect(createOneRecordMock).not.toHaveBeenCalled();
+ expect(updateOneRecordMock).toHaveBeenCalled();
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts
new file mode 100644
index 000000000000..cabcf7e6e681
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordInGroup.ts
@@ -0,0 +1,87 @@
+import { useRecoilCallback } from 'recoil';
+
+import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
+import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
+import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
+import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
+import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
+import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
+import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
+import { isDefined } from '~/utils/isDefined';
+
+export const useUpsertTableRecordInGroup = (recordGroupId: string) => {
+ const { objectMetadataItem, objectNameSingular } =
+ useRecordTableContextOrThrow();
+
+ const { createOneRecord } = useCreateOneRecord({
+ objectNameSingular,
+ shouldMatchRootQueryFilter: true,
+ });
+
+ const recordTablePendingRecordIdByGroupFamilyState =
+ useRecoilComponentCallbackStateV2(
+ recordTablePendingRecordIdByGroupComponentFamilyState,
+ );
+
+ const upsertTableRecordInGroup = useRecoilCallback(
+ ({ snapshot }) =>
+ (persistField: () => void, recordId: string, fieldName: string) => {
+ const labelIdentifierFieldMetadataItem =
+ getLabelIdentifierFieldMetadataItem(objectMetadataItem);
+
+ const fieldScopeId = getScopeIdFromComponentId(
+ `${recordId}-${fieldName}`,
+ );
+
+ const draftValueSelector = extractComponentSelector(
+ recordFieldInputDraftValueComponentSelector,
+ fieldScopeId,
+ );
+
+ const draftValue = getSnapshotValue(snapshot, draftValueSelector());
+
+ // We're in a record group
+ const recordTablePendingRecordId = getSnapshotValue(
+ snapshot,
+ recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
+ );
+
+ const recordGroupDefinition = getSnapshotValue(
+ snapshot,
+ recordGroupDefinitionFamilyState(recordGroupId),
+ );
+
+ const recordGroupFieldMetadataItem = objectMetadataItem.fields.find(
+ (fieldMetadata) =>
+ fieldMetadata.id === recordGroupDefinition?.fieldMetadataId,
+ );
+
+ if (
+ isDefined(recordTablePendingRecordId) &&
+ isDefined(recordGroupDefinition) &&
+ isDefined(recordGroupFieldMetadataItem) &&
+ isDefined(draftValue)
+ ) {
+ createOneRecord({
+ id: recordTablePendingRecordId,
+ [labelIdentifierFieldMetadataItem?.name ?? 'name']: draftValue,
+ [recordGroupFieldMetadataItem.name]: recordGroupDefinition.value,
+ position: 'first',
+ });
+ } else if (!recordTablePendingRecordId) {
+ persistField();
+ }
+ },
+ [
+ createOneRecord,
+ objectMetadataItem,
+ recordGroupId,
+ recordTablePendingRecordIdByGroupFamilyState,
+ ],
+ );
+
+ return { upsertTableRecordInGroup };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts
similarity index 65%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts
rename to packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts
index 083f244fc58d..c78a47d1edec 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useUpsertRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useUpsertTableRecordNoGroup.ts
@@ -1,32 +1,22 @@
import { useRecoilCallback } from 'recoil';
-import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { recordFieldInputDraftValueComponentSelector } from '@/object-record/record-field/states/selectors/recordFieldInputDraftValueComponentSelector';
-import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
-import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentSelector } from '@/ui/utilities/state/component-state/utils/extractComponentSelector';
import { isDefined } from '~/utils/isDefined';
-export const useUpsertRecord = ({
- objectNameSingular,
- recordTableId,
-}: {
- objectNameSingular: string;
- recordTableId: string;
-}) => {
- const hasRecordGroups = useRecoilComponentValueV2(
- hasRecordGroupsComponentSelector,
- );
+export const useUpsertTableRecordNoGroup = () => {
+ const { objectMetadataItem, objectNameSingular, recordTableId } =
+ useRecordTableContextOrThrow();
const { createOneRecord } = useCreateOneRecord({
objectNameSingular,
- shouldMatchRootQueryFilter: hasRecordGroups,
});
const recordTablePendingRecordIdState = useRecoilComponentCallbackStateV2(
@@ -34,28 +24,12 @@ export const useUpsertRecord = ({
recordTableId,
);
- const upsertRecord = useRecoilCallback(
+ const upsertTableRecordNoGroup = useRecoilCallback(
({ snapshot }) =>
(persistField: () => void, recordId: string, fieldName: string) => {
- const objectMetadataItems = snapshot
- .getLoadable(objectMetadataItemsState)
- .getValue();
-
- const foundObjectMetadataItem = objectMetadataItems.find(
- (item) => item.nameSingular === objectNameSingular,
- );
-
- if (!foundObjectMetadataItem) {
- throw new Error('Object metadata item cannot be found');
- }
-
const labelIdentifierFieldMetadataItem =
- getLabelIdentifierFieldMetadataItem(foundObjectMetadataItem);
+ getLabelIdentifierFieldMetadataItem(objectMetadataItem);
- const recordTablePendingRecordId = getSnapshotValue(
- snapshot,
- recordTablePendingRecordIdState,
- );
const fieldScopeId = getScopeIdFromComponentId(
`${recordId}-${fieldName}`,
);
@@ -67,6 +41,11 @@ export const useUpsertRecord = ({
const draftValue = getSnapshotValue(snapshot, draftValueSelector());
+ const recordTablePendingRecordId = getSnapshotValue(
+ snapshot,
+ recordTablePendingRecordIdState,
+ );
+
if (isDefined(recordTablePendingRecordId) && isDefined(draftValue)) {
createOneRecord({
id: recordTablePendingRecordId,
@@ -77,8 +56,8 @@ export const useUpsertRecord = ({
persistField();
}
},
- [createOneRecord, objectNameSingular, recordTablePendingRecordIdState],
+ [createOneRecord, objectMetadataItem, recordTablePendingRecordIdState],
);
- return { upsertRecord };
+ return { upsertTableRecordNoGroup };
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts
new file mode 100644
index 000000000000..8e2f0236474e
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecordInGroup.ts
@@ -0,0 +1,68 @@
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
+import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
+import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
+import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
+import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
+import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { useRecoilCallback } from 'recoil';
+import { v4 } from 'uuid';
+import { isDefined } from '~/utils/isDefined';
+
+export const useCreateNewTableRecordInGroup = () => {
+ const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow();
+
+ const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
+ scopeId: recordIndexId,
+ });
+
+ const setHotkeyScope = useSetHotkeyScope();
+
+ const recordTablePendingRecordIdByGroupFamilyState =
+ useRecoilComponentCallbackStateV2(
+ recordTablePendingRecordIdByGroupComponentFamilyState,
+ recordIndexId,
+ );
+
+ const { setActiveDropdownFocusIdAndMemorizePrevious } =
+ useSetActiveDropdownFocusIdAndMemorizePrevious();
+
+ const createNewTableRecordInGroup = useRecoilCallback(
+ ({ set }) =>
+ (recordGroupId: string) => {
+ const recordId = v4();
+
+ set(
+ recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
+ recordId,
+ );
+ setSelectedTableCellEditMode(-1, 0);
+ setHotkeyScope(
+ DEFAULT_CELL_SCOPE.scope,
+ DEFAULT_CELL_SCOPE.customScopes,
+ );
+
+ if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
+ setActiveDropdownFocusIdAndMemorizePrevious(
+ getDropdownFocusIdForRecordField(
+ recordId,
+ objectMetadataItem.labelIdentifierFieldMetadataId,
+ 'table-cell',
+ ),
+ );
+ }
+ },
+ [
+ objectMetadataItem,
+ recordTablePendingRecordIdByGroupFamilyState,
+ setActiveDropdownFocusIdAndMemorizePrevious,
+ setHotkeyScope,
+ setSelectedTableCellEditMode,
+ ],
+ );
+
+ return {
+ createNewTableRecordInGroup,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
index 15056d35102d..8e3739fdcdf6 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useCreateNewTableRecords.ts
@@ -1,33 +1,94 @@
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
-import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
import { useSelectedTableCellEditMode } from '@/object-record/record-table/record-table-cell/hooks/useSelectedTableCellEditMode';
+import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
+import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
+import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
+import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
-import { useContext } from 'react';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
+import { useRecoilCallback } from 'recoil';
import { v4 } from 'uuid';
+import { isDefined } from '~/utils/isDefined';
-export const useCreateNewTableRecord = (recordTableIdFromProps?: string) => {
- const { recordTableId } = useContext(RecordTableContext);
-
- const recordTableIdToUse = recordTableIdFromProps ?? recordTableId;
+export const useCreateNewTableRecord = (recordTableId: string) => {
+ const { objectMetadataItem } = useRecordIndexContextOrThrow();
const { setSelectedTableCellEditMode } = useSelectedTableCellEditMode({
- scopeId: recordTableIdToUse,
+ scopeId: recordTableId,
});
const setHotkeyScope = useSetHotkeyScope();
- const { setPendingRecordId } = useRecordTable({
- recordTableId: recordTableIdToUse,
- });
+ const setPendingRecordId = useSetRecoilComponentStateV2(
+ recordTablePendingRecordIdComponentState,
+ recordTableId,
+ );
+
+ const recordTablePendingRecordIdByGroupFamilyState =
+ useRecoilComponentCallbackStateV2(
+ recordTablePendingRecordIdByGroupComponentFamilyState,
+ recordTableId,
+ );
+
+ const { setActiveDropdownFocusIdAndMemorizePrevious } =
+ useSetActiveDropdownFocusIdAndMemorizePrevious();
const createNewTableRecord = () => {
- setPendingRecordId(v4());
+ const recordId = v4();
+
+ setPendingRecordId(recordId);
setSelectedTableCellEditMode(-1, 0);
setHotkeyScope(DEFAULT_CELL_SCOPE.scope, DEFAULT_CELL_SCOPE.customScopes);
+
+ if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
+ setActiveDropdownFocusIdAndMemorizePrevious(
+ getDropdownFocusIdForRecordField(
+ recordId,
+ objectMetadataItem.labelIdentifierFieldMetadataId,
+ 'table-cell',
+ ),
+ );
+ }
};
+ const createNewTableRecordInGroup = useRecoilCallback(
+ ({ set }) =>
+ (recordGroupId: string) => {
+ const recordId = v4();
+
+ set(
+ recordTablePendingRecordIdByGroupFamilyState(recordGroupId),
+ recordId,
+ );
+ setSelectedTableCellEditMode(-1, 0);
+ setHotkeyScope(
+ DEFAULT_CELL_SCOPE.scope,
+ DEFAULT_CELL_SCOPE.customScopes,
+ );
+
+ if (isDefined(objectMetadataItem.labelIdentifierFieldMetadataId)) {
+ setActiveDropdownFocusIdAndMemorizePrevious(
+ getDropdownFocusIdForRecordField(
+ recordId,
+ objectMetadataItem.labelIdentifierFieldMetadataId,
+ 'table-cell',
+ ),
+ );
+ }
+ },
+ [
+ objectMetadataItem,
+ recordTablePendingRecordIdByGroupFamilyState,
+ setActiveDropdownFocusIdAndMemorizePrevious,
+ setHotkeyScope,
+ setSelectedTableCellEditMode,
+ ],
+ );
+
return {
createNewTableRecord,
+ createNewTableRecordInGroup,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
index 65036a78e1c2..609c3f9080ac 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts
@@ -22,7 +22,6 @@ import { onColumnsChangeComponentState } from '@/object-record/record-table/stat
import { onEntityCountChangeComponentState } from '@/object-record/record-table/states/onEntityCountChangeComponentState';
import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState';
import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState';
-import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
import { tableFiltersComponentState } from '@/object-record/record-table/states/tableFiltersComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
@@ -240,11 +239,6 @@ export const useRecordTable = (props?: useRecordTableProps) => {
const isSomeCellInEditModeState =
useGetIsSomeCellInEditModeState(recordTableId);
- const setPendingRecordId = useSetRecoilComponentStateV2(
- recordTablePendingRecordIdComponentState,
- recordTableId,
- );
-
return {
onColumnsChange,
setAvailableTableColumns,
@@ -272,6 +266,5 @@ export const useRecordTable = (props?: useRecordTableProps) => {
setHasUserSelectedAllRows,
setOnToggleColumnFilter,
setOnToggleColumnSort,
- setPendingRecordId,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx
index aced5e0b3c38..dc74cff107d7 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider.tsx
@@ -1,12 +1,12 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
-import { ReactNode, useContext } from 'react';
+import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
@@ -18,7 +18,7 @@ export const RecordTableBodyDragDropContextProvider = ({
}: {
children: ReactNode;
}) => {
- const { objectNameSingular, recordTableId } = useContext(RecordTableContext);
+ const { objectNameSingular, recordTableId } = useRecordTableContextOrThrow();
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx
index 6e229447c9fd..98474d1efa9c 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider.tsx
@@ -1,5 +1,5 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
-import { ReactNode, useContext } from 'react';
+import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
@@ -7,7 +7,7 @@ import { getDraggedRecordPosition } from '@/object-record/record-board/utils/get
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@@ -20,7 +20,7 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
children: ReactNode;
}) => {
const { objectNameSingular, recordTableId, objectMetadataItem } =
- useContext(RecordTableContext);
+ useRecordTableContextOrThrow();
const { updateOneRecord: updateOneRow } = useUpdateOneRecord({
objectNameSingular,
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx
index 428da6e04ec1..4381106a8475 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyUnselectEffect.tsx
@@ -1,6 +1,7 @@
import { Key } from 'ts-key-enum';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
@@ -9,13 +10,13 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
type RecordTableBodyUnselectEffectProps = {
tableBodyRef: React.RefObject;
- recordTableId: string;
};
export const RecordTableBodyUnselectEffect = ({
tableBodyRef,
- recordTableId,
}: RecordTableBodyUnselectEffectProps) => {
+ const { recordTableId } = useRecordTableContextOrThrow();
+
const leaveTableFocus = useLeaveTableFocus(recordTableId);
const { resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx
index 47e7e5f8f171..dec939125df0 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBody.tsx
@@ -1,4 +1,5 @@
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
+import { RecordTableNoRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableNoRecordGroupBodyContextProvider';
import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows';
import { RecordTableBodyDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContextProvider';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
@@ -21,11 +22,13 @@ export const RecordTableNoRecordGroupBody = () => {
}
return (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx
index f2c2fb0f6ff7..8e3466afd336 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useDebouncedCallback } from 'use-debounce';
@@ -6,7 +6,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { tableEncounteredUnrecoverableErrorComponentState } from '@/object-record/record-table/states/tableEncounteredUnrecoverableErrorComponentState';
import { tableLastRowVisibleComponentState } from '@/object-record/record-table/states/tableLastRowVisibleComponentState';
@@ -18,7 +18,7 @@ import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableNoRecordGroupBodyEffect = () => {
- const { objectNameSingular } = useContext(RecordTableContext);
+ const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx
index 90569f73e345..e68e4ad15d00 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect.tsx
@@ -1,4 +1,4 @@
-import { useContext, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -7,13 +7,13 @@ import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useC
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
import { ROW_HEIGHT } from '@/object-record/record-table/constants/RowHeight';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
import { isNonEmptyString, isNull } from '@sniptt/guards';
import { useScrollToPosition } from '~/hooks/useScrollToPosition';
export const RecordTableRecordGroupBodyEffect = () => {
- const { objectNameSingular } = useContext(RecordTableContext);
+ const { objectNameSingular } = useRecordTableContextOrThrow();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx
index 6cfa2c722ad6..4d1a5d544e4e 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody.tsx
@@ -1,13 +1,13 @@
import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext';
import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
+import { RecordTableRecordGroupBodyContextProvider } from '@/object-record/record-table/components/RecordTableRecordGroupBodyContextProvider';
import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows';
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
-import { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow';
+import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
-import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionLoadMore';
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@@ -30,20 +30,19 @@ export const RecordTableRecordGroupsBody = () => {
return (
-
-
-
- {visibleRecordGroupIds.map((recordGroupId) => (
- (
+
-
-
-
-
-
-
+ {index > 0 && }
+
+
+
+
+
+
+
))}
);
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 b7ef7b3934b9..eb6cd16dc65d 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
@@ -5,8 +5,8 @@ import { BORDER_COMMON, ThemeContext } from 'twenty-ui';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useFieldFocus } from '@/object-record/record-field/hooks/useFieldFocus';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import {
DEFAULT_CELL_SCOPE,
useOpenRecordTableCellFromCell,
@@ -47,7 +47,7 @@ export const RecordTableCellBaseContainer = ({
const { hasSoftFocus, cellPosition } = useContext(RecordTableCellContext);
const { onMoveSoftFocusToCell, onCellMouseEnter } =
- useContext(RecordTableContext);
+ useRecordTableBodyContextOrThrow();
const handleContainerMouseMove = () => {
setIsFocused(true);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx
index 868990a61ff7..6814c021e76a 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellDisplayMode.tsx
@@ -1,5 +1,5 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { useContext } from 'react';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
@@ -7,9 +7,10 @@ export const RecordTableCellDisplayMode = ({
children,
softFocus,
}: React.PropsWithChildren<{ softFocus?: boolean }>) => {
- const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
const { recordId } = useContext(FieldContext);
+ const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
+
const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx
index 2f35fb265200..d04920b67014 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper.tsx
@@ -6,7 +6,7 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
import { RecordUpdateContext } from '@/object-record/record-table/contexts/EntityUpdateMutationHookContext';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope';
@@ -18,8 +18,10 @@ export const RecordTableCellFieldContextWrapper = ({
}: {
children: ReactNode;
}) => {
- const { objectMetadataItem } = useContext(RecordTableContext);
+ const { objectMetadataItem } = useRecordTableContextOrThrow();
+
const { columnDefinition } = useContext(RecordTableCellContext);
+
const { recordId, pathToShowPage } = useContext(RecordTableRowContext);
const updateRecord = useContext(RecordUpdateContext);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
index beba813cceba..c482690b3b41 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellFieldInput.tsx
@@ -4,17 +4,18 @@ import { FieldInput } from '@/object-record/record-field/components/FieldInput';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
import { FieldInputEvent } from '@/object-record/record-field/types/FieldInputEvent';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { getRecordFieldInputId } from '@/object-record/utils/getRecordFieldInputId';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useRecoilCallback } from 'recoil';
export const RecordTableCellFieldInput = () => {
+ const { recordId, fieldDefinition } = useContext(FieldContext);
+
const { onUpsertRecord, onMoveFocus, onCloseTableCell } =
- useContext(RecordTableContext);
+ useRecordTableBodyContextOrThrow();
- const { recordId, fieldDefinition } = useContext(FieldContext);
const isFieldReadOnly = useIsFieldValueReadOnly();
const handleEnter: FieldInputEvent = (persistField) => {
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx
index d77131dd3093..f590e5ccb5f1 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellGrip.tsx
@@ -3,19 +3,25 @@ import { useContext } from 'react';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
+import { css } from '@emotion/react';
import { IconListViewGrip } from 'twenty-ui';
-const StyledContainer = styled.div`
+const StyledContainer = styled.div<{ isPendingRow?: boolean }>`
+ border-color: transparent;
cursor: grab;
- width: 16px;
- height: 32px;
- z-index: 200;
display: flex;
- &:hover .icon {
- opacity: 1;
- }
+ height: 32px;
+ width: 16px;
+ ${({ isPendingRow }) =>
+ !isPendingRow
+ ? css`
+ &:hover .icon {
+ opacity: 1;
+ }
+ `
+ : ''};
- border-color: transparent;
+ z-index: 200;
`;
const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
@@ -24,7 +30,9 @@ const StyledIconWrapper = styled.div<{ isDragging: boolean }>`
`;
export const RecordTableCellGrip = () => {
- const { dragHandleProps, isDragging } = useContext(RecordTableRowContext);
+ const { dragHandleProps, isDragging, isPendingRow } = useContext(
+ RecordTableRowContext,
+ );
return (
{
hasRightBorder={false}
hasBottomBorder={false}
>
-
+
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
index b159c79c0c5e..0a327d097c6f 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx
@@ -21,7 +21,7 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useIsFieldValueReadOnly';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableCellDisplayContainer } from './RecordTableCellDisplayContainer';
type RecordTableCellSoftFocusModeProps = {
@@ -36,6 +36,8 @@ export const RecordTableCellSoftFocusMode = ({
const { columnIndex } = useContext(RecordTableCellContext);
const { recordId } = useContext(FieldContext);
+ const { onActionMenuDropdownOpened } = useRecordTableBodyContextOrThrow();
+
const isFieldReadOnly = useIsFieldValueReadOnly();
const { openTableCell } = useOpenRecordTableCellFromCell();
@@ -135,8 +137,6 @@ export const RecordTableCellSoftFocusMode = ({
*/
};
- const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
-
const handleActionMenuDropdown = (event: React.MouseEvent) => {
onActionMenuDropdownOpened(event, recordId);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx
new file mode 100644
index 000000000000..146ea602e22d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellInGroup.test.tsx
@@ -0,0 +1,101 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot } from 'recoil';
+
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
+import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
+import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
+import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
+import {
+ recordTableCell,
+ recordTableRow,
+} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
+import { useCloseRecordTableCellInGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup';
+import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
+import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
+import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
+import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
+import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
+import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
+
+const setHotkeyScope = jest.fn();
+
+jest.mock('@/ui/utilities/hotkey/hooks/useSetHotkeyScope', () => ({
+ useSetHotkeyScope: () => setHotkeyScope,
+}));
+
+const onColumnsChange = jest.fn();
+const recordTableId = 'scopeId';
+const recordGroupId = 'recordGroupId';
+
+const Wrapper = ({ children }: { children: React.ReactNode }) => (
+ {
+ snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
+ }}
+ >
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+);
+
+describe('useCloseRecordTableCellInGroup', () => {
+ it('should work as expected', async () => {
+ const { result } = renderHook(
+ () => {
+ const currentTableCellInEditModePosition = useRecoilComponentValueV2(
+ currentTableCellInEditModePositionComponentState,
+ );
+ const isTableCellInEditMode = useRecoilComponentFamilyValueV2(
+ isTableCellInEditModeComponentFamilyState,
+ currentTableCellInEditModePosition,
+ );
+ return {
+ ...useCloseRecordTableCellInGroup(recordGroupId),
+ ...useDragSelect(),
+ isTableCellInEditMode,
+ };
+ },
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+ act(() => {
+ result.current.closeTableCellInGroup();
+ });
+
+ expect(result.current.isDragSelectionStartEnabled()).toBe(true);
+ expect(result.current.isTableCellInEditMode).toBe(false);
+ expect(setHotkeyScope).toHaveBeenCalledWith('table-soft-focus');
+ });
+});
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx
similarity index 62%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx
rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx
index f3ce01a01759..37eda06f10ae 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/__tests__/useCloseRecordTableCellNoGroup.test.tsx
@@ -1,22 +1,26 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
+import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fieldDefinitions';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { RecordTableComponentInstance } from '@/object-record/record-table/components/RecordTableComponentInstance';
+import { RecordTableContextProvider } from '@/object-record/record-table/components/RecordTableContextProvider';
import { RecordTableCellContext } from '@/object-record/record-table/contexts/RecordTableCellContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import {
recordTableCell,
recordTableRow,
} from '@/object-record/record-table/record-table-cell/hooks/__mocks__/cell';
-import { useCloseRecordTableCell } from '@/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell';
+import { useCloseRecordTableCellNoGroup } from '@/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup';
import { currentTableCellInEditModePositionComponentState } from '@/object-record/record-table/states/currentTableCellInEditModePositionComponentState';
import { isTableCellInEditModeComponentFamilyState } from '@/object-record/record-table/states/isTableCellInEditModeComponentFamilyState';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
+import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const setHotkeyScope = jest.fn();
@@ -28,32 +32,42 @@ const onColumnsChange = jest.fn();
const recordTableId = 'scopeId';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
-
+ {
+ snapshot.set(objectMetadataItemsState, generatedMockObjectMetadataItems);
+ }}
+ >
-
-
-
- {children}
-
-
-
+
+
+
+ {children}
+
+
+
+
);
-describe('useCloseRecordTableCell', () => {
+describe('useCloseRecordTableCellNoGroup', () => {
it('should work as expected', async () => {
const { result } = renderHook(
() => {
@@ -65,7 +79,7 @@ describe('useCloseRecordTableCell', () => {
currentTableCellInEditModePosition,
);
return {
- ...useCloseRecordTableCell(),
+ ...useCloseRecordTableCellNoGroup(),
...useDragSelect(),
isTableCellInEditMode,
};
@@ -76,7 +90,7 @@ describe('useCloseRecordTableCell', () => {
);
act(() => {
- result.current.closeTableCell();
+ result.current.closeTableCellNoGroup();
});
expect(result.current.isDragSelectionStartEnabled()).toBe(true);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts
new file mode 100644
index 000000000000..926dade1b55d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellInGroup.ts
@@ -0,0 +1,56 @@
+import { useRecoilCallback } from 'recoil';
+
+import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
+import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
+
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
+import { recordTablePendingRecordIdByGroupComponentFamilyState } from '@/object-record/record-table/states/recordTablePendingRecordIdByGroupComponentFamilyState';
+import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
+import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
+
+export const useCloseRecordTableCellInGroup = (recordGroupId: string) => {
+ const { recordTableId } = useRecordTableContextOrThrow();
+
+ const setHotkeyScope = useSetHotkeyScope();
+ const { setDragSelectionStartEnabled } = useDragSelect();
+
+ const { toggleClickOutsideListener } = useClickOutsideListener(
+ SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
+ );
+
+ const closeCurrentTableCellInEditMode =
+ useCloseCurrentTableCellInEditMode(recordTableId);
+
+ const recordTablePendingRecordIdByGroupFamilyState =
+ useRecoilComponentCallbackStateV2(
+ recordTablePendingRecordIdByGroupComponentFamilyState,
+ recordTableId,
+ );
+
+ const closeTableCellInGroup = useRecoilCallback(
+ ({ reset }) =>
+ () => {
+ toggleClickOutsideListener(true);
+ setDragSelectionStartEnabled(true);
+ closeCurrentTableCellInEditMode();
+ setHotkeyScope(TableHotkeyScope.TableSoftFocus);
+
+ reset(recordTablePendingRecordIdByGroupFamilyState(recordGroupId));
+ },
+ [
+ closeCurrentTableCellInEditMode,
+ recordGroupId,
+ recordTablePendingRecordIdByGroupFamilyState,
+ setDragSelectionStartEnabled,
+ setHotkeyScope,
+ toggleClickOutsideListener,
+ ],
+ );
+
+ return {
+ closeTableCellInGroup,
+ };
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts
similarity index 66%
rename from packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts
rename to packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts
index 204406f88613..f9138eb3f805 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCellV2.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/internal/useCloseRecordTableCellNoGroup.ts
@@ -5,13 +5,18 @@ import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
+import { useCloseCurrentTableCellInEditMode } from '@/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode';
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
+import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
-import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
-import { TableHotkeyScope } from '../../types/TableHotkeyScope';
+import { useCallback } from 'react';
+
+export const useCloseRecordTableCellNoGroup = () => {
+ const { recordTableId } = useRecordTableContextOrThrow();
-export const useCloseRecordTableCellV2 = (recordTableId: string) => {
const setHotkeyScope = useSetHotkeyScope();
+
const { setDragSelectionStartEnabled } = useDragSelect();
const { toggleClickOutsideListener } = useClickOutsideListener(
@@ -25,18 +30,25 @@ export const useCloseRecordTableCellV2 = (recordTableId: string) => {
recordTablePendingRecordIdComponentState,
recordTableId,
);
+
const resetRecordTablePendingRecordId =
useResetRecoilState(pendingRecordIdState);
- const closeTableCell = async () => {
+ const closeTableCellNoGroup = useCallback(() => {
toggleClickOutsideListener(true);
setDragSelectionStartEnabled(true);
closeCurrentTableCellInEditMode();
setHotkeyScope(TableHotkeyScope.TableSoftFocus);
resetRecordTablePendingRecordId();
- };
+ }, [
+ closeCurrentTableCellInEditMode,
+ resetRecordTablePendingRecordId,
+ setDragSelectionStartEnabled,
+ setHotkeyScope,
+ toggleClickOutsideListener,
+ ]);
return {
- closeTableCell,
+ closeTableCellNoGroup,
};
};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts
deleted file mode 100644
index 36b32003b7d8..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useCloseRecordTableCell.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
-import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect';
-import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
-import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
-
-import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
-import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
-import { useResetRecoilState } from 'recoil';
-import { useCloseCurrentTableCellInEditMode } from '../../hooks/internal/useCloseCurrentTableCellInEditMode';
-import { TableHotkeyScope } from '../../types/TableHotkeyScope';
-
-export const useCloseRecordTableCell = () => {
- const setHotkeyScope = useSetHotkeyScope();
- const { setDragSelectionStartEnabled } = useDragSelect();
-
- const { toggleClickOutsideListener } = useClickOutsideListener(
- SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
- );
-
- const closeCurrentTableCellInEditMode = useCloseCurrentTableCellInEditMode();
- const pendingRecordIdState = useRecoilComponentCallbackStateV2(
- recordTablePendingRecordIdComponentState,
- );
- const resetRecordTablePendingRecordId =
- useResetRecoilState(pendingRecordIdState);
-
- const closeTableCell = async () => {
- toggleClickOutsideListener(true);
- setDragSelectionStartEnabled(true);
- closeCurrentTableCellInEditMode();
- setHotkeyScope(TableHotkeyScope.TableSoftFocus);
- resetRecordTablePendingRecordId();
- };
-
- return {
- closeTableCell,
- };
-};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
index 67583b2f94a1..326947483055 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellFromCell.ts
@@ -5,12 +5,12 @@ import { useIsFieldValueReadOnly } from '@/object-record/record-field/hooks/useI
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { CellHotkeyScopeContext } from '@/object-record/record-table/contexts/CellHotkeyScopeContext';
-import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
import { useCurrentTableCellPosition } from '@/object-record/record-table/record-table-cell/hooks/useCurrentCellPosition';
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
+import { useRecordTableBodyContextOrThrow } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
export const DEFAULT_CELL_SCOPE: HotkeyScope = {
@@ -28,13 +28,16 @@ export type OpenTableCellArgs = {
};
export const useOpenRecordTableCellFromCell = () => {
- const { onOpenTableCell } = useContext(RecordTableContext);
- const cellPosition = useCurrentTableCellPosition();
const customCellHotkeyScope = useContext(CellHotkeyScopeContext);
const { recordId, fieldDefinition } = useContext(FieldContext);
const { pathToShowPage, objectNameSingular } = useContext(
RecordTableRowContext,
);
+
+ const { onOpenTableCell } = useRecordTableBodyContextOrThrow();
+
+ const cellPosition = useCurrentTableCellPosition();
+
const isFieldReadOnly = useIsFieldValueReadOnly();
const openTableCell = (
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
index 7e885b4ada80..514bbda1e1e6 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2.ts
@@ -20,12 +20,11 @@ import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useC
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from '~/utils/isDefined';
-import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
+import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
-import { useContext } from 'react';
import { useNavigate } from 'react-router-dom';
import { TableHotkeyScope } from '../../types/TableHotkeyScope';
@@ -49,7 +48,7 @@ export const useOpenRecordTableCellV2 = (tableScopeId: string) => {
const { getClickOutsideListenerIsActivatedState } =
useClickOustideListenerStates(RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID);
- const { indexIdentifierUrl } = useContext(RecordIndexRootPropsContext);
+ const { indexIdentifierUrl } = useRecordIndexContextOrThrow();
const moveEditModeToTableCellPosition =
useMoveEditModeToTableCellPosition(tableScopeId);
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 9c28df97a983..ae08ea612f3c 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
@@ -1,12 +1,11 @@
import styled from '@emotion/styled';
import { MOBILE_VIEWPORT } from 'twenty-ui';
+import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableHeaderCell } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCell';
import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn';
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
-import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
-import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
const StyledTableHead = styled.thead`
cursor: pointer;
@@ -77,14 +76,8 @@ const StyledTableHead = styled.thead`
}
`;
-export const RecordTableHeader = ({
- objectNameSingular,
-}: {
- objectNameSingular: string;
-}) => {
- const visibleTableColumns = useRecoilComponentValueV2(
- visibleTableColumnsComponentSelector,
- );
+export const RecordTableHeader = () => {
+ const { visibleTableColumns } = useRecordTableContextOrThrow();
return (