From f6bfec882a7ff569fa448b45cd43949904f4efc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Sun, 1 Jun 2025 09:33:16 +0200 Subject: [PATCH] Improve lazy loading (#12393) Creating manual chunk was a bad idea, we should always solve lazy loading problem at the source instance. Setting a 4.5MB for the index bundle size, CI will fail if we go above. There is still a lot of room for optimizations! - More agressive lazy loading (e.g. xyflow and tiptap are still loaded in index!) - Add a prefetch mechanism - Add stronger CI checks to make sure libraries we've set asides are not added back - Fix AllIcons component with does not work as intended (loaded on initial load) --- .../ExportNoteActionSingleRecordAction.tsx | 7 +- .../utils/exportBlockNoteEditorToPdf.ts | 8 +- .../components/ActivityRichTextEditor.tsx | 2 +- .../utils/getActivityAttachmentIdsToDelete.ts | 2 +- .../modules/apollo/services/apollo.factory.ts | 13 +- .../CommandMenuEditRichTextPage.tsx | 35 ++++- .../components/AppErrorBoundary.tsx | 17 ++- .../components/SentryInitEffect.tsx | 103 ++++++++----- .../useObjectNamePluralFromSingular.test.tsx | 9 +- ... useObjectNameSingularFromPlural.test.tsx} | 9 +- .../hooks/useLoadMockedObjectMetadataItems.ts | 7 +- .../hooks/useObjectNamePluralFromSingular.ts | 15 +- .../hooks/useObjectNameSingularFromPlural.ts | 15 +- .../components/FormCountryCodeSelectInput.tsx | 2 +- .../components/RichTextV2FieldDisplay.tsx | 2 +- .../meta-types/hooks/useRichTextField.ts | 2 +- .../hooks/useRichTextFieldDisplay.ts | 2 +- .../meta-types/hooks/useRichTextV2Field.ts | 2 +- .../input/components/RichTextFieldInput.tsx | 35 ++++- .../record-show/components/CardComponents.tsx | 71 ++++++++- .../getFunctionInputFromSourceCode.test.ts | 16 +- .../utils/getFunctionInputFromSourceCode.ts | 8 +- .../components/WorkerMetricsGraph.tsx | 12 +- .../SettingsDataModelFieldPhonesForm.tsx | 2 +- .../SettingsDataModelOverviewEffect.tsx | 143 ++++++++++-------- .../useServerlessFunctionUpdateFormState.ts | 8 +- .../SignInAppNavigationDrawerMock.tsx | 8 +- .../components/SpreadsheetImportProvider.tsx | 39 ++++- .../date/components/InternalDatePicker.tsx | 134 ++++++++++------ .../components/internal/types/Country.ts | 2 +- .../editor/components/CustomSlashMenu.tsx | 2 +- .../getFirstNonEmptyLineOfRichText.test.ts | 2 +- .../utils/getFirstNonEmptyLineOfRichText.ts | 2 +- .../WorkflowEditActionSendEmail.stories.tsx | 1 - .../ObjectMetadataItemsDecorator.tsx | 13 +- packages/twenty-front/vite.config.ts | 94 ++++++++++-- .../enums/database-event-action.ts | 10 +- 37 files changed, 577 insertions(+), 277 deletions(-) rename packages/twenty-front/src/modules/object-metadata/hooks/__tests__/{useObjectNameSingularFromPlural.test.ts => useObjectNameSingularFromPlural.test.tsx} (61%) diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ExportNoteActionSingleRecordAction.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ExportNoteActionSingleRecordAction.tsx index 99726b445..bf9038513 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ExportNoteActionSingleRecordAction.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/components/ExportNoteActionSingleRecordAction.tsx @@ -1,7 +1,6 @@ import { Action } from '@/action-menu/actions/components/Action'; import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { BlockNoteEditor } from '@blocknote/core'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -33,15 +32,11 @@ export const ExportNoteActionSingleRecordAction = () => { console.warn(initialBody); } - const editor = BlockNoteEditor.create({ - initialContent: parsedBody, - }); - const { exportBlockNoteEditorToPdf } = await import( '@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf' ); - await exportBlockNoteEditorToPdf(editor, filename); + await exportBlockNoteEditorToPdf(parsedBody, filename); // TODO later: implement DOCX export // const { exportBlockNoteEditorToDocx } = await import( diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf.ts b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf.ts index 88fb48f42..cc359bdde 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf.ts +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf.ts @@ -1,4 +1,4 @@ -import { BlockNoteEditor } from '@blocknote/core'; +import { BlockNoteEditor, PartialBlock } from '@blocknote/core'; import { PDFExporter, pdfDefaultSchemaMappings, @@ -7,9 +7,13 @@ import { pdf } from '@react-pdf/renderer'; import { saveAs } from 'file-saver'; export const exportBlockNoteEditorToPdf = async ( - editor: BlockNoteEditor, + parsedBody: PartialBlock[], filename: string, ) => { + const editor = BlockNoteEditor.create({ + initialContent: parsedBody, + }); + const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings); const pdfDocument = await exporter.toReactPDFDocument(editor.document); diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx index bc7b9727d..d0d28348e 100644 --- a/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/ActivityRichTextEditor.tsx @@ -34,7 +34,7 @@ import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords'; import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly'; import { BlockEditor } from '@/ui/input/editor/components/BlockEditor'; -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import '@blocknote/core/fonts/inter.css'; import '@blocknote/mantine/style.css'; import { useCreateBlockNote } from '@blocknote/react'; diff --git a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts index e439b2327..2709d7288 100644 --- a/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts +++ b/packages/twenty-front/src/modules/activities/utils/getActivityAttachmentIdsToDelete.ts @@ -2,7 +2,7 @@ import { Attachment } from '@/activities/files/types/Attachment'; export const getActivityAttachmentIdsToDelete = ( newActivityBody: string, - oldActivityAttachments: Attachment[], + oldActivityAttachments: Attachment[] = [], ) => { if (oldActivityAttachments.length === 0) return []; diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index fedf01a11..1b351979e 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -18,7 +18,6 @@ import { AuthTokenPair } from '~/generated/graphql'; import { logDebug } from '~/utils/logDebug'; import { i18n } from '@lingui/core'; -import { captureException } from '@sentry/react'; import { GraphQLFormattedError } from 'graphql'; import { isDefined } from 'twenty-shared/utils'; import { cookieStorage } from '~/utils/cookie-storage'; @@ -160,7 +159,17 @@ export class ApolloFactory implements ApolloManager { }, Path: ${graphQLError.path}`, ); } - captureException(graphQLError); + import('@sentry/react') + .then(({ captureException }) => { + captureException(graphQLError); + }) + .catch((sentryError) => { + // eslint-disable-next-line no-console + console.error( + 'Failed to capture GraphQL error with Sentry:', + sentryError, + ); + }); } } } diff --git a/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx index 55d51a359..50917ee4b 100644 --- a/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx +++ b/packages/twenty-front/src/modules/command-menu/pages/rich-text-page/components/CommandMenuEditRichTextPage.tsx @@ -1,9 +1,18 @@ -import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { lazy, Suspense } from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useRecoilValue } from 'recoil'; import { viewableRichTextComponentState } from '../states/viewableRichTextComponentState'; +const ActivityRichTextEditor = lazy(() => + import('@/activities/components/ActivityRichTextEditor').then((module) => ({ + default: module.ActivityRichTextEditor, + })), +); + const StyledContainer = styled.div` box-sizing: border-box; margin: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(-2)}; @@ -11,6 +20,20 @@ const StyledContainer = styled.div` width: 100%; `; +const LoadingSkeleton = () => { + const theme = useTheme(); + + return ( + + + + ); +}; + export const CommandMenuEditRichTextPage = () => { const { activityId, activityObjectNameSingular } = useRecoilValue( viewableRichTextComponentState, @@ -27,10 +50,12 @@ export const CommandMenuEditRichTextPage = () => { return ( - + }> + + ); }; diff --git a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx index 3e88ab503..86eb81834 100644 --- a/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/AppErrorBoundary.tsx @@ -1,5 +1,4 @@ import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect'; -import { captureException } from '@sentry/react'; import { ErrorInfo, ReactNode } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; @@ -14,11 +13,17 @@ export const AppErrorBoundary = ({ FallbackComponent, resetOnLocationChange = true, }: AppErrorBoundaryProps) => { - const handleError = (error: Error, info: ErrorInfo) => { - captureException(error, (scope) => { - scope.setExtras({ info }); - return scope; - }); + const handleError = async (error: Error, info: ErrorInfo) => { + try { + const { captureException } = await import('@sentry/react'); + captureException(error, (scope) => { + scope.setExtras({ info }); + return scope; + }); + } catch (sentryError) { + // eslint-disable-next-line no-console + console.error('Failed to capture exception with Sentry:', sentryError); + } }; const handleReset = () => { diff --git a/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx index b40f7735a..7b3beab52 100644 --- a/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/SentryInitEffect.tsx @@ -1,9 +1,3 @@ -import { - browserTracingIntegration, - init, - replayIntegration, - setUser, -} from '@sentry/react'; import { isNonEmptyString } from '@sniptt/guards'; import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; @@ -27,38 +21,74 @@ export const SentryInitEffect = () => { const [isSentryUserDefined, setIsSentryUserDefined] = useState(false); useEffect(() => { - if ( - !isSentryInitializing && - isNonEmptyString(sentryConfig?.dsn) && - !isSentryInitialized - ) { - setIsSentryInitializing(true); - init({ - environment: sentryConfig?.environment ?? undefined, - release: sentryConfig?.release ?? undefined, - dsn: sentryConfig?.dsn, - integrations: [browserTracingIntegration({}), replayIntegration()], - tracePropagationTargets: ['localhost:3001', REACT_APP_SERVER_BASE_URL], - tracesSampleRate: 1.0, - replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0, - }); + const initializeSentry = async () => { + if ( + isNonEmptyString(sentryConfig?.dsn) && + !isSentryInitialized && + !isSentryInitializing + ) { + setIsSentryInitializing(true); - setIsSentryInitialized(true); - setIsSentryInitializing(false); - } + try { + const { init, browserTracingIntegration, replayIntegration } = + await import('@sentry/react'); - if (isDefined(currentUser) && !isSentryUserDefined) { - setUser({ - email: currentUser?.email, - id: currentUser?.id, - workspaceId: currentWorkspace?.id, - workspaceMemberId: currentWorkspaceMember?.id, - }); - setIsSentryUserDefined(true); - } else { - setUser(null); - } + init({ + environment: sentryConfig?.environment ?? undefined, + release: sentryConfig?.release ?? undefined, + dsn: sentryConfig?.dsn, + integrations: [browserTracingIntegration({}), replayIntegration()], + tracePropagationTargets: [ + 'localhost:3001', + REACT_APP_SERVER_BASE_URL, + ], + tracesSampleRate: 1.0, + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + }); + + setIsSentryInitialized(true); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to initialize Sentry:', error); + } finally { + setIsSentryInitializing(false); + } + } + }; + + const updateSentryUser = async () => { + if ( + isSentryInitialized && + isDefined(currentUser) && + !isSentryUserDefined + ) { + try { + const { setUser } = await import('@sentry/react'); + setUser({ + email: currentUser?.email, + id: currentUser?.id, + workspaceId: currentWorkspace?.id, + workspaceMemberId: currentWorkspaceMember?.id, + }); + setIsSentryUserDefined(true); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to set Sentry user:', error); + } + } else if (!isDefined(currentUser) && isSentryInitialized) { + try { + const { setUser } = await import('@sentry/react'); + setUser(null); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Failed to clear Sentry user:', error); + } + } + }; + + initializeSentry(); + updateSentryUser(); }, [ sentryConfig, isSentryInitialized, @@ -68,5 +98,6 @@ export const SentryInitEffect = () => { currentWorkspaceMember, isSentryUserDefined, ]); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNamePluralFromSingular.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNamePluralFromSingular.test.tsx index dc88c77e5..8d9038be4 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNamePluralFromSingular.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNamePluralFromSingular.test.tsx @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; +import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; import { useObjectNamePluralFromSingular } from '../useObjectNamePluralFromSingular'; describe('useObjectNamePluralFromSingular', () => { @@ -8,7 +9,13 @@ describe('useObjectNamePluralFromSingular', () => { const { result } = renderHook( () => useObjectNamePluralFromSingular({ objectNameSingular: 'person' }), { - wrapper: RecoilRoot, + wrapper: ({ children }) => ( + + + {children} + + + ), }, ); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.tsx similarity index 61% rename from packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.ts rename to packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.tsx index 1016ddd30..c3a5308fb 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectNameSingularFromPlural.test.tsx @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; +import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; import { useObjectNameSingularFromPlural } from '../useObjectNameSingularFromPlural'; describe('useObjectNameSingularFromPlural', () => { @@ -8,7 +9,13 @@ describe('useObjectNameSingularFromPlural', () => { const { result } = renderHook( () => useObjectNameSingularFromPlural({ objectNamePlural: 'people' }), { - wrapper: RecoilRoot, + wrapper: ({ children }) => ( + + + {children} + + + ), }, ); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useLoadMockedObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useLoadMockedObjectMetadataItems.ts index dda913133..f79c51253 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useLoadMockedObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useLoadMockedObjectMetadataItems.ts @@ -1,13 +1,16 @@ import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { useRecoilCallback } from 'recoil'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useLoadMockedObjectMetadataItems = () => { const loadMockedObjectMetadataItems = useRecoilCallback( ({ set, snapshot }) => - () => { + async () => { + const { generatedMockObjectMetadataItems } = await import( + '~/testing/mock-data/generatedMockObjectMetadataItems' + ); + if ( !isDeeplyEqual( snapshot.getLoadable(objectMetadataItemsState).getValue(), diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts index a080fdd2f..d7b7ebad6 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts @@ -1,33 +1,20 @@ import { useRecoilValue } from 'recoil'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDefined } from 'twenty-shared/utils'; -import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; export const useObjectNamePluralFromSingular = ({ objectNameSingular, }: { objectNameSingular: string; }) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); - - let objectMetadataItem = useRecoilValue( + const objectMetadataItem = useRecoilValue( objectMetadataItemFamilySelector({ objectName: objectNameSingular, objectNameType: 'singular', }), ); - if (!isWorkspaceActiveOrSuspended(currentWorkspace)) { - objectMetadataItem = - generatedMockObjectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ) ?? null; - } - if (!isDefined(objectMetadataItem)) { throw new Error( `Object metadata item not found for ${objectNameSingular} object`, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts index 1391bb10a..4218e1c87 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts @@ -1,33 +1,20 @@ import { useRecoilValue } from 'recoil'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDefined } from 'twenty-shared/utils'; -import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace'; export const useObjectNameSingularFromPlural = ({ objectNamePlural, }: { objectNamePlural: string; }) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); - - let objectMetadataItem = useRecoilValue( + const objectMetadataItem = useRecoilValue( objectMetadataItemFamilySelector({ objectName: objectNamePlural, objectNameType: 'plural', }), ); - if (!isWorkspaceActiveOrSuspended(currentWorkspace)) { - objectMetadataItem = - generatedMockObjectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.namePlural === objectNamePlural, - ) ?? null; - } - if (!isDefined(objectMetadataItem)) { throw new Error( `Object metadata item not found for ${objectNamePlural} object`, diff --git a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx index b8f895e68..a5bd5b7d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/form-types/components/FormCountryCodeSelectInput.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent'; import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'; -import { CountryCode } from 'libphonenumber-js'; +import type { CountryCode } from 'libphonenumber-js'; import { IconCircleOff, IconComponentProps } from 'twenty-ui/display'; import { SelectOption } from 'twenty-ui/input'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx index 3aa3611ee..88df5bd82 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/RichTextV2FieldDisplay.tsx @@ -1,6 +1,6 @@ import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay'; import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText'; -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { isDefined, parseJson } from 'twenty-shared/utils'; export const RichTextV2FieldDisplay = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts index c17240071..f3c752093 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextField.ts @@ -9,7 +9,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue'; -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { isNonEmptyString } from '@sniptt/guards'; import { FieldContext } from '../../contexts/FieldContext'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextFieldDisplay.ts index 10430a806..797c03ad9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextFieldDisplay.ts @@ -5,7 +5,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/contexts/Recor import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata'; import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata'; import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText'; -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { isDefined, parseJson } from 'twenty-shared/utils'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldContext } from '../../contexts/FieldContext'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts index 3941dbc8e..a7372ade8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRichTextV2Field.ts @@ -12,7 +12,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; import { usePersistField } from '@/object-record/record-field/hooks/usePersistField'; import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2'; import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2'; -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { isNonEmptyString } from '@sniptt/guards'; import { FieldContext } from '../../contexts/FieldContext'; import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx index 8ee376862..6b39d48d6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RichTextFieldInput.tsx @@ -1,4 +1,4 @@ -import { ActivityRichTextEditor } from '@/activities/components/ActivityRichTextEditor'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -8,11 +8,19 @@ import { FieldInputEvent, } from '@/object-record/record-field/types/FieldInputEvent'; import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useRef } from 'react'; +import { lazy, Suspense, useRef } from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display'; import { FloatingIconButton } from 'twenty-ui/input'; +const ActivityRichTextEditor = lazy(() => + import('@/activities/components/ActivityRichTextEditor').then((module) => ({ + default: module.ActivityRichTextEditor, + })), +); + export type RichTextFieldInputProps = { onClickOutside?: FieldInputClickOutsideEvent; onCancel?: () => void; @@ -38,6 +46,19 @@ const StyledCollapseButton = styled.div` display: flex; `; +const LoadingSkeleton = () => { + const theme = useTheme(); + + return ( + + + + ); +}; export const RichTextFieldInput = ({ targetableObject, onClickOutside, @@ -70,10 +91,12 @@ export const RichTextFieldInput = ({ return ( - + }> + + ` background: ${({ theme, isInRightDrawer }) => @@ -34,6 +34,15 @@ const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>` isInRightDrawer ? theme.spacing(4) : ''}; `; +const StyledLoadingSkeletonContainer = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + height: 100%; + padding: ${({ theme }) => theme.spacing(4)}; + width: 100%; +`; + type CardComponentProps = { targetableObject: Pick< ActivityTargetableObject, @@ -44,6 +53,48 @@ type CardComponentProps = { type CardComponentType = (props: CardComponentProps) => JSX.Element | null; +const LoadingSkeleton = () => { + const theme = useTheme(); + + return ( + + + + + + + + ); +}; + +const WorkflowVisualizer = lazy(() => + import('@/workflow/workflow-diagram/components/WorkflowVisualizer').then( + (module) => ({ + default: module.WorkflowVisualizer, + }), + ), +); + +const WorkflowVersionVisualizer = lazy(() => + import( + '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer' + ).then((module) => ({ + default: module.WorkflowVersionVisualizer, + })), +); + +const WorkflowRunVisualizer = lazy(() => + import('@/workflow/workflow-diagram/components/WorkflowRunVisualizer').then( + (module) => ({ + default: module.WorkflowRunVisualizer, + }), + ), +); + export const CardComponents: Record = { [CardType.TimelineCard]: ({ targetableObject, isInRightDrawer }) => ( = { }} > - + }> + + ); }, @@ -112,7 +165,9 @@ export const CardComponents: Record = { - + }> + + ); }, @@ -139,7 +194,9 @@ export const CardComponents: Record = { recordId={targetableObject.id} listenedFields={['status', 'output']} /> - + }> + + ); diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionInputFromSourceCode.test.ts b/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionInputFromSourceCode.test.ts index 541bf04bc..b00098725 100644 --- a/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionInputFromSourceCode.test.ts +++ b/packages/twenty-front/src/modules/serverless-functions/utils/__tests__/getFunctionInputFromSourceCode.test.ts @@ -1,23 +1,23 @@ import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; describe('getFunctionInputFromSourceCode', () => { - it('should return empty input if not parameter', () => { + it('should return empty input if not parameter', async () => { const fileContent = 'function testFunction() { return }'; - const result = getFunctionInputFromSourceCode(fileContent); + const result = await getFunctionInputFromSourceCode(fileContent); expect(result).toEqual({}); }); - it('should return first input if multiple parameters', () => { + it('should return first input if multiple parameters', async () => { const fileContent = 'function testFunction(params1: {}, params2: {}) { return }'; - const result = getFunctionInputFromSourceCode(fileContent); + const result = await getFunctionInputFromSourceCode(fileContent); expect(result).toEqual({}); }); - it('should return empty input if wrong parameter', () => { + it('should return empty input if wrong parameter', async () => { const fileContent = 'function testFunction(params: string) { return }'; - const result = getFunctionInputFromSourceCode(fileContent); + const result = await getFunctionInputFromSourceCode(fileContent); expect(result).toEqual({}); }); - it('should return input from source code', () => { + it('should return input from source code', async () => { const fileContent = ` function testFunction( params: { @@ -34,7 +34,7 @@ describe('getFunctionInputFromSourceCode', () => { } `; - const result = getFunctionInputFromSourceCode(fileContent); + const result = await getFunctionInputFromSourceCode(fileContent); expect(result).toEqual({ param1: null, param2: null, diff --git a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionInputFromSourceCode.ts b/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionInputFromSourceCode.ts index 5cf37ba05..3f48b5081 100644 --- a/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionInputFromSourceCode.ts +++ b/packages/twenty-front/src/modules/serverless-functions/utils/getFunctionInputFromSourceCode.ts @@ -1,16 +1,18 @@ import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema'; -import { getFunctionInputSchema } from '@/serverless-functions/utils/getFunctionInputSchema'; import { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput'; import { isObject } from '@sniptt/guards'; import { isDefined } from 'twenty-shared/utils'; -export const getFunctionInputFromSourceCode = ( +export const getFunctionInputFromSourceCode = async ( sourceCode?: string, -): FunctionInput => { +): Promise => { if (!isDefined(sourceCode)) { throw new Error('Source code is not defined'); } + const { getFunctionInputSchema } = await import( + '@/serverless-functions/utils/getFunctionInputSchema' + ); const functionInputSchema = getFunctionInputSchema(sourceCode); const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0]; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx index 223c56d6d..b3e08595b 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx +++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx @@ -6,7 +6,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { t } from '@lingui/core/macro'; import { ResponsiveLine } from '@nivo/line'; -import { isNumber } from '@tiptap/core'; import { QueueMetricsTimeRange, useGetQueueMetricsQuery, @@ -204,11 +203,12 @@ export const WorkerMetricsGraph = ({ .filter(([key]) => key !== '__typename') .map(([key, value]) => ({ label: key.charAt(0).toUpperCase() + key.slice(1), - value: isNumber(value) - ? value - : Array.isArray(value) - ? value.length - : String(value), + value: + typeof value === 'number' + ? value + : Array.isArray(value) + ? value.length + : String(value), }))} gridAutoColumns="1fr 1fr" labelAlign="left" diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx index 9d991d5a1..ee954d21b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/phones/components/SettingsDataModelFieldPhonesForm.tsx @@ -7,7 +7,7 @@ import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/u import { Select } from '@/ui/input/components/Select'; import { useCountries } from '@/ui/input/components/internal/hooks/useCountries'; import { useLingui } from '@lingui/react/macro'; -import { CountryCode } from 'libphonenumber-js'; +import type { CountryCode } from 'libphonenumber-js'; import { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display'; import { z } from 'zod'; import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString'; diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx index 758a185b0..6a076e2ff 100644 --- a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect.tsx @@ -1,4 +1,3 @@ -import dagre from '@dagrejs/dagre'; import { useTheme } from '@emotion/react'; import { Edge, Node } from '@xyflow/react'; import { useEffect } from 'react'; @@ -21,81 +20,91 @@ export const SettingsDataModelOverviewEffect = ({ useFilteredObjectMetadataItems(); useEffect(() => { - const g = new dagre.graphlib.Graph(); - g.setGraph({ rankdir: 'LR' }); - g.setDefaultEdgeLabel(() => ({})); + const loadDagreAndLayout = async () => { + const dagre = await import('@dagrejs/dagre'); - const edges: Edge[] = []; - const nodes = []; - let i = 0; - for (const object of items) { - nodes.push({ - id: object.namePlural, - width: 220, - height: 100, - position: { x: i * 300, y: 0 }, - data: object, - type: 'object', - }); - g.setNode(object.namePlural, { width: 220, height: 100 }); + const g = new dagre.default.graphlib.Graph(); + g.setGraph({ rankdir: 'LR' }); + g.setDefaultEdgeLabel(() => ({})); - for (const field of object.fields) { - if ( - isDefined(field.relationDefinition) && - isDefined( - items.find( - (x) => x.id === field.relationDefinition?.targetObjectMetadata.id, - ), - ) - ) { - const sourceObj = - field.relationDefinition?.sourceObjectMetadata.namePlural; - const targetObj = - field.relationDefinition?.targetObjectMetadata.namePlural; + const edges: Edge[] = []; + const nodes: Node[] = []; + let i = 0; + for (const object of items) { + nodes.push({ + id: object.namePlural, + width: 220, + height: 100, + position: { x: i * 300, y: 0 }, + data: object, + type: 'object', + }); + g.setNode(object.namePlural, { width: 220, height: 100 }); - edges.push({ - id: `${sourceObj}-${targetObj}`, - source: object.namePlural, - sourceHandle: `${field.id}-right`, - target: field.relationDefinition.targetObjectMetadata.namePlural, - targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`, - type: 'smoothstep', - style: { - strokeWidth: 1, - stroke: theme.color.gray, - }, - markerEnd: 'marker', - markerStart: 'marker', - data: { - sourceField: field.id, - targetField: field.relationDefinition.targetFieldMetadata.id, - relation: field.relationDefinition.direction, - sourceObject: sourceObj, - targetObject: targetObj, - }, - }); - if (!isUndefinedOrNull(sourceObj) && !isUndefinedOrNull(targetObj)) { - g.setEdge(sourceObj, targetObj); + for (const field of object.fields) { + if ( + isDefined(field.relationDefinition) && + isDefined( + items.find( + (x) => + x.id === field.relationDefinition?.targetObjectMetadata.id, + ), + ) + ) { + const sourceObj = + field.relationDefinition?.sourceObjectMetadata.namePlural; + const targetObj = + field.relationDefinition?.targetObjectMetadata.namePlural; + + edges.push({ + id: `${sourceObj}-${targetObj}`, + source: object.namePlural, + sourceHandle: `${field.id}-right`, + target: field.relationDefinition.targetObjectMetadata.namePlural, + targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`, + type: 'smoothstep', + style: { + strokeWidth: 1, + stroke: theme.color.gray, + }, + markerEnd: 'marker', + markerStart: 'marker', + data: { + sourceField: field.id, + targetField: field.relationDefinition.targetFieldMetadata.id, + relation: field.relationDefinition.direction, + sourceObject: sourceObj, + targetObject: targetObj, + }, + }); + if ( + !isUndefinedOrNull(sourceObj) && + !isUndefinedOrNull(targetObj) + ) { + g.setEdge(sourceObj, targetObj); + } } } + i++; } - i++; - } - dagre.layout(g); + dagre.default.layout(g); - nodes.forEach((node) => { - const nodeWithPosition = g.node(node.id); - node.position = { - // We are shifting the dagre node position (anchor=center center) to the top left - // so it matches the React Flow node anchor point (top left). - x: nodeWithPosition.x - node.width / 2, - y: nodeWithPosition.y - node.height / 2, - }; - }); + nodes.forEach((node) => { + const nodeWithPosition = g.node(node.id); + node.position = { + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + x: nodeWithPosition.x - (node.width ?? 0) / 2, + y: nodeWithPosition.y - (node.height ?? 0) / 2, + }; + }); - setNodes(nodes); - setEdges(edges); + setNodes(nodes); + setEdges(edges); + }; + + loadDagreAndLayout(); }, [items, setEdges, setNodes, theme]); return <>; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts index 7ce048665..432ee01e5 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts @@ -1,11 +1,11 @@ +import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'; +import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction'; import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode'; -import { Dispatch, SetStateAction, useState } from 'react'; -import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql'; import { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState'; +import { Dispatch, SetStateAction, useState } from 'react'; import { useRecoilState } from 'recoil'; -import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode'; -import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath'; +import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql'; export type ServerlessFunctionNewFormValues = { name: string; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInAppNavigationDrawerMock.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInAppNavigationDrawerMock.tsx index 8bb9071dc..67864aae7 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInAppNavigationDrawerMock.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInAppNavigationDrawerMock.tsx @@ -3,16 +3,17 @@ import { NavigationDrawer } from '@/ui/navigation/navigation-drawer/components/N import { NavigationDrawerFixedContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent'; import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { SettingsPath } from '@/types/SettingsPath'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; -import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; +import { useRecoilValue } from 'recoil'; import { IconSearch, IconSettings } from 'twenty-ui/display'; import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities'; +import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; const StyledMainSection = styled(NavigationDrawerSection)` min-height: fit-content; @@ -35,6 +36,7 @@ export const SignInAppNavigationDrawerMock = ({ }: SignInAppNavigationDrawerMockProps) => { const isMobile = useIsMobile(); const { t } = useLingui(); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return ( @@ -57,7 +59,7 @@ export const SignInAppNavigationDrawerMock = ({ + objectMetadataItems={objectMetadataItems.filter((item) => WORKSPACE_FAVORITES.includes(item.nameSingular), )} /> diff --git a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx index 886e4e31f..9cd15d700 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx @@ -1,12 +1,33 @@ +import { useTheme } from '@emotion/react'; import React from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useRecoilState, useSetRecoilState } from 'recoil'; -import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; - +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId'; +import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; import { matchColumnsState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; -import { SpreadsheetImport } from './SpreadsheetImport'; + +const SpreadsheetImport = React.lazy(() => + import('./SpreadsheetImport').then((module) => ({ + default: module.SpreadsheetImport, + })), +); + +const LoadingSkeleton = () => { + const theme = useTheme(); + + return ( + + + + ); +}; type SpreadsheetImportProviderProps = React.PropsWithChildren; @@ -36,11 +57,13 @@ export const SpreadsheetImportProvider = ( <> {props.children} {spreadsheetImportDialog.isOpen && spreadsheetImportDialog.options && ( - + }> + + )} ); diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx index 800703f68..c4f70f158 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx @@ -1,9 +1,11 @@ import styled from '@emotion/styled'; import { DateTime } from 'luxon'; -import ReactDatePicker from 'react-datepicker'; +import { lazy, Suspense, useContext } from 'react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader'; @@ -13,8 +15,8 @@ import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, } from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; +import { useTheme } from '@emotion/react'; import { t } from '@lingui/core/macro'; -import { useContext } from 'react'; import 'react-datepicker/dist/react-datepicker.css'; import { IconCalendarX } from 'twenty-ui/display'; import { @@ -276,6 +278,20 @@ const StyledButton = styled(MenuItemLeftContent)` justify-content: start; `; +const StyledDatePickerFallback = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.secondary}; + border-radius: ${({ theme }) => theme.border.radius.md}; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + height: 300px; + justify-content: center; + padding: ${({ theme }) => theme.spacing(4)}; + width: 280px; +`; + type DateTimePickerProps = { isRelative?: boolean; hideHeaderInput?: boolean; @@ -306,6 +322,8 @@ type DateTimePickerProps = { onClear?: () => void; }; +const ReactDatePicker = lazy(() => import('react-datepicker')); + export const DateTimePicker = ({ date, onChange, @@ -322,6 +340,7 @@ export const DateTimePicker = ({ const internalDate = date ?? new Date(); const { timeZone } = useContext(UserContext); + const theme = useTheme(); const { closeDropdown: closeDropdownMonthSelect } = useDropdown( MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, @@ -452,51 +471,80 @@ export const DateTimePicker = ({ return (
- + + + + + + + + } - renderCustomHeader={({ - prevMonthButtonDisabled, - nextMonthButtonDisabled, - }) => - isRelative ? ( - - ) : ( - + - ) - } - onSelect={handleDateSelect} - selectsMultiple={isRelative} - /> + } + renderCustomHeader={({ + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => + isRelative ? ( + + ) : ( + + ) + } + onSelect={handleDateSelect} + selectsMultiple={isRelative} + /> +
{clearable && ( diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/types/Country.ts b/packages/twenty-front/src/modules/ui/input/components/internal/types/Country.ts index ac11cc383..6d7aaef56 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/types/Country.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/types/Country.ts @@ -1,5 +1,5 @@ import { FlagComponent } from 'country-flag-icons/react/3x2'; -import { CountryCallingCode, CountryCode } from 'libphonenumber-js'; +import type { CountryCallingCode, CountryCode } from 'libphonenumber-js'; export type Country = { countryCode: CountryCode; diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx index f1f08ecc4..2002b9bbd 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx @@ -1,4 +1,4 @@ -import { SuggestionMenuProps } from '@blocknote/react'; +import type { SuggestionMenuProps } from '@blocknote/react'; import styled from '@emotion/styled'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; diff --git a/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts b/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts index 13986df3c..b47150baf 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts +++ b/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts @@ -1,4 +1,4 @@ -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { getFirstNonEmptyLineOfRichText } from '../getFirstNonEmptyLineOfRichText'; describe('getFirstNonEmptyLineOfRichText', () => { diff --git a/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts b/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts index 4cfafc6cc..8205fb95f 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts +++ b/packages/twenty-front/src/modules/ui/input/editor/utils/getFirstNonEmptyLineOfRichText.ts @@ -1,4 +1,4 @@ -import { PartialBlock } from '@blocknote/core'; +import type { PartialBlock } from '@blocknote/core'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; export const getFirstNonEmptyLineOfRichText = ( diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionSendEmail.stories.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionSendEmail.stories.tsx index a2dde7fb5..175a0ca38 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionSendEmail.stories.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/components/__stories__/WorkflowEditActionSendEmail.stories.tsx @@ -111,7 +111,6 @@ export const Default: Story = { const canvas = within(canvasElement); expect(await canvas.findByText('Send Email')).toBeVisible(); - expect(await canvas.findByText('Account')).toBeVisible(); expect(await canvas.findByText('Subject')).toBeVisible(); expect(await canvas.findByText('Body')).toBeVisible(); diff --git a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx index 47b5809ac..2a326935e 100644 --- a/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/ObjectMetadataItemsDecorator.tsx @@ -5,8 +5,8 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect'; import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider'; +import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { mockedUserData } from '~/testing/mock-data/users'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; @@ -19,15 +19,22 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => { const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState); + const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems(); + useEffect(() => { setCurrentWorkspaceMember(mockWorkspaceMembers[0]); setCurrentUser(mockedUserData); setCurrentUserWorkspace(mockedUserData.currentUserWorkspace); - }, [setCurrentUser, setCurrentWorkspaceMember, setCurrentUserWorkspace]); + loadMockedObjectMetadataItems(); + }, [ + setCurrentUser, + setCurrentWorkspaceMember, + setCurrentUserWorkspace, + loadMockedObjectMetadataItems, + ]); return ( <> - {!!objectMetadataItems.length && } diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 299d55dca..d63f7aba0 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -37,6 +37,14 @@ export default defineConfig(({ command, mode }) => { ? path.resolve(__dirname, './tsconfig.build.json') : path.resolve(__dirname, './tsconfig.dev.json'); + + const CHUNK_SIZE_WARNING_LIMIT = 1024 * 1024; // 1MB + // Please don't increase this limit for main index chunk + // If it gets too big then find modules in the code base + // that can be loaded lazily, there are more! + const MAIN_CHUNK_SIZE_LIMIT = 4.1 * 1024 * 1024; // 4.1MB for main index chunk + const OTHER_CHUNK_SIZE_LIMIT = 15 * 1024 * 1024; // 5MB for other chunks + const checkers: Checkers = { overlay: false, }; @@ -167,20 +175,82 @@ export default defineConfig(({ command, mode }) => { outDir: 'build', sourcemap: VITE_BUILD_SOURCEMAP === 'true', rollupOptions: { + // Don't use manual chunks as it causes many issue + // including this one we wasted a lot of time on: + // https://github.com/rollup/rollup/issues/2793 output: { - manualChunks: (id) => { - if (id.includes('@scalar')) { - return 'scalar'; - } + // Set chunk size warning limit (in bytes) - warns at 1MB + chunkSizeWarningLimit: CHUNK_SIZE_WARNING_LIMIT, + // Custom plugin to fail build if chunks exceed max size + plugins: [ + { + name: 'chunk-size-limit', + generateBundle(_options, bundle) { + const oversizedChunks: string[] = []; + + Object.entries(bundle).forEach(([fileName, chunk]) => { + if (chunk.type === 'chunk' && chunk.code) { + const size = Buffer.byteLength(chunk.code, 'utf8'); + const isMainChunk = fileName.includes('index') && chunk.isEntry; + const sizeLimit = isMainChunk ? MAIN_CHUNK_SIZE_LIMIT : OTHER_CHUNK_SIZE_LIMIT; + const limitType = isMainChunk ? 'main' : 'other'; + + if (size > sizeLimit) { + oversizedChunks.push(`${fileName} (${limitType}): ${(size / 1024 / 1024).toFixed(2)}MB (limit: ${(sizeLimit / 1024 / 1024).toFixed(0)}MB)`); + } + } + }); + + if (oversizedChunks.length > 0) { + const errorMessage = `Build failed: The following chunks exceed their size limits:\n${oversizedChunks.map(chunk => ` - ${chunk}`).join('\n')}`; + this.error(errorMessage); + } + } + }, + // TODO; later - think about prefetching modules such + // as date time picker, phone input etc... + /* + { + name: 'add-prefetched-modules', + transformIndexHtml(html: string, + ctx: { + path: string; + filename: string; + server?: ViteDevServer; + bundle?: import('rollup').OutputBundle; + chunk?: import('rollup').OutputChunk; + }) { - return null; - }, - }, - }, - modulePreload: { - resolveDependencies: (filename, deps, { hostId }) => { - return deps.filter(dep => !dep.includes('scalar')); - }, + const bundles = Object.keys(ctx.bundle ?? {}); + + let modernBundles = bundles.filter( + (bundle) => bundle.endsWith('.map') === false + ); + + + // Remove existing files and concatenate them into link tags + const prefechBundlesString = modernBundles + .filter((bundle) => html.includes(bundle) === false) + .map((bundle) => ``) + .join(''); + + // Use regular expression to get the content within + const headContent = html.match(/([\s\S]*)<\/head>/)?.[1] ?? ''; + // Insert the content of prefetch into the head + const newHeadContent = `${headContent}${prefechBundlesString}`; + // Replace the original head + html = html.replace( + /([\s\S]*)<\/head>/, + `${newHeadContent}` + ); + + return html; + + + }, + }*/ + ] + } }, }, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts index 4a4ed09f0..ed5d1705b 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/enums/database-event-action.ts @@ -1,7 +1,7 @@ export enum DatabaseEventAction { - CREATED = 'CREATED', - UPDATED = 'UPDATED', - DELETED = 'DELETED', - DESTROYED = 'DESTROYED', - RESTORED = 'RESTORED', + CREATED = 'created', + UPDATED = 'updated', + DELETED = 'deleted', + DESTROYED = 'destroyed', + RESTORED = 'restored', }