diff --git a/.gitignore b/.gitignore index 20f974922280..48296f0cb14f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,13 @@ storybook-static test-results/ dump.rdb .tinyb + +.notes +/data/ +/.devenv/ +/.direnv/ +/.pre-commit-config.yaml +/.envrc +/devenv.nix +/flake.lock +/flake.nix diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index de35163609bc..513273ddde0e 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -477,10 +477,12 @@ export type Mutation = { updateOneServerlessFunction: ServerlessFunction; updatePasswordViaResetToken: InvalidatePassword; updateWorkspace: Workspace; + updateWorkspaceFeatureFlag: Scalars['Boolean']; uploadFile: Scalars['String']; uploadImage: Scalars['String']; uploadProfilePicture: Scalars['String']; uploadWorkspaceLogo: Scalars['String']; + userLookupAdminPanel: UserLookup; verify: Verify; }; @@ -679,6 +681,13 @@ export type MutationUpdateWorkspaceArgs = { }; +export type MutationUpdateWorkspaceFeatureFlagArgs = { + featureFlag: Scalars['String']; + value: Scalars['Boolean']; + workspaceId: Scalars['String']; +}; + + export type MutationUploadFileArgs = { file: Scalars['Upload']; fileFolder?: InputMaybe; @@ -701,6 +710,11 @@ export type MutationUploadWorkspaceLogoArgs = { }; +export type MutationUserLookupAdminPanelArgs = { + userIdentifier: Scalars['String']; +}; + + export type MutationVerifyArgs = { loginToken: Scalars['String']; }; @@ -1247,6 +1261,20 @@ export type UserExists = { exists: Scalars['Boolean']; }; +export type UserInfo = { + __typename?: 'UserInfo'; + email: Scalars['String']; + firstName?: Maybe; + id: Scalars['String']; + lastName?: Maybe; +}; + +export type UserLookup = { + __typename?: 'UserLookup'; + user: UserInfo; + workspaces: Array; +}; + export type UserMappingOptionsUser = { __typename?: 'UserMappingOptionsUser'; user?: Maybe; @@ -1285,6 +1313,7 @@ export type Workspace = { __typename?: 'Workspace'; activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; + billingEntitlements?: Maybe>; billingSubscriptions?: Maybe>; createdAt: Scalars['DateTime']; currentBillingSubscription?: Maybe; @@ -1305,6 +1334,12 @@ export type Workspace = { }; +export type WorkspaceBillingEntitlementsArgs = { + filter?: BillingEntitlementFilter; + sorting?: Array; +}; + + export type WorkspaceBillingSubscriptionsArgs = { filter?: BillingSubscriptionFilter; sorting?: Array; @@ -1331,6 +1366,16 @@ export type WorkspaceEdge = { node: Workspace; }; +export type WorkspaceInfo = { + __typename?: 'WorkspaceInfo'; + featureFlags: Array; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + totalUsers: Scalars['Float']; + users: Array; +}; + export type WorkspaceInvitation = { __typename?: 'WorkspaceInvitation'; email: Scalars['String']; @@ -1376,6 +1421,30 @@ export type WorkspaceNameAndId = { id: Scalars['String']; }; +export type BillingEntitlement = { + __typename?: 'billingEntitlement'; + id: Scalars['UUID']; + key: Scalars['String']; + value: Scalars['Boolean']; + workspaceId: Scalars['String']; +}; + +export type BillingEntitlementFilter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type BillingEntitlementSort = { + direction: SortDirection; + field: BillingEntitlementSortFields; + nulls?: InputMaybe; +}; + +export enum BillingEntitlementSortFields { + Id = 'id' +} + export type Field = { __typename?: 'field'; createdAt: Scalars['DateTime']; @@ -1787,6 +1856,22 @@ export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string] export type SkipSyncEmailOnboardingStepMutation = { __typename?: 'Mutation', skipSyncEmailOnboardingStep: { __typename?: 'OnboardingStepSuccess', success: boolean } }; +export type UpdateWorkspaceFeatureFlagMutationVariables = Exact<{ + workspaceId: Scalars['String']; + featureFlag: Scalars['String']; + value: Scalars['Boolean']; +}>; + + +export type UpdateWorkspaceFeatureFlagMutation = { __typename?: 'Mutation', updateWorkspaceFeatureFlag: boolean }; + +export type UserLookupAdminPanelMutationVariables = Exact<{ + userIdentifier: Scalars['String']; +}>; + + +export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: string, value: boolean }> }> } }; + export type CreateOidcIdentityProviderMutationVariables = Exact<{ input: SetupOidcSsoInput; }>; @@ -3178,6 +3263,97 @@ export function useSkipSyncEmailOnboardingStepMutation(baseOptions?: Apollo.Muta export type SkipSyncEmailOnboardingStepMutationHookResult = ReturnType; export type SkipSyncEmailOnboardingStepMutationResult = Apollo.MutationResult; export type SkipSyncEmailOnboardingStepMutationOptions = Apollo.BaseMutationOptions; +export const UpdateWorkspaceFeatureFlagDocument = gql` + mutation UpdateWorkspaceFeatureFlag($workspaceId: String!, $featureFlag: String!, $value: Boolean!) { + updateWorkspaceFeatureFlag( + workspaceId: $workspaceId + featureFlag: $featureFlag + value: $value + ) +} + `; +export type UpdateWorkspaceFeatureFlagMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateWorkspaceFeatureFlagMutation__ + * + * To run a mutation, you first call `useUpdateWorkspaceFeatureFlagMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateWorkspaceFeatureFlagMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateWorkspaceFeatureFlagMutation, { data, loading, error }] = useUpdateWorkspaceFeatureFlagMutation({ + * variables: { + * workspaceId: // value for 'workspaceId' + * featureFlag: // value for 'featureFlag' + * value: // value for 'value' + * }, + * }); + */ +export function useUpdateWorkspaceFeatureFlagMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateWorkspaceFeatureFlagDocument, options); + } +export type UpdateWorkspaceFeatureFlagMutationHookResult = ReturnType; +export type UpdateWorkspaceFeatureFlagMutationResult = Apollo.MutationResult; +export type UpdateWorkspaceFeatureFlagMutationOptions = Apollo.BaseMutationOptions; +export const UserLookupAdminPanelDocument = gql` + mutation UserLookupAdminPanel($userIdentifier: String!) { + userLookupAdminPanel(userIdentifier: $userIdentifier) { + user { + id + email + firstName + lastName + } + workspaces { + id + name + logo + totalUsers + users { + id + email + firstName + lastName + } + featureFlags { + key + value + } + } + } +} + `; +export type UserLookupAdminPanelMutationFn = Apollo.MutationFunction; + +/** + * __useUserLookupAdminPanelMutation__ + * + * To run a mutation, you first call `useUserLookupAdminPanelMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUserLookupAdminPanelMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [userLookupAdminPanelMutation, { data, loading, error }] = useUserLookupAdminPanelMutation({ + * variables: { + * userIdentifier: // value for 'userIdentifier' + * }, + * }); + */ +export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UserLookupAdminPanelDocument, options); + } +export type UserLookupAdminPanelMutationHookResult = ReturnType; +export type UserLookupAdminPanelMutationResult = Apollo.MutationResult; +export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions; export const CreateOidcIdentityProviderDocument = gql` mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { createOIDCIdentityProvider(input: $input) { diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index a7b683e660f7..d5166179e1ca 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -234,17 +234,6 @@ const testCases = [ { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, { loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Impersonate, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam }, - { loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined }, - { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, { loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' }, diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index 45aa98098643..1491921e32fa 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,4 +1,5 @@ import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; +import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; @@ -16,6 +17,10 @@ export const AppRouter = () => { const isBillingPageEnabled = billing?.isBillingEnabled && !isFreeAccessEnabled; + const currentUser = useRecoilValue(currentUserState); + + const isAdminPageEnabled = currentUser?.canImpersonate; + return ( { isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, isSSOEnabled, + isAdminPageEnabled, )} /> ); diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index b758acdc1177..f8286c398b7f 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -242,11 +242,26 @@ const SettingsSecuritySSOIdentifyProvider = lazy(() => ), ); +const SettingsAdmin = lazy(() => + import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({ + default: module.SettingsAdmin, + })), +); + +const SettingsAdminFeatureFlags = lazy(() => + import('~/pages/settings/admin-panel/SettingsAdminFeatureFlags').then( + (module) => ({ + default: module.SettingsAdminFeatureFlags, + }), + ), +); + type SettingsRoutesProps = { isBillingEnabled?: boolean; isCRMMigrationEnabled?: boolean; isServerlessFunctionSettingsEnabled?: boolean; isSSOEnabled?: boolean; + isAdminPageEnabled?: boolean; }; export const SettingsRoutes = ({ @@ -254,6 +269,7 @@ export const SettingsRoutes = ({ isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, isSSOEnabled, + isAdminPageEnabled, }: SettingsRoutesProps) => ( }> @@ -375,6 +391,15 @@ export const SettingsRoutes = ({ /> )} + {isAdminPageEnabled && ( + <> + } /> + } + /> + + )} ); diff --git a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx index 0aa19e6e16cb..80afc3c8af56 100644 --- a/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx +++ b/packages/twenty-front/src/modules/app/hooks/useCreateAppRouter.tsx @@ -14,7 +14,6 @@ import { Authorize } from '~/pages/auth/Authorize'; import { Invite } from '~/pages/auth/Invite'; import { PasswordReset } from '~/pages/auth/PasswordReset'; import { SignInUp } from '~/pages/auth/SignInUp'; -import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; import { NotFound } from '~/pages/not-found/NotFound'; import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; @@ -30,6 +29,7 @@ export const useCreateAppRouter = ( isCRMMigrationEnabled?: boolean, isServerlessFunctionSettingsEnabled?: boolean, isSSOEnabled?: boolean, + isAdminPageEnabled?: boolean, ) => createBrowserRouter( createRoutesFromElements( @@ -54,7 +54,6 @@ export const useCreateAppRouter = ( element={} /> } /> - } /> } /> } /> } /> diff --git a/packages/twenty-front/src/modules/auth/components/Logo.tsx b/packages/twenty-front/src/modules/auth/components/Logo.tsx index 34ddf069cd91..9bc9c94aee94 100644 --- a/packages/twenty-front/src/modules/auth/components/Logo.tsx +++ b/packages/twenty-front/src/modules/auth/components/Logo.tsx @@ -3,65 +3,62 @@ import styled from '@emotion/styled'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; type LogoProps = { - workspaceLogo?: string | null; + primaryLogo?: string | null; + secondaryLogo?: string | null; }; const StyledContainer = styled.div` - height: 48px; + height: ${({ theme }) => theme.spacing(12)}; margin-bottom: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(4)}; position: relative; - width: 48px; + width: ${({ theme }) => theme.spacing(12)}; `; -const StyledTwentyLogo = styled.img` +const StyledSecondaryLogo = styled.img` border-radius: ${({ theme }) => theme.border.radius.xs}; - height: 24px; - width: 24px; + height: ${({ theme }) => theme.spacing(6)}; + width: ${({ theme }) => theme.spacing(6)}; `; -const StyledTwentyLogoContainer = styled.div` +const StyledSecondaryLogoContainer = styled.div` align-items: center; background-color: ${({ theme }) => theme.background.primary}; border-radius: ${({ theme }) => theme.border.radius.sm}; bottom: ${({ theme }) => `-${theme.spacing(3)}`}; display: flex; - height: 28px; + height: ${({ theme }) => theme.spacing(7)}; justify-content: center; position: absolute; right: ${({ theme }) => `-${theme.spacing(3)}`}; - width: 28px; + width: ${({ theme }) => theme.spacing(7)}; `; -type StyledMainLogoProps = { - logo?: string | null; -}; - -const StyledMainLogo = styled.div` - background: url(${(props) => props.logo}); +const StyledPrimaryLogo = styled.div<{ src: string }>` + background: url(${(props) => props.src}); background-size: cover; height: 100%; - width: 100%; `; -export const Logo = ({ workspaceLogo }: LogoProps) => { - if (!workspaceLogo) { - return ( - - - - ); - } +export const Logo = (props: LogoProps) => { + const defaultPrimaryLogoUrl = `${window.location.origin}/icons/android/android-launchericon-192-192.png`; + + const primaryLogoUrl = getImageAbsoluteURI( + props.primaryLogo ?? defaultPrimaryLogoUrl, + ); + const secondaryLogoUrl = getImageAbsoluteURI(props.secondaryLogo); return ( - - - - + + {secondaryLogoUrl && ( + + + + )} ); }; diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index e8ef0aab3606..33b15e83d6d3 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -69,6 +69,49 @@ export const useAuth = () => { const setDateTimeFormat = useSetRecoilState(dateTimeFormatState); + const clearSession = useRecoilCallback( + ({ snapshot }) => + async () => { + const emptySnapshot = snapshot_UNSTABLE(); + const iconsValue = snapshot.getLoadable(iconsState).getValue(); + const authProvidersValue = snapshot + .getLoadable(authProvidersState) + .getValue(); + const billing = snapshot.getLoadable(billingState).getValue(); + const isSignInPrefilled = snapshot + .getLoadable(isSignInPrefilledState) + .getValue(); + const supportChat = snapshot.getLoadable(supportChatState).getValue(); + const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); + const captchaProvider = snapshot + .getLoadable(captchaProviderState) + .getValue(); + const clientConfigApiStatus = snapshot + .getLoadable(clientConfigApiStatusState) + .getValue(); + const isCurrentUserLoaded = snapshot + .getLoadable(isCurrentUserLoadedState) + .getValue(); + const initialSnapshot = emptySnapshot.map(({ set }) => { + set(iconsState, iconsValue); + set(authProvidersState, authProvidersValue); + set(billingState, billing); + set(isSignInPrefilledState, isSignInPrefilled); + set(supportChatState, supportChat); + set(isDebugModeState, isDebugMode); + set(captchaProviderState, captchaProvider); + set(clientConfigApiStatusState, clientConfigApiStatus); + set(isCurrentUserLoadedState, isCurrentUserLoaded); + return undefined; + }); + goToRecoilSnapshot(initialSnapshot); + await client.clearStore(); + sessionStorage.clear(); + localStorage.clear(); + }, + [client, goToRecoilSnapshot], + ); + const handleChallenge = useCallback( async (email: string, password: string, captchaToken?: string) => { const challengeResult = await challenge({ @@ -212,51 +255,9 @@ export const useAuth = () => { [handleChallenge, handleVerify, setIsVerifyPendingState], ); - const handleSignOut = useRecoilCallback( - ({ snapshot }) => - async () => { - const emptySnapshot = snapshot_UNSTABLE(); - const iconsValue = snapshot.getLoadable(iconsState).getValue(); - const authProvidersValue = snapshot - .getLoadable(authProvidersState) - .getValue(); - const billing = snapshot.getLoadable(billingState).getValue(); - const isSignInPrefilled = snapshot - .getLoadable(isSignInPrefilledState) - .getValue(); - const supportChat = snapshot.getLoadable(supportChatState).getValue(); - const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue(); - const captchaProvider = snapshot - .getLoadable(captchaProviderState) - .getValue(); - const clientConfigApiStatus = snapshot - .getLoadable(clientConfigApiStatusState) - .getValue(); - const isCurrentUserLoaded = snapshot - .getLoadable(isCurrentUserLoadedState) - .getValue(); - - const initialSnapshot = emptySnapshot.map(({ set }) => { - set(iconsState, iconsValue); - set(authProvidersState, authProvidersValue); - set(billingState, billing); - set(isSignInPrefilledState, isSignInPrefilled); - set(supportChatState, supportChat); - set(isDebugModeState, isDebugMode); - set(captchaProviderState, captchaProvider); - set(clientConfigApiStatusState, clientConfigApiStatus); - set(isCurrentUserLoadedState, isCurrentUserLoaded); - return undefined; - }); - - goToRecoilSnapshot(initialSnapshot); - - await client.clearStore(); - sessionStorage.clear(); - localStorage.clear(); - }, - [client, goToRecoilSnapshot], - ); + const handleSignOut = useCallback(async () => { + await clearSession(); + }, [clearSession]); const handleCredentialsSignUp = useCallback( async ( @@ -340,7 +341,7 @@ export const useAuth = () => { verify: handleVerify, checkUserExists: { checkUserExistsData, checkUserExistsQuery }, - + clearSession, signOut: handleSignOut, signUpWithCredentials: handleCredentialsSignUp, signInWithCredentials: handleCrendentialsSignIn, diff --git a/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx b/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx index a7419d7c8f24..d3ca031163f2 100644 --- a/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx +++ b/packages/twenty-front/src/modules/favorites/components/FavoriteFolderNavigationDrawerItemDropdown.tsx @@ -1,13 +1,10 @@ import { FavoriteFolderHotkeyScope } from '@/favorites/constants/FavoriteFolderRightIconDropdownHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { - IconDotsVertical, - IconPencil, - IconTrash, - LightIconButton, - MenuItem, -} from 'twenty-ui'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { IconDotsVertical, IconPencil, IconTrash, MenuItem } from 'twenty-ui'; type FavoriteFolderNavigationDrawerItemDropdownProps = { folderId: string; @@ -16,12 +13,24 @@ type FavoriteFolderNavigationDrawerItemDropdownProps = { closeDropdown: () => void; }; +const StyledIconContainer = styled.div` + align-items: center; + background: transparent; + height: 24px; + width: 24px; + justify-content: center; + transition: background 0.1s ease; + display: flex; +`; + export const FavoriteFolderNavigationDrawerItemDropdown = ({ folderId, onRename, onDelete, closeDropdown, }: FavoriteFolderNavigationDrawerItemDropdownProps) => { + const theme = useTheme(); + const handleRename = () => { onRename(); closeDropdown(); @@ -41,7 +50,12 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({ usePortal data-select-disable clickableComponent={ - + + + } dropdownPlacement="right" dropdownOffset={{ y: -15 }} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx index 5e6810cfb0ce..35161f2fe3ba 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent.tsx @@ -10,46 +10,50 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { const { currentContentId, - viewType, recordIndexId, objectMetadataItem, onContentChange, closeDropdown, } = useOptionsDropdown(); - const { objectNamePlural } = useObjectNamePluralFromSingular({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); - const { hiddenRecordGroups, viewGroupFieldMetadataItem } = useRecordGroups({ + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); + + const { objectNamePlural } = useObjectNamePluralFromSingular({ objectNameSingular: objectMetadataItem.nameSingular, }); const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const viewGroupSettingsUrl = getSettingsPagePath( SettingsPath.ObjectFieldEdit, { objectSlug: objectNamePlural, - fieldSlug: viewGroupFieldMetadataItem?.name ?? '', + fieldSlug: recordGroupFieldMetadata?.name ?? '', }, ); @@ -61,11 +65,11 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> @@ -74,13 +78,13 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { StartIcon={IconChevronLeft} onClick={() => onContentChange('recordGroups')} > - Hidden {viewGroupFieldMetadataItem?.label} + Hidden {recordGroupFieldMetadata?.label} { objectNameSingular: objectMetadataItem.nameSingular, }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); + useScopedHotkeys( [Key.Escape], () => { @@ -64,10 +69,6 @@ export const ObjectOptionsDropdownMenuContent = () => { viewBarId: recordIndexId, }); - const { viewGroupFieldMetadataItem } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); - const { openObjectRecordsSpreasheetImportDialog } = useOpenObjectRecordsSpreadsheetImportDialog( objectMetadataItem.nameSingular, @@ -113,7 +114,7 @@ export const ObjectOptionsDropdownMenuContent = () => { onClick={() => onContentChange('recordGroups')} LeftIcon={IconLayoutList} text="Group by" - contextualText={viewGroupFieldMetadataItem?.label} + contextualText={recordGroupFieldMetadata?.label} hasSubMenu /> )} diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx index bb866024237d..238442058285 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent.tsx @@ -12,7 +12,7 @@ import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObje import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { useSearchRecordGroupField } from '@/object-record/object-options-dropdown/hooks/useSearchRecordGroupField'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { useHandleRecordGroupField } from '@/object-record/record-index/hooks/useHandleRecordGroupField'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; @@ -20,6 +20,7 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; @@ -38,9 +39,9 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { objectNameSingular: objectMetadataItem.nameSingular, }); - const { hiddenRecordGroups } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const { recordGroupFieldSearchInput, @@ -68,11 +69,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx index fcfeea029b31..b2a8c623da0d 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupSortContent.tsx @@ -8,24 +8,21 @@ import { } from 'twenty-ui'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const ObjectOptionsDropdownRecordGroupSortContent = () => { - const { - currentContentId, - objectMetadataItem, - onContentChange, - closeDropdown, - } = useOptionsDropdown(); + const { currentContentId, onContentChange, closeDropdown } = + useOptionsDropdown(); - const { hiddenRecordGroups } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const setRecordGroupSort = useSetRecoilComponentStateV2( recordIndexRecordGroupSortComponentState, @@ -39,11 +36,11 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => { useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx index 94db1268f1c4..19a892be1651 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent.tsx @@ -13,8 +13,10 @@ import { import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; import { useRecordGroupVisibility } from '@/object-record/record-group/hooks/useRecordGroupVisibility'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { hiddenRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; @@ -26,22 +28,20 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; export const ObjectOptionsDropdownRecordGroupsContent = () => { const isViewGroupEnabled = useIsFeatureEnabled('IS_VIEW_GROUPS_ENABLED'); - const { - currentContentId, - viewType, - recordIndexId, - objectMetadataItem, - onContentChange, - resetContent, - } = useOptionsDropdown(); + const { currentContentId, recordIndexId, onContentChange, resetContent } = + useOptionsDropdown(); - const { - hiddenRecordGroups, - visibleRecordGroups, - viewGroupFieldMetadataItem, - } = useRecordGroups({ - objectNameSingular: objectMetadataItem.nameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); + + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + + const hiddenRecordGroupIds = useRecoilComponentValueV2( + hiddenRecordGroupIdsComponentSelector, + ); const isDragableSortRecordGroup = useRecoilComponentValueV2( recordIndexRecordGroupIsDraggableSortComponentSelector, @@ -56,23 +56,21 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { handleHideEmptyRecordGroupChange, } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const { handleOrderChange: handleRecordGroupOrderChange } = useRecordGroupReorder({ - objectNameSingular: objectMetadataItem.nameSingular, viewBarId: recordIndexId, }); useEffect(() => { if ( currentContentId === 'hiddenRecordGroups' && - hiddenRecordGroups.length === 0 + hiddenRecordGroupIds.length === 0 ) { onContentChange('recordGroups'); } - }, [hiddenRecordGroups, currentContentId, onContentChange]); + }, [hiddenRecordGroupIds, currentContentId, onContentChange]); return ( <> @@ -86,9 +84,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { onClick={() => onContentChange('recordGroupFields')} LeftIcon={IconLayoutList} text={ - !viewGroupFieldMetadataItem + !recordGroupFieldMetadata ? 'Group by' - : `Group by "${viewGroupFieldMetadataItem.label}"` + : `Group by "${recordGroupFieldMetadata.label}"` } hasSubMenu /> @@ -108,12 +106,12 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { toggleSize="small" /> - {visibleRecordGroups.length > 0 && ( + {visibleRecordGroupIds.length > 0 && ( <> { /> )} - {hiddenRecordGroups.length > 0 && ( + {hiddenRecordGroupIds.length > 0 && ( <> onContentChange('hiddenRecordGroups')} LeftIcon={IconEyeOff} - text={`Hidden ${viewGroupFieldMetadataItem?.label ?? ''}`} + text={`Hidden ${recordGroupFieldMetadata?.label ?? ''}`} /> diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts index ccb19275b709..3dba54dc3835 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard.ts @@ -4,10 +4,11 @@ import { useRecoilState } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView'; import { GraphQLView } from '@/views/types/GraphQLView'; @@ -32,11 +33,12 @@ export const useObjectOptionsForBoard = ({ const { saveViewFields } = useSaveCurrentViewFields(viewBarId); const { updateCurrentView } = useUpdateCurrentView(viewBarId); - const { isCompactModeActiveState } = useRecordBoard(recordBoardId); - const [isCompactModeActive, setIsCompactModeActive] = useRecoilState( - isCompactModeActiveState, - ); + const [isCompactModeActive, setIsCompactModeActive] = + useRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 4c5076d9f8af..a28c71b8ee02 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 import { useContext, useRef } from 'react'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; +import { useRecoilCallback } from 'recoil'; import { Key } from 'ts-key-enum'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; @@ -9,11 +9,15 @@ import { RecordBoardHeader } from '@/object-record/record-board/components/Recor import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn'; import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; @@ -21,6 +25,9 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useScrollRestoration } from '~/hooks/useScrollRestoration'; const StyledContainer = styled.div` @@ -58,14 +65,17 @@ export const RecordBoard = () => { useContext(RecordBoardContext); const boardRef = useRef(null); - const { - columnIdsState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - allRecordIdsSelector, - } = useRecordBoardStates(recordBoardId); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + ); - const columnIds = useRecoilValue(columnIdsState); + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + ); const { resetRecordSelection, setRecordAsSelected } = useRecordBoardSelection(recordBoardId); @@ -85,15 +95,16 @@ export const RecordBoard = () => { const selectAll = useRecoilCallback( ({ snapshot }) => () => { - const allRecordIds = snapshot - .getLoadable(allRecordIdsSelector()) - .getValue(); + const allRecordIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); for (const recordId of allRecordIds) { setRecordAsSelected(recordId, true); } }, - [allRecordIdsSelector, setRecordAsSelected], + [recordIndexAllRowIdsState, setRecordAsSelected], ); useScopedHotkeys('ctrl+a,meta+a', selectAll, TableHotkeyScope.Table); @@ -111,42 +122,40 @@ export const RecordBoard = () => { if (!result.destination) return; const draggedRecordId = result.draggableId; - const sourceColumnId = result.source.droppableId; - const destinationColumnId = result.destination.droppableId; + const sourceRecordGroupId = result.source.droppableId; + const destinationRecordGroupId = result.destination.droppableId; const destinationIndexInColumn = result.destination.index; - if (!destinationColumnId || !selectFieldMetadataItem) return; + if (!destinationRecordGroupId || !selectFieldMetadataItem) return; - const column = snapshot - .getLoadable(columnsFamilySelector(destinationColumnId)) - .getValue(); + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(destinationRecordGroupId), + ); - if (!column) return; + if (!recordGroup) return; - const destinationColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(destinationColumnId)) - .getValue(); - const otherRecordsInDestinationColumn = - sourceColumnId === destinationColumnId - ? destinationColumnRecordIds.filter( + const destinationRecordByGroupIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(destinationRecordGroupId), + ); + const otherRecordIdsInDestinationColumn = + sourceRecordGroupId === destinationRecordGroupId + ? destinationRecordByGroupIds.filter( (recordId) => recordId !== draggedRecordId, ) - : destinationColumnRecordIds; + : destinationRecordByGroupIds; const recordBeforeId = - otherRecordsInDestinationColumn[destinationIndexInColumn - 1]; + otherRecordIdsInDestinationColumn[destinationIndexInColumn - 1]; const recordBefore = recordBeforeId - ? snapshot - .getLoadable(recordStoreFamilyState(recordBeforeId)) - .getValue() + ? getSnapshotValue(snapshot, recordStoreFamilyState(recordBeforeId)) : null; const recordAfterId = - otherRecordsInDestinationColumn[destinationIndexInColumn]; + otherRecordIdsInDestinationColumn[destinationIndexInColumn]; const recordAfter = recordAfterId - ? snapshot - .getLoadable(recordStoreFamilyState(recordAfterId)) - .getValue() + ? getSnapshotValue(snapshot, recordStoreFamilyState(recordAfterId)) : null; const draggedRecordPosition = getDraggedRecordPosition( @@ -157,14 +166,13 @@ export const RecordBoard = () => { updateOneRecord({ idToUpdate: draggedRecordId, updateOneRecordInput: { - [selectFieldMetadataItem.name]: column.value, + [selectFieldMetadataItem.name]: recordGroup.value, position: draggedRecordPosition, }, }); }, [ - columnsFamilySelector, - recordIdsByColumnIdFamilyState, + recordIndexRowIdsByGroupFamilyState, selectFieldMetadataItem, updateOneRecord, ], @@ -182,32 +190,36 @@ export const RecordBoard = () => { onColumnsChange={() => {}} onFieldsChange={() => {}} > - - - - - - - - - {columnIds.map((columnId) => ( - - ))} - - - - - - - - + + + + + + + + + + {visibleRecordGroupIds.map((recordGroupId) => ( + + ))} + + + + + + + + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index b284532386dd..e62c658115fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -1,7 +1,6 @@ -import { useRecoilValue } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; const StyledHeaderContainer = styled.div` @@ -24,14 +23,17 @@ const StyledHeaderContainer = styled.div` `; export const RecordBoardHeader = () => { - const { columnIdsState } = useRecordBoardStates(); - - const columnIds = useRecoilValue(columnIdsState); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); return ( - {columnIds.map((columnId) => ( - + {visibleRecordGroupIds.map((recordGroupId) => ( + ))} ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts deleted file mode 100644 index a26506254b67..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; -import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; -import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; -import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState'; -import { isRecordBoardFetchingRecordsComponentState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState'; -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { recordBoardFiltersComponentState } from '@/object-record/record-board/states/recordBoardFiltersComponentState'; -import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; -import { recordBoardObjectSingularNameComponentState } from '@/object-record/record-board/states/recordBoardObjectSingularNameComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { recordBoardSortsComponentState } from '@/object-record/record-board/states/recordBoardSortsComponentState'; -import { recordBoardAllRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector'; -import { recordBoardColumnsComponentFamilySelector } from '@/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector'; -import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; -import { recordBoardShouldFetchMoreComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector'; -import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; -import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; -import { getScopeIdOrUndefinedFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdOrUndefinedFromComponentId'; -import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState'; -import { extractComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/extractComponentReadOnlySelector'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; - -export const useRecordBoardStates = (recordBoardId?: string) => { - const scopeId = useAvailableScopeIdOrThrow( - RecordBoardScopeInternalContext, - getScopeIdOrUndefinedFromComponentId(recordBoardId), - ); - - return { - scopeId, - objectSingularNameState: extractComponentState( - recordBoardObjectSingularNameComponentState, - scopeId, - ), - kanbanFieldMetadataNameState: extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - scopeId, - ), - isFetchingRecordState: extractComponentState( - isRecordBoardFetchingRecordsComponentState, - scopeId, - ), - isFetchingRecordsByColumnState: extractComponentFamilyState( - isRecordBoardFetchingRecordsByColumnFamilyState, - scopeId, - ), - columnIdsState: extractComponentState( - recordBoardColumnIdsComponentState, - scopeId, - ), - columnsFamilySelector: extractComponentFamilyState( - recordBoardColumnsComponentFamilySelector, - scopeId, - ), - - filtersState: extractComponentState( - recordBoardFiltersComponentState, - scopeId, - ), - sortsState: extractComponentState(recordBoardSortsComponentState, scopeId), - fieldDefinitionsState: extractComponentState( - recordBoardFieldDefinitionsComponentState, - scopeId, - ), - visibleFieldDefinitionsState: extractComponentReadOnlySelector( - recordBoardVisibleFieldDefinitionsComponentSelector, - scopeId, - ), - - recordIdsByColumnIdFamilyState: extractComponentFamilyState( - recordBoardRecordIdsByColumnIdComponentFamilyState, - scopeId, - ), - isRecordBoardCardSelectedFamilyState: extractComponentFamilyState( - isRecordBoardCardSelectedComponentFamilyState, - scopeId, - ), - allRecordIdsSelector: extractComponentReadOnlySelector( - recordBoardAllRecordIdsComponentSelector, - scopeId, - ), - selectedRecordIdsSelector: extractComponentReadOnlySelector( - recordBoardSelectedRecordIdsComponentSelector, - scopeId, - ), - - isCompactModeActiveState: extractComponentState( - isRecordBoardCompactModeActiveComponentState, - scopeId, - ), - - shouldFetchMoreInColumnFamilyState: extractComponentFamilyState( - recordBoardShouldFetchMoreInColumnComponentFamilyState, - scopeId, - ), - shouldFetchMoreSelector: extractComponentReadOnlySelector( - recordBoardShouldFetchMoreComponentSelector, - scopeId, - ), - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts deleted file mode 100644 index 58e00b9900ba..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordBoardColumns = (recordBoardId?: string) => { - const { scopeId, columnIdsState, columnsFamilySelector } = - useRecordBoardStates(recordBoardId); - - const recordGroupSort = useRecoilComponentValueV2( - recordIndexRecordGroupSortComponentState, - recordBoardId, - ); - - const setColumns = useRecoilCallback( - ({ set, snapshot }) => - (columns: RecordGroupDefinition[]) => { - const currentColumnsIds = snapshot - .getLoadable(columnIdsState) - .getValue(); - - const sortedColumns = sortRecordGroupDefinitions( - columns, - recordGroupSort, - ); - - const columnIds = sortedColumns - .filter(({ isVisible }) => isVisible) - .map(({ id }) => id); - - if (!isDeeplyEqual(currentColumnsIds, columnIds)) { - set(columnIdsState, columnIds); - } - - columns.forEach((column) => { - const currentColumn = snapshot - .getLoadable(columnsFamilySelector(column.id)) - .getValue(); - - if (isDeeplyEqual(currentColumn, column)) { - return; - } - - set(columnsFamilySelector(column.id), column); - }); - }, - [columnIdsState, recordGroupSort, columnsFamilySelector], - ); - - return { - scopeId, - setColumns, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts deleted file mode 100644 index e5b61e4e6d94..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { - const { - scopeId, - recordIdsByColumnIdFamilyState, - columnsFamilySelector, - columnIdsState, - kanbanFieldMetadataNameState, - } = useRecordBoardStates(recordBoardId); - - const setRecordIds = useRecoilCallback( - ({ set, snapshot }) => - (records: ObjectRecord[]) => { - const columnIds = snapshot.getLoadable(columnIdsState).getValue(); - - columnIds.forEach((columnId) => { - const column = snapshot - .getLoadable(columnsFamilySelector(columnId)) - .getValue(); - - const existingColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(columnId)) - .getValue(); - - const kanbanFieldMetadataName = snapshot - .getLoadable(kanbanFieldMetadataNameState) - .getValue(); - - if (!kanbanFieldMetadataName) { - return; - } - - const columnRecordIds = records - .filter( - (record) => record[kanbanFieldMetadataName] === column?.value, - ) - .sort(sortRecordsByPosition) - .map((record) => record.id); - - if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) { - set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds); - } - }); - }, - [ - columnIdsState, - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - kanbanFieldMetadataNameState, - ], - ); - - return { - scopeId, - setRecordIds, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts deleted file mode 100644 index 25138622df44..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordIdsForColumn.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useRecoilCallback } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; -import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; - -export const useSetRecordIdsForColumn = (recordBoardId?: string) => { - const { - scopeId, - recordIdsByColumnIdFamilyState, - columnsFamilySelector, - kanbanFieldMetadataNameState, - } = useRecordBoardStates(recordBoardId); - - const setRecordIdsForColumn = useRecoilCallback( - ({ set, snapshot }) => - (columnId: string, records: ObjectRecord[]) => { - const column = snapshot - .getLoadable(columnsFamilySelector(columnId)) - .getValue(); - - const existingColumnRecordIds = snapshot - .getLoadable(recordIdsByColumnIdFamilyState(columnId)) - .getValue(); - - const kanbanFieldMetadataName = snapshot - .getLoadable(kanbanFieldMetadataNameState) - .getValue(); - - if (!kanbanFieldMetadataName) { - return; - } - - const columnRecordIds = records - .filter((record) => record[kanbanFieldMetadataName] === column?.value) - .sort(sortRecordsByPosition) - .map((record) => record.id); - - if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) { - set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds); - } - }, - [ - columnsFamilySelector, - recordIdsByColumnIdFamilyState, - kanbanFieldMetadataNameState, - ], - ); - - return { - scopeId, - setRecordIdsForColumn, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts deleted file mode 100644 index 755703d0e900..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useSetRecoilState } from 'recoil'; - -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useSetRecordBoardColumns } from '@/object-record/record-board/hooks/internal/useSetRecordBoardColumns'; -import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds'; -import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/internal/useSetRecordIdsForColumn'; - -export const useRecordBoard = (recordBoardId?: string) => { - const { - scopeId, - fieldDefinitionsState, - objectSingularNameState, - selectedRecordIdsSelector, - isCompactModeActiveState, - kanbanFieldMetadataNameState, - shouldFetchMoreSelector, - isFetchingRecordsByColumnState, - } = useRecordBoardStates(recordBoardId); - - const { setColumns } = useSetRecordBoardColumns(recordBoardId); - const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId); - const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId); - - const setFieldDefinitions = useSetRecoilState(fieldDefinitionsState); - const setObjectSingularName = useSetRecoilState(objectSingularNameState); - const setKanbanFieldMetadataName = useSetRecoilState( - kanbanFieldMetadataNameState, - ); - - return { - scopeId, - setColumns, - setRecordIds, - setFieldDefinitions, - setObjectSingularName, - setKanbanFieldMetadataName, - selectedRecordIdsSelector, - isCompactModeActiveState, - shouldFetchMoreSelector, - setRecordIdsForColumn, - isFetchingRecordsByColumnState, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index baba7e9f9a5a..8c20d3933acd 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -2,13 +2,25 @@ import { useRecoilCallback } from 'recoil'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; export const useRecordBoardSelection = (recordBoardId: string) => { - const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = - useRecordBoardStates(recordBoardId); + const isRecordBoardCardSelectedFamilyState = + useRecoilComponentCallbackStateV2( + isRecordBoardCardSelectedComponentFamilyState, + recordBoardId, + ); + + const recordBoardSelectedRecordIdsSelector = + useRecoilComponentCallbackStateV2( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, + ); const isActionMenuDropdownOpenState = extractComponentState( isDropdownOpenComponentState, @@ -22,9 +34,10 @@ export const useRecordBoardSelection = (recordBoardId: string) => { () => { set(isActionMenuDropdownOpenState, false); - const recordIds = snapshot - .getLoadable(selectedRecordIdsSelector()) - .getValue(); + const recordIds = getSnapshotValue( + snapshot, + recordBoardSelectedRecordIdsSelector, + ); for (const recordId of recordIds) { set(isRecordBoardCardSelectedFamilyState(recordId), false); @@ -32,7 +45,7 @@ export const useRecordBoardSelection = (recordBoardId: string) => { }, [ isActionMenuDropdownOpenState, - selectedRecordIdsSelector, + recordBoardSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts new file mode 100644 index 000000000000..0194a5bf366a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordBoardRecordIds.ts @@ -0,0 +1,110 @@ +import { useRecoilCallback } from 'recoil'; + +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { + const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentSelector, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + recordBoardId, + ); + + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + recordBoardId, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardId, + ); + + const setRecordIds = useRecoilCallback( + ({ set, snapshot }) => + (records: ObjectRecord[]) => { + const existingAllRowIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); + + const recordGroupIds = getSnapshotValue( + snapshot, + visibleRecordGroupIdsSelector, + ); + + for (const recordGroupId of recordGroupIds) { + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); + + const existingRecordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + const recordGroupFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + if (!isDefined(recordGroupFieldMetadata)) { + return; + } + + const recordGroupRowIds = records + .filter( + (record) => + record[recordGroupFieldMetadata.name] === recordGroup?.value, + ) + .sort(sortRecordsByPosition) + .map((record) => record.id); + + if (!isDeeplyEqual(existingRecordGroupRowIds, recordGroupRowIds)) { + set( + recordIndexRowIdsByGroupFamilyState(recordGroupId), + recordGroupRowIds, + ); + } + } + + const allRowIds: string[] = []; + + for (const recordGroupId of recordGroupIds) { + const tableRowIdsByGroup = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + allRowIds.push(...tableRowIdsByGroup); + } + + if (!isDeeplyEqual(existingAllRowIds, allRowIds)) { + set(recordIndexAllRowIdsState, allRowIds); + } + }, + [ + visibleRecordGroupIdsSelector, + recordIndexRowIdsByGroupFamilyState, + recordGroupFieldMetadataState, + recordIndexAllRowIdsState, + ], + ); + + return { + setRecordIds, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts new file mode 100644 index 000000000000..3ba7c3ee78a4 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useSetRecordIdsForColumn.ts @@ -0,0 +1,109 @@ +import { useRecoilCallback } from 'recoil'; + +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { sortRecordsByPosition } from '@/object-record/utils/sortRecordsByPosition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordIdsForColumn = (recordBoardId?: string) => { + const recordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, + recordBoardId, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + recordBoardId, + ); + + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, + recordBoardId, + ); + + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardId, + ); + + const setRecordIdsForColumn = useRecoilCallback( + ({ set, snapshot }) => + (currentRecordGroupId: string, records: ObjectRecord[]) => { + const existingAllRowIds = getSnapshotValue( + snapshot, + recordIndexAllRowIdsState, + ); + + const recordGroupIds = getSnapshotValue(snapshot, recordGroupIdsState); + + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(currentRecordGroupId), + ); + + const existingRecordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + ); + + const recordGroupFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + if (!isDefined(recordGroupFieldMetadata)) { + return; + } + + const recordGroupRowIds = records + .filter( + (record) => + record[recordGroupFieldMetadata.name] === recordGroup?.value, + ) + .sort(sortRecordsByPosition) + .map((record) => record.id); + + if (!isDeeplyEqual(existingRecordGroupRowIds, recordGroupRowIds)) { + set( + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + recordGroupRowIds, + ); + } + + const allRowIds: string[] = []; + + for (const recordGroupId of recordGroupIds) { + const tableRowIdsByGroup = + recordGroupId !== currentRecordGroupId + ? getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ) + : recordGroupRowIds; + + allRowIds.push(...tableRowIdsByGroup); + } + + if (!isDeeplyEqual(existingAllRowIds, allRowIds)) { + set(recordIndexAllRowIdsState, allRowIds); + } + }, + [ + recordGroupIdsState, + recordIndexRowIdsByGroupFamilyState, + recordGroupFieldMetadataState, + recordIndexAllRowIdsState, + ], + ); + + return { + setRecordIdsForColumn, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 92d897a614ae..a456c89a3d9f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -3,9 +3,11 @@ import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-me import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext'; +import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { FieldContext, RecordUpdateHook, @@ -22,11 +24,13 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { TextInput } from '@/ui/input/components/TextInput'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; +import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import styled from '@emotion/styled'; import { ReactNode, useContext, useState } from 'react'; import { InView, useInView } from 'react-intersection-observer'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { AnimatedEaseInOut, AvatarChipVariant, @@ -157,28 +161,31 @@ export const RecordBoardCard = ({ onCreateSuccess?: () => void; position?: 'first' | 'last'; }) => { + const { recordId } = useContext(RecordBoardCardContext); + const [newLabelValue, setNewLabelValue] = useState(''); + const { handleBlur, handleInputEnter } = useAddNewCard(); - const { recordId } = useContext(RecordBoardCardContext); + const { updateOneRecord, objectMetadataItem } = useContext(RecordBoardContext); - const { - isCompactModeActiveState, - isRecordBoardCardSelectedFamilyState, - visibleFieldDefinitionsState, - } = useRecordBoardStates(); - const isCompactModeActive = useRecoilValue(isCompactModeActiveState); - const [isCardExpanded, setIsCardExpanded] = useState(false); - - const [isCurrentCardSelected, setIsCurrentCardSelected] = useRecoilState( - isRecordBoardCardSelectedFamilyState(recordId), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const isCompactModeActive = useRecoilComponentValueV2( + isRecordBoardCompactModeActiveComponentState, ); + const [isCardExpanded, setIsCardExpanded] = useState(false); + + const [isCurrentCardSelected, setIsCurrentCardSelected] = + useRecoilComponentFamilyStateV2( + isRecordBoardCardSelectedComponentFamilyState, + recordId, + ); + const record = useRecoilValue(recordStoreFamilyState(recordId)); const recordBoardId = useAvailableScopeIdOrThrow( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 26072d4a7e06..1d850a8507fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -1,10 +1,12 @@ import styled from '@emotion/styled'; import { Droppable } from '@hello-pangea/dnd'; -import { useRecoilValue } from 'recoil'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { useRecoilValue } from 'recoil'; const StyledColumn = styled.div` background-color: ${({ theme }) => theme.background.primary}; @@ -25,27 +27,26 @@ type RecordBoardColumnProps = { export const RecordBoardColumn = ({ recordBoardColumnId, }: RecordBoardColumnProps) => { - const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = - useRecordBoardStates(); - const columnDefinition = useRecoilValue( - columnsFamilySelector(recordBoardColumnId), + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(recordBoardColumnId), ); - const recordIds = useRecoilValue( - recordIdsByColumnIdFamilyState(recordBoardColumnId), + const recordRowIdsByGroup = useRecoilComponentFamilyValueV2( + recordIndexRowIdsByGroupComponentFamilyState, + recordBoardColumnId, ); - if (!columnDefinition) { + if (!recordGroupDefinition) { return null; } return ( @@ -53,7 +54,7 @@ export const RecordBoardColumn = ({ )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index d3e540902b55..8cdc5c32ffac 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -5,7 +5,6 @@ import { useRecoilValue } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnCardContainerSkeletonLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader'; import { RecordBoardColumnCardsMemo } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsMemo'; import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader'; @@ -15,7 +14,10 @@ import { RecordBoardColumnNewRecordButton } from '@/object-record/record-board/r import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; const StyledColumnCardsContainer = styled.div` display: flex; @@ -56,16 +58,16 @@ export const RecordBoardColumnCardsContainer = ({ isRecordIndexBoardColumnLoadingFamilyState(columnId), ); - const { isCompactModeActiveState, visibleFieldDefinitionsState } = - useRecordBoardStates(); - - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); const numberOfFields = visibleFieldDefinitions.length; - const isCompactModeActive = useRecoilValue(isCompactModeActiveState); + const isCompactModeActive = useRecoilComponentValueV2( + isRecordBoardCompactModeActiveComponentState, + ); + const { isOpportunitiesCompanyFieldDisabled } = useIsOpportunitiesCompanyFieldDisabled(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx index ed6045033fec..bfbfb7e9d0e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu.tsx @@ -5,7 +5,6 @@ import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRec import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { ViewType } from '@/views/types/ViewType'; import { MenuItem } from 'twenty-ui'; const StyledMenuContainer = styled.div` @@ -26,9 +25,7 @@ export const RecordBoardColumnDropdownMenu = ({ }: RecordBoardColumnDropdownMenuProps) => { const boardColumnMenuRef = useRef(null); - const recordGroupActions = useRecordGroupActions({ - viewType: ViewType.Kanban, - }); + const recordGroupActions = useRecordGroupActions(); const closeMenu = useCallback(() => { onClose(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx index 7a610c3dda95..1143aaf5e953 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx @@ -1,11 +1,13 @@ +import styled from '@emotion/styled'; import { useContext, useEffect } from 'react'; import { useInView } from 'react-intersection-observer'; -import styled from '@emotion/styled'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { GRAY_SCALE } from 'twenty-ui'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState'; +import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; +import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; const StyledText = styled.div` align-items: center; @@ -19,15 +21,14 @@ const StyledText = styled.div` export const RecordBoardColumnFetchMoreLoader = () => { const { columnDefinition } = useContext(RecordBoardColumnContext); - const { shouldFetchMoreInColumnFamilyState, isFetchingRecordsByColumnState } = - useRecordBoardStates(); const isFetchingRecord = useRecoilValue( - isFetchingRecordsByColumnState({ columnId: columnDefinition.id }), + isRecordBoardFetchingRecordsByColumnFamilyState(columnDefinition.id), ); - const setShouldFetchMore = useSetRecoilState( - shouldFetchMoreInColumnFamilyState(columnDefinition.id), + const setShouldFetchMore = useSetRecoilComponentFamilyStateV2( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + columnDefinition.id, ); const { ref, inView } = useInView(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx index 1386b494d6ef..68de7adb566d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx @@ -1,8 +1,10 @@ import { isDefined } from 'twenty-ui'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilValue } from 'recoil'; type RecordBoardColumnHeaderWrapperProps = { @@ -12,14 +14,16 @@ type RecordBoardColumnHeaderWrapperProps = { export const RecordBoardColumnHeaderWrapper = ({ columnId, }: RecordBoardColumnHeaderWrapperProps) => { - const { columnsFamilySelector, recordIdsByColumnIdFamilyState } = - useRecordBoardStates(); - - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), + ); - const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId)); + const recordRowIdsByGroup = useRecoilComponentFamilyValueV2( + recordIndexRowIdsByGroupComponentFamilyState, + columnId, + ); - if (!isDefined(columnDefinition)) { + if (!isDefined(recordGroupDefinition)) { return null; } @@ -27,9 +31,9 @@ export const RecordBoardColumnHeaderWrapper = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts index cb087c0813a8..3926ceddbe12 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts @@ -1,12 +1,12 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; -import { useRecoilValue } from 'recoil'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const useColumnNewCardActions = (columnId: string) => { - const { visibleFieldDefinitionsState } = useRecordBoardStates(); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, ); + const labelIdentifierField = visibleFieldDefinitions.find( (field) => field.isLabelIdentifier, ); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts b/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts new file mode 100644 index 000000000000..1d83aab21078 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const RecordBoardComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts index 8cb7c99f4959..3b446676de7e 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState.ts @@ -1,7 +1,9 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; export const isRecordBoardCardSelectedComponentFamilyState = - createComponentFamilyState({ + createComponentFamilyStateV2({ key: 'isRecordBoardCardSelectedComponentFamilyState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts index 68741ee81a90..bde53d616ef1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState.ts @@ -1,7 +1,9 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; export const isRecordBoardCompactModeActiveComponentState = - createComponentState({ + createComponentStateV2({ key: 'isRecordBoardCompactModeActiveComponentState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts index dfaaffc0ec23..8ee4affb3cc5 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState.ts @@ -1,7 +1,10 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { atomFamily } from 'recoil'; -export const isRecordBoardFetchingRecordsByColumnFamilyState = - createComponentFamilyState({ - key: 'isRecordBoardFetchingRecordsByColumnFamilyState', - defaultValue: false, - }); +export const isRecordBoardFetchingRecordsByColumnFamilyState = atomFamily< + boolean, + RecordGroupDefinition['id'] +>({ + key: 'isRecordBoardFetchingRecordsByColumnFamilyState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts deleted file mode 100644 index c76a8777e328..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/isRecordBoardFetchingRecordsComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const isRecordBoardFetchingRecordsComponentState = - createComponentState({ - key: 'isRecordBoardFetchingRecordsComponentState', - defaultValue: false, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts deleted file mode 100644 index 3ae094376c22..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnIdsComponentState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardColumnIdsComponentState = createComponentState< - string[] ->({ - key: 'recordBoardColumnIdsComponentState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts deleted file mode 100644 index 1530820d803f..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardColumnsComponentFamilyState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const recordBoardColumnsComponentFamilyState = - createComponentFamilyState({ - key: 'recordBoardColumnsComponentFamilyState', - defaultValue: undefined, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts index e8fb862be662..36696c000453 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFieldDefinitionsComponentState.ts @@ -1,10 +1,12 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { RecordBoardFieldDefinition } from '@/object-record/record-board/types/RecordBoardFieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -export const recordBoardFieldDefinitionsComponentState = createComponentState< +export const recordBoardFieldDefinitionsComponentState = createComponentStateV2< RecordBoardFieldDefinition[] >({ key: 'recordBoardFieldDefinitionsComponentState', defaultValue: [], + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts deleted file mode 100644 index 7d493b349e5b..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardFiltersComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardFiltersComponentState = createComponentState({ - key: 'recordBoardFiltersComponentState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts deleted file mode 100644 index 26490c9298b3..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardKanbanFieldMetadataNameComponentState = - createComponentState({ - key: 'recordBoardKanbanFieldMetadataNameComponentState', - defaultValue: undefined, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts deleted file mode 100644 index 9cb2f0df334a..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; - -export const recordBoardRecordIdsByColumnIdComponentFamilyState = - createComponentFamilyState({ - key: 'recordBoardRecordIdsByColumnIdComponentFamilyState', - defaultValue: [], - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts index 01dd190990ef..2dec1d6b87b2 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState.ts @@ -1,7 +1,9 @@ -import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; export const recordBoardShouldFetchMoreInColumnComponentFamilyState = - createComponentFamilyState({ - key: 'onRecordBoardFetchMoreIrecordBoardShouldFetchMoreInColumnComponentFamilyStatesVisibleComponentFamilyState', + createComponentFamilyStateV2({ + key: 'recordBoardShouldFetchMoreInColumnComponentFamilyState', defaultValue: false, + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts deleted file mode 100644 index d2aa0923f335..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardSortsComponentState.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; -import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; - -export const recordBoardSortsComponentState = createComponentState({ - key: 'recordBoardSortsComponentState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts deleted file mode 100644 index 154da9313fa2..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardAllRecordIdsComponentSelector.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; - -export const recordBoardAllRecordIdsComponentSelector = - createComponentReadOnlySelector({ - key: 'recordBoardAllRecordIdsComponentSelector', - get: - ({ scopeId }) => - ({ get }) => { - const columnIds = get(recordBoardColumnIdsComponentState({ scopeId })); - - const recordIdsByColumn = columnIds.map((columnId) => - get( - recordBoardRecordIdsByColumnIdComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ), - ); - - const recordIds = recordIdsByColumn.flat(); - - return recordIds; - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts deleted file mode 100644 index fefff8451060..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardColumnsComponentFamilySelector.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { recordBoardColumnsComponentFamilyState } from '@/object-record/record-board/states/recordBoardColumnsComponentFamilyState'; -import { createComponentFamilySelector } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelector'; -import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; - -export const recordBoardColumnsComponentFamilySelector = - createComponentFamilySelector({ - key: 'recordBoardColumnsComponentFamilySelector', - get: - ({ - scopeId, - familyKey: columnId, - }: { - scopeId: string; - familyKey: string; - }) => - ({ get }) => { - return get( - recordBoardColumnsComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ); - }, - set: - ({ - scopeId, - familyKey: columnId, - }: { - scopeId: string; - familyKey: string; - }) => - ({ set }, newColumn) => { - set( - recordBoardColumnsComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - newColumn, - ); - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts index ce2b1e4cac4d..849d45735982 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector.ts @@ -1,32 +1,24 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { isRecordBoardCardSelectedComponentFamilyState } from '@/object-record/record-board/states/isRecordBoardCardSelectedComponentFamilyState'; -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const recordBoardSelectedRecordIdsComponentSelector = - createComponentReadOnlySelector({ + createComponentSelectorV2({ key: 'recordBoardSelectedRecordIdsSelector', + componentInstanceContext: RecordBoardComponentInstanceContext, get: - ({ scopeId }) => + ({ instanceId }) => ({ get }) => { - const columnIds = get(recordBoardColumnIdsComponentState({ scopeId })); - - const recordIdsByColumn = columnIds.map((columnId) => - get( - recordBoardRecordIdsByColumnIdComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ), + const allRowIds = get( + recordIndexAllRowIdsComponentState.atomFamily({ instanceId }), ); - const recordIds = recordIdsByColumn.flat(); - - return recordIds.filter( + return allRowIds.filter( (recordId) => get( - isRecordBoardCardSelectedComponentFamilyState({ - scopeId, + isRecordBoardCardSelectedComponentFamilyState.atomFamily({ + instanceId, familyKey: recordId, }), ) === true, diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts deleted file mode 100644 index 225ffafac6a9..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardShouldFetchMoreComponentFamilySelector.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; -import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; - -export const recordBoardShouldFetchMoreComponentSelector = - createComponentReadOnlySelector({ - key: 'recordBoardShouldFetchMoreComponentSelector', - get: - ({ scopeId }: { scopeId: string }) => - ({ get }) => { - const columnIds = get( - recordBoardColumnIdsComponentState({ - scopeId, - }), - ); - - const shouldFetchMoreInColumns = columnIds.map((columnId) => { - return get( - recordBoardShouldFetchMoreInColumnComponentFamilyState({ - scopeId, - familyKey: columnId, - }), - ); - }); - - return shouldFetchMoreInColumns.some(Boolean); - }, - }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts index 4b2732eb36d8..62ae69407bf0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector.ts @@ -1,13 +1,17 @@ +import { RecordBoardComponentInstanceContext } from '@/object-record/record-board/states/contexts/RecordBoardComponentInstanceContext'; import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; -import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const recordBoardVisibleFieldDefinitionsComponentSelector = - createComponentReadOnlySelector({ + createComponentSelectorV2({ key: 'recordBoardVisibleFieldDefinitionsComponentSelector', get: - ({ scopeId }) => + ({ instanceId }) => ({ get }) => - get(recordBoardFieldDefinitionsComponentState({ scopeId })) + get( + recordBoardFieldDefinitionsComponentState.atomFamily({ instanceId }), + ) .filter((field) => field.isVisible) .sort((a, b) => a.position - b.position), + componentInstanceContext: RecordBoardComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx index 70bd645af243..3a5d668ad005 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupMenuItemDraggable.tsx @@ -1,31 +1,45 @@ import { IconEye, IconEyeOff, MenuItemDraggable, Tag } from 'twenty-ui'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinition, RecordGroupDefinitionType, } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { useRecoilValue } from 'recoil'; import { isDefined } from '~/utils/isDefined'; type RecordGroupMenuItemDraggableProps = { - recordGroup: RecordGroupDefinition; + recordGroupId: string; showDragGrip?: boolean; isDraggable?: boolean; - onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + onVisibilityChange: (recordGroup: RecordGroupDefinition) => void; }; export const RecordGroupMenuItemDraggable = ({ - recordGroup, + recordGroupId, showDragGrip, isDraggable, onVisibilityChange, }: RecordGroupMenuItemDraggableProps) => { + const recordGroup = useRecoilValue( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroup)) { + return null; + } + const isNoValue = recordGroup.type === RecordGroupDefinitionType.NoValue; const getIconButtons = (recordGroup: RecordGroupDefinition) => { const iconButtons = [ { Icon: recordGroup.isVisible ? IconEyeOff : IconEye, - onClick: () => onVisibilityChange(recordGroup), + onClick: () => + onVisibilityChange({ + ...recordGroup, + isVisible: !recordGroup.isVisible, + }), }, ].filter(isDefined); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx index e43afd1eeff1..e655a402d017 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupsVisibilityDropdownSection.tsx @@ -13,17 +13,17 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; type RecordGroupsVisibilityDropdownSectionProps = { - recordGroups: RecordGroupDefinition[]; + recordGroupIds: string[]; isDraggable: boolean; onDragEnd?: OnDragEndResponder; - onVisibilityChange: (viewGroup: RecordGroupDefinition) => void; + onVisibilityChange: (recordGroup: RecordGroupDefinition) => void; title: string; showSubheader?: boolean; showDragGrip: boolean; }; export const RecordGroupsVisibilityDropdownSection = ({ - recordGroups, + recordGroupIds, isDraggable, onDragEnd, onVisibilityChange, @@ -43,12 +43,13 @@ export const RecordGroupsVisibilityDropdownSection = ({ {title} )} - {!!recordGroups.length && ( + {recordGroupIds.length > 0 && ( <> {!isDraggable ? ( - recordGroups.map((recordGroup) => ( + recordGroupIds.map((recordGroupId) => ( - {recordGroups.map((recordGroup, index) => ( + {recordGroupIds.map((recordGroupId, index) => ( { +export const useCurrentRecordGroupDefinition = () => { const context = useContext(RecordGroupContext); - const hasRecordGroups = useRecoilComponentValueV2( - hasRecordGroupDefinitionsComponentSelector, - recordTableId, + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(context?.recordGroupId), ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - recordTableId, - ); - - const recordGroupDefinition = useMemo(() => { - if (!hasRecordGroups) { - return undefined; - } - - if (!context) { - throw new Error( - 'useCurrentRecordGroupDefinition must be used within a RecordGroupContextProvider.', - ); - } - - return recordGroupDefinitions.find( - ({ id }) => id === context.recordGroupId, - ); - }, [context, hasRecordGroups, recordGroupDefinitions]); - return recordGroupDefinition; }; 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 b99af28b6957..b1bc9829d768 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 @@ -2,24 +2,18 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; 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 { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; -import { ViewType } from '@/views/types/ViewType'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useCallback, useContext, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; -type UseRecordGroupActionsParams = { - viewType: ViewType; -}; - -export const useRecordGroupActions = ({ - viewType, -}: UseRecordGroupActionsParams) => { +export const useRecordGroupActions = () => { const navigate = useNavigate(); const location = useLocation(); @@ -35,14 +29,13 @@ export const useRecordGroupActions = ({ objectNameSingular, }); - const { viewGroupFieldMetadataItem } = useRecordGroups({ - objectNameSingular, - }); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + ); const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, - viewType, }); const setNavigationMemorizedUrl = useSetRecoilState( @@ -52,11 +45,11 @@ export const useRecordGroupActions = ({ const navigateToSelectSettings = useCallback(() => { setNavigationMemorizedUrl(location.pathname + location.search); - if (!isDefined(viewGroupFieldMetadataItem)) { - throw new Error('viewGroupFieldMetadataItem is not a non-empty string'); + if (!isDefined(recordGroupFieldMetadata)) { + throw new Error('recordGroupFieldMetadata is not a non-empty string'); } - const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(viewGroupFieldMetadataItem)}`; + const settingsPath = `/settings/objects/${getObjectSlug(objectMetadataItem)}/${getFieldSlug(recordGroupFieldMetadata)}`; navigate(settingsPath); }, [ @@ -65,7 +58,7 @@ export const useRecordGroupActions = ({ location.search, navigate, objectMetadataItem, - viewGroupFieldMetadataItem, + recordGroupFieldMetadata, ]); const recordGroupActions: RecordGroupAction[] = useMemo( diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts index b0c738a0fbc5..9b1c038e1a38 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorder.ts @@ -1,59 +1,89 @@ import { OnDragEndResponder } from '@hello-pangea/dnd'; -import { useCallback } from 'react'; -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; -import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; +import { useRecoilCallback } from 'recoil'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; type UseRecordGroupHandlersParams = { - objectNameSingular: string; viewBarId: string; }; export const useRecordGroupReorder = ({ - objectNameSingular, viewBarId, }: UseRecordGroupHandlersParams) => { - const setRecordGroupDefinitions = useSetRecoilComponentStateV2( - recordGroupDefinitionsComponentState, - ); + const setRecordGroup = useSetRecordGroup(viewBarId); - const { visibleRecordGroups } = useRecordGroups({ - objectNameSingular: objectNameSingular, - }); + const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentSelector, + ); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); - const handleOrderChange: OnDragEndResponder = useCallback( - (result) => { - if (!result.destination) { - return; - } + const handleOrderChange: OnDragEndResponder = useRecoilCallback( + ({ snapshot }) => + (result) => { + if (!result.destination) { + return; + } + + const visibleRecordGroupIds = getSnapshotValue( + snapshot, + visibleRecordGroupIdsSelector, + ); + + const reorderedVisibleRecordGroupIds = moveArrayItem( + visibleRecordGroupIds, + { + fromIndex: result.source.index - 1, + toIndex: result.destination.index - 1, + }, + ); + + if ( + isDeeplyEqual(visibleRecordGroupIds, reorderedVisibleRecordGroupIds) + ) { + return; + } - const reorderedVisibleBoardGroups = moveArrayItem(visibleRecordGroups, { - fromIndex: result.source.index - 1, - toIndex: result.destination.index - 1, - }); + const updatedRecordGroups = reorderedVisibleRecordGroupIds.reduce< + RecordGroupDefinition[] + >((acc, recordGroupId, index) => { + const recordGroupDefinition = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); - if (isDeeplyEqual(visibleRecordGroups, reorderedVisibleBoardGroups)) - return; + if (!isDefined(recordGroupDefinition)) { + return acc; + } - const updatedGroups = [...reorderedVisibleBoardGroups].map( - (group, index) => ({ ...group, position: index }), - ); + return [ + ...acc, + { + ...recordGroupDefinition, + position: index, + }, + ]; + }, []); - setRecordGroupDefinitions(updatedGroups); - saveViewGroups(mapRecordGroupDefinitionsToViewGroups(updatedGroups)); - }, - [saveViewGroups, setRecordGroupDefinitions, visibleRecordGroups], + setRecordGroup(updatedRecordGroups); + saveViewGroups( + mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), + ); + }, + [saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector], ); return { - visibleRecordGroups, handleOrderChange, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts index 5f877bff3494..aa26edec35b9 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupVisibility.ts @@ -1,125 +1,113 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; -import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; -import { ViewType } from '@/views/types/ViewType'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; +import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; import { useRecoilCallback } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; type UseRecordGroupVisibilityParams = { viewBarId: string; - viewType: ViewType; }; export const useRecordGroupVisibility = ({ viewBarId, - viewType, }: UseRecordGroupVisibilityParams) => { - const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, ); - const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( - tableRowIdsByGroupComponentFamilyState, + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, viewBarId, ); - const { recordIdsByColumnIdFamilyState } = useRecordBoardStates(viewBarId); - const objectOptionsDropdownRecordGroupHideState = useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); - const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); + const { saveViewGroup, saveViewGroups } = useSaveCurrentViewGroups(viewBarId); const handleVisibilityChange = useRecoilCallback( - ({ snapshot, set }) => - async (updatedRecordGroupDefinition: RecordGroupDefinition) => { - const recordGroupDefinitions = getSnapshotValue( - snapshot, - recordGroupDefinitionsState, + ({ set }) => + async (updatedRecordGroup: RecordGroupDefinition) => { + set( + recordGroupDefinitionFamilyState(updatedRecordGroup.id), + updatedRecordGroup, ); - const updatedRecordGroupDefinitions = recordGroupDefinitions.map( - (groupDefinition) => - groupDefinition.id === updatedRecordGroupDefinition.id - ? { - ...groupDefinition, - isVisible: !groupDefinition.isVisible, - } - : groupDefinition, - ); - - set(recordGroupDefinitionsState, updatedRecordGroupDefinitions); - - saveViewGroups( - mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), - ); + saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup)); // If visibility is manually toggled, we should reset the hideEmptyRecordGroup state set(objectOptionsDropdownRecordGroupHideState, false); }, - [ - objectOptionsDropdownRecordGroupHideState, - recordGroupDefinitionsState, - saveViewGroups, - ], + [saveViewGroup, objectOptionsDropdownRecordGroupHideState], ); const handleHideEmptyRecordGroupChange = useRecoilCallback( ({ snapshot, set }) => async () => { - const recordGroupDefinitions = getSnapshotValue( + const updatedRecordGroupDefinitions: RecordGroupDefinition[] = []; + const recordGroupIds = getSnapshotValue( snapshot, - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, ); const currentHideState = getSnapshotValue( snapshot, objectOptionsDropdownRecordGroupHideState, ); + const newHideState = !currentHideState; - set(objectOptionsDropdownRecordGroupHideState, !currentHideState); - - const updatedRecordGroupDefinitions = recordGroupDefinitions.map( - (recordGroup) => { - // TODO: Maybe we can improve that and only use one state for both table and board - const recordGroupRowIds = - viewType === ViewType.Table - ? getSnapshotValue( - snapshot, - tableRowIdsByGroupFamilyState(recordGroup.id), - ) - : getSnapshotValue( - snapshot, - recordIdsByColumnIdFamilyState(recordGroup.id), - ); - - if (recordGroupRowIds.length > 0) { - return recordGroup; - } - - return { - ...recordGroup, - isVisible: currentHideState, - }; - }, - ); + set(objectOptionsDropdownRecordGroupHideState, newHideState); + + for (const recordGroupId of recordGroupIds) { + const recordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroup)) { + throw new Error( + `Record group with id ${recordGroupId} not found in snapshot`, + ); + } + + const recordGroupRowIds = getSnapshotValue( + snapshot, + recordIndexRowIdsByGroupFamilyState(recordGroupId), + ); + + if (recordGroupRowIds.length > 0) { + continue; + } + + const updatedRecordGroup = { + ...recordGroup, + isVisible: !newHideState, + }; + + set( + recordGroupDefinitionFamilyState(recordGroupId), + updatedRecordGroup, + ); + + updatedRecordGroupDefinitions.push(updatedRecordGroup); + } saveViewGroups( mapRecordGroupDefinitionsToViewGroups(updatedRecordGroupDefinitions), ); }, [ - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, objectOptionsDropdownRecordGroupHideState, saveViewGroups, - viewType, - tableRowIdsByGroupFamilyState, - recordIdsByColumnIdFamilyState, + recordIndexRowIdsByGroupFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts deleted file mode 100644 index 8f638dbbe486..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroups.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useMemo } from 'react'; - -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; -import { sortRecordGroupDefinitions } from '@/object-record/record-group/utils/sortRecordGroupDefinitions'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; - -type UseRecordGroupsParams = { - objectNameSingular: string; -}; - -export const useRecordGroups = ({ - objectNameSingular, -}: UseRecordGroupsParams) => { - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - ); - - const recordGroupSort = useRecoilComponentValueV2( - recordIndexRecordGroupSortComponentState, - ); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - - const viewGroupFieldMetadataItem = useMemo(() => { - if (recordGroupDefinitions.length === 0) return null; - // We're assuming that all groups have the same fieldMetadataId for now - const fieldMetadataId = - 'fieldMetadataId' in recordGroupDefinitions[0] - ? recordGroupDefinitions[0].fieldMetadataId - : null; - - if (!fieldMetadataId) return null; - - return objectMetadataItem.fields.find( - (field) => field.id === fieldMetadataId, - ); - }, [objectMetadataItem, recordGroupDefinitions]); - - const visibleRecordGroups = useMemo( - () => sortRecordGroupDefinitions(recordGroupDefinitions, recordGroupSort), - [recordGroupDefinitions, recordGroupSort], - ); - - const hiddenRecordGroups = useMemo( - () => recordGroupDefinitions.filter((boardGroup) => !boardGroup.isVisible), - [recordGroupDefinitions], - ); - - return { - hiddenRecordGroups, - visibleRecordGroups, - viewGroupFieldMetadataItem, - }; -}; 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 new file mode 100644 index 000000000000..d84e393a08c7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useSetRecordGroup.ts @@ -0,0 +1,83 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +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 { 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'; +import { isDefined } from '~/utils/isDefined'; + +export const useSetRecordGroup = (viewId?: string) => { + const { objectMetadataItem } = useContext(RecordIndexRootPropsContext); + + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, + viewId, + ); + + const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( + recordGroupFieldMetadataComponentState, + viewId, + ); + + return useRecoilCallback( + ({ snapshot, set }) => + (recordGroups: RecordGroupDefinition[]) => { + if (recordGroups.length === 0) { + return; + } + + const currentRecordGroupId = getSnapshotValue( + snapshot, + recordIndexRecordGroupIdsState, + ); + const fieldMetadataId = recordGroups[0].fieldMetadataId; + const fieldMetadata = objectMetadataItem.fields.find( + (field) => field.id === fieldMetadataId, + ); + const currentFieldMetadata = getSnapshotValue( + snapshot, + recordGroupFieldMetadataState, + ); + + // Set the field metadata linked to the record groups + if ( + isDefined(fieldMetadata) && + !isDeeplyEqual(fieldMetadata, currentFieldMetadata) + ) { + set(recordGroupFieldMetadataState, fieldMetadata); + } + + // Set the record groups by id + recordGroups.forEach((recordGroup) => { + const existingRecordGroup = getSnapshotValue( + snapshot, + recordGroupDefinitionFamilyState(recordGroup.id), + ); + + if (isDeeplyEqual(existingRecordGroup, recordGroup)) { + return; + } + + set(recordGroupDefinitionFamilyState(recordGroup.id), recordGroup); + }); + + const recordGroupIds = recordGroups.map(({ id }) => id); + + if (isDeeplyEqual(currentRecordGroupId, recordGroupIds)) { + return; + } + + // Set the record group ids + set(recordIndexRecordGroupIdsState, recordGroupIds); + }, + [ + objectMetadataItem.fields, + recordGroupFieldMetadataState, + recordIndexRecordGroupIdsState, + ], + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts new file mode 100644 index 000000000000..80cf70f68590 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionFamilyState.ts @@ -0,0 +1,10 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { atomFamily } from 'recoil'; + +export const recordGroupDefinitionFamilyState = atomFamily< + RecordGroupDefinition | undefined, + RecordGroupDefinition['id'] +>({ + key: 'recordGroupDefinitionFamilyState', + default: undefined, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts new file mode 100644 index 000000000000..144b47a043ac --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupFieldMetadataComponentState.ts @@ -0,0 +1,11 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordGroupFieldMetadataComponentState = createComponentStateV2< + FieldMetadataItem | undefined +>({ + key: 'recordGroupFieldMetadataComponentState', + defaultValue: undefined, + componentInstanceContext: ViewComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupIdsComponentState.ts similarity index 72% rename from packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts rename to packages/twenty-front/src/modules/object-record/record-group/states/recordGroupIdsComponentState.ts index 56ec80fcc243..6665fb6e44e3 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupDefinitionsComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupIdsComponentState.ts @@ -2,10 +2,10 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/Record import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const recordGroupDefinitionsComponentState = createComponentStateV2< - RecordGroupDefinition[] +export const recordGroupIdsComponentState = createComponentStateV2< + RecordGroupDefinition['id'][] >({ - key: 'recordGroupDefinitionsComponentState', + key: 'recordGroupIdsComponentState', defaultValue: [], componentInstanceContext: ViewComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts similarity index 52% rename from packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts rename to packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts index bccab902cc86..7603240105c2 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector.ts @@ -1,21 +1,21 @@ -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const hasRecordGroupDefinitionsComponentSelector = +export const hasRecordGroupsComponentSelector = createComponentSelectorV2({ - key: 'hasRecordGroupDefinitionsComponentSelector', + key: 'hasRecordGroupsComponentSelector', componentInstanceContext: ViewComponentInstanceContext, get: ({ instanceId }) => ({ get }) => { - const recordGroupDefinitions = get( - recordGroupDefinitionsComponentState.atomFamily({ + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ instanceId, }), ); - return recordGroupDefinitions.length > 0; + return recordGroupIds.length > 0; }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000000..4c57886cbff8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/hiddenRecordGroupIdsComponentSelector.ts @@ -0,0 +1,34 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const hiddenRecordGroupIdsComponentSelector = createComponentSelectorV2< + string[] +>({ + key: 'hiddenRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + return recordGroupIds.filter((recordGroupId) => { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + return false; + } + + return !recordGroupDefinition.isVisible; + }); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts new file mode 100644 index 000000000000..ce0e0432eabe --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/recordGroupDefinitionsComponentSelector.ts @@ -0,0 +1,37 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const recordGroupDefinitionsComponentSelector = + createComponentSelectorV2({ + key: 'recordGroupDefinitionsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + return recordGroupIds.reduce( + (acc, recordGroupId) => { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + return acc; + } + + return [...acc, recordGroupDefinition]; + }, + [], + ); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000000..4c2332ebbdde --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts @@ -0,0 +1,63 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; +import { sortedInsert } from '@/object-record/record-group/utils/sortedInsert'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; + +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const visibleRecordGroupIdsComponentSelector = createComponentSelectorV2< + string[] +>({ + key: 'visibleRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupSort = get( + recordIndexRecordGroupSortComponentState.atomFamily({ + instanceId, + }), + ); + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + const result: RecordGroupDefinition[] = []; + + const comparator = ( + a: RecordGroupDefinition, + b: RecordGroupDefinition, + ) => { + switch (recordGroupSort) { + case RecordGroupSort.Alphabetical: + return a.title.localeCompare(b.title); + case RecordGroupSort.ReverseAlphabetical: + return b.title.localeCompare(a.title); + case RecordGroupSort.Manual: + default: + return a.position - b.position; + } + }; + + for (const recordGroupId of recordGroupIds) { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if ( + isDefined(recordGroupDefinition) && + recordGroupDefinition.isVisible + ) { + sortedInsert(result, recordGroupDefinition, comparator); + } + } + + return result.map(({ id }) => id); + }, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts index 45c3a68b6073..afe2aa41a879 100644 --- a/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/sortRecordGroupDefinitions.ts @@ -5,7 +5,7 @@ export const sortRecordGroupDefinitions = ( recordGroupDefinitions: RecordGroupDefinition[], recordGroupSort: RecordGroupSort, ) => { - const visibleGroups = recordGroupDefinitions.filter( + const visibleRecordGroups = recordGroupDefinitions.filter( (boardGroup) => boardGroup.isVisible, ); @@ -17,15 +17,15 @@ export const sortRecordGroupDefinitions = ( switch (recordGroupSort) { case RecordGroupSort.Alphabetical: - return visibleGroups.sort((a, b) => + return visibleRecordGroups.sort((a, b) => compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase()), ); case RecordGroupSort.ReverseAlphabetical: - return visibleGroups.sort((a, b) => + return visibleRecordGroups.sort((a, b) => compareAlphabetical(a.title.toLowerCase(), b.title.toLowerCase(), true), ); case RecordGroupSort.Manual: default: - return visibleGroups.sort((a, b) => a.position - b.position); + return visibleRecordGroups.sort((a, b) => a.position - b.position); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts b/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts new file mode 100644 index 000000000000..5a7dd1027e66 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/utils/sortedInsert.ts @@ -0,0 +1,20 @@ +export const sortedInsert = ( + array: T[], + item: T, + comparator: (a: T, b: T) => number, +) => { + let low = 0; + let high = array.length; + + while (low < high) { + const mid = Math.floor((low + high) / 2); + + if (comparator(item, array[mid]) < 0) { + high = mid; + } else { + low = mid + 1; + } + } + + array.splice(low, 0, item); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx index 6f6a2e2bede4..bde4108f5eb0 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect.tsx @@ -5,7 +5,7 @@ import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState'; import { useLoadRecordIndexBoardColumn } from '@/object-record/record-index/hooks/useLoadRecordIndexBoardColumn'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; -import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2'; export const RecordIndexBoardColumnLoaderEffect = ({ objectNameSingular, @@ -18,20 +18,14 @@ export const RecordIndexBoardColumnLoaderEffect = ({ boardFieldMetadataId: string | null; columnId: string; }) => { - const [shouldFetchMore, setShouldFetchMore] = useRecoilState( - recordBoardShouldFetchMoreInColumnComponentFamilyState({ - scopeId: getScopeIdFromComponentId(recordBoardId), - familyKey: columnId, - }), + const [shouldFetchMore, setShouldFetchMore] = useRecoilComponentFamilyStateV2( + recordBoardShouldFetchMoreInColumnComponentFamilyState, + columnId, + recordBoardId, ); const [loadingRecordsForThisColumn, setLoadingRecordsForThisColumn] = - useRecoilState( - isRecordBoardFetchingRecordsByColumnFamilyState({ - scopeId: getScopeIdFromComponentId(recordBoardId), - familyKey: { columnId }, - }), - ); + useRecoilState(isRecordBoardFetchingRecordsByColumnFamilyState(columnId)); const { fetchMoreRecords, loading, records, hasNextPage } = useLoadRecordIndexBoardColumn({ diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx index 194580587dc9..e8bd37d0534a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoader.tsx @@ -1,9 +1,10 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; import { RecordIndexBoardColumnLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardColumnLoaderEffect'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RecordIndexBoardDataLoaderProps = { objectNameSingular: string; @@ -18,6 +19,10 @@ export const RecordIndexBoardDataLoader = ({ objectNameSingular, }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -26,18 +31,14 @@ export const RecordIndexBoardDataLoader = ({ (field) => field.id === recordIndexKanbanFieldMetadataId, ); - const { columnIdsState } = useRecordBoardStates(recordBoardId); - - const columnIds = useRecoilValue(columnIdsState); - return ( <> - {columnIds.map((columnId, index) => ( + {visibleRecordGroupIds.map((recordGroupId, index) => ( ))} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx index 02834d1e7f9e..81f64f3f6f8b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx @@ -1,101 +1,52 @@ import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; +import { recordBoardSelectedRecordIdsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsComponentSelector'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; -import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; type RecordIndexBoardDataLoaderEffectProps = { - objectNameSingular: string; recordBoardId: string; }; export const RecordIndexBoardDataLoaderEffect = ({ - objectNameSingular, recordBoardId, }: RecordIndexBoardDataLoaderEffectProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular, - }); - const recordIndexFieldDefinitions = useRecoilValue( recordIndexFieldDefinitionsState, ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, + const recordIndexIsCompactModeActive = useRecoilValue( + recordIndexIsCompactModeActiveState, ); - const recordIndexKanbanFieldMetadataId = useRecoilValue( - recordIndexKanbanFieldMetadataIdState, + const setRecordBoardFieldDefinitions = useSetRecoilComponentStateV2( + recordBoardFieldDefinitionsComponentState, + recordBoardId, ); - const recordIndexIsCompactModeActive = useRecoilValue( - recordIndexIsCompactModeActiveState, + const selectedRecordIds = useRecoilComponentValueV2( + recordBoardSelectedRecordIdsComponentSelector, + recordBoardId, ); - const { isCompactModeActiveState } = useRecordBoard(recordBoardId); - - const setIsCompactModeActive = useSetRecoilState(isCompactModeActiveState); + const setIsCompactModeActive = useSetRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); useEffect(() => { setIsCompactModeActive(recordIndexIsCompactModeActive); }, [recordIndexIsCompactModeActive, setIsCompactModeActive]); - const { - setColumns, - setObjectSingularName, - selectedRecordIdsSelector, - setFieldDefinitions, - setKanbanFieldMetadataName, - } = useRecordBoard(recordBoardId); - - useEffect(() => { - setFieldDefinitions(recordIndexFieldDefinitions); - }, [recordIndexFieldDefinitions, setFieldDefinitions]); - - useEffect(() => { - setObjectSingularName(objectNameSingular); - }, [objectNameSingular, setObjectSingularName]); - - useEffect(() => { - setColumns(recordGroupDefinitions); - }, [recordGroupDefinitions, setColumns]); - - // TODO: Remove this duplicate useEffect by ensuring it's not here because - // We want it to be triggered by a change of objectMetadataItem, which would be an anti-pattern - // As it is an unnecessary dependency useEffect(() => { - setFieldDefinitions(recordIndexFieldDefinitions); - }, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]); - - useEffect(() => { - if (isDefined(recordIndexKanbanFieldMetadataId)) { - const kanbanFieldMetadataName = objectMetadataItem?.fields.find( - (field) => - field.type === FieldMetadataType.Select && - field.id === recordIndexKanbanFieldMetadataId, - )?.name; - - if (isDefined(kanbanFieldMetadataName)) { - setKanbanFieldMetadataName(kanbanFieldMetadataName); - } - } - }, [ - objectMetadataItem, - recordIndexKanbanFieldMetadataId, - setKanbanFieldMetadataName, - ]); - - const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); + setRecordBoardFieldDefinitions(recordIndexFieldDefinitions); + }, [recordIndexFieldDefinitions, setRecordBoardFieldDefinitions]); const setContextStoreTargetedRecords = useSetRecoilComponentStateV2( contextStoreTargetedRecordsRuleComponentState, 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 feda1b8b966d..0da7a717210a 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 @@ -24,11 +24,9 @@ import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/compone import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { useSetRecordGroup } from '@/object-record/record-group/hooks/useSetRecordGroup'; import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; -import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewField } from '@/views/types/ViewField'; @@ -38,7 +36,7 @@ import { mapViewFieldsToColumnDefinitions } from '@/views/utils/mapViewFieldsToC import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewGroupsToRecordGroupDefinitions } from '@/views/utils/mapViewGroupsToRecordGroupDefinitions'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; -import { useContext } from 'react'; +import { useCallback, useContext } from 'react'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; const StyledContainer = styled.div` @@ -68,9 +66,7 @@ export const RecordIndexContainer = () => { objectNameSingular, } = useContext(RecordIndexRootPropsContext); - const recordGroupDefinitionsCallbackState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, - ); + const setRecordGroup = useSetRecordGroup(recordIndexId); const { columnDefinitions, filterDefinitions, sortDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); @@ -96,8 +92,6 @@ export const RecordIndexContainer = () => { recordTableId: recordIndexId, }); - const { setColumns } = useRecordBoard(recordIndexId); - const onViewFieldsChange = useRecoilCallback( ({ set, snapshot }) => (viewFields: ViewField[]) => { @@ -124,30 +118,16 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); - const onViewGroupsChange = useRecoilCallback( - ({ set, snapshot }) => - (viewGroups: ViewGroup[]) => { - const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({ - objectMetadataItem, - viewGroups, - }); - - setColumns(newGroupDefinitions); - - const existingRecordIndexGroupDefinitions = snapshot - .getLoadable(recordGroupDefinitionsCallbackState) - .getValue(); + const onViewGroupsChange = useCallback( + (viewGroups: ViewGroup[]) => { + const newGroupDefinitions = mapViewGroupsToRecordGroupDefinitions({ + objectMetadataItem, + viewGroups, + }); - if ( - !isDeeplyEqual( - existingRecordIndexGroupDefinitions, - newGroupDefinitions, - ) - ) { - set(recordGroupDefinitionsCallbackState, newGroupDefinitions); - } - }, - [objectMetadataItem, recordGroupDefinitionsCallbackState, setColumns], + setRecordGroup(newGroupDefinitions); + }, + [objectMetadataItem, setRecordGroup], ); const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( @@ -229,10 +209,7 @@ export const RecordIndexContainer = () => { objectNameSingular={objectNameSingular} recordBoardId={recordIndexId} /> - + )} 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 65c8b130399f..ace9040aee3e 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,8 +1,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +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'; @@ -11,6 +12,7 @@ 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; @@ -32,6 +34,10 @@ export const RecordIndexPageKanbanAddButton = () => { ); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); + const recordIndexKanbanFieldMetadataId = useRecoilValue( recordIndexKanbanFieldMetadataIdState, ); @@ -42,12 +48,11 @@ export const RecordIndexPageKanbanAddButton = () => { const isOpportunity = objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; - const { columnIdsState, visibleFieldDefinitionsState } = - useRecordBoardStates(recordIndexId); - const columnIds = useRecoilValue(columnIdsState); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, + recordIndexId, ); + const labelIdentifierField = visibleFieldDefinitions.find( (field) => field.isLabelIdentifier, ); @@ -101,11 +106,10 @@ export const RecordIndexPageKanbanAddButton = () => { dropdownComponents={ - {columnIds.map((columnId) => ( + {visibleRecordGroupIds.map((recordGroupId) => ( ))} diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx index effa5ce3a49c..c209a6b0ee3a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem.tsx @@ -1,7 +1,9 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { useRecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { MenuItem, Tag } from 'twenty-ui'; +import { isDefined } from '~/utils/isDefined'; const StyledMenuItem = styled(MenuItem)` width: calc(100% - 2 * var(--horizontal-padding)); @@ -9,20 +11,18 @@ const StyledMenuItem = styled(MenuItem)` type RecordIndexPageKanbanAddMenuItemProps = { columnId: string; - recordIndexId: string; onItemClick: (columnDefinition: any) => void; }; export const RecordIndexPageKanbanAddMenuItem = ({ columnId, - recordIndexId, onItemClick, }: RecordIndexPageKanbanAddMenuItemProps) => { - const { columnDefinition } = useRecordIndexPageKanbanAddMenuItem( - recordIndexId, - columnId, + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), ); - if (!columnDefinition) { + + if (!isDefined(recordGroupDefinition)) { return null; } @@ -31,24 +31,24 @@ export const RecordIndexPageKanbanAddMenuItem = ({ text={ } - onClick={() => onItemClick(columnDefinition)} + onClick={() => onItemClick(recordGroupDefinition)} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts index e1bc40ca0eef..94042f4e02b1 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/__tests__/useExportFetchRecords.test.ts @@ -8,14 +8,12 @@ import { import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; -import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { ViewType } from '@/views/types/ViewType'; import { MockedResponse } from '@apollo/client/testing'; import { expect } from '@storybook/test'; import gql from 'graphql-tag'; -import { useRecoilValue } from 'recoil'; import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -232,10 +230,12 @@ describe('useRecordData', () => { const callback = jest.fn(); const { result } = renderHook( () => { - const kanbanFieldNameState = extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - recordIndexId, - ); + const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = + useRecoilComponentStateV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + return { tableData: useExportFetchRecords({ recordIndexId, @@ -246,8 +246,8 @@ describe('useRecordData', () => { delayMs: 0, viewType: ViewType.Kanban, }), - useRecordBoardHook: useRecordBoard(recordIndexId), - kanbanFieldName: useRecoilValue(kanbanFieldNameState), + kanbanFieldName: recordGroupFieldMetadata?.name, + setRecordGroupFieldMetadata, kanbanData: useObjectOptionsForBoard({ objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, @@ -269,9 +269,7 @@ describe('useRecordData', () => { ); await act(async () => { - result.current.useRecordBoardHook.setKanbanFieldMetadataName( - updatedAtFieldMetadataItem?.name, - ); + result.current.setRecordGroupFieldMetadata(updatedAtFieldMetadataItem); }); await act(async () => { @@ -322,10 +320,12 @@ describe('useRecordData', () => { const callback = jest.fn(); const { result } = renderHook( () => { - const kanbanFieldNameState = extractComponentState( - recordBoardKanbanFieldMetadataNameComponentState, - recordIndexId, - ); + const [recordGroupFieldMetadata, setRecordGroupFieldMetadata] = + useRecoilComponentStateV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + return { tableData: useExportFetchRecords({ recordIndexId, @@ -336,8 +336,9 @@ describe('useRecordData', () => { delayMs: 0, viewType: ViewType.Table, }), - setKanbanFieldName: useRecordBoard(recordIndexId), - kanbanFieldName: useRecoilValue(kanbanFieldNameState), + objectMetadataItem, + kanbanFieldName: recordGroupFieldMetadata?.name, + setRecordGroupFieldMetadata, kanbanData: useObjectOptionsForBoard({ objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, @@ -351,9 +352,14 @@ describe('useRecordData', () => { ); await act(async () => { - result.current.setKanbanFieldName.setKanbanFieldMetadataName( - result.current.kanbanData.hiddenBoardFields[0].metadata.fieldName, - ); + const fieldMetadataItem = + result.current.objectMetadataItem?.fields.find( + (fieldMetadata) => + fieldMetadata.id === + result.current.kanbanData.hiddenBoardFields[0].fieldMetadataId, + ); + + result.current.setRecordGroupFieldMetadata(fieldMetadataItem); }); await act(async () => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts index bcc82de3073b..5237622268d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/export/hooks/useExportFetchRecords.ts @@ -1,5 +1,4 @@ import { useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; @@ -13,7 +12,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/object-options-dropdown/constants/ExportTableDataDefaultPageSize'; import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -68,10 +67,13 @@ export const useExportFetchRecords = ({ viewBarId: recordIndexId, }); - const { kanbanFieldMetadataNameState } = useRecordBoardStates(recordIndexId); - const kanbanFieldMetadataName = useRecoilValue(kanbanFieldMetadataNameState); + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + recordIndexId, + ); + const hiddenKanbanFieldColumn = hiddenBoardFields.find( - (column) => column.metadata.fieldName === kanbanFieldMetadataName, + (column) => column.metadata.fieldName === recordGroupFieldMetadata?.name, ); const columns = useRecoilComponentValueV2( visibleTableColumnsComponentSelector, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index 7b241909de0c..fee8cc97aa9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -1,12 +1,13 @@ import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/useSetRecordBoardRecordIds'; +import { isRecordBoardCompactModeActiveComponentState } from '@/object-record/record-board/states/isRecordBoardCompactModeActiveComponentState'; +import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -14,7 +15,7 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { recordIndexViewFilterGroupsState } from '@/object-record/record-index/states/recordIndexViewFilterGroupsState'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; type UseLoadRecordIndexBoardProps = { @@ -31,33 +32,28 @@ export const useLoadRecordIndexBoard = ({ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - const { - setRecordIds: setRecordIdsInBoard, - setFieldDefinitions, - setColumns, - isCompactModeActiveState, - } = useRecordBoard(recordBoardId); + + const setRecordBoardFieldDefinitions = useSetRecoilComponentStateV2( + recordBoardFieldDefinitionsComponentState, + recordBoardId, + ); + + const { setRecordIds: setRecordIdsInBoard } = + useSetRecordBoardRecordIds(recordBoardId); + const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); const recordIndexFieldDefinitions = useRecoilValue( recordIndexFieldDefinitionsState, ); useEffect(() => { - setFieldDefinitions(recordIndexFieldDefinitions); - }, [recordIndexFieldDefinitions, setFieldDefinitions]); + setRecordBoardFieldDefinitions(recordIndexFieldDefinitions); + }, [recordIndexFieldDefinitions, setRecordBoardFieldDefinitions]); const recordIndexViewFilterGroups = useRecoilValue( recordIndexViewFilterGroupsState, ); - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, - ); - - useEffect(() => { - setColumns(recordGroupDefinitions); - }, [recordGroupDefinitions, setColumns]); - const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); const requestFilters = computeViewRecordGqlOperationFilter( @@ -92,7 +88,10 @@ export const useLoadRecordIndexBoard = ({ const { setRecordCountInCurrentView } = useSetRecordCountInCurrentView(viewBarId); - const setIsCompactModeActive = useSetRecoilState(isCompactModeActiveState); + const setIsCompactModeActive = useSetRecoilComponentStateV2( + isRecordBoardCompactModeActiveComponentState, + recordBoardId, + ); useEffect(() => { setRecordIdsInBoard(records); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 99a5220335b6..2738118054fe 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -4,9 +4,9 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { useSetRecordIdsForColumn } from '@/object-record/record-board/hooks/useSetRecordIdsForColumn'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter'; +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; @@ -30,16 +30,18 @@ export const useLoadRecordIndexBoardColumn = ({ const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); - const { setRecordIdsForColumn } = useRecordBoard(recordBoardId); - const { columnsFamilySelector } = useRecordBoardStates(recordBoardId); + const { setRecordIdsForColumn } = useSetRecordIdsForColumn(recordBoardId); const { upsertRecords: upsertRecordsInStore } = useUpsertRecordsInStore(); + const recordGroupDefinition = useRecoilValue( + recordGroupDefinitionFamilyState(columnId), + ); + const recordIndexViewFilterGroups = useRecoilValue( recordIndexViewFilterGroupsState, ); const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); const requestFilters = computeViewRecordGqlOperationFilter( recordIndexFilters, @@ -60,9 +62,9 @@ export const useLoadRecordIndexBoardColumn = ({ const filter = { ...requestFilters, [recordIndexKanbanFieldMetadataItem?.name ?? '']: isDefined( - columnDefinition?.value, + recordGroupDefinition?.value, ) - ? { in: [columnDefinition?.value] } + ? { in: [recordGroupDefinition?.value] } : { is: 'NULL' }, }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 8162bb7dfc2e..d705ef14bc97 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -27,8 +27,7 @@ export const useFindManyParams = ( objectNameSingular, }); - const currentRecordGroupDefinition = - useCurrentRecordGroupDefinition(recordTableId); + const currentRecordGroupDefinition = useCurrentRecordGroupDefinition(); const tableViewFilterGroups = useRecoilComponentValueV2( tableViewFilterGroupsComponentState, diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts index aef238c2be0e..4386c2922128 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordBoardRecordGqlFields.ts @@ -1,9 +1,9 @@ -import { useRecoilValue } from 'recoil'; - import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectMetadataIdentifierFields } from '@/object-metadata/utils/getObjectMetadataIdentifierFields'; import { hasPositionField } from '@/object-metadata/utils/hasPositionField'; -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { recordBoardVisibleFieldDefinitionsComponentSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsComponentSelector'; +import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { isDefined } from '~/utils/isDefined'; export const useRecordBoardRecordGqlFields = ({ @@ -13,15 +13,17 @@ export const useRecordBoardRecordGqlFields = ({ recordBoardId: string; objectMetadataItem: ObjectMetadataItem; }) => { - const { kanbanFieldMetadataNameState, visibleFieldDefinitionsState } = - useRecordBoardStates(recordBoardId); + const visibleFieldDefinitions = useRecoilComponentValueV2( + recordBoardVisibleFieldDefinitionsComponentSelector, + recordBoardId, + ); const { imageIdentifierFieldMetadataItem, labelIdentifierFieldMetadataItem } = getObjectMetadataIdentifierFields({ objectMetadataItem }); - const kanbanFieldMetadataName = useRecoilValue(kanbanFieldMetadataNameState); - const visibleFieldDefinitions = useRecoilValue( - visibleFieldDefinitionsState(), + const recordGroupFieldMetadata = useRecoilComponentValueV2( + recordGroupFieldMetadataComponentState, + recordBoardId, ); const identifierQueryFields: Record = {}; @@ -59,8 +61,8 @@ export const useRecordBoardRecordGqlFields = ({ }, }; - if (isDefined(kanbanFieldMetadataName)) { - recordGqlFields[kanbanFieldMetadataName] = true; + if (isDefined(recordGroupFieldMetadata?.name)) { + recordGqlFields[recordGroupFieldMetadata.name] = true; } return recordGqlFields; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts deleted file mode 100644 index 8e5604cb0fe8..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddMenuItem.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; -import { useRecoilValue } from 'recoil'; - -export const useRecordIndexPageKanbanAddMenuItem = ( - recordIndexId: string, - columnId: string, -) => { - const { columnsFamilySelector } = useRecordBoardStates(recordIndexId); - const columnDefinition = useRecoilValue(columnsFamilySelector(columnId)); - - return { columnDefinition }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts new file mode 100644 index 000000000000..7e163fd08ea3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexAllRowIdsComponentState.ts @@ -0,0 +1,10 @@ +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; + +export const recordIndexAllRowIdsComponentState = createComponentStateV2< + string[] +>({ + key: 'recordIndexAllRowIdsComponentState', + defaultValue: [], + componentInstanceContext: ViewComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts similarity index 50% rename from packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts rename to packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts index 395f7f185bb7..fe35021c6eb5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState.ts @@ -1,10 +1,10 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -export const tableRowIdsByGroupComponentFamilyState = +export const recordIndexRowIdsByGroupComponentFamilyState = createComponentFamilyStateV2({ - key: 'tableRowIdsByGroupComponentFamilyState', + key: 'recordIndexRowIdsByGroupComponentFamilyState', defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, + componentInstanceContext: ViewComponentInstanceContext, }); 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 ac2fd976d25d..834761638081 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 @@ -1,7 +1,8 @@ import styled from '@emotion/styled'; import { isNonEmptyString, isNull } from '@sniptt/guards'; -import { hasRecordGroupDefinitionsComponentSelector } from '@/object-record/record-group/states/hasRecordGroupDefinitionsComponentSelector'; +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; 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'; @@ -16,7 +17,6 @@ import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -53,8 +53,8 @@ export const RecordTable = ({ recordTableId, ); - const tableRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -64,13 +64,13 @@ export const RecordTable = ({ ); const hasRecordGroups = useRecoilComponentValueV2( - hasRecordGroupDefinitionsComponentSelector, + hasRecordGroupsComponentSelector, recordTableId, ); const recordTableIsEmpty = !isRecordTableInitialLoading && - tableRowIds.length === 0 && + allRowIds.length === 0 && isNull(pendingRecordId); const { resetTableRowSelection, setRowSelected } = useRecordTable({ @@ -109,9 +109,7 @@ export const RecordTable = ({ {!hasRecordGroups ? ( ) : ( - + )} diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx index f36fa08ae48f..7febddcdfa36 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableNoRecordGroupRows.tsx @@ -1,14 +1,16 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableBodyFetchMoreLoader } from '@/object-record/record-table/record-table-body/components/RecordTableBodyFetchMoreLoader'; import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableNoRecordGroupRows = () => { - const rowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, + ); return ( <> - {rowIds.map((recordId, rowIndex) => { + {allRowIds.map((recordId, rowIndex) => { return ( { const recordGroupId = useCurrentRecordGroupId(); - const allRowIds = useRecoilComponentValueV2(tableAllRowIdsComponentState); + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, + ); const recordGroupRowIds = useRecoilComponentFamilyValueV2( - tableRowIdsByGroupComponentFamilyState, + recordIndexRowIdsByGroupComponentFamilyState, recordGroupId, ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx index 7b28090002e3..debedb73f53f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyHandler.tsx @@ -1,9 +1,9 @@ import { isNull } from '@sniptt/guards'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableEmptyState } from '@/object-record/record-table/empty-state/components/RecordTableEmptyState'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; type RecordTableEmptyHandlerProps = { @@ -20,8 +20,8 @@ export const RecordTableEmptyHandler = ({ recordTableId, ); - const tableRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -32,7 +32,7 @@ export const RecordTableEmptyHandler = ({ const recordTableIsEmpty = !isRecordTableInitialLoading && - tableRowIds.length === 0 && + allRowIds.length === 0 && isNull(pendingRecordId); if (recordTableIsEmpty) { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index 82b51a2e164b..409c30a461a4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -2,10 +2,10 @@ import { useRecoilCallback } from 'recoil'; import { getActionMenuDropdownIdFromActionMenuId } from '@/action-menu/utils/getActionMenuDropdownIdFromActionMenuId'; import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; @@ -18,8 +18,8 @@ export const useResetTableRowSelection = (recordTableId?: string) => { recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableIdFromContext, ); @@ -43,9 +43,9 @@ export const useResetTableRowSelection = (recordTableId?: string) => { return useRecoilCallback( ({ set, snapshot }) => () => { - const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), false); } @@ -54,7 +54,7 @@ export const useResetTableRowSelection = (recordTableId?: string) => { set(isActionMenuDropdownOpenState, false); }, [ - tableAllRowIdsState, + recordIndexAllRowIdsState, hasUserSelectedAllRowsState, isActionMenuDropdownOpenState, isRowSelectedFamilyState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts index 24d54fb1ba22..f715435c076e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts @@ -1,8 +1,8 @@ import { useRecoilCallback } from 'recoil'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-table/states/selectors/allRowsSelectedStatusComponentSelector'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -15,8 +15,8 @@ export const useSelectAllRows = (recordTableId?: string) => { isRowSelectedComponentFamilyState, recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -28,24 +28,24 @@ export const useSelectAllRows = (recordTableId?: string) => { allRowsSelectedStatusSelector, ); - const tableRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); if ( allRowsSelectedStatus === 'none' || allRowsSelectedStatus === 'some' ) { - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), true); } } else { - for (const rowId of tableRowIds) { + for (const rowId of allRowIds) { set(isRowSelectedFamilyState(rowId), false); } } }, [ allRowsSelectedStatusSelector, - tableAllRowIdsState, + recordIndexAllRowIdsState, isRowSelectedFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 7a35ef87a6ac..dcfa9e6f5483 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -1,11 +1,11 @@ import { useRecoilCallback } from 'recoil'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; +import { recordIndexRowIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRowIdsByGroupComponentFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { hasUserSelectedAllRowsComponentState } from '@/object-record/record-table/record-table-row/states/hasUserSelectedAllRowsFamilyState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; -import { tableRowIdsByGroupComponentFamilyState } from '@/object-record/record-table/states/tableRowIdsByGroupComponentFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; @@ -21,12 +21,12 @@ export const useSetRecordTableData = ({ recordTableId, onEntityCountChange, }: useSetRecordTableDataProps) => { - const tableRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( - tableRowIdsByGroupComponentFamilyState, + const recordIndexRowIdsByGroupFamilyState = useRecoilComponentCallbackStateV2( + recordIndexRowIdsByGroupComponentFamilyState, recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); const isRowSelectedFamilyState = useRecoilComponentCallbackStateV2( @@ -37,8 +37,8 @@ export const useSetRecordTableData = ({ hasUserSelectedAllRowsComponentState, recordTableId, ); - const recordGroupDefinitionsState = useRecoilComponentCallbackStateV2( - recordGroupDefinitionsComponentState, + const recordIndexRecordGroupIdsState = useRecoilComponentCallbackStateV2( + recordGroupIdsComponentState, recordTableId, ); @@ -46,11 +46,11 @@ export const useSetRecordTableData = ({ ({ set, snapshot }) => ({ records, - recordGroupId, + currentRecordGroupId, totalCount, }: { records: T[]; - recordGroupId?: string; + currentRecordGroupId?: string; totalCount?: number; }) => { for (const record of records) { @@ -66,9 +66,9 @@ export const useSetRecordTableData = ({ const currentRowIds = getSnapshotValue( snapshot, - recordGroupId - ? tableRowIdsByGroupFamilyState(recordGroupId) - : tableAllRowIdsState, + currentRecordGroupId + ? recordIndexRowIdsByGroupFamilyState(currentRecordGroupId) + : recordIndexAllRowIdsState, ); const hasUserSelectedAllRows = getSnapshotValue( @@ -76,9 +76,9 @@ export const useSetRecordTableData = ({ hasUserSelectedAllRowsState, ); - const recordGroupDefinitions = getSnapshotValue( + const recordGroupIds = getSnapshotValue( snapshot, - recordGroupDefinitionsState, + recordIndexRecordGroupIdsState, ); const recordIds = records.map((record) => record.id); @@ -90,39 +90,42 @@ export const useSetRecordTableData = ({ } } - if (isDefined(recordGroupId)) { + if (isDefined(currentRecordGroupId)) { // TODO: Hack to store all ids in the same order as the record group definitions // Should be replaced by something more efficient const allRowIds: string[] = []; - set(tableRowIdsByGroupFamilyState(recordGroupId), recordIds); + set( + recordIndexRowIdsByGroupFamilyState(currentRecordGroupId), + recordIds, + ); - for (const recordGroupDefinition of recordGroupDefinitions) { + for (const recordGroupId of recordGroupIds) { const tableRowIdsByGroup = - recordGroupDefinition.id !== recordGroupId + recordGroupId !== currentRecordGroupId ? getSnapshotValue( snapshot, - tableRowIdsByGroupFamilyState(recordGroupDefinition.id), + recordIndexRowIdsByGroupFamilyState(recordGroupId), ) : recordIds; allRowIds.push(...tableRowIdsByGroup); } - set(tableAllRowIdsState, allRowIds); + set(recordIndexAllRowIdsState, allRowIds); } else { - set(tableAllRowIdsState, recordIds); + set(recordIndexAllRowIdsState, recordIds); } onEntityCountChange(totalCount); } }, [ - tableRowIdsByGroupFamilyState, - tableAllRowIdsState, - recordGroupDefinitionsState, + recordIndexRowIdsByGroupFamilyState, + recordIndexAllRowIdsState, + hasUserSelectedAllRowsState, + recordIndexRecordGroupIdsState, onEntityCountChange, isRowSelectedFamilyState, - hasUserSelectedAllRowsState, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts index 7694d5f757af..bfbe8e0050a4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts @@ -3,9 +3,9 @@ import { useRecoilCallback } from 'recoil'; import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition'; @@ -17,8 +17,8 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { recordTableId, ); - const tableAllRowIdsState = useRecoilComponentCallbackStateV2( - tableAllRowIdsComponentState, + const recordIndexAllRowIdsState = useRecoilComponentCallbackStateV2( + recordIndexAllRowIdsComponentState, recordTableId, ); @@ -47,7 +47,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { const moveDown = useRecoilCallback( ({ snapshot }) => () => { - const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, @@ -64,7 +64,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { row: newRowIndex, }); }, - [tableAllRowIdsState, setSoftFocusPosition, softFocusPositionState], + [recordIndexAllRowIdsState, setSoftFocusPosition, softFocusPositionState], ); const numberOfTableColumnsSelector = useRecoilComponentCallbackStateV2( @@ -75,7 +75,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { const moveRight = useRecoilCallback( ({ snapshot }) => () => { - const allRowIds = getSnapshotValue(snapshot, tableAllRowIdsState); + const allRowIds = getSnapshotValue(snapshot, recordIndexAllRowIdsState); const softFocusPosition = getSnapshotValue( snapshot, softFocusPositionState, @@ -116,7 +116,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { } }, [ - tableAllRowIdsState, + recordIndexAllRowIdsState, softFocusPositionState, numberOfTableColumnsSelector, setSoftFocusPosition, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx index 0bcc4ac78cdc..cf3546e1d271 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext.tsx @@ -3,10 +3,10 @@ import { ReactNode, useContext } from 'react'; import { useSetRecoilState } from 'recoil'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { useComputeNewRowPosition } from '@/object-record/record-table/hooks/useComputeNewRowPosition'; import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; @@ -22,8 +22,8 @@ export const RecordTableBodyDragDropContext = ({ objectNameSingular, }); - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const { currentViewWithCombinedFiltersAndSorts } = @@ -43,7 +43,7 @@ export const RecordTableBodyDragDropContext = ({ return; } - const computeResult = computeNewRowPosition(result, tableAllRowIds); + const computeResult = computeNewRowPosition(result, allRowIds); if (!isDefined(computeResult)) { return; 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 f2a765d6d47f..19b13b6de290 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,22 +1,22 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableNoRecordGroupRows } from '@/object-record/record-table/components/RecordTableNoRecordGroupRows'; import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; 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 { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableNoRecordGroupBody = () => { - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, ); - if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { + if (isRecordTableInitialLoading && allRowIds.length === 0) { return ; } 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 155a0dcc6a4a..d768a67005dd 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 @@ -56,7 +56,7 @@ export const RecordTableRecordGroupBodyEffect = () => { if (!loading) { setRecordTableData({ records, - recordGroupId, + currentRecordGroupId: recordGroupId, totalCount, }); } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx index 9efde27e353e..ee45df94b8a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects.tsx @@ -1,18 +1,15 @@ import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; -import { recordGroupDefinitionsComponentState } from '@/object-record/record-group/states/recordGroupDefinitionsComponentState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; import { RecordTableRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffect'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableRecordGroupBodyEffects = () => { - const recordGroupDefinitions = useRecoilComponentValueV2( - recordGroupDefinitionsComponentState, + const recordGroupIds = useRecoilComponentValueV2( + recordGroupIdsComponentState, ); - return recordGroupDefinitions.map((recordGroupDefinition) => ( - + return recordGroupIds.map((recordGroupId) => ( + )); 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 d1f6ccb2fe7d..5f1b2dc77162 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,32 +1,28 @@ -import { useRecordGroups } from '@/object-record/record-group/hooks/useRecordGroups'; import { RecordGroupContext } from '@/object-record/record-group/states/context/RecordGroupContext'; +import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { RecordTableRecordGroupRows } from '@/object-record/record-table/components/RecordTableRecordGroupRows'; import { RecordTableBodyDragDropContext } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDragDropContext'; 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 { RecordTablePendingRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRow'; import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -type RecordTableRecordGroupsBodyProps = { - objectNameSingular: string; -}; - -export const RecordTableRecordGroupsBody = ({ - objectNameSingular, -}: RecordTableRecordGroupsBodyProps) => { - const tableAllRowIds = useRecoilComponentValueV2( - tableAllRowIdsComponentState, +export const RecordTableRecordGroupsBody = () => { + const allRowIds = useRecoilComponentValueV2( + recordIndexAllRowIdsComponentState, ); const isRecordTableInitialLoading = useRecoilComponentValueV2( isRecordTableInitialLoadingComponentState, ); - const { visibleRecordGroups } = useRecordGroups({ objectNameSingular }); + const visibleRecordGroupIds = useRecoilComponentValueV2( + visibleRecordGroupIdsComponentSelector, + ); - if (isRecordTableInitialLoading && tableAllRowIds.length === 0) { + if (isRecordTableInitialLoading && allRowIds.length === 0) { return ; } @@ -34,10 +30,10 @@ export const RecordTableRecordGroupsBody = ({ - {visibleRecordGroups.map((recordGroupDefinition) => ( + {visibleRecordGroupIds.map((recordGroupId) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx index 124d9a388ef7..31bcc917f192 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableColumnHeadDropdownMenu.tsx @@ -15,6 +15,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { onToggleColumnFilterComponentState } from '@/object-record/record-table/states/onToggleColumnFilterComponentState'; import { onToggleColumnSortComponentState } from '@/object-record/record-table/states/onToggleColumnSortComponentState'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useTableColumns } from '../../hooks/useTableColumns'; import { ColumnDefinition } from '../../types/ColumnDefinition'; @@ -91,43 +92,45 @@ export const RecordTableColumnHeadDropdownMenu = ({ const canHide = column.isLabelIdentifier !== true; return ( - - {isFilterable && ( - - )} - {isSortable && ( - - )} - {showSeparator && } - {canMoveLeft && ( - - )} - {canMoveRight && ( - - )} - {canHide && ( - - )} - + + + {isFilterable && ( + + )} + {isSortable && ( + + )} + {showSeparator && } + {canMoveLeft && ( + + )} + {canMoveRight && ( + + )} + {canHide && ( + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 9b8ea55ccabe..5a3e4299a445 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -65,6 +65,8 @@ const StyledColumnHeaderCell = styled.th<{ }`; } }}; + + // TODO: refactor this, each component should own its CSS div { overflow: hidden; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index 6ffb331a05d5..ff78e7c3f094 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -13,6 +13,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordTableHeaderPlusButtonContent = () => { @@ -41,21 +42,19 @@ export const RecordTableHeaderPlusButtonContent = () => { return ( <> - {hiddenTableColumns.length > 0 && ( - <> - - {hiddenTableColumns.map((column) => ( - handleAddColumn(column)} - LeftIcon={getIcon(column.iconName)} - text={column.label} - /> - ))} - - - - )} + + + {hiddenTableColumns.map((column) => ( + handleAddColumn(column)} + LeftIcon={getIcon(column.iconName)} + text={column.label} + /> + ))} + + + ({ get }) => { - const tableRowIds = get( - tableAllRowIdsComponentState.atomFamily({ + const allRowIds = get( + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); @@ -29,7 +30,7 @@ export const allRowsSelectedStatusComponentSelector = const allRowsSelectedStatus = numberOfSelectedRows === 0 ? 'none' - : selectedRowIds.length === tableRowIds.length + : selectedRowIds.length === allRowIds.length ? 'all' : 'some'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts index ced5cb600fc4..734056d3fe7c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/selectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const selectedRowIdsComponentSelector = createComponentSelectorV2< @@ -11,13 +11,14 @@ export const selectedRowIdsComponentSelector = createComponentSelectorV2< get: ({ instanceId }) => ({ get }) => { - const rowIds = get( - tableAllRowIdsComponentState.atomFamily({ + const allRowIds = get( + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); - return rowIds.filter( + return allRowIds.filter( (rowId) => get( isRowSelectedComponentFamilyState.atomFamily({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts index 1579e41b06c6..00fcba743dc1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts @@ -1,6 +1,6 @@ +import { recordIndexAllRowIdsComponentState } from '@/object-record/record-index/states/recordIndexAllRowIdsComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { tableAllRowIdsComponentState } from '@/object-record/record-table/states/tableAllRowIdsComponentState'; import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; export const unselectedRowIdsComponentSelector = createComponentSelectorV2< @@ -12,7 +12,8 @@ export const unselectedRowIdsComponentSelector = createComponentSelectorV2< ({ instanceId }) => ({ get }) => { const rowIds = get( - tableAllRowIdsComponentState.atomFamily({ + // TODO: Working because instanceId is the same, but we're not in the same context, should be changed ! + recordIndexAllRowIdsComponentState.atomFamily({ instanceId, }), ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts deleted file mode 100644 index e6f8ef4b24af..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-table/states/tableAllRowIdsComponentState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext'; -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; - -export const tableAllRowIdsComponentState = createComponentStateV2({ - key: 'tableAllRowIdsComponentState', - defaultValue: [], - componentInstanceContext: RecordTableComponentInstanceContext, -}); diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx new file mode 100644 index 000000000000..146734e75ad1 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminImpersonateUsers.tsx @@ -0,0 +1,67 @@ +import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate'; +import { TextInput } from '@/ui/input/components/TextInput'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { Button, H2Title, IconUser, Section } from 'twenty-ui'; + +const StyledLinkContainer = styled.div` + margin-right: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +const StyledErrorSection = styled.div` + color: ${({ theme }) => theme.font.color.danger}; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +export const SettingsAdminImpersonateUsers = () => { + const [userId, setUserId] = useState(''); + const { handleImpersonate, isLoading, error, canImpersonate } = + useImpersonate(); + + if (!canImpersonate) { + return ( +
+ +
+ ); + } + + return ( +
+ + + + handleImpersonate(userId)} + /> + +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts new file mode 100644 index 000000000000..e2e90e825119 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs.ts @@ -0,0 +1,2 @@ +export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID = + 'settings-admin-feature-flags-tab-id'; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts new file mode 100644 index 000000000000..8077e86c261e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/updateWorkspaceFeatureFlag.ts @@ -0,0 +1,15 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_WORKSPACE_FEATURE_FLAG = gql` + mutation UpdateWorkspaceFeatureFlag( + $workspaceId: String! + $featureFlag: String! + $value: Boolean! + ) { + updateWorkspaceFeatureFlag( + workspaceId: $workspaceId + featureFlag: $featureFlag + value: $value + ) + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts new file mode 100644 index 000000000000..a4f14c5bd6d2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/graphql/mutations/userLookupAdminPanel.ts @@ -0,0 +1,30 @@ +import { gql } from '@apollo/client'; + +export const USER_LOOKUP_ADMIN_PANEL = gql` + mutation UserLookupAdminPanel($userIdentifier: String!) { + userLookupAdminPanel(userIdentifier: $userIdentifier) { + user { + id + email + firstName + lastName + } + workspaces { + id + name + logo + totalUsers + users { + id + email + firstName + lastName + } + featureFlags { + key + value + } + } + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts new file mode 100644 index 000000000000..22ccd2b388e3 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useFeatureFlagsManagement.ts @@ -0,0 +1,91 @@ +import { UserLookup } from '@/settings/admin-panel/types/UserLookup'; +import { useState } from 'react'; +import { isDefined } from 'twenty-ui'; +import { + useUpdateWorkspaceFeatureFlagMutation, + useUserLookupAdminPanelMutation, +} from '~/generated/graphql'; + +export const useFeatureFlagsManagement = () => { + const [userLookupResult, setUserLookupResult] = useState( + null, + ); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const [userLookup] = useUserLookupAdminPanelMutation({ + onCompleted: (data) => { + setIsLoading(false); + if (isDefined(data?.userLookupAdminPanel)) { + setUserLookupResult(data.userLookupAdminPanel); + } + }, + onError: (error) => { + setIsLoading(false); + setError(error.message); + }, + }); + + const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation(); + + const handleUserLookup = async (userIdentifier: string) => { + setError(null); + setIsLoading(true); + setUserLookupResult(null); + + const response = await userLookup({ + variables: { userIdentifier }, + }); + + return response.data?.userLookupAdminPanel; + }; + + const handleFeatureFlagUpdate = async ( + workspaceId: string, + featureFlag: string, + value: boolean, + ) => { + setError(null); + const previousState = userLookupResult; + + if (isDefined(userLookupResult)) { + setUserLookupResult({ + ...userLookupResult, + workspaces: userLookupResult.workspaces.map((workspace) => + workspace.id === workspaceId + ? { + ...workspace, + featureFlags: workspace.featureFlags.map((flag) => + flag.key === featureFlag ? { ...flag, value } : flag, + ), + } + : workspace, + ), + }); + } + + const response = await updateFeatureFlag({ + variables: { + workspaceId, + featureFlag, + value, + }, + onError: (error) => { + if (isDefined(previousState)) { + setUserLookupResult(previousState); + } + setError(error.message); + }, + }); + + return !!response.data; + }; + + return { + userLookupResult, + handleUserLookup, + handleFeatureFlagUpdate, + isLoading, + error, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts new file mode 100644 index 000000000000..1046c70c917f --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -0,0 +1,60 @@ +import { useAuth } from '@/auth/hooks/useAuth'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { tokenPairState } from '@/auth/states/tokenPairState'; +import { AppPath } from '@/types/AppPath'; +import { useState } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useImpersonateMutation } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; +import { sleep } from '~/utils/sleep'; + +export const useImpersonate = () => { + const { clearSession } = useAuth(); + const [currentUser, setCurrentUser] = useRecoilState(currentUserState); + const setTokenPair = useSetRecoilState(tokenPairState); + const [impersonate] = useImpersonateMutation(); + + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleImpersonate = async (userId: string) => { + if (!userId.trim()) { + setError('Please enter a user ID'); + return; + } + + setIsLoading(true); + setError(null); + + try { + const impersonateResult = await impersonate({ + variables: { userId }, + }); + + if (isDefined(impersonateResult.errors)) { + throw impersonateResult.errors; + } + + if (!impersonateResult.data?.impersonate) { + throw new Error('No impersonate result'); + } + + const { user, tokens } = impersonateResult.data.impersonate; + await clearSession(); + setCurrentUser(user); + setTokenPair(tokens); + await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. + window.location.href = AppPath.Index; + } catch (error) { + setError('Failed to impersonate user. Please try again.'); + setIsLoading(false); + } + }; + + return { + handleImpersonate, + isLoading, + error, + canImpersonate: currentUser?.canImpersonate, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts new file mode 100644 index 000000000000..2c9ab89136fc --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/FeatureFlag.ts @@ -0,0 +1,4 @@ +export type FeatureFlag = { + key: string; + value: boolean; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts new file mode 100644 index 000000000000..0cb66e283f18 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/UserLookup.ts @@ -0,0 +1,11 @@ +import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo'; + +export type UserLookup = { + user: { + id: string; + email: string; + firstName?: string | null; + lastName?: string | null; + }; + workspaces: WorkspaceInfo[]; +}; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts b/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts new file mode 100644 index 000000000000..3d36fe20336d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/admin-panel/types/WorkspaceInfo.ts @@ -0,0 +1,15 @@ +import { FeatureFlag } from '@/settings/admin-panel/types/FeatureFlag'; + +export type WorkspaceInfo = { + id: string; + name: string; + logo?: string | null; + totalUsers: number; + users: { + id: string; + email: string; + firstName?: string | null; + lastName?: string | null; + }[]; + featureFlags: FeatureFlag[]; +}; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx index 54a46360a51b..4910e7a2aa8c 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx @@ -13,6 +13,7 @@ import { IconKey, IconMail, IconRocket, + IconServer, IconSettings, IconTool, IconUserCircle, @@ -21,6 +22,7 @@ import { } from 'twenty-ui'; import { useAuth } from '@/auth/hooks/useAuth'; +import { currentUserState } from '@/auth/states/currentUserState'; import { billingState } from '@/client-config/states/billingState'; import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem'; import { useExpandedAnimation } from '@/settings/hooks/useExpandedAnimation'; @@ -84,6 +86,8 @@ export const SettingsNavigationDrawerItems = () => { const isBillingPageEnabled = billing?.isBillingEnabled && !isFreeAccessEnabled; + const currentUser = useRecoilValue(currentUserState); + const isAdminPageEnabled = currentUser?.canImpersonate; // TODO: Refactor this part to only have arrays of navigation items const currentPathName = useLocation().pathname; @@ -230,6 +234,13 @@ export const SettingsNavigationDrawerItems = () => { + {isAdminPageEnabled && ( + + )} { - elements.floating.style.maxHeight = - availableHeight >= elements.floating.scrollHeight - ? '' - : `${availableHeight}px`; - - elements.floating.style.height = 'auto'; + apply: ({ availableHeight }) => { + flushSync(() => { + setDropdownMaxHeight(availableHeight); + }); }, boundary: document.querySelector('#root') ?? undefined, }), @@ -149,8 +154,15 @@ export const Dropdown = ({ [closeDropdown, isDropdownOpen], ); + const dropdownMenuStyles = { + ...floatingStyles, + maxHeight: dropdownMaxHeight, + }; + return ( - <> +
{clickableComponent && ( @@ -175,7 +187,7 @@ export const Dropdown = ({ width={dropdownMenuWidth ?? dropdownWidth} data-select-disable ref={refs.setFloating} - style={floatingStyles} + style={dropdownMenuStyles} > {dropdownComponents} @@ -187,7 +199,7 @@ export const Dropdown = ({ width={dropdownMenuWidth ?? dropdownWidth} data-select-disable ref={refs.setFloating} - style={floatingStyles} + style={dropdownMenuStyles} > {dropdownComponents} @@ -199,6 +211,6 @@ export const Dropdown = ({
- +
); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx index 2510c90c619a..26ef95317184 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenu.tsx @@ -26,6 +26,8 @@ const StyledDropdownMenu = styled.div<{ z-index: 30; width: ${({ width = 200 }) => typeof width === 'number' ? `${width}px` : width}; + + overflow: hidden; `; export const DropdownMenu = StyledDropdownMenu; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx index 1338d1c4d892..ca642bcc6d1d 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuItemsContainer.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; - const StyledDropdownMenuItemsExternalContainer = styled.div<{ hasMaxHeight?: boolean; }>` @@ -18,10 +16,6 @@ const StyledDropdownMenuItemsExternalContainer = styled.div<{ width: calc(100% - 2 * var(--padding)); `; -const StyledScrollWrapper = styled(ScrollWrapper)` - width: 100%; -`; - const StyledDropdownMenuItemsInternalContainer = styled.div` align-items: stretch; display: flex; @@ -48,17 +42,9 @@ export const DropdownMenuItemsContainer = ({ hasMaxHeight={hasMaxHeight} className={className} > - {hasMaxHeight ? ( - - - {children} - - - ) : ( - - {children} - - )} + + {children} + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx index 8c8501d45973..f2599f103c48 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSeparator.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; const StyledDropdownMenuSeparator = styled.div` background-color: ${({ theme }) => theme.border.color.light}; - height: 1px; + min-height: 1px; width: 100%; `; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts new file mode 100644 index 000000000000..243cbde49439 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/contexts/DropdownComponeInstanceContext.ts @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const DropdownComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts new file mode 100644 index 000000000000..9cd8185c09d1 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/states/dropdownMaxHeightComponentStateV2.ts @@ -0,0 +1,10 @@ +import { DropdownComponentInstanceContext } from '@/ui/layout/dropdown/contexts/DropdownComponeInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const dropdownMaxHeightComponentStateV2 = createComponentStateV2< + number | undefined +>({ + key: 'dropdownMaxHeightComponentStateV2', + componentInstanceContext: DropdownComponentInstanceContext, + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx index ee0a46590417..21fd9ea7355e 100644 --- a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx +++ b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useShowAuthModal.test.tsx @@ -243,17 +243,6 @@ const testCases = [ { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true }, { loc: AppPath.DevelopersCatchAll, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: false }, - { loc: AppPath.Impersonate, isLogged: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: true }, - { loc: AppPath.Impersonate, isLogged: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: false }, - { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: true }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: false }, { loc: AppPath.Authorize, isLogged: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: false }, diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx index af965ce982c0..71f2d5203c5e 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/Tab.tsx @@ -15,6 +15,7 @@ type TabProps = { disabled?: boolean; pill?: string | ReactElement; to?: string; + logo?: string; }; const StyledTab = styled('button', { @@ -61,6 +62,10 @@ const StyledHover = styled.span` background: ${({ theme }) => theme.background.quaternary}; } `; +const StyledLogo = styled.img` + height: 14px; + width: 14px; +`; export const Tab = ({ id, @@ -72,6 +77,7 @@ export const Tab = ({ disabled, pill, to, + logo, }: TabProps) => { const theme = useTheme(); return ( @@ -85,6 +91,7 @@ export const Tab = ({ to={to} > + {logo && } {Icon && } {title} {pill && typeof pill === 'string' ? : pill} diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 7d724d67e918..7dc93e7ee624 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -19,6 +19,7 @@ export type SingleTabProps = { disabled?: boolean; pill?: string | React.ReactElement; cards?: LayoutCard[]; + logo?: string; }; type TabListProps = { @@ -71,6 +72,7 @@ export const TabList = ({ key={tab.id} title={tab.title} Icon={tab.Icon} + logo={tab.logo} active={tab.id === activeTabId} disabled={tab.disabled ?? loading} pill={tab.pill} diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts index b7e7abf9f325..976362e46fc0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts @@ -23,7 +23,7 @@ export const useWorkspaceSwitching = () => { availableSSOIdentityProvidersState, ); const setSignInUpStep = useSetRecoilState(signInUpStepState); - const { signOut } = useAuth(); + const { clearSession } = useAuth(); const switchWorkspace = async (workspaceId: string) => { if (currentWorkspace?.id === workspaceId) return; @@ -50,7 +50,7 @@ export const useWorkspaceSwitching = () => { } if (jwt.data.generateJWT.availableSSOIDPs.length > 1) { - await signOut(); + await clearSession(); setAvailableWorkspacesForSSOState( jwt.data.generateJWT.availableSSOIDPs, ); diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts new file mode 100644 index 000000000000..1286621e10ba --- /dev/null +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2.ts @@ -0,0 +1,30 @@ +import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { ComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateV2'; +import { globalComponentInstanceContextMap } from '@/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap'; +import { SerializableParam, useRecoilState } from 'recoil'; + +export const useRecoilComponentFamilyStateV2 = < + StateType, + FamilyKey extends SerializableParam, +>( + componentState: ComponentFamilyStateV2, + familyKey: FamilyKey, + instanceIdFromProps?: string, +) => { + const componentInstanceContext = globalComponentInstanceContextMap.get( + componentState.key, + ); + + if (!componentInstanceContext) { + throw new Error( + `Instance context for key "${componentState.key}" is not defined`, + ); + } + + const instanceId = useAvailableComponentInstanceIdOrThrow( + componentInstanceContext, + instanceIdFromProps, + ); + + return useRecoilState(componentState.atomFamily({ instanceId, familyKey })); +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts index 384b2628414f..246c03f03f0c 100644 --- a/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts +++ b/packages/twenty-front/src/modules/views/hooks/useSaveCurrentViewGroups.ts @@ -20,6 +20,56 @@ export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { viewBarComponentId, ); + const saveViewGroup = useRecoilCallback( + ({ snapshot }) => + async (viewGroupToSave: ViewGroup) => { + const currentViewId = snapshot + .getLoadable(currentViewIdCallbackState) + .getValue(); + + if (!currentViewId) { + return; + } + + const view = await getViewFromCache(currentViewId); + + if (isUndefinedOrNull(view)) { + return; + } + + const currentViewGroups = view.viewGroups; + + const existingField = currentViewGroups.find( + (currentViewGroup) => + currentViewGroup.fieldValue === viewGroupToSave.fieldValue, + ); + + if (isUndefinedOrNull(existingField)) { + return; + } + + if ( + isDeeplyEqual( + { + position: existingField.position, + isVisible: existingField.isVisible, + }, + { + position: viewGroupToSave.position, + isVisible: viewGroupToSave.isVisible, + }, + ) + ) { + return; + } + + await updateViewGroupRecords([ + { ...viewGroupToSave, id: existingField.id }, + ]); + }, + [currentViewIdCallbackState, getViewFromCache, updateViewGroupRecords], + ); + const saveViewGroups = useRecoilCallback( ({ snapshot }) => async (viewGroupsToSave: ViewGroup[]) => { @@ -91,6 +141,7 @@ export const useSaveCurrentViewGroups = (viewBarComponentId?: string) => { ); return { + saveViewGroup, saveViewGroups, }; }; diff --git a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts index b9251945195c..0d1647b3e16e 100644 --- a/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts +++ b/packages/twenty-front/src/modules/views/utils/mapRecordGroupDefinitionsToViewGroups.ts @@ -1,17 +1,9 @@ import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { ViewGroup } from '@/views/types/ViewGroup'; +import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; export const mapRecordGroupDefinitionsToViewGroups = ( groupDefinitions: RecordGroupDefinition[], ): ViewGroup[] => { - return groupDefinitions.map( - (groupDefinition): ViewGroup => ({ - __typename: 'ViewGroup', - id: groupDefinition.id, - fieldMetadataId: groupDefinition.fieldMetadataId, - position: groupDefinition.position, - isVisible: groupDefinition.isVisible ?? true, - fieldValue: groupDefinition.value ?? '', - }), - ); + return groupDefinitions.map(recordGroupDefinitionToViewGroup); }; diff --git a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts index 7c7aba97abcf..73f4782479be 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewGroupsToRecordGroupDefinitions.ts @@ -1,3 +1,4 @@ +import { v4 } from 'uuid'; import { isDefined } from '~/utils/isDefined'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -42,16 +43,7 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ ); if (!selectedOption) { - return { - id: 'no-value', - title: 'No Value', - type: RecordGroupDefinitionType.NoValue, - value: null, - position: viewGroup.position, - isVisible: viewGroup.isVisible, - fieldMetadataId: selectFieldMetadataItem.id, - color: 'transparent', - } satisfies RecordGroupDefinition; + return null; } return { @@ -65,8 +57,32 @@ export const mapViewGroupsToRecordGroupDefinitions = ({ isVisible: viewGroup.isVisible, } as RecordGroupDefinition; }) - .filter(isDefined) - .sort((a, b) => a.position - b.position); + .filter(isDefined); - return recordGroupDefinitionsFromViewGroups; + if (selectFieldMetadataItem.isNullable === true) { + const viewGroup = viewGroups.find( + (viewGroup) => viewGroup.fieldValue === '', + ); + + const noValueColumn = { + id: viewGroup?.id ?? v4(), + title: 'No Value', + type: RecordGroupDefinitionType.NoValue, + value: null, + position: + viewGroup?.position ?? + recordGroupDefinitionsFromViewGroups + .map((option) => option.position) + .reduce((a, b) => Math.max(a, b), 0) + 1, + isVisible: viewGroup?.isVisible ?? true, + fieldMetadataId: selectFieldMetadataItem.id, + color: 'transparent', + } satisfies RecordGroupDefinition; + + return [...recordGroupDefinitionsFromViewGroups, noValueColumn]; + } + + return recordGroupDefinitionsFromViewGroups.sort( + (a, b) => a.position - b.position, + ); }; diff --git a/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts new file mode 100644 index 000000000000..74ae28f5f553 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/recordGroupDefinitionToViewGroup.ts @@ -0,0 +1,15 @@ +import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; +import { ViewGroup } from '@/views/types/ViewGroup'; + +export const recordGroupDefinitionToViewGroup = ( + recordGroup: RecordGroupDefinition, +): ViewGroup => { + return { + __typename: 'ViewGroup', + id: recordGroup.id, + fieldMetadataId: recordGroup.fieldMetadataId, + position: recordGroup.position, + isVisible: recordGroup.isVisible ?? true, + fieldValue: recordGroup.value ?? '', + }; +}; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index d83e857901ea..d2488f480428 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -74,7 +74,7 @@ export const Invite = () => { return ( <> - + {title} {isDefined(currentWorkspace) ? ( diff --git a/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx b/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx deleted file mode 100644 index 6ac9d3f6c59f..000000000000 --- a/packages/twenty-front/src/pages/impersonate/ImpersonateEffect.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; -import { useCallback, useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - -import { useIsLogged } from '@/auth/hooks/useIsLogged'; -import { currentUserState } from '@/auth/states/currentUserState'; -import { tokenPairState } from '@/auth/states/tokenPairState'; -import { AppPath } from '@/types/AppPath'; -import { useImpersonateMutation } from '~/generated/graphql'; -import { isDefined } from '~/utils/isDefined'; - -export const ImpersonateEffect = () => { - const navigate = useNavigate(); - const { userId } = useParams(); - - const [currentUser, setCurrentUser] = useRecoilState(currentUserState); - const setTokenPair = useSetRecoilState(tokenPairState); - - const [impersonate] = useImpersonateMutation(); - - const isLogged = useIsLogged(); - - const handleImpersonate = useCallback(async () => { - if (!isNonEmptyString(userId)) { - return; - } - - const impersonateResult = await impersonate({ - variables: { userId }, - }); - - if (isDefined(impersonateResult.errors)) { - throw impersonateResult.errors; - } - - if (!impersonateResult.data?.impersonate) { - throw new Error('No impersonate result'); - } - - setCurrentUser({ - ...impersonateResult.data.impersonate.user, - // Todo also set WorkspaceMember - }); - setTokenPair(impersonateResult.data?.impersonate.tokens); - - return impersonateResult.data?.impersonate; - }, [userId, impersonate, setCurrentUser, setTokenPair]); - - useEffect(() => { - if ( - isLogged && - currentUser?.canImpersonate === true && - isNonEmptyString(userId) - ) { - handleImpersonate(); - } else { - // User is not allowed to impersonate or not logged in - navigate(AppPath.Index); - } - }, [userId, currentUser, isLogged, handleImpersonate, navigate]); - - return <>; -}; diff --git a/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx b/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx deleted file mode 100644 index b1b44773ee76..000000000000 --- a/packages/twenty-front/src/pages/impersonate/__stories__/ImpersonateEffect.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; - -import { - PageDecorator, - PageDecoratorArgs, -} from '~/testing/decorators/PageDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; -import { sleep } from '~/utils/sleep'; - -import { AppPath } from '@/types/AppPath'; -import { ImpersonateEffect } from '../ImpersonateEffect'; - -const meta: Meta = { - title: 'Pages/Impersonate/Impersonate', - component: ImpersonateEffect, - decorators: [PageDecorator], - args: { - routePath: AppPath.Impersonate, - routeParams: { ':userId': '1' }, - }, - parameters: { - msw: graphqlMocks, - }, -}; - -export default meta; - -export type Story = StoryObj; - -export const Default: Story = { - play: async () => { - await sleep(100); - }, -}; diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx new file mode 100644 index 000000000000..3060866ce350 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx @@ -0,0 +1,39 @@ +import { SettingsAdminImpersonateUsers } from '@/settings/admin-panel/components/SettingsAdminImpersonateUsers'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { useTheme } from '@emotion/react'; +import { IconFlag, UndecoratedLink } from 'twenty-ui'; + +export const SettingsAdmin = () => { + const theme = useTheme(); + return ( + + + + + + } + title="Feature Flags" + /> + + + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx new file mode 100644 index 000000000000..e4cf5a5e7445 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminFeatureFlags.tsx @@ -0,0 +1,240 @@ +import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs'; +import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { TabList } from '@/ui/layout/tab/components/TabList'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; +import { Table } from '@/ui/layout/table/components/Table'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableHeader } from '@/ui/layout/table/components/TableHeader'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { + Button, + getImageAbsoluteURI, + H1Title, + H1TitleFontColor, + H2Title, + IconSearch, + isDefined, + Section, + Toggle, +} from 'twenty-ui'; + +const StyledLinkContainer = styled.div` + margin-right: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +const StyledErrorSection = styled.div` + color: ${({ theme }) => theme.font.color.danger}; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledUserInfo = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(5)}; +`; + +const StyledTable = styled(Table)` + margin-top: ${({ theme }) => theme.spacing(0.5)}; +`; + +const StyledTabListContainer = styled.div` + align-items: center; + border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; + box-sizing: border-box; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledContentContainer = styled.div` + flex: 1; + width: 100%; + padding: ${({ theme }) => theme.spacing(4)} 0; +`; + +export const SettingsAdminFeatureFlags = () => { + const [userIdentifier, setUserIdentifier] = useState(''); + + const { activeTabIdState, setActiveTabId } = useTabList( + SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID, + ); + const activeTabId = useRecoilValue(activeTabIdState); + + const { + userLookupResult, + handleUserLookup, + handleFeatureFlagUpdate, + isLoading, + error, + } = useFeatureFlagsManagement(); + + const handleSearch = async () => { + setActiveTabId(''); + + const result = await handleUserLookup(userIdentifier); + + if ( + isDefined(result?.workspaces) && + result.workspaces.length > 0 && + !error + ) { + setActiveTabId(result.workspaces[0].id); + } + }; + + const shouldShowUserData = userLookupResult && !error; + + const activeWorkspace = userLookupResult?.workspaces.find( + (workspace) => workspace.id === activeTabId, + ); + + const tabs = + userLookupResult?.workspaces.map((workspace) => ({ + id: workspace.id, + title: workspace.name, + logo: + getImageAbsoluteURI( + workspace.logo === null ? DEFAULT_WORKSPACE_LOGO : workspace.logo, + ) ?? '', + })) ?? []; + + const renderWorkspaceContent = () => { + if (!activeWorkspace) return null; + + return ( + <> + + 1 ? 'Users' : 'User' + }`} + description={'Total Users'} + /> + + + Feature Flag + Status + + + {activeWorkspace.featureFlags.map((flag) => ( + + {flag.key} + + + handleFeatureFlagUpdate( + activeWorkspace.id, + flag.key, + newValue, + ) + } + /> + + + ))} + + + ); + }; + + return ( + + +
+ + + + + + +
+ + {shouldShowUserData && ( +
+ + + + + + + + + + + + + {renderWorkspaceContent()} + +
+ )} +
+
+ ); +}; diff --git a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts index 819567796773..eb2665ec9411 100644 --- a/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts +++ b/packages/twenty-front/src/utils/image/getImageAbsoluteURI.ts @@ -1,15 +1,26 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config'; -export const getImageAbsoluteURI = (imageUrl?: string | null) => { +type ImageAbsoluteURI = T extends string + ? string + : null; + +export const getImageAbsoluteURI = ( + imageUrl: T, +): ImageAbsoluteURI => { if (!imageUrl) { - return null; + return null as ImageAbsoluteURI; } - if (imageUrl?.startsWith('https:') || imageUrl?.startsWith('http:')) { - return imageUrl; + if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) { + return imageUrl as ImageAbsoluteURI; } - const serverFilesUrl = REACT_APP_SERVER_BASE_URL; + const serverFilesUrl = new URL(REACT_APP_SERVER_BASE_URL); + + serverFilesUrl.pathname = `/files/`; + serverFilesUrl.pathname += imageUrl.startsWith('/') + ? imageUrl.slice(1) + : imageUrl; - return `${serverFilesUrl}/files/${imageUrl}`; + return serverFilesUrl.toString() as ImageAbsoluteURI; }; diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts new file mode 100644 index 000000000000..375a507c229f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts @@ -0,0 +1,19 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-panel.resolver'; +import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User, Workspace, FeatureFlagEntity], 'core'), + AuthModule, + ], + providers: [AdminPanelResolver, AdminPanelService], + exports: [AdminPanelService], +}) +export class AdminPanelModule {} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts new file mode 100644 index 000000000000..2bc2f099cadf --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts @@ -0,0 +1,57 @@ +import { UseFilters, UseGuards } from '@nestjs/common'; +import { Args, Mutation, Resolver } from '@nestjs/graphql'; + +import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; +import { ImpersonateInput } from 'src/engine/core-modules/admin-panel/dtos/impersonate.input'; +import { UpdateWorkspaceFeatureFlagInput } from 'src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input'; +import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; +import { UserLookupInput } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.input'; +import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity'; +import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +@Resolver() +@UseFilters(AuthGraphqlApiExceptionFilter) +export class AdminPanelResolver { + constructor(private adminService: AdminPanelService) {} + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => Verify) + async impersonate( + @Args() impersonateInput: ImpersonateInput, + @AuthUser() user: User, + ): Promise { + return await this.adminService.impersonate(impersonateInput.userId, user); + } + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => UserLookup) + async userLookupAdminPanel( + @Args() userLookupInput: UserLookupInput, + @AuthUser() user: User, + ): Promise { + return await this.adminService.userLookup( + userLookupInput.userIdentifier, + user, + ); + } + + @UseGuards(WorkspaceAuthGuard, UserAuthGuard) + @Mutation(() => Boolean) + async updateWorkspaceFeatureFlag( + @Args() updateFlagInput: UpdateWorkspaceFeatureFlagInput, + @AuthUser() user: User, + ): Promise { + await this.adminService.updateWorkspaceFeatureFlags( + updateFlagInput.workspaceId, + updateFlagInput.featureFlag, + user, + updateFlagInput.value, + ); + + return true; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts new file mode 100644 index 000000000000..7679e4b54c53 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -0,0 +1,179 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; +import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Injectable() +export class AdminPanelService { + constructor( + private readonly accessTokenService: AccessTokenService, + private readonly refreshTokenService: RefreshTokenService, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, + ) {} + + async impersonate(userIdentifier: string, userImpersonating: User) { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot impersonate', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const isEmail = userIdentifier.includes('@'); + + const user = await this.userRepository.findOne({ + where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, + relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], + }); + + if (!user) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + if (!user.defaultWorkspace.allowImpersonation) { + throw new AuthException( + 'Impersonation not allowed', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const accessToken = await this.accessTokenService.generateAccessToken( + user.id, + user.defaultWorkspaceId, + ); + const refreshToken = await this.refreshTokenService.generateRefreshToken( + user.id, + user.defaultWorkspaceId, + ); + + return { + user, + tokens: { + accessToken, + refreshToken, + }, + }; + } + + async userLookup( + userIdentifier: string, + userImpersonating: User, + ): Promise { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot access user info', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const isEmail = userIdentifier.includes('@'); + + const targetUser = await this.userRepository.findOne({ + where: isEmail ? { email: userIdentifier } : { id: userIdentifier }, + relations: [ + 'workspaces', + 'workspaces.workspace', + 'workspaces.workspace.workspaceUsers', + 'workspaces.workspace.workspaceUsers.user', + 'workspaces.workspace.featureFlags', + ], + }); + + if (!targetUser) { + throw new AuthException( + 'User not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + const allFeatureFlagKeys = Object.values(FeatureFlagKey); + + return { + user: { + id: targetUser.id, + email: targetUser.email, + firstName: targetUser.firstName, + lastName: targetUser.lastName, + }, + workspaces: targetUser.workspaces.map((userWorkspace) => ({ + id: userWorkspace.workspace.id, + name: userWorkspace.workspace.displayName ?? '', + totalUsers: userWorkspace.workspace.workspaceUsers.length, + logo: userWorkspace.workspace.logo, + users: userWorkspace.workspace.workspaceUsers.map((workspaceUser) => ({ + id: workspaceUser.user.id, + email: workspaceUser.user.email, + firstName: workspaceUser.user.firstName, + lastName: workspaceUser.user.lastName, + })), + featureFlags: allFeatureFlagKeys.map((key) => ({ + key, + value: + userWorkspace.workspace.featureFlags?.find( + (flag) => flag.key === key, + )?.value ?? false, + })) as FeatureFlagEntity[], + })), + }; + } + + async updateWorkspaceFeatureFlags( + workspaceId: string, + featureFlag: FeatureFlagKey, + userImpersonating: User, + value: boolean, + ) { + if (!userImpersonating.canImpersonate) { + throw new AuthException( + 'User cannot update feature flags', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + relations: ['featureFlags'], + }); + + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.INVALID_INPUT, + ); + } + + const existingFlag = workspace.featureFlags?.find( + (flag) => flag.key === featureFlag, + ); + + if (existingFlag) { + await this.featureFlagRepository.update(existingFlag.id, { value }); + } else { + await this.featureFlagRepository.save({ + key: featureFlag, + value, + workspaceId: workspace.id, + }); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/impersonate.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.input.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/auth/dto/impersonate.input.ts rename to packages/twenty-server/src/engine/core-modules/admin-panel/dtos/impersonate.input.ts diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts new file mode 100644 index 000000000000..5f134b7d9326 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/update-workspace-feature-flag.input.ts @@ -0,0 +1,21 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; + +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +@ArgsType() +export class UpdateWorkspaceFeatureFlagInput { + @Field(() => String) + @IsNotEmpty() + @IsString() + workspaceId: string; + + @Field(() => String) + @IsNotEmpty() + featureFlag: FeatureFlagKey; + + @Field(() => Boolean) + @IsBoolean() + value: boolean; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts new file mode 100644 index 000000000000..f67fe31a471d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.entity.ts @@ -0,0 +1,48 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; + +@ObjectType() +class UserInfo { + @Field(() => String) + id: string; + + @Field(() => String) + email: string; + + @Field(() => String, { nullable: true }) + firstName?: string; + + @Field(() => String, { nullable: true }) + lastName?: string; +} + +@ObjectType() +class WorkspaceInfo { + @Field(() => String) + id: string; + + @Field(() => String) + name: string; + + @Field(() => String, { nullable: true }) + logo?: string; + + @Field(() => Number) + totalUsers: number; + + @Field(() => [UserInfo]) + users: UserInfo[]; + + @Field(() => [FeatureFlagEntity]) + featureFlags: FeatureFlagEntity[]; +} + +@ObjectType() +export class UserLookup { + @Field(() => UserInfo) + user: UserInfo; + + @Field(() => [WorkspaceInfo]) + workspaces: WorkspaceInfo[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts new file mode 100644 index 000000000000..971c18635290 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/dtos/user-lookup.input.ts @@ -0,0 +1,11 @@ +import { ArgsType, Field } from '@nestjs/graphql'; + +import { IsNotEmpty, IsString } from 'class-validator'; + +@ArgsType() +export class UserLookupInput { + @Field(() => String) + @IsNotEmpty() + @IsString() + userIdentifier: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 2d6fc31b65a0..e3387c5c6100 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -22,6 +22,7 @@ import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/sw import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; +import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -96,6 +97,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; MicrosoftAPIsService, AppTokenService, AccessTokenService, + RefreshTokenService, LoginTokenService, ResetPasswordService, SwitchWorkspaceService, @@ -103,6 +105,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; ApiKeyService, OAuthService, ], - exports: [AccessTokenService, LoginTokenService], + exports: [AccessTokenService, LoginTokenService, RefreshTokenService], }) export class AuthModule {} diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index d819bc84c582..c74afe573db7 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -38,7 +38,6 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { ChallengeInput } from './dto/challenge.input'; -import { ImpersonateInput } from './dto/impersonate.input'; import { LoginToken } from './dto/login-token.entity'; import { SignUpInput } from './dto/sign-up.input'; import { ApiKeyToken, AuthTokens } from './dto/token.entity'; @@ -228,15 +227,6 @@ export class AuthResolver { return { tokens: tokens }; } - @UseGuards(WorkspaceAuthGuard, UserAuthGuard) - @Mutation(() => Verify) - async impersonate( - @Args() impersonateInput: ImpersonateInput, - @AuthUser() user: User, - ): Promise { - return await this.authService.impersonate(impersonateInput.userId, user); - } - @UseGuards(WorkspaceAuthGuard) @Mutation(() => ApiKeyToken) async generateApiKeyToken( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 8a99656954a5..7de331e5d6b1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -188,53 +188,6 @@ export class AuthService { return { isValid: !!workspace }; } - async impersonate(userIdToImpersonate: string, userImpersonating: User) { - if (!userImpersonating.canImpersonate) { - throw new AuthException( - 'User cannot impersonate', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } - - const user = await this.userRepository.findOne({ - where: { - id: userIdToImpersonate, - }, - relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'], - }); - - if (!user) { - throw new AuthException( - 'User not found', - AuthExceptionCode.USER_NOT_FOUND, - ); - } - - if (!user.defaultWorkspace.allowImpersonation) { - throw new AuthException( - 'Impersonation not allowed', - AuthExceptionCode.FORBIDDEN_EXCEPTION, - ); - } - - const accessToken = await this.accessTokenService.generateAccessToken( - user.id, - user.defaultWorkspaceId, - ); - const refreshToken = await this.refreshTokenService.generateRefreshToken( - user.id, - user.defaultWorkspaceId, - ); - - return { - user, - tokens: { - accessToken, - refreshToken, - }, - }; - } - async generateAuthorizationCode( authorizeAppInput: AuthorizeAppInput, user: User, diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index a04e6cc67524..7ba8899150ce 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -3,6 +3,7 @@ import { HttpAdapterHost } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; +import { AdminPanelModule } from 'src/engine/core-modules/admin-panel/admin-panel.module'; import { AppTokenModule } from 'src/engine/core-modules/app-token/app-token.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; @@ -70,6 +71,7 @@ import { FileModule } from './file/file.module'; WorkspaceEventEmitterModule, ActorModule, TelemetryModule, + AdminPanelModule, EnvironmentModule.forRoot({}), RedisClientModule, FileStorageModule.forRootAsync({ diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts index e6d095e9ec55..c0ab6d4058fb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts @@ -13,6 +13,7 @@ import { generateDeterministicIndexName } from 'src/engine/metadata-modules/inde import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { + WorkspaceMigrationIndexAction, WorkspaceMigrationIndexActionType, WorkspaceMigrationTableAction, WorkspaceMigrationTableActionType, @@ -103,6 +104,55 @@ export class IndexMetadataService { ); } + async recomputeIndexMetadataForObject( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + ) { + const indexesToRecompute = await this.indexMetadataRepository.find({ + where: { + objectMetadataId: updatedObjectMetadata.id, + workspaceId, + }, + relations: ['indexFieldMetadatas.fieldMetadata'], + }); + + const recomputedIndexes: { + indexMetadata: IndexMetadataEntity; + previousName: string; + newName: string; + }[] = []; + + for (const index of indexesToRecompute) { + const previousIndexName = index.name; + const tableName = computeObjectTargetTable(updatedObjectMetadata); + + const indexFieldsMetadataOrdered = index.indexFieldMetadatas.sort( + (a, b) => a.order - b.order, + ); + + const columnNames = indexFieldsMetadataOrdered.map( + (indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name, + ); + + const newIndexName = `IDX_${generateDeterministicIndexName([ + tableName, + ...columnNames, + ])}`; + + await this.indexMetadataRepository.update(index.id, { + name: newIndexName, + }); + + recomputedIndexes.push({ + indexMetadata: index, + previousName: previousIndexName, + newName: newIndexName, + }); + } + + return recomputedIndexes; + } + async deleteIndexMetadata( workspaceId: string, objectMetadata: ObjectMetadataEntity, @@ -179,4 +229,55 @@ export class IndexMetadataService { [migration], ); } + + async createIndexRecomputeMigrations( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + recomputedIndexes: { + indexMetadata: IndexMetadataEntity; + previousName: string; + newName: string; + }[], + ) { + for (const recomputedIndex of recomputedIndexes) { + const { previousName, newName, indexMetadata } = recomputedIndex; + + const tableName = computeObjectTargetTable(objectMetadata); + + const indexFieldsMetadataOrdered = indexMetadata.indexFieldMetadatas.sort( + (a, b) => a.order - b.order, + ); + + const columnNames = indexFieldsMetadataOrdered.map( + (indexFieldMetadata) => indexFieldMetadata.fieldMetadata.name, + ); + + const migration = { + name: tableName, + action: WorkspaceMigrationTableActionType.ALTER_INDEXES, + indexes: [ + { + action: WorkspaceMigrationIndexActionType.DROP, + name: previousName, + columns: [], + isUnique: indexMetadata.isUnique, + } satisfies WorkspaceMigrationIndexAction, + { + action: WorkspaceMigrationIndexActionType.CREATE, + columns: columnNames, + name: newName, + isUnique: indexMetadata.isUnique, + where: indexMetadata.indexWhereClause, + type: indexMetadata.indexType, + } satisfies WorkspaceMigrationIndexAction, + ], + } satisfies WorkspaceMigrationTableAction; + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`update-${objectMetadata.nameSingular}-index`), + workspaceId, + [migration], + ); + } + } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 6b0074d1e683..2c2cac74a604 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -13,6 +13,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; @@ -49,6 +50,7 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; WorkspaceMetadataVersionModule, RemoteTableRelationsModule, SearchModule, + IndexMetadataModule, ], services: [ ObjectMetadataService, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 9a82a9515520..cd5c8e37729b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -10,6 +10,7 @@ import { FindManyOptions, FindOneOptions, In, Not, Repository } from 'typeorm'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; import { @@ -26,7 +27,6 @@ import { validateObjectMetadataInputOrThrow, } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util'; import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; -import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { SearchService } from 'src/engine/metadata-modules/search/search.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -55,6 +55,7 @@ export class ObjectMetadataService extends TypeOrmQueryService - this.objectMetadataRelationService.createMetadata( - objectMetadataInput.workspaceId, - createdObjectMetadata, - mapUdtNameToFieldType( - objectMetadataInput.primaryKeyColumnType ?? 'uuid', - ), - objectMetadataInput.primaryKeyFieldMetadataSettings, - relationType, - ), - ), - ); - - await this.objectMetadataMigrationService.createRelationMigrations( - createdObjectMetadata, - createdRelatedObjectMetadata, - ); - } - private async handleObjectNameAndLabelUpdates( existingObjectMetadata: ObjectMetadataEntity, objectMetadataForUpdate: ObjectMetadataEntity, @@ -447,17 +430,37 @@ export class ObjectMetadataService extends TypeOrmQueryService - relations.map((relation) => relation.toObjectMetadataId), - ); - - const foreignKeyFieldMetadataForStandardRelation = - await this.fieldMetadataRepository.find({ - where: { - isCustom: false, - settings: { - isForeignKey: true, - }, - name: `${existingObjectMetadata.nameSingular}Id`, - workspaceId: workspaceId, - }, - }); - - await Promise.all( - foreignKeyFieldMetadataForStandardRelation.map( - async (foreignKeyFieldMetadata) => { - if ( - relatedObjectsIds.includes( - foreignKeyFieldMetadata.objectMetadataId, - ) - ) { - const relatedObject = - await this.objectMetadataRepository.findOneBy({ - id: foreignKeyFieldMetadata.objectMetadataId, - workspaceId: workspaceId, - }); - - if (relatedObject) { - // 1. Update to and from relation fieldMetadata - const toFieldRelationFieldMetadataId = - await this.fieldMetadataRepository - .findOneByOrFail({ - name: existingObjectMetadata.nameSingular, - objectMetadataId: relatedObject.id, - workspaceId: workspaceId, - }) - .then((field) => field.id); - - const { description: descriptionForToField } = - buildDescriptionForRelationFieldMetadataOnToField({ - relationObjectMetadataNamePlural: relatedObject.namePlural, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - }); - - await this.fieldMetadataRepository.update( - toFieldRelationFieldMetadataId, - { - name: updatedObjectMetadata.nameSingular, - label: updatedObjectMetadata.labelSingular, - description: descriptionForToField, - }, - ); - - const fromFieldRelationFieldMetadataId = - await this.relationMetadataRepository - .findOneByOrFail({ - fromObjectMetadataId: existingObjectMetadata.id, - toObjectMetadataId: relatedObject.id, - toFieldMetadataId: toFieldRelationFieldMetadataId, - workspaceId, - }) - .then((relation) => relation?.fromFieldMetadataId); - - await this.fieldMetadataRepository.update( - fromFieldRelationFieldMetadataId, - { - description: - buildDescriptionForRelationFieldMetadataOnFromField({ - relationObjectMetadataNamePlural: - relatedObject.namePlural, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - }).description, - }, - ); - - // 2. Update foreign key fieldMetadata - const { - name: updatedNameForForeignKeyFieldMetadata, - label: updatedLabelForForeignKeyFieldMetadata, - description: updatedDescriptionForForeignKeyFieldMetadata, - } = buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ - targetObjectNameSingular: updatedObjectMetadata.nameSingular, - targetObjectLabelSingular: - updatedObjectMetadata.labelSingular, - relatedObjectLabelSingular: relatedObject.labelSingular, - }); - - await this.fieldMetadataRepository.update( - foreignKeyFieldMetadata.id, - { - name: updatedNameForForeignKeyFieldMetadata, - label: updatedLabelForForeignKeyFieldMetadata, - description: updatedDescriptionForForeignKeyFieldMetadata, - }, - ); - - const relatedObjectTableName = - computeObjectTargetTable(relatedObject); - const columnName = `${existingObjectMetadata.nameSingular}Id`; - const columnType = fieldMetadataTypeToColumnType( - foreignKeyFieldMetadata.type, - ); + for (const { + relatedObjectMetadata, + foreignKeyFieldMetadata, + } of relationsAndForeignKeysMetadata) { + const relatedObjectTableName = computeObjectTargetTable( + relatedObjectMetadata, + ); + const columnName = `${existingObjectMetadata.nameSingular}Id`; + const columnType = fieldMetadataTypeToColumnType( + foreignKeyFieldMetadata.type, + ); - await this.workspaceMigrationService.createCustomMigration( - generateMigrationName( - `rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObject.nameSingular}`, - ), - workspaceId, - [ - { - name: relatedObjectTableName, - action: WorkspaceMigrationTableActionType.ALTER, - columns: [ - { - action: WorkspaceMigrationColumnActionType.ALTER, - currentColumnDefinition: { - columnName, - columnType, - isNullable: true, - defaultValue: null, - }, - alteredColumnDefinition: { - columnName: `${updatedObjectMetadata.nameSingular}Id`, - columnType, - isNullable: true, - defaultValue: null, - }, - }, - ], - }, - ], - ); - } - } - }, + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `rename-${existingObjectMetadata.nameSingular}-to-${updatedObjectMetadata.nameSingular}-in-${relatedObjectMetadata.nameSingular}`, ), + workspaceId, + [ + { + name: relatedObjectTableName, + action: WorkspaceMigrationTableActionType.ALTER, + columns: [ + { + action: WorkspaceMigrationColumnActionType.ALTER, + currentColumnDefinition: { + columnName, + columnType, + isNullable: true, + defaultValue: null, + }, + alteredColumnDefinition: { + columnName: `${updatedObjectMetadata.nameSingular}Id`, + columnType, + isNullable: true, + defaultValue: null, + }, + }, + ], + }, + ], ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts index 74dac774e0f7..a720a59e8fc0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service.ts @@ -18,17 +18,27 @@ import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { CUSTOM_OBJECT_STANDARD_FIELD_IDS, STANDARD_OBJECT_FIELD_IDS, } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-icons'; +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; import { capitalize } from 'src/utils/capitalize'; +const DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS = [ + STANDARD_OBJECT_IDS.timelineActivity, + STANDARD_OBJECT_IDS.favorite, + STANDARD_OBJECT_IDS.attachment, + STANDARD_OBJECT_IDS.noteTarget, + STANDARD_OBJECT_IDS.taskTarget, +]; + @Injectable() export class ObjectMetadataRelationService { constructor( @@ -40,46 +50,63 @@ export class ObjectMetadataRelationService { private readonly relationMetadataRepository: Repository, ) {} - public async createMetadata( + public async createRelationsAndForeignKeysMetadata( + workspaceId: string, + createdObjectMetadata: ObjectMetadataEntity, + { primaryKeyFieldMetadataSettings, primaryKeyColumnType }, + ) { + const relatedObjectMetadataCollection = await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.createRelationAndForeignKeyMetadata( + workspaceId, + createdObjectMetadata, + mapUdtNameToFieldType(primaryKeyColumnType ?? 'uuid'), + primaryKeyFieldMetadataSettings, + relationObjectMetadataStandardId, + ), + ), + ); + + return relatedObjectMetadataCollection; + } + + private async createRelationAndForeignKeyMetadata( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, objectPrimaryKeyType: FieldMetadataType, objectPrimaryKeyFieldSettings: | FieldMetadataSettings | undefined, - relatedObjectMetadataName: string, + relationObjectMetadataStandardId: string, ) { const relatedObjectMetadata = await this.objectMetadataRepository.findOneByOrFail({ - nameSingular: relatedObjectMetadataName, + standardId: relationObjectMetadataStandardId, workspaceId: workspaceId, + isCustom: false, }); - await this.createForeignKeyFieldMetadata( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - objectPrimaryKeyType, - objectPrimaryKeyFieldSettings, - ); - - const relationFieldMetadata = await this.createRelationFields( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ); + const relationFieldMetadataCollection = + await this.createRelationFieldMetadas( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + objectPrimaryKeyType, + objectPrimaryKeyFieldSettings, + ); - await this.createRelationMetadata( + await this.createRelationMetadataFromFieldMetadatas( workspaceId, createdObjectMetadata, relatedObjectMetadata, - relationFieldMetadata, + relationFieldMetadataCollection, ); return relatedObjectMetadata; } - private async createForeignKeyFieldMetadata( + private async createRelationFieldMetadas( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, @@ -88,99 +115,187 @@ export class ObjectMetadataRelationService { | FieldMetadataSettings | undefined, ) { - const customStandardFieldId = - STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; + return this.fieldMetadataRepository.save([ + this.buildFromFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + ), + this.buildToFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + ), + this.buildForeignKeyFieldMetadata( + workspaceId, + createdObjectMetadata, + relatedObjectMetadata, + objectPrimaryKeyType, + objectPrimaryKeyFieldSettings, + ), + ]); + } - if (!customStandardFieldId) { - throw new Error( - `Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`, - ); - } + public async updateRelationsAndForeignKeysMetadata( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + ): Promise< + { + relatedObjectMetadata: ObjectMetadataEntity; + foreignKeyFieldMetadata: FieldMetadataEntity; + toFieldMetadata: FieldMetadataEntity; + fromFieldMetadata: FieldMetadataEntity; + }[] + > { + return await Promise.all( + DEFAULT_RELATIONS_OBJECTS_STANDARD_IDS.map( + async (relationObjectMetadataStandardId) => + this.updateRelationAndForeignKeyMetadata( + workspaceId, + updatedObjectMetadata, + relationObjectMetadataStandardId, + ), + ), + ); + } - const { name, label, description } = - buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ - targetObjectNameSingular: createdObjectMetadata.nameSingular, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, - relatedObjectLabelSingular: relatedObjectMetadata.labelSingular, + private async updateRelationAndForeignKeyMetadata( + workspaceId: string, + updatedObjectMetadata: ObjectMetadataEntity, + relationObjectMetadataStandardId: string, + ) { + const relatedObjectMetadata = + await this.objectMetadataRepository.findOneByOrFail({ + standardId: relationObjectMetadataStandardId, + workspaceId: workspaceId, + isCustom: false, }); - await this.fieldMetadataRepository.save({ + const toFieldMetadataUpdateCriteria = { + standardId: createRelationDeterministicUuid({ + objectId: updatedObjectMetadata.id, + standardId: + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + }; + const toFieldMetadataUpdateData = this.buildToFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + true, + ); + const toFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + toFieldMetadataUpdateCriteria, + ); + const toFieldMetadata = await this.fieldMetadataRepository.save({ + ...toFieldMetadataToUpdate, + ...toFieldMetadataUpdateData, + }); + + const fromFieldMetadataUpdateCriteria = { + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[relatedObjectMetadata.namePlural], + objectMetadataId: updatedObjectMetadata.id, + workspaceId: workspaceId, + }; + const fromFieldMetadataUpdateData = this.buildFromFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + true, + ); + const fromFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + fromFieldMetadataUpdateCriteria, + ); + const fromFieldMetadata = await this.fieldMetadataRepository.save({ + ...fromFieldMetadataToUpdate, + ...fromFieldMetadataUpdateData, + }); + + const foreignKeyFieldMetadataUpdateCriteria = { standardId: createForeignKeyDeterministicUuid({ - objectId: createdObjectMetadata.id, - standardId: customStandardFieldId, + objectId: updatedObjectMetadata.id, + standardId: + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom, }), objectMetadataId: relatedObjectMetadata.id, workspaceId: workspaceId, - isCustom: false, - isActive: true, - type: objectPrimaryKeyType, - name, - label, - description, - icon: undefined, - isNullable: true, - isSystem: true, - defaultValue: undefined, - settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + }; + const foreignKeyFieldMetadataUpdateData = this.buildForeignKeyFieldMetadata( + workspaceId, + updatedObjectMetadata, + relatedObjectMetadata, + FieldMetadataType.UUID, + undefined, + true, + ); + const foreignKeyFieldMetadataToUpdate = + await this.fieldMetadataRepository.findOneBy( + foreignKeyFieldMetadataUpdateCriteria, + ); + const foreignKeyFieldMetadata = await this.fieldMetadataRepository.save({ + ...foreignKeyFieldMetadataToUpdate, + ...foreignKeyFieldMetadataUpdateData, }); - } - private async createRelationFields( - workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, - relatedObjectMetadata: ObjectMetadataEntity, - ) { - return await this.fieldMetadataRepository.save([ - this.createFromField( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ), - this.createToField( - workspaceId, - createdObjectMetadata, - relatedObjectMetadata, - ), - ]); + return { + relatedObjectMetadata, + foreignKeyFieldMetadata, + toFieldMetadata, + fromFieldMetadata, + }; } - private createFromField( + private buildFromFieldMetadata( workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, + isUpdate = false, ) { const relationObjectMetadataNamePlural = relatedObjectMetadata.namePlural; const { description } = buildDescriptionForRelationFieldMetadataOnFromField( { relationObjectMetadataNamePlural, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, }, ); return { - standardId: - CUSTOM_OBJECT_STANDARD_FIELD_IDS[relationObjectMetadataNamePlural], - objectMetadataId: createdObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - name: relatedObjectMetadata.namePlural, - label: capitalize(relationObjectMetadataNamePlural), description, - icon: - STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] || - 'IconBuildingSkyscraper', - isNullable: true, + ...(!isUpdate + ? { + standardId: + CUSTOM_OBJECT_STANDARD_FIELD_IDS[ + relationObjectMetadataNamePlural + ], + objectMetadataId: objectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: relatedObjectMetadata.namePlural, + label: capitalize(relationObjectMetadataNamePlural), + description, + icon: + STANDARD_OBJECT_ICONS[relatedObjectMetadata.nameSingular] || + 'IconBuildingSkyscraper', + isNullable: true, + } + : {}), }; } - private createToField( + private buildToFieldMetadata( workspaceId: string, - createdObjectMetadata: ObjectMetadataEntity, + objectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, + isUpdate = false, ) { const customStandardFieldId = STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; @@ -193,35 +308,96 @@ export class ObjectMetadataRelationService { const { description } = buildDescriptionForRelationFieldMetadataOnToField({ relationObjectMetadataNamePlural: relatedObjectMetadata.namePlural, - targetObjectLabelSingular: createdObjectMetadata.labelSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, }); return { - standardId: createRelationDeterministicUuid({ - objectId: createdObjectMetadata.id, - standardId: customStandardFieldId, - }), - objectMetadataId: relatedObjectMetadata.id, - workspaceId: workspaceId, - isCustom: false, - isActive: true, - isSystem: true, - type: FieldMetadataType.RELATION, - name: createdObjectMetadata.nameSingular, - label: createdObjectMetadata.labelSingular, + name: objectMetadata.nameSingular, + label: objectMetadata.labelSingular, + description, + ...(!isUpdate + ? { + standardId: createRelationDeterministicUuid({ + objectId: objectMetadata.id, + standardId: customStandardFieldId, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + isSystem: true, + type: FieldMetadataType.RELATION, + name: objectMetadata.nameSingular, + label: objectMetadata.labelSingular, + description, + icon: 'IconBuildingSkyscraper', + isNullable: true, + } + : {}), + }; + } + + private buildForeignKeyFieldMetadata( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + relatedObjectMetadata: ObjectMetadataEntity, + objectPrimaryKeyType: FieldMetadataType, + objectPrimaryKeyFieldSettings: + | FieldMetadataSettings + | undefined, + isUpdate = false, + ) { + const customStandardFieldId = + STANDARD_OBJECT_FIELD_IDS[relatedObjectMetadata.nameSingular].custom; + + if (!customStandardFieldId) { + throw new Error( + `Custom standard field ID not found for ${relatedObjectMetadata.nameSingular}`, + ); + } + + const { name, label, description } = + buildNameLabelAndDescriptionForForeignKeyFieldMetadata({ + targetObjectNameSingular: objectMetadata.nameSingular, + targetObjectLabelSingular: objectMetadata.labelSingular, + relatedObjectLabelSingular: relatedObjectMetadata.labelSingular, + }); + + return { + name, + label, description, - icon: 'IconBuildingSkyscraper', - isNullable: true, + ...(!isUpdate + ? { + standardId: createForeignKeyDeterministicUuid({ + objectId: objectMetadata.id, + standardId: customStandardFieldId, + }), + objectMetadataId: relatedObjectMetadata.id, + workspaceId: workspaceId, + isCustom: false, + isActive: true, + type: objectPrimaryKeyType, + name, + label, + description, + icon: undefined, + isNullable: true, + isSystem: true, + defaultValue: undefined, + settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true }, + } + : {}), }; } - private async createRelationMetadata( + private async createRelationMetadataFromFieldMetadatas( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, relatedObjectMetadata: ObjectMetadataEntity, - relationFieldMetadata: FieldMetadataEntity[], + relationFieldMetadataCollection: FieldMetadataEntity[], ) { - const relationFieldMetadataMap = relationFieldMetadata.reduce( + const relationFieldMetadataMap = relationFieldMetadataCollection.reduce( (acc, fieldMetadata: FieldMetadataEntity) => { if (fieldMetadata.type === FieldMetadataType.RELATION) { acc[fieldMetadata.objectMetadataId] = fieldMetadata; @@ -247,7 +423,10 @@ export class ObjectMetadataRelationService { ]); } - async updateObjectRelationships(objectMetadataId: string, isActive: boolean) { + async updateObjectRelationshipsActivationStatus( + objectMetadataId: string, + isActive: boolean, + ) { const affectedRelations = await this.relationMetadataRepository.find({ where: [ { fromObjectMetadataId: objectMetadataId }, diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 2f29a2788327..bbede9ec949e 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -130,6 +130,7 @@ export { IconFilter, IconFilterCog, IconFilterOff, + IconFlag, IconFocusCentered, IconFolder, IconFolderPlus, @@ -215,10 +216,11 @@ export { IconRotate2, IconSearch, IconSend, + IconServer, IconSettings, IconSettingsAutomation, - IconSortAZ, IconSlash, + IconSortAZ, IconSortDescending, IconSortZA, IconSparkles,