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

View File

@ -1,7 +1,11 @@
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer'; import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
import '@cyntler/react-doc-viewer/dist/index.css'; import '@cyntler/react-doc-viewer/dist/index.css';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; 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'; import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
const StyledDocumentViewerContainer = styled.div` const StyledDocumentViewerContainer = styled.div`
@ -12,13 +16,6 @@ const StyledDocumentViewerContainer = styled.div`
width: 100%; width: 100%;
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
.react-doc-viewer {
height: 100%;
width: 100%;
overflow: auto;
background: none;
}
#react-doc-viewer #header-bar { #react-doc-viewer #header-bar {
display: none; display: none;
} }
@ -26,6 +23,17 @@ const StyledDocumentViewerContainer = styled.div`
#react-doc-viewer #pdf-controls { #react-doc-viewer #pdf-controls {
display: none !important; 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 = { type DocumentViewerProps = {
@ -87,6 +95,7 @@ export const DocumentViewer = ({
documentUrl, documentUrl,
}: DocumentViewerProps) => { }: DocumentViewerProps) => {
const theme = useTheme(); const theme = useTheme();
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
const { extension } = getFileNameAndExtension(documentName); const { extension } = getFileNameAndExtension(documentName);
const fileExtension = extension?.toLowerCase().replace('.', '') ?? ''; const fileExtension = extension?.toLowerCase().replace('.', '') ?? '';
@ -94,12 +103,32 @@ export const DocumentViewer = ({
? MIME_TYPE_MAPPING[fileExtension] ? MIME_TYPE_MAPPING[fileExtension]
: undefined; : 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 ( return (
<StyledDocumentViewerContainer> <StyledDocumentViewerContainer>
<DocViewer <DocViewer
documents={[ documents={[
{ {
uri: documentUrl, uri:
fileExtension === 'csv' && isDefined(csvPreview)
? window.URL.createObjectURL(
new Blob([csvPreview], { type: 'text/csv' }),
)
: documentUrl,
fileName: documentName, fileName: documentName,
fileType: mimeType, 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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; 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 { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display'; import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@ -94,14 +95,26 @@ export const EventCardMessage = ({
); );
if (shouldHandleNotFound) { 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)) { if (loading || !isDefined(message)) {
return <div>Loading...</div>; return (
<div>
<Trans>Loading...</Trans>
</div>
);
} }
const messageParticipantHandles = message.messageParticipants const messageParticipantHandles = message.messageParticipants
@ -117,7 +130,7 @@ export const EventCardMessage = ({
{message.subject !== {message.subject !==
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
? message.subject ? message.subject
: 'Subject not shared'} : `Subject not shared`}
</StyledEmailTitle> </StyledEmailTitle>
<StyledEmailParticipants> <StyledEmailParticipants>
<OverflowingTextWithTooltip text={messageParticipantHandles} /> <OverflowingTextWithTooltip text={messageParticipantHandles} />

View File

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

View File

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

View File

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