Date formatting per workspace member settings (#6408)
Implement date formatting per workspace member settings We'll need another round to maybe initialize all workspaces on the default settings. For now the default behavior is to take system settings if nothing is found in DB. --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -34,6 +34,7 @@ import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { UserProvider } from '@/users/components/UserProvider';
|
||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect';
|
||||
import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
|
||||
import { PageChangeEffect } from '~/effect-components/PageChangeEffect';
|
||||
@ -74,18 +75,17 @@ import { SettingsIntegrationEditDatabaseConnection } from '~/pages/settings/inte
|
||||
import { SettingsIntegrationNewDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection';
|
||||
import { SettingsIntegrations } from '~/pages/settings/integrations/SettingsIntegrations';
|
||||
import { SettingsIntegrationShowDatabaseConnection } from '~/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection';
|
||||
import { SettingsAppearance } from '~/pages/settings/profile/appearance/components/SettingsAppearance';
|
||||
import { Releases } from '~/pages/settings/Releases';
|
||||
import { SettingsAppearance } from '~/pages/settings/SettingsAppearance';
|
||||
import { SettingsServerlessFunctionDetailWrapper } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetailWrapper';
|
||||
import { SettingsServerlessFunctions } from '~/pages/settings/serverless-functions/SettingsServerlessFunctions';
|
||||
import { SettingsServerlessFunctionsNew } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew';
|
||||
import { SettingsBilling } from '~/pages/settings/SettingsBilling';
|
||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||
import { Tasks } from '~/pages/tasks/Tasks';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
import { SettingsServerlessFunctions } from '~/pages/settings/serverless-functions/SettingsServerlessFunctions';
|
||||
import { SettingsServerlessFunctionsNew } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { SettingsServerlessFunctionDetailWrapper } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetailWrapper';
|
||||
|
||||
const ProvidersThatNeedRouterContext = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@ -1015,11 +1015,29 @@ export type WorkspaceMember = {
|
||||
__typename?: 'WorkspaceMember';
|
||||
avatarUrl?: Maybe<Scalars['String']>;
|
||||
colorScheme: Scalars['String'];
|
||||
dateFormat?: Maybe<WorkspaceMemberDateFormatEnum>;
|
||||
id: Scalars['UUID'];
|
||||
locale: Scalars['String'];
|
||||
locale?: Maybe<Scalars['String']>;
|
||||
name: FullName;
|
||||
timeFormat?: Maybe<WorkspaceMemberTimeFormatEnum>;
|
||||
timeZone?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** Date format as Month first, Day first, Year first or system as default */
|
||||
export enum WorkspaceMemberDateFormatEnum {
|
||||
DayFirst = 'DAY_FIRST',
|
||||
MonthFirst = 'MONTH_FIRST',
|
||||
System = 'SYSTEM',
|
||||
YearFirst = 'YEAR_FIRST'
|
||||
}
|
||||
|
||||
/** Time time as Military, Standard or system as default */
|
||||
export enum WorkspaceMemberTimeFormatEnum {
|
||||
Hour_12 = 'HOUR_12',
|
||||
Hour_24 = 'HOUR_24',
|
||||
System = 'SYSTEM'
|
||||
}
|
||||
|
||||
export type Field = {
|
||||
__typename?: 'field';
|
||||
createdAt: Scalars['DateTime'];
|
||||
@ -1242,7 +1260,7 @@ export type ImpersonateMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
|
||||
export type RenewTokenMutationVariables = Exact<{
|
||||
appToken: Scalars['String'];
|
||||
@ -1274,7 +1292,7 @@ export type VerifyMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||
|
||||
export type CheckUserExistsQueryVariables = Exact<{
|
||||
email: Scalars['String'];
|
||||
@ -1335,7 +1353,7 @@ export type GetAisqlQueryQueryVariables = Exact<{
|
||||
|
||||
export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } };
|
||||
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||
|
||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@ -1352,7 +1370,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, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||
|
||||
export type AddUserToWorkspaceMutationVariables = Exact<{
|
||||
inviteHash: Scalars['String'];
|
||||
@ -1506,6 +1524,9 @@ export const UserQueryFragmentFragmentDoc = gql`
|
||||
colorScheme
|
||||
avatarUrl
|
||||
locale
|
||||
timeZone
|
||||
dateFormat
|
||||
timeFormat
|
||||
}
|
||||
defaultWorkspace {
|
||||
id
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
@ -8,13 +7,14 @@ import {
|
||||
startOfMonth,
|
||||
} from 'date-fns';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
|
||||
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
|
||||
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
|
||||
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
|
||||
import { hasCalendarEventStarted } from '@/activities/calendar/utils/hasCalendarEventStarted';
|
||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||
import { TimelineCalendarEvent } from '~/generated/graphql';
|
||||
|
||||
type CalendarCurrentEventCursorProps = {
|
||||
calendarEvent: TimelineCalendarEvent;
|
||||
|
||||
@ -5,7 +5,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns';
|
||||
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
|
||||
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||
import { TimelineCalendarEvent } from '~/generated/graphql';
|
||||
|
||||
type CalendarDayCardContentProps = {
|
||||
calendarEvents: TimelineCalendarEvent[];
|
||||
|
||||
@ -3,7 +3,13 @@ import styled from '@emotion/styled';
|
||||
import { format } from 'date-fns';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Avatar, AvatarGroup, IconArrowRight, IconLock } from 'twenty-ui';
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
IconArrowRight,
|
||||
IconLock,
|
||||
isDefined,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
|
||||
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
|
||||
@ -14,9 +20,10 @@ import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEv
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { Card } from '@/ui/layout/card/components/Card';
|
||||
import { CardContent } from '@/ui/layout/card/components/CardContent';
|
||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||
import { CalendarChannelVisibility } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
TimelineCalendarEvent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type CalendarEventRowProps = {
|
||||
calendarEvent: TimelineCalendarEvent;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||
import { TimelineCalendarEvent } from '~/generated/graphql';
|
||||
|
||||
type CalendarContextValue = {
|
||||
calendarEventsByDayTime: Record<number, TimelineCalendarEvent[] | undefined>;
|
||||
|
||||
@ -1,41 +1,87 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { CalendarChannelVisibility } from '~/generated/graphql';
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
TimelineCalendarEvent,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const calendarEvents: CalendarEvent[] = [
|
||||
const calendarEvents: TimelineCalendarEvent[] = [
|
||||
{
|
||||
id: '1234',
|
||||
externalCreatedAt: '2024-02-17T20:45:43.854Z',
|
||||
isFullDay: false,
|
||||
startsAt: '2024-02-17T21:45:27.822Z',
|
||||
visibility: CalendarChannelVisibility.Metadata,
|
||||
__typename: 'CalendarEvent',
|
||||
conferenceLink: {
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-def-ghi',
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
__typename: 'LinksMetadata',
|
||||
},
|
||||
conferenceSolution: 'GoogleMeet',
|
||||
description: 'Description',
|
||||
endsAt: '2024-02-17T22:45:27.822Z',
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
{
|
||||
id: '5678',
|
||||
externalCreatedAt: '2024-02-18T19:43:37.854Z',
|
||||
isFullDay: false,
|
||||
startsAt: '2024-02-18T21:43:27.754Z',
|
||||
visibility: CalendarChannelVisibility.ShareEverything,
|
||||
__typename: 'CalendarEvent',
|
||||
conferenceLink: {
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-def-ghi',
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
__typename: 'LinksMetadata',
|
||||
},
|
||||
conferenceSolution: 'GoogleMeet',
|
||||
description: 'Description',
|
||||
endsAt: '2024-02-17T22:45:27.822Z',
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
{
|
||||
id: '91011',
|
||||
externalCreatedAt: '2024-02-19T20:45:20.854Z',
|
||||
isFullDay: true,
|
||||
startsAt: '2024-02-19T22:05:27.653Z',
|
||||
visibility: CalendarChannelVisibility.Metadata,
|
||||
__typename: 'CalendarEvent',
|
||||
conferenceLink: {
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-def-ghi',
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
__typename: 'LinksMetadata',
|
||||
},
|
||||
conferenceSolution: 'GoogleMeet',
|
||||
description: 'Description',
|
||||
endsAt: '2024-02-17T22:45:27.822Z',
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
{
|
||||
id: '121314',
|
||||
externalCreatedAt: '2024-02-20T20:45:12.854Z',
|
||||
isFullDay: true,
|
||||
startsAt: '2024-02-20T23:15:23.150Z',
|
||||
visibility: CalendarChannelVisibility.ShareEverything,
|
||||
__typename: 'CalendarEvent',
|
||||
conferenceLink: {
|
||||
primaryLinkUrl: 'https://meet.google.com/abc-def-ghi',
|
||||
primaryLinkLabel: 'Google Meet',
|
||||
__typename: 'LinksMetadata',
|
||||
},
|
||||
conferenceSolution: 'GoogleMeet',
|
||||
description: 'Description',
|
||||
endsAt: '2024-02-17T22:45:27.822Z',
|
||||
isCanceled: false,
|
||||
location: 'Location',
|
||||
participants: [],
|
||||
title: 'Title',
|
||||
__typename: 'TimelineCalendarEvent',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { getYear, isThisMonth, startOfDay, startOfMonth } from 'date-fns';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { findUpcomingCalendarEvent } from '@/activities/calendar/utils/findUpcomingCalendarEvent';
|
||||
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
|
||||
import { TimelineCalendarEvent } from '~/generated/graphql';
|
||||
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { sortDesc } from '~/utils/sort';
|
||||
|
||||
type CalendarEventGeneric = Omit<
|
||||
CalendarEvent,
|
||||
'participants' | 'externalCreatedAt' | '__typename'
|
||||
>;
|
||||
|
||||
export const useCalendarEvents = <T extends CalendarEventGeneric>(
|
||||
calendarEvents: T[],
|
||||
) => {
|
||||
export const useCalendarEvents = (calendarEvents: TimelineCalendarEvent[]) => {
|
||||
const calendarEventsByDayTime = groupArrayItemsBy(
|
||||
calendarEvents,
|
||||
(calendarEvent) =>
|
||||
@ -36,14 +29,14 @@ export const useCalendarEvents = <T extends CalendarEventGeneric>(
|
||||
|
||||
const monthTimesByYear = groupArrayItemsBy(sortedMonthTimes, getYear);
|
||||
|
||||
const getPreviousCalendarEvent = (calendarEvent: T) => {
|
||||
const getPreviousCalendarEvent = (calendarEvent: TimelineCalendarEvent) => {
|
||||
const calendarEventIndex = calendarEvents.indexOf(calendarEvent);
|
||||
return calendarEventIndex < calendarEvents.length - 1
|
||||
? calendarEvents[calendarEventIndex + 1]
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const getNextCalendarEvent = (calendarEvent: T) => {
|
||||
const getNextCalendarEvent = (calendarEvent: TimelineCalendarEvent) => {
|
||||
const calendarEventIndex = calendarEvents.indexOf(calendarEvent);
|
||||
return calendarEventIndex > 0
|
||||
? calendarEvents[calendarEventIndex - 1]
|
||||
|
||||
@ -6,6 +6,8 @@ import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
formatToHumanReadableDay,
|
||||
formatToHumanReadableMonth,
|
||||
@ -107,6 +109,8 @@ export const EventCardCalendarEvent = ({
|
||||
|
||||
const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();
|
||||
|
||||
const { timeZone } = useContext(UserContext);
|
||||
|
||||
if (isDefined(error)) {
|
||||
const shouldHideMessageContent = error.graphQLErrors.some(
|
||||
(e) => e.extensions?.code === 'FORBIDDEN',
|
||||
@ -138,12 +142,14 @@ export const EventCardCalendarEvent = ({
|
||||
throw new Error("Can't render a calendarEvent without a start date");
|
||||
}
|
||||
|
||||
const startsAtMonth = formatToHumanReadableMonth(startsAtDate);
|
||||
const startsAtMonth = formatToHumanReadableMonth(startsAtDate, timeZone);
|
||||
|
||||
const startsAtDay = formatToHumanReadableDay(startsAtDate);
|
||||
const startsAtDay = formatToHumanReadableDay(startsAtDate, timeZone);
|
||||
|
||||
const startsAtHour = formatToHumanReadableTime(startsAtDate);
|
||||
const endsAtHour = endsAtDate ? formatToHumanReadableTime(endsAtDate) : null;
|
||||
const startsAtHour = formatToHumanReadableTime(startsAtDate, timeZone);
|
||||
const endsAtHour = endsAtDate
|
||||
? formatToHumanReadableTime(endsAtDate, timeZone)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<StyledEventCardCalendarEventContainer
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
snapshot_UNSTABLE,
|
||||
useGotoRecoilSnapshot,
|
||||
@ -32,6 +32,12 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { currentUserState } from '../states/currentUserState';
|
||||
import { tokenPairState } from '../states/tokenPairState';
|
||||
|
||||
@ -56,6 +62,8 @@ export const useAuth = () => {
|
||||
|
||||
const goToRecoilSnapshot = useGotoRecoilSnapshot();
|
||||
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
|
||||
const handleChallenge = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
const challengeResult = await challenge({
|
||||
@ -96,17 +104,42 @@ export const useAuth = () => {
|
||||
setTokenPair(verifyResult.data?.verify.tokens);
|
||||
|
||||
const user = verifyResult.data?.verify.user;
|
||||
|
||||
let workspaceMember = null;
|
||||
|
||||
setCurrentUser(user);
|
||||
|
||||
if (isDefined(user.workspaceMember)) {
|
||||
workspaceMember = {
|
||||
...user.workspaceMember,
|
||||
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
||||
};
|
||||
|
||||
setCurrentWorkspaceMember(workspaceMember);
|
||||
|
||||
// TODO: factorize with UserProviderEffect
|
||||
setDateTimeFormat({
|
||||
timeZone:
|
||||
workspaceMember.timeZone && workspaceMember.timeZone !== 'system'
|
||||
? workspaceMember.timeZone
|
||||
: detectTimeZone(),
|
||||
dateFormat: isDefined(user.workspaceMember.dateFormat)
|
||||
? getDateFormatFromWorkspaceDateFormat(
|
||||
user.workspaceMember.dateFormat,
|
||||
)
|
||||
: detectDateFormat(),
|
||||
timeFormat: isDefined(user.workspaceMember.timeFormat)
|
||||
? getTimeFormatFromWorkspaceTimeFormat(
|
||||
user.workspaceMember.timeFormat,
|
||||
)
|
||||
: detectTimeFormat(),
|
||||
});
|
||||
}
|
||||
|
||||
const workspace = user.defaultWorkspace ?? null;
|
||||
|
||||
setCurrentWorkspace(workspace);
|
||||
|
||||
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
|
||||
const validWorkspaces = verifyResult.data?.verify.user.workspaces
|
||||
.filter(
|
||||
@ -117,6 +150,7 @@ export const useAuth = () => {
|
||||
|
||||
setWorkspaces(validWorkspaces);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
workspaceMember,
|
||||
@ -131,6 +165,7 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMember,
|
||||
setCurrentWorkspace,
|
||||
setWorkspaces,
|
||||
setDateTimeFormat,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
export enum DateFormat {
|
||||
SYSTEM = 'SYSTEM',
|
||||
MONTH_FIRST = 'MMM d, yyyy', // US
|
||||
DAY_FIRST = 'd MMM, yyyy', // UK
|
||||
YEAR_FIRST = 'yyyy MMM d',
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export enum TimeFormat {
|
||||
SYSTEM = 'SYSTEM',
|
||||
HOUR_24 = 'HH:mm',
|
||||
HOUR_12 = 'h:mm aa',
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const dateTimeFormatState = createState<{
|
||||
timeZone: string;
|
||||
dateFormat: DateFormat;
|
||||
timeFormat: TimeFormat;
|
||||
}>({
|
||||
key: 'dateTimeFormatState',
|
||||
defaultValue: {
|
||||
timeZone: detectTimeZone(),
|
||||
dateFormat: DateFormat.MONTH_FIRST,
|
||||
timeFormat: TimeFormat['HOUR_24'],
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
|
||||
describe('detectDateFormat', () => {
|
||||
it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => {
|
||||
// Mock the Intl.DateTimeFormat to return a specific format
|
||||
const mockDateTimeFormat = jest.fn().mockReturnValue({
|
||||
formatToParts: () => [
|
||||
{ type: 'month', value: '01' },
|
||||
{ type: 'day', value: '01' },
|
||||
{ type: 'year', value: '2022' },
|
||||
],
|
||||
supportedLocalesOf: () => [],
|
||||
}) as any;
|
||||
global.Intl.DateTimeFormat = mockDateTimeFormat;
|
||||
|
||||
const result = detectDateFormat();
|
||||
|
||||
expect(result).toBe(DateFormat.MONTH_FIRST);
|
||||
});
|
||||
|
||||
it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => {
|
||||
// Mock the Intl.DateTimeFormat to return a specific format
|
||||
const mockDateTimeFormat = jest.fn().mockReturnValue({
|
||||
formatToParts: () => [
|
||||
{ type: 'day', value: '01' },
|
||||
{ type: 'month', value: '01' },
|
||||
{ type: 'year', value: '2022' },
|
||||
],
|
||||
}) as any;
|
||||
global.Intl.DateTimeFormat = mockDateTimeFormat;
|
||||
|
||||
const result = detectDateFormat();
|
||||
|
||||
expect(result).toBe(DateFormat.DAY_FIRST);
|
||||
});
|
||||
|
||||
it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => {
|
||||
// Mock the Intl.DateTimeFormat to return a specific format
|
||||
const mockDateTimeFormat = jest.fn().mockReturnValue({
|
||||
formatToParts: () => [
|
||||
{ type: 'year', value: '2022' },
|
||||
{ type: 'month', value: '01' },
|
||||
{ type: 'day', value: '01' },
|
||||
],
|
||||
}) as any;
|
||||
global.Intl.DateTimeFormat = mockDateTimeFormat;
|
||||
|
||||
const result = detectDateFormat();
|
||||
|
||||
expect(result).toBe(DateFormat.YEAR_FIRST);
|
||||
});
|
||||
|
||||
it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => {
|
||||
// Mock the Intl.DateTimeFormat to return a specific format
|
||||
const mockDateTimeFormat = jest.fn().mockReturnValue({
|
||||
formatToParts: () => [
|
||||
{ type: 'hour', value: '12' },
|
||||
{ type: 'minute', value: '00' },
|
||||
{ type: 'second', value: '00' },
|
||||
],
|
||||
}) as any;
|
||||
global.Intl.DateTimeFormat = mockDateTimeFormat;
|
||||
|
||||
const result = detectDateFormat();
|
||||
|
||||
expect(result).toBe(DateFormat.MONTH_FIRST);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,30 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
|
||||
describe('detectTimeFormat', () => {
|
||||
it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => {
|
||||
// Mock the resolvedOptions method to return hour12 as true
|
||||
const mockResolvedOptions = jest.fn(() => ({ hour12: true }));
|
||||
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
|
||||
resolvedOptions: mockResolvedOptions,
|
||||
})) as any;
|
||||
|
||||
const result = detectTimeFormat();
|
||||
|
||||
expect(result).toBe(TimeFormat.HOUR_12);
|
||||
expect(mockResolvedOptions).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => {
|
||||
// Mock the resolvedOptions method to return hour12 as false
|
||||
const mockResolvedOptions = jest.fn(() => ({ hour12: false }));
|
||||
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
|
||||
resolvedOptions: mockResolvedOptions,
|
||||
})) as any;
|
||||
|
||||
const result = detectTimeFormat();
|
||||
|
||||
expect(result).toBe(TimeFormat.HOUR_24);
|
||||
expect(mockResolvedOptions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel';
|
||||
|
||||
describe('formatTimeZoneLabel', () => {
|
||||
it('should format the time zone label correctly when location is included in the label', () => {
|
||||
const ianaTimeZone = 'Europe/Paris';
|
||||
const expectedLabel = '(GMT+02:00) Central European Summer Time - Paris';
|
||||
|
||||
const formattedLabel = formatTimeZoneLabel(ianaTimeZone);
|
||||
|
||||
expect(formattedLabel).toEqual(expectedLabel);
|
||||
});
|
||||
|
||||
it('should format the time zone label correctly when location is not included in the label', () => {
|
||||
const ianaTimeZone = 'America/New_York';
|
||||
const expectedLabel = '(GMT-04:00) Eastern Daylight Time - New York';
|
||||
|
||||
const formattedLabel = formatTimeZoneLabel(ianaTimeZone);
|
||||
|
||||
expect(formattedLabel).toEqual(expectedLabel);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
|
||||
export const detectDateFormat = (): DateFormat => {
|
||||
const date = new Date();
|
||||
const formatter = new Intl.DateTimeFormat(navigator.language);
|
||||
const parts = formatter.formatToParts(date);
|
||||
|
||||
const partOrder = parts
|
||||
.filter((part) => ['year', 'month', 'day'].includes(part.type))
|
||||
.map((part) => part.type);
|
||||
|
||||
if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST;
|
||||
if (partOrder[0] === 'day') return DateFormat.DAY_FIRST;
|
||||
if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST;
|
||||
|
||||
return DateFormat.MONTH_FIRST;
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const detectTimeFormat = () => {
|
||||
const isHour12 = Intl.DateTimeFormat(navigator.language, {
|
||||
hour: 'numeric',
|
||||
}).resolvedOptions().hour12;
|
||||
if (isDefined(isHour12) && isHour12) return TimeFormat.HOUR_12;
|
||||
return TimeFormat.HOUR_24;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel';
|
||||
import { AVAILABLE_TIME_ZONE_OPTIONS_BY_LABEL } from '@/settings/accounts/constants/AvailableTimezoneOptionsByLabel';
|
||||
import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel';
|
||||
|
||||
/**
|
||||
* Finds the matching available IANA time zone select option from a given IANA time zone.
|
||||
@ -0,0 +1,15 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
export const formatDatetime = (
|
||||
date: Date | string,
|
||||
timeZone: string,
|
||||
dateFormat: string,
|
||||
timeFormat: string,
|
||||
) => {
|
||||
return formatInTimeZone(
|
||||
parseDate(date).toJSDate(),
|
||||
timeZone,
|
||||
`${dateFormat} ${timeFormat}`,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
export const formatDateISOStringToDate = (
|
||||
date: string,
|
||||
timeZone: string,
|
||||
dateFormat: DateFormat,
|
||||
) => {
|
||||
return formatInTimeZone(new Date(date), timeZone, `${dateFormat}`);
|
||||
};
|
||||
@ -0,0 +1,16 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
export const formatDateISOStringToDateTime = (
|
||||
date: string,
|
||||
timeZone: string,
|
||||
dateFormat: DateFormat,
|
||||
timeFormat: TimeFormat,
|
||||
) => {
|
||||
return formatInTimeZone(
|
||||
new Date(date),
|
||||
timeZone,
|
||||
`${dateFormat} ${timeFormat}`,
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
import defaultLocale from 'date-fns/locale/en-US';
|
||||
|
||||
/**
|
||||
* Formats a IANA time zone to a select option label.
|
||||
* @param ianaTimeZone IANA time zone
|
||||
* @returns Formatted label
|
||||
* @example 'Europe/Paris' => '(GMT+01:00) Central European Time - Paris'
|
||||
*/
|
||||
export const formatTimeZoneLabel = (ianaTimeZone: string) => {
|
||||
const timeZoneWithGmtOffset = formatInTimeZone(
|
||||
Date.now(),
|
||||
ianaTimeZone,
|
||||
`(OOOO) zzzz`,
|
||||
{ locale: defaultLocale },
|
||||
);
|
||||
const ianaTimeZoneParts = ianaTimeZone.split('/');
|
||||
const location =
|
||||
ianaTimeZoneParts.length > 1
|
||||
? ianaTimeZoneParts.slice(-1)[0].replaceAll('_', ' ')
|
||||
: undefined;
|
||||
|
||||
const timeZoneLabel =
|
||||
!location || timeZoneWithGmtOffset.includes(location)
|
||||
? timeZoneWithGmtOffset
|
||||
: [timeZoneWithGmtOffset, location].join(' - ');
|
||||
|
||||
return timeZoneLabel;
|
||||
};
|
||||
@ -0,0 +1,20 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getDateFormatFromWorkspaceDateFormat = (
|
||||
workspaceDateFormat: WorkspaceMemberDateFormatEnum,
|
||||
) => {
|
||||
switch (workspaceDateFormat) {
|
||||
case WorkspaceMemberDateFormatEnum.System:
|
||||
return detectDateFormat();
|
||||
case WorkspaceMemberDateFormatEnum.MonthFirst:
|
||||
return DateFormat.MONTH_FIRST;
|
||||
case WorkspaceMemberDateFormatEnum.DayFirst:
|
||||
return DateFormat.DAY_FIRST;
|
||||
case WorkspaceMemberDateFormatEnum.YearFirst:
|
||||
return DateFormat.YEAR_FIRST;
|
||||
default:
|
||||
return DateFormat.MONTH_FIRST;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getTimeFormatFromWorkspaceTimeFormat = (
|
||||
workspaceTimeFormat: WorkspaceMemberTimeFormatEnum,
|
||||
) => {
|
||||
switch (workspaceTimeFormat) {
|
||||
case WorkspaceMemberTimeFormatEnum.System:
|
||||
return detectTimeFormat();
|
||||
case WorkspaceMemberTimeFormatEnum.Hour_24:
|
||||
return TimeFormat.HOUR_24;
|
||||
case WorkspaceMemberTimeFormatEnum.Hour_12:
|
||||
return TimeFormat.HOUR_12;
|
||||
default:
|
||||
return TimeFormat.HOUR_24;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { WorkspaceMemberDateFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getWorkspaceDateFormatFromDateFormat = (
|
||||
dateFormat: DateFormat,
|
||||
) => {
|
||||
switch (dateFormat) {
|
||||
case DateFormat.SYSTEM:
|
||||
return WorkspaceMemberDateFormatEnum.System;
|
||||
case DateFormat.MONTH_FIRST:
|
||||
return WorkspaceMemberDateFormatEnum.MonthFirst;
|
||||
case DateFormat.DAY_FIRST:
|
||||
return WorkspaceMemberDateFormatEnum.DayFirst;
|
||||
case DateFormat.YEAR_FIRST:
|
||||
return WorkspaceMemberDateFormatEnum.YearFirst;
|
||||
default:
|
||||
return WorkspaceMemberDateFormatEnum.MonthFirst;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,17 @@
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { WorkspaceMemberTimeFormatEnum } from '~/generated/graphql';
|
||||
|
||||
export const getWorkspaceTimeFormatFromTimeFormat = (
|
||||
timeFormat: TimeFormat,
|
||||
) => {
|
||||
switch (timeFormat) {
|
||||
case TimeFormat.SYSTEM:
|
||||
return WorkspaceMemberTimeFormatEnum.System;
|
||||
case TimeFormat.HOUR_24:
|
||||
return WorkspaceMemberTimeFormatEnum.Hour_24;
|
||||
case TimeFormat.HOUR_12:
|
||||
return WorkspaceMemberTimeFormatEnum.Hour_12;
|
||||
default:
|
||||
return WorkspaceMemberTimeFormatEnum.Hour_24;
|
||||
}
|
||||
};
|
||||
@ -1,7 +1,10 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { DateFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateFieldDisplay';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
@ -12,6 +15,19 @@ const meta: Meta = {
|
||||
MemoryRouterDecorator,
|
||||
getFieldDecorator('person', 'createdAt'),
|
||||
ComponentDecorator,
|
||||
(Story) => {
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
dateFormat: DateFormat.SYSTEM,
|
||||
timeFormat: TimeFormat.SYSTEM,
|
||||
timeZone: 'UTC',
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</UserContext.Provider>
|
||||
);
|
||||
},
|
||||
],
|
||||
component: DateFieldDisplay,
|
||||
args: {},
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { DateTimeFieldDisplay } from '@/object-record/record-field/meta-types/display/components/DateTimeFieldDisplay';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||
@ -12,6 +15,19 @@ const meta: Meta = {
|
||||
MemoryRouterDecorator,
|
||||
getFieldDecorator('person', 'createdAt'),
|
||||
ComponentDecorator,
|
||||
(Story) => {
|
||||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
dateFormat: DateFormat.SYSTEM,
|
||||
timeFormat: TimeFormat.SYSTEM,
|
||||
timeZone: 'UTC',
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</UserContext.Provider>
|
||||
);
|
||||
},
|
||||
],
|
||||
component: DateTimeFieldDisplay,
|
||||
args: {},
|
||||
|
||||
@ -10,7 +10,7 @@ import { H2Title } from 'twenty-ui';
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
TimelineCalendarEvent,
|
||||
} from '~/generated-metadata/graphql';
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledGeneralContainer = styled.div`
|
||||
display: flex;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
import { DateFormat } from '@/settings/accounts/constants/DateFormat';
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
type SettingsAccountsCalendarDateFormatSelectProps = {
|
||||
@ -21,12 +21,12 @@ export const SettingsAccountsCalendarDateFormatSelect = ({
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
label: formatInTimeZone(Date.now(), timeZone, DateFormat.US),
|
||||
value: DateFormat.US,
|
||||
label: formatInTimeZone(Date.now(), timeZone, DateFormat.MONTH_FIRST),
|
||||
value: DateFormat.MONTH_FIRST,
|
||||
},
|
||||
{
|
||||
label: formatInTimeZone(Date.now(), timeZone, DateFormat.UK),
|
||||
value: DateFormat.UK,
|
||||
label: formatInTimeZone(Date.now(), timeZone, DateFormat.DAY_FIRST),
|
||||
value: DateFormat.DAY_FIRST,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
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 { DateFormat } from '@/settings/accounts/constants/DateFormat';
|
||||
import { TimeFormat } from '@/settings/accounts/constants/TimeFormat';
|
||||
import { detectTimeZone } from '@/settings/accounts/utils/detectTimeZone';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -19,10 +20,10 @@ export const SettingsAccountsCalendarDisplaySettings = () => {
|
||||
const [timeZone, setTimeZone] = useState(detectTimeZone());
|
||||
|
||||
// TODO: use the user's saved date format.
|
||||
const [dateFormat, setDateFormat] = useState(DateFormat.US);
|
||||
const [dateFormat, setDateFormat] = useState(DateFormat.MONTH_FIRST);
|
||||
|
||||
// TODO: use the user's saved time format.
|
||||
const [timeFormat, setTimeFormat] = useState(TimeFormat['24h']);
|
||||
const [timeFormat, setTimeFormat] = useState(TimeFormat['HOUR_24']);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
import { TimeFormat } from '@/settings/accounts/constants/TimeFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
type SettingsAccountsCalendarTimeFormatSelectProps = {
|
||||
@ -24,17 +24,17 @@ export const SettingsAccountsCalendarTimeFormatSelect = ({
|
||||
label: `24h (${formatInTimeZone(
|
||||
Date.now(),
|
||||
timeZone,
|
||||
TimeFormat['24h'],
|
||||
TimeFormat.HOUR_24,
|
||||
)})`,
|
||||
value: TimeFormat['24h'],
|
||||
value: TimeFormat.HOUR_24,
|
||||
},
|
||||
{
|
||||
label: `12h (${formatInTimeZone(
|
||||
Date.now(),
|
||||
timeZone,
|
||||
TimeFormat['12h'],
|
||||
TimeFormat.HOUR_12,
|
||||
)})`,
|
||||
value: TimeFormat['12h'],
|
||||
value: TimeFormat.HOUR_12,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableTimeZoneOption';
|
||||
import { AVAILABLE_TIMEZONE_OPTIONS } from '@/settings/accounts/constants/AvailableTimezoneOptions';
|
||||
import { detectTimeZone } from '@/settings/accounts/utils/detectTimeZone';
|
||||
import { findAvailableTimeZoneOption } from '@/settings/accounts/utils/findAvailableTimeZoneOption';
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
type SettingsAccountsCalendarTimeZoneSelectProps = {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable @nx/workspace-max-consts-per-file */
|
||||
import { IANA_TIME_ZONES } from '@/settings/accounts/constants/IanaTimeZones';
|
||||
import { IANA_TIME_ZONES } from '@/localization/constants/IanaTimeZones';
|
||||
import { formatTimeZoneLabel } from '@/settings/accounts/utils/formatTimeZoneLabel';
|
||||
import { SelectOption } from '@/ui/input/components/Select';
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
export enum DateFormat {
|
||||
US = 'MMM d, yyyy',
|
||||
UK = 'd MMM yyyy',
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export enum TimeFormat {
|
||||
'24h' = 'HH:mm',
|
||||
'12h' = 'h:mm aa',
|
||||
}
|
||||
@ -1,13 +1,18 @@
|
||||
import { formatISOStringToHumanReadableDate } from '~/utils/date-utils';
|
||||
|
||||
import { formatDateISOStringToDate } from '@/localization/utils/formatDateISOStringToDate';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateDisplay = ({ value }: DateDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value ? formatISOStringToHumanReadableDate(value) : ''}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
export const DateDisplay = ({ value }: DateDisplayProps) => {
|
||||
const { dateFormat, timeZone } = useContext(UserContext);
|
||||
|
||||
const formattedDate = value
|
||||
? formatDateISOStringToDate(value, timeZone, dateFormat)
|
||||
: '';
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
};
|
||||
|
||||
@ -1,13 +1,18 @@
|
||||
import { formatISOStringToHumanReadableDateTime } from '~/utils/date-utils';
|
||||
|
||||
import { formatDateISOStringToDateTime } from '@/localization/utils/formatDateISOStringToDateTime';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import { EllipsisDisplay } from './EllipsisDisplay';
|
||||
|
||||
type DateTimeDisplayProps = {
|
||||
value: string | null | undefined;
|
||||
};
|
||||
|
||||
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => (
|
||||
<EllipsisDisplay>
|
||||
{value ? formatISOStringToHumanReadableDateTime(value) : ''}
|
||||
</EllipsisDisplay>
|
||||
);
|
||||
export const DateTimeDisplay = ({ value }: DateTimeDisplayProps) => {
|
||||
const { dateFormat, timeFormat, timeZone } = useContext(UserContext);
|
||||
|
||||
const formattedDate = value
|
||||
? formatDateISOStringToDateTime(value, timeZone, dateFormat, timeFormat)
|
||||
: '';
|
||||
|
||||
return <EllipsisDisplay>{formattedDate}</EllipsisDisplay>;
|
||||
};
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useIMask } from 'react-imask';
|
||||
import { css } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useIMask } from 'react-imask';
|
||||
|
||||
import { DATE_BLOCKS } from '@/ui/input/components/internal/date/constants/DateBlocks';
|
||||
import { DATE_MASK } from '@/ui/input/components/internal/date/constants/DateMask';
|
||||
@ -41,6 +41,7 @@ type DateTimeInputProps = {
|
||||
onChange?: (date: Date | null) => void;
|
||||
date: Date | null;
|
||||
isDateTimeInput?: boolean;
|
||||
userTimezone?: string;
|
||||
onError?: (error: Error) => void;
|
||||
};
|
||||
|
||||
@ -48,6 +49,7 @@ export const DateTimeInput = ({
|
||||
date,
|
||||
onChange,
|
||||
isDateTimeInput,
|
||||
userTimezone,
|
||||
}: DateTimeInputProps) => {
|
||||
const parsingFormat = isDateTimeInput ? 'MM/dd/yyyy HH:mm' : 'MM/dd/yyyy';
|
||||
|
||||
@ -55,7 +57,7 @@ export const DateTimeInput = ({
|
||||
|
||||
const parseDateToString = useCallback(
|
||||
(date: any) => {
|
||||
const dateParsed = DateTime.fromJSDate(date);
|
||||
const dateParsed = DateTime.fromJSDate(date, { zone: userTimezone });
|
||||
|
||||
const dateWithoutTime = DateTime.fromJSDate(date)
|
||||
.toLocal()
|
||||
@ -70,19 +72,19 @@ export const DateTimeInput = ({
|
||||
});
|
||||
|
||||
const formattedDate = isDateTimeInput
|
||||
? dateParsed.toFormat(parsingFormat)
|
||||
? dateParsed.setZone(userTimezone).toFormat(parsingFormat)
|
||||
: dateWithoutTime.toFormat(parsingFormat);
|
||||
|
||||
return formattedDate;
|
||||
},
|
||||
[parsingFormat, isDateTimeInput],
|
||||
[parsingFormat, isDateTimeInput, userTimezone],
|
||||
);
|
||||
|
||||
const parseStringToDate = (str: string) => {
|
||||
setHasError(false);
|
||||
|
||||
const parsedDate = isDateTimeInput
|
||||
? DateTime.fromFormat(str, parsingFormat)
|
||||
? DateTime.fromFormat(str, parsingFormat, { zone: userTimezone })
|
||||
: DateTime.fromFormat(str, parsingFormat, { zone: 'utc' });
|
||||
|
||||
const isValid = parsedDate.isValid;
|
||||
|
||||
@ -17,6 +17,8 @@ import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/compone
|
||||
import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useContext } from 'react';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
export const months = [
|
||||
@ -322,6 +324,8 @@ export const InternalDatePicker = ({
|
||||
}: InternalDatePickerProps) => {
|
||||
const internalDate = date ?? new Date();
|
||||
|
||||
const { timeZone } = useContext(UserContext);
|
||||
|
||||
const { closeDropdown } = useDropdown(MONTH_AND_YEAR_DROPDOWN_ID);
|
||||
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||
@ -377,10 +381,56 @@ export const InternalDatePicker = ({
|
||||
onChange?.(newDate);
|
||||
};
|
||||
|
||||
const handleAddMonth = () => {
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
|
||||
.plus({ months: 1 })
|
||||
.toJSDate();
|
||||
|
||||
onChange?.(dateParsed);
|
||||
};
|
||||
|
||||
const handleSubtractMonth = () => {
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
|
||||
.minus({ months: 1 })
|
||||
.toJSDate();
|
||||
|
||||
onChange?.(dateParsed);
|
||||
};
|
||||
|
||||
const handleChangeYear = (year: number) => {
|
||||
const newDate = new Date(internalDate);
|
||||
newDate.setFullYear(year);
|
||||
onChange?.(newDate);
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, { zone: timeZone })
|
||||
.set({ year: year })
|
||||
.toJSDate();
|
||||
|
||||
onChange?.(dateParsed);
|
||||
};
|
||||
|
||||
const handleDateChange = (date: Date) => {
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, {
|
||||
zone: isDateTimeInput ? timeZone : 'local',
|
||||
})
|
||||
.set({
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
})
|
||||
.toJSDate();
|
||||
|
||||
onChange?.(dateParsed);
|
||||
};
|
||||
|
||||
const handleDateSelect = (date: Date) => {
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, {
|
||||
zone: isDateTimeInput ? timeZone : 'local',
|
||||
})
|
||||
.set({
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
})
|
||||
.toJSDate();
|
||||
|
||||
handleMouseSelect?.(dateParsed);
|
||||
};
|
||||
|
||||
const dateWithoutTime = DateTime.fromJSDate(internalDate)
|
||||
@ -395,7 +445,29 @@ export const InternalDatePicker = ({
|
||||
millisecond: 0,
|
||||
})
|
||||
.toJSDate();
|
||||
const dateToUse = isDateTimeInput ? date : dateWithoutTime;
|
||||
|
||||
const dateParsed = DateTime.fromJSDate(internalDate, {
|
||||
zone: isDateTimeInput ? timeZone : 'local',
|
||||
});
|
||||
|
||||
// We have to force a end of day on the computer local timezone with the given date
|
||||
// Because JS Date API cannot hold a timezone other than the local one
|
||||
// And if we don't do that workaround we will have problems when changing the date
|
||||
// Because the shown date will have 1 day more or less than the real date
|
||||
// Leading to bugs where we select 1st of January and it shows 31st of December for example
|
||||
const endOfDayDateTimeInLocalTimezone = DateTime.now().set({
|
||||
day: dateParsed.get('day'),
|
||||
month: dateParsed.get('month'),
|
||||
year: dateParsed.get('year'),
|
||||
hour: 23,
|
||||
minute: 59,
|
||||
second: 59,
|
||||
millisecond: 999,
|
||||
});
|
||||
|
||||
const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate();
|
||||
|
||||
const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime;
|
||||
|
||||
return (
|
||||
<StyledContainer onKeyDown={handleKeyDown}>
|
||||
@ -405,27 +477,16 @@ export const InternalDatePicker = ({
|
||||
selected={dateToUse}
|
||||
openToDate={isDefined(dateToUse) ? dateToUse : undefined}
|
||||
disabledKeyboardNavigation
|
||||
onChange={(newDate) => {
|
||||
newDate?.setHours(internalDate.getUTCHours());
|
||||
newDate?.setUTCMinutes(internalDate.getUTCMinutes());
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
onChange={handleDateChange}
|
||||
customInput={
|
||||
<DateTimeInput
|
||||
date={dateToUse}
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
userTimezone={timeZone}
|
||||
/>
|
||||
}
|
||||
onMonthChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
onYearChange={(newDate) => {
|
||||
onChange?.(newDate);
|
||||
}}
|
||||
renderCustomHeader={({
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
}) => (
|
||||
@ -434,6 +495,7 @@ export const InternalDatePicker = ({
|
||||
date={internalDate}
|
||||
isDateTimeInput={isDateTimeInput}
|
||||
onChange={onChange}
|
||||
userTimezone={timeZone}
|
||||
/>
|
||||
<StyledCustomDatePickerHeader>
|
||||
<Select
|
||||
@ -441,51 +503,33 @@ export const InternalDatePicker = ({
|
||||
options={months}
|
||||
disableBlur
|
||||
onChange={handleChangeMonth}
|
||||
value={internalDate.getUTCMonth()}
|
||||
value={endOfDayInLocalTimezone.getMonth()}
|
||||
fullWidth
|
||||
/>
|
||||
<Select
|
||||
dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID}
|
||||
onChange={handleChangeYear}
|
||||
value={internalDate.getUTCFullYear()}
|
||||
value={endOfDayInLocalTimezone.getFullYear()}
|
||||
options={years}
|
||||
disableBlur
|
||||
fullWidth
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronLeft}
|
||||
onClick={() => decreaseMonth()}
|
||||
onClick={handleSubtractMonth}
|
||||
size="medium"
|
||||
disabled={prevMonthButtonDisabled}
|
||||
/>
|
||||
<LightIconButton
|
||||
Icon={IconChevronRight}
|
||||
onClick={() => increaseMonth()}
|
||||
onClick={handleAddMonth}
|
||||
size="medium"
|
||||
disabled={nextMonthButtonDisabled}
|
||||
/>
|
||||
</StyledCustomDatePickerHeader>
|
||||
</>
|
||||
)}
|
||||
onSelect={(date: Date) => {
|
||||
const dateParsedWithoutTime = DateTime.fromObject(
|
||||
{
|
||||
day: date.getDate(),
|
||||
month: date.getMonth() + 1,
|
||||
year: date.getFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
},
|
||||
{ zone: 'utc' },
|
||||
).toJSDate();
|
||||
|
||||
const dateForUpdate = isDateTimeInput
|
||||
? date
|
||||
: dateParsedWithoutTime;
|
||||
|
||||
handleMouseSelect?.(dateForUpdate);
|
||||
}}
|
||||
onSelect={handleDateSelect}
|
||||
/>
|
||||
</div>
|
||||
{clearable && (
|
||||
|
||||
@ -2,7 +2,9 @@ import React from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { UserContext } from '@/users/contexts/UserContext';
|
||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||
import { UserOrMetadataLoader } from '~/loading/components/UserOrMetadataLoader';
|
||||
|
||||
@ -10,10 +12,20 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => {
|
||||
const isCurrentUserLoaded = useRecoilValue(isCurrentUserLoadedState);
|
||||
const isMatchingLocation = useIsMatchingLocation();
|
||||
|
||||
const dateTimeFormat = useRecoilValue(dateTimeFormatState);
|
||||
|
||||
return !isCurrentUserLoaded &&
|
||||
!isMatchingLocation(AppPath.CreateWorkspace) ? (
|
||||
<UserOrMetadataLoader />
|
||||
) : (
|
||||
<>{children}</>
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
dateFormat: dateTimeFormat.dateFormat,
|
||||
timeFormat: dateTimeFormat.timeFormat,
|
||||
timeZone: dateTimeFormat.timeZone,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,6 +6,12 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
@ -20,6 +26,8 @@ export const UserProviderEffect = () => {
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
|
||||
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
|
||||
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -47,6 +55,20 @@ export const UserProviderEffect = () => {
|
||||
...workspaceMember,
|
||||
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
|
||||
});
|
||||
|
||||
// TODO: factorize
|
||||
setDateTimeFormat({
|
||||
timeZone:
|
||||
workspaceMember.timeZone && workspaceMember.timeZone !== 'system'
|
||||
? workspaceMember.timeZone
|
||||
: detectTimeZone(),
|
||||
dateFormat: isDefined(workspaceMember.dateFormat)
|
||||
? getDateFormatFromWorkspaceDateFormat(workspaceMember.dateFormat)
|
||||
: detectDateFormat(),
|
||||
timeFormat: isDefined(workspaceMember.timeFormat)
|
||||
? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat)
|
||||
: detectTimeFormat(),
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(userWorkspaces)) {
|
||||
@ -65,6 +87,7 @@ export const UserProviderEffect = () => {
|
||||
setWorkspaces,
|
||||
queryData?.currentUser,
|
||||
setIsCurrentUserLoaded,
|
||||
setDateTimeFormat,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type UserContextType = {
|
||||
dateFormat: DateFormat;
|
||||
timeFormat: TimeFormat;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const UserContext = createContext<UserContextType>(
|
||||
{} as UserContextType,
|
||||
);
|
||||
@ -18,6 +18,9 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
colorScheme
|
||||
avatarUrl
|
||||
locale
|
||||
timeZone
|
||||
dateFormat
|
||||
timeFormat
|
||||
}
|
||||
defaultWorkspace {
|
||||
id
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
import {
|
||||
WorkspaceMemberDateFormatEnum,
|
||||
WorkspaceMemberTimeFormatEnum,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export type ColorScheme = 'Dark' | 'Light' | 'System';
|
||||
|
||||
export type WorkspaceMember = {
|
||||
@ -9,10 +14,13 @@ export type WorkspaceMember = {
|
||||
lastName: string;
|
||||
};
|
||||
avatarUrl?: string | null;
|
||||
locale: string;
|
||||
locale?: string | null;
|
||||
colorScheme?: ColorScheme;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
userEmail: string;
|
||||
userId: string;
|
||||
timeZone?: string | null;
|
||||
dateFormat?: WorkspaceMemberDateFormatEnum | null;
|
||||
timeFormat?: WorkspaceMemberTimeFormatEnum | null;
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { SettingsAppearance } from '../SettingsAppearance';
|
||||
import { SettingsAppearance } from '../profile/appearance/components/SettingsAppearance';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'Pages/Settings/SettingsAppearance',
|
||||
|
||||
@ -0,0 +1,145 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||
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 {
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
import { isEmptyObject } from '~/utils/isEmptyObject';
|
||||
import { logError } from '~/utils/logError';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const DateTimeSettings = () => {
|
||||
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const [dateTimeFormat, setDateTimeFormat] =
|
||||
useRecoilState(dateTimeFormatState);
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
});
|
||||
|
||||
const updateWorkspaceMember = async (changedFields: any) => {
|
||||
if (!currentWorkspaceMember?.id) {
|
||||
throw new Error('User is not logged in');
|
||||
}
|
||||
|
||||
try {
|
||||
await updateOneRecord({
|
||||
idToUpdate: currentWorkspaceMember.id,
|
||||
updateOneRecordInput: changedFields,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isDefined(currentWorkspaceMember)) return;
|
||||
|
||||
const handleSettingsChange = (
|
||||
settingName: 'timeZone' | 'dateFormat' | 'timeFormat',
|
||||
value: string,
|
||||
) => {
|
||||
const workspaceMember: any = {};
|
||||
const dateTime: any = {};
|
||||
|
||||
switch (settingName) {
|
||||
case 'timeZone': {
|
||||
workspaceMember[settingName] = value;
|
||||
dateTime[settingName] = value === 'system' ? detectTimeZone() : value;
|
||||
break;
|
||||
}
|
||||
case 'dateFormat': {
|
||||
workspaceMember[settingName] = getWorkspaceDateFormatFromDateFormat(
|
||||
value as DateFormat,
|
||||
);
|
||||
dateTime[settingName] =
|
||||
(value as DateFormat) === DateFormat.SYSTEM
|
||||
? detectDateFormat()
|
||||
: (value as DateFormat);
|
||||
break;
|
||||
}
|
||||
case 'timeFormat': {
|
||||
workspaceMember[settingName] = getWorkspaceTimeFormatFromTimeFormat(
|
||||
value as TimeFormat,
|
||||
);
|
||||
dateTime[settingName] =
|
||||
(value as TimeFormat) === TimeFormat.SYSTEM
|
||||
? detectTimeFormat()
|
||||
: (value as TimeFormat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmptyObject(dateTime)) {
|
||||
setDateTimeFormat({
|
||||
...dateTimeFormat,
|
||||
...dateTime,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEmptyObject(workspaceMember)) {
|
||||
setCurrentWorkspaceMember({
|
||||
...currentWorkspaceMember,
|
||||
...workspaceMember,
|
||||
});
|
||||
updateWorkspaceMember(workspaceMember);
|
||||
}
|
||||
};
|
||||
|
||||
const timeZone =
|
||||
currentWorkspaceMember.timeZone === 'system'
|
||||
? 'system'
|
||||
: dateTimeFormat.timeZone;
|
||||
|
||||
const dateFormat =
|
||||
currentWorkspaceMember.dateFormat === WorkspaceMemberDateFormatEnum.System
|
||||
? DateFormat.SYSTEM
|
||||
: dateTimeFormat.dateFormat;
|
||||
|
||||
const timeFormat =
|
||||
currentWorkspaceMember.timeFormat === WorkspaceMemberTimeFormatEnum.System
|
||||
? TimeFormat.SYSTEM
|
||||
: dateTimeFormat.timeFormat;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<DateTimeSettingsTimeZoneSelect
|
||||
value={timeZone}
|
||||
onChange={(value) => handleSettingsChange('timeZone', value)}
|
||||
/>
|
||||
<DateTimeSettingsDateFormatSelect
|
||||
value={dateFormat}
|
||||
onChange={(value) => handleSettingsChange('dateFormat', value)}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
<DateTimeSettingsTimeFormatSelect
|
||||
value={timeFormat}
|
||||
onChange={(value) => handleSettingsChange('timeFormat', value)}
|
||||
timeZone={timeZone}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
type DateTimeSettingsDateFormatSelectProps = {
|
||||
value: DateFormat;
|
||||
onChange: (nextValue: DateFormat) => void;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const DateTimeSettingsDateFormatSelect = ({
|
||||
onChange,
|
||||
timeZone,
|
||||
value,
|
||||
}: DateTimeSettingsDateFormatSelectProps) => {
|
||||
const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone;
|
||||
return (
|
||||
<Select
|
||||
dropdownId="datetime-settings-date-format"
|
||||
dropdownWidth={218}
|
||||
label="Date format"
|
||||
fullWidth
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
label: `System settings`,
|
||||
value: DateFormat.SYSTEM,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
Date.now(),
|
||||
setTimeZone,
|
||||
DateFormat.MONTH_FIRST,
|
||||
)}`,
|
||||
value: DateFormat.MONTH_FIRST,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
Date.now(),
|
||||
setTimeZone,
|
||||
DateFormat.DAY_FIRST,
|
||||
)}`,
|
||||
value: DateFormat.DAY_FIRST,
|
||||
},
|
||||
{
|
||||
label: `${formatInTimeZone(
|
||||
Date.now(),
|
||||
setTimeZone,
|
||||
DateFormat.YEAR_FIRST,
|
||||
)}`,
|
||||
value: DateFormat.YEAR_FIRST,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { formatInTimeZone } from 'date-fns-tz';
|
||||
|
||||
import { TimeFormat } from '@/localization/constants/TimeFormat';
|
||||
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
type DateTimeSettingsTimeFormatSelectProps = {
|
||||
value: TimeFormat;
|
||||
onChange: (nextValue: TimeFormat) => void;
|
||||
timeZone: string;
|
||||
};
|
||||
|
||||
export const DateTimeSettingsTimeFormatSelect = ({
|
||||
onChange,
|
||||
timeZone,
|
||||
value,
|
||||
}: DateTimeSettingsTimeFormatSelectProps) => {
|
||||
const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone;
|
||||
return (
|
||||
<Select
|
||||
dropdownId="datetime-settings-time-format"
|
||||
dropdownWidth={218}
|
||||
label="Time format"
|
||||
fullWidth
|
||||
value={value}
|
||||
options={[
|
||||
{
|
||||
label: 'System settings',
|
||||
value: TimeFormat.SYSTEM,
|
||||
},
|
||||
{
|
||||
label: `24h (${formatInTimeZone(
|
||||
Date.now(),
|
||||
setTimeZone,
|
||||
TimeFormat.HOUR_24,
|
||||
)})`,
|
||||
value: TimeFormat.HOUR_24,
|
||||
},
|
||||
{
|
||||
label: `12h (${formatInTimeZone(
|
||||
Date.now(),
|
||||
setTimeZone,
|
||||
TimeFormat.HOUR_12,
|
||||
)})`,
|
||||
value: TimeFormat.HOUR_12,
|
||||
},
|
||||
]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
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 DateTimeSettingsTimeZoneSelectProps = {
|
||||
value?: string;
|
||||
onChange: (nextValue: string) => void;
|
||||
};
|
||||
|
||||
export const DateTimeSettingsTimeZoneSelect = ({
|
||||
value = detectTimeZone(),
|
||||
onChange,
|
||||
}: DateTimeSettingsTimeZoneSelectProps) => {
|
||||
return (
|
||||
<Select
|
||||
dropdownId="settings-accounts-calendar-time-zone"
|
||||
dropdownWidth={416}
|
||||
label="Time zone"
|
||||
fullWidth
|
||||
value={
|
||||
value === 'system'
|
||||
? 'System settings'
|
||||
: findAvailableTimeZoneOption(value)?.value
|
||||
}
|
||||
options={[
|
||||
{ label: 'System settings', value: 'system' },
|
||||
...AVAILABLE_TIMEZONE_OPTIONS,
|
||||
]}
|
||||
onChange={onChange}
|
||||
withSearchInput
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -6,6 +6,7 @@ import { ColorSchemePicker } from '@/ui/input/color-scheme/components/ColorSchem
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
||||
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
|
||||
|
||||
const StyledH1Title = styled(H1Title)`
|
||||
margin-bottom: 0;
|
||||
@ -22,6 +23,13 @@ export const SettingsAppearance = () => {
|
||||
<H2Title title="Theme" />
|
||||
<ColorSchemePicker value={colorScheme} onChange={setColorScheme} />
|
||||
</Section>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Date and time"
|
||||
description="Configure how dates are displayed across the app"
|
||||
/>
|
||||
<DateTimeSettings />
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
@ -1,5 +1,7 @@
|
||||
import { TimelineCalendarEvent } from '~/generated-metadata/graphql';
|
||||
import { CalendarChannelVisibility } from '~/generated/graphql';
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
TimelineCalendarEvent,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const mockedTimelineCalendarEvents: TimelineCalendarEvent[] = [
|
||||
{
|
||||
|
||||
@ -151,7 +151,7 @@ const getMonthLabels = () => {
|
||||
.map((date) => formatter.format(date));
|
||||
};
|
||||
|
||||
const getMonthLabelsMemoized = moize(getMonthLabels);
|
||||
export const getMonthLabelsMemoized = moize(getMonthLabels);
|
||||
|
||||
export const formatISOStringToHumanReadableDateTime = (date: string) => {
|
||||
const monthLabels = getMonthLabelsMemoized();
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
describe('formatToHumanReadableMonth', () => {
|
||||
it('should format the date to a human-readable month', () => {
|
||||
const date = new Date('2022-01-01');
|
||||
const result = formatToHumanReadableMonth(date);
|
||||
const result = formatToHumanReadableMonth(date, 'UTC');
|
||||
expect(result).toBe('Jan');
|
||||
});
|
||||
});
|
||||
@ -15,7 +15,7 @@ describe('formatToHumanReadableMonth', () => {
|
||||
describe('formatToHumanReadableDay', () => {
|
||||
it('should format the date to a human-readable day', () => {
|
||||
const date = new Date('2022-01-01');
|
||||
const result = formatToHumanReadableDay(date);
|
||||
const result = formatToHumanReadableDay(date, 'UTC');
|
||||
expect(result).toBe('1');
|
||||
});
|
||||
});
|
||||
@ -23,7 +23,7 @@ describe('formatToHumanReadableDay', () => {
|
||||
describe('formatToHumanReadableTime', () => {
|
||||
it('should format the date to a human-readable time', () => {
|
||||
const date = new Date('2022-01-01T12:30:00');
|
||||
const result = formatToHumanReadableTime(date);
|
||||
const result = formatToHumanReadableTime(date, 'UTC');
|
||||
// it seems when running locally on MacOS the space is not the same
|
||||
expect(['12:30 PM', '12:30 PM']).toContain(result);
|
||||
});
|
||||
|
||||
@ -1,26 +1,38 @@
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
export const formatToHumanReadableMonth = (date: Date | string) => {
|
||||
export const formatToHumanReadableMonth = (
|
||||
date: Date | string,
|
||||
timeZone: string,
|
||||
) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
timeZone: timeZone,
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
export const formatToHumanReadableDay = (date: Date | string) => {
|
||||
export const formatToHumanReadableDay = (
|
||||
date: Date | string,
|
||||
timeZone: string,
|
||||
) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
day: 'numeric',
|
||||
timeZone: timeZone,
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
export const formatToHumanReadableTime = (date: Date | string) => {
|
||||
export const formatToHumanReadableTime = (
|
||||
date: Date | string,
|
||||
timeZone: string,
|
||||
) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZone: timeZone,
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import { join as joinPath } from 'path';
|
||||
|
||||
import { kebabCase } from 'src/utils/kebab-case';
|
||||
|
||||
@ -23,7 +24,7 @@ export class CommandLogger {
|
||||
fileName: string,
|
||||
data: unknown,
|
||||
append = false,
|
||||
): Promise<void> {
|
||||
): Promise<string> {
|
||||
const path = `./logs/${kebabCase(this.className)}`;
|
||||
|
||||
if (existsSync(path) === false) {
|
||||
@ -31,17 +32,20 @@ export class CommandLogger {
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
`${path}/${fileName}.json`,
|
||||
JSON.stringify(data, null, 2),
|
||||
{
|
||||
flag: append ? 'a' : 'w',
|
||||
},
|
||||
);
|
||||
const logFilePath = `${path}/${fileName}.json`;
|
||||
|
||||
await fs.writeFile(logFilePath, JSON.stringify(data, null, 2), {
|
||||
flag: append ? 'a' : 'w',
|
||||
});
|
||||
|
||||
const absoluteLogFilePath = joinPath(process.cwd(), logFilePath);
|
||||
|
||||
return absoluteLogFilePath;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error writing to file ${path}/${fileName}.json: ${err?.message}`,
|
||||
);
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,10 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import {
|
||||
WorkspaceMemberDateFormatEnum,
|
||||
WorkspaceMemberTimeFormatEnum,
|
||||
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@ObjectType('FullName')
|
||||
export class FullName {
|
||||
@ -27,6 +31,15 @@ export class WorkspaceMember {
|
||||
@Field({ nullable: true })
|
||||
avatarUrl: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
@Field({ nullable: true })
|
||||
locale: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
timeZone: string;
|
||||
|
||||
@Field(() => WorkspaceMemberDateFormatEnum, { nullable: true })
|
||||
dateFormat: WorkspaceMemberDateFormatEnum;
|
||||
|
||||
@Field(() => WorkspaceMemberTimeFormatEnum, { nullable: true })
|
||||
timeFormat: WorkspaceMemberTimeFormatEnum;
|
||||
}
|
||||
|
||||
@ -70,6 +70,9 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
firstName: workspaceMembers[0].nameFirstName,
|
||||
lastName: workspaceMembers[0].nameLastName,
|
||||
};
|
||||
userWorkspaceMember.timeZone = workspaceMembers[0].timeZone;
|
||||
userWorkspaceMember.dateFormat = workspaceMembers[0].dateFormat;
|
||||
userWorkspaceMember.timeFormat = workspaceMembers[0].timeFormat;
|
||||
|
||||
return userWorkspaceMember;
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { WorkspaceHealthMode } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface';
|
||||
import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface';
|
||||
import { WorkspaceHealthMode } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface';
|
||||
|
||||
import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
|
||||
import { CommandLogger } from 'src/command/command-logger';
|
||||
import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
|
||||
|
||||
interface WorkspaceHealthCommandOptions {
|
||||
workspaceId: string;
|
||||
@ -48,13 +48,20 @@ export class WorkspaceHealthCommand extends CommandRunner {
|
||||
chalk.red(`Workspace is not healthy, found ${issues.length} issues`),
|
||||
);
|
||||
|
||||
if (options.dryRun) {
|
||||
await this.commandLogger.writeLog(
|
||||
`workspace-health-issues-${options.workspaceId}`,
|
||||
issues,
|
||||
for (let issueIndex = 0; issueIndex < issues.length; issueIndex++) {
|
||||
this.logger.log(
|
||||
chalk.red(`Issue #${issueIndex + 1} : ${issues[issueIndex].message}`),
|
||||
);
|
||||
this.logger.log(chalk.yellow('Issues written to log'));
|
||||
}
|
||||
|
||||
const logFilePath = await this.commandLogger.writeLog(
|
||||
`workspace-health-issues-${options.workspaceId}`,
|
||||
issues,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
chalk.yellow(`Issues written to log at : ${logFilePath}`),
|
||||
);
|
||||
}
|
||||
|
||||
if (options.fix) {
|
||||
|
||||
@ -369,6 +369,9 @@ export const WORKSPACE_MEMBER_STANDARD_FIELD_IDS = {
|
||||
calendarEventParticipants: '20202020-0dbc-4841-9ce1-3e793b5b3512',
|
||||
timelineActivities: '20202020-e15b-47b8-94fe-8200e3c66615',
|
||||
auditLogs: '20202020-2f54-4739-a5e2-99563385e83d',
|
||||
timeZone: '20202020-2d33-4c21-a86e-5943b050dd54',
|
||||
dateFormat: '20202020-af13-4e11-b1e7-b8cf5ea13dc0',
|
||||
timeFormat: '20202020-8acb-4cf8-a851-a6ed443c8d81',
|
||||
};
|
||||
|
||||
export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/full-name.composite-type';
|
||||
@ -27,6 +29,30 @@ import { MessageParticipantWorkspaceEntity } from 'src/modules/messaging/common/
|
||||
import { AuditLogWorkspaceEntity } from 'src/modules/timeline/standard-objects/audit-log.workspace-entity';
|
||||
import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||
|
||||
export enum WorkspaceMemberDateFormatEnum {
|
||||
SYSTEM = 'SYSTEM',
|
||||
MONTH_FIRST = 'MONTH_FIRST',
|
||||
DAY_FIRST = 'DAY_FIRST',
|
||||
YEAR_FIRST = 'YEAR_FIRST',
|
||||
}
|
||||
|
||||
export enum WorkspaceMemberTimeFormatEnum {
|
||||
SYSTEM = 'SYSTEM',
|
||||
HOUR_12 = 'HOUR_12',
|
||||
HOUR_24 = 'HOUR_24',
|
||||
}
|
||||
|
||||
registerEnumType(WorkspaceMemberTimeFormatEnum, {
|
||||
name: 'WorkspaceMemberTimeFormatEnum',
|
||||
description: 'Time time as Military, Standard or system as default',
|
||||
});
|
||||
|
||||
registerEnumType(WorkspaceMemberDateFormatEnum, {
|
||||
name: 'WorkspaceMemberDateFormatEnum',
|
||||
description:
|
||||
'Date format as Month first, Day first, Year first or system as default',
|
||||
});
|
||||
|
||||
@WorkspaceEntity({
|
||||
standardId: STANDARD_OBJECT_IDS.workspaceMember,
|
||||
namePlural: 'workspaceMembers',
|
||||
@ -242,4 +268,80 @@ export class WorkspaceMemberWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
@WorkspaceIsNullable()
|
||||
@WorkspaceIsSystem()
|
||||
auditLogs: Relation<AuditLogWorkspaceEntity[]>;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.timeZone,
|
||||
type: FieldMetadataType.TEXT,
|
||||
label: 'Time zone',
|
||||
defaultValue: "'system'",
|
||||
description: 'User time zone',
|
||||
icon: 'IconTimezone',
|
||||
})
|
||||
timeZone: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.dateFormat,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Date format',
|
||||
description: "User's preferred date format",
|
||||
icon: 'IconCalendarEvent',
|
||||
options: [
|
||||
{
|
||||
value: WorkspaceMemberDateFormatEnum.SYSTEM,
|
||||
label: 'System',
|
||||
position: 0,
|
||||
color: 'turquoise',
|
||||
},
|
||||
{
|
||||
value: WorkspaceMemberDateFormatEnum.MONTH_FIRST,
|
||||
label: 'Month First',
|
||||
position: 1,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: WorkspaceMemberDateFormatEnum.DAY_FIRST,
|
||||
label: 'Day First',
|
||||
position: 2,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
value: WorkspaceMemberDateFormatEnum.YEAR_FIRST,
|
||||
label: 'Year First',
|
||||
position: 3,
|
||||
color: 'sky',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${WorkspaceMemberDateFormatEnum.SYSTEM}'`,
|
||||
})
|
||||
dateFormat: string;
|
||||
|
||||
@WorkspaceField({
|
||||
standardId: WORKSPACE_MEMBER_STANDARD_FIELD_IDS.timeFormat,
|
||||
type: FieldMetadataType.SELECT,
|
||||
label: 'Time format',
|
||||
description: "User's preferred time format",
|
||||
icon: 'IconClock2',
|
||||
options: [
|
||||
{
|
||||
value: WorkspaceMemberTimeFormatEnum.SYSTEM,
|
||||
label: 'System',
|
||||
position: 0,
|
||||
color: 'sky',
|
||||
},
|
||||
{
|
||||
value: WorkspaceMemberTimeFormatEnum.HOUR_24,
|
||||
label: '24HRS',
|
||||
position: 1,
|
||||
color: 'red',
|
||||
},
|
||||
{
|
||||
value: WorkspaceMemberTimeFormatEnum.HOUR_12,
|
||||
label: '12HRS',
|
||||
position: 2,
|
||||
color: 'purple',
|
||||
},
|
||||
],
|
||||
defaultValue: `'${WorkspaceMemberTimeFormatEnum.SYSTEM}'`,
|
||||
})
|
||||
timeFormat: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user