ef499b6d47
## Summary - Re-enable one lint rule that was temporarily disabled during the ESLint-to-Oxlint migration: - **`twenty/sort-css-properties-alphabetically`** in twenty-front — 578 violations auto-fixed across 390 files - Document why **`typescript/consistent-type-imports`** cannot be auto-fixed in twenty-server: NestJS relies on `emitDecoratorMetadata` for DI, so converting constructor parameter imports to `import type` erases them at compile time and breaks dependency injection at runtime - Right-size CI runners, reducing 8-core usage from 18 jobs to 3: | Change | Jobs | Rationale | |--------|------|-----------| | **Keep 8-core** | `ci-merge-queue/e2e-test`, `ci-front/front-sb-build`, `ci-front/front-build` | Heavy builds needing max CPU + memory (10GB NODE_OPTIONS, full Storybook webpack bundling) | | **8-core → 4-core** | `ci-server` (build, lint-typecheck, validation, test, integration-test), `ci-front/front-sb-test`, `ci-zapier/server-setup`, `ci-sdk/sdk-e2e-test` | Already sharded into 10-12 parallel instances, I/O-bound (DB/Redis), or moderate single builds | | **8-core → 2-core** | `ci-emails/emails-test` | Trivially lightweight (build + curl health check) | | **Removed** | `ci-front/front-chromatic-deployment` | Dead code — permanently disabled with `if: false` | - Fix merge queue CI issues: - **Concurrency**: Use `merge_group.base_ref` instead of unique merge group ref so new queue entries cancel previous runs - **Required status checks**: Add `merge_group` trigger to all 6 required CI workflows (front, server, shared, website, docker-compose, sdk) with `changed-files-check` auto-skipped for merge_group events — status check jobs auto-pass without re-running full CI - **Build caching**: Add Nx build cache restore/save to E2E test job with fallback to `main` branch cache for faster frontend and server builds ## Test plan - [ ] CI passes on this PR (verifies lint rule auto-fix works) - [ ] Verify 4-core runner jobs complete within their 30-minute timeouts - [ ] Verify merge queue status checks auto-pass (ci-front-status-check, ci-server-status-check, etc.) - [ ] Verify merge queue E2E concurrency cancels previous runs when a new PR enters the queue
150 lines
4.5 KiB
TypeScript
150 lines
4.5 KiB
TypeScript
import { WorkflowAttachmentChip } from '@/advanced-text-editor/components/WorkflowAttachmentChip';
|
|
import { useUploadWorkflowFile } from '@/advanced-text-editor/hooks/useUploadWorkflowFile';
|
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
|
|
|
import { styled } from '@linaria/react';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { type ChangeEvent, useContext, useRef } from 'react';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { type WorkflowAttachment } from 'twenty-shared/workflow';
|
|
import { IconUpload } from 'twenty-ui/display';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
type WorkflowSendEmailAttachmentsProps = {
|
|
files: WorkflowAttachment[];
|
|
onChange: (files: WorkflowAttachment[]) => 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: ${themeCssVariables.background.transparent.lighter};
|
|
border: 1px solid ${themeCssVariables.border.color.medium};
|
|
border-radius: ${themeCssVariables.border.radius.sm};
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
min-height: ${({ hasFiles }) => (hasFiles ? 'auto' : '24px')};
|
|
padding-bottom: ${themeCssVariables.spacing[1]};
|
|
padding-left: ${themeCssVariables.spacing[2]};
|
|
padding-right: ${themeCssVariables.spacing[2]};
|
|
padding-top: ${themeCssVariables.spacing[1]};
|
|
|
|
&:hover {
|
|
background-color: ${themeCssVariables.background.transparent.light};
|
|
border-color: ${themeCssVariables.border.color.strong};
|
|
}
|
|
`;
|
|
|
|
const StyledChipsContainer = styled.div`
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
`;
|
|
|
|
const StyledUploadAreaLabel = styled.div`
|
|
color: ${themeCssVariables.font.color.secondary};
|
|
color: ${themeCssVariables.font.color.tertiary};
|
|
display: flex;
|
|
font-size: ${themeCssVariables.font.size.sm};
|
|
font-weight: ${themeCssVariables.font.weight.medium};
|
|
gap: ${themeCssVariables.spacing[1]};
|
|
justify-content: center;
|
|
`;
|
|
|
|
export const WorkflowSendEmailAttachments = ({
|
|
files,
|
|
label,
|
|
onChange,
|
|
}: WorkflowSendEmailAttachmentsProps) => {
|
|
const { theme } = useContext(ThemeContext);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
const { uploadWorkflowFile } = useUploadWorkflowFile();
|
|
const { t } = useLingui();
|
|
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(isDefined);
|
|
|
|
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: WorkflowAttachment) => (
|
|
<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>
|
|
);
|
|
};
|