From 23499735b4d688cde7415365bab8d756f2af3011 Mon Sep 17 00:00:00 2001 From: Melvin Jariwala Date: Thu, 2 Jan 2025 18:44:28 +0530 Subject: [PATCH 1/8] fix: Resolve "Can't delete an account" issue (#9232) (#9238) ### Summary This pull request addresses the issue described in #9232, where attempting to delete a user account results in a `TypeError: Cannot read properties of undefined (reading 'dataSourceService')`. ### Changes Made - Fixed the `this` context issue in the `deleteUserFromWorkspace` method by ensuring it is correctly bound. - Updated the `deleteUser` method to use a bound function when calling `deleteUserFromWorkspace`. ### Linked Issue This pull request fixes #9232. ### Additional Notes - Please review the changes carefully to ensure no unintended side effects in the user or workspace deletion process. - Suggestions for further improvement are welcome. --------- Co-authored-by: Lucas Bordeau Co-authored-by: Weiko --- .../core-modules/user/services/user.service.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index c1793d03eec6..43a8c6d0024b 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -96,14 +96,14 @@ export class UserService extends TypeOrmQueryService { assert(workspaceMember, 'WorkspaceMember not found'); - if (workspaceMembers.length === 1) { - await this.workspaceService.deleteWorkspace(workspaceId); - } - await workspaceDataSource?.query( `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, ); + if (workspaceMembers.length === 1) { + await this.workspaceService.deleteWorkspace(workspaceId); + } + const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ where: { nameSingular: 'workspaceMember', @@ -136,7 +136,9 @@ export class UserService extends TypeOrmQueryService { userValidator.assertIsDefinedOrThrow(user); - await Promise.all(user.workspaces.map(this.deleteUserFromWorkspace)); + await Promise.all( + user.workspaces.map(this.deleteUserFromWorkspace.bind(this)), + ); return user; } From 0dff20775bf645dd066b81b47a8ad1b1f99ef2c1 Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 Jan 2025 16:15:25 +0100 Subject: [PATCH 2/8] Fix user deletion when workspace is deleted (#9321) ## Context 2 issues here: - We use a metadata repository find method without providing a workspaceId: In practice this is not an issue in this specific part but let's avoid that pattern - await this.workspaceService.deleteWorkspace(workspaceId); deletes almost everything, emitting an event on workspaceMember could potentially bring issues and not much values imho --- .../user/services/user.service.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 43a8c6d0024b..60ed6c29a7a2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -6,23 +6,23 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { userValidator } from 'src/engine/core-modules/user/user.validate'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { Workspace, WorkspaceActivationStatus, } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { userValidator } from 'src/engine/core-modules/user/user.validate'; -import { - AuthException, - AuthExceptionCode, -} from 'src/engine/core-modules/auth/auth.exception'; // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UserService extends TypeOrmQueryService { @@ -100,16 +100,19 @@ export class UserService extends TypeOrmQueryService { `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, ); - if (workspaceMembers.length === 1) { - await this.workspaceService.deleteWorkspace(workspaceId); - } - const objectMetadata = await this.objectMetadataRepository.findOneOrFail({ where: { nameSingular: 'workspaceMember', + workspaceId: workspaceId, }, }); + if (workspaceMembers.length === 1) { + await this.workspaceService.deleteWorkspace(workspaceId); + + return; + } + this.workspaceEventEmitter.emitDatabaseBatchEvent({ objectMetadataNameSingular: 'workspaceMember', action: DatabaseEventAction.DELETED, From 866c29e9ee328b76e43cd46a4246b85e555451ec Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 2 Jan 2025 16:22:29 +0100 Subject: [PATCH 3/8] Deprecate share email thread (#9319) ## Context Following this https://github.com/twentyhq/twenty/issues/4199 This has not been fully implemented, after 5months of dead code I'm removing the feature for the time being until we re-prioritise the feature (unlikely during these next 6 months) to keep the codebase a bit cleaner (no need to maintain dead features) Feel free to reopen / revert this PR once feature is ready ## Test locally after importing emails --- .../src/generated-metadata/graphql.ts | 63 ++++++++++++++++--- .../twenty-front/src/generated/graphql.tsx | 2 - ...ThreadMessagesOperationSignatureFactory.ts | 17 +---- .../hooks/useRightDrawerEmailThread.ts | 7 --- .../MessageThreadSubscribersTopBar.tsx | 41 ------------ .../components/RightDrawerTopBar.tsx | 2 - .../RightDrawerTopBarDropdownButton.tsx | 27 -------- .../data-seed-dev-workspace.command.ts | 14 ----- .../typeorm-seeds/core/feature-flags.ts | 5 -- .../enums/feature-flag-key.enum.ts | 1 - .../standard-objects/index.ts | 2 - ...sage-thread-subscriber.workspace-entity.ts | 59 ----------------- .../message-thread.workspace-entity.ts | 17 ----- .../workspace-member.workspace-entity.ts | 17 ----- 14 files changed, 54 insertions(+), 220 deletions(-) delete mode 100644 packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx delete mode 100644 packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx delete mode 100644 packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 52ed927fb8d1..417a5c9696d1 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -32,12 +32,6 @@ export type ActivateWorkspaceInput = { displayName?: InputMaybe; }; -export type ActivateWorkspaceOutput = { - __typename?: 'ActivateWorkspaceOutput'; - loginToken: AuthToken; - workspace: Workspace; -}; - export type Analytics = { __typename?: 'Analytics'; /** Boolean that confirms query was dispatched */ @@ -128,6 +122,12 @@ export type Billing = { isBillingEnabled: Scalars['Boolean']['output']; }; +/** The different billing plans available */ +export enum BillingPlanKey { + Enterprise = 'ENTERPRISE', + Pro = 'PRO' +} + export type BillingSubscription = { __typename?: 'BillingSubscription'; id: Scalars['UUID']['output']; @@ -371,7 +371,7 @@ export type ExecuteServerlessFunctionInput = { export type FeatureFlag = { __typename?: 'FeatureFlag'; id: Scalars['UUID']['output']; - key: Scalars['String']['output']; + key: FeatureFlagKey; value: Scalars['Boolean']['output']; workspaceId: Scalars['String']['output']; }; @@ -382,6 +382,28 @@ export type FeatureFlagFilter = { or?: InputMaybe>; }; +export enum FeatureFlagKey { + IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled', + IsAggregateQueryEnabled = 'IsAggregateQueryEnabled', + IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled', + IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled', + IsCopilotEnabled = 'IsCopilotEnabled', + IsCrmMigrationEnabled = 'IsCrmMigrationEnabled', + IsEventObjectEnabled = 'IsEventObjectEnabled', + IsFreeAccessEnabled = 'IsFreeAccessEnabled', + IsFunctionSettingsEnabled = 'IsFunctionSettingsEnabled', + IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', + IsJsonFilterEnabled = 'IsJsonFilterEnabled', + IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', + IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled', + IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', + IsSsoEnabled = 'IsSSOEnabled', + IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', + IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', + IsViewGroupsEnabled = 'IsViewGroupsEnabled', + IsWorkflowEnabled = 'IsWorkflowEnabled' +} + export type FeatureFlagSort = { direction: SortDirection; field: FeatureFlagSortFields; @@ -481,6 +503,12 @@ export enum IdentityProviderType { Saml = 'SAML' } +export type ImpersonateOutput = { + __typename?: 'ImpersonateOutput'; + loginToken: AuthToken; + workspace: WorkspaceSubdomainAndId; +}; + export type IndexConnection = { __typename?: 'IndexConnection'; /** Array of edges. */ @@ -544,7 +572,7 @@ export enum MessageChannelVisibility { export type Mutation = { __typename?: 'Mutation'; activateWorkflowVersion: Scalars['Boolean']['output']; - activateWorkspace: ActivateWorkspaceOutput; + activateWorkspace: Workspace; addUserToWorkspace: User; addUserToWorkspaceByInviteToken: User; authorizeApp: AuthorizeApp; @@ -579,13 +607,13 @@ export type Mutation = { generateApiKeyToken: ApiKeyToken; generateTransientToken: TransientToken; getAuthorizationUrl: GetAuthorizationUrlOutput; - impersonate: AuthTokens; + impersonate: ImpersonateOutput; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; resendWorkspaceInvitation: SendInvitationsOutput; runWorkflowVersion: WorkflowRun; sendInvitations: SendInvitationsOutput; - signUp: LoginToken; + signUp: SignUpOutput; skipSyncEmailOnboardingStep: OnboardingStepSuccess; switchWorkspace: PublicWorkspaceDataOutput; syncRemoteTable: RemoteTable; @@ -645,7 +673,9 @@ export type MutationChallengeArgs = { export type MutationCheckoutSessionArgs = { + plan?: BillingPlanKey; recurringInterval: SubscriptionInterval; + requirePaymentMethod?: Scalars['Boolean']['input']; successUrlPath?: InputMaybe; }; @@ -1368,6 +1398,12 @@ export type SetupSsoOutput = { type: IdentityProviderType; }; +export type SignUpOutput = { + __typename?: 'SignUpOutput'; + loginToken: AuthToken; + workspace: WorkspaceSubdomainAndId; +}; + /** Sort Directions */ export enum SortDirection { Asc = 'ASC', @@ -1751,6 +1787,7 @@ export type WorkspaceEdge = { export type WorkspaceInfo = { __typename?: 'WorkspaceInfo'; + allowImpersonation: Scalars['Boolean']['output']; featureFlags: Array; id: Scalars['String']['output']; logo?: Maybe; @@ -1804,6 +1841,12 @@ export type WorkspaceNameAndId = { id: Scalars['String']['output']; }; +export type WorkspaceSubdomainAndId = { + __typename?: 'WorkspaceSubdomainAndId'; + id: Scalars['String']['output']; + subdomain: Scalars['String']['output']; +}; + export type BillingCustomer = { __typename?: 'billingCustomer'; id: Scalars['UUID']['output']; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index d7167df82462..e334be4951a9 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -117,7 +117,6 @@ export type Billing = { /** The different billing plans available */ export enum BillingPlanKey { - Base = 'BASE', Enterprise = 'ENTERPRISE', Pro = 'PRO' } @@ -325,7 +324,6 @@ export enum FeatureFlagKey { IsFunctionSettingsEnabled = 'IsFunctionSettingsEnabled', IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled', - IsMessageThreadSubscriberEnabled = 'IsMessageThreadSubscriberEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', diff --git a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts index f818332fddf4..e07ad860f774 100644 --- a/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/operation-signatures/factories/fetchAllThreadMessagesOperationSignatureFactory.ts @@ -2,13 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperationSignatureFactory = - ({ - messageThreadId, - isSubscribersEnabled, - }: { - messageThreadId: string; - isSubscribersEnabled: boolean; - }) => ({ + ({ messageThreadId }: { messageThreadId: string }) => ({ objectNameSingular: CoreObjectNameSingular.Message, variables: { filter: { @@ -33,15 +27,6 @@ export const fetchAllThreadMessagesOperationSignatureFactory: RecordGqlOperation receivedAt: true, messageThread: { id: true, - subscribers: isSubscribersEnabled - ? { - workspaceMember: { - id: true, - name: true, - avatarUrl: true, - }, - } - : undefined, }, messageParticipants: { id: true, diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts index 68c37d70d4f9..8f58f06137ac 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts @@ -14,9 +14,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { isDefined } from 'twenty-ui'; -import { FeatureFlagKey } from '~/generated/graphql'; export const useRightDrawerEmailThread = () => { const viewableRecordId = useRecoilValue(viewableRecordIdState); @@ -38,14 +36,9 @@ export const useRightDrawerEmailThread = () => { }, }); - const isMessageThreadSubscribersEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsMessageThreadSubscriberEnabled, - ); - const FETCH_ALL_MESSAGES_OPERATION_SIGNATURE = fetchAllThreadMessagesOperationSignatureFactory({ messageThreadId: viewableRecordId, - isSubscribersEnabled: isMessageThreadSubscribersEnabled, }); const { diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx deleted file mode 100644 index a7befd2ca9c5..000000000000 --- a/packages/twenty-front/src/modules/activities/right-drawer/components/MessageThreadSubscribersTopBar.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; - -import { EmailThreadMembersChip } from '@/activities/emails/components/EmailThreadMembersChip'; -import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { isDefined } from 'twenty-ui'; -import { FeatureFlagKey } from '~/generated/graphql'; - -const StyledButtonContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; - padding: ${({ theme }) => theme.spacing(2)}; -`; - -export const MessageThreadSubscribersTopBar = () => { - const isMessageThreadSubscriberEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsMessageThreadSubscriberEnabled, - ); - - const messageThread = useRecoilValue(messageThreadState); - - const numberOfSubscribers = messageThread?.subscribers?.length ?? 0; - - const shouldShowMembersChip = numberOfSubscribers > 0; - - if ( - !isMessageThreadSubscriberEnabled || - !isDefined(messageThread) || - !shouldShowMembersChip - ) { - return null; - } - - return ( - - - - ); -}; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx index 2a5945c5bd7d..1283073a3130 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx @@ -9,7 +9,6 @@ import { isNewViewableRecordLoadingState } from '@/object-record/record-right-dr import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton'; -import { RightDrawerTopBarDropdownButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton'; import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton'; import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton'; import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar'; @@ -118,7 +117,6 @@ export const RightDrawerTopBar = () => { )} - {!isMobile && !isRightDrawerMinimized && ( )} diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx deleted file mode 100644 index 823d6bdbffa9..000000000000 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarDropdownButton.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { MessageThreadSubscribersTopBar } from '@/activities/right-drawer/components/MessageThreadSubscribersTopBar'; -import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState'; -import { rightDrawerPageState } from '@/ui/layout/right-drawer/states/rightDrawerPageState'; -import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage'; -import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; -import { useRecoilState } from 'recoil'; -import { isDefined } from 'twenty-ui'; - -const RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG: ComponentByRightDrawerPage = - { - [RightDrawerPages.ViewEmailThread]: , - }; - -export const RightDrawerTopBarDropdownButton = () => { - const [isRightDrawerMinimized] = useRecoilState(isRightDrawerMinimizedState); - - const [rightDrawerPage] = useRecoilState(rightDrawerPageState); - - if (isRightDrawerMinimized || !isDefined(rightDrawerPage)) { - return null; - } - - const dropdownButtonComponent = - RIGHT_DRAWER_TOP_BAR_DROPDOWN_BUTTON_CONFIG[rightDrawerPage]; - - return dropdownButtonComponent ?? <>; -}; diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index 893e604ee60a..934a9dd870e6 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -22,7 +22,6 @@ import { seedWorkspaceFavorites } from 'src/database/typeorm-seeds/workspace/fav import { seedMessageChannelMessageAssociation } from 'src/database/typeorm-seeds/workspace/message-channel-message-associations'; import { seedMessageChannel } from 'src/database/typeorm-seeds/workspace/message-channels'; import { seedMessageParticipant } from 'src/database/typeorm-seeds/workspace/message-participants'; -import { seedMessageThreadSubscribers } from 'src/database/typeorm-seeds/workspace/message-thread-subscribers'; import { seedMessageThread } from 'src/database/typeorm-seeds/workspace/message-threads'; import { seedMessage } from 'src/database/typeorm-seeds/workspace/messages'; import { seedOpportunity } from 'src/database/typeorm-seeds/workspace/opportunities'; @@ -182,12 +181,6 @@ export class DataSeedWorkspaceCommand extends CommandRunner { dataSourceMetadata.workspaceId, ); - const isMessageThreadSubscriberEnabled = - await this.featureFlagService.isFeatureEnabled( - FeatureFlagKey.IsMessageThreadSubscriberEnabled, - dataSourceMetadata.workspaceId, - ); - const isWorkflowEnabled = await this.featureFlagService.isFeatureEnabled( FeatureFlagKey.IsWorkflowEnabled, @@ -207,13 +200,6 @@ export class DataSeedWorkspaceCommand extends CommandRunner { await seedMessageThread(entityManager, dataSourceMetadata.schema); await seedConnectedAccount(entityManager, dataSourceMetadata.schema); - if (isMessageThreadSubscriberEnabled) { - await seedMessageThreadSubscribers( - entityManager, - dataSourceMetadata.schema, - ); - } - await seedMessage(entityManager, dataSourceMetadata.schema); await seedMessageChannel(entityManager, dataSourceMetadata.schema); await seedMessageChannelMessageAssociation( diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index f5a0bcef4e64..fd9957cf22c6 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -45,11 +45,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, - { - key: FeatureFlagKey.IsMessageThreadSubscriberEnabled, - workspaceId: workspaceId, - value: false, - }, { key: FeatureFlagKey.IsAnalyticsV2Enabled, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 5b4bb59a79b1..703a4fd6a9dc 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -7,7 +7,6 @@ export enum FeatureFlagKey { IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED', IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED', IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED', - IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', IsSSOEnabled = 'IS_SSO_ENABLED', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index 2cbb13561a7f..73536e65ede6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -12,7 +12,6 @@ import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/f import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; -import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; @@ -66,7 +65,6 @@ export const standardObjectMetadataDefinitions = [ WorkflowRunWorkspaceEntity, WorkspaceMemberWorkspaceEntity, MessageThreadWorkspaceEntity, - MessageThreadSubscriberWorkspaceEntity, MessageWorkspaceEntity, MessageChannelWorkspaceEntity, MessageParticipantWorkspaceEntity, diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts deleted file mode 100644 index 8d459f5adb86..000000000000 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; - -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; -import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; -import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; -import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; -import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; -import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; -import { MESSAGE_THREAD_SUBSCRIBER_STANDARD_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 { MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -@WorkspaceEntity({ - standardId: STANDARD_OBJECT_IDS.messageThreadSubscriber, - namePlural: 'messageThreadSubscriber', - labelSingular: 'Message Thread Subscriber', - labelPlural: 'Message Threads Subscribers', - description: 'Message Thread Subscribers', - icon: STANDARD_OBJECT_ICONS.messageThreadSubscriber, -}) -@WorkspaceIsNotAuditLogged() -@WorkspaceIsSystem() -@WorkspaceGate({ - featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, -}) -export class MessageThreadSubscriberWorkspaceEntity extends BaseWorkspaceEntity { - @WorkspaceRelation({ - standardId: MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS.messageThread, - type: RelationMetadataType.MANY_TO_ONE, - label: 'Message Thread', - description: 'Message Thread', - icon: 'IconMessage', - inverseSideFieldKey: 'subscribers', - inverseSideTarget: () => MessageThreadWorkspaceEntity, - }) - messageThread: Relation; - - @WorkspaceJoinColumn('messageThread') - messageThreadId: string; - - @WorkspaceRelation({ - standardId: MESSAGE_THREAD_SUBSCRIBER_STANDARD_FIELD_IDS.workspaceMember, - type: RelationMetadataType.MANY_TO_ONE, - label: 'Workspace Member', - description: 'Workspace Member that is part of the message thread', - icon: 'IconCircleUser', - inverseSideFieldKey: 'messageThreadSubscribers', - inverseSideTarget: () => WorkspaceMemberWorkspaceEntity, - }) - workspaceMember: Relation; - - @WorkspaceJoinColumn('workspaceMember') - workspaceMemberId: string; -} diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts index f6e09d310ca0..90889d619382 100644 --- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts +++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-thread.workspace-entity.ts @@ -1,13 +1,11 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; -import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; @@ -15,7 +13,6 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { MESSAGE_THREAD_STANDARD_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 { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity'; @WorkspaceEntity({ @@ -40,18 +37,4 @@ export class MessageThreadWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsNullable() messages: Relation; - - @WorkspaceRelation({ - standardId: MESSAGE_THREAD_STANDARD_FIELD_IDS.messageThreadSubscribers, - type: RelationMetadataType.ONE_TO_MANY, - label: 'Message Thread Subscribers', - description: 'Message Thread Subscribers', - icon: 'IconMessage', - inverseSideTarget: () => MessageThreadSubscriberWorkspaceEntity, - onDelete: RelationOnDeleteAction.CASCADE, - }) - @WorkspaceGate({ - featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, - }) - subscribers: Relation; } diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 326c0ec885ef..5e6e87069b3d 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -2,7 +2,6 @@ import { registerEnumType } from '@nestjs/graphql'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -15,7 +14,6 @@ import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; -import { WorkspaceGate } from 'src/engine/twenty-orm/decorators/workspace-gate.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; @@ -34,7 +32,6 @@ import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/com import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-participant.workspace-entity'; -import { MessageThreadSubscriberWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread-subscriber.workspace-entity'; import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; @@ -163,20 +160,6 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { }) favorites: Relation; - @WorkspaceRelation({ - standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.messageThreadSubscribers, - type: RelationMetadataType.ONE_TO_MANY, - label: 'Message thread subscribers', - description: 'Message thread subscribers for this workspace member', - icon: 'IconMessage', - inverseSideTarget: () => MessageThreadSubscriberWorkspaceEntity, - onDelete: RelationOnDeleteAction.CASCADE, - }) - @WorkspaceGate({ - featureFlag: FeatureFlagKey.IsMessageThreadSubscriberEnabled, - }) - messageThreadSubscribers: Relation; - @WorkspaceRelation({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.accountOwnerForCompanies, type: RelationMetadataType.ONE_TO_MANY, From 0f1458cbe98c786e55e963be8d4edc2d5595ee2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 2 Jan 2025 16:40:28 +0100 Subject: [PATCH 4/8] fix: view group followup (#9162) This PR fixes all followup that @Bonapara add on Discord. - [x] When no group by is set, clicking on group by should open the "field selection" menu - [x] When closed, chevron should be "chevron-right" instead of "chevron-up" - [x] Sort : Add ability to switch from alphabetical to manual when moving a option in sort alphabetical - [x] Add subtext for group by and sort - [x] Group by menu display bug - [x] Changing the sort should not close the menu - [x] Group by Activation -> shows empty state + is slow - [x] Switching from Kanban view Settings to Table Options menu displays an empty menu - [x] Unnecessary spacing under groups - [x] When no "select" are set on an object, redirect the user directly to the new Select field page - [x] Sort : Default should be manual - [x] Hidding "no value" displays all options and remove the "hide empty group" toggle - [x] Hide Empty group option disappeared - [x] Group by should not be persisted on "Locked/Main view" (**For now we just disable the group by on main view**) - [x] Hide Empty group should not be activated by default on Opportunities Kanban view - [ ] Animate the group opening/closing (**We'll be done later**) Performance improvement: https://github.com/user-attachments/assets/fd2acf66-0e56-45d0-8b2f-99c62e57d6f7 https://github.com/user-attachments/assets/80f1a2e1-9f77-4923-b85d-acb9cad96886 Also fix #9036 --------- Co-authored-by: Lucas Bordeau --- .../twenty-front/src/generated/graphql.tsx | 1 - .../triggerAttachRelationOptimisticEffect.ts | 4 +- .../components/ObjectOptionsDropdown.tsx | 1 + ...tionsDropdownHiddenRecordGroupsContent.tsx | 2 + .../ObjectOptionsDropdownMenuContent.tsx | 15 ++- ...ptionsDropdownRecordGroupFieldsContent.tsx | 23 +++-- ...tOptionsDropdownRecordGroupSortContent.tsx | 4 +- ...jectOptionsDropdownRecordGroupsContent.tsx | 70 ++++++++------ .../record-board/components/RecordBoard.tsx | 10 +- .../components/RecordBoardHeader.tsx | 10 +- .../hooks/useSetRecordBoardRecordIds.ts | 11 ++- .../RecordBoardColumnDropdownMenu.tsx | 7 +- .../RecordGroupReorderConfirmationModal.tsx | 39 ++++++++ .../hooks/useRecordGroupActions.ts | 10 +- .../hooks/useRecordGroupReorder.ts | 18 +++- .../useRecordGroupReorderConfirmationModal.ts | 78 +++++++++++++++ .../hooks/useRecordGroupVisibility.ts | 20 ++-- ...oupReorderConfirmationModalVisibleState.ts | 6 ++ .../recordGroupPendingDragEndReorderState.ts | 8 ++ ...vailableRecordGroupIdsComponentSelector.ts | 50 ++++++++++ ...leRecordGroupIdsComponentFamilySelector.ts | 84 ++++++++++++++++ .../visibleRecordGroupIdsComponentSelector.ts | 83 ---------------- ...> RecordIndexAddRecordInGroupDropdown.tsx} | 27 ++++-- .../RecordIndexPageKanbanAddButton.tsx | 9 +- .../RecordIndexPageTableAddButton.tsx | 10 +- .../hooks/useHandleRecordGroupField.ts | 2 + ...ndexRecordGroupHideComponentFamilyState.ts | 19 ++++ ...ecordIndexRecordGroupHideComponentState.ts | 9 -- .../record-table/components/RecordTable.tsx | 10 +- .../components/RecordTableEmptyState.tsx | 14 ++- ...ordTableEmptyStateByGroupNoRecordAtAll.tsx | 52 ++++++++++ .../RecordTableEmptyStateDisplay.tsx | 46 +++++---- ...rdTableEmptyStateNoGroupNoRecordAtAll.tsx} | 4 +- ...dTableEmptyStateNoRecordFoundForFilter.tsx | 2 +- .../RecordTableEmptyStateRemote.tsx | 2 +- .../RecordTableEmptyStateSoftDelete.tsx | 2 +- ...mptyStateNoGroupNoRecordAtAll.stories.tsx} | 9 +- .../RecordTableRecordGroupsBody.tsx | 13 +-- .../RecordTableRecordGroupEmptyRow.tsx | 7 -- .../RecordTableRecordGroupSection.tsx | 6 +- .../hasPendingRecordComponentSelector.ts | 46 +++++++++ .../settings/utils/getSettingsPagePath.ts | 7 ++ .../components/DropdownMenuItemsContainer.tsx | 1 + ...SetFocusedDropdownIdAndMemorizePrevious.ts | 2 +- .../modal/components/ConfirmationModal.tsx | 4 +- .../scroll/components/ScrollWrapper.tsx | 19 +++- .../utils/createComponentFamilyStateV2.ts | 20 +++- .../internal/usePersistViewGroupRecords.ts | 95 ++++--------------- .../enums/feature-flag-key.enum.ts | 1 - 49 files changed, 674 insertions(+), 318 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts rename packages/twenty-front/src/modules/object-record/record-index/components/{RecordIndexPageTableAddButtonInGroup.tsx => RecordIndexAddRecordInGroupDropdown.tsx} (74%) create mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts delete mode 100644 packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx rename packages/twenty-front/src/modules/object-record/record-table/empty-state/components/{RecordTableEmptyStateNoRecordAtAll.tsx => RecordTableEmptyStateNoGroupNoRecordAtAll.tsx} (92%) rename packages/twenty-front/src/modules/object-record/record-table/empty-state/components/__stories__/{RecordTableEmptyStateNoRecordAtAll.stories.tsx => RecordTableEmptyStateNoGroupNoRecordAtAll.stories.tsx} (76%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/selectors/hasPendingRecordComponentSelector.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index e334be4951a9..0d3840221505 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -330,7 +330,6 @@ export enum FeatureFlagKey { IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', - IsViewGroupsEnabled = 'IsViewGroupsEnabled', IsWorkflowEnabled = 'IsWorkflowEnabled' } diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts index bb650cad78f3..5dd85d297ecf 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerAttachRelationOptimisticEffect.ts @@ -32,7 +32,7 @@ export const triggerAttachRelationOptimisticEffect = ({ id: targetRecordCacheId, fields: { [fieldNameOnTargetRecord]: (targetRecordFieldValue, { toReference }) => { - const fieldValueisObjectRecordConnectionWithRefs = + const fieldValueIsObjectRecordConnectionWithRefs = isObjectRecordConnectionWithRefs( sourceObjectNameSingular, targetRecordFieldValue, @@ -47,7 +47,7 @@ export const triggerAttachRelationOptimisticEffect = ({ return targetRecordFieldValue; } - if (fieldValueisObjectRecordConnectionWithRefs) { + if (fieldValueIsObjectRecordConnectionWithRefs) { const nextEdges: RecordGqlRefEdge[] = [ ...targetRecordFieldValue.edges, { diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx index f051fa4e3b39..06d39bf6adef 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdown.tsx @@ -34,6 +34,7 @@ export const ObjectOptionsDropdown = ({ clickableComponent={ Options } + onClose={handleResetContent} dropdownComponents={ { const { + viewType, currentContentId, recordIndexId, objectMetadataItem, @@ -47,6 +48,7 @@ export const ObjectOptionsDropdownHiddenRecordGroupsContent = () => { const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); const viewGroupSettingsUrl = getSettingsPagePath( diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx index 62b06acd71f6..a739c457b7d8 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent.tsx @@ -30,8 +30,7 @@ import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { ViewType } from '@/views/types/ViewType'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; export const ObjectOptionsDropdownMenuContent = () => { const { @@ -42,10 +41,6 @@ export const ObjectOptionsDropdownMenuContent = () => { closeDropdown, } = useOptionsDropdown(); - const isViewGroupEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsViewGroupsEnabled, - ); - const { getIcon } = useIcons(); const { currentViewWithCombinedFiltersAndSorts: currentView } = useGetCurrentView(); @@ -120,9 +115,13 @@ export const ObjectOptionsDropdownMenuContent = () => { contextualText={`${visibleBoardFields.length} shown`} hasSubMenu /> - {(viewType === ViewType.Kanban || isViewGroupEnabled) && ( + {viewType === ViewType.Kanban && currentView?.key !== 'INDEX' && ( onContentChange('recordGroups')} + onClick={() => + isDefined(recordGroupFieldMetadata) + ? onContentChange('recordGroups') + : onContentChange('recordGroupFields') + } LeftIcon={IconLayoutList} text="Group by" contextualText={recordGroupFieldMetadata?.label} 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 d5fa0be6844f..3c0a0f9bf38c 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 @@ -26,6 +26,7 @@ import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMe import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useLocation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { @@ -36,6 +37,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { recordIndexId, objectMetadataItem, onContentChange, + resetContent, closeDropdown, } = useOptionsDropdown(); @@ -47,7 +49,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { hiddenRecordGroupIdsComponentSelector, ); - const recordGroupFieldMetadataItem = useRecoilComponentValueV2( + const recordGroupFieldMetadata = useRecoilComponentValueV2( recordGroupFieldMetadataComponentState, ); @@ -64,11 +66,14 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { viewBarComponentId: recordIndexId, }); - const newFieldSettingsUrl = getSettingsPagePath( - SettingsPath.ObjectNewFieldSelect, + const newSelectFieldSettingsUrl = getSettingsPagePath( + SettingsPath.ObjectNewFieldConfigure, { objectSlug: objectNamePlural, }, + { + fieldType: FieldMetadataType.Select, + }, ); const location = useLocation(); @@ -101,7 +106,11 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { <> onContentChange('recordGroups')} + onClick={() => + isDefined(recordGroupFieldMetadata) + ? onContentChange('recordGroups') + : resetContent() + } > Group by @@ -114,13 +123,13 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { {filteredRecordGroupFieldMetadataItems.map((fieldMetadataItem) => ( handleRecordGroupFieldChange(fieldMetadataItem)} LeftIcon={getIcon(fieldMetadataItem.icon)} text={fieldMetadataItem.label} @@ -130,7 +139,7 @@ export const ObjectOptionsDropdownRecordGroupFieldsContent = () => { { setNavigationMemorizedUrl(location.pathname + location.search); closeDropdown(); 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 c4b6d05f148f..4882334694dc 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 @@ -17,8 +17,7 @@ import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/ import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const ObjectOptionsDropdownRecordGroupSortContent = () => { - const { currentContentId, onContentChange, closeDropdown } = - useOptionsDropdown(); + const { currentContentId, onContentChange } = useOptionsDropdown(); const hiddenRecordGroupIds = useRecoilComponentValueV2( hiddenRecordGroupIdsComponentSelector, @@ -30,7 +29,6 @@ export const ObjectOptionsDropdownRecordGroupSortContent = () => { const handleRecordGroupSortChange = (sort: RecordGroupSort) => { setRecordGroupSort(sort); - closeDropdown(); }; useEffect(() => { 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 32c6eedeb033..e1fab350f276 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 @@ -11,47 +11,54 @@ import { } from 'twenty-ui'; import { useOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useOptionsDropdown'; +import { RecordGroupReorderConfirmationModal } from '@/object-record/record-group/components/RecordGroupReorderConfirmationModal'; import { RecordGroupsVisibilityDropdownSection } from '@/object-record/record-group/components/RecordGroupsVisibilityDropdownSection'; -import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; +import { useRecordGroupReorderConfirmationModal } from '@/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal'; 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +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 { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { FeatureFlagKey } from '~/generated/graphql'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; export const ObjectOptionsDropdownRecordGroupsContent = () => { - const isViewGroupEnabled = useIsFeatureEnabled( - FeatureFlagKey.IsViewGroupsEnabled, - ); + const { + viewType, + currentContentId, + recordIndexId, + onContentChange, + resetContent, + } = useOptionsDropdown(); - const { currentContentId, recordIndexId, onContentChange, resetContent } = - useOptionsDropdown(); + const { currentViewWithCombinedFiltersAndSorts: currentView } = + useGetCurrentView(); const recordGroupFieldMetadata = useRecoilComponentValueV2( recordGroupFieldMetadataComponentState, ); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + viewType, ); const hiddenRecordGroupIds = useRecoilComponentValueV2( hiddenRecordGroupIdsComponentSelector, ); - const isDragableSortRecordGroup = useRecoilComponentValueV2( - recordIndexRecordGroupIsDraggableSortComponentSelector, + const hideEmptyRecordGroup = useRecoilComponentFamilyValueV2( + recordIndexRecordGroupHideComponentFamilyState, + viewType, ); - const hideEmptyRecordGroup = useRecoilComponentValueV2( - recordIndexRecordGroupHideComponentState, + const recordGroupSort = useRecoilComponentValueV2( + recordIndexRecordGroupSortComponentState, ); const { @@ -59,12 +66,16 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { handleHideEmptyRecordGroupChange, } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); - const { handleOrderChange: handleRecordGroupOrderChange } = - useRecordGroupReorder({ - viewBarId: recordIndexId, - }); + const { + handleRecordGroupOrderChangeWithModal, + handleRecordGroupReorderConfirmClick, + } = useRecordGroupReorderConfirmationModal({ + recordIndexId, + viewType, + }); useEffect(() => { if ( @@ -81,22 +92,20 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { Group by - {isViewGroupEnabled && ( + {currentView?.key !== 'INDEX' && ( <> onContentChange('recordGroupFields')} LeftIcon={IconLayoutList} - text={ - !recordGroupFieldMetadata - ? 'Group by' - : `Group by "${recordGroupFieldMetadata.label}"` - } + text="Group by" + contextualText={recordGroupFieldMetadata?.label} hasSubMenu /> onContentChange('recordGroupSort')} LeftIcon={IconSortDescending} text="Sort" + contextualText={recordGroupSort} hasSubMenu /> @@ -115,9 +124,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { @@ -134,6 +143,9 @@ export const ObjectOptionsDropdownRecordGroupsContent = () => { )} + ); }; 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 88c253ba35d7..3049897e5a71 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 @@ -15,7 +15,7 @@ import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoar 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; @@ -26,8 +26,9 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis 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 { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { ViewType } from '@/views/types/ViewType'; import { useScrollRestoration } from '~/hooks/useScrollRestoration'; const StyledContainer = styled.div` @@ -64,8 +65,9 @@ export const RecordBoard = () => { useContext(RecordBoardContext); const boardRef = useRef(null); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); const recordIndexRecordIdsByGroupFamilyState = 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 e62c658115fa..3ca0b6e680ec 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,6 +1,7 @@ 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; +import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2'; +import { ViewType } from '@/views/types/ViewType'; import styled from '@emotion/styled'; const StyledHeaderContainer = styled.div` @@ -23,8 +24,9 @@ const StyledHeaderContainer = styled.div` `; export const RecordBoardHeader = () => { - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); return ( 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 index 60916cfea64f..63b127408c0a 100644 --- 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 @@ -2,18 +2,19 @@ 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; 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 { ViewType } from '@/views/types/ViewType'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { - const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentFamilySelector, ); const recordGroupFieldMetadataState = useRecoilComponentCallbackStateV2( @@ -32,7 +33,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { (records: ObjectRecord[]) => { const recordGroupIds = getSnapshotValue( snapshot, - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector(ViewType.Kanban), ); for (const recordGroupId of recordGroupIds) { @@ -72,7 +73,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { } }, [ - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector, recordIndexRecordIdsByGroupFamilyState, recordGroupFieldMetadataState, ], 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 5e096dc02be6..bcf0c98e3066 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 @@ -4,9 +4,10 @@ import { useCallback, useRef } from 'react'; import { useRecordGroupActions } from '@/object-record/record-group/hooks/useRecordGroupActions'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { ViewType } from '@/views/types/ViewType'; import { MenuItem } from 'twenty-ui'; -import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer'; const StyledMenuContainer = styled.div` position: absolute; @@ -27,7 +28,9 @@ export const RecordBoardColumnDropdownMenu = ({ }: RecordBoardColumnDropdownMenuProps) => { const boardColumnMenuRef = useRef(null); - const recordGroupActions = useRecordGroupActions(); + const recordGroupActions = useRecordGroupActions({ + viewType: ViewType.Kanban, + }); const closeMenu = useCallback(() => { onClose(); diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx new file mode 100644 index 000000000000..0a906ec1aba1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx @@ -0,0 +1,39 @@ +import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { createPortal } from 'react-dom'; +import { useRecoilState } from 'recoil'; + +type RecordGroupReorderConfirmationModalProps = { + onConfirmClick: () => void; +}; + +export const RecordGroupReorderConfirmationModal = ({ + onConfirmClick, +}: RecordGroupReorderConfirmationModalProps) => { + const [ + isRecordGroupReorderConfirmationModalVisible, + setIsRecordGroupReorderConfirmationModalVisible, + ] = useRecoilState(isRecordGroupReorderConfirmationModalVisibleState); + + const recordGroupSort = useRecoilComponentValueV2( + recordIndexRecordGroupSortComponentState, + ); + + if (!isRecordGroupReorderConfirmationModalVisible) { + return null; + } + + return createPortal( + , + document.body, + ); +}; 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 2c88b9609eb0..6121f944b46a 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 @@ -8,12 +8,19 @@ import { RecordGroupAction } from '@/object-record/record-group/types/RecordGrou import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { ViewType } from '@/views/types/ViewType'; import { useCallback, useContext, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { IconEyeOff, IconSettings, isDefined } from 'twenty-ui'; -export const useRecordGroupActions = () => { +type UseRecordGroupActionsParams = { + viewType: ViewType; +}; + +export const useRecordGroupActions = ({ + viewType, +}: UseRecordGroupActionsParams) => { const navigate = useNavigate(); const location = useLocation(); @@ -34,6 +41,7 @@ export const useRecordGroupActions = () => { const { handleVisibilityChange: handleRecordGroupVisibilityChange } = useRecordGroupVisibility({ viewBarId: recordIndexId, + viewType, }); const setNavigationMemorizedUrl = useSetRecoilState( 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 9b1c038e1a38..93e1f05c73d7 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 @@ -2,11 +2,12 @@ import { OnDragEndResponder } from '@hello-pangea/dnd'; 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 { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector'; 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 { ViewType } from '@/views/types/ViewType'; import { mapRecordGroupDefinitionsToViewGroups } from '@/views/utils/mapRecordGroupDefinitionsToViewGroups'; import { useRecoilCallback } from 'recoil'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; @@ -15,15 +16,17 @@ import { isDefined } from '~/utils/isDefined'; type UseRecordGroupHandlersParams = { viewBarId: string; + viewType: ViewType; }; export const useRecordGroupReorder = ({ viewBarId, + viewType, }: UseRecordGroupHandlersParams) => { const setRecordGroup = useSetRecordGroup(viewBarId); - const visibleRecordGroupIdsSelector = useRecoilComponentCallbackStateV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIdsFamilySelector = useRecoilComponentCallbackStateV2( + visibleRecordGroupIdsComponentFamilySelector, ); const { saveViewGroups } = useSaveCurrentViewGroups(viewBarId); @@ -37,7 +40,7 @@ export const useRecordGroupReorder = ({ const visibleRecordGroupIds = getSnapshotValue( snapshot, - visibleRecordGroupIdsSelector, + visibleRecordGroupIdsFamilySelector(viewType), ); const reorderedVisibleRecordGroupIds = moveArrayItem( @@ -80,7 +83,12 @@ export const useRecordGroupReorder = ({ mapRecordGroupDefinitionsToViewGroups(updatedRecordGroups), ); }, - [saveViewGroups, setRecordGroup, visibleRecordGroupIdsSelector], + [ + saveViewGroups, + setRecordGroup, + viewType, + visibleRecordGroupIdsFamilySelector, + ], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts new file mode 100644 index 000000000000..2831cc1f50a6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/hooks/useRecordGroupReorderConfirmationModal.ts @@ -0,0 +1,78 @@ +import { useRecordGroupReorder } from '@/object-record/record-group/hooks/useRecordGroupReorder'; +import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState'; +import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector'; +import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { ViewType } from '@/views/types/ViewType'; +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; + +type UseRecordGroupReorderConfirmationModalParams = { + recordIndexId: string; + viewType: ViewType; +}; + +export const useRecordGroupReorderConfirmationModal = ({ + recordIndexId, + viewType, +}: UseRecordGroupReorderConfirmationModalParams) => { + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + const { goBackToPreviousDropdownFocusId } = + useGoBackToPreviousDropdownFocusId(); + + const setIsRecordGroupReorderConfirmationModalVisible = useSetRecoilState( + isRecordGroupReorderConfirmationModalVisibleState, + ); + + const [pendingDragEndReorder, setPendingDragEndReorder] = + useState | null>(null); + + const { handleOrderChange: handleRecordGroupOrderChange } = + useRecordGroupReorder({ + viewBarId: recordIndexId, + viewType, + }); + + const isDragableSortRecordGroup = useRecoilComponentValueV2( + recordIndexRecordGroupIsDraggableSortComponentSelector, + ); + + const setRecordGroupSort = useSetRecoilComponentStateV2( + recordIndexRecordGroupSortComponentState, + ); + + const handleRecordGroupOrderChangeWithModal: OnDragEndResponder = ( + result, + provided, + ) => { + if (!isDragableSortRecordGroup) { + setIsRecordGroupReorderConfirmationModalVisible(true); + setActiveDropdownFocusIdAndMemorizePrevious(null); + setPendingDragEndReorder([result, provided]); + } else { + handleRecordGroupOrderChange(result, provided); + } + }; + + const handleConfirmClick = () => { + if (!pendingDragEndReorder) { + throw new Error('pendingDragEndReorder is not set'); + } + + setRecordGroupSort(RecordGroupSort.Manual); + setPendingDragEndReorder(null); + handleRecordGroupOrderChange(...pendingDragEndReorder); + goBackToPreviousDropdownFocusId(); + }; + + return { + handleRecordGroupOrderChangeWithModal, + handleRecordGroupReorderConfirmClick: handleConfirmClick, + }; +}; 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 405d637e2d15..b006459a36ca 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,20 +1,25 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; -import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useSaveCurrentViewGroups } from '@/views/hooks/useSaveCurrentViewGroups'; +import { ViewType } from '@/views/types/ViewType'; import { recordGroupDefinitionToViewGroup } from '@/views/utils/recordGroupDefinitionToViewGroup'; import { useRecoilCallback } from 'recoil'; type UseRecordGroupVisibilityParams = { viewBarId: string; + viewType: ViewType; }; export const useRecordGroupVisibility = ({ viewBarId, + viewType, }: UseRecordGroupVisibilityParams) => { - const objectOptionsDropdownRecordGroupHideState = - useRecoilComponentCallbackStateV2(recordIndexRecordGroupHideComponentState); + const objectOptionsDropdownRecordGroupHideFamilyState = + useRecoilComponentCallbackStateV2( + recordIndexRecordGroupHideComponentFamilyState, + ); const { saveViewGroup } = useSaveCurrentViewGroups(viewBarId); @@ -27,22 +32,19 @@ export const useRecordGroupVisibility = ({ ); saveViewGroup(recordGroupDefinitionToViewGroup(updatedRecordGroup)); - - // If visibility is manually toggled, we should reset the hideEmptyRecordGroup state - set(objectOptionsDropdownRecordGroupHideState, false); }, - [saveViewGroup, objectOptionsDropdownRecordGroupHideState], + [saveViewGroup], ); const handleHideEmptyRecordGroupChange = useRecoilCallback( ({ set }) => async () => { set( - objectOptionsDropdownRecordGroupHideState, + objectOptionsDropdownRecordGroupHideFamilyState(viewType), (currentHideState) => !currentHideState, ); }, - [objectOptionsDropdownRecordGroupHideState], + [viewType, objectOptionsDropdownRecordGroupHideFamilyState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts new file mode 100644 index 000000000000..3f0d0c0475a2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isRecordGroupReorderConfirmationModalVisibleState = atom({ + key: 'isRecordGroupReorderConfirmationModalVisibleState', + default: false, +}); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts new file mode 100644 index 000000000000..e3ca2769f3da --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/recordGroupPendingDragEndReorderState.ts @@ -0,0 +1,8 @@ +import { OnDragEndResponder } from '@hello-pangea/dnd'; +import { atom } from 'recoil'; + +export const recordGroupPendingDragEndReorderState = + atom | null>({ + key: 'recordGroupPendingDragEndReorderState', + default: null, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts new file mode 100644 index 000000000000..e500f12345bb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector.ts @@ -0,0 +1,50 @@ +import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState'; +import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState'; +import { + RecordGroupDefinition, + RecordGroupDefinitionType, +} from '@/object-record/record-group/types/RecordGroupDefinition'; +import { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; +import { createComponentSelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentSelectorV2'; + +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { isDefined } from '~/utils/isDefined'; + +export const availableRecordGroupIdsComponentSelector = + createComponentSelectorV2({ + key: 'availableRecordGroupIdsComponentSelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId }) => + ({ get }) => { + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + + const result: RecordGroupDefinition[] = []; + + for (const recordGroupId of recordGroupIds) { + const recordGroupDefinition = get( + recordGroupDefinitionFamilyState(recordGroupId), + ); + + if (!isDefined(recordGroupDefinition)) { + continue; + } + + if ( + recordGroupDefinition.type === RecordGroupDefinitionType.NoValue + ) { + continue; + } + + recordGroupSortedInsert(result, recordGroupDefinition, (a, b) => + a.title.localeCompare(b.title), + ); + } + + return result.map(({ id }) => id); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts new file mode 100644 index 000000000000..139e3806bacd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector.ts @@ -0,0 +1,84 @@ +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 { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; +import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; +import { createComponentFamilySelectorV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilySelectorV2'; + +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { ViewType } from '@/views/types/ViewType'; +import { isDefined } from '~/utils/isDefined'; + +export const visibleRecordGroupIdsComponentFamilySelector = + createComponentFamilySelectorV2({ + key: 'visibleRecordGroupIdsComponentFamilySelector', + componentInstanceContext: ViewComponentInstanceContext, + get: + ({ instanceId, familyKey }) => + ({ get }) => { + const recordGroupSort = get( + recordIndexRecordGroupSortComponentState.atomFamily({ + instanceId, + }), + ); + const recordGroupIds = get( + recordGroupIdsComponentState.atomFamily({ + instanceId, + }), + ); + const hideEmptyRecordGroup = get( + recordIndexRecordGroupHideComponentFamilyState.atomFamily({ + instanceId, + familyKey, + }), + ); + + 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), + ); + const recordIds = get( + recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({ + instanceId, + familyKey: recordGroupId, + }), + ); + + if (!isDefined(recordGroupDefinition)) { + continue; + } + + if (hideEmptyRecordGroup && recordIds.length === 0) { + continue; + } + + if (!recordGroupDefinition.isVisible) { + continue; + } + + recordGroupSortedInsert(result, recordGroupDefinition, comparator); + } + + return result.map(({ id }) => id); + }, + }); 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 deleted file mode 100644 index abd766868eee..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector.ts +++ /dev/null @@ -1,83 +0,0 @@ -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 { recordGroupSortedInsert } from '@/object-record/record-group/utils/recordGroupSortedInsert'; -import { recordIndexRecordGroupHideComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentState'; -import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState'; -import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState'; - -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< - RecordGroupDefinition['id'][] ->({ - key: 'visibleRecordGroupIdsComponentSelector', - componentInstanceContext: ViewComponentInstanceContext, - get: - ({ instanceId }) => - ({ get }) => { - const recordGroupSort = get( - recordIndexRecordGroupSortComponentState.atomFamily({ - instanceId, - }), - ); - const recordGroupIds = get( - recordGroupIdsComponentState.atomFamily({ - instanceId, - }), - ); - const hideEmptyRecordGroup = get( - recordIndexRecordGroupHideComponentState.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), - ); - const recordIds = get( - recordIndexRecordIdsByGroupComponentFamilyState.atomFamily({ - instanceId, - familyKey: recordGroupId, - }), - ); - - if (!isDefined(recordGroupDefinition)) { - continue; - } - - if (hideEmptyRecordGroup && recordIds.length === 0) { - continue; - } - - if (!recordGroupDefinition.isVisible) { - continue; - } - - recordGroupSortedInsert(result, recordGroupDefinition, comparator); - } - - return result.map(({ id }) => id); - }, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx similarity index 74% rename from packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx rename to packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx index c40959b1fa44..cd310e58e532 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown.tsx @@ -1,5 +1,5 @@ import { recordGroupFieldMetadataComponentState } from '@/object-record/record-group/states/recordGroupFieldMetadataComponentState'; -import { visibleRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentSelector'; +import { availableRecordGroupIdsComponentSelector } from '@/object-record/record-group/states/selectors/availableRecordGroupIdsComponentSelector'; import { RecordGroupDefinition } from '@/object-record/record-group/types/RecordGroupDefinition'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; @@ -8,18 +8,27 @@ import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious'; import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilCallback } from 'recoil'; -export const RecordIndexPageTableAddButtonInGroup = () => { - const dropdownId = `record-index-page-table-add-button-dropdown`; +type RecordIndexAddRecordInGroupDropdownProps = { + dropdownId: string; + clickableComponent: React.ReactNode; +}; +export const RecordIndexAddRecordInGroupDropdown = ({ + dropdownId, + clickableComponent, +}: RecordIndexAddRecordInGroupDropdownProps) => { const { objectMetadataItem } = useRecordIndexContextOrThrow(); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const { setActiveDropdownFocusIdAndMemorizePrevious } = + useSetActiveDropdownFocusIdAndMemorizePrevious(); + + const recordGroupIds = useRecoilComponentValueV2( + availableRecordGroupIdsComponentSelector, ); const recordGroupFieldMetadata = useRecoilComponentValueV2( @@ -44,11 +53,13 @@ export const RecordIndexPageTableAddButtonInGroup = () => { (recordGroup: RecordGroupDefinition) => { set(isRecordGroupTableSectionToggledState(recordGroup.id), true); createNewTableRecordInGroup(recordGroup.id); + setActiveDropdownFocusIdAndMemorizePrevious(null); closeDropdown(); }, [ closeDropdown, createNewTableRecordInGroup, + setActiveDropdownFocusIdAndMemorizePrevious, isRecordGroupTableSectionToggledState, ], ); @@ -61,11 +72,11 @@ export const RecordIndexPageTableAddButtonInGroup = () => { } + clickableComponent={clickableComponent} dropdownId={dropdownId} dropdownComponents={ - {visibleRecordGroupIds.map((recordGroupId) => ( + {recordGroupIds.map((recordGroupId) => ( { const { recordIndexId, objectMetadataItem } = useRecordIndexContextOrThrow(); - const visibleRecordGroupIds = useRecoilComponentValueV2( - visibleRecordGroupIdsComponentSelector, + const visibleRecordGroupIds = useRecoilComponentFamilyValueV2( + visibleRecordGroupIdsComponentFamilySelector, + ViewType.Kanban, ); const recordIndexKanbanFieldMetadataId = useRecoilValue( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx index fe169ffe7150..aef0ef97ae54 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageTableAddButton.tsx @@ -1,6 +1,7 @@ import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; -import { RecordIndexPageTableAddButtonInGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonInGroup'; +import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown'; import { RecordIndexPageTableAddButtonNoGroup } from '@/object-record/record-index/components/RecordIndexPageTableAddButtonNoGroup'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; export const RecordIndexPageTableAddButton = () => { @@ -12,5 +13,10 @@ export const RecordIndexPageTableAddButton = () => { return ; } - return ; + return ( + } + /> + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts index 84cd0ff54248..51d8d0179796 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleRecordGroupField.ts @@ -61,6 +61,8 @@ export const useHandleRecordGroupField = ({ (option) => !existingGroupKeys.has(`${fieldMetadataItem.id}:${option.value}`), ) + // Alphabetically sort the options by default + .sort((a, b) => a.value.localeCompare(b.value)) .map( (option, index) => ({ diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts new file mode 100644 index 000000000000..da9bed69caab --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState.ts @@ -0,0 +1,19 @@ +import { createComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentFamilyStateV2'; +import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; +import { ViewType } from '@/views/types/ViewType'; + +export const recordIndexRecordGroupHideComponentFamilyState = + createComponentFamilyStateV2({ + key: 'recordIndexRecordGroupHideComponentFamilyState', + defaultValue: ({ familyKey }) => { + switch (familyKey) { + case ViewType.Kanban: + return false; + case ViewType.Table: + return true; + default: + return false; + } + }, + componentInstanceContext: ViewComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts deleted file mode 100644 index 3500e331e57e..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexRecordGroupHideComponentState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; -import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; - -export const recordIndexRecordGroupHideComponentState = - createComponentStateV2({ - key: 'recordIndexRecordGroupHideComponentState', - defaultValue: false, - 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 12dc2d83addd..571510fcb2b6 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,5 +1,5 @@ import styled from '@emotion/styled'; -import { isNonEmptyString, isNull } from '@sniptt/guards'; +import { isNonEmptyString } from '@sniptt/guards'; import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector'; @@ -16,7 +16,7 @@ import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter'; 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 { hasPendingRecordComponentSelector } from '@/object-record/record-table/states/selectors/hasPendingRecordComponentSelector'; 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'; @@ -54,8 +54,8 @@ export const RecordTable = () => { recordTableId, ); - const pendingRecordId = useRecoilComponentValueV2( - recordTablePendingRecordIdComponentState, + const hasPendingRecord = useRecoilComponentValueV2( + hasPendingRecordComponentSelector, recordTableId, ); @@ -67,7 +67,7 @@ export const RecordTable = () => { const recordTableIsEmpty = !isRecordTableInitialLoading && allRecordIds.length === 0 && - isNull(pendingRecordId); + !hasPendingRecord; const { resetTableRowSelection, setRowSelected } = useRecordTable({ recordTableId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx index 680ab7b40684..5d9619a53409 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyState.tsx @@ -1,6 +1,8 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { hasRecordGroupsComponentSelector } from '@/object-record/record-group/states/selectors/hasRecordGroupsComponentSelector'; import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; -import { RecordTableEmptyStateNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordAtAll'; +import { RecordTableEmptyStateByGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll'; +import { RecordTableEmptyStateNoGroupNoRecordAtAll } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoGroupNoRecordAtAll'; import { RecordTableEmptyStateNoRecordFoundForFilter } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateNoRecordFoundForFilter'; import { RecordTableEmptyStateRemote } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateRemote'; import { RecordTableEmptyStateSoftDelete } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete'; @@ -11,6 +13,10 @@ export const RecordTableEmptyState = () => { const { recordTableId, objectNameSingular, objectMetadataItem } = useRecordTableContextOrThrow(); + const hasRecordGroups = useRecoilComponentValueV2( + hasRecordGroupsComponentSelector, + ); + const { totalCount } = useFindManyRecords({ objectNameSingular, limit: 1 }); const noRecordAtAll = totalCount === 0; @@ -26,7 +32,11 @@ export const RecordTableEmptyState = () => { } else if (isSoftDeleteActive === true) { return ; } else if (noRecordAtAll) { - return ; + if (hasRecordGroups) { + return ; + } + + return ; } else { return ; } diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx new file mode 100644 index 000000000000..8259f8172537 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateByGroupNoRecordAtAll.tsx @@ -0,0 +1,52 @@ +import { Button, IconPlus } from 'twenty-ui'; + +import { useObjectLabel } from '@/object-metadata/hooks/useObjectLabel'; +import { RecordIndexAddRecordInGroupDropdown } from '@/object-record/record-index/components/RecordIndexAddRecordInGroupDropdown'; +import { recordIndexRecordGroupHideComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordGroupHideComponentFamilyState'; +import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext'; +import { RecordTableEmptyStateDisplay } from '@/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay'; +import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; +import { ViewType } from '@/views/types/ViewType'; + +export const RecordTableEmptyStateByGroupNoRecordAtAll = () => { + const { objectMetadataItem } = useRecordTableContextOrThrow(); + + const setHideEmptyRecordGroup = useSetRecoilComponentFamilyStateV2( + recordIndexRecordGroupHideComponentFamilyState, + ViewType.Table, + ); + + const objectLabel = useObjectLabel(objectMetadataItem); + + const buttonTitle = `Add a ${objectLabel}`; + + const title = `Add your first ${objectLabel}`; + + const subTitle = `Use our API or add your first ${objectLabel} manually`; + + const handleButtonClick = () => { + // When we have no records in the group, we want to show the empty state + setHideEmptyRecordGroup(false); + }; + + return ( + + } + /> + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 8b12987a0488..58abce2f2941 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -11,41 +11,49 @@ import { IconComponent, } from 'twenty-ui'; +type RecordTableEmptyStateDisplayButtonComponentProps = { + buttonComponent?: React.ReactNode; +}; + +type RecordTableEmptyStateDisplayButtonProps = { + ButtonIcon: IconComponent; + buttonTitle: string; + onClick: () => void; +}; + type RecordTableEmptyStateDisplayProps = { animatedPlaceholderType: AnimatedPlaceholderType; title: string; subTitle: string; - Icon: IconComponent; - buttonTitle: string; - onClick: () => void; -}; +} & ( + | RecordTableEmptyStateDisplayButtonComponentProps + | RecordTableEmptyStateDisplayButtonProps +); -export const RecordTableEmptyStateDisplay = ({ - Icon, - animatedPlaceholderType, - buttonTitle, - onClick, - subTitle, - title, -}: RecordTableEmptyStateDisplayProps) => { +export const RecordTableEmptyStateDisplay = ( + props: RecordTableEmptyStateDisplayProps, +) => { const { objectMetadataItem } = useRecordTableContextOrThrow(); const isReadOnly = isObjectMetadataReadOnly(objectMetadataItem); return ( - + - {title} + + {props.title} + - {subTitle} + {props.subTitle} - {!isReadOnly && ( + {'buttonComponent' in props && props.buttonComponent} + {'buttonTitle' in props && !isReadOnly && (