From c0e6fb6fdbb47c640c2561e8cc54fd112f6b947e Mon Sep 17 00:00:00 2001 From: Shashank Suman <103516291+SShanks451@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:06:44 +0530 Subject: [PATCH 1/8] added left padding in filter chip (#7800) Fixes: #7779 --------- Co-authored-by: Shashank Suman --- .../src/modules/views/components/SortOrFilterChip.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 1b55e6a32790..55c2a77f3c59 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -40,6 +40,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>` font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; padding: ${({ theme }) => theme.spacing(0.5) + ' ' + theme.spacing(2)}; + margin-left: ${({ theme }) => theme.spacing(2)}; user-select: none; white-space: nowrap; From a45d3148ac372c7e7ec8b7c3a595455eb25201ed Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:07:03 +0530 Subject: [PATCH 2/8] fix: Blocklist table optimised for all viewports (#7618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - This PR fixes the issue #7549 - Optimised blocktable for all viewports ## Changes - Screenshot 2024-10-12 at 5 11 11 PM https://github.com/user-attachments/assets/d5fa063d-2819-4a9d-a9b2-e3ceefe65c8d --------- Co-authored-by: Charles Bochet --- .../components/SettingsAccountsBlocklistTable.tsx | 8 +++++--- .../components/SettingsAccountsBlocklistTableRow.tsx | 12 +++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx index a4c9f5306fad..3d513dc1eff4 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx @@ -1,11 +1,10 @@ -import styled from '@emotion/styled'; - import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; +import styled from '@emotion/styled'; type SettingsAccountsBlocklistTableProps = { blocklist: BlocklistItem[]; @@ -28,7 +27,10 @@ export const SettingsAccountsBlocklistTable = ({ <> {blocklist.length > 0 && ( - + Email/Domain Added to blocklist diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx index 9a1148447a17..30cf3a37a313 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx @@ -1,4 +1,4 @@ -import { IconX } from 'twenty-ui'; +import { IconX, OverflowingTextWithTooltip } from 'twenty-ui'; import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { IconButton } from '@/ui/input/button/components/IconButton'; @@ -16,8 +16,14 @@ export const SettingsAccountsBlocklistTableRow = ({ onRemove, }: SettingsAccountsBlocklistTableRowProps) => { return ( - - {blocklistItem.handle} + + + + {blocklistItem.createdAt ? formatToHumanReadableDate(blocklistItem.createdAt) From 249c7324a2c3aed53e777db999549b3d2cd371df Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Thu, 17 Oct 2024 22:40:30 +0200 Subject: [PATCH 3/8] Improve error message for Graphql API (#7805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![CleanShot 2024-10-17 at 11 39 39](https://github.com/user-attachments/assets/616b8317-de1f-4b61-b2b4-980b14b09f66) This improves this error message. --------- Co-authored-by: Félix Malfait --- .../src/engine/decorators/auth/auth-user.decorator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts index 75f3a982e949..35d3ccc08da4 100644 --- a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts +++ b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts @@ -15,7 +15,9 @@ export const AuthUser = createParamDecorator( const request = getRequest(ctx); if (!options?.allowUndefined && !request.user) { - throw new ForbiddenException("You're not authorized to do this"); + throw new ForbiddenException( + "You're not authorized to do this. Note: This endpoint requires a user and won't work with just an API key.", + ); } return request.user; From 6f5dc1c924773606a4520066c3bee3181331852e Mon Sep 17 00:00:00 2001 From: Syed Hamza Hussain <96618778+SyedHamzaHussain000@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:55:50 +0500 Subject: [PATCH 4/8] Bug Fix: created new div and p tag styles and wrap it on the workspace member as container (#7581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hello, Hope you are doing well.I created a special style for the text to make sure it stays in one line and wont exceed the width if the text width will be more then 80px it will ecplise and set ... at the end of the text. I created these 2 styles variables and wrap my text in these styles StyledObjectSummary StyledEllipsisParagraph Fixes #7574 #Screens Shots Screenshot 2024-10-10 at 10 58 04 PM Screenshot 2024-10-10 at 10 58 20 PM --------- Co-authored-by: Charles Bochet --- .../SettingsDataModelFieldSettingsFormCard.tsx | 4 +++- ...sDataModelFieldRelationSettingsFormCard.tsx | 1 - .../SettingsDataModelFieldPreviewCard.tsx | 1 - .../objects/SettingsDataModelObjectSummary.tsx | 18 +++++++++++------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index 9b50515b10b9..ca1ef939abdb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -129,7 +129,9 @@ export const SettingsDataModelFieldSettingsFormCard = ({ fieldMetadataItem, objectMetadataItem, }: SettingsDataModelFieldSettingsFormCardProps) => { - if (!previewableTypes.includes(fieldMetadataItem.type)) return null; + if (!previewableTypes.includes(fieldMetadataItem.type)) { + return null; + } if (fieldMetadataItem.type === FieldMetadataType.Boolean) { return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx index 0372ba071417..59bee20064ea 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx @@ -23,7 +23,6 @@ type SettingsDataModelFieldRelationSettingsFormCardProps = { } & Pick; const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; flex: 1 1 100%; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx index 06684997a4b2..360c9a300692 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx @@ -19,7 +19,6 @@ const StyledCard = styled(Card)` `; const StyledCardContent = styled(CardContent)` - display: grid; padding: ${({ theme }) => theme.spacing(2)}; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx index f6b8aaa0ab54..931fb6990e09 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useIcons } from 'twenty-ui'; +import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; @@ -14,15 +14,17 @@ export type SettingsDataModelObjectSummaryProps = { const StyledObjectSummary = styled.div` align-items: center; display: flex; - gap: ${({ theme }) => theme.spacing(2)}; justify-content: space-between; `; const StyledObjectName = styled.div` - align-items: center; display: flex; - font-weight: ${({ theme }) => theme.font.weight.medium}; - gap: ${({ theme }) => theme.spacing(1)}; + gap: ${({ theme }) => theme.spacing(2)}; + max-width: 60%; +`; + +const StyledIconContainer = styled.div` + flex-shrink: 0; `; export const SettingsDataModelObjectSummary = ({ @@ -38,8 +40,10 @@ export const SettingsDataModelObjectSummary = ({ return ( - - {objectMetadataItem.labelPlural} + + + + From 8f7ca6a0e32d2ad3d77a42a645906af772861dc9 Mon Sep 17 00:00:00 2001 From: Pushpender <129095696+Pushpender1122@users.noreply.github.com> Date: Fri, 18 Oct 2024 03:51:57 +0530 Subject: [PATCH 5/8] Fix Google Auth displays Status: 401 on screen (#7659) When the user presses the cancel button, the server sends the following response: ![image](https://github.com/user-attachments/assets/cb68cf01-b32c-4680-a811-cd917db88ca9) {"statusCode": 401, "message": "Unauthorized"} Now, when the user clicks the cancel button, they are redirected to the home page for login. Related Issue Fixes #7584 --------- Co-authored-by: Charles Bochet --- .../core-modules/auth/auth.exception.ts | 1 + .../controllers/google-auth.controller.ts | 2 ++ .../filters/auth-oauth-exception.filter.ts | 34 +++++++++++++++++++ .../auth/guards/google-oauth.guard.ts | 12 +++++++ .../src/utils/apply-cors-to-exceptions.ts | 4 +-- 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 2387ff9d310a..62b215f2691b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -16,4 +16,5 @@ export enum AuthExceptionCode { UNAUTHENTICATED = 'UNAUTHENTICATED', INVALID_DATA = 'INVALID_DATA', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED', } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 6ae9b11d7429..c674569d43bf 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -9,6 +9,7 @@ import { import { Response } from 'express'; +import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard'; @@ -33,6 +34,7 @@ export class GoogleAuthController { @Get('redirect') @UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard) + @UseFilters(AuthOAuthExceptionFilter) async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) { const { firstName, diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts new file mode 100644 index 000000000000..008e7d11032f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -0,0 +1,34 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + InternalServerErrorException, +} from '@nestjs/common'; + +import { Response } from 'express'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Catch(AuthException) +export class AuthOAuthExceptionFilter implements ExceptionFilter { + constructor(private readonly environmentService: EnvironmentService) {} + + catch(exception: AuthException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + switch (exception.code) { + case AuthExceptionCode.OAUTH_ACCESS_DENIED: + response + .status(403) + .redirect(this.environmentService.get('FRONT_BASE_URL')); + break; + default: + throw new InternalServerErrorException(exception.message); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index dd9fbf17f2c0..f4675888b2e8 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -1,6 +1,11 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { constructor() { @@ -14,6 +19,13 @@ export class GoogleOauthGuard extends AuthGuard('google') { const workspaceInviteHash = request.query.inviteHash; const workspacePersonalInviteToken = request.query.inviteToken; + if (request.query.error === 'access_denied') { + throw new AuthException( + 'Google OAuth access denied', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } + if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { request.params.workspaceInviteHash = workspaceInviteHash; } diff --git a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts index 0dd3a5cc12ef..eba73f8e0571 100644 --- a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts +++ b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts @@ -1,7 +1,7 @@ import { - ExceptionFilter, - Catch, ArgumentsHost, + Catch, + ExceptionFilter, HttpException, } from '@nestjs/common'; From f6c094a56fa1c63d937c03215c6eef27417fcbbb Mon Sep 17 00:00:00 2001 From: Hitarth Sheth <133380930+Hitarthsheth07@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:49:42 -0400 Subject: [PATCH 6/8] [FIX] fix navigation overflow (#7795) FIX #7733 Fixes the overflow and responsive problem on large and small devices. ![image](https://github.com/user-attachments/assets/6cd8b33f-a52f-4452-b161-9c84ebbb4cce) ![image](https://github.com/user-attachments/assets/c8c0386f-e2a2-4f96-a06e-7e37f54c0564) The 'Workspace' title is fixed and only links under it are scrolled when overflown. --------- Co-authored-by: Lucas Bordeau --- .../components/WorkspaceFavorites.tsx | 7 +- ...igationDrawerItemForObjectMetadataItem.tsx | 84 ++++++++++ .../NavigationDrawerOpenedSection.tsx | 5 - ...ionDrawerSectionForObjectMetadataItems.tsx | 154 ++++++------------ ...erSectionForObjectMetadataItemsWrapper.tsx | 6 - .../components/NavigationDrawer.tsx | 9 +- ...avigationDrawerAnimatedCollapseWrapper.tsx | 6 +- .../components/NavigationDrawerItem.tsx | 1 - .../components/NavigationDrawerSection.tsx | 2 + .../scroll/contexts/ScrollWrapperContexts.tsx | 7 +- 10 files changed, 157 insertions(+), 124 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx diff --git a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx index cf106211405b..b975799fd499 100644 --- a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx @@ -2,17 +2,13 @@ import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigatio import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const WorkspaceFavorites = () => { - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); - const { activeObjectMetadataItems: objectMetadataItemsToDisplay } = useFilteredObjectMetadataItemsForWorkspaceFavorites(); const loading = useIsPrefetchLoading(); + if (loading) { return ; } @@ -21,7 +17,6 @@ export const WorkspaceFavorites = () => { ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx new file mode 100644 index 000000000000..8c7f1e3ceda2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -0,0 +1,84 @@ +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; +import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; +import { View } from '@/views/types/View'; +import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; +import { useLocation } from 'react-router-dom'; +import { useIcons } from 'twenty-ui'; + +export type NavigationDrawerItemForObjectMetadataItemProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const NavigationDrawerItemForObjectMetadataItem = ({ + objectMetadataItem, +}: NavigationDrawerItemForObjectMetadataItemProps) => { + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + + const objectMetadataViews = getObjectMetadataItemViews( + objectMetadataItem.id, + views, + ); + + const { getIcon } = useIcons(); + const currentPath = useLocation().pathname; + const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); + + const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( + objectMetadataItem.id, + ); + + const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; + + const navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; + + const isActive = currentPath === `/objects/${objectMetadataItem.namePlural}`; + const shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1; + + const sortedObjectMetadataViews = [...objectMetadataViews].sort( + (viewA, viewB) => + viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + ); + + const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( + (view) => viewId === view.id, + ); + + const subItemArrayLength = sortedObjectMetadataViews.length; + + return ( + + + {shouldSubItemsBeDisplayed && + sortedObjectMetadataViews.map((view, index) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx index fb17b643078f..b17ad4c310e2 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx @@ -5,9 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerOpenedSection = () => { const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -15,7 +12,6 @@ export const NavigationDrawerOpenedSection = () => { (item) => !item.isRemote, ); - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); const currentObjectNamePlural = useParams().objectNamePlural; @@ -49,7 +45,6 @@ export const NavigationDrawerOpenedSection = () => { ) diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx index 5e666e0cf542..9960670d1beb 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx @@ -1,18 +1,13 @@ -import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; -import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; +import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; -import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; -import { View } from '@/views/types/View'; -import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; -import { useLocation } from 'react-router-dom'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; -import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; -import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; + const ORDERED_STANDARD_OBJECTS = [ 'person', 'company', @@ -21,111 +16,59 @@ const ORDERED_STANDARD_OBJECTS = [ 'note', ]; +const StyledObjectsMetaDataItemsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.betweenSiblingsGap}; + width: 100%; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex: 1; + overflow-y: auto; +`; + export const NavigationDrawerSectionForObjectMetadataItems = ({ sectionTitle, isRemote, - views, objectMetadataItems, }: { sectionTitle: string; isRemote: boolean; - views: View[]; objectMetadataItems: ObjectMetadataItem[]; }) => { const { toggleNavigationSection, isNavigationSectionOpenState } = useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); - const { getIcon } = useIcons(); - const currentPath = useLocation().pathname; - const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); - - const renderObjectMetadataItems = () => { - return [ - ...objectMetadataItems - .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - const indexA = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemA.nameSingular, - ); - const indexB = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemB.nameSingular, - ); - if (indexA === -1 || indexB === -1) { - return objectMetadataItemA.nameSingular.localeCompare( - objectMetadataItemB.nameSingular, - ); - } - return indexA - indexB; - }), - ...objectMetadataItems - .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - return new Date(objectMetadataItemA.createdAt) < - new Date(objectMetadataItemB.createdAt) - ? 1 - : -1; - }), - ].map((objectMetadataItem) => { - const objectMetadataViews = getObjectMetadataItemViews( - objectMetadataItem.id, - views, - ); - const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( - objectMetadataItem.id, - ); - const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; - - const navigationPath = `/objects/${objectMetadataItem.namePlural}${ - viewId ? `?view=${viewId}` : '' - }`; - - const isActive = - currentPath === `/objects/${objectMetadataItem.namePlural}`; - const shouldSubItemsBeDisplayed = - isActive && objectMetadataViews.length > 1; - - const sortedObjectMetadataViews = [...objectMetadataViews].sort( - (viewA, viewB) => - viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + const sortedStandardObjectMetadataItems = [...objectMetadataItems] + .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + const indexA = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemA.nameSingular, ); - - const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( - (view) => viewId === view.id, + const indexB = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemB.nameSingular, ); + if (indexA === -1 || indexB === -1) { + return objectMetadataItemA.nameSingular.localeCompare( + objectMetadataItemB.nameSingular, + ); + } + return indexA - indexB; + }); - const subItemArrayLength = sortedObjectMetadataViews.length; - - return ( - - - {shouldSubItemsBeDisplayed && - sortedObjectMetadataViews.map((view, index) => ( - - ))} - - ); + const sortedCustomObjectMetadataItems = [...objectMetadataItems] + .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + return new Date(objectMetadataItemA.createdAt) < + new Date(objectMetadataItemB.createdAt) + ? 1 + : -1; }); - }; + + const objectMetadataItemsForNavigationItems = [ + ...sortedStandardObjectMetadataItems, + ...sortedCustomObjectMetadataItems, + ]; return ( objectMetadataItems.length > 0 && ( @@ -136,7 +79,18 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ onClick={() => toggleNavigationSection()} /> - {isNavigationSectionOpen && renderObjectMetadataItems()} + + + {isNavigationSectionOpen && + objectMetadataItemsForNavigationItems.map( + (objectMetadataItem) => ( + + ), + )} + + ) ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx index 2127db1fc604..91a22ca5ab1e 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx @@ -6,9 +6,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ isRemote, @@ -21,8 +18,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( (item) => (isRemote ? item.isRemote : !item.isRemote), ); - - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); if (loading && isDefined(currentUser)) { @@ -33,7 +28,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 8fbd853a56ed..33e9ef152708 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -9,10 +9,10 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; +import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; import { isNavigationDrawerExpandedState } from '../../states/isNavigationDrawerExpanded'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerHeader } from './NavigationDrawerHeader'; -import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; export type NavigationDrawerProps = { children: ReactNode; @@ -22,7 +22,10 @@ export type NavigationDrawerProps = { title?: string; }; -const StyledAnimatedContainer = styled(motion.div)``; +const StyledAnimatedContainer = styled(motion.div)` + max-height: 100vh; + overflow: hidden; +`; const StyledContainer = styled.div<{ isSettings?: boolean; @@ -51,6 +54,8 @@ const StyledItemsContainer = styled.div` display: flex; flex-direction: column; margin-bottom: auto; + overflow: hidden; + flex: 1; `; export const NavigationDrawer = ({ diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx index 19575ff0d483..3a4d42541d26 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx @@ -1,9 +1,9 @@ +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimationControls, motion, TargetAndTransition } from 'framer-motion'; import { useRecoilValue } from 'recoil'; -import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; -import { useTheme } from '@emotion/react'; -import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; const StyledAnimatedContainer = styled(motion.div)``; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index d98fc547091c..65944df37e5d 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -142,7 +142,6 @@ const StyledKeyBoardShortcut = styled.div` const StyledNavigationDrawerItemContainer = styled.div` display: flex; - flex-grow: 1; width: 100%; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx index 2ba98503329b..afc7fe803744 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx @@ -6,6 +6,8 @@ const StyledSection = styled.div` gap: ${({ theme }) => theme.betweenSiblingsGap}; width: 100%; margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex-shrink: 1; + overflow: hidden; `; export { StyledSection as NavigationDrawerSection }; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx index b6f7d2103a60..1d83d1ddd94b 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx @@ -17,7 +17,8 @@ export type ContextProviderName = | 'tabList' | 'releases' | 'test' - | 'showPageActivityContainer'; + | 'showPageActivityContainer' + | 'navigationDrawer'; const createScrollWrapperContext = (id: string) => createContext({ @@ -47,6 +48,8 @@ export const ReleasesScrollWrapperContext = createScrollWrapperContext('releases'); export const ShowPageActivityContainerScrollWrapperContext = createScrollWrapperContext('showPageActivityContainer'); +export const NavigationDrawerScrollWrapperContext = + createScrollWrapperContext('navigationDrawer'); export const TestScrollWrapperContext = createScrollWrapperContext('test'); export const getContextByProviderName = ( @@ -77,6 +80,8 @@ export const getContextByProviderName = ( return TestScrollWrapperContext; case 'showPageActivityContainer': return ShowPageActivityContainerScrollWrapperContext; + case 'navigationDrawer': + return NavigationDrawerScrollWrapperContext; default: throw new Error('Context Provider not available'); } From 0c24001e23060f112d81d41c97eb5ceca6aa52d6 Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 18 Oct 2024 10:20:21 +0200 Subject: [PATCH 7/8] Fix update event webhook triggering (#7814) --- .../graphql-query-runner.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 7eb55d60ae95..2330ec472815 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -361,19 +361,21 @@ export class GraphqlQueryRunnerService { authContext.workspace.id, ); + const resultWithGettersArray = Array.isArray(resultWithGetters) + ? resultWithGetters + : [resultWithGetters]; + await this.workspaceQueryHookService.executePostQueryHooks( authContext, objectMetadataItem.nameSingular, operationName, - Array.isArray(resultWithGetters) - ? resultWithGetters - : [resultWithGetters], + resultWithGettersArray, ); const jobOperation = this.operationNameToJobOperation(operationName); if (jobOperation) { - await this.triggerWebhooks(resultWithGetters, jobOperation, options); + await this.triggerWebhooks(resultWithGettersArray, jobOperation, options); } return resultWithGetters; From 8cadcdf577f505c107de31dc73a8d25bf21f8274 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:00:21 +0200 Subject: [PATCH 8/8] add dynamic dates for webhookGraphDataUsage (#7720) **Before:** Only last 5 days where displayed on Developers Settings Webhook Usage Graph. ![image](https://github.com/user-attachments/assets/7b7f2e6b-9637-489e-a7a7-5a3cb70525aa) **Now** Added component where you can select the time range where you want to view the webhook usage. To do better the styling and content depassing . Screenshot 2024-10-15 at 16 56 45 **In order to test** 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_TOKEN to your token from the workspace twenty_analytics_playground 3. Write your client tinybird token in SettingsDeveloppersWebhookDetail.tsx in line 93 4. Create a Webhook in twenty and set wich events it needs to track 5. Run twenty-worker in order to make the webhooks work. 6. Do your tasks in order to populate the data 7. Enter to settings> webhook>your webhook and the statistics section should be displayed. 8. Select the desired time range in the dropdown **To do list** - Tooltip is truncated when accessing values at the right end of the graph - DateTicks needs to follow a more clear standard - Update this PR with more representative images --- .../twenty-front/src/generated/graphql.tsx | 4 +- .../src/modules/auth/hooks/useAuth.ts | 6 +- .../components/ClientConfigProviderEffect.tsx | 9 +- .../graphql/queries/getClientConfig.ts | 1 + .../states/isAnalyticsEnabledState.ts | 6 + .../constants/DateFormatWithoutYear.ts | 11 ++ .../utils/__tests__/detectDateFormat.test.ts | 17 +-- .../utils/__tests__/detectTimeFormat.test.ts | 9 +- ...tDateISOStringToDateTimeSimplified.test.js | 90 +++++++++++ .../localization/utils/detectDateFormat.ts | 10 +- .../localization/utils/detectTimeFormat.ts | 6 +- ...formatDateISOStringToDateTimeSimplified.ts | 18 +++ .../getDateFormatFromWorkspaceDateFormat.ts | 2 +- .../getTimeFormatFromWorkspaceTimeFormat.ts | 2 +- .../SettingsDevelopersWebhookTooltip.tsx | 89 +++++++++++ .../SettingsDevelopersWebhookUsageGraph.tsx | 142 ++++++++++++++++-- ...tingsDevelopersWebhookUsageGraphEffect.tsx | 89 +---------- .../constants/WebhookGraphApiOptionsMap.ts | 6 + .../developers/webhook/hooks/useGraphData.tsx | 23 +++ .../__tests__/fetchGraphDataOrThrow.test.js | 115 ++++++++++++++ .../webhook/utils/fetchGraphDataOrThrow.ts | 80 ++++++++++ .../users/components/UserProviderEffect.tsx | 6 +- .../SettingsDevelopersWebhookDetail.tsx | 10 +- .../components/DateTimeSettings.tsx | 4 +- .../DateTimeSettingsDateFormatSelect.tsx | 2 +- .../DateTimeSettingsTimeFormatSelect.tsx | 2 +- .../client-config/client-config.entity.ts | 3 + .../client-config/client-config.resolver.ts | 1 + 28 files changed, 631 insertions(+), 132 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts create mode 100644 packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts create mode 100644 packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js create mode 100644 packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 5950c81d5cc1..7f053fc6b10f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -141,6 +141,7 @@ export enum CaptchaDriverType { export type ClientConfig = { __typename?: 'ClientConfig'; + analyticsEnabled: Scalars['Boolean']; api: ApiConfig; authProviders: AuthProviders; billing: Billing; @@ -1599,7 +1600,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -2765,6 +2766,7 @@ export const GetClientConfigDocument = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 7a7de0807f1b..ae13d831fb7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -32,6 +32,8 @@ import { import { isDefined } from '~/utils/isDefined'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +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'; @@ -143,12 +145,12 @@ export const useAuth = () => { ? getDateFormatFromWorkspaceDateFormat( user.workspaceMember.dateFormat, ) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(user.workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat( user.workspaceMember.timeFormat, ) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 9eccbeb98e10..ed06d3f0ee69 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,23 +1,24 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; +import { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useGetClientConfigQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const ClientConfigProviderEffect = () => { const setAuthProviders = useSetRecoilState(authProvidersState); const setIsDebugMode = useSetRecoilState(isDebugModeState); + const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); @@ -50,6 +51,7 @@ export const ClientConfigProviderEffect = () => { magicLink: false, }); setIsDebugMode(data?.clientConfig.debugMode); + setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled); setIsSignInPrefilled(data?.clientConfig.signInPrefilled); setIsSignUpDisabled(data?.clientConfig.signUpDisabled); @@ -84,6 +86,7 @@ export const ClientConfigProviderEffect = () => { setCaptchaProvider, setChromeExtensionId, setApiConfig, + setIsAnalyticsEnabled, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index e702acefa4f1..9a060b0d7b2b 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -16,6 +16,7 @@ export const GET_CLIENT_CONFIG = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts new file mode 100644 index 000000000000..50c0f5c89c25 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isAnalyticsEnabledState = createState({ + key: 'isAnalyticsEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts new file mode 100644 index 000000000000..a1c7f2af3b72 --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts @@ -0,0 +1,11 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; + +type DateFormatWithoutYear = { + [K in keyof typeof DateFormat]: string; +}; +export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = { + SYSTEM: 'SYSTEM', + MONTH_FIRST: 'MMM d', + DAY_FIRST: 'd MMM', + YEAR_FIRST: 'MMM d', +}; diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts index 2b641f302a63..b267622bf0cc 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts @@ -1,8 +1,7 @@ -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', () => { + it('should return MONTH_FIRST if the detected format starts with month', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -16,10 +15,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); - it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => { + it('should return DAY_FIRST if the detected format starts with day', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -32,10 +31,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.DAY_FIRST); + expect(result).toBe('DAY_FIRST'); }); - it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => { + it('should return YEAR_FIRST if the detected format starts with year', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -48,10 +47,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.YEAR_FIRST); + expect(result).toBe('YEAR_FIRST'); }); - it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => { + it('should return 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: () => [ @@ -64,6 +63,6 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts index 6433495789ee..9445068a5f7f 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts @@ -1,8 +1,7 @@ -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', () => { + it('should return 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(() => ({ @@ -11,11 +10,11 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_12); + expect(result).toBe('HOUR_12'); expect(mockResolvedOptions).toHaveBeenCalled(); }); - it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => { + it('should return 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(() => ({ @@ -24,7 +23,7 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_24); + expect(result).toBe('HOUR_24'); expect(mockResolvedOptions).toHaveBeenCalled(); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js new file mode 100644 index 000000000000..4caee3aedf0d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js @@ -0,0 +1,90 @@ +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { formatInTimeZone } from 'date-fns-tz'; +// Mock the imported modules +jest.mock('@/localization/utils/detectDateFormat'); +jest.mock('date-fns-tz'); + +describe('formatDateISOStringToDateTimeSimplified', () => { + const mockDate = new Date('2023-08-15T10:30:00Z'); + const mockTimeZone = 'America/New_York'; + const mockTimeFormat = 'HH:mm'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 15 · 06:30'); + }); + + it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => { + detectDateFormat.mockReturnValue('DAY_FIRST'); + formatInTimeZone.mockReturnValue('15 Oct · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'd MMM · HH:mm', + ); + expect(result).toBe('15 Oct · 06:30'); + }); + + it('should use the provided time format', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + 'h:mm aa', + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · h:mm aa', + ); + expect(result).toBe('Oct 15 · 6:30 AM'); + }); + + it('should handle different time zones', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 16 · 02:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + 'Asia/Tokyo', + mockTimeFormat, + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + 'Asia/Tokyo', + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 16 · 02:30'); + }); +}); diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts index b503ef826e60..e38b018df445 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts @@ -1,6 +1,6 @@ import { DateFormat } from '@/localization/constants/DateFormat'; -export const detectDateFormat = (): DateFormat => { +export const detectDateFormat = (): keyof typeof DateFormat => { const date = new Date(); const formatter = new Intl.DateTimeFormat(navigator.language); const parts = formatter.formatToParts(date); @@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => { .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; + if (partOrder[0] === 'month') return 'MONTH_FIRST'; + if (partOrder[0] === 'day') return 'DAY_FIRST'; + if (partOrder[0] === 'year') return 'YEAR_FIRST'; - return DateFormat.MONTH_FIRST; + return 'MONTH_FIRST'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts index 01bad17167a5..d6d914d83637 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts @@ -1,14 +1,14 @@ import { TimeFormat } from '@/localization/constants/TimeFormat'; import { isDefined } from '~/utils/isDefined'; -export const detectTimeFormat = () => { +export const detectTimeFormat = (): keyof typeof TimeFormat => { const isHour12 = Intl.DateTimeFormat(navigator.language, { hour: 'numeric', }).resolvedOptions().hour12; if (isDefined(isHour12) && isHour12) { - return TimeFormat.HOUR_12; + return 'HOUR_12'; } - return TimeFormat.HOUR_24; + return 'HOUR_24'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts new file mode 100644 index 000000000000..c96d9f2f885d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts @@ -0,0 +1,18 @@ +import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatInTimeZone } from 'date-fns-tz'; + +export const formatDateISOStringToDateTimeSimplified = ( + date: Date, + timeZone: string, + timeFormat: TimeFormat, +) => { + const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()]; + + return formatInTimeZone( + date, + timeZone, + `${simplifiedDateFormat} · ${timeFormat}`, + ); +}; diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts index f32bdbb93355..09293fbb8ec8 100644 --- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts @@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = ( ) => { switch (workspaceDateFormat) { case WorkspaceMemberDateFormatEnum.System: - return detectDateFormat(); + return DateFormat[detectDateFormat()]; case WorkspaceMemberDateFormatEnum.MonthFirst: return DateFormat.MONTH_FIRST; case WorkspaceMemberDateFormatEnum.DayFirst: diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts index f6aebb43779b..7519d0cb4068 100644 --- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts @@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = ( ) => { switch (workspaceTimeFormat) { case WorkspaceMemberTimeFormatEnum.System: - return detectTimeFormat(); + return TimeFormat[detectTimeFormat()]; case WorkspaceMemberTimeFormatEnum.Hour_24: return TimeFormat.HOUR_24; case WorkspaceMemberTimeFormatEnum.Hour_12: diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx new file mode 100644 index 000000000000..40925c5d3830 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx @@ -0,0 +1,89 @@ +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { UserContext } from '@/users/contexts/UserContext'; +import styled from '@emotion/styled'; +import { Point } from '@nivo/line'; +import { ReactElement, useContext } from 'react'; + +const StyledTooltipContainer = styled.div` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.md}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + width: 128px; + flex-direction: column; + justify-content: center; + background: ${({ theme }) => theme.background.transparent.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + backdrop-filter: ${({ theme }) => theme.blur.medium}; +`; + +const StyledTooltipDateContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: center; + font-weight: ${({ theme }) => theme.font.weight.medium}; + font-family: ${({ theme }) => theme.font.family}; + gap: ${({ theme }) => theme.spacing(2)}; + color: ${({ theme }) => theme.font.color.secondary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTooltipDataRow = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: space-between; + color: ${({ theme }) => theme.font.color.tertiary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledLine = styled.div` + background-color: ${({ theme }) => theme.border.color.medium}; + height: 1px; + width: 100%; +`; +const StyledColorPoint = styled.div<{ color: string }>` + background-color: ${({ color }) => color}; + border-radius: 50%; + height: 8px; + width: 8px; + display: inline-block; +`; +const StyledDataDefinition = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(2)}; +`; +const StyledSpan = styled.span` + color: ${({ theme }) => theme.font.color.primary}; +`; +type SettingsDevelopersWebhookTooltipProps = { + point: Point; +}; +export const SettingsDevelopersWebhookTooltip = ({ + point, +}: SettingsDevelopersWebhookTooltipProps): ReactElement => { + const { timeFormat, timeZone } = useContext(UserContext); + const windowInterval = new Date(point.data.x); + const windowIntervalDate = formatDateISOStringToDateTimeSimplified( + windowInterval, + timeZone, + timeFormat, + ); + return ( + + + {windowIntervalDate} + + + + + + {String(point.serieId)} + + {String(point.data.y)} + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx index eb2e359fff16..9626c6712eef 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx @@ -1,8 +1,13 @@ +import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip'; +import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData'; import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; +import { Select } from '@/ui/input/components/Select'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ResponsiveLine } from '@nivo/line'; import { Section } from '@react-email/components'; -import { useRecoilValue } from 'recoil'; +import { useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { H2Title } from 'twenty-ui'; export type NivoLineInput = { @@ -14,22 +19,102 @@ export type NivoLineInput = { }>; }; const StyledGraphContainer = styled.div` - height: 200px; - width: 100%; + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + height: 199px; + + padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)}; + width: 496px; +`; +const StyledTitleContainer = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; `; -export const SettingsDeveloppersWebhookUsageGraph = () => { + +type SettingsDevelopersWebhookUsageGraphProps = { + webhookId: string; +}; + +export const SettingsDevelopersWebhookUsageGraph = ({ + webhookId, +}: SettingsDevelopersWebhookUsageGraphProps) => { const webhookGraphData = useRecoilValue(webhookGraphDataState); + const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + const theme = useTheme(); + + const [windowLengthGraphOption, setWindowLengthGraphOption] = useState< + '7D' | '1D' | '12H' | '4H' + >('7D'); + + const { fetchGraphData } = useGraphData(webhookId); return ( <> {webhookGraphData.length ? (
- + + + Boolean) debugMode: boolean; + @Field(() => Boolean) + analyticsEnabled: boolean; + @Field(() => Support) support: Support; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 3615066a4390..f6ba1aaf4abd 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -48,6 +48,7 @@ export class ClientConfigResolver { 'MUTATION_MAXIMUM_AFFECTED_RECORDS', ), }, + analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), }; return Promise.resolve(clientConfig);