Support variables file email attachment (#21613)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21613?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
+89
-29
@@ -1,13 +1,16 @@
|
||||
import { useUploadWorkflowFile } from '@/advanced-text-editor/hooks/useUploadWorkflowFile';
|
||||
import { AttachmentChip } from '@/file/components/AttachmentChip';
|
||||
import { useFileUpload } from '@/file-upload/hooks/useFileUpload';
|
||||
import { VariableChip } from '@/object-record/record-field/ui/form-types/components/VariableChip';
|
||||
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { useContext, useId } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type WorkflowAttachment } from 'twenty-shared/workflow';
|
||||
import { type WorkflowEmailFiles } from 'twenty-shared/workflow';
|
||||
import { IconUpload } from 'twenty-ui-deprecated/display';
|
||||
import {
|
||||
ThemeContext,
|
||||
@@ -15,9 +18,11 @@ import {
|
||||
} from 'twenty-ui-deprecated/theme-constants';
|
||||
|
||||
type WorkflowSendEmailAttachmentsProps = {
|
||||
files: WorkflowAttachment[];
|
||||
onChange: (files: WorkflowAttachment[]) => void;
|
||||
files: WorkflowEmailFiles;
|
||||
onChange: (files: WorkflowEmailFiles) => void;
|
||||
label?: string;
|
||||
readonly?: boolean;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -25,11 +30,24 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledUploadArea = styled.div<{ hasFiles: boolean }>`
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledUploadArea = styled.div<{ hasFiles: boolean; hasPicker: boolean }>`
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-bottom-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-bottom-right-radius: ${({ hasPicker }) =>
|
||||
hasPicker ? '0' : themeCssVariables.border.radius.sm};
|
||||
border-right: ${({ hasPicker }) =>
|
||||
hasPicker ? 'none' : `1px solid ${themeCssVariables.border.color.medium}`};
|
||||
border-top-left-radius: ${themeCssVariables.border.radius.sm};
|
||||
border-top-right-radius: ${({ hasPicker }) =>
|
||||
hasPicker ? '0' : themeCssVariables.border.radius.sm};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: ${({ hasFiles }) => (hasFiles ? 'auto' : '24px')};
|
||||
@@ -51,6 +69,10 @@ const StyledChipsContainer = styled.div`
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledVariableChipWrapper = styled.span`
|
||||
display: inline-flex;
|
||||
`;
|
||||
|
||||
const StyledUploadAreaLabel = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: flex;
|
||||
@@ -64,7 +86,10 @@ export const WorkflowSendEmailAttachments = ({
|
||||
files,
|
||||
label,
|
||||
onChange,
|
||||
readonly,
|
||||
VariablePicker,
|
||||
}: WorkflowSendEmailAttachmentsProps) => {
|
||||
const instanceId = useId();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { uploadWorkflowFile } = useUploadWorkflowFile();
|
||||
const { openFileUpload } = useFileUpload();
|
||||
@@ -83,41 +108,76 @@ export const WorkflowSendEmailAttachments = ({
|
||||
};
|
||||
|
||||
const handleAddFileClick = () => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
|
||||
openFileUpload({
|
||||
multiple: true,
|
||||
onUpload: handleUploadFiles,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveFile = (fileId: string) => {
|
||||
onChange(files.filter((file) => file.id !== fileId));
|
||||
const handleAddVariable = (variableName: string) => {
|
||||
onChange([...files, variableName]);
|
||||
};
|
||||
|
||||
const handleRemoveItem = (indexToRemove: number) => {
|
||||
onChange(files.filter((_, index) => index !== indexToRemove));
|
||||
};
|
||||
|
||||
const hasPicker = isDefined(VariablePicker) && !readonly;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||
|
||||
<StyledUploadArea
|
||||
hasFiles={files.length > 0}
|
||||
onClick={handleAddFileClick}
|
||||
>
|
||||
{files.length > 0 ? (
|
||||
<StyledChipsContainer>
|
||||
{files.map((file: WorkflowAttachment) => (
|
||||
<AttachmentChip
|
||||
key={file.id}
|
||||
file={file}
|
||||
onRemove={() => handleRemoveFile(file.id)}
|
||||
/>
|
||||
))}
|
||||
</StyledChipsContainer>
|
||||
) : (
|
||||
<StyledUploadAreaLabel>
|
||||
<IconUpload size={theme.icon.size.sm} />
|
||||
<span>{t`Upload file`}</span>
|
||||
</StyledUploadAreaLabel>
|
||||
)}
|
||||
</StyledUploadArea>
|
||||
<StyledRow>
|
||||
<StyledUploadArea
|
||||
hasFiles={files.length > 0}
|
||||
hasPicker={hasPicker}
|
||||
onClick={handleAddFileClick}
|
||||
>
|
||||
{files.length > 0 ? (
|
||||
<StyledChipsContainer>
|
||||
{files.map((file, index) =>
|
||||
isString(file) ? (
|
||||
<StyledVariableChipWrapper
|
||||
key={index}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<VariableChip
|
||||
rawVariableName={file}
|
||||
onRemove={
|
||||
readonly ? undefined : () => handleRemoveItem(index)
|
||||
}
|
||||
/>
|
||||
</StyledVariableChipWrapper>
|
||||
) : (
|
||||
<AttachmentChip
|
||||
key={index}
|
||||
file={file}
|
||||
onRemove={() => handleRemoveItem(index)}
|
||||
readonly={readonly}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledChipsContainer>
|
||||
) : (
|
||||
<StyledUploadAreaLabel>
|
||||
<IconUpload size={theme.icon.size.sm} />
|
||||
<span>{t`Upload file`}</span>
|
||||
</StyledUploadAreaLabel>
|
||||
)}
|
||||
</StyledUploadArea>
|
||||
|
||||
{hasPicker ? (
|
||||
<VariablePicker
|
||||
instanceId={instanceId}
|
||||
onVariableSelect={handleAddVariable}
|
||||
/>
|
||||
) : null}
|
||||
</StyledRow>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
type EmailRecipients,
|
||||
type WorkflowAttachment,
|
||||
type WorkflowEmailFiles,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
export type EmailFormData = {
|
||||
@@ -8,6 +8,6 @@ export type EmailFormData = {
|
||||
recipients: Required<EmailRecipients>;
|
||||
subject: string;
|
||||
body: string;
|
||||
files: WorkflowAttachment[];
|
||||
files: WorkflowEmailFiles;
|
||||
inReplyTo: string;
|
||||
};
|
||||
|
||||
+2
@@ -385,9 +385,11 @@ export const WorkflowEditActionEmailBase = ({
|
||||
<WorkflowSendEmailAttachments
|
||||
label={t`Attachments`}
|
||||
files={formData.files}
|
||||
readonly={actionOptions.readonly}
|
||||
onChange={(files) => {
|
||||
handleFieldChange('files', files);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
|
||||
@@ -81,16 +81,15 @@ export class FileService {
|
||||
async getFileStreamById({
|
||||
fileId,
|
||||
workspaceId,
|
||||
fileFolder,
|
||||
allowedFileFolders = [FileFolder.Workflow],
|
||||
}: {
|
||||
fileId: string;
|
||||
workspaceId: string;
|
||||
fileFolder: FileFolder;
|
||||
allowedFileFolders?: FileFolder[];
|
||||
}): Promise<{ stream: Readable; mimeType: string } | null> {
|
||||
const file = await this.fileRepository.findOne(workspaceId, {
|
||||
where: {
|
||||
id: fileId,
|
||||
path: Like(`${fileFolder}/%`),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -98,6 +97,12 @@ export class FileService {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [fileFolder] = file.path.split('/');
|
||||
|
||||
if (!allowedFileFolders.includes(fileFolder as FileFolder)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const application = await this.applicationRepository.findOne({
|
||||
where: {
|
||||
id: file.applicationId,
|
||||
@@ -116,7 +121,7 @@ export class FileService {
|
||||
try {
|
||||
const stream = await this.fileStorageService.readFile({
|
||||
resourcePath: removeFileFolderFromFileEntityPath(file.path),
|
||||
fileFolder,
|
||||
fileFolder: fileFolder as FileFolder,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export const EMAIL_ATTACHMENT_FILE_FOLDERS = [
|
||||
FileFolder.Workflow,
|
||||
FileFolder.FilesField,
|
||||
FileFolder.EmailAttachment,
|
||||
];
|
||||
-3
@@ -1,7 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { EmailToolInputZodSchema } from 'src/engine/core-modules/tool/tools/email-tool/email-tool.schema';
|
||||
import { EmailToolException } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception';
|
||||
@@ -34,7 +32,6 @@ export class DraftEmailTool implements Tool {
|
||||
const result = await this.emailComposerService.composeEmail(
|
||||
parameters,
|
||||
context,
|
||||
{ attachmentsFileFolder: FileFolder.Workflow },
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
+3
-9
@@ -8,7 +8,6 @@ import { MAX_EMAIL_RECIPIENTS } from 'twenty-shared/constants';
|
||||
import {
|
||||
ConnectedAccountProvider,
|
||||
type EmailAttachment,
|
||||
FileFolder,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||
import { In, IsNull, LessThanOrEqual, type Repository } from 'typeorm';
|
||||
@@ -16,6 +15,7 @@ import { z } from 'zod';
|
||||
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
import { EMAIL_ATTACHMENT_FILE_FOLDERS } from 'src/engine/core-modules/tool/tools/email-tool/constants/email-attachment-file-folders.const';
|
||||
import {
|
||||
EmailToolException,
|
||||
EmailToolExceptionCode,
|
||||
@@ -185,7 +185,6 @@ export class EmailComposerService {
|
||||
private async getAttachments(
|
||||
files: Array<EmailAttachment>,
|
||||
workspaceId: string,
|
||||
fileFolder: FileFolder,
|
||||
): Promise<MessageAttachment[]> {
|
||||
if (files.length === 0) {
|
||||
return [];
|
||||
@@ -224,7 +223,7 @@ export class EmailComposerService {
|
||||
const fileStream = await this.fileService.getFileStreamById({
|
||||
fileId: fileMetadata.id,
|
||||
workspaceId,
|
||||
fileFolder,
|
||||
allowedFileFolders: EMAIL_ATTACHMENT_FILE_FOLDERS,
|
||||
});
|
||||
|
||||
if (fileStream === null) {
|
||||
@@ -312,7 +311,6 @@ export class EmailComposerService {
|
||||
async composeEmail(
|
||||
parameters: ComposeEmailParams,
|
||||
context: ToolExecutionContext,
|
||||
options: { attachmentsFileFolder: FileFolder },
|
||||
): Promise<EmailComposerResult> {
|
||||
const { workspaceId } = context;
|
||||
const { subject, body, files, inReplyTo } = parameters;
|
||||
@@ -390,11 +388,7 @@ export class EmailComposerService {
|
||||
);
|
||||
}
|
||||
|
||||
const attachments = await this.getAttachments(
|
||||
files || [],
|
||||
workspaceId,
|
||||
options.attachmentsFileFolder,
|
||||
);
|
||||
const attachments = await this.getAttachments(files || [], workspaceId);
|
||||
|
||||
const { JSDOM } = await import('jsdom');
|
||||
const window = new JSDOM('').window;
|
||||
|
||||
-3
@@ -1,7 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { EmailToolInputZodSchema } from 'src/engine/core-modules/tool/tools/email-tool/email-tool.schema';
|
||||
import { EmailToolException } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception';
|
||||
@@ -33,7 +31,6 @@ export class SendEmailTool implements Tool {
|
||||
const result = await this.emailComposerService.composeEmail(
|
||||
parameters,
|
||||
context,
|
||||
{ attachmentsFileFolder: FileFolder.Workflow },
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
-3
@@ -7,8 +7,6 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { Args, Mutation } from '@nestjs/graphql';
|
||||
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
@@ -70,7 +68,6 @@ export class SendEmailResolver {
|
||||
inReplyTo: input.inReplyTo,
|
||||
},
|
||||
{ workspaceId: workspace.id },
|
||||
{ attachmentsFileFolder: FileFolder.EmailAttachment },
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
+6
-5
@@ -9,6 +9,7 @@ import {
|
||||
type EmailStepLogMode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/build-email-step-log.util';
|
||||
import { resolveEmailBody } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/resolve-email-body.util';
|
||||
import { resolveEmailFiles } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/resolve-email-files.util';
|
||||
import { ToolBackedWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-backed/tool-backed.workflow-action';
|
||||
|
||||
export abstract class EmailWorkflowActionBase extends ToolBackedWorkflowAction<WorkflowSendEmailActionInput> {
|
||||
@@ -18,13 +19,13 @@ export abstract class EmailWorkflowActionBase extends ToolBackedWorkflowAction<W
|
||||
rawInput: WorkflowSendEmailActionInput,
|
||||
context: Record<string, unknown>,
|
||||
): Promise<WorkflowSendEmailActionInput> {
|
||||
if (!isDefined(rawInput.body)) {
|
||||
return rawInput;
|
||||
}
|
||||
const files = resolveEmailFiles(rawInput.files, context);
|
||||
|
||||
const renderedBody = await resolveEmailBody(rawInput.body, context);
|
||||
const body = isDefined(rawInput.body)
|
||||
? await resolveEmailBody(rawInput.body, context)
|
||||
: rawInput.body;
|
||||
|
||||
return { ...rawInput, body: renderedBody };
|
||||
return { ...rawInput, body, files };
|
||||
}
|
||||
|
||||
protected buildStepLog({
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { type EmailAttachment } from 'twenty-shared/types';
|
||||
import { type EmailRecipients } from 'twenty-shared/workflow';
|
||||
|
||||
export type WorkflowSendEmailActionInput = {
|
||||
@@ -5,5 +6,6 @@ export type WorkflowSendEmailActionInput = {
|
||||
recipients: EmailRecipients;
|
||||
subject?: string;
|
||||
body?: string;
|
||||
files?: EmailAttachment[];
|
||||
inReplyTo?: string;
|
||||
};
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { resolveEmailFiles } from 'src/modules/workflow/workflow-executor/workflow-actions/mail-sender/utils/resolve-email-files.util';
|
||||
|
||||
describe('resolveEmailFiles', () => {
|
||||
it('conforms static uploads to id/name, dropping extra metadata', () => {
|
||||
const upload = {
|
||||
id: 'file-1',
|
||||
name: 'logo.png',
|
||||
size: 1024,
|
||||
type: 'image/png',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(resolveEmailFiles([upload], {})).toEqual([
|
||||
{ id: 'file-1', name: 'logo.png' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves a variable bound to a record Files field and flattens it', () => {
|
||||
const context = {
|
||||
step: {
|
||||
first: {
|
||||
files: [
|
||||
{ fileId: 'file-1', label: 'Contract', extension: '.pdf' },
|
||||
{ fileId: 'file-2', label: 'Proposal', extension: '.pdf' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(resolveEmailFiles(['{{step.first.files}}'], context)).toEqual([
|
||||
{ id: 'file-1', name: 'Contract.pdf' },
|
||||
{ id: 'file-2', name: 'Proposal.pdf' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('mixes a static upload with a resolved record Files variable', () => {
|
||||
const context = {
|
||||
step: { files: [{ fileId: 'file-2', label: 'Deck', extension: '.pdf' }] },
|
||||
};
|
||||
|
||||
expect(
|
||||
resolveEmailFiles(
|
||||
[{ id: 'file-1', name: 'brochure.pdf' }, '{{step.files}}'],
|
||||
context,
|
||||
),
|
||||
).toEqual([
|
||||
{ id: 'file-1', name: 'brochure.pdf' },
|
||||
{ id: 'file-2', name: 'Deck.pdf' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to the file id when no usable name is present', () => {
|
||||
expect(resolveEmailFiles([{ id: 'file-1' }], {})).toEqual([
|
||||
{ id: 'file-1', name: 'file-1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns an empty array when there is nothing to attach', () => {
|
||||
expect(resolveEmailFiles(undefined, {})).toEqual([]);
|
||||
expect(resolveEmailFiles([], {})).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips entries that resolve to nothing or lack a file id', () => {
|
||||
const context = { step: {} };
|
||||
|
||||
expect(
|
||||
resolveEmailFiles(['{{step.missing}}', { name: 'no-id.pdf' }], context),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type EmailAttachment } from 'twenty-shared/types';
|
||||
import { isDefined, resolveInput } from 'twenty-shared/utils';
|
||||
import { type WorkflowAttachment } from 'twenty-shared/workflow';
|
||||
|
||||
import { type FileOutput } from 'src/engine/api/common/common-args-processors/data-arg-processor/types/file-item.type';
|
||||
|
||||
export const resolveEmailFiles = (
|
||||
files: unknown,
|
||||
context: Record<string, unknown>,
|
||||
): EmailAttachment[] => {
|
||||
const resolvedFiles = resolveInput(files, context);
|
||||
|
||||
if (!Array.isArray(resolvedFiles)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return resolvedFiles
|
||||
.flat()
|
||||
.map((file): EmailAttachment | undefined => {
|
||||
if (!isDefined(file) || typeof file !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const attachment = file as WorkflowAttachment | FileOutput;
|
||||
const id = 'id' in attachment ? attachment.id : attachment.fileId;
|
||||
|
||||
if (!isNonEmptyString(id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const name =
|
||||
'id' in attachment
|
||||
? attachment.name
|
||||
: [attachment.label, attachment.extension]
|
||||
.filter(isNonEmptyString)
|
||||
.join('');
|
||||
|
||||
return { id, name: isNonEmptyString(name) ? name : id };
|
||||
})
|
||||
.filter(isDefined);
|
||||
};
|
||||
@@ -51,7 +51,11 @@ export { workflowLogicFunctionActionSettingsSchema } from './schemas/logic-funct
|
||||
export { workflowManualTriggerSchema } from './schemas/manual-trigger-schema';
|
||||
export { objectRecordSchema } from './schemas/object-record-schema';
|
||||
export { workflowSendEmailActionSchema } from './schemas/send-email-action-schema';
|
||||
export { workflowSendEmailActionSettingsSchema } from './schemas/send-email-action-settings-schema';
|
||||
export type { WorkflowEmailFiles } from './schemas/send-email-action-settings-schema';
|
||||
export {
|
||||
workflowEmailFilesSchema,
|
||||
workflowSendEmailActionSettingsSchema,
|
||||
} from './schemas/send-email-action-settings-schema';
|
||||
export { stepFilterGroupSchema } from './schemas/step-filter-group-schema';
|
||||
export { stepFilterSchema } from './schemas/step-filter-schema';
|
||||
export { workflowUpdateRecordActionSchema } from './schemas/update-record-action-schema';
|
||||
|
||||
@@ -2,6 +2,24 @@ import { z } from 'zod';
|
||||
import { baseWorkflowActionSettingsSchema } from './base-workflow-action-settings-schema';
|
||||
import { workflowFileSchema } from './workflow-file-action-schema';
|
||||
|
||||
export const workflowEmailFilesSchema = z
|
||||
.array(
|
||||
z.union([
|
||||
workflowFileSchema,
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^{{[^{}]+}}$/,
|
||||
'Expected a workflow variable reference like {{stepId.path}}',
|
||||
)
|
||||
.describe('A workflow variable reference resolving to files'),
|
||||
]),
|
||||
)
|
||||
.optional()
|
||||
.default([]);
|
||||
|
||||
export type WorkflowEmailFiles = z.infer<typeof workflowEmailFilesSchema>;
|
||||
|
||||
export const workflowSendEmailActionSettingsSchema =
|
||||
baseWorkflowActionSettingsSchema.extend({
|
||||
input: z.object({
|
||||
@@ -13,7 +31,7 @@ export const workflowSendEmailActionSettingsSchema =
|
||||
}),
|
||||
subject: z.string().optional(),
|
||||
body: z.string().optional(),
|
||||
files: z.array(workflowFileSchema).optional().default([]),
|
||||
files: workflowEmailFilesSchema,
|
||||
inReplyTo: z.string().trim().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user