chunk csv file before preview (#11886)
closes https://github.com/twentyhq/twenty/issues/10971
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
@ -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} />
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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: [],
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user