feat: Attachement for Send Email workflow node (#15044)
## Description - This PR addresses the one issue out of https://github.com/twentyhq/core-team-issues/issues/1685 - Added backend support for workflow node to support attachement - updated send email schema, core utility - added workflowattachmentRow and workflowsendEmailAttachment file to handle file attachment in email workflow - updated Google and Microsoft to use MailComposer which unifies with SMTP provider and improves mail structure ## Visual Appearance https://github.com/user-attachments/assets/16478569-0a83-417e-a85e-70e41fe83343 --------- Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
+84
@@ -0,0 +1,84 @@
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { IconMapping } from '@/file/utils/fileIconMappings';
|
||||
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
|
||||
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { AvatarChip } from 'twenty-ui/components';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowAttachmentChipProps = {
|
||||
file: WorkflowAttachmentType;
|
||||
onRemove: () => void;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
const StyledChip = styled.div<{ deletable: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
column-gap: ${({ theme }) => theme.spacing(1)};
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
max-width: 140px;
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledDelete = styled.button`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.medium};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const WorkflowAttachmentChip = ({
|
||||
file,
|
||||
onRemove,
|
||||
readonly = false,
|
||||
}: WorkflowAttachmentChipProps) => {
|
||||
const iconColors = useFileCategoryColors();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledChip data-chip deletable={!readonly}>
|
||||
<AvatarChip
|
||||
Icon={IconMapping[getFileType(file.name)]}
|
||||
IconBackgroundColor={iconColors[getFileType(file.name)]}
|
||||
/>
|
||||
<StyledLabel title={file.name}>{file.name}</StyledLabel>
|
||||
|
||||
{!readonly && (
|
||||
<StyledDelete onClick={onRemove}>
|
||||
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
|
||||
</StyledDelete>
|
||||
)}
|
||||
</StyledChip>
|
||||
);
|
||||
};
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { WorkflowAttachmentChip } from '@/advanced-text-editor/components/WorkflowAttachmentChip';
|
||||
import { useUploadWorkflowFile } from '@/advanced-text-editor/hooks/useUploadWorkflowFile';
|
||||
|
||||
import { type WorkflowAttachmentType } from '@/workflow/workflow-steps/workflow-actions/email-action/types/WorkflowAttachmentType';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type ChangeEvent, useRef } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconUpload } from 'twenty-ui/display';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
type WorkflowSendEmailAttachmentsProps = {
|
||||
files: WorkflowAttachmentType[];
|
||||
onChange: (files: WorkflowAttachmentType[]) => void;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledFileInput = styled.input`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
const StyledUploadArea = styled.div<{ hasFiles: boolean }>`
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: ${({ hasFiles }) => (hasFiles ? 'auto' : '24px')};
|
||||
justify-content: center;
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
border-color: ${({ theme }) => theme.border.color.strong};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledChipsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledUploadAreaLabel = styled.div`
|
||||
justify-content: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const WorkflowSendEmailAttachments = ({
|
||||
files,
|
||||
label,
|
||||
onChange,
|
||||
}: WorkflowSendEmailAttachmentsProps) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { uploadWorkflowFile } = useUploadWorkflowFile();
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
const handleAddFileClick = (e: React.MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
const isInsideChip = target.closest('[data-chip]') !== null;
|
||||
const isInsideButton = target.closest('button') !== null;
|
||||
const isSvgOrPath = target.tagName === 'svg' || target.tagName === 'path';
|
||||
|
||||
if (isInsideChip || isInsideButton || isSvgOrPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileInputRef.current !== null) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
};
|
||||
|
||||
const onUploadFiles = async (filesToUpload: File[]) => {
|
||||
const uploadedFiles = await Promise.all(
|
||||
filesToUpload.map((file) => uploadWorkflowFile(file)),
|
||||
);
|
||||
|
||||
const successfulUploads = uploadedFiles.filter(
|
||||
(file): file is WorkflowAttachmentType => file !== null,
|
||||
);
|
||||
|
||||
if (successfulUploads.length > 0) {
|
||||
onChange([...files, ...successfulUploads]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFiles = event.target.files;
|
||||
if (isDefined(selectedFiles)) {
|
||||
onUploadFiles(Array.from(selectedFiles));
|
||||
}
|
||||
if (fileInputRef.current !== null) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
onChange(files.filter((file) => file.id !== fileId));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
|
||||
<StyledFileInput
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
|
||||
<StyledUploadArea
|
||||
hasFiles={files.length > 0}
|
||||
onClick={handleAddFileClick}
|
||||
>
|
||||
{files.length > 0 ? (
|
||||
<StyledChipsContainer>
|
||||
{files.map((file: WorkflowAttachmentType) => (
|
||||
<WorkflowAttachmentChip
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={() => handleRemoveFile(file.id)}
|
||||
/>
|
||||
))}
|
||||
</StyledChipsContainer>
|
||||
) : (
|
||||
<StyledUploadAreaLabel>
|
||||
<IconUpload size={theme.icon.size.sm} />
|
||||
<span>{t`Upload file`}</span>
|
||||
</StyledUploadAreaLabel>
|
||||
)}
|
||||
</StyledUploadArea>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user