chunk csv file before preview (#11886)

closes https://github.com/twentyhq/twenty/issues/10971
This commit is contained in:
Etienne
2025-05-06 17:43:32 +02:00
committed by GitHub
parent f2691e53a0
commit a2388d2dc7
7 changed files with 95 additions and 24 deletions

View File

@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { IconLock } from 'twenty-ui/display';
import { Card, CardContent } from 'twenty-ui/layout';
@ -29,7 +30,7 @@ export const CalendarEventNotSharedContent = () => {
<StyledVisibilityCard>
<StyledVisibilityCardContent>
<IconLock size={theme.icon.size.sm} />
Not shared
<Trans>Not shared</Trans>
</StyledVisibilityCardContent>
</StyledVisibilityCard>
);

View File

@ -1,7 +1,11 @@
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
import '@cyntler/react-doc-viewer/dist/index.css';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
const StyledDocumentViewerContainer = styled.div`
@ -12,13 +16,6 @@ const StyledDocumentViewerContainer = styled.div`
width: 100%;
background: ${({ theme }) => theme.background.secondary};
.react-doc-viewer {
height: 100%;
width: 100%;
overflow: auto;
background: none;
}
#react-doc-viewer #header-bar {
display: none;
}
@ -26,6 +23,17 @@ const StyledDocumentViewerContainer = styled.div`
#react-doc-viewer #pdf-controls {
display: none !important;
}
#react-doc-viewer,
#proxy-renderer,
#msdoc-renderer {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: auto;
background: none;
}
`;
type DocumentViewerProps = {
@ -87,6 +95,7 @@ export const DocumentViewer = ({
documentUrl,
}: DocumentViewerProps) => {
const theme = useTheme();
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
const { extension } = getFileNameAndExtension(documentName);
const fileExtension = extension?.toLowerCase().replace('.', '') ?? '';
@ -94,12 +103,32 @@ export const DocumentViewer = ({
? MIME_TYPE_MAPPING[fileExtension]
: undefined;
useEffect(() => {
if (fileExtension === 'csv') {
fetchCsvPreview(documentUrl).then((content) => {
setCsvPreview(content);
});
}
}, [documentUrl, fileExtension]);
if (fileExtension === 'csv' && !isDefined(csvPreview))
return (
<StyledDocumentViewerContainer>
<Trans>Loading csv ... </Trans>
</StyledDocumentViewerContainer>
);
return (
<StyledDocumentViewerContainer>
<DocViewer
documents={[
{
uri: documentUrl,
uri:
fileExtension === 'csv' && isDefined(csvPreview)
? window.URL.createObjectURL(
new Blob([csvPreview], { type: 'text/csv' }),
)
: documentUrl,
fileName: documentName,
fileType: mimeType,
},

View File

@ -0,0 +1,22 @@
import Papa from 'papaparse';
const DEFAULT_PREVIEW_ROWS = 50;
export const fetchCsvPreview = async (url: string): Promise<string> => {
const response = await fetch(url);
const text = await response.text();
const result = Papa.parse(text, {
preview: DEFAULT_PREVIEW_ROWS,
skipEmptyLines: true,
header: true,
});
const data = result.data as Record<string, string>[];
const csvContent = Papa.unparse(data, {
header: true,
});
return csvContent;
};

View File

@ -6,6 +6,7 @@ import { EventCardMessageForbidden } from '@/activities/timeline-activities/rows
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { Trans } from '@lingui/react/macro';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@ -94,14 +95,26 @@ export const EventCardMessage = ({
);
if (shouldHandleNotFound) {
return <div>Message not found</div>;
return (
<div>
<Trans>Message not found</Trans>
</div>
);
}
return <div>Error loading message</div>;
return (
<div>
<Trans>Error loading message</Trans>
</div>
);
}
if (loading || !isDefined(message)) {
return <div>Loading...</div>;
return (
<div>
<Trans>Loading...</Trans>
</div>
);
}
const messageParticipantHandles = message.messageParticipants
@ -117,7 +130,7 @@ export const EventCardMessage = ({
{message.subject !==
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
? message.subject
: 'Subject not shared'}
: `Subject not shared`}
</StyledEmailTitle>
<StyledEmailParticipants>
<OverflowingTextWithTooltip text={messageParticipantHandles} />

View File

@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { IconLock } from 'twenty-ui/display';
const StyledEmailBodyNotSharedContainer = styled.div`
@ -44,7 +45,9 @@ export const EventCardMessageBodyNotShared = ({
<StyledEmailBodyNotSharedIconContainer>
<IconLock />
</StyledEmailBodyNotSharedIconContainer>
<span>Not shared by {notSharedByFullName}</span>
<span>
<Trans>Not shared by {notSharedByFullName}</Trans>
</span>
</StyledEmailBodyNotShared>
</StyledEmailBodyNotSharedContainer>
);

View File

@ -1,6 +1,6 @@
import { EventCardMessageBodyNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageBodyNotShared';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
const StyledEventCardMessageContainer = styled.div`
display: flex;
flex-direction: column;
@ -32,7 +32,7 @@ export const EventCardMessageForbidden = ({
<StyledEventCardMessageContainer>
<StyledEmailContent>
<StyledEmailTitle>
<span>Subject not shared</span>
<Trans>Subject not shared</Trans>
</StyledEmailTitle>
<EventCardMessageBodyNotShared
notSharedByFullName={notSharedByFullName}

View File

@ -4,7 +4,9 @@ import { HttpResponse, graphql } from 'msw';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -12,6 +14,7 @@ const meta: Meta<typeof EventCardMessage> = {
title: 'Modules/TimelineActivities/Rows/Message/EventCardMessage',
component: EventCardMessage,
decorators: [
I18nFrontDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,
@ -66,21 +69,21 @@ export const NotShared: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Subject not shared');
await canvas.findByText(`Subject not shared`);
},
parameters: {
msw: {
handlers: [
graphql.query('FindOneMessage', () => {
return HttpResponse.json({
errors: [
{
message: 'Forbidden',
extensions: {
code: 'FORBIDDEN',
},
data: {
message: {
id: '1',
subject: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
text: FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED,
messageParticipants: [],
},
],
},
});
}),
],