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)
This commit is contained in:
@ -1,7 +1,6 @@
|
|||||||
import { Action } from '@/action-menu/actions/components/Action';
|
import { Action } from '@/action-menu/actions/components/Action';
|
||||||
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
import { useSelectedRecordIdOrThrow } from '@/action-menu/actions/record-actions/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { BlockNoteEditor } from '@blocknote/core';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
@ -33,15 +32,11 @@ export const ExportNoteActionSingleRecordAction = () => {
|
|||||||
console.warn(initialBody);
|
console.warn(initialBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = BlockNoteEditor.create({
|
|
||||||
initialContent: parsedBody,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { exportBlockNoteEditorToPdf } = await import(
|
const { exportBlockNoteEditorToPdf } = await import(
|
||||||
'@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf'
|
'@/action-menu/actions/record-actions/single-record/utils/exportBlockNoteEditorToPdf'
|
||||||
);
|
);
|
||||||
|
|
||||||
await exportBlockNoteEditorToPdf(editor, filename);
|
await exportBlockNoteEditorToPdf(parsedBody, filename);
|
||||||
|
|
||||||
// TODO later: implement DOCX export
|
// TODO later: implement DOCX export
|
||||||
// const { exportBlockNoteEditorToDocx } = await import(
|
// const { exportBlockNoteEditorToDocx } = await import(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { BlockNoteEditor } from '@blocknote/core';
|
import { BlockNoteEditor, PartialBlock } from '@blocknote/core';
|
||||||
import {
|
import {
|
||||||
PDFExporter,
|
PDFExporter,
|
||||||
pdfDefaultSchemaMappings,
|
pdfDefaultSchemaMappings,
|
||||||
@ -7,9 +7,13 @@ import { pdf } from '@react-pdf/renderer';
|
|||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
|
||||||
export const exportBlockNoteEditorToPdf = async (
|
export const exportBlockNoteEditorToPdf = async (
|
||||||
editor: BlockNoteEditor,
|
parsedBody: PartialBlock[],
|
||||||
filename: string,
|
filename: string,
|
||||||
) => {
|
) => {
|
||||||
|
const editor = BlockNoteEditor.create({
|
||||||
|
initialContent: parsedBody,
|
||||||
|
});
|
||||||
|
|
||||||
const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings);
|
const exporter = new PDFExporter(editor.schema, pdfDefaultSchemaMappings);
|
||||||
|
|
||||||
const pdfDocument = await exporter.toReactPDFDocument(editor.document);
|
const pdfDocument = await exporter.toReactPDFDocument(editor.document);
|
||||||
|
|||||||
@ -34,7 +34,7 @@ import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRec
|
|||||||
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||||
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
import { useIsRecordReadOnly } from '@/object-record/record-field/hooks/useIsRecordReadOnly';
|
||||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
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/core/fonts/inter.css';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
import { useCreateBlockNote } from '@blocknote/react';
|
import { useCreateBlockNote } from '@blocknote/react';
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Attachment } from '@/activities/files/types/Attachment';
|
|||||||
|
|
||||||
export const getActivityAttachmentIdsToDelete = (
|
export const getActivityAttachmentIdsToDelete = (
|
||||||
newActivityBody: string,
|
newActivityBody: string,
|
||||||
oldActivityAttachments: Attachment[],
|
oldActivityAttachments: Attachment[] = [],
|
||||||
) => {
|
) => {
|
||||||
if (oldActivityAttachments.length === 0) return [];
|
if (oldActivityAttachments.length === 0) return [];
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import { AuthTokenPair } from '~/generated/graphql';
|
|||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
|
|
||||||
import { i18n } from '@lingui/core';
|
import { i18n } from '@lingui/core';
|
||||||
import { captureException } from '@sentry/react';
|
|
||||||
import { GraphQLFormattedError } from 'graphql';
|
import { GraphQLFormattedError } from 'graphql';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { cookieStorage } from '~/utils/cookie-storage';
|
import { cookieStorage } from '~/utils/cookie-storage';
|
||||||
@ -160,7 +159,17 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
|||||||
}, Path: ${graphQLError.path}`,
|
}, 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { viewableRichTextComponentState } from '../states/viewableRichTextComponentState';
|
import { viewableRichTextComponentState } from '../states/viewableRichTextComponentState';
|
||||||
|
|
||||||
|
const ActivityRichTextEditor = lazy(() =>
|
||||||
|
import('@/activities/components/ActivityRichTextEditor').then((module) => ({
|
||||||
|
default: module.ActivityRichTextEditor,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(-2)};
|
margin: ${({ theme }) => theme.spacing(4)} ${({ theme }) => theme.spacing(-2)};
|
||||||
@ -11,6 +20,20 @@ const StyledContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LoadingSkeleton = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={theme.background.tertiary}
|
||||||
|
highlightColor={theme.background.transparent.lighter}
|
||||||
|
borderRadius={theme.border.radius.sm}
|
||||||
|
>
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const CommandMenuEditRichTextPage = () => {
|
export const CommandMenuEditRichTextPage = () => {
|
||||||
const { activityId, activityObjectNameSingular } = useRecoilValue(
|
const { activityId, activityObjectNameSingular } = useRecoilValue(
|
||||||
viewableRichTextComponentState,
|
viewableRichTextComponentState,
|
||||||
@ -27,10 +50,12 @@ export const CommandMenuEditRichTextPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<ActivityRichTextEditor
|
<Suspense fallback={<LoadingSkeleton />}>
|
||||||
activityId={activityId}
|
<ActivityRichTextEditor
|
||||||
activityObjectNameSingular={activityObjectNameSingular}
|
activityId={activityId}
|
||||||
/>
|
activityObjectNameSingular={activityObjectNameSingular}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect';
|
import { AppErrorBoundaryEffect } from '@/error-handler/components/internal/AppErrorBoundaryEffect';
|
||||||
import { captureException } from '@sentry/react';
|
|
||||||
import { ErrorInfo, ReactNode } from 'react';
|
import { ErrorInfo, ReactNode } from 'react';
|
||||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
|
||||||
|
|
||||||
@ -14,11 +13,17 @@ export const AppErrorBoundary = ({
|
|||||||
FallbackComponent,
|
FallbackComponent,
|
||||||
resetOnLocationChange = true,
|
resetOnLocationChange = true,
|
||||||
}: AppErrorBoundaryProps) => {
|
}: AppErrorBoundaryProps) => {
|
||||||
const handleError = (error: Error, info: ErrorInfo) => {
|
const handleError = async (error: Error, info: ErrorInfo) => {
|
||||||
captureException(error, (scope) => {
|
try {
|
||||||
scope.setExtras({ info });
|
const { captureException } = await import('@sentry/react');
|
||||||
return scope;
|
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 = () => {
|
const handleReset = () => {
|
||||||
|
|||||||
@ -1,9 +1,3 @@
|
|||||||
import {
|
|
||||||
browserTracingIntegration,
|
|
||||||
init,
|
|
||||||
replayIntegration,
|
|
||||||
setUser,
|
|
||||||
} from '@sentry/react';
|
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
@ -27,38 +21,74 @@ export const SentryInitEffect = () => {
|
|||||||
const [isSentryUserDefined, setIsSentryUserDefined] = useState(false);
|
const [isSentryUserDefined, setIsSentryUserDefined] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
const initializeSentry = async () => {
|
||||||
!isSentryInitializing &&
|
if (
|
||||||
isNonEmptyString(sentryConfig?.dsn) &&
|
isNonEmptyString(sentryConfig?.dsn) &&
|
||||||
!isSentryInitialized
|
!isSentryInitialized &&
|
||||||
) {
|
!isSentryInitializing
|
||||||
setIsSentryInitializing(true);
|
) {
|
||||||
init({
|
setIsSentryInitializing(true);
|
||||||
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);
|
try {
|
||||||
setIsSentryInitializing(false);
|
const { init, browserTracingIntegration, replayIntegration } =
|
||||||
}
|
await import('@sentry/react');
|
||||||
|
|
||||||
if (isDefined(currentUser) && !isSentryUserDefined) {
|
init({
|
||||||
setUser({
|
environment: sentryConfig?.environment ?? undefined,
|
||||||
email: currentUser?.email,
|
release: sentryConfig?.release ?? undefined,
|
||||||
id: currentUser?.id,
|
dsn: sentryConfig?.dsn,
|
||||||
workspaceId: currentWorkspace?.id,
|
integrations: [browserTracingIntegration({}), replayIntegration()],
|
||||||
workspaceMemberId: currentWorkspaceMember?.id,
|
tracePropagationTargets: [
|
||||||
});
|
'localhost:3001',
|
||||||
setIsSentryUserDefined(true);
|
REACT_APP_SERVER_BASE_URL,
|
||||||
} else {
|
],
|
||||||
setUser(null);
|
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,
|
sentryConfig,
|
||||||
isSentryInitialized,
|
isSentryInitialized,
|
||||||
@ -68,5 +98,6 @@ export const SentryInitEffect = () => {
|
|||||||
currentWorkspaceMember,
|
currentWorkspaceMember,
|
||||||
isSentryUserDefined,
|
isSentryUserDefined,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||||
import { useObjectNamePluralFromSingular } from '../useObjectNamePluralFromSingular';
|
import { useObjectNamePluralFromSingular } from '../useObjectNamePluralFromSingular';
|
||||||
|
|
||||||
describe('useObjectNamePluralFromSingular', () => {
|
describe('useObjectNamePluralFromSingular', () => {
|
||||||
@ -8,7 +9,13 @@ describe('useObjectNamePluralFromSingular', () => {
|
|||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useObjectNamePluralFromSingular({ objectNameSingular: 'person' }),
|
() => useObjectNamePluralFromSingular({ objectNameSingular: 'person' }),
|
||||||
{
|
{
|
||||||
wrapper: RecoilRoot,
|
wrapper: ({ children }) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<JestObjectMetadataItemSetter>
|
||||||
|
{children}
|
||||||
|
</JestObjectMetadataItemSetter>
|
||||||
|
</RecoilRoot>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||||
import { useObjectNameSingularFromPlural } from '../useObjectNameSingularFromPlural';
|
import { useObjectNameSingularFromPlural } from '../useObjectNameSingularFromPlural';
|
||||||
|
|
||||||
describe('useObjectNameSingularFromPlural', () => {
|
describe('useObjectNameSingularFromPlural', () => {
|
||||||
@ -8,7 +9,13 @@ describe('useObjectNameSingularFromPlural', () => {
|
|||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => useObjectNameSingularFromPlural({ objectNamePlural: 'people' }),
|
() => useObjectNameSingularFromPlural({ objectNamePlural: 'people' }),
|
||||||
{
|
{
|
||||||
wrapper: RecoilRoot,
|
wrapper: ({ children }) => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<JestObjectMetadataItemSetter>
|
||||||
|
{children}
|
||||||
|
</JestObjectMetadataItemSetter>
|
||||||
|
</RecoilRoot>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1,13 +1,16 @@
|
|||||||
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
export const useLoadMockedObjectMetadataItems = () => {
|
export const useLoadMockedObjectMetadataItems = () => {
|
||||||
const loadMockedObjectMetadataItems = useRecoilCallback(
|
const loadMockedObjectMetadataItems = useRecoilCallback(
|
||||||
({ set, snapshot }) =>
|
({ set, snapshot }) =>
|
||||||
() => {
|
async () => {
|
||||||
|
const { generatedMockObjectMetadataItems } = await import(
|
||||||
|
'~/testing/mock-data/generatedMockObjectMetadataItems'
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isDeeplyEqual(
|
!isDeeplyEqual(
|
||||||
snapshot.getLoadable(objectMetadataItemsState).getValue(),
|
snapshot.getLoadable(objectMetadataItemsState).getValue(),
|
||||||
|
|||||||
@ -1,33 +1,20 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
||||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
|
||||||
|
|
||||||
export const useObjectNamePluralFromSingular = ({
|
export const useObjectNamePluralFromSingular = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
}) => {
|
}) => {
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const objectMetadataItem = useRecoilValue(
|
||||||
|
|
||||||
let objectMetadataItem = useRecoilValue(
|
|
||||||
objectMetadataItemFamilySelector({
|
objectMetadataItemFamilySelector({
|
||||||
objectName: objectNameSingular,
|
objectName: objectNameSingular,
|
||||||
objectNameType: 'singular',
|
objectNameType: 'singular',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
|
||||||
objectMetadataItem =
|
|
||||||
generatedMockObjectMetadataItems.find(
|
|
||||||
(objectMetadataItem) =>
|
|
||||||
objectMetadataItem.nameSingular === objectNameSingular,
|
|
||||||
) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefined(objectMetadataItem)) {
|
if (!isDefined(objectMetadataItem)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Object metadata item not found for ${objectNameSingular} object`,
|
`Object metadata item not found for ${objectNameSingular} object`,
|
||||||
|
|||||||
@ -1,33 +1,20 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
|
||||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
|
||||||
|
|
||||||
export const useObjectNameSingularFromPlural = ({
|
export const useObjectNameSingularFromPlural = ({
|
||||||
objectNamePlural,
|
objectNamePlural,
|
||||||
}: {
|
}: {
|
||||||
objectNamePlural: string;
|
objectNamePlural: string;
|
||||||
}) => {
|
}) => {
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const objectMetadataItem = useRecoilValue(
|
||||||
|
|
||||||
let objectMetadataItem = useRecoilValue(
|
|
||||||
objectMetadataItemFamilySelector({
|
objectMetadataItemFamilySelector({
|
||||||
objectName: objectNamePlural,
|
objectName: objectNamePlural,
|
||||||
objectNameType: 'plural',
|
objectNameType: 'plural',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isWorkspaceActiveOrSuspended(currentWorkspace)) {
|
|
||||||
objectMetadataItem =
|
|
||||||
generatedMockObjectMetadataItems.find(
|
|
||||||
(objectMetadataItem) =>
|
|
||||||
objectMetadataItem.namePlural === objectNamePlural,
|
|
||||||
) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDefined(objectMetadataItem)) {
|
if (!isDefined(objectMetadataItem)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Object metadata item not found for ${objectNamePlural} object`,
|
`Object metadata item not found for ${objectNamePlural} object`,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|||||||
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
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 { IconCircleOff, IconComponentProps } from 'twenty-ui/display';
|
||||||
import { SelectOption } from 'twenty-ui/input';
|
import { SelectOption } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay';
|
import { useRichTextV2FieldDisplay } from '@/object-record/record-field/meta-types/hooks/useRichTextV2FieldDisplay';
|
||||||
import { getFirstNonEmptyLineOfRichText } from '@/ui/input/editor/utils/getFirstNonEmptyLineOfRichText';
|
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';
|
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const RichTextV2FieldDisplay = () => {
|
export const RichTextV2FieldDisplay = () => {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
||||||
import { isFieldRichTextValue } from '@/object-record/record-field/types/guards/isFieldRichTextValue';
|
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 { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useRecordFieldValue } from '@/object-record/record-store/contexts/Recor
|
|||||||
import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldRichTextValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '@/object-record/record-field/types/guards/assertFieldMetadata';
|
||||||
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
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 { isDefined, parseJson } from 'twenty-shared/utils';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
import { usePersistField } from '@/object-record/record-field/hooks/usePersistField';
|
||||||
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
import { isFieldRichTextV2 } from '@/object-record/record-field/types/guards/isFieldRichTextV2';
|
||||||
import { isFieldRichTextV2Value } from '@/object-record/record-field/types/guards/isFieldRichTextValueV2';
|
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 { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
||||||
|
|||||||
@ -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 { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
|
import { useRichTextCommandMenu } from '@/command-menu/hooks/useRichTextCommandMenu';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -8,11 +8,19 @@ import {
|
|||||||
FieldInputEvent,
|
FieldInputEvent,
|
||||||
} from '@/object-record/record-field/types/FieldInputEvent';
|
} from '@/object-record/record-field/types/FieldInputEvent';
|
||||||
import { DEFAULT_CELL_SCOPE } from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
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 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 { IconLayoutSidebarLeftCollapse } from 'twenty-ui/display';
|
||||||
import { FloatingIconButton } from 'twenty-ui/input';
|
import { FloatingIconButton } from 'twenty-ui/input';
|
||||||
|
|
||||||
|
const ActivityRichTextEditor = lazy(() =>
|
||||||
|
import('@/activities/components/ActivityRichTextEditor').then((module) => ({
|
||||||
|
default: module.ActivityRichTextEditor,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
export type RichTextFieldInputProps = {
|
export type RichTextFieldInputProps = {
|
||||||
onClickOutside?: FieldInputClickOutsideEvent;
|
onClickOutside?: FieldInputClickOutsideEvent;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@ -38,6 +46,19 @@ const StyledCollapseButton = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const LoadingSkeleton = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={theme.background.tertiary}
|
||||||
|
highlightColor={theme.background.transparent.lighter}
|
||||||
|
borderRadius={theme.border.radius.sm}
|
||||||
|
>
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
||||||
export const RichTextFieldInput = ({
|
export const RichTextFieldInput = ({
|
||||||
targetableObject,
|
targetableObject,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
@ -70,10 +91,12 @@ export const RichTextFieldInput = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer ref={containerRef}>
|
<StyledContainer ref={containerRef}>
|
||||||
<ActivityRichTextEditor
|
<Suspense fallback={<LoadingSkeleton />}>
|
||||||
activityId={targetableObject.id}
|
<ActivityRichTextEditor
|
||||||
activityObjectNameSingular={targetableObject.targetObjectNameSingular}
|
activityId={targetableObject.id}
|
||||||
/>
|
activityObjectNameSingular={targetableObject.targetObjectNameSingular}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
<StyledCollapseButton>
|
<StyledCollapseButton>
|
||||||
<FloatingIconButton
|
<FloatingIconButton
|
||||||
Icon={IconLayoutSidebarLeftCollapse}
|
Icon={IconLayoutSidebarLeftCollapse}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Calendar } from '@/activities/calendar/components/Calendar';
|
import { Calendar } from '@/activities/calendar/components/Calendar';
|
||||||
|
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||||
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
|
||||||
import { Attachments } from '@/activities/files/components/Attachments';
|
import { Attachments } from '@/activities/files/components/Attachments';
|
||||||
import { Notes } from '@/activities/notes/components/Notes';
|
import { Notes } from '@/activities/notes/components/Notes';
|
||||||
@ -10,16 +11,15 @@ import { CardType } from '@/object-record/record-show/types/CardType';
|
|||||||
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
import { ListenRecordUpdatesEffect } from '@/subscription/components/ListenUpdatesEffect';
|
||||||
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer';
|
||||||
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
import { getWorkflowVisualizerComponentInstanceId } from '@/workflow/utils/getWorkflowVisualizerComponentInstanceId';
|
||||||
import { WorkflowRunVisualizer } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizer';
|
|
||||||
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
import { WorkflowRunVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowRunVisualizerEffect';
|
||||||
import { WorkflowVersionVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizer';
|
|
||||||
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
import { WorkflowVersionVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVersionVisualizerEffect';
|
||||||
import { WorkflowVisualizer } from '@/workflow/workflow-diagram/components/WorkflowVisualizer';
|
|
||||||
import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
|
import { WorkflowVisualizerEffect } from '@/workflow/workflow-diagram/components/WorkflowVisualizerEffect';
|
||||||
import { WorkflowRunVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowRunVisualizerComponentInstanceContext';
|
import { WorkflowRunVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowRunVisualizerComponentInstanceContext';
|
||||||
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
import { WorkflowVisualizerComponentInstanceContext } from '@/workflow/workflow-diagram/states/contexts/WorkflowVisualizerComponentInstanceContext';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useId } from 'react';
|
import { lazy, Suspense, useId } from 'react';
|
||||||
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
|
|
||||||
const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
|
const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
|
||||||
background: ${({ theme, isInRightDrawer }) =>
|
background: ${({ theme, isInRightDrawer }) =>
|
||||||
@ -34,6 +34,15 @@ const StyledGreyBox = styled.div<{ isInRightDrawer?: boolean }>`
|
|||||||
isInRightDrawer ? theme.spacing(4) : ''};
|
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 = {
|
type CardComponentProps = {
|
||||||
targetableObject: Pick<
|
targetableObject: Pick<
|
||||||
ActivityTargetableObject,
|
ActivityTargetableObject,
|
||||||
@ -44,6 +53,48 @@ type CardComponentProps = {
|
|||||||
|
|
||||||
type CardComponentType = (props: CardComponentProps) => JSX.Element | null;
|
type CardComponentType = (props: CardComponentProps) => JSX.Element | null;
|
||||||
|
|
||||||
|
const LoadingSkeleton = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledLoadingSkeletonContainer>
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={theme.background.tertiary}
|
||||||
|
highlightColor={theme.background.transparent.lighter}
|
||||||
|
borderRadius={theme.border.radius.sm}
|
||||||
|
>
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} />
|
||||||
|
</SkeletonTheme>
|
||||||
|
</StyledLoadingSkeletonContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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, CardComponentType> = {
|
export const CardComponents: Record<CardType, CardComponentType> = {
|
||||||
[CardType.TimelineCard]: ({ targetableObject, isInRightDrawer }) => (
|
[CardType.TimelineCard]: ({ targetableObject, isInRightDrawer }) => (
|
||||||
<TimelineActivities
|
<TimelineActivities
|
||||||
@ -95,7 +146,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkflowVisualizerEffect workflowId={targetableObject.id} />
|
<WorkflowVisualizerEffect workflowId={targetableObject.id} />
|
||||||
<WorkflowVisualizer workflowId={targetableObject.id} />
|
<Suspense fallback={<LoadingSkeleton />}>
|
||||||
|
<WorkflowVisualizer workflowId={targetableObject.id} />
|
||||||
|
</Suspense>
|
||||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -112,7 +165,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
|||||||
<WorkflowVersionVisualizerEffect
|
<WorkflowVersionVisualizerEffect
|
||||||
workflowVersionId={targetableObject.id}
|
workflowVersionId={targetableObject.id}
|
||||||
/>
|
/>
|
||||||
<WorkflowVersionVisualizer workflowVersionId={targetableObject.id} />
|
<Suspense fallback={<LoadingSkeleton />}>
|
||||||
|
<WorkflowVersionVisualizer workflowVersionId={targetableObject.id} />
|
||||||
|
</Suspense>
|
||||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -139,7 +194,9 @@ export const CardComponents: Record<CardType, CardComponentType> = {
|
|||||||
recordId={targetableObject.id}
|
recordId={targetableObject.id}
|
||||||
listenedFields={['status', 'output']}
|
listenedFields={['status', 'output']}
|
||||||
/>
|
/>
|
||||||
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
<Suspense fallback={<LoadingSkeleton />}>
|
||||||
|
<WorkflowRunVisualizer workflowRunId={targetableObject.id} />
|
||||||
|
</Suspense>
|
||||||
</WorkflowRunVisualizerComponentInstanceContext.Provider>
|
</WorkflowRunVisualizerComponentInstanceContext.Provider>
|
||||||
</WorkflowVisualizerComponentInstanceContext.Provider>
|
</WorkflowVisualizerComponentInstanceContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
||||||
|
|
||||||
describe('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 fileContent = 'function testFunction() { return }';
|
||||||
const result = getFunctionInputFromSourceCode(fileContent);
|
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
it('should return first input if multiple parameters', () => {
|
it('should return first input if multiple parameters', async () => {
|
||||||
const fileContent =
|
const fileContent =
|
||||||
'function testFunction(params1: {}, params2: {}) { return }';
|
'function testFunction(params1: {}, params2: {}) { return }';
|
||||||
const result = getFunctionInputFromSourceCode(fileContent);
|
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||||
expect(result).toEqual({});
|
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 fileContent = 'function testFunction(params: string) { return }';
|
||||||
const result = getFunctionInputFromSourceCode(fileContent);
|
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||||
expect(result).toEqual({});
|
expect(result).toEqual({});
|
||||||
});
|
});
|
||||||
it('should return input from source code', () => {
|
it('should return input from source code', async () => {
|
||||||
const fileContent = `
|
const fileContent = `
|
||||||
function testFunction(
|
function testFunction(
|
||||||
params: {
|
params: {
|
||||||
@ -34,7 +34,7 @@ describe('getFunctionInputFromSourceCode', () => {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = getFunctionInputFromSourceCode(fileContent);
|
const result = await getFunctionInputFromSourceCode(fileContent);
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
param1: null,
|
param1: null,
|
||||||
param2: null,
|
param2: null,
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import { getDefaultFunctionInputFromInputSchema } from '@/serverless-functions/utils/getDefaultFunctionInputFromInputSchema';
|
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 { FunctionInput } from '@/workflow/workflow-steps/workflow-actions/code-action/types/FunctionInput';
|
||||||
import { isObject } from '@sniptt/guards';
|
import { isObject } from '@sniptt/guards';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
export const getFunctionInputFromSourceCode = (
|
export const getFunctionInputFromSourceCode = async (
|
||||||
sourceCode?: string,
|
sourceCode?: string,
|
||||||
): FunctionInput => {
|
): Promise<FunctionInput> => {
|
||||||
if (!isDefined(sourceCode)) {
|
if (!isDefined(sourceCode)) {
|
||||||
throw new Error('Source code is not defined');
|
throw new Error('Source code is not defined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { getFunctionInputSchema } = await import(
|
||||||
|
'@/serverless-functions/utils/getFunctionInputSchema'
|
||||||
|
);
|
||||||
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
const functionInputSchema = getFunctionInputSchema(sourceCode);
|
||||||
|
|
||||||
const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0];
|
const result = getDefaultFunctionInputFromInputSchema(functionInputSchema)[0];
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { ResponsiveLine } from '@nivo/line';
|
import { ResponsiveLine } from '@nivo/line';
|
||||||
import { isNumber } from '@tiptap/core';
|
|
||||||
import {
|
import {
|
||||||
QueueMetricsTimeRange,
|
QueueMetricsTimeRange,
|
||||||
useGetQueueMetricsQuery,
|
useGetQueueMetricsQuery,
|
||||||
@ -204,11 +203,12 @@ export const WorkerMetricsGraph = ({
|
|||||||
.filter(([key]) => key !== '__typename')
|
.filter(([key]) => key !== '__typename')
|
||||||
.map(([key, value]) => ({
|
.map(([key, value]) => ({
|
||||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||||
value: isNumber(value)
|
value:
|
||||||
? value
|
typeof value === 'number'
|
||||||
: Array.isArray(value)
|
? value
|
||||||
? value.length
|
: Array.isArray(value)
|
||||||
: String(value),
|
? value.length
|
||||||
|
: String(value),
|
||||||
}))}
|
}))}
|
||||||
gridAutoColumns="1fr 1fr"
|
gridAutoColumns="1fr 1fr"
|
||||||
labelAlign="left"
|
labelAlign="left"
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/u
|
|||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
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 { IconCircleOff, IconComponentProps, IconMap } from 'twenty-ui/display';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import dagre from '@dagrejs/dagre';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import { Edge, Node } from '@xyflow/react';
|
import { Edge, Node } from '@xyflow/react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@ -21,81 +20,91 @@ export const SettingsDataModelOverviewEffect = ({
|
|||||||
useFilteredObjectMetadataItems();
|
useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const g = new dagre.graphlib.Graph();
|
const loadDagreAndLayout = async () => {
|
||||||
g.setGraph({ rankdir: 'LR' });
|
const dagre = await import('@dagrejs/dagre');
|
||||||
g.setDefaultEdgeLabel(() => ({}));
|
|
||||||
|
|
||||||
const edges: Edge[] = [];
|
const g = new dagre.default.graphlib.Graph();
|
||||||
const nodes = [];
|
g.setGraph({ rankdir: 'LR' });
|
||||||
let i = 0;
|
g.setDefaultEdgeLabel(() => ({}));
|
||||||
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 });
|
|
||||||
|
|
||||||
for (const field of object.fields) {
|
const edges: Edge[] = [];
|
||||||
if (
|
const nodes: Node[] = [];
|
||||||
isDefined(field.relationDefinition) &&
|
let i = 0;
|
||||||
isDefined(
|
for (const object of items) {
|
||||||
items.find(
|
nodes.push({
|
||||||
(x) => x.id === field.relationDefinition?.targetObjectMetadata.id,
|
id: object.namePlural,
|
||||||
),
|
width: 220,
|
||||||
)
|
height: 100,
|
||||||
) {
|
position: { x: i * 300, y: 0 },
|
||||||
const sourceObj =
|
data: object,
|
||||||
field.relationDefinition?.sourceObjectMetadata.namePlural;
|
type: 'object',
|
||||||
const targetObj =
|
});
|
||||||
field.relationDefinition?.targetObjectMetadata.namePlural;
|
g.setNode(object.namePlural, { width: 220, height: 100 });
|
||||||
|
|
||||||
edges.push({
|
for (const field of object.fields) {
|
||||||
id: `${sourceObj}-${targetObj}`,
|
if (
|
||||||
source: object.namePlural,
|
isDefined(field.relationDefinition) &&
|
||||||
sourceHandle: `${field.id}-right`,
|
isDefined(
|
||||||
target: field.relationDefinition.targetObjectMetadata.namePlural,
|
items.find(
|
||||||
targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`,
|
(x) =>
|
||||||
type: 'smoothstep',
|
x.id === field.relationDefinition?.targetObjectMetadata.id,
|
||||||
style: {
|
),
|
||||||
strokeWidth: 1,
|
)
|
||||||
stroke: theme.color.gray,
|
) {
|
||||||
},
|
const sourceObj =
|
||||||
markerEnd: 'marker',
|
field.relationDefinition?.sourceObjectMetadata.namePlural;
|
||||||
markerStart: 'marker',
|
const targetObj =
|
||||||
data: {
|
field.relationDefinition?.targetObjectMetadata.namePlural;
|
||||||
sourceField: field.id,
|
|
||||||
targetField: field.relationDefinition.targetFieldMetadata.id,
|
edges.push({
|
||||||
relation: field.relationDefinition.direction,
|
id: `${sourceObj}-${targetObj}`,
|
||||||
sourceObject: sourceObj,
|
source: object.namePlural,
|
||||||
targetObject: targetObj,
|
sourceHandle: `${field.id}-right`,
|
||||||
},
|
target: field.relationDefinition.targetObjectMetadata.namePlural,
|
||||||
});
|
targetHandle: `${field.relationDefinition.targetObjectMetadata}-left`,
|
||||||
if (!isUndefinedOrNull(sourceObj) && !isUndefinedOrNull(targetObj)) {
|
type: 'smoothstep',
|
||||||
g.setEdge(sourceObj, targetObj);
|
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) => {
|
nodes.forEach((node) => {
|
||||||
const nodeWithPosition = g.node(node.id);
|
const nodeWithPosition = g.node(node.id);
|
||||||
node.position = {
|
node.position = {
|
||||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
// 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).
|
// so it matches the React Flow node anchor point (top left).
|
||||||
x: nodeWithPosition.x - node.width / 2,
|
x: nodeWithPosition.x - (node.width ?? 0) / 2,
|
||||||
y: nodeWithPosition.y - node.height / 2,
|
y: nodeWithPosition.y - (node.height ?? 0) / 2,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
setNodes(nodes);
|
setNodes(nodes);
|
||||||
setEdges(edges);
|
setEdges(edges);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDagreAndLayout();
|
||||||
}, [items, setEdges, setNodes, theme]);
|
}, [items, setEdges, setNodes, theme]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|||||||
@ -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 { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||||
import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode';
|
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 { serverlessFunctionTestDataFamilyState } from '@/workflow/states/serverlessFunctionTestDataFamilyState';
|
||||||
|
import { Dispatch, SetStateAction, useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { getFunctionInputFromSourceCode } from '@/serverless-functions/utils/getFunctionInputFromSourceCode';
|
import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql';
|
||||||
import { INDEX_FILE_PATH } from '@/serverless-functions/constants/IndexFilePath';
|
|
||||||
|
|
||||||
export type ServerlessFunctionNewFormValues = {
|
export type ServerlessFunctionNewFormValues = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -3,16 +3,17 @@ import { NavigationDrawer } from '@/ui/navigation/navigation-drawer/components/N
|
|||||||
import { NavigationDrawerFixedContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent';
|
import { NavigationDrawerFixedContent } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerFixedContent';
|
||||||
|
|
||||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||||
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||||
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
|
||||||
import { IconSearch, IconSettings } from 'twenty-ui/display';
|
import { IconSearch, IconSettings } from 'twenty-ui/display';
|
||||||
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
import { getOsControlSymbol, useIsMobile } from 'twenty-ui/utilities';
|
||||||
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
|
|
||||||
const StyledMainSection = styled(NavigationDrawerSection)`
|
const StyledMainSection = styled(NavigationDrawerSection)`
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
@ -35,6 +36,7 @@ export const SignInAppNavigationDrawerMock = ({
|
|||||||
}: SignInAppNavigationDrawerMockProps) => {
|
}: SignInAppNavigationDrawerMockProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationDrawer className={className} title={DEFAULT_WORKSPACE_NAME}>
|
<NavigationDrawer className={className} title={DEFAULT_WORKSPACE_NAME}>
|
||||||
@ -57,7 +59,7 @@ export const SignInAppNavigationDrawerMock = ({
|
|||||||
<NavigationDrawerSectionForObjectMetadataItems
|
<NavigationDrawerSectionForObjectMetadataItems
|
||||||
sectionTitle={t`Workspace`}
|
sectionTitle={t`Workspace`}
|
||||||
isRemote={false}
|
isRemote={false}
|
||||||
objectMetadataItems={generatedMockObjectMetadataItems.filter((item) =>
|
objectMetadataItems={objectMetadataItems.filter((item) =>
|
||||||
WORKSPACE_FAVORITES.includes(item.nameSingular),
|
WORKSPACE_FAVORITES.includes(item.nameSingular),
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,12 +1,33 @@
|
|||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
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 { 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 { matchColumnsState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
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 (
|
||||||
|
<SkeletonTheme
|
||||||
|
baseColor={theme.background.tertiary}
|
||||||
|
highlightColor={theme.background.transparent.lighter}
|
||||||
|
borderRadius={theme.border.radius.sm}
|
||||||
|
>
|
||||||
|
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} />
|
||||||
|
</SkeletonTheme>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type SpreadsheetImportProviderProps = React.PropsWithChildren;
|
type SpreadsheetImportProviderProps = React.PropsWithChildren;
|
||||||
|
|
||||||
@ -36,11 +57,13 @@ export const SpreadsheetImportProvider = (
|
|||||||
<>
|
<>
|
||||||
{props.children}
|
{props.children}
|
||||||
{spreadsheetImportDialog.isOpen && spreadsheetImportDialog.options && (
|
{spreadsheetImportDialog.isOpen && spreadsheetImportDialog.options && (
|
||||||
<SpreadsheetImport
|
<React.Suspense fallback={<LoadingSkeleton />}>
|
||||||
onClose={handleClose}
|
<SpreadsheetImport
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
onClose={handleClose}
|
||||||
{...spreadsheetImportDialog.options}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
/>
|
{...spreadsheetImportDialog.options}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DateTime } from 'luxon';
|
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 { 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 { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader';
|
||||||
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput';
|
||||||
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader';
|
||||||
@ -13,8 +15,8 @@ import {
|
|||||||
VariableDateViewFilterValueDirection,
|
VariableDateViewFilterValueDirection,
|
||||||
VariableDateViewFilterValueUnit,
|
VariableDateViewFilterValueUnit,
|
||||||
} from '@/views/view-filter-value/utils/resolveDateViewFilterValue';
|
} from '@/views/view-filter-value/utils/resolveDateViewFilterValue';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { useContext } from 'react';
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { IconCalendarX } from 'twenty-ui/display';
|
import { IconCalendarX } from 'twenty-ui/display';
|
||||||
import {
|
import {
|
||||||
@ -276,6 +278,20 @@ const StyledButton = styled(MenuItemLeftContent)`
|
|||||||
justify-content: start;
|
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 = {
|
type DateTimePickerProps = {
|
||||||
isRelative?: boolean;
|
isRelative?: boolean;
|
||||||
hideHeaderInput?: boolean;
|
hideHeaderInput?: boolean;
|
||||||
@ -306,6 +322,8 @@ type DateTimePickerProps = {
|
|||||||
onClear?: () => void;
|
onClear?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ReactDatePicker = lazy(() => import('react-datepicker'));
|
||||||
|
|
||||||
export const DateTimePicker = ({
|
export const DateTimePicker = ({
|
||||||
date,
|
date,
|
||||||
onChange,
|
onChange,
|
||||||
@ -322,6 +340,7 @@ export const DateTimePicker = ({
|
|||||||
const internalDate = date ?? new Date();
|
const internalDate = date ?? new Date();
|
||||||
|
|
||||||
const { timeZone } = useContext(UserContext);
|
const { timeZone } = useContext(UserContext);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
const { closeDropdown: closeDropdownMonthSelect } = useDropdown(
|
||||||
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
|
||||||
@ -452,51 +471,80 @@ export const DateTimePicker = ({
|
|||||||
return (
|
return (
|
||||||
<StyledContainer calendarDisabled={isRelative}>
|
<StyledContainer calendarDisabled={isRelative}>
|
||||||
<div className={clearable ? 'clearable ' : ''}>
|
<div className={clearable ? 'clearable ' : ''}>
|
||||||
<ReactDatePicker
|
<Suspense
|
||||||
open={true}
|
fallback={
|
||||||
selected={hasDate ? dateToUse : undefined}
|
<StyledDatePickerFallback>
|
||||||
selectedDates={selectedDates}
|
<SkeletonTheme
|
||||||
openToDate={hasDate ? dateToUse : new Date()}
|
baseColor={theme.background.tertiary}
|
||||||
disabledKeyboardNavigation
|
highlightColor={theme.background.transparent.lighter}
|
||||||
onChange={handleDateChange as any}
|
borderRadius={4}
|
||||||
customInput={
|
>
|
||||||
<DateTimeInput
|
<Skeleton
|
||||||
date={internalDate}
|
width={200}
|
||||||
isDateTimeInput={isDateTimeInput}
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
|
||||||
onChange={onChange}
|
/>
|
||||||
userTimezone={timeZone}
|
<Skeleton
|
||||||
/>
|
width={240}
|
||||||
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.l}
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
width={220}
|
||||||
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.m}
|
||||||
|
/>
|
||||||
|
<Skeleton
|
||||||
|
width={180}
|
||||||
|
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||||
|
/>
|
||||||
|
</SkeletonTheme>
|
||||||
|
</StyledDatePickerFallback>
|
||||||
}
|
}
|
||||||
renderCustomHeader={({
|
>
|
||||||
prevMonthButtonDisabled,
|
<ReactDatePicker
|
||||||
nextMonthButtonDisabled,
|
open={true}
|
||||||
}) =>
|
selected={hasDate ? dateToUse : undefined}
|
||||||
isRelative ? (
|
selectedDates={selectedDates}
|
||||||
<RelativeDatePickerHeader
|
openToDate={hasDate ? dateToUse : new Date()}
|
||||||
direction={relativeDate?.direction ?? 'PAST'}
|
disabledKeyboardNavigation
|
||||||
amount={relativeDate?.amount}
|
onChange={handleDateChange as any}
|
||||||
unit={relativeDate?.unit ?? 'DAY'}
|
customInput={
|
||||||
onChange={onRelativeDateChange}
|
<DateTimeInput
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AbsoluteDatePickerHeader
|
|
||||||
date={internalDate}
|
date={internalDate}
|
||||||
onChange={onChange}
|
|
||||||
onChangeMonth={handleChangeMonth}
|
|
||||||
onChangeYear={handleChangeYear}
|
|
||||||
onAddMonth={handleAddMonth}
|
|
||||||
onSubtractMonth={handleSubtractMonth}
|
|
||||||
prevMonthButtonDisabled={prevMonthButtonDisabled}
|
|
||||||
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
|
||||||
isDateTimeInput={isDateTimeInput}
|
isDateTimeInput={isDateTimeInput}
|
||||||
timeZone={timeZone}
|
onChange={onChange}
|
||||||
hideInput={hideHeaderInput}
|
userTimezone={timeZone}
|
||||||
/>
|
/>
|
||||||
)
|
}
|
||||||
}
|
renderCustomHeader={({
|
||||||
onSelect={handleDateSelect}
|
prevMonthButtonDisabled,
|
||||||
selectsMultiple={isRelative}
|
nextMonthButtonDisabled,
|
||||||
/>
|
}) =>
|
||||||
|
isRelative ? (
|
||||||
|
<RelativeDatePickerHeader
|
||||||
|
direction={relativeDate?.direction ?? 'PAST'}
|
||||||
|
amount={relativeDate?.amount}
|
||||||
|
unit={relativeDate?.unit ?? 'DAY'}
|
||||||
|
onChange={onRelativeDateChange}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AbsoluteDatePickerHeader
|
||||||
|
date={internalDate}
|
||||||
|
onChange={onChange}
|
||||||
|
onChangeMonth={handleChangeMonth}
|
||||||
|
onChangeYear={handleChangeYear}
|
||||||
|
onAddMonth={handleAddMonth}
|
||||||
|
onSubtractMonth={handleSubtractMonth}
|
||||||
|
prevMonthButtonDisabled={prevMonthButtonDisabled}
|
||||||
|
nextMonthButtonDisabled={nextMonthButtonDisabled}
|
||||||
|
isDateTimeInput={isDateTimeInput}
|
||||||
|
timeZone={timeZone}
|
||||||
|
hideInput={hideHeaderInput}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onSelect={handleDateSelect}
|
||||||
|
selectsMultiple={isRelative}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
{clearable && (
|
{clearable && (
|
||||||
<StyledButtonContainer onClick={handleClear}>
|
<StyledButtonContainer onClick={handleClear}>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { FlagComponent } from 'country-flag-icons/react/3x2';
|
import { FlagComponent } from 'country-flag-icons/react/3x2';
|
||||||
import { CountryCallingCode, CountryCode } from 'libphonenumber-js';
|
import type { CountryCallingCode, CountryCode } from 'libphonenumber-js';
|
||||||
|
|
||||||
export type Country = {
|
export type Country = {
|
||||||
countryCode: CountryCode;
|
countryCode: CountryCode;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SuggestionMenuProps } from '@blocknote/react';
|
import type { SuggestionMenuProps } from '@blocknote/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { PartialBlock } from '@blocknote/core';
|
import type { PartialBlock } from '@blocknote/core';
|
||||||
import { getFirstNonEmptyLineOfRichText } from '../getFirstNonEmptyLineOfRichText';
|
import { getFirstNonEmptyLineOfRichText } from '../getFirstNonEmptyLineOfRichText';
|
||||||
|
|
||||||
describe('getFirstNonEmptyLineOfRichText', () => {
|
describe('getFirstNonEmptyLineOfRichText', () => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { PartialBlock } from '@blocknote/core';
|
import type { PartialBlock } from '@blocknote/core';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
export const getFirstNonEmptyLineOfRichText = (
|
export const getFirstNonEmptyLineOfRichText = (
|
||||||
|
|||||||
@ -111,7 +111,6 @@ export const Default: Story = {
|
|||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
expect(await canvas.findByText('Send Email')).toBeVisible();
|
expect(await canvas.findByText('Send Email')).toBeVisible();
|
||||||
|
|
||||||
expect(await canvas.findByText('Account')).toBeVisible();
|
expect(await canvas.findByText('Account')).toBeVisible();
|
||||||
expect(await canvas.findByText('Subject')).toBeVisible();
|
expect(await canvas.findByText('Subject')).toBeVisible();
|
||||||
expect(await canvas.findByText('Body')).toBeVisible();
|
expect(await canvas.findByText('Body')).toBeVisible();
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
|
|||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { ObjectMetadataItemsLoadEffect } from '@/object-metadata/components/ObjectMetadataItemsLoadEffect';
|
|
||||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||||
|
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { mockedUserData } from '~/testing/mock-data/users';
|
import { mockedUserData } from '~/testing/mock-data/users';
|
||||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||||
@ -19,15 +19,22 @@ export const ObjectMetadataItemsDecorator: Decorator = (Story) => {
|
|||||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||||
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
|
||||||
|
|
||||||
|
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||||
setCurrentUser(mockedUserData);
|
setCurrentUser(mockedUserData);
|
||||||
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
|
||||||
}, [setCurrentUser, setCurrentWorkspaceMember, setCurrentUserWorkspace]);
|
loadMockedObjectMetadataItems();
|
||||||
|
}, [
|
||||||
|
setCurrentUser,
|
||||||
|
setCurrentWorkspaceMember,
|
||||||
|
setCurrentUserWorkspace,
|
||||||
|
loadMockedObjectMetadataItems,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ObjectMetadataItemsLoadEffect />
|
|
||||||
<PreComputedChipGeneratorsProvider>
|
<PreComputedChipGeneratorsProvider>
|
||||||
{!!objectMetadataItems.length && <Story />}
|
{!!objectMetadataItems.length && <Story />}
|
||||||
</PreComputedChipGeneratorsProvider>
|
</PreComputedChipGeneratorsProvider>
|
||||||
|
|||||||
@ -37,6 +37,14 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
? path.resolve(__dirname, './tsconfig.build.json')
|
? path.resolve(__dirname, './tsconfig.build.json')
|
||||||
: path.resolve(__dirname, './tsconfig.dev.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 = {
|
const checkers: Checkers = {
|
||||||
overlay: false,
|
overlay: false,
|
||||||
};
|
};
|
||||||
@ -167,20 +175,82 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
outDir: 'build',
|
outDir: 'build',
|
||||||
sourcemap: VITE_BUILD_SOURCEMAP === 'true',
|
sourcemap: VITE_BUILD_SOURCEMAP === 'true',
|
||||||
rollupOptions: {
|
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: {
|
output: {
|
||||||
manualChunks: (id) => {
|
// Set chunk size warning limit (in bytes) - warns at 1MB
|
||||||
if (id.includes('@scalar')) {
|
chunkSizeWarningLimit: CHUNK_SIZE_WARNING_LIMIT,
|
||||||
return 'scalar';
|
// Custom plugin to fail build if chunks exceed max size
|
||||||
}
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'chunk-size-limit',
|
||||||
|
generateBundle(_options, bundle) {
|
||||||
|
const oversizedChunks: string[] = [];
|
||||||
|
|
||||||
return null;
|
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;
|
||||||
modulePreload: {
|
const sizeLimit = isMainChunk ? MAIN_CHUNK_SIZE_LIMIT : OTHER_CHUNK_SIZE_LIMIT;
|
||||||
resolveDependencies: (filename, deps, { hostId }) => {
|
const limitType = isMainChunk ? 'main' : 'other';
|
||||||
return deps.filter(dep => !dep.includes('scalar'));
|
|
||||||
},
|
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;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
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) => `<link rel="prefetch" href="${ctx.server?.config.base}${bundle}">`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Use regular expression to get the content within <head> </head>
|
||||||
|
const headContent = html.match(/<head>([\s\S]*)<\/head>/)?.[1] ?? '';
|
||||||
|
// Insert the content of prefetch into the head
|
||||||
|
const newHeadContent = `${headContent}${prefechBundlesString}`;
|
||||||
|
// Replace the original head
|
||||||
|
html = html.replace(
|
||||||
|
/<head>([\s\S]*)<\/head>/,
|
||||||
|
`<head>${newHeadContent}</head>`
|
||||||
|
);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
}*/
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
export enum DatabaseEventAction {
|
export enum DatabaseEventAction {
|
||||||
CREATED = 'CREATED',
|
CREATED = 'created',
|
||||||
UPDATED = 'UPDATED',
|
UPDATED = 'updated',
|
||||||
DELETED = 'DELETED',
|
DELETED = 'deleted',
|
||||||
DESTROYED = 'DESTROYED',
|
DESTROYED = 'destroyed',
|
||||||
RESTORED = 'RESTORED',
|
RESTORED = 'restored',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user