From ad69ed9aba77481a5eb51ba7e59bb8ea76875221 Mon Sep 17 00:00:00 2001 From: Naifer <161821705+omarNaifer12@users.noreply.github.com> Date: Wed, 30 Jul 2025 11:46:05 +0100 Subject: [PATCH] feat: allow users to start the calendar week on Monday (#13295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolve twentyhq/core-team-issues#1255 - Introduced a new field isWeekStartMonday in the workspaceMember table. - Added a function updateCalendarStartDay to update the isWeekStartMonday value when the user toggles the corresponding field in the settings. The logic works for me, but I couldn’t find a way to create the column locally in the database for the workspaceMember tables. Please let me know if this approach looks good and how to properly create dynamic columns that are not directly handled via migration. https://github.com/user-attachments/assets/4eebada5-6a96-4a88-8e96-98f1501859e3 --------- Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait --- .../src/generated-metadata/graphql.ts | 12 +-- .../twenty-front/src/generated/graphql.ts | 1 + .../constants/CalendarStartDay.ts | 6 ++ .../utils/detectCalendarStartDay.ts | 74 +++++++++++++++++++ .../utils/findAvailableTimeZoneOption.ts | 2 +- ...ttingsAccountsCalendarDateFormatSelect.tsx | 34 --------- ...ettingsAccountsCalendarDisplaySettings.tsx | 15 ++-- ...ttingsAccountsCalendarTimeFormatSelect.tsx | 42 ----------- ...SettingsAccountsCalendarTimeZoneSelect.tsx | 26 ------- .../DateTimeSettingsDateFormatSelect.tsx | 73 ++++++++++++++++++ .../DateTimeSettingsTimeFormatSelect.tsx | 70 ++++++++++++++++++ .../DateTimeSettingsTimeZoneSelect.tsx | 42 +++++++++++ .../constants/AvailableTimezoneOptions.ts | 2 +- .../AvailableTimezoneOptionsByLabel.ts | 0 .../components/internal/date/DaySelect.tsx | 42 +++++++++++ .../date/components/InternalDatePicker.tsx | 12 ++- .../date/constants/SelectDayDropdownId.ts | 1 + .../internal/date/types/DayNameWithIndex.ts | 4 + .../fragments/workspaceMemberQueryFragment.ts | 1 + .../workspace-member/types/WorkspaceMember.ts | 1 + .../components/DateTimeSettings.tsx | 28 ++++++- ...DateTimeSettingsCalendarStartDaySelect.tsx | 57 ++++++++++++++ .../DateTimeSettingsTimeZoneSelect.tsx | 5 +- .../components/SettingsExperience.tsx | 21 +++--- .../user/dtos/workspace-member.dto.ts | 8 +- .../workspace-member-transpiler.service.ts | 2 + .../constants/standard-field-ids.ts | 1 + .../workspace-member.workspace-entity.ts | 14 ++++ .../components/internal/date/DaySelect.tsx | 42 +++++++++++ .../date/constants/SelectDayDropdownId.ts | 1 + .../internal/date/types/DayNameWithIndex.ts | 4 + 31 files changed, 507 insertions(+), 136 deletions(-) create mode 100644 packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts create mode 100644 packages/twenty-front/src/modules/localization/utils/detectCalendarStartDay.ts delete mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDateFormatSelect.tsx delete mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeFormatSelect.tsx delete mode 100644 packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect.tsx create mode 100644 packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsDateFormatSelect.tsx create mode 100644 packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx create mode 100644 packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeZoneSelect.tsx rename packages/twenty-front/src/modules/settings/{accounts => experience}/constants/AvailableTimezoneOptions.ts (90%) rename packages/twenty-front/src/modules/settings/{accounts => experience}/constants/AvailableTimezoneOptionsByLabel.ts (100%) create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/DaySelect.tsx create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/constants/SelectDayDropdownId.ts create mode 100644 packages/twenty-front/src/modules/ui/input/components/internal/date/types/DayNameWithIndex.ts create mode 100644 packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect.tsx create mode 100644 src/modules/ui/input/components/internal/date/DaySelect.tsx create mode 100644 src/modules/ui/input/components/internal/date/constants/SelectDayDropdownId.ts create mode 100644 src/modules/ui/input/components/internal/date/types/DayNameWithIndex.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 4af448020c..5379c6aba8 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -3028,6 +3028,7 @@ export type WorkspaceInviteHashValid = { export type WorkspaceMember = { __typename?: 'WorkspaceMember'; avatarUrl?: Maybe; + calendarStartDay?: Maybe; colorScheme: Scalars['String']; dateFormat?: Maybe; id: Scalars['UUID']; @@ -3729,7 +3730,7 @@ export type UpdateWorkspaceMemberRoleMutationVariables = Exact<{ }>; -export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; +export type UpdateWorkspaceMemberRoleMutation = { __typename?: 'Mutation', updateWorkspaceMemberRole: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, roles?: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean }> | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } }; export type UpsertObjectPermissionsMutationVariables = Exact<{ upsertObjectPermissionsInput: UpsertObjectPermissionsInput; @@ -3748,7 +3749,7 @@ export type UpsertPermissionFlagsMutation = { __typename?: 'Mutation', upsertPer export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>; -export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null }> }; +export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }>, permissionFlags?: Array<{ __typename?: 'PermissionFlag', id: string, flag: PermissionFlagType, roleId: string }> | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null }> }; export type CreateApprovedAccessDomainMutationVariables = Exact<{ input: CreateApprovedAccessDomainInput; @@ -3879,7 +3880,7 @@ export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutationVaria export type VerifyTwoFactorAuthenticationMethodForAuthenticatedUserMutation = { __typename?: 'Mutation', verifyTwoFactorAuthenticationMethodForAuthenticatedUser: { __typename?: 'VerifyTwoFactorAuthenticationMethodOutput', success: boolean } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } }; export type WorkspaceUrlsFragmentFragment = { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }; @@ -3898,7 +3899,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canAccessFullAdminPanel: boolean, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars?: any | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, deletedWorkspaceMembers?: Array<{ __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', permissionFlags?: Array | null, objectRecordsPermissions?: Array | null, objectPermissions?: Array<{ __typename?: 'ObjectPermission', objectMetadataId: string, canReadObjectRecords?: boolean | null, canUpdateObjectRecords?: boolean | null, canSoftDeleteObjectRecords?: boolean | null, canDestroyObjectRecords?: boolean | null, restrictedFields?: any | null }> | null, twoFactorAuthenticationMethodSummary?: Array<{ __typename?: 'TwoFactorAuthenticationMethodDTO', twoFactorAuthenticationMethodId: any, status: string, strategy: string }> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, isCustomDomainEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, isTwoFactorAuthenticationEnforced: boolean, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlagDTO', key: FeatureFlagKey, value: boolean }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null, metadata: any, billingSubscriptionItems?: Array<{ __typename?: 'BillingSubscriptionItem', id: any, hasReachedCurrentPeriodCap: boolean, quantity?: number | null, billingProduct?: { __typename?: 'BillingProduct', name: string, description: string, metadata: { __typename?: 'BillingProductMetadata', planKey: BillingPlanKey, priceUsageBased: BillingUsageType, productKey: BillingProductKey } } | null }> | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, metadata: any }>, defaultRole?: { __typename?: 'Role', id: string, label: string, description?: string | null, icon?: string | null, canUpdateAllSettings: boolean, canAccessAllTools: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean } | null, defaultAgent?: { __typename?: 'Agent', id: any } | null } | null, availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> } } }; export type ActivateWorkflowVersionMutationVariables = Exact<{ workflowVersionId: Scalars['String']; @@ -3998,7 +3999,7 @@ export type GetWorkspaceInvitationsQuery = { __typename?: 'Query', findWorkspace export type DeletedWorkspaceMemberQueryFragmentFragment = { __typename?: 'DeletedWorkspaceMember', id: any, avatarUrl?: string | null, userEmail: string, name: { __typename?: 'FullName', firstName: string, lastName: string } }; -export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }; +export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, calendarStartDay?: number | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }; export type ActivateWorkspaceMutationVariables = Exact<{ input: ActivateWorkspaceInput; @@ -4142,6 +4143,7 @@ export const WorkspaceMemberQueryFragmentFragmentDoc = gql` timeZone dateFormat timeFormat + calendarStartDay } `; export const DeletedWorkspaceMemberQueryFragmentFragmentDoc = gql` diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index a49a9d0d0d..7cdce583db 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -2856,6 +2856,7 @@ export type WorkspaceInviteHashValid = { export type WorkspaceMember = { __typename?: 'WorkspaceMember'; avatarUrl?: Maybe; + calendarStartDay?: Maybe; colorScheme: Scalars['String']; dateFormat?: Maybe; id: Scalars['UUID']; diff --git a/packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts b/packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts new file mode 100644 index 0000000000..740ed13ba2 --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/CalendarStartDay.ts @@ -0,0 +1,6 @@ +export enum CalendarStartDay { + SYSTEM = 7, + SUNDAY = 0, + MONDAY = 1, + SATURDAY = 6, +} diff --git a/packages/twenty-front/src/modules/localization/utils/detectCalendarStartDay.ts b/packages/twenty-front/src/modules/localization/utils/detectCalendarStartDay.ts new file mode 100644 index 0000000000..30759ced0a --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/detectCalendarStartDay.ts @@ -0,0 +1,74 @@ +import { CalendarStartDay } from '@/localization/constants/CalendarStartDay'; + +const MONDAY_KEY: keyof typeof CalendarStartDay = 'MONDAY'; +const SATURDAY_KEY: keyof typeof CalendarStartDay = 'SATURDAY'; +const SUNDAY_KEY: keyof typeof CalendarStartDay = 'SUNDAY'; + +export const detectCalendarStartDay = (): keyof typeof CalendarStartDay => { + // Use Intl.Locale to get the first day of the week from the user's locale + // This requires a modern browser that supports Intl.Locale + try { + const locale = new Intl.Locale(navigator.language); + + // Check if the weekInfo property is available (newer browsers) + if ( + 'weekInfo' in locale && + locale.weekInfo !== null && + locale.weekInfo !== undefined && + typeof locale.weekInfo === 'object' && + 'firstDay' in locale.weekInfo + ) { + const firstDay = locale.weekInfo.firstDay; + + // Map Intl.Locale firstDay values to our enum keys + // Intl.Locale uses 1=Monday, 7=Sunday, 6=Saturday + switch (firstDay) { + case 1: + return MONDAY_KEY; + case 6: + return SATURDAY_KEY; + case 7: + default: + return SUNDAY_KEY; + } + } + } catch (error) { + // Fallback if Intl.Locale is not supported or fails + } + + // Fallback: Use a heuristic based on common locale patterns + const language = navigator.language.toLowerCase(); + + // Most European countries, Australia, New Zealand start with Monday + if ( + language.startsWith('de') || // German + language.startsWith('fr') || // French + language.startsWith('es') || // Spanish + language.startsWith('it') || // Italian + language.startsWith('pt') || // Portuguese + language.startsWith('nl') || // Dutch + language.startsWith('sv') || // Swedish + language.startsWith('no') || // Norwegian + language.startsWith('da') || // Danish + language.startsWith('fi') || // Finnish + language.startsWith('pl') || // Polish + language.startsWith('ru') || // Russian + language.startsWith('en-gb') || // British English + language.startsWith('en-au') || // Australian English + language.startsWith('en-nz') // New Zealand English + ) { + return MONDAY_KEY; + } + + // Middle Eastern countries often start with Saturday + if ( + language.startsWith('ar') || // Arabic + language.startsWith('he') || // Hebrew + language.startsWith('fa') // Persian + ) { + return SATURDAY_KEY; + } + + // Default to Sunday (US, Canada, Japan, etc.) + return SUNDAY_KEY; +}; diff --git a/packages/twenty-front/src/modules/localization/utils/findAvailableTimeZoneOption.ts b/packages/twenty-front/src/modules/localization/utils/findAvailableTimeZoneOption.ts index a1ce9a26b8..ada7868b34 100644 --- a/packages/twenty-front/src/modules/localization/utils/findAvailableTimeZoneOption.ts +++ b/packages/twenty-front/src/modules/localization/utils/findAvailableTimeZoneOption.ts @@ -1,5 +1,5 @@ import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel'; -import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/accounts/constants/AvailableTimezoneOptionsByLabel'; +import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/experience/constants/AvailableTimezoneOptionsByLabel'; /** * Finds the matching available IANA time zone select option from a given IANA time zone. diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDateFormatSelect.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDateFormatSelect.tsx deleted file mode 100644 index 7555556c97..0000000000 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarDateFormatSelect.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { formatInTimeZone } from 'date-fns-tz'; - -import { DateFormat } from '@/localization/constants/DateFormat'; -import { Select } from '@/ui/input/components/Select'; - -type SettingsAccountsCalendarDateFormatSelectProps = { - value: DateFormat; - onChange: (nextValue: DateFormat) => void; - timeZone: string; -}; - -export const SettingsAccountsCalendarDateFormatSelect = ({ - onChange, - timeZone, - value, -}: SettingsAccountsCalendarDateFormatSelectProps) => ( - -); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect.tsx deleted file mode 100644 index db3155a4ee..0000000000 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { detectTimeZone } from '@/localization/utils/detectTimeZone'; -import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; -import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/accounts/constants/AvailableTimezoneOptions'; - -import { Select } from '@/ui/input/components/Select'; - -type SettingsAccountsCalendarTimeZoneSelectProps = { - value?: string; - onChange: (nextValue: string) => void; -}; - -export const SettingsAccountsCalendarTimeZoneSelect = ({ - value = detectTimeZone(), - onChange, -}: SettingsAccountsCalendarTimeZoneSelectProps) => ( - + ); +}; diff --git a/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx new file mode 100644 index 0000000000..aa9d329bbc --- /dev/null +++ b/packages/twenty-front/src/modules/settings/experience/components/DateTimeSettingsTimeFormatSelect.tsx @@ -0,0 +1,70 @@ +import { formatInTimeZone } from 'date-fns-tz'; + +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; +import { detectTimeZone } from '@/localization/utils/detectTimeZone'; +import { Select } from '@/ui/input/components/Select'; +import { useLingui } from '@lingui/react/macro'; + +type DateTimeSettingsTimeFormatSelectProps = { + value: TimeFormat; + onChange: (nextValue: TimeFormat) => void; + timeZone: string; +}; + +export const DateTimeSettingsTimeFormatSelect = ({ + onChange, + timeZone, + value, +}: DateTimeSettingsTimeFormatSelectProps) => { + const { t } = useLingui(); + const systemTimeZone = detectTimeZone(); + + const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone; + + const systemTimeFormat = TimeFormat[detectTimeFormat()]; + + const systemTimeFormatLabel = formatInTimeZone( + Date.now(), + usedTimeZone, + systemTimeFormat, + ); + + const hour24Label = formatInTimeZone( + Date.now(), + usedTimeZone, + TimeFormat.HOUR_24, + ); + + const hour12Label = formatInTimeZone( + Date.now(), + usedTimeZone, + TimeFormat.HOUR_12, + ); + + return ( + []), + ]} + onChange={onChange} + withSearchInput + /> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptions.ts b/packages/twenty-front/src/modules/settings/experience/constants/AvailableTimezoneOptions.ts similarity index 90% rename from packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptions.ts rename to packages/twenty-front/src/modules/settings/experience/constants/AvailableTimezoneOptions.ts index 548b8261e3..b850d44919 100644 --- a/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptions.ts +++ b/packages/twenty-front/src/modules/settings/experience/constants/AvailableTimezoneOptions.ts @@ -1,6 +1,6 @@ import { getTimezoneOffset } from 'date-fns-tz'; -import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/accounts/constants/AvailableTimezoneOptionsByLabel'; +import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/experience/constants/AvailableTimezoneOptionsByLabel'; export const AVAILABLE_TIMEZONE_OPTIONS = Object.values( AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL, diff --git a/packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts b/packages/twenty-front/src/modules/settings/experience/constants/AvailableTimezoneOptionsByLabel.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/accounts/constants/AvailableTimezoneOptionsByLabel.ts rename to packages/twenty-front/src/modules/settings/experience/constants/AvailableTimezoneOptionsByLabel.ts diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/DaySelect.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/DaySelect.tsx new file mode 100644 index 0000000000..7f199b7708 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/DaySelect.tsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react'; + +import { SELECT_DAY_DROPDOWN_ID } from '@/ui/input/components/internal/date/constants/SelectDayDropdownId'; +import { DayNameWithIndex } from '@/ui/input/components/internal/date/types/DayNameWithIndex'; +import { Select } from '@/ui/input/components/Select'; +import { SelectOption } from 'twenty-ui/input'; + +export const DaySelect = ({ + label, + selectedDayIndex, + onChange, + dayList, +}: { + label: string; + selectedDayIndex: number; + onChange: (dayIndex: number | string) => void; + dayList: DayNameWithIndex[]; +}) => { + const options: SelectOption[] = useMemo(() => { + const days = dayList?.map>( + ({ day, index }) => ({ + label: day, + value: index, + }), + ); + + return days; + }, [dayList]); + + return ( + + ); +}; diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx index b8baf88715..606c00cfef 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx @@ -1,9 +1,10 @@ import { detectTimeZone } from '@/localization/utils/detectTimeZone'; import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption'; -import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/accounts/constants/AvailableTimezoneOptions'; +import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/experience/constants/AvailableTimezoneOptions'; import { Select } from '@/ui/input/components/Select'; import { t } from '@lingui/core/macro'; import { isDefined } from 'twenty-shared/utils'; +import { SelectOption } from 'twenty-ui/input'; type DateTimeSettingsTimeZoneSelectProps = { value?: string; @@ -32,7 +33,7 @@ export const DateTimeSettingsTimeZoneSelect = ({ : t`System settings`, value: 'system', }, - ...AVAILABLE_TIMEZONE_OPTIONS, + ...(AVAILABLE_TIMEZONE_OPTIONS as SelectOption[]), ]} onChange={onChange} withSearchInput diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsExperience.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsExperience.tsx index 031b251695..5ca9e8fa86 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsExperience.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsExperience.tsx @@ -4,12 +4,12 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; import { Trans, useLingui } from '@lingui/react/macro'; +import { H2Title } from 'twenty-ui/display'; +import { ColorSchemePicker } from 'twenty-ui/input'; +import { Section } from 'twenty-ui/layout'; import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings'; import { LocalePicker } from '~/pages/settings/profile/appearance/components/LocalePicker'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; -import { ColorSchemePicker } from 'twenty-ui/input'; -import { H2Title } from 'twenty-ui/display'; -import { Section } from 'twenty-ui/layout'; export const SettingsExperience = () => { const { colorScheme, setColorScheme } = useColorScheme(); @@ -38,13 +38,6 @@ export const SettingsExperience = () => { systemLabel={t`System settings`} /> -
- - -
{ />
+ +
+ + +
); diff --git a/packages/twenty-server/src/engine/core-modules/user/dtos/workspace-member.dto.ts b/packages/twenty-server/src/engine/core-modules/user/dtos/workspace-member.dto.ts index 78f3a1f8b4..ca88582ccf 100644 --- a/packages/twenty-server/src/engine/core-modules/user/dtos/workspace-member.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/user/dtos/workspace-member.dto.ts @@ -1,6 +1,7 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, Int, ObjectType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { Max, Min } from 'class-validator'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; @@ -38,6 +39,11 @@ export class WorkspaceMember { @Field({ nullable: true }) locale: string; + @Field(() => Int, { nullable: true }) + @Min(0) + @Max(7) + calendarStartDay: number; + @Field({ nullable: true }) timeZone: string; diff --git a/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts index 8331a424cb..a7b5c1c53a 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/workspace-member-transpiler.service.ts @@ -60,6 +60,7 @@ export class WorkspaceMemberTranspiler { timeFormat, timeZone, dateFormat, + calendarStartDay, } = workspaceMemberEntity; const avatarUrl = this.generateSignedAvatarUrl({ @@ -84,6 +85,7 @@ export class WorkspaceMemberTranspiler { timeFormat: timeFormat as WorkspaceMemberTimeFormatEnum, timeZone, roles, + calendarStartDay, } satisfies WorkspaceMember; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index ed3ffa2471..af40768f86 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -532,6 +532,7 @@ export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = { dateFormat: '20202020-af13-4e11-b1e7-b8cf5ea13dc0', timeFormat: '20202020-8acb-4cf8-a851-a6ed443c8d81', searchVector: '20202020-46d0-4e7f-bc26-74c0edaeb619', + calendarStartDay: '20202020-92d0-1d7f-a126-25ededa6b142', }; export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts index 821caf972e..a31ff6156a 100644 --- a/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workspace-member/standard-objects/workspace-member.workspace-entity.ts @@ -7,6 +7,7 @@ import { FieldMetadataType } from 'twenty-shared/types'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface'; import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface'; import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; +import { NumberDataType } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type'; @@ -143,6 +144,19 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsSystem() userEmail: string; + @WorkspaceField({ + standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.calendarStartDay, + type: FieldMetadataType.NUMBER, + label: msg`Start of the week`, + defaultValue: 7, + description: msg`User's preferred start day of the week`, + settings: { + dataType: NumberDataType.INT, + }, + }) + @WorkspaceIsSystem() + calendarStartDay: number; + @WorkspaceField({ standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.userId, type: FieldMetadataType.UUID, diff --git a/src/modules/ui/input/components/internal/date/DaySelect.tsx b/src/modules/ui/input/components/internal/date/DaySelect.tsx new file mode 100644 index 0000000000..44cadd527c --- /dev/null +++ b/src/modules/ui/input/components/internal/date/DaySelect.tsx @@ -0,0 +1,42 @@ +import { useMemo } from 'react'; + +import { SELECT_DAY_DROPDOWN_ID } from '@/ui/input/components/internal/date/constants/SelectDayDropdownId'; +import { DayNameWithIndex } from '@/ui/input/components/internal/date/types/DayNameWithIndex'; +import { Select } from '@/ui/input/components/Select'; +import { SelectOption } from 'twenty-ui/input'; + +export const DaySelect = ({ + label, + selectedDayIndex, + onChange, + dayList, +}: { + label: string; + selectedDayIndex: number; + onChange: (dayIndex: number | string) => void; + dayList: DayNameWithIndex[]; +}) => { + const options: SelectOption[] = useMemo(() => { + const days = dayList?.map>( + ({ day, index }) => ({ + label: day, + value: index, + }), + ); + + return days; + }, [dayList]); + + return ( +