feat: allow users to start the calendar week on Monday (#13295)

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 <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Naifer
2025-07-30 11:46:05 +01:00
committed by GitHub
parent 1972e2fbf7
commit ad69ed9aba
31 changed files with 507 additions and 136 deletions
@@ -3028,6 +3028,7 @@ export type WorkspaceInviteHashValid = {
export type WorkspaceMember = {
__typename?: 'WorkspaceMember';
avatarUrl?: Maybe<Scalars['String']>;
calendarStartDay?: Maybe<Scalars['Int']>;
colorScheme: Scalars['String'];
dateFormat?: Maybe<WorkspaceMemberDateFormatEnum>;
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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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<PermissionFlagType> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | 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`
@@ -2856,6 +2856,7 @@ export type WorkspaceInviteHashValid = {
export type WorkspaceMember = {
__typename?: 'WorkspaceMember';
avatarUrl?: Maybe<Scalars['String']>;
calendarStartDay?: Maybe<Scalars['Int']>;
colorScheme: Scalars['String'];
dateFormat?: Maybe<WorkspaceMemberDateFormatEnum>;
id: Scalars['UUID'];
@@ -0,0 +1,6 @@
export enum CalendarStartDay {
SYSTEM = 7,
SUNDAY = 0,
MONDAY = 1,
SATURDAY = 6,
}
@@ -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;
};
@@ -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.
@@ -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) => (
<Select
dropdownId="settings-accounts-calendar-date-format"
label="Date format"
fullWidth
value={value}
options={[
{
label: formatInTimeZone(Date.now(), timeZone, DateFormat.MONTH_FIRST),
value: DateFormat.MONTH_FIRST,
},
{
label: formatInTimeZone(Date.now(), timeZone, DateFormat.DAY_FIRST),
value: DateFormat.DAY_FIRST,
},
]}
onChange={onChange}
/>
);
@@ -1,9 +1,9 @@
import styled from '@emotion/styled';
import { useState } from 'react';
import { SettingsAccountsCalendarDateFormatSelect } from '@/settings/accounts/components/SettingsAccountsCalendarDateFormatSelect';
import { SettingsAccountsCalendarTimeFormatSelect } from '@/settings/accounts/components/SettingsAccountsCalendarTimeFormatSelect';
import { SettingsAccountsCalendarTimeZoneSelect } from '@/settings/accounts/components/SettingsAccountsCalendarTimeZoneSelect';
import { DateTimeSettingsDateFormatSelect } from '@/settings/experience/components/DateTimeSettingsDateFormatSelect';
import { DateTimeSettingsTimeFormatSelect } from '@/settings/experience/components/DateTimeSettingsTimeFormatSelect';
import { DateTimeSettingsTimeZoneSelect } from '@/settings/experience/components/DateTimeSettingsTimeZoneSelect';
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
@@ -27,16 +27,13 @@ export const SettingsAccountsCalendarDisplaySettings = () => {
return (
<StyledContainer>
<SettingsAccountsCalendarTimeZoneSelect
value={timeZone}
onChange={setTimeZone}
/>
<SettingsAccountsCalendarDateFormatSelect
<DateTimeSettingsTimeZoneSelect value={timeZone} onChange={setTimeZone} />
<DateTimeSettingsDateFormatSelect
value={dateFormat}
onChange={setDateFormat}
timeZone={timeZone}
/>
<SettingsAccountsCalendarTimeFormatSelect
<DateTimeSettingsTimeFormatSelect
value={timeFormat}
onChange={setTimeFormat}
timeZone={timeZone}
@@ -1,42 +0,0 @@
import { formatInTimeZone } from 'date-fns-tz';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { Select } from '@/ui/input/components/Select';
type SettingsAccountsCalendarTimeFormatSelectProps = {
value: TimeFormat;
onChange: (nextValue: TimeFormat) => void;
timeZone: string;
};
export const SettingsAccountsCalendarTimeFormatSelect = ({
onChange,
timeZone,
value,
}: SettingsAccountsCalendarTimeFormatSelectProps) => (
<Select
dropdownId="settings-accounts-calendar-time-format"
label="Time format"
fullWidth
value={value}
options={[
{
label: `24h (${formatInTimeZone(
Date.now(),
timeZone,
TimeFormat.HOUR_24,
)})`,
value: TimeFormat.HOUR_24,
},
{
label: `12h (${formatInTimeZone(
Date.now(),
timeZone,
TimeFormat.HOUR_12,
)})`,
value: TimeFormat.HOUR_12,
},
]}
onChange={onChange}
/>
);
@@ -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) => (
<Select
dropdownId="settings-accounts-calendar-time-zone"
dropdownWidth={416}
label="Time zone"
fullWidth
value={findAvailableTimeZoneOption(value)?.value}
options={AVAILABLE_TIMEZONE_OPTIONS}
onChange={onChange}
withSearchInput
/>
);
@@ -0,0 +1,73 @@
import { formatInTimeZone } from 'date-fns-tz';
import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
type DateTimeSettingsDateFormatSelectProps = {
value: DateFormat;
onChange: (nextValue: DateFormat) => void;
timeZone: string;
};
export const DateTimeSettingsDateFormatSelect = ({
onChange,
timeZone,
value,
}: DateTimeSettingsDateFormatSelectProps) => {
const systemTimeZone = detectTimeZone();
const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone;
const systemDateFormat = DateFormat[detectDateFormat()];
const systemDateFormatLabel = formatInTimeZone(
Date.now(),
usedTimeZone,
systemDateFormat,
);
return (
<Select
dropdownId="datetime-settings-date-format"
dropdownWidth={218}
label={t`Date format`}
fullWidth
dropdownWidthAuto
value={value}
options={[
{
label: t`System settings - ${systemDateFormatLabel}`,
value: DateFormat.SYSTEM,
},
{
label: `${formatInTimeZone(
Date.now(),
usedTimeZone,
DateFormat.MONTH_FIRST,
)}`,
value: DateFormat.MONTH_FIRST,
},
{
label: `${formatInTimeZone(
Date.now(),
usedTimeZone,
DateFormat.DAY_FIRST,
)}`,
value: DateFormat.DAY_FIRST,
},
{
label: `${formatInTimeZone(
Date.now(),
usedTimeZone,
DateFormat.YEAR_FIRST,
)}`,
value: DateFormat.YEAR_FIRST,
},
]}
onChange={onChange}
/>
);
};
@@ -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 (
<Select
dropdownId="datetime-settings-time-format"
dropdownWidth={218}
label={t`Time format`}
dropdownWidthAuto
fullWidth
value={value}
options={[
{
label: t`System Settings - ${systemTimeFormatLabel}`,
value: TimeFormat.SYSTEM,
},
{
label: t`24h (${hour24Label})`,
value: TimeFormat.HOUR_24,
},
{
label: t`12h (${hour12Label})`,
value: TimeFormat.HOUR_12,
},
]}
onChange={onChange}
/>
);
};
@@ -0,0 +1,42 @@
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption';
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;
onChange: (nextValue: string) => void;
};
export const DateTimeSettingsTimeZoneSelect = ({
value = detectTimeZone(),
onChange,
}: DateTimeSettingsTimeZoneSelectProps) => {
const systemTimeZone = detectTimeZone();
const systemTimeZoneOption = findAvailableTimeZoneOption(systemTimeZone);
return (
<Select
dropdownId="datetime-settings-time-zone"
label={t`Time zone`}
dropdownWidthAuto
fullWidth
value={value}
options={[
{
label: isDefined(systemTimeZoneOption)
? t`System settings`.concat(` - ${systemTimeZoneOption.label}`)
: t`System settings`,
value: 'system',
},
...(AVAILABLE_TIMEZONE_OPTIONS as SelectOption<string>[]),
]}
onChange={onChange}
withSearchInput
/>
);
};
@@ -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,
@@ -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<string | number>[] = useMemo(() => {
const days = dayList?.map<SelectOption<string | number>>(
({ day, index }) => ({
label: day,
value: index,
}),
);
return days;
}, [dayList]);
return (
<Select
fullWidth
dropdownId={SELECT_DAY_DROPDOWN_ID}
options={options}
label={label}
onChange={onChange}
value={selectedDayIndex}
dropdownWidth={218}
dropdownWidthAuto
/>
);
};
@@ -4,6 +4,9 @@ import { lazy, Suspense, useContext } from 'react';
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { CalendarStartDay } from '@/localization/constants/CalendarStartDay';
import { detectCalendarStartDay } from '@/localization/utils/detectCalendarStartDay';
import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
@@ -17,6 +20,7 @@ import {
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import 'react-datepicker/dist/react-datepicker.css';
import { useRecoilValue } from 'recoil';
import { IconCalendarX } from 'twenty-ui/display';
import {
MenuItemLeftContent,
@@ -343,7 +347,7 @@ export const DateTimePicker = ({
const { closeDropdown: closeDropdownMonthSelect } = useCloseDropdown();
const { closeDropdown: closeDropdownYearSelect } = useCloseDropdown();
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const handleClear = () => {
closeDropdowns();
onClear?.();
@@ -501,6 +505,12 @@ export const DateTimePicker = ({
openToDate={hasDate ? dateToUse : new Date()}
disabledKeyboardNavigation
onChange={handleDateChange as any}
calendarStartDay={
currentWorkspaceMember?.calendarStartDay ===
CalendarStartDay.SYSTEM
? CalendarStartDay[detectCalendarStartDay()]
: (currentWorkspaceMember?.calendarStartDay ?? undefined)
}
customInput={
<DateTimeInput
date={internalDate}
@@ -0,0 +1 @@
export const SELECT_DAY_DROPDOWN_ID = 'select-day-dropdown-Id';
@@ -0,0 +1,4 @@
export type DayNameWithIndex = {
day: string;
index: number;
};
@@ -14,5 +14,6 @@ export const WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
timeZone
dateFormat
timeFormat
calendarStartDay
}
`;
@@ -23,6 +23,7 @@ export type WorkspaceMember = {
timeZone?: string | null;
dateFormat?: WorkspaceMemberDateFormatEnum | null;
timeFormat?: WorkspaceMemberTimeFormatEnum | null;
calendarStartDay?: number | null;
};
export type WorkspaceInvitation = {
@@ -13,16 +13,18 @@ import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { getWorkspaceDateFormatFromDateFormat } from '@/localization/utils/getWorkspaceDateFormatFromDateFormat';
import { getWorkspaceTimeFormatFromTimeFormat } from '@/localization/utils/getWorkspaceTimeFormatFromTimeFormat';
import { DateTimeSettingsDateFormatSelect } from '@/settings/experience/components/DateTimeSettingsDateFormatSelect';
import { DateTimeSettingsTimeFormatSelect } from '@/settings/experience/components/DateTimeSettingsTimeFormatSelect';
import { DateTimeSettingsTimeZoneSelect } from '@/settings/experience/components/DateTimeSettingsTimeZoneSelect';
import { isNumber } from '@tiptap/core';
import { isDefined } from 'twenty-shared/utils';
import {
WorkspaceMemberDateFormatEnum,
WorkspaceMemberTimeFormatEnum,
} from '~/generated/graphql';
import { DateTimeSettingsDateFormatSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect';
import { DateTimeSettingsTimeFormatSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect';
import { DateTimeSettingsTimeZoneSelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect';
import { DateTimeSettingsCalendarStartDaySelect } from '~/pages/settings/profile/appearance/components/DateTimeSettingsCalendarStartDaySelect';
import { isEmptyObject } from '~/utils/isEmptyObject';
import { logError } from '~/utils/logError';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
display: flex;
@@ -58,6 +60,20 @@ export const DateTimeSettings = () => {
if (!isDefined(currentWorkspaceMember)) return;
const handleUpdateCalendarStartDay = async (index: number) => {
if (!isNumber(index) || index > 7 || index < 0) return;
const changedFields = {
calendarStartDay: index ?? 0,
};
setCurrentWorkspaceMember({
...currentWorkspaceMember,
...changedFields,
});
updateWorkspaceMember(changedFields);
};
const handleSettingsChange = (
settingName: 'timeZone' | 'dateFormat' | 'timeFormat',
value: string,
@@ -140,6 +156,10 @@ export const DateTimeSettings = () => {
onChange={(value) => handleSettingsChange('timeFormat', value)}
timeZone={timeZone}
/>
<DateTimeSettingsCalendarStartDaySelect
value={currentWorkspaceMember?.calendarStartDay ?? 0}
onChange={handleUpdateCalendarStartDay}
/>
</StyledContainer>
);
};
@@ -0,0 +1,57 @@
import { useMemo } from 'react';
import { CalendarStartDay } from '@/localization/constants/CalendarStartDay';
import { detectCalendarStartDay } from '@/localization/utils/detectCalendarStartDay';
import { Select } from '@/ui/input/components/Select';
import { DayNameWithIndex } from '@/ui/input/components/internal/date/types/DayNameWithIndex';
import { t } from '@lingui/core/macro';
import { SelectOption } from 'twenty-ui/input';
type DateTimeSettingsCalendarStartDaySelectProps = {
value: number;
onChange: (nextValue: number) => void;
};
export const DateTimeSettingsCalendarStartDaySelect = ({
value,
onChange,
}: DateTimeSettingsCalendarStartDaySelectProps) => {
const systemCalendarStartDay = CalendarStartDay[detectCalendarStartDay()];
const options: SelectOption<number>[] = useMemo(() => {
const systemDayLabel =
systemCalendarStartDay === CalendarStartDay.SUNDAY
? t`System settings - Sunday`
: systemCalendarStartDay === CalendarStartDay.MONDAY
? t`System settings - Monday`
: t`System settings - Saturday`;
const allowedDaysWeek: DayNameWithIndex[] = [
{
day: systemDayLabel,
index: CalendarStartDay.SYSTEM,
},
{ day: t`Sunday`, index: CalendarStartDay.SUNDAY },
{ day: t`Monday`, index: CalendarStartDay.MONDAY },
{ day: t`Saturday`, index: CalendarStartDay.SATURDAY },
];
return allowedDaysWeek.map(({ day, index }) => ({
label: day,
value: index,
}));
}, [systemCalendarStartDay]);
return (
<Select
dropdownId="datetime-settings-calendar-start-day"
dropdownWidth={218}
label={t`Calendar start day`}
fullWidth
dropdownWidthAuto
value={value}
options={options}
onChange={onChange}
/>
);
};
@@ -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<string>[]),
]}
onChange={onChange}
withSearchInput
@@ -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`}
/>
</Section>
<Section>
<H2Title
title={t`Date and time`}
description={t`Configure how dates are displayed across the app`}
/>
<DateTimeSettings />
</Section>
<Section>
<H2Title
@@ -53,6 +46,14 @@ export const SettingsExperience = () => {
/>
<LocalePicker />
</Section>
<Section>
<H2Title
title={t`Date and time`}
description={t`Configure how dates are displayed across the app`}
/>
<DateTimeSettings />
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
@@ -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;
@@ -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;
}
@@ -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 = {
@@ -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,
@@ -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<string | number>[] = useMemo(() => {
const days = dayList?.map<SelectOption<string | number>>(
({ day, index }) => ({
label: day,
value: index,
}),
);
return days;
}, [dayList]);
return (
<Select
fullWidth
dropdownId={SELECT_DAY_DROPDOWN_ID}
options={options}
label={label}
onChange={onChange}
value={selectedDayIndex}
dropdownWidth={218}
dropdownWidthAuto
/>
);
};
@@ -0,0 +1 @@
export const SELECT_DAY_DROPDOWN_ID = 'select-day-dropdown-Id';
@@ -0,0 +1,4 @@
export type DayNameWithIndex = {
day: string;
index: number;
};