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..0d3840221505 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,14 +324,12 @@ export enum FeatureFlagKey { IsFunctionSettingsEnabled = 'IsFunctionSettingsEnabled', IsGmailSendEmailScopeEnabled = 'IsGmailSendEmailScopeEnabled', IsJsonFilterEnabled = 'IsJsonFilterEnabled', - IsMessageThreadSubscriberEnabled = 'IsMessageThreadSubscriberEnabled', IsMicrosoftSyncEnabled = 'IsMicrosoftSyncEnabled', IsPageHeaderV2Enabled = 'IsPageHeaderV2Enabled', IsPostgreSqlIntegrationEnabled = 'IsPostgreSQLIntegrationEnabled', IsSsoEnabled = 'IsSSOEnabled', IsStripeIntegrationEnabled = 'IsStripeIntegrationEnabled', IsUniqueIndexesEnabled = 'IsUniqueIndexesEnabled', - IsViewGroupsEnabled = 'IsViewGroupsEnabled', IsWorkflowEnabled = 'IsWorkflowEnabled' } 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/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/hooks/__tests__/useAggregateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx index 6157455bf3cf..c29d42880198 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useAggregateRecordsQuery.test.tsx @@ -5,10 +5,14 @@ import { useAggregateRecordsQuery } from '@/object-record/hooks/useAggregateReco import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery'; import { renderHook } from '@testing-library/react'; +import { getColumnNameForAggregateOperation } from 'twenty-shared'; import { FieldMetadataType } from '~/generated/graphql'; jest.mock('@/object-metadata/hooks/useObjectMetadataItem'); jest.mock('@/object-record/utils/generateAggregateQuery'); +jest.mock('twenty-shared', () => ({ + getColumnNameForAggregateOperation: jest.fn(), +})); const mockObjectMetadataItem: ObjectMetadataItem = { nameSingular: 'company', @@ -65,6 +69,7 @@ describe('useAggregateRecordsQuery', () => { }); it('should handle simple count operation', () => { + (getColumnNameForAggregateOperation as jest.Mock).mockReturnValue('name'); const { result } = renderHook(() => useAggregateRecordsQuery({ objectNameSingular: 'company', @@ -86,6 +91,7 @@ describe('useAggregateRecordsQuery', () => { }); it('should handle field aggregation', () => { + (getColumnNameForAggregateOperation as jest.Mock).mockReturnValue('amount'); const { result } = renderHook(() => useAggregateRecordsQuery({ objectNameSingular: 'company', 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-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts index d16166f63b7d..38b45de7e30f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/computeAggregateValueAndLabel.test.ts @@ -140,8 +140,8 @@ describe('computeAggregateValueAndLabel', () => { expect(result).toEqual({ value: 42, - label: 'Count', - labelWithFieldName: 'Count', + label: 'Count all', + labelWithFieldName: 'Count all', }); }); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts index 4ea4c2e422aa..de7afc3cc885 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/__tests__/getAggregateOperationLabel.test.ts @@ -10,8 +10,23 @@ describe('getAggregateOperationLabel', () => { ); expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.sum)).toBe('Sum'); expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)).toBe( - 'Count', + 'Count all', ); + expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.countEmpty)).toBe( + 'Count empty', + ); + expect(getAggregateOperationLabel(AGGREGATE_OPERATIONS.countNotEmpty)).toBe( + 'Count not empty', + ); + expect( + getAggregateOperationLabel(AGGREGATE_OPERATIONS.countUniqueValues), + ).toBe('Count unique values'); + expect( + getAggregateOperationLabel(AGGREGATE_OPERATIONS.percentageEmpty), + ).toBe('Percent empty'); + expect( + getAggregateOperationLabel(AGGREGATE_OPERATIONS.percentageNotEmpty), + ).toBe('Percent not empty'); }); it('should throw error for unknown operation', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts index 03a97d0d2d6f..d68654660def 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/computeAggregateValueAndLabel.ts @@ -2,6 +2,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { AggregateRecordsData } from '@/object-record/hooks/useAggregateRecords'; import { getAggregateOperationLabel } from '@/object-record/record-board/record-board-column/utils/getAggregateOperationLabel'; import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations'; +import { COUNT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/countAggregateOperationOptions'; +import { PERCENT_AGGREGATE_OPERATION_OPTIONS } from '@/object-record/record-table/record-table-footer/constants/percentAggregateOperationOption'; import isEmpty from 'lodash.isempty'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { formatAmount } from '~/utils/format/formatAmount'; @@ -47,10 +49,12 @@ export const computeAggregateValueAndLabel = ({ let value; - if (aggregateOperation === AGGREGATE_OPERATIONS.count) { + if (COUNT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) { value = aggregateValue; } else if (!isDefined(aggregateValue)) { value = '-'; + } else if (PERCENT_AGGREGATE_OPERATION_OPTIONS.includes(aggregateOperation)) { + value = `${formatNumber(Number(aggregateValue) * 100)}%`; } else { value = Number(aggregateValue); diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts index 8a59d0e7d882..d2b969e31d2d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/utils/getAggregateOperationLabel.ts @@ -11,7 +11,17 @@ export const getAggregateOperationLabel = (operation: AGGREGATE_OPERATIONS) => { case AGGREGATE_OPERATIONS.sum: return 'Sum'; case AGGREGATE_OPERATIONS.count: - return 'Count'; + return 'Count all'; + case AGGREGATE_OPERATIONS.countEmpty: + return 'Count empty'; + case AGGREGATE_OPERATIONS.countNotEmpty: + return 'Count not empty'; + case AGGREGATE_OPERATIONS.countUniqueValues: + return 'Count unique values'; + case AGGREGATE_OPERATIONS.percentageEmpty: + return 'Percent empty'; + case AGGREGATE_OPERATIONS.percentageNotEmpty: + return 'Percent not empty'; default: throw new Error(`Unknown aggregate operation: ${operation}`); } diff --git a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts b/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts index 8ecb4603b951..76dd903f5a3f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/types/RecordBoardColumnHeaderAggregateContentId.ts @@ -1,4 +1,5 @@ export type RecordBoardColumnHeaderAggregateContentId = | 'aggregateOperations' | 'aggregateFields' + | 'countAggregateOperationsOptions' | 'moreAggregateOperationOptions'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx index bfa0a901c703..1b38f7016a00 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/FormFieldInput.tsx @@ -1,11 +1,13 @@ import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput'; import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput'; import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput'; +import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput'; import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput'; import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput'; import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput'; import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; +import { FormPhoneFieldInput } from '@/object-record/record-field/form-types/components/FormPhoneFieldInput'; import { FormRawJsonFieldInput } from '@/object-record/record-field/form-types/components/FormRawJsonFieldInput'; import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput'; @@ -19,6 +21,7 @@ import { FieldLinksValue, FieldMetadata, FieldMultiSelectValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress'; import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean'; @@ -29,12 +32,12 @@ import { isFieldFullName } from '@/object-record/record-field/types/guards/isFie import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks'; import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; +import { isFieldPhones } from '@/object-record/record-field/types/guards/isFieldPhones'; import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson'; import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid'; import { JsonValue } from 'type-fest'; -import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput'; type FormFieldInputProps = { field: FieldDefinition; @@ -109,6 +112,13 @@ export const FormFieldInput = ({ onPersist={onPersist} VariablePicker={VariablePicker} /> + ) : isFieldPhones(field) ? ( + ) : isFieldDate(field) ? ( void; + readonly?: boolean; + VariablePicker?: VariablePickerComponent; +}) => { + const countries = useCountries(); + + const options: SelectOption[] = useMemo(() => { + const countryList = countries.map( + ({ countryName, countryCode, callingCode, Flag }) => ({ + label: `${countryName} (+${callingCode})`, + value: countryCode, + color: 'transparent', + icon: (props: IconComponentProps) => + Flag({ width: props.size, height: props.size }), + }), + ); + return [ + { + label: 'No country', + value: '', + icon: IconCircleOff, + }, + ...countryList, + ]; + }, [countries]); + + const onChange = (countryCode: string | null) => { + if (readonly) { + return; + } + + if (countryCode === null) { + onPersist(''); + } else { + onPersist(countryCode); + } + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx index 6cd9d7ef658a..866ac1da2368 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountrySelectInput.tsx @@ -13,7 +13,7 @@ export const FormCountrySelectInput = ({ VariablePicker, }: { selectedCountryName: string; - onPersist: (countryCode: string) => void; + onPersist: (country: string) => void; readonly?: boolean; VariablePicker?: VariablePickerComponent; }) => { @@ -39,15 +39,15 @@ export const FormCountrySelectInput = ({ ]; }, [countries]); - const onChange = (countryCode: string | null) => { + const onChange = (country: string | null) => { if (readonly) { return; } - if (countryCode === null) { + if (country === null) { onPersist(''); } else { - onPersist(countryCode); + onPersist(country); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx new file mode 100644 index 000000000000..b172a457b263 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormFieldHint.tsx @@ -0,0 +1,10 @@ +import styled from '@emotion/styled'; + +const StyledFormFieldHint = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + margin-top: ${({ theme }) => theme.spacing(1)}; +`; + +export const FormFieldHint = StyledFormFieldHint; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx index 2ce19596571b..55be9d9c3763 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormNumberFieldInput.tsx @@ -1,3 +1,4 @@ +import { FormFieldHint } from '@/object-record/record-field/form-types/components/FormFieldHint'; import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer'; import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer'; import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer'; @@ -24,6 +25,7 @@ type FormNumberFieldInputProps = { defaultValue: number | string | undefined; onPersist: (value: number | null | string) => void; VariablePicker?: VariablePickerComponent; + hint?: string; }; export const FormNumberFieldInput = ({ @@ -32,6 +34,7 @@ export const FormNumberFieldInput = ({ defaultValue, onPersist, VariablePicker, + hint, }: FormNumberFieldInputProps) => { const inputId = useId(); @@ -125,6 +128,8 @@ export const FormNumberFieldInput = ({ /> ) : null} + + {hint ? {hint} : null} ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx new file mode 100644 index 000000000000..f51b01260124 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormPhoneFieldInput.tsx @@ -0,0 +1,64 @@ +import { FormCountryCodeSelectInput } from '@/object-record/record-field/form-types/components/FormCountryCodeSelectInput'; +import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer'; +import { FormNestedFieldInputContainer } from '@/object-record/record-field/form-types/components/FormNestedFieldInputContainer'; +import { FormNumberFieldInput } from '@/object-record/record-field/form-types/components/FormNumberFieldInput'; +import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; +import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; +import { InputLabel } from '@/ui/input/components/InputLabel'; +import { CountryCode, getCountryCallingCode } from 'libphonenumber-js'; + +type FormPhoneFieldInputProps = { + label?: string; + defaultValue?: FieldPhonesValue; + onPersist: (value: FieldPhonesValue) => void; + VariablePicker?: VariablePickerComponent; + readonly?: boolean; +}; + +export const FormPhoneFieldInput = ({ + label, + defaultValue, + onPersist, + readonly, + VariablePicker, +}: FormPhoneFieldInputProps) => { + const handleCountryChange = (newCountry: string) => { + const newCallingCode = getCountryCallingCode(newCountry as CountryCode); + + onPersist({ + primaryPhoneCountryCode: newCountry, + primaryPhoneCallingCode: newCallingCode, + primaryPhoneNumber: defaultValue?.primaryPhoneNumber || '', + }); + }; + + const handleNumberChange = (number: string | number | null) => { + onPersist({ + primaryPhoneCountryCode: defaultValue?.primaryPhoneCountryCode || '', + primaryPhoneCallingCode: defaultValue?.primaryPhoneCallingCode || '', + primaryPhoneNumber: number ? `${number}` : '', + }); + }; + + return ( + + {label && {label}} + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx index 4f985aa922d6..353fe487c02a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormSelectFieldInput.tsx @@ -225,6 +225,7 @@ export const FormSelectFieldInput = ({ color={selectedOption.color ?? 'transparent'} label={selectedOption.label} Icon={selectedOption.icon ?? undefined} + isUsedInForm /> ) : null} diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx new file mode 100644 index 000000000000..5a49cc9ce0fd --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormCountryCodeSelectInput.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/test'; + +import { FormCountryCodeSelectInput } from '../FormCountryCodeSelectInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormCountryCodeSelectInput', + component: FormCountryCodeSelectInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + selectedCountryCode: 'FR', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Country Code'); + }, +}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx new file mode 100644 index 000000000000..eec784fa0bce --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/__stories__/FormPhoneFieldInput.stories.tsx @@ -0,0 +1,34 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/test'; + +import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; +import { FormPhoneFieldInput } from '../FormPhoneFieldInput'; + +const meta: Meta = { + title: 'UI/Data/Field/Form/Input/FormPhoneFieldInput', + component: FormPhoneFieldInput, + args: {}, + argTypes: {}, +}; + +export default meta; + +type Story = StoryObj; + +const defaultPhoneValue: FieldPhonesValue = { + primaryPhoneNumber: '0612345678', + primaryPhoneCountryCode: 'FR', + primaryPhoneCallingCode: '33', +}; + +export const Default: Story = { + args: { + label: 'Phone', + defaultValue: defaultPhoneValue, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await canvas.findByText('Phone'); + }, +}; 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/constants/AggregateOperations.ts b/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts index 1ee85c25220e..74a9bc6d6a64 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/constants/AggregateOperations.ts @@ -4,4 +4,9 @@ export enum AGGREGATE_OPERATIONS { avg = 'AVG', sum = 'SUM', count = 'COUNT', + countEmpty = 'COUNT_EMPTY', + countNotEmpty = 'COUNT_NOT_EMPTY', + countUniqueValues = 'COUNT_UNIQUE_VALUES', + percentageEmpty = 'PERCENTAGE_EMPTY', + percentageNotEmpty = 'PERCENTAGE_NOT_EMPTY', } 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 && (